# Files changed · PR \#16695 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/16695/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[feat\(server\): work a linked environment starts stays within its link](https://git.cafe/pingdotgg/t3code/pull/16695.md)

Version: 16

Head commit: e7be3191310fe8c8ee8493338a2e98665a999d4d

Base commit: 641ee467aa5ceb9126f7c52e2b8b002bf258e0a4

Comparison base: 641ee467aa5ceb9126f7c52e2b8b002bf258e0a4

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/server/src/auth/EnvironmentAuth.ts

Status: modified; +5; −0

```diff
@@ -95,6 +95,8 @@
   readonly sessionId: AuthSessionId;
   readonly label: string;
   readonly access: AuthMcpClientAccess;
+  /** Another T3 Code environment's link, whose work here stays within it. */
+  readonly peerLink: boolean;
 }
 
 export interface AuthenticatedSession {
@@ -556,6 +558,7 @@
     readonly issueMcpClientSession: (input: {
       readonly label: string;
       readonly access: AuthMcpClientAccess;
+      readonly peerLink: boolean;
       readonly client: AuthClientMetadata;
     }) => Effect.Effect<
       {
@@ -1274,6 +1277,7 @@
               access: session.scopes.includes(AuthOrchestrationOperateScope)
                 ? (session.runtimeModeCeiling ?? "approval-required")
                 : "read-only",
+              peerLink: session.peerLink === true,
             } satisfies McpClientSession)
           : Effect.fail(
               new ServerAuthInvalidCredentialError({
@@ -1293,6 +1297,7 @@
         scopes: mcpClientScopes(input.access),
         ttl: MCP_CLIENT_SESSION_TTL,
         ...(input.access === "read-only" ? {} : { runtimeModeCeiling: input.access }),
+        peerLink: input.peerLink,
         client: { ...input.client, label: input.label, deviceType: "bot" },
       })
       .pipe(
```

## apps/server/src/auth/McpOAuth.ts

Status: modified; +25; −4

```diff
@@ -13,6 +13,7 @@
   type AuthEnvironmentScope,
   AuthOrchestrationOperateScope,
   AuthOrchestrationReadScope,
+  MCP_PEER_LINK_SOFTWARE_ID,
 } from "@t3tools/contracts";
 import { encodeOAuthScope } from "@t3tools/shared/oauthScope";
 import * as Clock from "effect/Clock";
@@ -139,6 +140,8 @@
   v: Schema.Literal(1),
   n: Schema.String,
   r: Schema.Array(Schema.String),
+  /** Registered as another T3 Code environment's peer link. */
+  p: Schema.optionalKey(Schema.Literal(1)),
 });
 const decodeClientIdPayload = Schema.decodeUnknownOption(Schema.fromJsonString(ClientIdPayload));
 
@@ -146,6 +149,7 @@
   readonly clientId: string;
   readonly name: string;
   readonly redirectUris: ReadonlyArray<string>;
+  readonly peerLink: boolean;
 }
 
 export class McpOAuthPageError extends Schema.TaggedError<McpOAuthPageError>()(
@@ -179,6 +183,7 @@
 interface PendingCode {
   readonly clientId: string;
   readonly clientName: string;
+  readonly peerLink: boolean;
   readonly redirectUri: string;
   readonly codeChallenge: string;
   readonly resource: string;
@@ -247,8 +252,10 @@
 
   const sign = (domain: string, payload: string) => signPayload(`${domain}.${payload}`, signingKey);
 
+  const signClientId = (name: string, redirectUris: ReadonlyArray<string>, peerLink: boolean) => {
+    const body = base64UrlEncode(
+      JSON.stringify({ v: 1, n: name, r: redirectUris, ...(peerLink ? { p: 1 } : {}) }),
-  const signClientId = (name: string, redirectUris: ReadonlyArray<string>) => {
-    const body = base64UrlEncode(JSON.stringify({ v: 1, n: name, r: redirectUris }));
+    );
     return `${body}.${sign("mcp-client-id", body)}`;
   };
 
@@ -264,7 +271,12 @@
     }
     return Option.getOrUndefined(
       decodeClientIdPayload(json).pipe(
+        Option.map((payload) => ({
+          clientId,
+          name: payload.n,
+          redirectUris: payload.r,
-        Option.map((payload) => ({ clientId, name: payload.n, redirectUris: payload.r })),
+          peerLink: payload.p === 1,
+        })),
       ),
     );
   };
@@ -313,7 +325,13 @@
       // than rejected: RFC 7591 lets the server answer with what it supports. Claude Code
       // asks for refresh_token, and hosted agents may ask for a client secret; every client
       // is registered as public and proves itself with PKCE instead.
+      const peerLink = metadata.software_id === MCP_PEER_LINK_SOFTWARE_ID;
+      return {
+        clientId: signClientId(name, redirectUris, peerLink),
-      return { clientId: signClientId(name, redirectUris), name, redirectUris };
+        name,
+        redirectUris,
+        peerLink,
+      };
     },
   );
 
@@ -395,6 +413,7 @@
       next.set(code, {
         clientId: authorization.client.clientId,
         clientName: authorization.client.name,
+        peerLink: authorization.client.peerLink,
         redirectUri: authorization.redirectUri,
         codeChallenge: authorization.codeChallenge,
         resource: authorization.resource,
@@ -498,6 +517,7 @@
         .issueMcpClientSession({
           label: pending.clientName,
           access: pending.access,
+          peerLink: pending.peerLink,
           client: deriveAuthClientMetadata({ request }),
         })
         .pipe(
@@ -549,6 +569,7 @@
                   sessionId: client.sessionId,
                   label: client.label,
                   access: client.access,
+                  linked: client.peerLink,
                 },
                 capabilities: new Set<McpInvocationContext.McpCapability>([
                   "orchestration",
```

## apps/server/src/auth/SessionStore.ts

Status: modified; +7; −0

