@gajae-code/coding-agent 0.15.6 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/types/cli/args.d.ts +2 -0
  3. package/dist/types/cli/root-flags.d.ts +14 -0
  4. package/dist/types/cli/update-cli.d.ts +13 -0
  5. package/dist/types/cli.d.ts +7 -0
  6. package/dist/types/commands/launch.d.ts +10 -0
  7. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  8. package/dist/types/config/file-lock.d.ts +5 -0
  9. package/dist/types/config/model-registry.d.ts +6 -0
  10. package/dist/types/config/settings-schema.d.ts +10 -1
  11. package/dist/types/config/settings.d.ts +2 -0
  12. package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
  13. package/dist/types/crash/record-loader.d.ts +15 -0
  14. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  15. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  16. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
  17. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  18. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  19. package/dist/types/master-mode/context.d.ts +21 -0
  20. package/dist/types/master-mode/first-request.d.ts +34 -0
  21. package/dist/types/master-mode/snapshot.d.ts +32 -0
  22. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  23. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  24. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  25. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  26. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  27. package/dist/types/modes/print-mode.d.ts +17 -0
  28. package/dist/types/modes/ui-language.d.ts +7 -1
  29. package/dist/types/sdk/broker/broker.d.ts +77 -1
  30. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  31. package/dist/types/sdk/broker/index.d.ts +3 -0
  32. package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
  33. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  34. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  35. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  36. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  37. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  38. package/dist/types/sdk/broker/state-version.d.ts +4 -1
  39. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  40. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  41. package/dist/types/sdk/bus/index.d.ts +6 -0
  42. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  43. package/dist/types/sdk/cli/index.d.ts +1 -0
  44. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  45. package/dist/types/sdk/cli/rows.d.ts +4 -3
  46. package/dist/types/sdk/cli/session-cli.d.ts +18 -2
  47. package/dist/types/sdk/client/client.d.ts +3 -2
  48. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  49. package/dist/types/sdk/host/control/index.d.ts +1 -1
  50. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  51. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  52. package/dist/types/sdk/host/host.d.ts +10 -0
  53. package/dist/types/sdk/host/session-runtime.d.ts +52 -0
  54. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  55. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  56. package/dist/types/sdk/lifecycle-session.d.ts +2 -0
  57. package/dist/types/sdk/router/session-router.d.ts +2 -0
  58. package/dist/types/sdk/session.d.ts +16 -0
  59. package/dist/types/sdk/startup-capability.d.ts +2 -1
  60. package/dist/types/session/agent-session.d.ts +5 -0
  61. package/dist/types/session/credential-store-errors.d.ts +1 -0
  62. package/dist/types/session/session-manager.d.ts +65 -0
  63. package/dist/types/session/yield-queue.d.ts +5 -4
  64. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  65. package/dist/types/tools/bash.d.ts +4 -0
  66. package/dist/types/tools/gh.d.ts +8 -0
  67. package/dist/types/tools/index.d.ts +4 -0
  68. package/package.json +7 -7
  69. package/scripts/generate-sdk-operation-inventory.ts +6 -0
  70. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  71. package/src/cli/accounts-cli.ts +9 -5
  72. package/src/cli/args.ts +17 -0
  73. package/src/cli/fast-help.ts +1 -0
  74. package/src/cli/root-flags.ts +5 -0
  75. package/src/cli/update-cli.ts +77 -11
  76. package/src/commands/launch.ts +86 -18
  77. package/src/commands/sdk.ts +113 -21
  78. package/src/config/autorouting-tier-map.ts +12 -0
  79. package/src/config/file-lock.ts +158 -83
  80. package/src/config/model-preset-registry.ts +1 -5
  81. package/src/config/model-profiles.ts +5 -5
  82. package/src/config/model-registry.ts +23 -0
  83. package/src/config/model-resolver.ts +5 -0
  84. package/src/config/settings-schema.ts +8 -1
  85. package/src/config/settings.ts +5 -0
  86. package/src/coordinator-mcp/projection-scan.ts +21 -0
  87. package/src/coordinator-mcp/question-state.ts +9 -11
  88. package/src/coordinator-mcp/server.ts +94 -20
  89. package/src/crash/index-store.ts +2 -0
  90. package/src/crash/record-loader.ts +10 -0
  91. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  92. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  93. package/src/gjc-runtime/session-state-lock.ts +79 -6
  94. package/src/gjc-runtime/session-state-sidecar.ts +929 -23
  95. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  96. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
  97. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  98. package/src/hooks/skill-state.ts +6 -0
  99. package/src/internal-urls/docs-index.generated.ts +7 -7
  100. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  101. package/src/main.ts +57 -2
  102. package/src/master-mode/context.ts +50 -0
  103. package/src/master-mode/first-request.ts +65 -0
  104. package/src/master-mode/snapshot.ts +205 -0
  105. package/src/modes/acp/acp-agent.ts +605 -156
  106. package/src/modes/components/footer.ts +4 -2
  107. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  108. package/src/modes/components/oauth-selector.ts +6 -1
  109. package/src/modes/components/settings-selector.ts +1 -0
  110. package/src/modes/components/status-line/model-name.ts +53 -0
  111. package/src/modes/components/status-line/priority-row.ts +107 -0
  112. package/src/modes/components/status-line/segments.ts +23 -9
  113. package/src/modes/components/tool-status-header.ts +151 -15
  114. package/src/modes/controllers/input-controller.ts +28 -0
  115. package/src/modes/controllers/selector-controller.ts +191 -96
  116. package/src/modes/print-mode.ts +64 -0
  117. package/src/modes/ui-language.ts +49 -2
  118. package/src/prompts/system/master-mode.md +28 -0
  119. package/src/sdk/broker/broker.ts +1636 -26
  120. package/src/sdk/broker/endpoint-authority.ts +86 -0
  121. package/src/sdk/broker/ensure.ts +5 -0
  122. package/src/sdk/broker/index.ts +3 -0
  123. package/src/sdk/broker/lifecycle.ts +186 -41
  124. package/src/sdk/broker/master-capability.ts +232 -0
  125. package/src/sdk/broker/session-index.ts +121 -17
  126. package/src/sdk/broker/session-scope.ts +272 -0
  127. package/src/sdk/broker/spawn-authority.ts +1031 -0
  128. package/src/sdk/broker/spawn-substrate.ts +548 -0
  129. package/src/sdk/broker/state-version.ts +26 -14
  130. package/src/sdk/broker/transport.ts +1 -0
  131. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  132. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  133. package/src/sdk/bus/chat-daemon-runtime.ts +35 -4
  134. package/src/sdk/bus/index.ts +51 -4
  135. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  136. package/src/sdk/cli/index.ts +1 -0
  137. package/src/sdk/cli/master-cli.ts +259 -0
  138. package/src/sdk/cli/rows.ts +11 -8
  139. package/src/sdk/cli/session-cli.ts +264 -5
  140. package/src/sdk/client/client.ts +14 -8
  141. package/src/sdk/host/control/dispatch.ts +99 -38
  142. package/src/sdk/host/control/index.ts +2 -0
  143. package/src/sdk/host/control/operations.ts +2 -0
  144. package/src/sdk/host/control/runtime-gate.ts +203 -11
  145. package/src/sdk/host/host.ts +27 -2
  146. package/src/sdk/host/session-runtime.test.ts +210 -6
  147. package/src/sdk/host/session-runtime.ts +291 -49
  148. package/src/sdk/lifecycle/broker-client.ts +28 -0
  149. package/src/sdk/lifecycle/service.ts +279 -20
  150. package/src/sdk/lifecycle-session.ts +10 -2
  151. package/src/sdk/protocol/operation-inventory.generated.json +51 -0
  152. package/src/sdk/protocol/operation-registry.ts +13 -3
  153. package/src/sdk/router/session-router.ts +67 -59
  154. package/src/sdk/session.ts +357 -155
  155. package/src/sdk/startup-capability.ts +1 -0
  156. package/src/session/agent-session.ts +640 -103
  157. package/src/session/blob-store.ts +15 -5
  158. package/src/session/credential-store-errors.ts +2 -0
  159. package/src/session/internal/managed-session-storage.ts +153 -78
  160. package/src/session/session-manager.ts +337 -24
  161. package/src/session/yield-queue.ts +24 -10
  162. package/src/setup/frictionless-onboarding.ts +2 -0
  163. package/src/telemetry/events.ts +1138 -12
  164. package/src/tools/bash.ts +256 -42
  165. package/src/tools/gh.ts +104 -48
  166. package/src/tools/github-cache.ts +2 -2
  167. package/src/tools/index.ts +4 -0
