From 6dff8916344a394c9166221df7e1d2345cefec9c Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Mon, 17 Aug 2026 10:22:40 -0600
Subject: [PATCH 01/11] Isolate the legacy GHCR mirror in its own release job
(#1620)
---
.github/workflows/publish-selfhost-docker.yml | 23 +++++++++++++++----
1 file changed, 19 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/publish-selfhost-docker.yml b/.github/workflows/publish-selfhost-docker.yml
index 51715ff860..1da6470320 100644
--- a/.github/workflows/publish-selfhost-docker.yml
+++ b/.github/workflows/publish-selfhost-docker.yml
@@ -281,9 +281,6 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
packages: write
- env:
- HAS_LEGACY_TOKEN: ${{ secrets.GHCR_LEGACY_TOKEN != '' }}
-
steps:
- name: Download image digests
uses: actions/download-artifact@v4
@@ -330,10 +327,28 @@ jobs:
docker buildx imagetools create "${tag_args[@]}" "${sources[@]}"
+ # Mirrors each release to the pre-org-move namespace
+ # (ghcr.io/rhyssullivan/executor-selfhost), which GHCR cannot redirect.
+ # Deliberately a separate job: the canonical publish above never depends on
+ # it, and a failure here (an expired GHCR_LEGACY_TOKEN, historically) shows
+ # as one red job pointing at exactly what to fix.
+ mirror-legacy:
+ needs:
+ - metadata
+ - merge
+ runs-on: blacksmith-4vcpu-ubuntu-2404
+ env:
+ HAS_LEGACY_TOKEN: ${{ secrets.GHCR_LEGACY_TOKEN != '' }}
+
+ steps:
- name: Skip legacy GHCR mirror (GHCR_LEGACY_TOKEN not configured)
if: env.HAS_LEGACY_TOKEN != 'true'
run: echo "GHCR_LEGACY_TOKEN is not configured; skipping legacy GHCR mirror."
+ - name: Set up Docker Buildx
+ if: env.HAS_LEGACY_TOKEN == 'true'
+ uses: docker/setup-buildx-action@v3
+
- name: Log in to legacy GHCR namespace
if: env.HAS_LEGACY_TOKEN == 'true'
uses: docker/login-action@v3
@@ -342,7 +357,7 @@ jobs:
username: rhyssullivan
password: ${{ secrets.GHCR_LEGACY_TOKEN }}
- - name: Mirror self-host image to legacy GHCR namespace
+ - name: Mirror release tags to the legacy namespace
if: env.HAS_LEGACY_TOKEN == 'true'
shell: bash
env:
From 86c68afef9bf8b7c19ab58f59acfedca0b3c4ca7 Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Mon, 17 Aug 2026 10:48:21 -0600
Subject: [PATCH 02/11] Stamp client identity on MCP execution spans (#1621)
---
.changeset/mcp-client-span-attribution.md | 8 ++
apps/cloud/src/mcp/session-durable-object.ts | 4 +
.../src/mcp/agent-session-durable-object.ts | 54 ++++++++++++
packages/hosts/mcp/src/tool-server-core.ts | 84 ++++++++++++++++++-
packages/hosts/mcp/src/tool-server.test.ts | 74 ++++++++++++++++
packages/hosts/mcp/src/tool-server.ts | 43 ++++++++++
6 files changed, 263 insertions(+), 4 deletions(-)
create mode 100644 .changeset/mcp-client-span-attribution.md
diff --git a/.changeset/mcp-client-span-attribution.md b/.changeset/mcp-client-span-attribution.md
new file mode 100644
index 0000000000..c3fc4cda24
--- /dev/null
+++ b/.changeset/mcp-client-span-attribution.md
@@ -0,0 +1,8 @@
+---
+"@executor-js/host-mcp": patch
+"@executor-js/cloudflare": patch
+---
+
+**MCP execution spans now carry the client identity (`mcp.client.*`)**
+
+The `clientInfo` a client self-reports at `initialize` (or in a modern request's `_meta`) previously existed only on the initialize request itself, which has no session id yet, so execution telemetry could not be segmented by client. Execute, execute-action, and resume spans (and their descendants) now carry `mcp.client.name` / `mcp.client.version` / `mcp.client.title` alongside the existing session join keys. Cloudflare session Durable Objects persist the reported identity in session meta, so attribution survives cold restores; it feeds telemetry only, never behavior.
diff --git a/apps/cloud/src/mcp/session-durable-object.ts b/apps/cloud/src/mcp/session-durable-object.ts
index c83a5eddb5..b2bdbe11d2 100644
--- a/apps/cloud/src/mcp/session-durable-object.ts
+++ b/apps/cloud/src/mcp/session-durable-object.ts
@@ -379,6 +379,10 @@ export class McpSessionDOSqlite extends McpAgentSessionDOBase
+ If you connect a Google Workspace service, Executor uses the permissions you grant to perform the actions you + request through that integration. Depending on the service and permissions you choose, this may include accessing + or modifying Google Calendar events, Google Sheets spreadsheets, or other Google Workspace content. Executor stores + OAuth credentials and connection metadata so the integration can continue working, and processes Google Workspace + content when carrying out your requested tool calls and returning their results to you or the agent you directed to + make the call. +
++ We do not use Google Workspace API data for advertising or to train generalized artificial-intelligence or + machine-learning models. We disclose it only as needed to provide the user-requested integration, operate and secure + the Services, comply with law, or as otherwise described in this Policy with the user's consent. +
++ Executor's use and transfer of information received from Google Workspace APIs adheres to the + Google API Services User Data Policy, + including its Limited Use requirements. +
+
If you submit a URL to our website's API detection tool, contact us by email, or otherwise communicate with us,
diff --git a/e2e/scenarios/first-party-oauth.test.ts b/e2e/scenarios/first-party-oauth.test.ts
new file mode 100644
index 0000000000..cb483882c5
--- /dev/null
+++ b/e2e/scenarios/first-party-oauth.test.ts
@@ -0,0 +1,252 @@
+// First-party OAuth clients: the cloud host declares executor-owned apps via
+// env (`FIRST_PARTY_GITHUB_CLIENT_ID/SECRET`, set by the e2e cloud boot), and
+// every org can connect through them with nothing to paste. Three guarantees:
+//
+// 1. Listing: `oauth.listClients` surfaces `first-party:github` with a
+// `first_party` origin and its public client id — no create call ever ran.
+// 2. Flow: `oauth.start` through the first-party slug redirects to the
+// provider's authorize endpoint carrying the env-configured client id and
+// this platform's `/api/oauth/callback` — proof the config-resolved
+// identity (not a stored row) drives the flow. The redirect is asserted,
+// never followed: github.com is not visited.
+// 3. Guardrails: the reserved `first-party:` namespace is rejected by
+// createClient, so no org can shadow the host's app with its own row.
+import { randomBytes } from "node:crypto";
+
+import { expect } from "@effect/vitest";
+import { Effect } from "effect";
+import { composePluginApi } from "@executor-js/api/server";
+import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
+import {
+ AuthTemplateSlug,
+ ConnectionName,
+ IntegrationSlug,
+ OAuthClientSlug,
+} from "@executor-js/sdk/shared";
+
+import { scenario } from "../src/scenario";
+import { Api, Target } from "../src/services";
+
+const api = composePluginApi([openApiHttpPlugin()] as const);
+
+const unique = (prefix: string) => `${prefix}_${randomBytes(4).toString("hex")}`;
+
+/** A minimal integration whose OAuth template points at GitHub's endpoints, so
+ * the first-party `first-party:github` app is the matching client for it. */
+const githubShapedIntegrationSpec = {
+ spec: {
+ kind: "blob" as const,
+ value: JSON.stringify({
+ openapi: "3.0.3",
+ info: { title: "GitHub-shaped API", version: "1.0.0" },
+ paths: {
+ "/user": {
+ get: {
+ operationId: "getUser",
+ tags: ["default"],
+ responses: { "200": { description: "the caller" } },
+ },
+ },
+ },
+ }),
+ },
+ baseUrl: "https://api.github.com",
+ authenticationTemplate: [
+ {
+ slug: "oauth",
+ kind: "oauth2" as const,
+ authorizationUrl: "https://github.com/login/oauth/authorize",
+ tokenUrl: "https://github.com/login/oauth/access_token",
+ scopes: ["repo", "read:org"],
+ },
+ ],
+} as const;
+
+const googleShapedIntegrationSpec = (scopes: readonly string[]) => ({
+ spec: {
+ kind: "blob" as const,
+ value: JSON.stringify({
+ openapi: "3.0.3",
+ info: { title: "Google-shaped API", version: "1.0.0" },
+ paths: {
+ "/resource": {
+ get: {
+ operationId: "getResource",
+ tags: ["default"],
+ responses: { "200": { description: "a Google resource" } },
+ },
+ },
+ },
+ }),
+ },
+ baseUrl: "https://www.googleapis.com",
+ authenticationTemplate: [
+ {
+ slug: "oauth",
+ kind: "oauth2" as const,
+ authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
+ tokenUrl: "https://oauth2.googleapis.com/token",
+ scopes,
+ },
+ ],
+});
+
+scenario(
+ "First-party OAuth · the host-declared GitHub app is listed and drives the authorize redirect",
+ {},
+ Effect.scoped(
+ Effect.gen(function* () {
+ const target = yield* Target;
+ // First-party registrations are a cloud-host capability. This scenario
+ // intentionally does not apply to self-host, whose operator supplies
+ // their own OAuth apps through its existing registration flow.
+ if (target.name !== "cloud") return;
+ const { client: makeApiClient } = yield* Api;
+ const identity = yield* target.newIdentity();
+ const client = yield* makeApiClient(api, identity);
+
+ // 1. The config-declared app appears in listings with its public id.
+ const clients = yield* client.oauth.listClients();
+ const firstParty = clients.find((c) => String(c.slug) === "first-party:github");
+ expect(firstParty, "the env-declared first-party GitHub app is listed").toBeDefined();
+ expect(firstParty?.origin.kind).toBe("first_party");
+ expect(firstParty?.clientId).toBe("e2e-first-party-github");
+
+ // 2. A start through the first-party slug builds GitHub's authorize URL
+ // from the config identity and this platform's served callback.
+ const integration = IntegrationSlug.make(unique("fpgh"));
+ yield* client.openapi.addSpec({
+ payload: { ...githubShapedIntegrationSpec, slug: integration },
+ });
+ const started = yield* client.oauth.start({
+ payload: {
+ client: OAuthClientSlug.make("first-party:github"),
+ clientOwner: "org",
+ owner: "org",
+ name: ConnectionName.make("main"),
+ integration,
+ template: AuthTemplateSlug.make("oauth"),
+ },
+ });
+ expect(started.status, "oauth.start redirects to the provider").toBe("redirect");
+ const authorizationUrl = started.status === "redirect" ? started.authorizationUrl : "";
+ const authorize = new URL(authorizationUrl);
+ expect(authorize.origin + authorize.pathname).toBe(
+ "https://github.com/login/oauth/authorize",
+ );
+ expect(authorize.searchParams.get("client_id")).toBe("e2e-first-party-github");
+ expect(authorize.searchParams.get("redirect_uri")).toBe(
+ new URL("/api/oauth/callback", target.baseUrl).toString(),
+ );
+
+ // 3. The reserved namespace cannot be shadowed by a stored row. The
+ // server rejects with a StorageError, which the HTTP edge scrubs to an
+ // opaque InternalError — assert the rejection, then prove the listed
+ // app is still the config-declared one (same public id, same origin).
+ yield* client.oauth
+ .createClient({
+ payload: {
+ owner: "org",
+ slug: OAuthClientSlug.make("first-party:github"),
+ authorizationUrl: "https://github.com/login/oauth/authorize",
+ tokenUrl: "https://github.com/login/oauth/access_token",
+ grant: "authorization_code",
+ clientId: "impostor",
+ clientSecret: "impostor-secret",
+ },
+ })
+ .pipe(Effect.flip);
+ const after = yield* client.oauth.listClients();
+ const survivors = after.filter((c) => String(c.slug) === "first-party:github");
+ expect(survivors, "exactly one first-party:github remains listed").toHaveLength(1);
+ expect(survivors[0]?.origin.kind).toBe("first_party");
+ expect(survivors[0]?.clientId, "the impostor never shadowed the host's app").toBe(
+ "e2e-first-party-github",
+ );
+ }),
+ ),
+);
+
+scenario(
+ "First-party OAuth · Google offers Calendar and Sheets but refuses Gmail scopes",
+ {},
+ Effect.scoped(
+ Effect.gen(function* () {
+ const target = yield* Target;
+ if (target.name !== "cloud") return;
+ const { client: makeApiClient } = yield* Api;
+ const identity = yield* target.newIdentity();
+ const client = yield* makeApiClient(api, identity);
+
+ const clients = yield* client.oauth.listClients();
+ const google = clients.find((candidate) => String(candidate.slug) === "first-party:google");
+ expect(google, "the env-declared first-party Google app is listed").toBeDefined();
+ expect(google?.origin.kind).toBe("first_party");
+ if (google?.origin.kind !== "first_party") return;
+ expect(google.origin.allowedScopes).toContain("https://www.googleapis.com/auth/calendar");
+ expect(google.origin.allowedScopes).toContain("https://www.googleapis.com/auth/spreadsheets");
+ expect(google.origin.allowedScopes).not.toContain("https://mail.google.com/");
+ expect(google.origin.allowedScopes).not.toContain("https://www.googleapis.com/auth/drive");
+
+ const calendar = IntegrationSlug.make(unique("google_calendar"));
+ yield* client.openapi.addSpec({
+ payload: {
+ ...googleShapedIntegrationSpec([
+ "openid",
+ "email",
+ "profile",
+ "https://www.googleapis.com/auth/calendar",
+ ]),
+ slug: calendar,
+ },
+ });
+ const started = yield* client.oauth.start({
+ payload: {
+ client: OAuthClientSlug.make("first-party:google"),
+ clientOwner: "org",
+ owner: "org",
+ name: ConnectionName.make("calendar"),
+ integration: calendar,
+ template: AuthTemplateSlug.make("oauth"),
+ },
+ });
+ expect(started.status).toBe("redirect");
+ const authorizationUrl = started.status === "redirect" ? started.authorizationUrl : "";
+ const authorize = new URL(authorizationUrl);
+ expect(authorize.origin + authorize.pathname).toBe(
+ "https://accounts.google.com/o/oauth2/v2/auth",
+ );
+ expect(authorize.searchParams.get("client_id")).toBe("e2e-first-party-google");
+ expect(authorize.searchParams.get("access_type")).toBe("offline");
+ expect(new Set(authorize.searchParams.get("scope")?.split(" ") ?? [])).toEqual(
+ new Set(["openid", "email", "profile", "https://www.googleapis.com/auth/calendar"]),
+ );
+
+ const gmail = IntegrationSlug.make(unique("google_gmail"));
+ yield* client.openapi.addSpec({
+ payload: {
+ ...googleShapedIntegrationSpec([
+ "openid",
+ "email",
+ "profile",
+ "https://mail.google.com/",
+ ]),
+ slug: gmail,
+ },
+ });
+ const blocked = yield* client.oauth
+ .start({
+ payload: {
+ client: OAuthClientSlug.make("first-party:google"),
+ clientOwner: "org",
+ owner: "org",
+ name: ConnectionName.make("gmail"),
+ integration: gmail,
+ template: AuthTemplateSlug.make("oauth"),
+ },
+ })
+ .pipe(Effect.flip);
+ expect(blocked).toBeDefined();
+ }),
+ ),
+);
diff --git a/e2e/setup/cloud.boot.ts b/e2e/setup/cloud.boot.ts
index 3c072a2f4b..3544afabe0 100644
--- a/e2e/setup/cloud.boot.ts
+++ b/e2e/setup/cloud.boot.ts
@@ -94,6 +94,21 @@ export const bootCloud = async (options: CloudBootOptions): Promise