```diff
@@ -59,6 +59,8 @@
   readonly scopes: ReadonlyArray<AuthEnvironmentScope>;
   readonly proofKeyThumbprint?: string;
   readonly runtimeModeCeiling?: RuntimeMode;
+  /** An MCP client that is another T3 Code environment's peer link. */
+  readonly peerLink?: true;
 }
 
 export type SessionCredentialChange =
@@ -376,6 +378,7 @@
       readonly client?: AuthClientMetadata;
       readonly proofKeyThumbprint?: string;
       readonly runtimeModeCeiling?: RuntimeMode;
+      readonly peerLink?: boolean;
       readonly replaceActiveForSubjectAndMethod?: boolean;
       readonly replaceSessionId?: AuthSessionId;
     }) => Effect.Effect<IssuedSession, SessionCredentialInternalError>;
@@ -434,6 +437,8 @@
   method: Schema.Literals(["browser-session-cookie", "bearer-access-token", "dpop-access-token"]),
   jkt: Schema.optionalKey(Schema.String),
   rtc: Schema.optionalKey(RuntimeMode),
+  /** Signed, so a client cannot shed its peer link's limits. */
+  plk: Schema.optionalKey(Schema.Literal(1)),
   iat: Schema.Number,
   exp: Schema.Number,
 });
@@ -670,6 +675,7 @@
         method: input?.method ?? "browser-session-cookie",
         ...(input?.proofKeyThumbprint ? { jkt: input.proofKeyThumbprint } : {}),
         ...(input?.runtimeModeCeiling ? { rtc: input.runtimeModeCeiling } : {}),
+        ...(input?.peerLink === true ? { plk: 1 as const } : {}),
         iat: issuedAt.epochMilliseconds,
         exp: expiresAt.epochMilliseconds,
       };
@@ -851,6 +857,7 @@
         scopes: claims.scopes,
         ...(claims.jkt ? { proofKeyThumbprint: claims.jkt } : {}),
         ...(claims.rtc ? { runtimeModeCeiling: claims.rtc } : {}),
+        ...(claims.plk === 1 ? { peerLink: true as const } : {}),
       } satisfies VerifiedSession;
     },
   );
```

## apps/server/src/cli/thread.ts

Status: modified; +1; −0

```diff
@@ -221,6 +221,7 @@
         // The CLI runs as the machine's owner. New threads still start at
         // the project's default mode, never at this ceiling by default.
         access: "full-access",
+        peerLink: false,
         client: { deviceType: "bot" },
       }),
       (session) =>
```

## apps/server/src/environment/ServerEnvironment.ts

Status: modified; +1; −0

```diff
@@ -268,6 +268,7 @@
       serverBrowser: true,
       peerLinks: true,
       mcpModeLimitHeader: true,
+      linkFence: true,
     },
   };
 
```

## apps/server/src/mcp/McpHttpServer.test.ts

Status: modified; +1; −0

```diff
@@ -851,6 +851,7 @@
               (input) => input,
               (_, modes) =>
                 Effect.succeed({ modes: `${modes.runtimeMode}/${modes.interactionMode}` }),
+              "stamped",
             ),
           }),
         ).pipe(
```

## apps/server/src/mcp/McpInvocationContext.ts

Status: modified; +5; −0

```diff
@@ -32,6 +32,11 @@
   readonly label: string;
   readonly access: AuthMcpClientAccess;
   /**
+   * Another T3 Code environment's peer link. What it starts here carries its
+   * session as `linkOrigin`, and it changes only that work (`linkOrigin.ts`).
+   */
+  readonly linked?: boolean | undefined;
+  /**
    * Narrower modes the client asked for on this request (`T3-Mode-Limit`):
    * another environment passing on the limits of the agent it calls for.
    */
```

## apps/server/src/mcp/McpToolAccess.test.ts

Status: modified; +15; −1

```diff
@@ -144,6 +144,7 @@
   starts_threads: McpToolAccess.startsThreads(
     (input) => input,
     (_, modes) => Effect.succeed({ ran: `${modes.runtimeMode}/${modes.interactionMode}` }),
+    "stamped",
   ),
   writes_environment: McpToolAccess.writesEnvironment(() => ran),
 };
@@ -232,6 +233,15 @@
 const supervisedClient = clientCaller("approval-required");
 const fullAccessClient = clientCaller("full-access");
 const readOnlyClient = clientCaller("read-only");
+const linkedClient: McpInvocationContext.McpInvocationScope = {
+  ...supervisedClient,
+  client: { ...supervisedClient.client!, linked: true },
+};
+// A full-access link's agents can do anything this environment's user can.
+const fullAccessLinkedClient: McpInvocationContext.McpInvocationScope = {
+  ...fullAccessClient,
+  client: { ...fullAccessClient.client!, linked: true },
+};
 const previewOnly = { ...fullAccess, capabilities: new Set(["preview"] as const) };
 
 it.effect.each([
@@ -266,6 +276,10 @@
   ["writes_with_capability", previewOnly, {}, "ran"],
   ["writes_with_capability", readOnlyClient, {}, "capability_denied"],
   ["writes_with_capability", ended, {}, "parent_not_active"],
+  ["writes", linkedClient, {}, "capability_denied"],
+  ["writes_with_capability", linkedClient, {}, "capability_denied"],
+  ["writes", fullAccessLinkedClient, {}, "ran"],
+  ["writes_with_capability", fullAccessLinkedClient, {}, "ran"],
 
   ["writes_threads", supervised, {}, "ran"],
   ["writes_threads", supervised, { threadId: planThreadId }, "ran"],
@@ -375,7 +389,7 @@
     writes: McpToolAccess.writes(ranHere),
     writes_with_capability: McpToolAccess.writesWithCapability(ranHere),
     writes_threads: McpToolAccess.writesThreads((input) => [input.threadId], ranHere),
-    starts_threads: McpToolAccess.startsThreads((input) => input, ranHere),
+    starts_threads: McpToolAccess.startsThreads((input) => input, ranHere, "stamped"),
     writes_environment: McpToolAccess.writesEnvironment(ranHere),
   }),
 ).pipe(
```

## apps/server/src/mcp/McpToolAccess.ts

Status: modified; +66; −11

