@gajae-code/coding-agent 0.15.5 → 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 (272) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/args.d.ts +2 -0
  4. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  5. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  6. package/dist/types/cli/root-flags.d.ts +14 -0
  7. package/dist/types/cli/update-cli.d.ts +15 -0
  8. package/dist/types/cli.d.ts +7 -0
  9. package/dist/types/commands/auth-gateway.d.ts +3 -0
  10. package/dist/types/commands/gc.d.ts +13 -0
  11. package/dist/types/commands/launch.d.ts +10 -0
  12. package/dist/types/commands/model-presets.d.ts +21 -0
  13. package/dist/types/config/autorouting-tier-map.d.ts +120 -0
  14. package/dist/types/config/file-lock.d.ts +54 -2
  15. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  16. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  17. package/dist/types/config/keybindings.d.ts +3 -2
  18. package/dist/types/config/model-preset-registry.d.ts +368 -0
  19. package/dist/types/config/model-profile-activation.d.ts +39 -0
  20. package/dist/types/config/model-profiles.d.ts +5 -3
  21. package/dist/types/config/model-registry.d.ts +17 -5
  22. package/dist/types/config/settings-schema.d.ts +19 -1
  23. package/dist/types/config/settings.d.ts +2 -0
  24. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  25. package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
  26. package/dist/types/crash/record-loader.d.ts +15 -0
  27. package/dist/types/extensibility/extensions/types.d.ts +2 -5
  28. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  29. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  30. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  31. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  33. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -1
  34. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  36. package/dist/types/index.d.ts +1 -0
  37. package/dist/types/main.d.ts +3 -1
  38. package/dist/types/master-mode/context.d.ts +21 -0
  39. package/dist/types/master-mode/first-request.d.ts +34 -0
  40. package/dist/types/master-mode/snapshot.d.ts +32 -0
  41. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  42. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  43. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  44. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  45. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  46. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  47. package/dist/types/modes/jobs-observer.d.ts +36 -0
  48. package/dist/types/modes/print-mode.d.ts +17 -0
  49. package/dist/types/modes/ui-language.d.ts +7 -1
  50. package/dist/types/sdk/broker/broker.d.ts +80 -1
  51. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  52. package/dist/types/sdk/broker/index.d.ts +3 -0
  53. package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
  54. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  56. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  57. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  58. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  59. package/dist/types/sdk/broker/state-version.d.ts +4 -1
  60. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  61. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  62. package/dist/types/sdk/bus/index.d.ts +6 -0
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  65. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  66. package/dist/types/sdk/cli/index.d.ts +1 -0
  67. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  68. package/dist/types/sdk/cli/rows.d.ts +4 -3
  69. package/dist/types/sdk/cli/session-cli.d.ts +18 -2
  70. package/dist/types/sdk/client/client.d.ts +3 -2
  71. package/dist/types/sdk/client/discovery.d.ts +1 -0
  72. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  73. package/dist/types/sdk/host/control/index.d.ts +1 -1
  74. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  75. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  76. package/dist/types/sdk/host/host.d.ts +10 -0
  77. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  78. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +67 -1
  80. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  81. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  82. package/dist/types/sdk/lifecycle-session.d.ts +2 -0
  83. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  84. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  85. package/dist/types/sdk/router/session-router.d.ts +11 -0
  86. package/dist/types/sdk/session.d.ts +16 -0
  87. package/dist/types/sdk/startup-capability.d.ts +2 -1
  88. package/dist/types/session/account-inventory.d.ts +1 -0
  89. package/dist/types/session/agent-session.d.ts +24 -7
  90. package/dist/types/session/credential-store-errors.d.ts +1 -0
  91. package/dist/types/session/fold-coordinator.d.ts +201 -0
  92. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  93. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  94. package/dist/types/session/session-manager.d.ts +65 -0
  95. package/dist/types/session/yield-queue.d.ts +5 -4
  96. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  97. package/dist/types/telemetry/control.d.ts +11 -0
  98. package/dist/types/telemetry/events.d.ts +20 -0
  99. package/dist/types/telemetry/index.d.ts +3 -0
  100. package/dist/types/telemetry/transport.d.ts +22 -0
  101. package/dist/types/tools/bash-interactive.d.ts +23 -0
  102. package/dist/types/tools/bash.d.ts +4 -0
  103. package/dist/types/tools/gh.d.ts +8 -0
  104. package/dist/types/tools/index.d.ts +10 -5
  105. package/dist/types/web/search/providers/base.d.ts +9 -4
  106. package/dist/types/web/search/providers/xai.d.ts +2 -1
  107. package/dist/types/web/search/types.d.ts +14 -0
  108. package/package.json +10 -7
  109. package/scripts/generate-hotkeys-docs.ts +4 -0
  110. package/scripts/generate-sdk-operation-inventory.ts +9 -3
  111. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  112. package/src/async/job-manager.ts +671 -39
  113. package/src/cli/accounts-cli.ts +9 -5
  114. package/src/cli/args.ts +17 -0
  115. package/src/cli/auth-broker-cli.ts +40 -12
  116. package/src/cli/auth-gateway-cli.ts +273 -97
  117. package/src/cli/config-cli.ts +2 -2
  118. package/src/cli/fast-help.ts +4 -0
  119. package/src/cli/model-presets-cli.ts +100 -0
  120. package/src/cli/root-flags.ts +5 -0
  121. package/src/cli/update-cli.ts +117 -16
  122. package/src/cli.ts +6 -2
  123. package/src/commands/auth-gateway.ts +12 -8
  124. package/src/commands/gc.ts +19 -0
  125. package/src/commands/launch.ts +86 -18
  126. package/src/commands/model-presets.ts +24 -0
  127. package/src/commands/sdk.ts +113 -21
  128. package/src/config/autorouting-tier-map.ts +44 -0
  129. package/src/config/file-lock-gc.ts +9 -4
  130. package/src/config/file-lock.ts +890 -99
  131. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  132. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  133. package/src/config/keybindings.ts +6 -2
  134. package/src/config/model-preset-registry.ts +2795 -0
  135. package/src/config/model-profile-activation.ts +115 -28
  136. package/src/config/model-profile-contract.ts +4 -1
  137. package/src/config/model-profiles.ts +56 -14
  138. package/src/config/model-registry.ts +1044 -122
  139. package/src/config/model-resolver.ts +5 -0
  140. package/src/config/settings-schema.ts +19 -1
  141. package/src/config/settings.ts +11 -1
  142. package/src/coordinator-mcp/model-preset.ts +32 -17
  143. package/src/coordinator-mcp/projection-scan.ts +592 -0
  144. package/src/coordinator-mcp/question-state.ts +9 -11
  145. package/src/coordinator-mcp/server.ts +111 -42
  146. package/src/coordinator-mcp/session-reaper.ts +9 -3
  147. package/src/crash/index-store.ts +2 -0
  148. package/src/crash/record-loader.ts +10 -0
  149. package/src/extensibility/extensions/loader.ts +0 -1
  150. package/src/extensibility/extensions/types.ts +2 -3
  151. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  152. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  153. package/src/gjc-runtime/gc-render.ts +29 -0
  154. package/src/gjc-runtime/gc-runtime.ts +174 -11
  155. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  156. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  157. package/src/gjc-runtime/session-state-lock.ts +1321 -147
  158. package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
  159. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  160. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
  161. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  162. package/src/hooks/skill-state.ts +6 -0
  163. package/src/index.ts +1 -0
  164. package/src/internal-urls/docs-index.generated.ts +14 -13
  165. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  166. package/src/lsp/config.ts +4 -3
  167. package/src/lsp/path-trust.ts +12 -6
  168. package/src/main.ts +66 -4
  169. package/src/master-mode/context.ts +50 -0
  170. package/src/master-mode/first-request.ts +65 -0
  171. package/src/master-mode/snapshot.ts +205 -0
  172. package/src/modes/acp/acp-agent.ts +616 -161
  173. package/src/modes/components/footer.ts +4 -2
  174. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  175. package/src/modes/components/jobs-overlay-model.ts +99 -16
  176. package/src/modes/components/model-selector.ts +158 -18
  177. package/src/modes/components/oauth-selector.ts +6 -1
  178. package/src/modes/components/settings-selector.ts +1 -0
  179. package/src/modes/components/status-line/model-name.ts +53 -0
  180. package/src/modes/components/status-line/priority-row.ts +107 -0
  181. package/src/modes/components/status-line/segments.ts +29 -10
  182. package/src/modes/components/tool-status-header.ts +151 -15
  183. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  184. package/src/modes/controllers/input-controller.ts +51 -6
  185. package/src/modes/controllers/selector-controller.ts +191 -96
  186. package/src/modes/interactive-mode.ts +14 -1
  187. package/src/modes/jobs-observer.ts +149 -16
  188. package/src/modes/print-mode.ts +64 -0
  189. package/src/modes/runtime-init.ts +1 -1
  190. package/src/modes/ui-language.ts +49 -2
  191. package/src/modes/utils/injected-user-submission.ts +15 -1
  192. package/src/prompts/system/master-mode.md +28 -0
  193. package/src/prompts/tools/bash.md +2 -2
  194. package/src/prompts/tools/fold-receipt.md +3 -0
  195. package/src/prompts/tools/job.md +1 -1
  196. package/src/sdk/acp/adapter.ts +17 -2
  197. package/src/sdk/broker/broker.ts +1671 -34
  198. package/src/sdk/broker/endpoint-authority.ts +86 -0
  199. package/src/sdk/broker/ensure.ts +5 -0
  200. package/src/sdk/broker/index.ts +3 -0
  201. package/src/sdk/broker/lifecycle.ts +217 -46
  202. package/src/sdk/broker/master-capability.ts +232 -0
  203. package/src/sdk/broker/session-index.ts +121 -17
  204. package/src/sdk/broker/session-scope.ts +272 -0
  205. package/src/sdk/broker/spawn-authority.ts +1031 -0
  206. package/src/sdk/broker/spawn-substrate.ts +548 -0
  207. package/src/sdk/broker/state-version.ts +26 -14
  208. package/src/sdk/broker/transport.ts +1 -0
  209. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  210. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  211. package/src/sdk/bus/chat-daemon-runtime.ts +46 -5
  212. package/src/sdk/bus/index.ts +51 -4
  213. package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
  214. package/src/sdk/bus/prompt-reconciliation.ts +28 -4
  215. package/src/sdk/bus/slack-conversation.ts +5 -1
  216. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  217. package/src/sdk/cli/index.ts +1 -0
  218. package/src/sdk/cli/master-cli.ts +259 -0
  219. package/src/sdk/cli/rows.ts +11 -8
  220. package/src/sdk/cli/session-cli.ts +276 -6
  221. package/src/sdk/client/client.ts +14 -8
  222. package/src/sdk/client/discovery.ts +3 -1
  223. package/src/sdk/host/control/dispatch.ts +101 -37
  224. package/src/sdk/host/control/index.ts +2 -0
  225. package/src/sdk/host/control/operations.ts +2 -0
  226. package/src/sdk/host/control/runtime-gate.ts +203 -11
  227. package/src/sdk/host/host.ts +27 -2
  228. package/src/sdk/host/model-pin.ts +185 -25
  229. package/src/sdk/host/sdk-run-capability.ts +1 -0
  230. package/src/sdk/host/session-runtime.test.ts +581 -43
  231. package/src/sdk/host/session-runtime.ts +655 -101
  232. package/src/sdk/lifecycle/broker-client.ts +28 -0
  233. package/src/sdk/lifecycle/service.ts +279 -20
  234. package/src/sdk/lifecycle-session.ts +10 -2
  235. package/src/sdk/prompt-deadline-manager.ts +16 -1
  236. package/src/sdk/protocol/adapter-validation.ts +27 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +53 -2
  238. package/src/sdk/protocol/operation-registry.ts +17 -4
  239. package/src/sdk/router/session-router.ts +294 -154
  240. package/src/sdk/session.ts +440 -167
  241. package/src/sdk/startup-capability.ts +1 -0
  242. package/src/session/account-inventory.ts +27 -11
  243. package/src/session/agent-session.ts +1292 -180
  244. package/src/session/blob-store.ts +15 -5
  245. package/src/session/credential-store-errors.ts +2 -0
  246. package/src/session/fold-coordinator.ts +433 -0
  247. package/src/session/internal/managed-session-storage.ts +153 -78
  248. package/src/session/sdk-run-capability-internal.ts +15 -0
  249. package/src/session/sdk-run-capability.ts +2 -0
  250. package/src/session/session-manager.ts +342 -28
  251. package/src/session/yield-queue.ts +24 -10
  252. package/src/setup/frictionless-onboarding.ts +2 -0
  253. package/src/setup/provider-presets.json +1 -1
  254. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  255. package/src/task/executor.ts +19 -3
  256. package/src/telemetry/control.ts +26 -0
  257. package/src/telemetry/events.ts +1268 -0
  258. package/src/telemetry/index.ts +3 -0
  259. package/src/telemetry/transport.ts +87 -0
  260. package/src/tools/bash-interactive.ts +182 -85
  261. package/src/tools/bash.ts +1028 -109
  262. package/src/tools/gh.ts +104 -48
  263. package/src/tools/github-cache.ts +2 -2
  264. package/src/tools/index.ts +11 -5
  265. package/src/tools/tool-catalog.generated.ts +1 -1
  266. package/src/web/search/provider.ts +120 -11
  267. package/src/web/search/providers/anthropic.ts +89 -17
  268. package/src/web/search/providers/base.ts +9 -4
  269. package/src/web/search/providers/gemini.ts +87 -15
  270. package/src/web/search/providers/openai-compatible.ts +23 -8
  271. package/src/web/search/providers/xai.ts +98 -12
  272. package/src/web/search/types.ts +16 -0