@@ -37,8 +37,15 @@ import {
37
37
  settleOwnedWork,
38
38
  } from "../../session/terminal-abort";
39
39
  import { parseThinkingLevel } from "../../thinking";
40
+ import { readEndpointFile } from "../broker/endpoint-authority";
40
41
  import { ensureBroker } from "../broker/ensure";
41
- import { SessionIndex } from "../broker/session-index";
42
+ import { processIncarnation } from "../broker/process-incarnation";
43
+ import {
44
+ type MasterRoleAttestationV2,
45
+ resolveSessionLocator,
46
+ SessionIndex,
47
+ type SessionLocatorV2,
48
+ } from "../broker/session-index";
42
49
  import {
43
50
  parseSyntheticModelId,
44
51
  resolveSyntheticModelSelection,
@@ -58,8 +65,12 @@ import {
58
65
  resolveReconciliationSessionFile,
59
66
  } from "../reconciliation-extensions";
60
67
  import { sanitizeTurnResultContent, type TurnResultContent } from "../turn-result";
61
- import { type ControlSurface, controlRequestFromFrame, dispatchControl } from "./control";
62
- import { BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD } from "./control/runtime-gate";
68
+ import { type ControlSurface, controlRequestFromFrame, dispatchControl, terminalAbortIdentity } from "./control";
69
+ import {
70
+ BROKER_RUNTIME_ABORT_CAPABILITY_FIELD,
71
+ BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD,
72
+ hasBrokerRuntimeAbortCapability,
73
+ } from "./control/runtime-gate";
63
74
  import { SessionSdkHost, type SessionSdkHostOptions } from "./host";
64
75
  import { clearAutoroutingInactive, isAutoroutingInactive, markAutoroutingInactive } from "./internal-autorouting-state";
65
76
  import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "./query";
@@ -103,6 +114,65 @@ const SDK_ONLY_TERMINAL_PUBLICATION_WAIT_MS = 1_000;
103
114
  * after this bound are abandoned — their durable broker state is the recovery
104
115
  * authority, and outcomes are inherently uncertain. */
105
116
  const GATE_RESOLUTION_QUIESCENCE_MS = 5_000;
117
+ /** Master verification fields are ephemeral protocol values, not unbounded input. */
118
+ const MASTER_AUTH_VALUE_MAX_LENGTH = 512;
119
+ const MASTER_AUTH_VALUE_PATTERN = /^[A-Za-z0-9_-]+$/u;
120
+ /** Keep replay protection finite even when an authenticated client floods nonces. */
121
+ const MASTER_NONCE_REPLAY_MAX_ENTRIES = 1_024;
122
+ /** Verification exchanges complete in seconds; retain replay evidence for a bounded window. */
123
+ const MASTER_NONCE_REPLAY_TTL_MS = 5 * 60_000;
124
+
125
+ export type MasterCapabilityReplayState = Map<string, number>;
126
+
127
+ export function verifyMasterCapabilityFrame(input: {
128
+ frame: { nonce: unknown; attestationEpoch: unknown; capability: unknown };
129
+ expectedCapability: string | undefined;
130
+ expectedEpoch: string | undefined;
131
+ replay: MasterCapabilityReplayState;
132
+ now?: number;
133
+ }): { ok: boolean; nonce: string; attestationEpoch: string } {
134
+ const now = input.now ?? Date.now();
135
+ for (const [consumedNonce, expiresAt] of input.replay) {
136
+ if (expiresAt <= now) input.replay.delete(consumedNonce);
137
+ }
138
+ const bounded = (value: unknown): value is string =>
139
+ typeof value === "string" &&
140
+ value.length > 0 &&
141
+ value.length <= MASTER_AUTH_VALUE_MAX_LENGTH &&
142
+ MASTER_AUTH_VALUE_PATTERN.test(value);
143
+ const nonce = bounded(input.frame.nonce) ? input.frame.nonce : "";
144
+ const epoch = bounded(input.frame.attestationEpoch) ? input.frame.attestationEpoch : "";
145
+ const capability = bounded(input.frame.capability) ? input.frame.capability : "";
146
+ const expectedCapability = bounded(input.expectedCapability) ? input.expectedCapability : "";
147
+ const expectedEpoch = bounded(input.expectedEpoch) ? input.expectedEpoch : "";
148
+ const authenticatedFields =
149
+ bounded(input.frame.nonce) &&
150
+ bounded(input.frame.attestationEpoch) &&
151
+ bounded(input.frame.capability) &&
152
+ bounded(input.expectedCapability) &&
153
+ bounded(input.expectedEpoch) &&
154
+ epoch === expectedEpoch;
155
+ const expectedBytes = Buffer.from(expectedCapability);
156
+ const capabilityBytes = Buffer.from(capability);
157
+ const capabilityMatches =
158
+ authenticatedFields &&
159
+ expectedBytes.length === capabilityBytes.length &&
160
+ crypto.timingSafeEqual(expectedBytes, capabilityBytes);
161
+ const reusable = capabilityMatches && !input.replay.has(nonce);
162
+ if (reusable) {
163
+ if (input.replay.size >= MASTER_NONCE_REPLAY_MAX_ENTRIES) {
164
+ const oldest = input.replay.keys().next().value;
165
+ if (typeof oldest === "string") input.replay.delete(oldest);
166
+ }
167
+ input.replay.set(nonce, now + MASTER_NONCE_REPLAY_TTL_MS);
168
+ }
169
+ return {
170
+ ok: reusable,
171
+ nonce,
172
+ attestationEpoch: authenticatedFields ? epoch : "",
173
+ };
174
+ }
175
+
106
176
  /** Maximum time a replaced runtime may retain a lifecycle persistence task. */
107
177
  const LIFECYCLE_QUIESCENCE_MS = 1_000;
108
178
 
@@ -142,6 +212,8 @@ export interface SessionSdkRuntimeOptions
142
212
  settings?: Settings;
143
213
  /** Mutable shadow of patched config values merged into query readback. */
144
214
  configOverrides?: Map<string, unknown>;
215
+ /** Stable master lineage identity retained across session switches/branches. */
216
+ masterOwnerSessionId?: string;
145
217
  }