```diff
@@ -1,5 +1,6 @@
 import {
   type EnvironmentId,
+  type OrchestrationV2LinkOrigin,
   OrchestratorMcpEnvironmentTarget,
   OrchestratorMcpFailure,
   type ProviderInteractionMode,
@@ -20,6 +21,7 @@
   type DispatchModeRefusal,
 } from "../orchestration-v2/DispatchModeLimit.ts";
 import * as PeerForwarding from "../peer/PeerForwarding.ts";
+import { assertNotLinked, assertSameLink, callerLinkOrigin } from "./linkOrigin.ts";
 import * as McpInvocationContext from "./McpInvocationContext.ts";
 import { resolveInteractionMode, resolveRuntimeMode } from "./OrchestratorMcpService.ts";
 import {
@@ -142,19 +144,42 @@
     undefined,
   );
 
+/**
-export const writes = <P, A, E, R>(handle: (params: P) => Effect.Effect<A, E, R>) =>
+ * Changes something that belongs to no thread, such as a pending upload or a
+ * scheduled task. Work a linked environment started may not: none of it
+ * carries the link's origin, so none of it is that link's.
+ */
+export const writes = <P, A, E, R>(
+  handle: (params: P) => Effect.Effect<A, E, R>,
+  options?: {
+    /** Whether a linked caller may make it, as for uploads its own sends need. */
+    readonly linkedCallers?: "allowed";
+  },
+) =>
   declare(
+    (params: P) =>
+      orchestratingCaller.pipe(
+        Effect.tap((caller) =>
+          options?.linkedCallers === "allowed"
+            ? Effect.void
+            : assertNotLinked(caller, "change this environment's scheduled tasks"),
+        ),
+        Effect.flatMap(() => handle(params)),
-    (params: P) => orchestratingCaller.pipe(Effect.flatMap(() => handle(params))),
+      ),
     () => orchestratingCaller,
   );
 
 /**
  * Unlike `writes` it does not need thread control; the handler checks its
- * capability, as the capability toolkits do.
+ * capability, as the capability toolkits do. Linked work is refused the same way.
  */
 export const writesWithCapability = <P, A, E, R>(handle: (params: P) => Effect.Effect<A, E, R>) =>
   declare(
+    (params: P) =>
+      writingCaller.pipe(
+        Effect.tap((caller) => assertNotLinked(caller, "start work here that is not its link's")),
+        Effect.flatMap(() => handle(params)),
+      ),
-    (params: P) => writingCaller.pipe(Effect.flatMap(() => handle(params))),
     () => writingCaller,
   );
 
@@ -173,6 +198,7 @@
             .pipe(Effect.mapError(unavailable));
           if (target !== null && target.deletedAt === null) {
             yield* assertTargetWithinLimits(caller.limits, target);
+            yield* assertSameLink(caller, target);
           }
         }
         // The target's user can raise its modes after the check above; the
@@ -205,29 +231,42 @@
 export interface StartedModes {
   readonly runtimeMode: RuntimeMode;
   readonly interactionMode: ProviderInteractionMode;
+  /** The linked environment the caller acts for. The thread must carry it. */
+  readonly linkOrigin: OrchestrationV2LinkOrigin | undefined;
 }
 
+/**
+ * Starts threads with the modes `modes` requests, which may not be broader
+ * than the caller's. `linked` says what a linked caller gets: threads stamped
+ * with its `linkOrigin`, or a refusal where the tool cannot carry one.
+ */
 export const startsThreads = <P, A, E, R>(
   modes: (params: P) => {
     readonly runtimeMode?: RuntimeMode | undefined;
     readonly interactionMode?: ProviderInteractionMode | undefined;
   },
   handle: (params: P, modes: StartedModes) => Effect.Effect<A, E, R>,
+  linked: "stamped" | { readonly refused: string },
 ) => {
-  const started = Effect.fn("McpToolAccess.started")(function* (params: P) {
-    const { limits } = yield* writingCaller;
+  const started = Effect.fn("McpToolAccess.started")(function* (caller: Caller, params: P) {
     const requested = modes(params);
     return {
+      runtimeMode: yield* resolveRuntimeMode(caller.limits.runtimeMode, requested.runtimeMode),
-      runtimeMode: yield* resolveRuntimeMode(limits.runtimeMode, requested.runtimeMode),
       interactionMode: yield* resolveInteractionMode(
-        limits.interactionMode,
+        caller.limits.interactionMode,
         requested.interactionMode,
       ),
+      linkOrigin: callerLinkOrigin(caller),
     } satisfies StartedModes;
   });
   return declare(
+    (params: P) =>
+      Effect.gen(function* () {
+        const caller = yield* writingCaller;
+        if (linked !== "stamped") yield* assertNotLinked(caller, linked.refused);
-    (params: P) => Effect.flatMap(started(params), (modes) => handle(params, modes)),
-    started,
+        return yield* handle(params, yield* started(caller, params));
+      }),
+    (params: P) => Effect.flatMap(writingCaller, (caller) => started(caller, params)),
   );
 };
 
@@ -243,13 +282,29 @@
     params: P,
     check: Effect.Effect<Caller, OrchestratorMcpFailure, CheckServices>,
   ) => Effect.Effect<A, E, R>,
+  options?: {
+    /**
+     * Whether a linked caller may make it, as for adding a project: it leaves
+     * everything already here as it was.
+     */
+    readonly linkedCallers?: "allowed";
+  },
 ) => {
   const check = orchestratingCaller.pipe(
+    Effect.tap((caller) =>
+      options?.linkedCallers === "allowed"
+        ? Effect.void
+        : assertNotLinked(caller, "change projects or environment settings"),
+    ),
     Effect.tap((caller) => assertFullAccess(caller, fullAccessRequired)),
   );
   return declare(
     (params: P) => check.pipe(Effect.flatMap(() => handle(params, check))),
+    // Linked work never reaches a link at all; that is checked as the call goes.
+    () =>
-    () => check,
+      orchestratingCaller.pipe(
+        Effect.tap((caller) => assertFullAccess(caller, fullAccessRequired)),
+      ),
   );
 };
 
```

## apps/server/src/mcp/OrchestratorMcpService.ts

Status: modified; +3; −0

```diff
@@ -2594,6 +2594,9 @@
                   interactionMode,
                   branch: parent.thread.branch,
                   worktreePath: parent.thread.worktreePath,
+                  ...(parent.thread.linkOrigin === undefined
+                    ? {}
+                    : { linkOrigin: parent.thread.linkOrigin }),
                 })
                 .pipe(
                   Effect.mapError((error) =>
```

## apps/server/src/mcp/WorktreeMcpService.ts

Status: modified; +3; −1

```diff
@@ -374,7 +374,9 @@
                       scriptName: result.scriptName,
                       terminalId: result.terminalId,
                     }
-                  : { status: "no-script" },
+                  : result.status === "skipped-for-link"
+                    ? { status: "skipped" }
+                    : { status: "no-script" },
               ),
               reportFailed("worktree handoff setup script failed"),
             );
```

## apps/server/src/mcp/linkOrigin.test.ts

Status: added; +386; −0