@@ -3,9 +3,15 @@ import {
3
3
  DEFAULT_MODEL_SELECTION_RECOVERY_MESSAGE,
4
4
  parseDefaultModelSelectionRecovery,
5
5
  } from "../../../session/default-model-selection";
6
+ import { validateRequiredPromptText } from "../../protocol/adapter-validation";
6
7
  import { OPERATIONS, type Operation } from "../../protocol/operation-registry";
7
8
  import type { ControlInput, ControlSurface, ControlValue } from "./operations";
8
- import { brokerRuntimeCloseCapability, hasBrokerRuntimeCloseCapability } from "./runtime-gate";
9
+ import {
10
+ BROKER_RUNTIME_ABORT_CAPABILITY_FIELD,
11
+ brokerRuntimeCloseCapability,
12
+ hasBrokerRuntimeAbortCapability,
13
+ hasBrokerRuntimeCloseCapability,
14
+ } from "./runtime-gate";
9
15
 
10
16
  export interface ControlRequest {
11
17
  id: string;
@@ -148,29 +154,35 @@ function inputHash(input: unknown): string {
148
154
  .update(JSON.stringify(canonicalize(input)))
149
155
  .digest("hex");
150
156
  }
151
- /**
152
- * Normalize a WELL-FORMED terminal abort input for the idempotency hash:
153
- * omitted scope defaults to "turn", so `{mode:"terminal"}` and
154
- * `{mode:"terminal", scope:"turn"}` share one idempotency key (the durable
155
- * terminal-scope replay hashes the same normalized payload). Malformed
156
- * inputs (unknown fields, invalid mode/scope) are left raw so they are
157
- * rejected downstream and never collide with a valid input's key.
158
- */
159
- function normalizeTerminalAbortInputForHash(input: unknown): unknown {
160
- if (typeof input !== "object" || input === null) return input;
161
- const record = input as Record<string, unknown>;
162
- if (record.mode !== "terminal") return input;
163
- for (const key of Object.keys(record)) if (!TERMINAL_ABORT_FIELDS.has(key)) return input;
164
- const scope = record.scope;
165
- if (scope !== undefined && scope !== "turn" && scope !== "owned") return input;
166
- return { mode: "terminal", scope: scope === undefined ? "turn" : scope };
157
+
158
+ export interface TerminalAbortIdentity {
159
+ input: { mode: "terminal"; scope: "turn" | "owned"; operator?: true };
160
+ inputHash: string;
161
+ }
162
+
163
+ /** One strict, capability-free identity shared by dispatch, durable admission, and delivery observation. */
164
+ export function terminalAbortIdentity(input: unknown, operatorAuthorized: boolean): TerminalAbortIdentity | undefined {
165
+ if (!isInput(input) || input.mode !== "terminal") return undefined;
166
+ for (const key of Object.keys(input)) if (!TERMINAL_ABORT_FIELDS.has(key)) return undefined;
167
+ let scope: "turn" | "owned";
168
+ if (input.scope === undefined || input.scope === "turn") scope = "turn";
169
+ else if (input.scope === "owned") scope = "owned";
170
+ else return undefined;
171
+ if (input.operator !== undefined && input.operator !== true) return undefined;
172
+ if (input.operator === true && !operatorAuthorized) return undefined;
173
+ const normalized = {
174
+ mode: "terminal" as const,
175
+ scope,
176
+ ...(input.operator === true ? { operator: true as const } : {}),
177
+ };
178
+ return { input: normalized, inputHash: inputHash(normalized) };
167
179
  }
168
180
 
169
181
  function text(input: ControlInput, key = "text"): string {
170
182
  return input[key] as string;
171
183
  }
172
184
 
173
- const TERMINAL_ABORT_FIELDS = new Set(["mode", "scope"]);
185
+ const TERMINAL_ABORT_FIELDS = new Set(["mode", "scope", "operator"]);
174
186
 
175
187
  function invalidInput(message: string): never {
176
188
  throw new TypedControlError("invalid_input", message);
@@ -183,15 +195,23 @@ function invalidInput(message: string): never {
183
195
  * input is dropped and the ordinary argument-less `surface.abort()` runs.
184
196
  *
185
197
  * Terminal mode (`mode:"terminal"`) is validated strictly and side-effect-free
186
- * before any surface call: only `mode`/`scope` fields are accepted, `scope`
187
- * must be `"turn"` or `"owned"` (default `"turn"`), and a nonempty idempotency
188
- * key of at most 128 UTF-8 bytes is required on the request envelope. Terminal
189
- * semantics (see the approved plan): stop the root worker's current turn and
190
- * block only its own continuation routes; left-running owned work keeps
191
- * running and its completions are delivered normally so the root worker can
192
- * resume with a fresh attempt — owned delivery is NOT suppressed.
198
+ * before any surface call: only `mode`/`scope`/`operator` fields are accepted,
199
+ * `scope` must be `"turn"` or `"owned"` (default `"turn"`), and a nonempty
200
+ * idempotency key of at most 128 UTF-8 bytes is required on the request envelope.
201
+ * A cross-connection local operator must arrive through the Broker route with a
202
+ * private lifecycle capability, set `operator:true`, and confirm the destructive
203
+ * control request explicitly. Terminal semantics (see the approved plan) always
204
+ * stop the root worker's current turn. `scope:"turn"` leaves owned work running
205
+ * so its completion can resume the root worker; `scope:"owned"` additionally
206
+ * requires exact causal proof and stops that owned work. Operator authority
207
+ * changes only connection ownership, never those settlement proofs.
193
208
  */
194
- function invokeAbort(surface: ControlSurface, input: ControlInput, idempotencyKey: string | undefined): ControlValue {
209
+ function invokeAbort(
210
+ surface: ControlSurface,
211
+ input: ControlInput,
212
+ confirm: boolean | undefined,
213
+ idempotencyKey: string | undefined,
214
+ ): ControlValue {
195
215
  const mode = input.mode === undefined ? "turn" : input.mode;
196
216
  if (mode === "turn") return surface.abort();
197
217
  if (mode !== "terminal") invalidInput('turn.abort mode must be "turn" or "terminal".');
@@ -199,12 +219,18 @@ function invokeAbort(surface: ControlSurface, input: ControlInput, idempotencyKe
199
219
  if (!TERMINAL_ABORT_FIELDS.has(key)) invalidInput(`Unknown turn.abort terminal field: ${key}`);
200
220
  const scope = input.scope === undefined ? "turn" : input.scope;
201
221
  if (scope !== "turn" && scope !== "owned") invalidInput('turn.abort terminal scope must be "turn" or "owned".');
222
+ if (input.operator !== undefined && input.operator !== true)
223
+ invalidInput("turn.abort terminal operator must be true when provided.");
224
+ if (input.operator === true && confirm !== true) invalidInput("operator terminal abort requires confirm:true.");
202
225
  if (typeof idempotencyKey !== "string" || idempotencyKey.length === 0)
203
226
  invalidInput("terminal abort requires a nonempty idempotency key.");
204
227
  if (new TextEncoder().encode(idempotencyKey).length > 128)
205
228
  invalidInput("terminal abort idempotency key must be at most 128 UTF-8 bytes.");
206
229
  if (!surface.abortTerminal) invalidInput("terminal abort is not supported by this surface.");
207
- return surface.abortTerminal({ mode: "terminal", scope }, idempotencyKey);
230
+ return surface.abortTerminal(
231
+ { mode: "terminal", scope, ...(input.operator === true ? { operator: true } : {}) },
232
+ idempotencyKey,
233
+ );
208
234
  }
209
235
  function invoke(
210
236
  surface: ControlSurface,
@@ -221,7 +247,7 @@ function invoke(
221
247
  case "turn.follow_up":
222
248
  return surface.followUp(text(input));
223
249
  case "turn.abort":
224
- return invokeAbort(surface, input, idempotencyKey);
250
+ return invokeAbort(surface, input, confirm, idempotencyKey);
225
251
  case "turn.abort_and_prompt":
226
252
  return surface.abortAndPrompt(text(input));
227
253
  case "ask.answer":
@@ -403,11 +429,7 @@ function idempotent(
403
429
  const now = Date.now();
404
430
  for (const [key, entry] of requests) if (entry.expiresAt <= now) requests.delete(key);
405
431
  const key = `${row.sdkId}\u0000${request.idempotencyKey}`;
406
- // Terminal abort normalizes the omitted scope BEFORE hashing so the
407
- // defaulted and explicit shapes share one idempotency key (and reach the
408
- // durable terminal-scope replay on eviction); malformed inputs stay raw.
409
- const hashInput = row.sdkId === "turn.abort" ? normalizeTerminalAbortInputForHash(request.input) : request.input;
410
- const hash = inputHash(hashInput);
432
+ const hash = inputHash(request.input);
411
433
  const existing = requests.get(key);
412
434
  if (existing) {
413
435
  requests.delete(key);
@@ -444,6 +466,10 @@ export function dispatchControl(
444
466
  failure(request.id, "unknown_operation", `Unknown control operation: ${request.operation}.`),
445
467
  );
446
468
  const brokerCloseAuthorized = row.sdkId === "session.close" && hasBrokerRuntimeCloseCapability(request.input);
469
+ const brokerAbortInput = row.sdkId === "turn.abort" && isInput(request.input) ? request.input : undefined;
470
+ const brokerAbortFieldPresent =
471
+ brokerAbortInput !== undefined && Object.hasOwn(brokerAbortInput, BROKER_RUNTIME_ABORT_CAPABILITY_FIELD);
472
+ const brokerAbortAuthorized = row.sdkId === "turn.abort" && hasBrokerRuntimeAbortCapability(request.input);
447
473
  if (BROKER_LIFECYCLE_CONTROL_OPERATIONS.has(row.sdkId) && !brokerCloseAuthorized)
448
474
  return Promise.resolve(
449
475
  failure(
@@ -452,6 +478,20 @@ export function dispatchControl(
452
478
  `${request.operation} is available only through the Broker lifecycle service.`,
453
479
  ),
454
480
  );
481
+ if (!isInput(request.input))
482
+ return Promise.resolve(failure(request.id, "invalid_input", "Control input must be an object."));
483
+ if (
484
+ row.sdkId === "turn.abort" &&
485
+ ((brokerAbortFieldPresent && !brokerAbortAuthorized) ||
486
+ (request.input.operator === true && !brokerAbortAuthorized))
487
+ )
488
+ return Promise.resolve(
489
+ failure(
490
+ request.id,
491
+ "operation_prohibited",
492
+ "Operator terminal aborts are available only through the Broker control route.",
493
+ ),
494
+ );
455
495
  if (
456
496
  !brokerCloseAuthorized &&
457
497
  surface.installedOperations instanceof Set &&
@@ -460,18 +500,42 @@ export function dispatchControl(
460
500
  return Promise.resolve(
461
501
  failure(request.id, "operation_not_session_owned", `${request.operation} is not installed for this session.`),
462
502
  );
463
- if (!isInput(request.input))
464
- return Promise.resolve(failure(request.id, "invalid_input", "Control input must be an object."));
503
+ let dispatchRequest: ControlRequest = request;
504
+ if (row.sdkId === "turn.abort" && request.input.mode === "terminal") {
505
+ const publicInput = brokerAbortInput
506
+ ? (() => {
507
+ const { [BROKER_RUNTIME_ABORT_CAPABILITY_FIELD]: _capability, ...input } = brokerAbortInput;
508
+ return input;
509
+ })()
510
+ : request.input;
511
+ const identity = terminalAbortIdentity(publicInput, brokerAbortAuthorized);
512
+ if (!identity)
513
+ return Promise.resolve(failure(request.id, "invalid_input", "Terminal turn.abort input is malformed."));
514
+ if (identity.input.operator === true && request.confirm !== true)
515
+ return Promise.resolve(failure(request.id, "invalid_input", "operator terminal abort requires confirm:true."));
516
+ if (typeof request.idempotencyKey !== "string" || request.idempotencyKey.length === 0)
517
+ return Promise.resolve(
518
+ failure(request.id, "invalid_input", "terminal abort requires a nonempty idempotency key."),
519
+ );
520
+ if (new TextEncoder().encode(request.idempotencyKey).length > 128)
521
+ return Promise.resolve(
522
+ failure(request.id, "invalid_input", "terminal abort idempotency key must be at most 128 UTF-8 bytes."),
523
+ );
524
+ dispatchRequest = { ...request, input: identity.input };
525
+ }
526
+ const promptError = validateRequiredPromptText(row.sdkId, dispatchRequest.input as ControlInput);
527
+ if (promptError) return Promise.resolve(failure(request.id, promptError.code, promptError.message));
465
528
  if ((row.sdkId === "context.clear" || row.sdkId === "session.delete") && request.confirm !== true)
466
529
  return Promise.resolve(
467
530
  failure(request.id, "invalid_input", "confirm: true is required for this destructive operation."),
468
531
  );
469
- const work = () => execute(surface, row, request);
532
+ const work = () => execute(surface, row, dispatchRequest);
470
533
  if (row.sdkId === "turn.abort_and_prompt") {
471
534
  const cancellable = surface as PreflightCancellableSurface;
472
535
  if (Object.hasOwn(cancellable, "cancelPendingPreflights")) cancellable.cancelPendingPreflights?.();
473
536
  return serialize(surface, work);
474
537
  }
475
- if (row.idempotency === "idempotent" && request.idempotencyKey) return idempotent(surface, row, request, work);
538
+ if (row.idempotency === "idempotent" && dispatchRequest.idempotencyKey)
539
+ return idempotent(surface, row, dispatchRequest, work);
476
540
  return row.idempotency === "ordered" && row.sdkId !== "retry.now" ? serialize(surface, work) : work();
477
541
  }
@@ -6,6 +6,8 @@ export {
6
6
  type ControlResponse,
7
7
  controlRequestFromFrame,
8
8
  dispatchControl,
9
+ type TerminalAbortIdentity,
9
10
  TypedControlError,
11
+ terminalAbortIdentity,
10
12
  } from "./dispatch";
11
13
  export type { AbortScope, ControlInput, ControlSurface, ControlValue } from "./operations";
@@ -11,6 +11,8 @@ export type AbortScope = "turn" | "owned";
11
11
  export interface TerminalAbortInput {
12
12
  mode: "terminal";
13
13
  scope?: AbortScope;
14
+ /** Broker-authorized local override; public SDK callers cannot grant this authority. */
15
+ operator?: boolean;
14
16
  }
15
17
  export type ControlInput = Record<string, unknown>;
16
18
 
@@ -1,13 +1,19 @@
1
+ import { findOperation } from "../../protocol/operation-registry";
2
+
1
3
  /**
2
- * Private field carried only on the Broker's endpoint close request.
4
+ * Private fields carried only on Broker-authorized endpoint controls.
3
5
  *
4
- * The value is the lifecycle effect marker bound to the serving process. It is
5
- * never published in endpoint discovery or lifecycle service results; callers
6
- * that do not hold the Broker's indexed lifecycle record cannot satisfy this
7
- * gate.
6
+ * Their values are lifecycle effect markers bound to the serving process. They
7
+ * are never published in endpoint discovery or lifecycle service results;
8
+ * callers that do not hold the Broker's indexed lifecycle record cannot satisfy
9
+ * either gate.
8
10
  */
9
11
  export const BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD = "__gjcBrokerCloseCapability";
12
+ /** Private field carried only on the Broker-authorized terminal-abort request. */
13
+ export const BROKER_RUNTIME_ABORT_CAPABILITY_FIELD = "__gjcBrokerAbortCapability";
10
14
  const EXPECTED_BROKER_RUNTIME_CLOSE_CAPABILITY = process.env.GJC_LIFECYCLE_REQUEST_ID;
15
+ const EXPECTED_BROKER_RUNTIME_ABORT_CAPABILITY = process.env.GJC_LIFECYCLE_REQUEST_ID;
16
+ let brokerRuntimeAbortCapabilityOverrideForTest: string | undefined;
11
17
 
12
18
  function record(value: unknown): Record<string, unknown> | undefined {
13
19
  return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
@@ -18,13 +24,188 @@ export function brokerRuntimeCloseCapability(input: unknown): string | undefined
18
24
  return typeof value === "string" && value.length > 0 ? value : undefined;
19
25
  }
20
26
 
21
- /** Remove the private close capability before a request is exposed to diagnostics. */
22
- export function redactBrokerRuntimeCloseCapability(frame: Record<string, unknown>): Record<string, unknown> {
23
- if (frame.type !== "control_request" || frame.operation !== "session.close") return frame;
27
+ export function brokerRuntimeAbortCapability(input: unknown): string | undefined {
28
+ const value = record(input)?.[BROKER_RUNTIME_ABORT_CAPABILITY_FIELD];
29
+ return typeof value === "string" && value.length > 0 ? value : undefined;
30
+ }
31
+
32
+ /** Remove private Broker capabilities before a request is exposed to diagnostics. */
33
+ export function redactBrokerRuntimeCapabilities(frame: Record<string, unknown>): Record<string, unknown> {
34
+ if (frame.type !== "control_request") return frame;
24
35
  const input = record(frame.input);
25
- if (!input || !Object.hasOwn(input, BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD)) return frame;
26
- const { [BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD]: _capability, ...publicInput } = input;
27
- return { ...frame, input: publicInput };
36
+ if (!input) return frame;
37
+ let redacted = false;
38
+ const publicInput: Record<string, unknown> = {};
39
+ for (const [key, value] of Object.entries(input)) {
40
+ if (key === BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD || key === BROKER_RUNTIME_ABORT_CAPABILITY_FIELD) {
41
+ redacted = true;
42
+ continue;
43
+ }
44
+ publicInput[key] = value;
45
+ }
46
+ return redacted ? { ...frame, input: publicInput } : frame;
47
+ }
48
+
49
+ /**
50
+ * Frame-level keys an observer may see. `SdkFrame` is an open
51
+ * `Record<string, unknown>` and `#observeRequest` runs BEFORE dispatch
52
+ * validation, so an adversarial or malformed frame can carry content under any
53
+ * top-level key. Only structural routing values survive. `idempotencyKey`,
54
+ * `id`, and other correlation values are caller-chosen and are deliberately
55
+ * absent; so are `cursor` (a signed pagination token that can carry
56
+ * session/revision data) and `expectedRevision` (no diagnostic consumer reads
57
+ * it). Even allowlisted routing names are checked against their protocol
58
+ * values below: a field-name allowlist alone is fail-open for scalar secrets.
59
+ */
60
+ const OBSERVABLE_FRAME_FIELDS = new Set(["type", "operation", "query"]);
61
+
62
+ /**
63
+ * Frame keys whose value must match an exact expected type to survive.
64
+ * `confirm` is a structural destructive-op gate, so only the boolean form is
65
+ * routing information; a caller-authored string under that name is content.
66
+ */
67
+ const OBSERVABLE_TYPED_FRAME_FIELDS: Readonly<Record<string, "boolean">> = { confirm: "boolean" };
68
+
69
+ const OBSERVABLE_FRAME_LITERAL_FIELDS: Readonly<Record<string, ReadonlySet<string>>> = {
70
+ type: new Set(["control_request", "query_request"]),
71
+ };
72
+
73
+ const OBSERVER_MARKER_MAX_COUNT = 4096;
74
+
75
+ /**
76
+ * Input fields an observer may see. This is an ALLOWLIST on purpose: a
77
+ * denylist is fail-open, so every unlisted or newly added field would leak by
78
+ * default. Only structural routing/correlation values are preserved; all other
79
+ * values are caller content and are replaced with a shape marker.
80
+ */
81
+ const OBSERVABLE_INPUT_FIELDS = new Set(["mode", "scope", "kind", "level", "on", "confirm"]);
82
+
83
+ /**
84
+ * Input fields that carry a finite protocol value. Correlation identifiers,
85
+ * names, and operation arguments remain redacted even when they look like
86
+ * ordinary identifiers: callers are free to put arbitrary content in them.
87
+ */
88
+ const OBSERVABLE_INPUT_LITERAL_FIELDS: Readonly<Record<string, ReadonlySet<string>>> = {
89
+ mode: new Set([
90
+ "turn",
91
+ "terminal",
92
+ "owned",
93
+ "allow",
94
+ "always-allow",
95
+ "deny",
96
+ "always-deny",
97
+ "prompt",
98
+ "all",
99
+ "one-at-a-time",
100
+ "immediate",
101
+ "wait",
102
+ ]),
103
+ scope: new Set(["turn", "owned"]),
104
+ kind: new Set(["prompt", "skill"]),
105
+ level: new Set(["inherit", "off", "minimal", "low", "medium", "high", "xhigh", "max"]),
106
+ };
107
+
108
+ const OBSERVABLE_INPUT_TYPED_FIELDS: Readonly<Record<string, "boolean">> = {
109
+ on: "boolean",
110
+ confirm: "boolean",
111
+ };
112
+
113
+ /** Replaces caller content with a shape-preserving marker for observers. */
114
+ function redactedContentMarker(value: unknown): string {
115
+ if (typeof value === "string") {
116
+ const count = Math.min(value.length, OBSERVER_MARKER_MAX_COUNT);
117
+ return `[redacted ${count}${value.length > count ? "+" : ""} chars]`;
118
+ }
119
+ if (Array.isArray(value)) {
120
+ const count = Math.min(value.length, OBSERVER_MARKER_MAX_COUNT);
121
+ return `[redacted ${count}${value.length > count ? "+" : ""} items]`;
122
+ }
123
+ if (value !== null && typeof value === "object") {
124
+ const fields = Object.keys(value).length;
125
+ const count = Math.min(fields, OBSERVER_MARKER_MAX_COUNT);
126
+ return `[redacted ${count}${fields > count ? "+" : ""} fields]`;
127
+ }
128
+ return "[redacted]";
129
+ }
130
+
131
+ function setObservedField(target: Record<string, unknown>, key: string, value: unknown): void {
132
+ Object.defineProperty(target, key, { configurable: true, enumerable: true, writable: true, value });
133
+ }
134
+
135
+ function observableFrameValue(key: string, value: unknown): unknown {
136
+ const requiredType = OBSERVABLE_TYPED_FRAME_FIELDS[key];
137
+ if (requiredType !== undefined) return typeof value === requiredType ? value : redactedContentMarker(value);
138
+ if (key === "operation" || key === "query") {
139
+ const known =
140
+ findOperation("control", String(value)) ??
141
+ findOperation("global", String(value)) ??
142
+ findOperation("query", String(value)) ??
143
+ findOperation("reverse", String(value));
144
+ return typeof value === "string" && value.length <= 128 && known !== undefined
145
+ ? value
146
+ : redactedContentMarker(value);
147
+ }
148
+ const allowed = OBSERVABLE_FRAME_LITERAL_FIELDS[key];
149
+ return allowed?.has(typeof value === "string" ? value : "") ? value : redactedContentMarker(value);
150
+ }
151
+
152
+ /**
153
+ * Strips caller content from a request frame before it reaches a diagnostic
154
+ * observer. Known protocol operation names and finite enum values survive so
155
+ * instrumentation stays useful; caller-chosen ids and all other values are
156
+ * redacted, including nested objects, so a field this module has never heard
157
+ * of cannot leak.
158
+ */
159
+ export function redactObservedRequestContent(frame: Record<string, unknown>): Record<string, unknown> {
160
+ const observed: Record<string, unknown> = {};
161
+ for (const [key, value] of Object.entries(frame)) {
162
+ if (value === undefined) continue;
163
+ if (key === "input") {
164
+ const input = record(value);
165
+ // A non-record payload is content, not "nothing to redact". Returning
166
+ // early here would hand a string or array `input` through verbatim.
167
+ setObservedField(
168
+ observed,
169
+ key,
170
+ input === undefined ? redactedContentMarker(value) : redactObservedInput(input),
171
+ );
172
+ continue;
173
+ }
174
+ // Structural routing scalars survive; every other top-level key, including
175
+ // one this module has never heard of, is redacted.
176
+ setObservedField(
177
+ observed,
178
+ key,
179
+ OBSERVABLE_FRAME_FIELDS.has(key) || Object.hasOwn(OBSERVABLE_TYPED_FRAME_FIELDS, key)
180
+ ? observableFrameValue(key, value)
181
+ : redactedContentMarker(value),
182
+ );
183
+ }
184
+ return observed;
185
+ }
186
+
187
+ /** Redacts one input payload, preserving only structural scalar routing fields. */
188
+ function redactObservedInput(input: Record<string, unknown>): Record<string, unknown> {
189
+ const redacted: Record<string, unknown> = {};
190
+ for (const [field, value] of Object.entries(input)) {
191
+ if (value === undefined) continue;
192
+ const requiredType = OBSERVABLE_INPUT_TYPED_FIELDS[field];
193
+ if (requiredType !== undefined) {
194
+ setObservedField(redacted, field, typeof value === requiredType ? value : redactedContentMarker(value));
195
+ continue;
196
+ }
197
+ const allowed = OBSERVABLE_INPUT_LITERAL_FIELDS[field];
198
+ // A finite protocol value is safe; every caller-chosen scalar, including
199
+ // an allowlisted field with a forged string, is content and is redacted.
200
+ setObservedField(
201
+ redacted,
202
+ field,
203
+ OBSERVABLE_INPUT_FIELDS.has(field) && allowed?.has(typeof value === "string" ? value : "")
204
+ ? value
205
+ : redactedContentMarker(value),
206
+ );
207
+ }
208
+ return redacted;
28
209
  }
29
210
 
30
211
  /**
@@ -38,3 +219,14 @@ export function hasBrokerRuntimeCloseCapability(input: unknown): boolean {
38
219
  const actual = brokerRuntimeCloseCapability(input);
39
220
  return typeof expected === "string" && expected.length > 0 && actual === expected;
40
221
  }
222
+
223
+ /** Test-only override for the abort gate; production authority remains process-local. */
224
+ export function setBrokerRuntimeAbortCapabilityForTest(capability: string | undefined): void {
225
+ brokerRuntimeAbortCapabilityOverrideForTest = capability;
226
+ }
227
+
228
+ export function hasBrokerRuntimeAbortCapability(input: unknown): boolean {
229
+ const expected = brokerRuntimeAbortCapabilityOverrideForTest ?? EXPECTED_BROKER_RUNTIME_ABORT_CAPABILITY;
230
+ const actual = brokerRuntimeAbortCapability(input);
231
+ return typeof expected === "string" && expected.length > 0 && actual === expected;
232
+ }
@@ -1,6 +1,6 @@
1
1
  import { logger } from "@gajae-code/utils";
2
2
  import { AUTOROUTING_INACTIVE_WARNING } from "../../config/autorouting-contract";
3
- import { redactBrokerRuntimeCloseCapability } from "./control/runtime-gate";
3
+ import { redactBrokerRuntimeCapabilities, redactObservedRequestContent } from "./control/runtime-gate";
4
4
  import { type EventFrame, SessionEventStream } from "./events";
5
5
  import { isAutoroutingInactive } from "./internal-autorouting-state";
6
6
  import { type ProviderLease, ReverseLeaseError, ReverseLeaseRuntime } from "./reverse-leases";
@@ -88,6 +88,12 @@ export interface SessionSdkHostOptions extends HostEndpointAdapters {
88
88
  * that fails is never read as authorization.
89
89
  */
90
90
  activationGate?: SessionActivationGate;
91
+ /** Private one-shot master-capability verification; never durable or public SDK surface. */
92
+ masterCapabilityVerify?: (frame: { nonce: unknown; attestationEpoch: unknown; capability: unknown }) => {
93
+ ok: boolean;
94
+ nonce: string;
95
+ attestationEpoch: string;
96
+ };
91
97
  }
92
98
 
93
99
  /** Shared by the replay filter and transport live broadcasts: a connection
@@ -416,6 +422,21 @@ export class SessionSdkHost {
416
422
  return;
417
423
  }
418
424
  switch (frame.type) {
425
+ case "master_capability_verify": {
426
+ const result = this.#options.masterCapabilityVerify?.({
427
+ nonce: frame.nonce,
428
+ attestationEpoch: frame.attestationEpoch,
429
+ capability: frame.capability,
430
+ }) ?? { ok: false, nonce: "", attestationEpoch: "" };
431
+ await this.#send(connectionId, {
432
+ type: "master_capability_verify_result",
433
+ id: typeof frame.id === "string" ? frame.id : "",
434
+ ok: result.ok,
435
+ nonce: result.nonce,
436
+ attestationEpoch: result.attestationEpoch,
437
+ });
438
+ break;
439
+ }
419
440
  case "control_request": {
420
441
  this.#observeRequest("control", connectionId, frame);
421
442
  // Deferred readiness withholds `session_ready`, but the control
@@ -616,7 +637,11 @@ export class SessionSdkHost {
616
637
  }
617
638
  #observeRequest(kind: "control" | "query", connectionId: string, frame: SdkFrame): void {
618
639
  try {
619
- this.#options.onRequest?.(kind, connectionId, redactBrokerRuntimeCloseCapability(frame));
640
+ this.#options.onRequest?.(
641
+ kind,
642
+ connectionId,
643
+ redactObservedRequestContent(redactBrokerRuntimeCapabilities(frame)),
644
+ );
620
645
  } catch {
621
646
  // Diagnostic observers must not change request handling.
622
647
  }