146
218
 
147
219
  export interface SdkOnlyInvocationRecord extends InvocationCorrelation {
@@ -400,6 +472,8 @@ export interface CreateSdkSessionRuntimeOptions {
400
472
  agentDir: string;
401
473
  /** Lifecycle-owned sessions require broker publication before they become usable. */
402
474
  brokerRegistrationRequired?: boolean;
475
+ /** Trusted broker-issued lifecycle marker bound to lifecycle host index events. */
476
+ lifecycleRequestId?: string;
403
477
  createTransport(input: {
404
478
  sessionId: string;
405
479
  stateRoot: string;
@@ -411,6 +485,12 @@ export interface CreateSdkSessionRuntimeOptions {
411
485
  onSdkRequest?: SessionSdkHostOptions["onRequest"];
412
486
  /** Mutable shadow of patched config values merged into query readback. */
413
487
  configOverrides?: Map<string, unknown>;
488
+ /** In-memory master capability for private broker verification only. */
489
+ masterCapability?: string;
490
+ /** Opaque direct-role epoch this effective host may adopt. */
491
+ masterAttestationEpoch?: string;
492
+ /** Stable master lineage identity retained across session switches/branches. */
493
+ masterOwnerSessionId?: string;
414
494
  /** Private session-owned terminal-abort capabilities; never exposed on ExtensionContext. */
415
495
  terminalAbortSeams?: SdkOnlyTerminalAbortSeams;
416
496
  /** Callback when a frame is admitted to the runtime (test harness). */
@@ -1117,18 +1197,15 @@ function createQuerySurface(
1117
1197
  kind: ctx.sessionMetadata?.kind ?? "main",
1118
1198
  });
1119
1199
  const lastAssistant = () => {
1120
- for (const entry of ctx.sessionManager.getBranch().toReversed()) {
1121
- if (entry.type !== "message" || entry.message.role !== "assistant") continue;
1122
- const content = entry.message.content;
1123
- if (typeof content === "string") return content;
1124
- if (Array.isArray(content))
1125
- return content
1126
- .filter(
1127
- (block): block is { type: "text"; text: string } =>
1128
- block.type === "text" && typeof block.text === "string",
1129
- )
1130
- .map(block => block.text)
1131
- .join("");
1200
+ const transcript =
1201
+ typeof (ctx as Partial<ExtensionContext>).getTranscript === "function" ? ctx.getTranscript() : [];
1202
+ for (const entry of transcript.toReversed()) {
1203
+ if (entry.role !== "assistant") continue;
1204
+ const text =
1205
+ typeof entry.body === "string"
1206
+ ? entry.body
1207
+ : entry.content?.flatMap(block => (block.type === "text" ? [block.text] : [])).join("\n");
1208
+ if (text !== undefined && text.trim().length > 0) return text;
1132
1209
  }
1133
1210
  return undefined;
1134
1211
  };
@@ -2269,7 +2346,7 @@ function createControlSurface(
2269
2346
  }
2270
2347
  };
2271
2348
  const terminalAbort = async (
2272
- input: { mode: "terminal"; scope?: "turn" | "owned" },
2349
+ input: { mode: "terminal"; scope?: "turn" | "owned"; operator?: boolean },
2273
2350
  idempotencyKey?: string,
2274
2351
  ): Promise<unknown> => {
2275
2352
  const scope = input.scope === "owned" ? "owned" : "turn";
@@ -2300,10 +2377,12 @@ function createControlSurface(
2300
2377
  typeof idempotencyKey === "string"
2301
2378
  ? crypto.createHash("sha256").update(idempotencyKey).digest("hex")
2302
2379
  : undefined;
2303
- const inputHash = crypto
2304
- .createHash("sha256")
2305
- .update(JSON.stringify({ mode: "terminal", scope }))
2306
- .digest("hex");
2380
+ const abortIdentity = terminalAbortIdentity(
2381
+ { mode: "terminal", scope, ...(input.operator === true ? { operator: true } : {}) },
2382
+ input.operator === true,
2383
+ );
2384
+ if (!abortIdentity) throw new Error("Terminal abort identity is invalid after dispatch validation.");
2385
+ const inputHash = abortIdentity.inputHash;
2307
2386
  const stored = (record: SdkOnlyTerminalScopeRecord | SdkOnlyEvictedTerminalKeyEntry) => ({
2308
2387
  responseState: record.responseState ?? "pending",
2309
2388
  responsePayloadHash: record.responsePayloadHash ?? inputHash,
@@ -2676,6 +2755,16 @@ function createControlSurface(
2676
2755
  pendingPreflights.delete(requesterBucketKey);
2677
2756
  }
2678
2757
  };
2758
+ const returnNoActiveTurn = async () => {
2759
+ const result = {
2760
+ ok: true,
2761
+ selection: scope,
2762
+ turn: "no_active_turn",
2763
+ terminal: "terminal_no_effect",
2764
+ };
2765
+ await finalizeNoEffectReservation(result);
2766
+ return result;
2767
+ };
2679
2768
  if (!handle || epoch === undefined) {
2680
2769
  if ((await writeNoEffect()) === "conflict") {
2681
2770
  throw Object.assign(new Error("Idempotency key was reused with different input."), {
@@ -2691,6 +2780,10 @@ function createControlSurface(
2691
2780
  if (replayed !== undefined) return replayed;
2692
2781
  }
2693
2782
  cancelRequesterPreflights();
2783
+ // Operator authority overrides connection ownership only for a turn
2784
+ // observed at admission. It must never adopt a successor that starts
2785
+ // while the durable no-effect reservation is being written.
2786
+ if (input.operator === true) return await returnNoActiveTurn();
2694
2787
  // A prompt for this requester may have become ACTIVE while the
2695
2788
  // reservation awaited the filesystem transaction: its submit()
2696
2789
  // cleanup already removed the preflight callback, so cancelling here
@@ -2706,14 +2799,7 @@ function createControlSurface(
2706
2799
  // No prompt won the race: finalize the reserved row so a later
2707
2800
  // same-key retry replays this deterministic no_active_turn result
2708
2801
  // (review thread P2).
2709
- const noActiveTurnResult = {
2710
- ok: true,
2711
- selection: scope,
2712
- turn: "no_active_turn",
2713
- terminal: "terminal_no_effect",
2714
- };
2715
- await finalizeNoEffectReservation(noActiveTurnResult);
2716
- return noActiveTurnResult;
2802
+ return await returnNoActiveTurn();
2717
2803
  }
2718
2804
  handle = recheckedHandle;
2719
2805
  epoch = recheckedEpoch;
@@ -2743,7 +2829,11 @@ function createControlSurface(
2743
2829
  // cleared after a terminal lifecycle boundary) authorizes no client, and
2744
2830
  // a stale prior owner authorizes only that old client — never a later
2745
2831
  // turn it did not submit (review thread P1).
2746
- if (handle && (abortingConnectionId === undefined || !currentOwnerConnectionIds().has(abortingConnectionId))) {
2832
+ if (
2833
+ input.operator !== true &&
2834
+ handle &&
2835
+ (abortingConnectionId === undefined || !currentOwnerConnectionIds().has(abortingConnectionId))
2836
+ ) {
2747
2837
  if ((await writeNoEffect()) === "conflict") {
2748
2838
  throw Object.assign(new Error("Idempotency key was reused with different input."), {
2749
2839
  code: "idempotency_conflict",
@@ -3412,6 +3502,107 @@ export function registerSdkOnlyNotificationCommand(api: ExtensionAPI): void {
3412
3502
  });
3413
3503
  }
3414
3504
 
3505
+ export function masterAttestationForEffectiveHost(input: {
3506
+ masterCapability: string | undefined;
3507
+ attestationEpoch: string | undefined;
3508
+ ownerSessionId?: string;
3509
+ sessionId: string;
3510
+ pid: number;
3511
+ processIncarnation: string | undefined;
3512
+ direct: MasterRoleAttestationV2 | undefined;
3513
+ }): MasterRoleAttestationV2 | undefined {
3514
+ const direct = input.direct;
3515
+ if (
3516
+ input.masterCapability === undefined ||
3517
+ input.attestationEpoch === undefined ||
3518
+ direct === undefined ||
3519
+ direct.ownerSessionId !== (input.ownerSessionId ?? input.sessionId) ||
3520
+ direct.launchPid !== input.pid ||
3521
+ direct.launchProcessIncarnation !== input.processIncarnation ||
3522
+ direct.attestationEpoch !== input.attestationEpoch
3523
+ )
3524
+ return undefined;
3525
+ return direct;
3526
+ }
3527
+
3528
+ function masterDirectAttestation(input: {
3529
+ masterCapability: string | undefined;
3530
+ attestationEpoch: string | undefined;
3531
+ ownerSessionId?: string;
3532
+ sessionId: string;
3533
+ pid: number;
3534
+ processIncarnation: string | undefined;
3535
+ }): MasterRoleAttestationV2 | undefined {
3536
+ if (
3537
+ input.masterCapability === undefined ||
3538
+ input.attestationEpoch === undefined ||
3539
+ input.processIncarnation === undefined
3540
+ )
3541
+ return undefined;
3542
+ return {
3543
+ version: 2,
3544
+ ownerSessionId: input.ownerSessionId ?? input.sessionId,
3545
+ launchPid: input.pid,
3546
+ launchProcessIncarnation: input.processIncarnation,
3547
+ role: "master",
3548
+ attestationEpoch: input.attestationEpoch,
3549
+ };
3550
+ }
3551
+
3552
+ function sameMasterAttestation(left: MasterRoleAttestationV2, right: MasterRoleAttestationV2): boolean {
3553
+ return (
3554
+ left.version === right.version &&
3555
+ left.ownerSessionId === right.ownerSessionId &&
3556
+ left.launchPid === right.launchPid &&
3557
+ left.launchProcessIncarnation === right.launchProcessIncarnation &&
3558
+ left.role === right.role &&
3559
+ left.attestationEpoch === right.attestationEpoch
3560
+ );
3561
+ }
3562
+
3563
+ /**
3564
+ * Establish direct master authority for the session currently hosted by this
3565
+ * process. Session identity transitions retain the master capability and epoch
3566
+ * but require a new direct attestation before their effective endpoint can
3567
+ * adopt that authority.
3568
+ */
3569
+ export async function reattestMasterSessionIdentity(input: {
3570
+ index: SessionIndex;
3571
+ locator: SessionLocatorV2;
3572
+ masterCapability: string | undefined;
3573
+ attestationEpoch: string | undefined;
3574
+ ownerSessionId?: string;
3575
+ sessionId: string;
3576
+ pid: number;
3577
+ processIncarnation: string | undefined;
3578
+ }): Promise<MasterRoleAttestationV2 | undefined> {
3579
+ const masterRole = masterDirectAttestation(input);
3580
+ if (!masterRole) return undefined;
3581
+ const directExists = input.index.listSessionIdentities().some(row => {
3582
+ const processIdentity = row.hostIncarnation ?? row.processIncarnation;
3583
+ return (
3584
+ row.sessionId === input.sessionId &&
3585
+ row.endpointGeneration === 0 &&
3586
+ row.pid === input.pid &&
3587
+ processIdentity === input.processIncarnation &&
3588
+ row.masterRole !== undefined &&
3589
+ sameMasterAttestation(row.masterRole, masterRole)
3590
+ );
3591
+ });
3592
+ if (!directExists) {
3593
+ await input.index.append({
3594
+ type: "host_registered",
3595
+ sessionId: input.sessionId,
3596
+ locator: input.locator,
3597
+ endpointGeneration: 0,
3598
+ pid: input.pid,
3599
+ processIncarnation: input.processIncarnation,
3600
+ masterRole,
3601
+ });
3602
+ }
3603
+ return masterRole;
3604
+ }
3605
+
3415
3606
  function quiescingFrame(frame: Record<string, unknown>): Record<string, unknown> | undefined {
3416
3607
  const error = { code: "session_quiescing", message: "The session endpoint is being replaced." };
3417
3608
  const id = typeof frame.id === "string" ? frame.id : "";
@@ -4172,6 +4363,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4172
4363
  await current.registerBroker();
4173
4364
  current.runtime.emitEvent({ type: "turn_start", sessionId: ctx.sessionManager.getSessionId() });
4174
4365
  });
4366
+ const consumedMasterNonces = new Map<string, number>();
4175
4367
  api.on("turn_end", (_event, ctx) =>
4176
4368
  lifecycleStateForContext(ctx, "agent_end")?.runtime.emitEvent({
4177
4369
  type: "turn_end",
@@ -4731,6 +4923,13 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4731
4923
  };
4732
4924
  runtime = new SessionSdkSessionRuntime({
4733
4925
  transport,
4926
+ masterCapabilityVerify: frame =>
4927
+ verifyMasterCapabilityFrame({
4928
+ frame,
4929
+ expectedCapability: options.masterCapability,
4930
+ expectedEpoch: options.masterAttestationEpoch,
4931
+ replay: consumedMasterNonces,
4932
+ }),
4734
4933
  onFrameAdmission: (_connectionId, frame) =>
4735
4934
  inputGate.quiescing ? quiescingFrame(frame as Record<string, unknown>) : undefined,
4736
4935
  control: async (connectionId, frame) => {
@@ -4776,21 +4975,13 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4776
4975
  (request.input as { mode?: unknown }).mode !== "terminal"
4777
4976
  )
4778
4977
  return;
4779
- const input = request.input as Record<string, unknown>;
4780
- const mode = input.mode;
4781
- const rawScope = input.scope;
4782
- if (mode !== "terminal") return;
4783
- if (rawScope !== undefined && rawScope !== "turn" && rawScope !== "owned") return;
4784
- // A malformed retry (e.g. {mode:"terminal", scope:"turn", extra:true})
4785
- // rejected by dispatch must never hash as the valid turn input and
4786
- // advance the legitimate stored row (review thread P2).
4787
- for (const key of Object.keys(input)) if (key !== "mode" && key !== "scope") return;
4788
- const scopeInput = rawScope === "owned" ? "owned" : "turn";
4978
+ const rawInput = request.input as Record<string, unknown>;
4979
+ const operatorAuthorized = hasBrokerRuntimeAbortCapability(rawInput);
4980
+ const { [BROKER_RUNTIME_ABORT_CAPABILITY_FIELD]: _capability, ...publicInput } = rawInput;
4981
+ const abortIdentity = terminalAbortIdentity(publicInput, operatorAuthorized);
4982
+ if (!abortIdentity) return;
4789
4983
  const keyHash = crypto.createHash("sha256").update(String(request.idempotencyKey)).digest("hex");
4790
- const inputHash = crypto
4791
- .createHash("sha256")
4792
- .update(JSON.stringify({ mode: "terminal", scope: scopeInput }))
4793
- .digest("hex");
4984
+ const inputHash = abortIdentity.inputHash;
4794
4985
  // Hash the ACTUAL written response payload: the durable state may only
4795
4986
  // advance when the written response corresponds to the row's payload.
4796
4987
  // When more than 256 concurrent requests evict an in-flight abort from
@@ -4846,21 +5037,72 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4846
5037
  const disposeGate = ctx.workflowGate?.onGateEmitted?.(gate =>
4847
5038
  runtime.emitEvent({ kind: "workflow_gate", payload: gate }),
4848
5039
  );
5040
+ let publishedEndpointUrl: string | undefined;
4849
5041
  let brokerRegistered = false;
4850
5042
  const registerBroker = async (): Promise<void> => {
4851
5043
  if (brokerRegistered) return;
4852
5044
  try {
5045
+ if (options.brokerRegistrationRequired && !options.lifecycleRequestId)
5046
+ throw new Error("Lifecycle broker registration requires a request identity.");
4853
5047
  await ensureBroker({ agentDir: options.agentDir });
4854
5048
  const index = await new SessionIndex(options.agentDir).open();
4855
- const locator = { repo: path.resolve(ctx.cwd), stateRoot };
5049
+ const locator = await resolveSessionLocator(ctx.cwd, stateRoot);
5050
+ const effectiveIncarnation = processIncarnation(process.pid);
5051
+ const direct = await reattestMasterSessionIdentity({
5052
+ index,
5053
+ locator,
5054
+ masterCapability: options.masterCapability,
5055
+ attestationEpoch: options.masterAttestationEpoch,
5056
+ ownerSessionId: options.masterOwnerSessionId,
5057
+ sessionId,
5058
+ pid: process.pid,
5059
+ processIncarnation: effectiveIncarnation,
5060
+ });
4856
5061
  await runtime.registerWithBroker({
4857
5062
  register: async input => {
4858
- const endpointMtimeMs = (await fs.stat(path.join(input.stateRoot, "sdk", `${input.sessionId}.json`)))
4859
- .mtimeMs;
4860
- await index.append({ type: "host_registered", ...input, locator, pid: process.pid, endpointMtimeMs });
5063
+ if (publishedEndpointUrl === undefined)
5064
+ throw new Error("SDK transport endpoint was not published before broker registration.");
5065
+ const endpointPath = path.join(input.stateRoot, "sdk", `${input.sessionId}.json`);
5066
+ const file = await readEndpointFile(endpointPath);
5067
+ if (!file) throw new Error("SDK endpoint could not be read as a stable regular file.");
5068
+ const endpoint = JSON.parse(file.source) as Record<string, unknown>;
5069
+ if (
5070
+ endpoint.sessionId !== input.sessionId ||
5071
+ endpoint.pid !== process.pid ||
5072
+ endpoint.url !== publishedEndpointUrl ||
5073
+ endpoint.token !== transport.token
5074
+ )
5075
+ throw new Error("SDK endpoint did not match the published transport authority.");
5076
+ const endpointMtimeMs = file.mtimeMs;
5077
+ const endpointFileId = `${file.dev}:${file.ino}`;
5078
+ const masterRole = masterAttestationForEffectiveHost({
5079
+ masterCapability: options.masterCapability,
5080
+ attestationEpoch: options.masterAttestationEpoch,
5081
+ ownerSessionId: options.masterOwnerSessionId,
5082
+ sessionId: input.sessionId,
5083
+ pid: process.pid,
5084
+ processIncarnation: effectiveIncarnation,
5085
+ direct,
5086
+ });
5087
+ await index.append({
5088
+ type: "host_registered",
5089
+ ...input,
5090
+ locator,
5091
+ pid: process.pid,
5092
+ endpointMtimeMs,
5093
+ endpointFileId,
5094
+ ...(options.lifecycleRequestId ? { lifecycleRequestId: options.lifecycleRequestId } : {}),
5095
+ ...(masterRole ? { masterRole } : {}),
5096
+ });
4861
5097
  },
4862
5098
  unregister: async input => {
4863
- await index.append({ type: "host_unregistered", ...input, locator, pid: process.pid });
5099
+ await index.append({
5100
+ type: "host_unregistered",
5101
+ ...input,
5102
+ locator,
5103
+ pid: process.pid,
5104
+ ...(options.lifecycleRequestId ? { lifecycleRequestId: options.lifecycleRequestId } : {}),
5105
+ });
4864
5106
  },
4865
5107
  });
4866
5108
  brokerRegistered = true;
@@ -4899,7 +5141,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4899
5141
  lifecycleOwnerHolder.state = runtimeOwner;
4900
5142
  active = runtimeOwner;
4901
5143
  try {
4902
- await runtime.start();
5144
+ publishedEndpointUrl = (await runtime.start()).url;
4903
5145
  await registerBroker();
4904
5146
  } catch (error) {
4905
5147
  active = undefined;
@@ -51,3 +51,31 @@ export class AgentDirSessionLifecycleClient implements SessionLifecycleClient {
51
51
  export function createBrokerSessionLifecycleService(agentDir: string): SessionLifecycleService {
52
52
  return new SessionLifecycleService(new AgentDirSessionLifecycleClient(agentDir));
53
53
  }
54
+
55
+ /**
56
+ * Dedicated dispatch for the local master `session.spawn` surface. It shares
57
+ * the lifecycle client's credential boundary but bypasses the generic mutation
58
+ * outcome types: spawn responses are validated by the caller's allowlist.
59
+ */
60
+ export async function dispatchSpawnGlobal(
61
+ agentDir: string,
62
+ input: Record<string, unknown>,
63
+ idempotencyKey: string,
64
+ timeoutMs: number,
65
+ ): Promise<unknown> {
66
+ await ensureBroker({ agentDir });
67
+ const discovery = await readSdkBrokerDiscovery(agentDir);
68
+ if (!discovery) throw new Error("SDK broker discovery is unavailable.");
69
+ const client = await SdkClient.connect(discovery.url, discovery.token, { timeoutMs });
70
+ try {
71
+ return await client.global("session.spawn", input, { idempotencyKey, timeoutMs });
72
+ } finally {
73
+ try {
74
+ await client.close();
75
+ } catch (error) {
76
+ logger.warn("SDK spawn client cleanup failed", {
77
+ error: error instanceof Error ? error.message : String(error),
78
+ });
79
+ }
80
+ }
81
+ }