```diff
@@ -1,0 +1,386 @@
+import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
+import * as NodeServices from "@effect/platform-node/NodeServices";
+import { assert, it } from "@effect/vitest";
+import {
+  CommandId,
+  ContextTransferId,
+  EnvironmentId,
+  NodeId,
+  type OrchestrationV2Run,
+  ProjectId,
+  ProviderDriverKind,
+  ProviderInstanceId,
+  RunId,
+  ThreadId,
+} from "@t3tools/contracts";
+import * as Effect from "effect/Effect";
+import * as Option from "effect/Option";
+import * as Layer from "effect/Layer";
+import { McpSchema, McpServer } from "effect/ai";
+
+import * as ServerConfig from "../config.ts";
+import { CodexProviderCapabilitiesV2 } from "../orchestration-v2/Adapters/CodexAdapterV2.ts";
+import * as Orchestrator from "../orchestration-v2/Orchestrator.ts";
+import * as ProjectionStore from "../orchestration-v2/ProjectionStore.ts";
+import type * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
+import * as ProviderAdapterRegistry from "../orchestration-v2/ProviderAdapterRegistry.ts";
+import { makeSubagentChildThread } from "@t3tools/provider-core/server/subagentProjection";
+import * as ThreadForkService from "../orchestration-v2/ThreadForkService.ts";
+import * as ThreadLaunch from "../orchestration-v2/ThreadLaunchService.ts";
+import * as ThreadManagement from "../orchestration-v2/ThreadManagementService.ts";
+import * as ProviderReplayHarness from "../orchestration-v2/testkit/ProviderReplayHarness.ts";
+import * as SqlitePersistence from "../persistence/Sqlite.ts";
+import * as ManagedProjectFolders from "../project/ManagedProjectFolders.ts";
+import * as ProjectService from "../project/ProjectService.ts";
+import * as ProviderRegistry from "../provider/ProviderRegistry.ts";
+import * as ScheduledTaskService from "../scheduledTasks/ScheduledTaskService.ts";
+import * as ServerSettings from "../serverSettings.ts";
+import * as CommandReceiptStore from "../orchestration-v2/CommandReceiptStore.ts";
+import * as SecretRequests from "../secrets/SecretRequests.ts";
+import * as SourceControlRepositoryService from "../sourceControl/SourceControlRepositoryService.ts";
+import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
+import * as McpHttpServer from "./McpHttpServer.ts";
+import * as McpInvocationContext from "./McpInvocationContext.ts";
+import * as PeerForwarding from "../peer/PeerForwarding.ts";
+import * as ThreadSearch from "../orchestration-v2/ThreadSearch.ts";
+
+// A linked environment's session drives this one's real orchestrator through
+// its real T3 tools. What the link starts carries its origin, and the link
+// changes only that.
+
+const instanceId = ProviderInstanceId.make("codex");
+const projectId = ProjectId.make("project:link-origin");
+const adapter = {
+  instanceId,
+  driver: ProviderDriverKind.make("codex"),
+  getCapabilities: () => Effect.succeed(CodexProviderCapabilitiesV2),
+  planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" as const }),
+  openSession: () => Effect.die("No provider process needed for these commands"),
+} as ProviderAdapter.ProviderAdapterV2["Service"];
+const layerDatabase = SqlitePersistence.layerMemory;
+const layerOrchestrator = Layer.mergeAll(
+  layerDatabase,
+  ProjectionStore.layer.pipe(Layer.provide(layerDatabase)),
+  ProviderReplayHarness.layerWithRegistry(
+    { name: "link-origin" },
+    ProviderAdapterRegistry.layerFromAdapters([adapter]),
+    { databaseLayer: layerDatabase, runEffectWorker: false },
+  ),
+);
+const layerOrchestration = ThreadManagement.layer.pipe(
+  Layer.provide(CommandReceiptStore.layer.pipe(Layer.provide(layerDatabase))),
+  Layer.provideMerge(Layer.merge(layerOrchestrator, ServerSettings.layerTest())),
+);
+
+/** Launches dispatch the real thread.create, and nothing more: no workspace, no run. */
+const layerLaunches = Layer.effect(
+  ThreadLaunch.ThreadLaunchService,
+  Effect.gen(function* () {
+    const threads = yield* ThreadManagement.ThreadManagementService;
+    return ThreadLaunch.ThreadLaunchService.of({
+      launch: (input: ThreadLaunch.ThreadLaunchInput) =>
+        threads
+          .dispatch({
+            type: "thread.create",
+            commandId: input.commandId,
+            threadId: input.threadId!,
+            projectId: input.projectId,
+            title: input.title,
+            modelSelection: input.modelSelection,
+            runtimeMode: input.runtimeMode,
+            interactionMode: input.interactionMode,
+            branch: null,
+            worktreePath: null,
+            ...(input.linkOrigin === undefined ? {} : { linkOrigin: input.linkOrigin }),
+            createdBy: input.createdBy,
+            creationSource: input.creationSource,
+          })
+          .pipe(
+            Effect.flatMap(() => threads.getThreadProjection(input.threadId!)),
+            Effect.map((projection) => ({
+              threadId: input.threadId!,
+              projection,
+              resumed: false,
+            })),
+            Effect.orDie,
+          ),
+    } as unknown as ThreadLaunch.ThreadLaunchService["Service"]);
+  }),
+);
+
+const layerTools = Layer.mergeAll(
+  McpHttpServer.layerOrchestratorToolkit,
+  McpHttpServer.layerThreadToolkit,
+  McpHttpServer.layerProjectRegistration,
+).pipe(
+  Layer.provideMerge(McpServer.McpServer.layer),
+  Layer.provide(layerLaunches),
+  Layer.provide(NodeCrypto.layer),
+  Layer.provide(
+    Layer.mock(ProviderRegistry.ProviderRegistry)({ getProviders: Effect.succeed([]) }),
+  ),
+  Layer.provide(
+    Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({
+      list: () => Effect.succeed([]),
+    }),
+  ),
+  Layer.provide(Layer.mock(ScheduledTaskService.ScheduledTaskService)({})),
+  Layer.provide(
+    Layer.mock(ProjectService.ProjectService)({
+      getById: () => Effect.succeed(Option.none()),
+      create: (input) =>
+        Effect.succeed({
+          id: input.projectId,
+          title: input.title,
+          workspaceRoot: input.workspaceRoot,
+          defaultModelSelection: null,
+          scripts: [],
+          createdAt: "2026-10-18T00:00:00.000Z",
+          updatedAt: "2026-10-18T00:00:00.000Z",
+          deletedAt: null,
+        }),
+      update: (input) =>
+        Effect.succeed({
+          id: input.projectId,
+          title: input.title ?? "Untitled",
+          workspaceRoot: "/p/t3code",
+          defaultModelSelection: null,
+          scripts: [],
+          createdAt: "2026-10-18T00:00:00.000Z",
+          updatedAt: "2026-10-18T00:00:00.000Z",
+          deletedAt: null,
+        }),
+    }),
+  ),
+  Layer.provide(Layer.mock(SecretRequests.SecretRequests)({})),
+  Layer.provide(Layer.mock(PeerForwarding.PeerForwarding)({})),
+  Layer.provide(Layer.mock(ThreadSearch.ThreadSearch)({})),
+  Layer.provide(
+    Layer.mock(ManagedProjectFolders.ManagedProjectFolders)({ namedProjectsRoot: "/p" }),
+  ),
+  Layer.provide(Layer.mock(GitVcsDriver.GitVcsDriver)({})),
+  Layer.provide(
+    Layer.mock(SourceControlRepositoryService.SourceControlRepositoryService)({
+      cloneRepository: (input) =>
+        Effect.succeed({
+          cwd: input.destinationPath,
+          remoteUrl: "https://github.com/pingdotgg/t3code.git",
+          repository: null,
+        }),
+    }),
+  ),
+  Layer.provide(
+    ServerConfig.layerTest(process.cwd(), { prefix: "t3-link-origin-" }).pipe(
+      Layer.provide(NodeServices.layer),
+    ),
+  ),
+  Layer.provide(NodeServices.layer),
+);
+
+const mcpClient = McpSchema.McpServerClient.of({
+  clientId: 1,
+  protocolVersion: "2025-06-18",
+  clientCapabilities: {},
+  clientInfo: { name: "link-origin", version: "1" },
+  initializePayload: {
+    protocolVersion: "2025-06-18",
+    capabilities: {},
+    clientInfo: { name: "link-origin", version: "1" },
+  },
+  getClient: Effect.die("unused"),
+});
+
+const clientScope = (
+  sessionId: string,
+  linked: boolean,
+  access: McpInvocationContext.McpClientCaller["access"],
+): McpInvocationContext.McpInvocationScope => ({
+  environmentId: EnvironmentId.make("environment:box"),
+  requestNamespace: `client:${sessionId}`,
+  thread: undefined,
+  client: { sessionId, label: `T3 Code · ${sessionId}`, access, linked },
+  capabilities: new Set(["orchestration", "worktree", "pull-requests"]),
+  issuedAt: 0,
+});
+const laptop = clientScope("laptop", true, "auto");
+const desk = clientScope("desk", true, "auto");
+const trusted = clientScope("trusted", true, "full-access");
+const claudeCode = clientScope("claude-code", false, "full-access");
+
+const call = (
+  scope: McpInvocationContext.McpInvocationScope,
+  name: string,
+  args: Record<string, unknown>,
+) =>
+  McpServer.McpServer.pipe(
+    Effect.flatMap((server) => server.callTool({ name, arguments: args })),
+    Effect.provideService(McpInvocationContext.McpInvocationContext, scope),
+    Effect.provideService(McpSchema.McpServerClient, mcpClient),
+  );
+
+const failureCode = (result: McpSchema.CallToolResult) => {
+  const text = result.content[0];
+  return result.isError === true && text?.type === "text"
+    ? (JSON.parse(text.text) as { code: string }).code
+    : undefined;
+};
+
+const launch = (scope: McpInvocationContext.McpInvocationScope, title: string) =>
+  call(scope, "t3_thread_launch", {
+    projectId,
+    title,
+    modelSelection: { instanceId, model: "gpt-5" },
+  }).pipe(
+    Effect.map((result) => {
+      assert.equal(result.isError, false, JSON.stringify(result.content));
+      return (result.structuredContent as { threadId: ThreadId }).threadId;
+    }),
+  );
+
+const usersOwnThread = Effect.gen(function* () {
+  const orchestrator = yield* Orchestrator.OrchestratorV2;
+  const threadId = ThreadId.make("thread:users-own");
+  yield* orchestrator.dispatch({
+    type: "thread.create",
+    commandId: CommandId.make("create:users-own"),
+    threadId,
+    projectId,
+    title: "The user's own",
+    modelSelection: { instanceId, model: "gpt-5" },
+    // Within an `auto` link's modes, so only the fence decides.
+    runtimeMode: "auto",
+    interactionMode: "default",
+    branch: null,
+    worktreePath: null,
+    createdBy: "user",
+    creationSource: "web",
+  });
+  return threadId;
+});
+
+it.layer(Layer.provideMerge(layerTools, layerOrchestration))("work a link starts", (it) => {
+  it.effect("is stamped, and so is the work it derives", () =>
+    Effect.gen(function* () {
+      const projections = yield* ProjectionStore.ProjectionStoreV2;
+      const launched = yield* launch(laptop, "Started from the laptop");
+      const shell = yield* projections.getThreadShell(launched);
+      assert.deepEqual(shell?.linkOrigin, {
+        sessionId: "laptop",
+        label: "T3 Code · laptop",
+        access: "auto",
+      });
+
+      // A subagent of it and a fork of it are built from its thread, origin included.
+      const thread = (yield* ThreadManagement.ThreadManagementService.pipe(
+        Effect.flatMap((threads) => threads.getThreadProjection(launched)),
+      )).thread;
+      const child = makeSubagentChildThread({
+        parentThread: thread,
+        childThreadId: ThreadId.make("thread:child"),
+        parentNodeId: NodeId.make("node:child"),
+        activeProviderThreadId: null,
+        providerInstanceId: instanceId,
+        modelSelection: { instanceId, model: "gpt-5" },
+        title: "Child",
+        now: thread.createdAt,
+        createdBy: "agent",
+        creationSource: "mcp",
+      });
+      assert.deepEqual(child.linkOrigin, shell?.linkOrigin);
+      const fork = yield* ThreadForkService.ThreadForkServiceV2.pipe(
+        Effect.flatMap((forks) =>
+          forks.plan({
+            sourceProjection: { thread },
+            sourceRun: { id: RunId.make("run:source"), status: "completed" } as OrchestrationV2Run,
+            sourceProviderThread: undefined,
+            canonicalSourcePoint: { threadId: launched },
+            transferId: ContextTransferId.make("transfer:fork"),
+            targetThreadId: ThreadId.make("thread:fork"),
+            createdBy: "agent",
+            creationSource: "mcp",
+            createdAt: thread.createdAt,
+          }),
+        ),
+        Effect.provide(ThreadForkService.layer),
+      );
+      assert.deepEqual(fork.targetThread.linkOrigin, shell?.linkOrigin);
+
+      // An ordinary outside agent's launch is not a link's.
+      const plain = yield* launch(claudeCode, "Started by Claude Code");
+      assert.equal((yield* projections.getThreadShell(plain))?.linkOrigin, undefined);
+    }),
+  );
+
+  it.effect("changes only that link's threads below full access", () =>
+    Effect.gen(function* () {
+      const projections = yield* ProjectionStore.ProjectionStoreV2;
+      const users = yield* usersOwnThread;
+      const ownLaunch = yield* launch(laptop, "The laptop's");
+      const rename = (scope: McpInvocationContext.McpInvocationScope, threadId: ThreadId) =>
+        call(scope, "t3_thread_update", {
+          threadId,
+          action: "rename",
+          title: `Renamed by ${scope.client!.sessionId}`,
+        });
+
+      // The link may change what it started.
+      assert.equal((yield* rename(laptop, ownLaunch)).isError, false);
+      assert.equal((yield* projections.getThreadShell(ownLaunch))?.title, "Renamed by laptop");
+      // Not the user's own thread, nor another link's.
+      assert.equal(failureCode(yield* rename(laptop, users)), "capability_denied");
+      assert.equal(failureCode(yield* rename(desk, ownLaunch)), "capability_denied");
+      assert.equal((yield* projections.getThreadShell(users))?.title, "The user's own");
+      // An ordinary outside agent is not fenced.
+      assert.equal((yield* rename(claudeCode, users)).isError, false);
+      // Naming this environment's own id is still this environment's thread.
+      for (const [name, args] of [
+        ["t3_thread_send", { message: "From the laptop." }],
+        ["t3_thread_interrupt", {}],
+      ] as const) {
+        const result = yield* call(laptop, name, {
+          ...args,
+          environmentId: laptop.environmentId,
+          threadId: users,
+        });
+        assert.equal(failureCode(result), "capability_denied", name);
+      }
+
+      // Nor scheduled tasks, which belong to no thread.
+      assert.equal(
+        failureCode(yield* call(laptop, "delete_scheduled_task", { scheduledTaskId: "any" })),
+        "capability_denied",
+      );
+
+      // Reads are not fenced.
+      const read = yield* call(laptop, "t3_thread_read", { threadId: users });
+      assert.equal(read.isError, false);
+    }),
+  );
+
+  it.effect("is not fenced when the link has full access, but still says where it came from", () =>
+    Effect.gen(function* () {
+      const projections = yield* ProjectionStore.ProjectionStoreV2;
+      const users = yield* usersOwnThread;
+      const launched = yield* launch(trusted, "Started from the desk");
+      assert.deepEqual((yield* projections.getThreadShell(launched))?.linkOrigin, {
+        sessionId: "trusted",
+        label: "T3 Code · trusted",
+        access: "full-access",
+      });
+
+      const renamed = yield* call(trusted, "t3_thread_update", {
+        threadId: users,
+        action: "rename",
+        title: "Renamed from the desk",
+      });
+      assert.equal(renamed.isError, false, JSON.stringify(renamed.content));
+      for (const [name, args] of [
+        ["t3_project_update", { projectId, title: "The desk's now" }],
+        ["t3_project_create", { title: "t3code", workspaceRoot: "/p/t3code" }],
+      ] as const) {
+        const changed = yield* call(trusted, name, args);
+        assert.equal(changed.isError, false, `${name}: ${JSON.stringify(changed.content)}`);
+      }
+    }),
+  );
+});
```

## apps/server/src/mcp/linkOrigin.ts

Status: added; +67; −0

```diff
@@ -1,0 +1,67 @@
+import {
+  linkOriginIsFenced,
+  OrchestratorMcpFailure,
+  type OrchestrationV2LinkOrigin,
+  type OrchestrationV2ThreadShell,
+} from "@t3tools/contracts";
+import * as Effect from "effect/Effect";
+
+import type { Caller } from "./threadAccess.ts";
+
+/**
+ * Work a linked environment starts here stays within that link, unless the
+ * link has full access. An agent reaches this environment through a link as
+ * one MCP client session, and everything that session starts, and everything
+ * that work starts in turn, carries the session in `linkOrigin`. Below full
+ * access such a caller may change only work from the same link: not the
+ * user's own threads, projects or settings, nor work from another link, and
+ * it may not use this environment's own links. Reads are not fenced.
+ *
+ * This is a routing rule for T3's own tools. An agent the link starts still
+ * runs as this environment's user, which is why a full-access link, whose
+ * agents can do anything that user can, is not fenced at all.
+ */
+
+/** The link a caller acts for: its own session, or the link that started its thread. */
+export const callerLinkOrigin = (
+  caller: Pick<Caller, "scope" | "caller">,
+): OrchestrationV2LinkOrigin | undefined =>
+  caller.caller?.linkOrigin ??
+  (caller.scope.client?.linked === true
+    ? {
+        sessionId: caller.scope.client.sessionId,
+        label: caller.scope.client.label,
+        access: caller.scope.client.access,
+      }
+    : undefined);
+
+/** The link that fences a caller, if any: one paired below full access. */
+const fencingLinkOrigin = (caller: Pick<Caller, "scope" | "caller">) => {
+  const origin = callerLinkOrigin(caller);
+  return origin !== undefined && linkOriginIsFenced(origin) ? origin : undefined;
+};
+
+const outsideLink = (origin: OrchestrationV2LinkOrigin, message: string) =>
+  new OrchestratorMcpFailure({
+    code: "capability_denied",
+    message: `This work was started from ${origin.label} through a linked environment, so ${message} A link paired with full access is not limited this way.`,
+  });
+
+/** A thread a fenced caller may change: one its own link started. */
+export const assertSameLink = (
+  caller: Pick<Caller, "scope" | "caller">,
+  target: Pick<OrchestrationV2ThreadShell, "id" | "linkOrigin">,
+) => {
+  const origin = fencingLinkOrigin(caller);
+  return origin === undefined || target.linkOrigin?.sessionId === origin.sessionId
+    ? Effect.void
+    : Effect.fail(outsideLink(origin, `it may change only threads that link started.`));
+};
+
+/** Fenced work never changes what the environment already has: projects, settings, scheduled runs. */
+export const assertNotLinked = (caller: Pick<Caller, "scope" | "caller">, what: string) => {
+  const origin = fencingLinkOrigin(caller);
+  return origin === undefined
+    ? Effect.void
+    : Effect.fail(outsideLink(origin, `it cannot ${what}.`));
+};
```

## apps/server/src/mcp/toolkits/attachment/handlers.ts

Status: modified; +30; −24

```diff
@@ -49,31 +49,37 @@
   Effect.sync(() => {
     const uploadOwners = new Map<string, { readonly owner: string; readonly issuedAt: number }>();
     return {
+      // A pending upload belongs to its caller until a send claims it, so
+      // linked callers may stage the uploads their own sends need.
+      t3_attachment_prepare_upload: McpToolAccess.writes(
+        (input) =>
+          Effect.gen(function* () {
+            const result = yield* Upload.issueAttachmentUploadUrl(input.upload).pipe(
+              Effect.mapError(unavailable),
+            );
+            const now = yield* Clock.currentTimeMillis;
+            for (const [id, entry] of uploadOwners) {
+              if (now - entry.issuedAt > UPLOAD_OWNER_TTL_MS) uploadOwners.delete(id);
+            }
+            uploadOwners.set(result.attachmentId, { owner: yield* uploadOwner, issuedAt: now });
+            return result;
-      t3_attachment_prepare_upload: McpToolAccess.writes((input) =>
-        Effect.gen(function* () {
-          const result = yield* Upload.issueAttachmentUploadUrl(input.upload).pipe(
+          }),
-            Effect.mapError(unavailable),
-          );
-          const now = yield* Clock.currentTimeMillis;
-          for (const [id, entry] of uploadOwners) {
-            if (now - entry.issuedAt > UPLOAD_OWNER_TTL_MS) uploadOwners.delete(id);
-          }
-          uploadOwners.set(result.attachmentId, { owner: yield* uploadOwner, issuedAt: now });
-          return result;
-        }),
+        { linkedCallers: "allowed" },
       ),
+      t3_attachment_discard: McpToolAccess.writes(
+        (input) =>
+          Effect.gen(function* () {
+            if (uploadOwners.get(input.attachmentId)?.owner !== (yield* uploadOwner)) {
+              return yield* new OrchestratorMcpFailure({
+                code: "invalid_request",
+                message: "Only the caller that prepared a pending upload can discard it.",
+              });
+            }
+            yield* Upload.deletePendingAttachment(input.attachmentId);
+            uploadOwners.delete(input.attachmentId);
+            return {};
+          }),
-      t3_attachment_discard: McpToolAccess.writes((input) =>
-        Effect.gen(function* () {
-          if (uploadOwners.get(input.attachmentId)?.owner !== (yield* uploadOwner)) {
+        { linkedCallers: "allowed" },
-            return yield* new OrchestratorMcpFailure({
-              code: "invalid_request",
-              message: "Only the caller that prepared a pending upload can discard it.",
-            });
-          }
-          yield* Upload.deletePendingAttachment(input.attachmentId);
-          uploadOwners.delete(input.attachmentId);
-          return {};
-        }),
       ),
       t3_thread_send_attachments: McpToolAccess.writesThreads(
         (input) => [input.threadId],
```

## apps/server/src/mcp/toolkits/orchestrator/handlers.ts

Status: modified; +2; −0

```diff
@@ -66,6 +66,8 @@
         const service = yield* OrchestratorMcpService.OrchestratorMcpService;
         return yield* service.scheduleTask(scope, input);
       }),
+    // A scheduled run starts long after its caller, with nothing to carry the link.
+    { refused: "schedule tasks in this environment" },
   ),
   list_scheduled_tasks: McpToolAccess.reads((input) =>
     Effect.gen(function* () {
```

## apps/server/src/mcp/toolkits/project/handlers.ts

Status: modified; +64; −53

```diff
@@ -63,7 +63,7 @@
 export const layer = McpToolAccess.toLayer(ProjectToolkit, {
   t3_thread_launch: McpToolAccess.startsThreads(
     (input) => input,
-    (input, { runtimeMode, interactionMode }) =>
+    (input, { runtimeMode, interactionMode, linkOrigin }) =>
       Effect.gen(function* () {
         const scope = yield* McpInvocationContext.McpInvocationContext;
         const context = yield* readCaller();
@@ -206,6 +206,7 @@
               }),
           createdBy: "agent",
           creationSource: "mcp",
+          ...(linkOrigin === undefined ? {} : { linkOrigin }),
         }).pipe(
           Effect.mapError((error) => {
             if (error._tag === "AttachmentClaimError") {
@@ -233,6 +234,8 @@
           status: run?.status ?? null,
         };
       }),
+    // The launched thread carries the caller's link, if it has one.
+    "stamped",
   ),
   t3_project_list: McpToolAccess.reads((input) =>
     Effect.gen(function* () {
@@ -256,49 +259,54 @@
       return result.value;
     }),
   ),
+  t3_project_create: McpToolAccess.writesEnvironment(
+    ({ workspaceRoot, ...input }) =>
+      Effect.gen(function* () {
-  t3_project_create: McpToolAccess.writesEnvironment(({ workspaceRoot, ...input }) =>
-    Effect.gen(function* () {
-      const projects = yield* Project.ProjectService;
+        const projects = yield* Project.ProjectService;
-      if (workspaceRoot === undefined) {
-        // Project creation records no model default (only an update does), so
-        // reject what this mode would otherwise drop silently.
+        if (workspaceRoot === undefined) {
+          // Project creation records no model default (only an update does), so
+          // reject what this mode would otherwise drop silently.
+          if (
+            input.scripts !== undefined ||
+            input.createWorkspaceRootIfMissing !== undefined ||
+            input.defaultModelSelection !== undefined
+          )
+            return yield* new OrchestratorMcpFailure({
+              code: "invalid_request",
+              message:
+                "A project started from its title takes only a title; set scripts or defaultModelSelection afterwards with t3_project_update.",
+            });
+          const folders = yield* ManagedProjectFolders.ManagedProjectFolders;
+          const created = yield* folders.createNamedProject({ name: input.title }).pipe(
-        if (
-          input.scripts !== undefined ||
-          input.createWorkspaceRootIfMissing !== undefined ||
-          input.defaultModelSelection !== undefined
-        )
-          return yield* new OrchestratorMcpFailure({
-            code: "invalid_request",
-            message:
-              "A project started from its title takes only a title; set scripts or defaultModelSelection afterwards with t3_project_update.",
-          });
-        const folders = yield* ManagedProjectFolders.ManagedProjectFolders;
-        const created = yield* folders
-          .createNamedProject({ name: input.title })
-          .pipe(
             Effect.mapError(
               (error) =>
+                new OrchestratorMcpFailure({
-                new OrchestratorMcpFailure({ code: "orchestration_error", message: error.message }),
+                  code: "orchestration_error",
+                  message: error.message,
+                }),
             ),
           );
+          const project = yield* projects
+            .getById(created.projectId)
+            .pipe(
+              Effect.mapError(unavailable),
+              Effect.flatMap(
+                Option.match({ onNone: () => Effect.fail(unavailable()), onSome: Effect.succeed }),
+              ),
+            );
+          return {
+            ...project,
+            ...(created.commitError === undefined ? {} : { commitError: created.commitError }),
+          };
+        }
+        const commandId = yield* newCommandId();
+        return yield* projects
+          .create({ ...input, workspaceRoot, commandId, projectId: ProjectId.make(commandId) })
+          .pipe(Effect.mapError(projectFailure));
+      }),
-        const project = yield* projects
+    // Linked work may bring the repository it works in; an existing
+    // registered workspace is still refused.
+    { linkedCallers: "allowed" },
-          .getById(created.projectId)
-          .pipe(
-            Effect.mapError(unavailable),
-            Effect.flatMap(
-              Option.match({ onNone: () => Effect.fail(unavailable()), onSome: Effect.succeed }),
-            ),
-          );
-        return {
-          ...project,
-          ...(created.commitError === undefined ? {} : { commitError: created.commitError }),
-        };
-      }
-      const commandId = yield* newCommandId();
-      return yield* projects
-        .create({ ...input, workspaceRoot, commandId, projectId: ProjectId.make(commandId) })
-        .pipe(Effect.mapError(projectFailure));
-    }),
   ),
   t3_project_update: McpToolAccess.writesEnvironment((input) =>
     Effect.gen(function* () {
@@ -316,18 +324,21 @@
         .pipe(Effect.mapError(projectFailure));
     }),
   ),
+  t3_project_clone: McpToolAccess.writesEnvironment(
+    (input) =>
+      Effect.gen(function* () {
+        const repositories = yield* Repositories.SourceControlRepositoryService;
+        return yield* repositories.cloneRepository(input).pipe(
+          Effect.mapError(
+            (error) =>
+              new OrchestratorMcpFailure({
+                code: "orchestration_error",
+                message: error.detail,
+              }),
+          ),
+        );
+      }),
+    // An existing destination is never adopted.
-  t3_project_clone: McpToolAccess.writesEnvironment((input) =>
-    Effect.gen(function* () {
-      const repositories = yield* Repositories.SourceControlRepositoryService;
-      return yield* repositories.cloneRepository(input).pipe(
-        Effect.mapError(
-          (error) =>
-            new OrchestratorMcpFailure({
-              code: "orchestration_error",
-              message: error.detail,
-            }),
+    { linkedCallers: "allowed" },
-        ),
-      );
-    }),
   ),
 });
```

## apps/server/src/orchestration-v2/Orchestrator.ts

Status: modified; +1; −0

```diff
@@ -2459,6 +2459,7 @@
       worktreePath: command.worktreePath,
       activeProviderThreadId: null,
       pullRequests: [],
+      ...(command.linkOrigin === undefined ? {} : { linkOrigin: command.linkOrigin }),
       lineage: {
         parentThreadId: null,
         relationshipToParent: null,
```

## apps/server/src/orchestration-v2/ProjectionStore.ts

Status: modified; +6; −0

```diff
@@ -1596,6 +1596,9 @@
     ...(projection.thread.historyOrigin === undefined
       ? {}
       : { historyOrigin: projection.thread.historyOrigin }),
+    ...(projection.thread.linkOrigin === undefined
+      ? {}
+      : { linkOrigin: projection.thread.linkOrigin }),
     latestRunId: latestRun?.id ?? null,
     latestRunRequestedAt: latestRun?.requestedAt ?? null,
     latestRunStartedAt: latestRun?.startedAt ?? null,
@@ -1854,6 +1857,9 @@
     ...(input.state.thread.historyOrigin === undefined
       ? {}
       : { historyOrigin: input.state.thread.historyOrigin }),
+    ...(input.state.thread.linkOrigin === undefined
+      ? {}
+      : { linkOrigin: input.state.thread.linkOrigin }),
     latestRunId: input.state.latestRunId,
     latestRunRequestedAt: input.state.latestRunRequestedAt,
     latestRunStartedAt: input.state.latestRunStartedAt,
```

## apps/server/src/orchestration-v2/ThreadLaunchService.ts

Status: modified; +4; −0

```diff
@@ -9,6 +9,7 @@
   type ModelSelection,
   type OrchestrationV2Actor,
   type OrchestrationV2CreationSource,
+  type OrchestrationV2LinkOrigin,
   type OrchestrationV2ProviderThreadNativeMetadata,
   type OrchestrationV2ThreadProjection,
   type ProviderDriverKind,
@@ -97,6 +98,8 @@
   };
   readonly createdBy: OrchestrationV2Actor;
   readonly creationSource: OrchestrationV2CreationSource;
+  /** The linked environment this launch is for, if one asked for it. */
+  readonly linkOrigin?: OrchestrationV2LinkOrigin;
 }
 
 type PreparationInput = Pick<
@@ -923,6 +926,7 @@
                 ...(input.importedNativeThread === undefined
                   ? {}
                   : { importedNativeThread: input.importedNativeThread }),
+                ...(input.linkOrigin === undefined ? {} : { linkOrigin: input.linkOrigin }),
                 createdBy: input.createdBy,
                 creationSource: input.creationSource,
               });
```

## apps/server/src/orchestration-v2/ThreadManagementService.test.ts

Status: modified; +33; −0

```diff
@@ -2,6 +2,7 @@
 import {
   CommandId,
   MessageId,
+  type OrchestrationV2Command,
   type OrchestrationV2Run,
   type OrchestrationV2StoredEvent,
   type OrchestrationV2ThreadProjection,
@@ -25,6 +26,38 @@
 import * as ProviderSessionManager from "./ProviderSessionManager.ts";
 import * as ThreadManagementService from "./ThreadManagementService.ts";
 
+it("stamps authoritative provenance on commands that create threads or messages", () => {
+  const command: OrchestrationV2Command = {
+    type: "thread.create",
+    createdBy: "agent",
+    creationSource: "mcp",
+    commandId: CommandId.make("command:thread-management:create"),
+    threadId: ThreadId.make("thread:thread-management:create"),
+    projectId: ProjectId.make("project:thread-management"),
+    title: "Thread management",
+    modelSelection: {
+      instanceId: ProviderInstanceId.make("codex"),
+      model: "gpt-5-codex",
+    },
+    runtimeMode: "full-access",
+    interactionMode: "default",
+    branch: null,
+    worktreePath: null,
+    // A client cannot claim a linked environment's origin for its own thread.
+    linkOrigin: { sessionId: "forged", label: "Somewhere else" },
+  };
+
+  const stamped = ThreadManagementService.withCreationProvenance(command, {
+    createdBy: "user",
+    creationSource: "web",
+  });
+  expect(stamped).toMatchObject({
+    createdBy: "user",
+    creationSource: "web",
+  });
+  expect(stamped).not.toHaveProperty("linkOrigin");
+});
+
 const layerReceiptsTest = CommandReceiptStore.layer.pipe(
   Layer.provide(SqlitePersistence.layerMemory),
 );
```

[Next page](https://git.cafe/pingdotgg/t3code/pull/16695/diffs?after=ImFwcHMvc2VydmVyL3NyYy9vcmNoZXN0cmF0aW9uLXYyL1RocmVhZE1hbmFnZW1lbnRTZXJ2aWNlLnRlc3QudHMi&expectedVersion=16&format=markdown)
