@gajae-code/coding-agent 0.12.21 → 0.13.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 (222) hide show
  1. package/CHANGELOG.md +67 -2
  2. package/dist/types/cli/args.d.ts +5 -2
  3. package/dist/types/cli/root-flags.d.ts +246 -0
  4. package/dist/types/cli.d.ts +11 -19
  5. package/dist/types/commands/daemon.d.ts +0 -30
  6. package/dist/types/commands/launch.d.ts +11 -19
  7. package/dist/types/commands/quick-lane.d.ts +32 -0
  8. package/dist/types/commands/sdk.d.ts +21 -15
  9. package/dist/types/config/file-lock-gc.d.ts +2 -0
  10. package/dist/types/config/model-equivalence.d.ts +20 -0
  11. package/dist/types/config/model-profile-activation.d.ts +62 -17
  12. package/dist/types/config/model-profiles.d.ts +10 -0
  13. package/dist/types/config/model-registry.d.ts +68 -1
  14. package/dist/types/config/model-resolver.d.ts +15 -4
  15. package/dist/types/config/models-config-schema.d.ts +7 -2
  16. package/dist/types/config/provider-selection-policy.d.ts +62 -0
  17. package/dist/types/config/settings-schema.d.ts +36 -78
  18. package/dist/types/config/settings.d.ts +2 -0
  19. package/dist/types/edit/renderer.d.ts +5 -2
  20. package/dist/types/edit/streaming.d.ts +13 -0
  21. package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
  22. package/dist/types/extensibility/extensions/compact-handler.d.ts +2 -1
  23. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  24. package/dist/types/gjc-runtime/gc-runtime.d.ts +19 -5
  25. package/dist/types/internal-urls/local-protocol.d.ts +1 -0
  26. package/dist/types/modes/components/model-selector.d.ts +0 -2
  27. package/dist/types/modes/components/provider-order-context.d.ts +45 -0
  28. package/dist/types/modes/components/provider-order-editor.d.ts +24 -0
  29. package/dist/types/modes/components/settings-defs.d.ts +5 -1
  30. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  31. package/dist/types/quick-lane/classify.d.ts +40 -0
  32. package/dist/types/runtime/stt-settings-setup.d.ts +18 -0
  33. package/dist/types/sdk/broker/broker.d.ts +56 -2
  34. package/dist/types/sdk/broker/operation-receipt-ledger.d.ts +25 -0
  35. package/dist/types/sdk/broker/session-index.d.ts +83 -2
  36. package/dist/types/sdk/broker/state-version.d.ts +1 -1
  37. package/dist/types/sdk/bus/notification-service.d.ts +6 -0
  38. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +8 -1
  39. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +26 -0
  41. package/dist/types/sdk/cli/rows.d.ts +98 -0
  42. package/dist/types/sdk/cli/session-cli.d.ts +35 -3
  43. package/dist/types/sdk/elevation/allowlist.d.ts +20 -0
  44. package/dist/types/sdk/elevation/audit.d.ts +93 -0
  45. package/dist/types/sdk/elevation/capability.d.ts +3 -0
  46. package/dist/types/sdk/elevation/digest.d.ts +72 -0
  47. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +85 -0
  48. package/dist/types/sdk/elevation/grant-ledger.d.ts +163 -0
  49. package/dist/types/sdk/elevation/owner.d.ts +48 -0
  50. package/dist/types/sdk/guides/cache.d.ts +98 -0
  51. package/dist/types/sdk/guides/catalog.d.ts +156 -0
  52. package/dist/types/sdk/guides/cli.d.ts +39 -0
  53. package/dist/types/sdk/guides/manifest.d.ts +66 -0
  54. package/dist/types/sdk/guides/verify.d.ts +66 -0
  55. package/dist/types/sdk/host/control/dispatch.d.ts +8 -0
  56. package/dist/types/sdk/host/control/index.d.ts +1 -1
  57. package/dist/types/sdk/host/control/operations.d.ts +3 -2
  58. package/dist/types/sdk/host/query/cursor.d.ts +10 -0
  59. package/dist/types/sdk/host/query/handlers.d.ts +21 -0
  60. package/dist/types/sdk/host/session-runtime.d.ts +9 -0
  61. package/dist/types/sdk/mcp/server.d.ts +2 -2
  62. package/dist/types/sdk/prompt-status.d.ts +11 -0
  63. package/dist/types/sdk/session.d.ts +4 -0
  64. package/dist/types/sdk/transport/index.d.ts +1 -0
  65. package/dist/types/sdk/transport/relay.d.ts +1 -0
  66. package/dist/types/sdk/transport/serve-cli.d.ts +20 -1
  67. package/dist/types/session/agent-session.d.ts +32 -18
  68. package/dist/types/session/blob-store.d.ts +13 -7
  69. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  70. package/dist/types/session/internal/managed-session-storage.d.ts +25 -1
  71. package/dist/types/session/internal/session-memory-sidecar.d.ts +5 -5
  72. package/dist/types/session/session-manager.d.ts +139 -10
  73. package/dist/types/session/session-storage.d.ts +48 -0
  74. package/dist/types/setup/provider-onboarding.d.ts +7 -1
  75. package/dist/types/slash-commands/builtin-registry.d.ts +14 -0
  76. package/dist/types/stt/setup.d.ts +2 -0
  77. package/dist/types/task/executor.d.ts +10 -2
  78. package/dist/types/tools/browser/runtime-diagnostics.d.ts +65 -0
  79. package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
  80. package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
  81. package/dist/types/tools/browser/tab-worker.d.ts +2 -0
  82. package/dist/types/tools/browser.d.ts +2 -0
  83. package/dist/types/tools/image-gen.d.ts +14 -18
  84. package/dist/types/tools/index.d.ts +2 -0
  85. package/dist/types/tools/resource-gc.d.ts +2 -0
  86. package/dist/types/utils/pasted-image-path.d.ts +6 -0
  87. package/package.json +9 -9
  88. package/scripts/generate-sdk-operation-inventory.ts +8 -0
  89. package/scripts/run-test-manifest.ts +61 -21
  90. package/scripts/verify-gjc-sdk-canonicalization.ts +9 -4
  91. package/src/cli/args.ts +62 -9
  92. package/src/cli/completion-cli.ts +1 -0
  93. package/src/cli/fast-help.ts +1 -0
  94. package/src/cli/root-flags.ts +100 -0
  95. package/src/cli/setup-cli.ts +6 -2
  96. package/src/cli.ts +42 -65
  97. package/src/commands/acp.ts +1 -1
  98. package/src/commands/daemon.ts +2 -31
  99. package/src/commands/gc.ts +1 -1
  100. package/src/commands/launch.ts +8 -129
  101. package/src/commands/quick-lane.ts +51 -0
  102. package/src/commands/sdk.ts +200 -26
  103. package/src/commands/setup.ts +4 -2
  104. package/src/config/file-lock-gc.ts +4 -1
  105. package/src/config/model-equivalence.ts +76 -7
  106. package/src/config/model-profile-activation.ts +1115 -265
  107. package/src/config/model-profiles.ts +134 -2
  108. package/src/config/model-registry.ts +352 -50
  109. package/src/config/model-resolver.ts +289 -38
  110. package/src/config/models-config-schema.ts +11 -4
  111. package/src/config/provider-selection-policy.ts +146 -0
  112. package/src/config/settings-schema.ts +50 -77
  113. package/src/config/settings.ts +40 -0
  114. package/src/coordinator-mcp/server.ts +32 -2
  115. package/src/edit/renderer.ts +175 -223
  116. package/src/edit/streaming.ts +108 -1
  117. package/src/export/html/index.ts +57 -7
  118. package/src/extensibility/custom-tools/types.ts +2 -0
  119. package/src/extensibility/extensions/compact-handler.ts +3 -2
  120. package/src/extensibility/extensions/runner.ts +6 -0
  121. package/src/extensibility/extensions/types.ts +3 -0
  122. package/src/gjc-runtime/gc-runtime.ts +379 -20
  123. package/src/gjc-runtime/launch-worktree.ts +5 -1
  124. package/src/gjc-runtime/team-runtime.ts +59 -2
  125. package/src/gjc-runtime/tmux-owner-isolation.ts +19 -0
  126. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +16 -1
  127. package/src/internal-urls/docs-index.generated.ts +17 -13
  128. package/src/internal-urls/local-protocol.ts +1 -0
  129. package/src/main.ts +40 -3
  130. package/src/memories/index.ts +4 -2
  131. package/src/modes/acp/acp-agent.ts +235 -91
  132. package/src/modes/components/model-selector.ts +206 -42
  133. package/src/modes/components/provider-order-context.ts +197 -0
  134. package/src/modes/components/provider-order-editor.ts +441 -0
  135. package/src/modes/components/settings-defs.ts +21 -1
  136. package/src/modes/components/settings-selector.ts +45 -2
  137. package/src/modes/components/tool-execution.ts +47 -15
  138. package/src/modes/controllers/command-controller.ts +6 -3
  139. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  140. package/src/modes/controllers/input-controller.ts +73 -2
  141. package/src/modes/controllers/selector-controller.ts +260 -153
  142. package/src/modes/prompt-suggestion-controller.ts +1 -1
  143. package/src/modes/runtime-init.ts +1 -0
  144. package/src/modes/utils/ui-helpers.ts +18 -0
  145. package/src/prompts/tools/browser.md +1 -0
  146. package/src/quick-lane/classify.ts +257 -0
  147. package/src/rlm/index.ts +1 -1
  148. package/src/runtime/stt-settings-setup.ts +57 -0
  149. package/src/sdk/acp/adapter.ts +2 -1
  150. package/src/sdk/broker/broker.ts +780 -12
  151. package/src/sdk/broker/lifecycle.ts +70 -29
  152. package/src/sdk/broker/operation-receipt-ledger.ts +144 -0
  153. package/src/sdk/broker/session-index.ts +463 -48
  154. package/src/sdk/broker/state-version.ts +13 -9
  155. package/src/sdk/broker/transport.ts +14 -1
  156. package/src/sdk/bus/index.ts +34 -13
  157. package/src/sdk/bus/kind-aware-reconciliation.ts +21 -2
  158. package/src/sdk/bus/notification-service.ts +47 -1
  159. package/src/sdk/bus/prompt-reconciliation.ts +22 -3
  160. package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
  161. package/src/sdk/bus/telegram-daemon.ts +54 -3
  162. package/src/sdk/cli/rows.ts +247 -0
  163. package/src/sdk/cli/session-cli.ts +930 -79
  164. package/src/sdk/elevation/allowlist.ts +54 -0
  165. package/src/sdk/elevation/audit.ts +208 -0
  166. package/src/sdk/elevation/capability.ts +41 -0
  167. package/src/sdk/elevation/digest.ts +129 -0
  168. package/src/sdk/elevation/dispatch-receipt.ts +184 -0
  169. package/src/sdk/elevation/grant-ledger.ts +1142 -0
  170. package/src/sdk/elevation/owner.ts +109 -0
  171. package/src/sdk/guides/cache.ts +363 -0
  172. package/src/sdk/guides/catalog.ts +554 -0
  173. package/src/sdk/guides/cli.ts +235 -0
  174. package/src/sdk/guides/manifest.ts +152 -0
  175. package/src/sdk/guides/verify.ts +178 -0
  176. package/src/sdk/host/control/dispatch.ts +49 -1
  177. package/src/sdk/host/control/index.ts +1 -0
  178. package/src/sdk/host/control/operations.ts +8 -1
  179. package/src/sdk/host/query/cursor.ts +47 -3
  180. package/src/sdk/host/query/handlers.ts +155 -2
  181. package/src/sdk/host/sdk-surface-parity.test.ts +1 -0
  182. package/src/sdk/host/session-runtime.test.ts +391 -1
  183. package/src/sdk/host/session-runtime.ts +308 -73
  184. package/src/sdk/host/surface-policy.ts +3 -0
  185. package/src/sdk/host/websocket-transport.ts +7 -1
  186. package/src/sdk/mcp/server.ts +88 -15
  187. package/src/sdk/prompt-status.ts +8 -0
  188. package/src/sdk/protocol/operation-inventory.generated.json +62 -0
  189. package/src/sdk/protocol/operation-registry.ts +3 -2
  190. package/src/sdk/session.ts +87 -39
  191. package/src/sdk/transport/index.ts +1 -0
  192. package/src/sdk/transport/relay.ts +3 -0
  193. package/src/sdk/transport/serve-cli.ts +111 -24
  194. package/src/secrets/obfuscator.ts +92 -12
  195. package/src/session/agent-session.ts +322 -93
  196. package/src/session/blob-store.ts +60 -8
  197. package/src/session/fallback-chain-controller.ts +35 -0
  198. package/src/session/internal/managed-session-scope.ts +25 -3
  199. package/src/session/internal/managed-session-storage.ts +574 -73
  200. package/src/session/internal/session-memory-sidecar.ts +19 -29
  201. package/src/session/session-manager.ts +2166 -743
  202. package/src/session/session-storage.ts +715 -48
  203. package/src/setup/provider-onboarding.ts +19 -3
  204. package/src/setup/provider-presets.json +3 -1
  205. package/src/slash-commands/builtin-registry.ts +85 -3
  206. package/src/stt/downloader.ts +2 -3
  207. package/src/stt/setup.ts +29 -4
  208. package/src/task/executor.ts +22 -18
  209. package/src/task/index.ts +24 -1
  210. package/src/tools/archive-reader.ts +11 -2
  211. package/src/tools/browser/runtime-diagnostics.ts +249 -0
  212. package/src/tools/browser/tab-protocol.ts +4 -0
  213. package/src/tools/browser/tab-supervisor.ts +5 -0
  214. package/src/tools/browser/tab-worker.ts +36 -1
  215. package/src/tools/browser.ts +7 -0
  216. package/src/tools/image-gen.ts +110 -166
  217. package/src/tools/index.ts +2 -0
  218. package/src/tools/read.ts +1 -1
  219. package/src/tools/resource-gc.ts +6 -1
  220. package/src/tools/tool-catalog.generated.ts +5 -1
  221. package/src/utils/pasted-image-path.ts +93 -0
  222. package/src/web/search/index.ts +3 -3
@@ -0,0 +1,235 @@
1
+ import { getAgentDir } from "@gajae-code/utils";
2
+ import { readGuideCache } from "./cache";
3
+ import { BUNDLED_GUIDE_MANIFESTS, GuideCatalog, guideFetchPolicy, isGuideFetchUrlAllowed } from "./catalog";
4
+ import { GUIDE_CLIENT_VERSION, GUIDE_PINNED_KEYS } from "./verify";
5
+
6
+ /**
7
+ * `gjc sdk guides` command family (routing lives in `src/commands/sdk.ts`).
8
+ *
9
+ * Verbs:
10
+ * refresh --url <https url> fetch + verify the online manifest and advisory
11
+ * text, install the verified cache, report selection.
12
+ * Fails operationally (exit 1) whenever the online
13
+ * manifest is not selected: fallback cache/bundled
14
+ * content is reported as a failure, never as a
15
+ * successful refresh
16
+ * list offline selection: verified cache, else bundled
17
+ * show <guideId> render the advisory text for one guide (data only)
18
+ * status selection provenance, cache health, fetch policy
19
+ * trust pinned key ids and the no-credential fetch boundary
20
+ *
21
+ * The subsystem is advisory-text only: it never executes guide content, never
22
+ * mutates configuration, and never accepts or emits credentials. The only
23
+ * state it writes is the verified guide cache during `refresh`.
24
+ */
25
+ export type SdkGuidesCliAction = "refresh" | "list" | "show" | "status" | "trust";
26
+
27
+ export interface SdkGuidesCliArgs {
28
+ action?: string;
29
+ /** Guide id for `show`. */
30
+ guideId?: string;
31
+ /** Online manifest URL for `refresh`; must satisfy the HTTPS allowlist. */
32
+ url?: string;
33
+ agentDir?: string;
34
+ /** Bounded fetch timeout for `refresh`. */
35
+ timeoutMs?: number;
36
+ /** Fetch implementation override for `refresh` (test seam); defaults to the global fetch. */
37
+ fetchImpl?: typeof fetch;
38
+ }
39
+
40
+ const GUIDE_CLI_ACTIONS: readonly string[] = ["refresh", "list", "show", "status", "trust"];
41
+
42
+ class SdkGuidesCliError extends Error {
43
+ constructor(
44
+ readonly code: string,
45
+ message: string,
46
+ readonly exitCode: 1 | 2,
47
+ ) {
48
+ super(message);
49
+ }
50
+ }
51
+
52
+ function writeJson(value: unknown): void {
53
+ process.stdout.write(`${JSON.stringify(value)}\n`);
54
+ }
55
+
56
+ function usageFailure(message: string): SdkGuidesCliError {
57
+ return new SdkGuidesCliError("usage", message, 2);
58
+ }
59
+
60
+ function operationalFailure(code: string, message: string): SdkGuidesCliError {
61
+ return new SdkGuidesCliError(code, message, 1);
62
+ }
63
+
64
+ /**
65
+ * Usage-shaped errors (bad verb, missing argument, URL outside the allowlist)
66
+ * exit 2; every verification/operational failure exits 1. Fail-closed codes
67
+ * from the verification pipeline map to exit 1 so scripts observe a hard
68
+ * failure on tamper, unknown key, rollback, expiry, or corrupt cache.
69
+ */
70
+ function toCliError(error: unknown): SdkGuidesCliError {
71
+ if (error instanceof SdkGuidesCliError) return error;
72
+ const code = error instanceof Error && "code" in error ? String((error as { code: unknown }).code) : undefined;
73
+ const message = error instanceof Error ? error.message : String(error);
74
+ switch (code) {
75
+ case "usage":
76
+ case "invalid_input":
77
+ case "fetch_forbidden":
78
+ return new SdkGuidesCliError(code ?? "operation_failed", message, 2);
79
+ default:
80
+ return operationalFailure(code ?? "operation_failed", message);
81
+ }
82
+ }
83
+
84
+ function requireValue(value: string | undefined, flag: string): string {
85
+ if (value === undefined || value.length === 0) throw usageFailure(`${flag} is required.`);
86
+ return value;
87
+ }
88
+
89
+ function manifestSummary(manifest: {
90
+ manifestId: string;
91
+ keyId: string;
92
+ sequence: number;
93
+ issuedAt: number;
94
+ expiresAt: number;
95
+ minimumSdkVersion: number;
96
+ }) {
97
+ return {
98
+ manifestId: manifest.manifestId,
99
+ keyId: manifest.keyId,
100
+ sequence: manifest.sequence,
101
+ issuedAt: manifest.issuedAt,
102
+ expiresAt: manifest.expiresAt,
103
+ minimumSdkVersion: manifest.minimumSdkVersion,
104
+ };
105
+ }
106
+
107
+ async function runRefresh(agentDir: string, args: SdkGuidesCliArgs): Promise<unknown> {
108
+ const url = requireValue(args.url, "--url");
109
+ if (!isGuideFetchUrlAllowed(url)) throw usageFailure(`--url ${url} is outside the HTTPS allowlist.`);
110
+ const catalog = new GuideCatalog({
111
+ agentDir,
112
+ onlineUrl: url,
113
+ timeoutMs: args.timeoutMs,
114
+ fetchImpl: args.fetchImpl,
115
+ });
116
+ const result = await catalog.refresh();
117
+ if (!result.ok) throw operationalFailure(result.error.code, result.error.message);
118
+ const selection = result.value;
119
+ if (selection.source !== "online") {
120
+ const reasons = selection.warnings.length > 0 ? ` ${selection.warnings.join("; ")}` : "";
121
+ throw operationalFailure(
122
+ "online_refresh_failed",
123
+ `Online refresh did not select an online source (selected ${selection.source} instead)${reasons}.`,
124
+ );
125
+ }
126
+ return {
127
+ source: selection.source,
128
+ manifest: manifestSummary(selection.manifest),
129
+ guides: selection.guides.map(guide => ({ id: guide.id, title: guide.title, sha256: guide.sha256 })),
130
+ warnings: selection.warnings,
131
+ };
132
+ }
133
+
134
+ async function runList(agentDir: string): Promise<unknown> {
135
+ const catalog = new GuideCatalog({ agentDir });
136
+ const result = await catalog.load();
137
+ if (!result.ok) throw operationalFailure(result.error.code, result.error.message);
138
+ const selection = result.value;
139
+ return {
140
+ source: selection.source,
141
+ manifest: manifestSummary(selection.manifest),
142
+ guides: selection.guides.map(guide => ({ id: guide.id, title: guide.title, sha256: guide.sha256 })),
143
+ warnings: selection.warnings,
144
+ };
145
+ }
146
+
147
+ async function runShow(agentDir: string, args: SdkGuidesCliArgs): Promise<unknown> {
148
+ const guideId = requireValue(args.guideId, "<guideId>");
149
+ const catalog = new GuideCatalog({ agentDir });
150
+ const result = await catalog.advisory(guideId);
151
+ if (!result.ok) throw operationalFailure(result.error.code, result.error.message);
152
+ return {
153
+ source: result.value.source,
154
+ guideId: result.value.guideId,
155
+ title: result.value.title,
156
+ text: result.value.text,
157
+ };
158
+ }
159
+
160
+ async function runStatus(agentDir: string): Promise<unknown> {
161
+ const now = Date.now();
162
+ const cache = await readGuideCache({ agentDir, now });
163
+ const cacheView =
164
+ !cache.ok && cache.error.code === "missing_cache"
165
+ ? { present: false }
166
+ : {
167
+ present: true,
168
+ verified: cache.ok,
169
+ code: cache.ok ? undefined : cache.error.code,
170
+ manifestId: cache.ok ? cache.value.manifest.manifestId : undefined,
171
+ sequence: cache.ok ? cache.value.manifest.sequence : undefined,
172
+ expiresAt: cache.ok ? cache.value.manifest.expiresAt : undefined,
173
+ };
174
+ const catalog = new GuideCatalog({ agentDir });
175
+ const selection = await catalog.load();
176
+ return {
177
+ clientVersion: GUIDE_CLIENT_VERSION,
178
+ source: selection.ok ? selection.value.source : undefined,
179
+ manifest: selection.ok ? manifestSummary(selection.value.manifest) : undefined,
180
+ cache: cacheView,
181
+ bundled: BUNDLED_GUIDE_MANIFESTS.length,
182
+ fetch: guideFetchPolicy(),
183
+ warnings: selection.ok ? selection.value.warnings : [],
184
+ };
185
+ }
186
+
187
+ function runTrust(): unknown {
188
+ return {
189
+ clientVersion: GUIDE_CLIENT_VERSION,
190
+ pinnedKeys: GUIDE_PINNED_KEYS.map(key => ({ keyId: key.keyId, source: key.source })),
191
+ fetch: guideFetchPolicy(),
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Runs the `gjc sdk guides` command family. Exported for command routing from
197
+ * `src/commands/sdk.ts` and for direct service use; the injected `write` /
198
+ * `setExitCode` hooks keep the surface testable without touching stdout or
199
+ * the process exit code.
200
+ */
201
+ export async function runSdkGuidesCli(
202
+ args: SdkGuidesCliArgs,
203
+ writeOutput: (value: unknown) => void = writeJson,
204
+ setExitCode: (exitCode: 1 | 2) => void = exitCode => {
205
+ process.exitCode = exitCode;
206
+ },
207
+ ): Promise<void> {
208
+ try {
209
+ const action = args.action;
210
+ if (action === undefined || !GUIDE_CLI_ACTIONS.includes(action))
211
+ throw usageFailure(`Expected one of: ${GUIDE_CLI_ACTIONS.join(", ")}.`);
212
+ const agentDir = args.agentDir ?? getAgentDir();
213
+ switch (action) {
214
+ case "refresh":
215
+ writeOutput({ ok: true, result: await runRefresh(agentDir, args) });
216
+ return;
217
+ case "list":
218
+ writeOutput({ ok: true, result: await runList(agentDir) });
219
+ return;
220
+ case "show":
221
+ writeOutput({ ok: true, result: await runShow(agentDir, args) });
222
+ return;
223
+ case "status":
224
+ writeOutput({ ok: true, result: await runStatus(agentDir) });
225
+ return;
226
+ case "trust":
227
+ writeOutput({ ok: true, result: runTrust() });
228
+ return;
229
+ }
230
+ } catch (error) {
231
+ const cliError = toCliError(error);
232
+ writeOutput({ ok: false, error: { code: cliError.code, message: cliError.message } });
233
+ setExitCode(cliError.exitCode);
234
+ }
235
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Trusted advisory guide manifest: types, canonical encoding, and parsing.
3
+ *
4
+ * A manifest is a signed, time-boxed bundle of advisory guide bindings. Each
5
+ * entry binds a stable guide id to the SHA-256 digest of its advisory text, so
6
+ * the advisory content can be fetched separately and verified against the
7
+ * signed manifest. Manifests are advisory text only: they never carry
8
+ * instructions to execute or configuration to apply.
9
+ */
10
+ export const GUIDES_MANIFEST_VERSION = 1;
11
+
12
+ /** Maximum byte length of a single advisory text file. */
13
+ export const GUIDE_ADVISORY_MAX_BYTES = 256 * 1024;
14
+ /** Maximum number of guides bound by a single manifest. */
15
+ export const GUIDE_MANIFEST_MAX_ENTRIES = 256;
16
+ /** Maximum manifest JSON byte length (enforced at fetch and cache read time). */
17
+ export const GUIDE_MANIFEST_MAX_BYTES = 1024 * 1024;
18
+ export const GUIDE_ID_MAX_LENGTH = 128;
19
+ export const GUIDE_TITLE_MAX_LENGTH = 256;
20
+
21
+ export interface GuideEntryV1 {
22
+ /** Path-safe, stable guide id, e.g. "troubleshooting/socket". */
23
+ id: string;
24
+ /** Human-readable advisory title. */
25
+ title: string;
26
+ /** Hex SHA-256 of the advisory text bytes; binds content to the signed manifest. */
27
+ sha256: string;
28
+ }
29
+
30
+ export interface GuideManifestV1 {
31
+ version: 1;
32
+ /** Stable channel identity the signature binds to; the rollback floor is per manifestId. */
33
+ manifestId: string;
34
+ /** Key id of the pinned Ed25519 public key that detached-signed the canonical manifest bytes. */
35
+ keyId: string;
36
+ /** Monotonic per-manifestId sequence; the cache refuses installs at or below the floor. */
37
+ sequence: number;
38
+ /** Milliseconds since epoch; the manifest is not authoritative before this instant. */
39
+ issuedAt: number;
40
+ /** Milliseconds since epoch; the manifest is not authoritative after this instant. */
41
+ expiresAt: number;
42
+ /** Minimum SDK advisory client version required to consume this manifest. */
43
+ minimumSdkVersion: number;
44
+ guides: GuideEntryV1[];
45
+ }
46
+
47
+ const SHA256_HEX = /^[a-f0-9]{64}$/;
48
+ const GUIDE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*){0,15}$/;
49
+ const MANIFEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
50
+
51
+ /**
52
+ * Deterministic UTF-8 encoding of a guide manifest. Object keys are sorted,
53
+ * separators are compact, and the output is byte-identical for semantically
54
+ * equal manifests regardless of the original JSON formatting. The detached
55
+ * Ed25519 signature is verified over exactly these bytes, so a re-formatted
56
+ * manifest file still verifies while any semantic change breaks the signature.
57
+ */
58
+ export function canonicalGuideManifestBytes(manifest: GuideManifestV1): Buffer {
59
+ return Buffer.from(canonicalGuideJson(manifest), "utf8");
60
+ }
61
+
62
+ function canonicalGuideJson(value: unknown): string {
63
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
64
+ if (Array.isArray(value)) return `[${value.map(canonicalGuideJson).join(",")}]`;
65
+ const record = value as Record<string, unknown>;
66
+ return `{${Object.keys(record)
67
+ .sort()
68
+ .map(key => `${JSON.stringify(key)}:${canonicalGuideJson(record[key])}`)
69
+ .join(",")}}`;
70
+ }
71
+
72
+ export type GuideManifestParseResult =
73
+ | { ok: true; manifest: GuideManifestV1 }
74
+ | { ok: false; error: { code: "invalid_manifest" | "unsupported_version"; message: string } };
75
+
76
+ function manifestParseFailure(
77
+ code: "invalid_manifest" | "unsupported_version",
78
+ message: string,
79
+ ): GuideManifestParseResult {
80
+ return { ok: false, error: { code, message } };
81
+ }
82
+
83
+ /**
84
+ * Strict shape validation for a parsed manifest value. Fails closed on any
85
+ * out-of-bounds field; a manifest for a newer format version is refused with
86
+ * `unsupported_version` so callers can distinguish "too new" from "malformed".
87
+ */
88
+ export function parseGuideManifest(value: unknown): GuideManifestParseResult {
89
+ if (typeof value !== "object" || value === null || Array.isArray(value))
90
+ return manifestParseFailure("invalid_manifest", "Guide manifest must be a JSON object.");
91
+ const record = value as Record<string, unknown>;
92
+ if (!Number.isSafeInteger(record.version))
93
+ return manifestParseFailure("invalid_manifest", "Guide manifest version must be an integer.");
94
+ const version = record.version as number;
95
+ if (version > GUIDES_MANIFEST_VERSION)
96
+ return manifestParseFailure(
97
+ "unsupported_version",
98
+ `Guide manifest version ${version} is newer than the supported version ${GUIDES_MANIFEST_VERSION}.`,
99
+ );
100
+ if (version !== GUIDES_MANIFEST_VERSION)
101
+ return manifestParseFailure(
102
+ "invalid_manifest",
103
+ `Guide manifest version ${version} is not supported (expected ${GUIDES_MANIFEST_VERSION}).`,
104
+ );
105
+ if (typeof record.manifestId !== "string" || !MANIFEST_ID_PATTERN.test(record.manifestId))
106
+ return manifestParseFailure("invalid_manifest", "Guide manifestId is malformed.");
107
+ if (typeof record.keyId !== "string" || !SHA256_HEX.test(record.keyId))
108
+ return manifestParseFailure("invalid_manifest", "Guide manifest keyId must be a SHA-256 hex digest.");
109
+ if (!Number.isSafeInteger(record.sequence) || (record.sequence as number) < 1)
110
+ return manifestParseFailure("invalid_manifest", "Guide manifest sequence must be a positive integer.");
111
+ if (!Number.isSafeInteger(record.issuedAt) || (record.issuedAt as number) < 0)
112
+ return manifestParseFailure("invalid_manifest", "Guide manifest issuedAt must be a non-negative timestamp.");
113
+ if (!Number.isSafeInteger(record.expiresAt) || (record.expiresAt as number) <= (record.issuedAt as number))
114
+ return manifestParseFailure("invalid_manifest", "Guide manifest expiresAt must be after issuedAt.");
115
+ if (!Number.isSafeInteger(record.minimumSdkVersion) || (record.minimumSdkVersion as number) < 1)
116
+ return manifestParseFailure("invalid_manifest", "Guide manifest minimumSdkVersion must be a positive integer.");
117
+ if (!Array.isArray(record.guides) || record.guides.length === 0 || record.guides.length > GUIDE_MANIFEST_MAX_ENTRIES)
118
+ return manifestParseFailure(
119
+ "invalid_manifest",
120
+ `Guide manifest guides must contain between 1 and ${GUIDE_MANIFEST_MAX_ENTRIES} entries.`,
121
+ );
122
+ const guides: GuideEntryV1[] = [];
123
+ const seenIds = new Set<string>();
124
+ for (const rawEntry of record.guides) {
125
+ if (typeof rawEntry !== "object" || rawEntry === null || Array.isArray(rawEntry))
126
+ return manifestParseFailure("invalid_manifest", "Guide entry must be a JSON object.");
127
+ const entry = rawEntry as Record<string, unknown>;
128
+ if (typeof entry.id !== "string" || entry.id.length > GUIDE_ID_MAX_LENGTH || !GUIDE_ID_PATTERN.test(entry.id))
129
+ return manifestParseFailure("invalid_manifest", "Guide entry id is malformed.");
130
+ if (seenIds.has(entry.id))
131
+ return manifestParseFailure("invalid_manifest", `Guide entry id ${entry.id} is duplicated.`);
132
+ if (typeof entry.title !== "string" || entry.title.length === 0 || entry.title.length > GUIDE_TITLE_MAX_LENGTH)
133
+ return manifestParseFailure("invalid_manifest", "Guide entry title is malformed.");
134
+ if (typeof entry.sha256 !== "string" || !SHA256_HEX.test(entry.sha256))
135
+ return manifestParseFailure("invalid_manifest", "Guide entry sha256 must be a SHA-256 hex digest.");
136
+ seenIds.add(entry.id);
137
+ guides.push({ id: entry.id, title: entry.title, sha256: entry.sha256 });
138
+ }
139
+ return {
140
+ ok: true,
141
+ manifest: {
142
+ version: GUIDES_MANIFEST_VERSION,
143
+ manifestId: record.manifestId,
144
+ keyId: record.keyId,
145
+ sequence: record.sequence as number,
146
+ issuedAt: record.issuedAt as number,
147
+ expiresAt: record.expiresAt as number,
148
+ minimumSdkVersion: record.minimumSdkVersion as number,
149
+ guides,
150
+ },
151
+ };
152
+ }
@@ -0,0 +1,178 @@
1
+ import { createHash, createPublicKey, verify as cryptoVerify, type KeyObject } from "node:crypto";
2
+ import {
3
+ canonicalGuideManifestBytes,
4
+ GUIDES_MANIFEST_VERSION,
5
+ type GuideEntryV1,
6
+ type GuideManifestV1,
7
+ } from "./manifest";
8
+
9
+ /**
10
+ * Version of the SDK advisory guide client. Manifests carry a
11
+ * `minimumSdkVersion` compatibility floor; a manifest that requires a newer
12
+ * client is refused (`incompatible`) instead of being partially understood.
13
+ */
14
+ export const GUIDE_CLIENT_VERSION = 1;
15
+
16
+ /** Ed25519 detached signatures are always exactly 64 bytes. */
17
+ export const GUIDE_SIGNATURE_BYTES = 64;
18
+
19
+ export interface GuidePinnedKey {
20
+ keyId: string;
21
+ /** DER-encoded SPKI public key, hex. */
22
+ spkiDerHex: string;
23
+ source: "bundled";
24
+ }
25
+
26
+ /**
27
+ * Pinned Ed25519 public keys accepted for guide manifests. A manifest whose
28
+ * `keyId` is not in this registry is refused (`unknown_key`) — the registry is
29
+ * compiled into the binary and is the only trust root for advisory content.
30
+ * Private key material is never shipped; manifests are signed out-of-band.
31
+ */
32
+ const guidePinnedKeys: GuidePinnedKey[] = [
33
+ {
34
+ keyId: "6c4b134ff9fb86a52d55cb6bb7c2fab938405b53b4148afc4249a2cb6f504bce",
35
+ spkiDerHex: "302a300506032b6570032100ef665d05c6795341dfc893866d8fe5be4b48891c0ed0d125940d7032de37723e",
36
+ source: "bundled",
37
+ },
38
+ ];
39
+ export const GUIDE_PINNED_KEYS: readonly GuidePinnedKey[] = Object.freeze(
40
+ guidePinnedKeys.map(key => Object.freeze({ ...key })),
41
+ );
42
+
43
+ export const GUIDE_PINNED_KEY_IDS: readonly string[] = Object.freeze(GUIDE_PINNED_KEYS.map(key => key.keyId));
44
+
45
+ export type GuideVerificationErrorCode =
46
+ | "invalid_manifest"
47
+ | "unsupported_version"
48
+ | "unknown_key"
49
+ | "corrupt_signature"
50
+ | "invalid_signature"
51
+ | "not_yet_valid"
52
+ | "expired"
53
+ | "incompatible"
54
+ | "rollback"
55
+ | "hash_mismatch"
56
+ | (string & {});
57
+
58
+ export type GuideVerificationResult<T> =
59
+ | { ok: true; value: T }
60
+ | { ok: false; error: { code: GuideVerificationErrorCode; message: string } };
61
+
62
+ function guideFailure<T>(code: GuideVerificationErrorCode, message: string): GuideVerificationResult<T> {
63
+ return { ok: false, error: { code, message } };
64
+ }
65
+
66
+ const guideKeyCache = new Map<string, KeyObject>();
67
+
68
+ /** Test-only override: installs an additional trusted key for deterministic tests. */
69
+ export function addTestGuidePinnedKey(key: GuidePinnedKey): void {
70
+ if (process.env.GJC_TEST_GUIDE_KEYS !== "1") throw new Error("Test guide key injection is disabled.");
71
+ guideKeyCache.delete(key.keyId);
72
+ guidePinnedKeys.push(key);
73
+ }
74
+
75
+ /** Test-only override: removes a previously installed test key. */
76
+ export function removeTestGuidePinnedKey(keyId: string): void {
77
+ if (process.env.GJC_TEST_GUIDE_KEYS !== "1") throw new Error("Test guide key injection is disabled.");
78
+ guideKeyCache.delete(keyId);
79
+ const index = guidePinnedKeys.findIndex(key => key.keyId === keyId);
80
+ if (index >= 0) guidePinnedKeys.splice(index, 1);
81
+ }
82
+
83
+ function pinnedGuideKey(keyId: string): GuidePinnedKey | undefined {
84
+ return guidePinnedKeys.find(key => key.keyId === keyId);
85
+ }
86
+
87
+ function guidePublicKey(pinned: GuidePinnedKey): KeyObject {
88
+ const cached = guideKeyCache.get(pinned.keyId);
89
+ if (cached) return cached;
90
+ const key = createPublicKey({ key: Buffer.from(pinned.spkiDerHex, "hex"), format: "der", type: "spki" });
91
+ guideKeyCache.set(pinned.keyId, key);
92
+ return key;
93
+ }
94
+
95
+ /**
96
+ * Verifies a guide manifest end to end and fails closed on the first broken
97
+ * property. Order matters: identity and tamper checks (unknown key, malformed
98
+ * or mismatched signature) run before temporal and compatibility checks, so a
99
+ * tampered or untrusted manifest is never masked by an expiry message.
100
+ */
101
+ export function verifyGuideManifest(params: {
102
+ manifest: GuideManifestV1;
103
+ signatureBytes: Uint8Array;
104
+ now: number;
105
+ }): GuideVerificationResult<{ keyId: string }> {
106
+ const { manifest, signatureBytes, now } = params;
107
+ if (manifest.version !== GUIDES_MANIFEST_VERSION)
108
+ return guideFailure(
109
+ "unsupported_version",
110
+ `Guide manifest version ${manifest.version} is newer than the supported version ${GUIDES_MANIFEST_VERSION}.`,
111
+ );
112
+ const pinned = pinnedGuideKey(manifest.keyId);
113
+ if (!pinned) return guideFailure("unknown_key", `Guide manifest keyId ${manifest.keyId} is not pinned.`);
114
+ if (signatureBytes.length !== GUIDE_SIGNATURE_BYTES)
115
+ return guideFailure(
116
+ "corrupt_signature",
117
+ `Detached signature must be exactly ${GUIDE_SIGNATURE_BYTES} bytes (Ed25519), got ${signatureBytes.length}.`,
118
+ );
119
+ const canonical = canonicalGuideManifestBytes(manifest);
120
+ let valid: boolean;
121
+ try {
122
+ valid = cryptoVerify(null, canonical, guidePublicKey(pinned), signatureBytes);
123
+ } catch {
124
+ return guideFailure("invalid_signature", "Detached signature verification failed.");
125
+ }
126
+ if (!valid)
127
+ return guideFailure("invalid_signature", "Detached signature does not match the canonical manifest bytes.");
128
+ if (!Number.isSafeInteger(now) || now < manifest.issuedAt)
129
+ return guideFailure("not_yet_valid", "Guide manifest is not authoritative until its issuedAt instant.");
130
+ if (now > manifest.expiresAt)
131
+ return guideFailure("expired", "Guide manifest has expired and is no longer authoritative.");
132
+ if (manifest.minimumSdkVersion > GUIDE_CLIENT_VERSION)
133
+ return guideFailure(
134
+ "incompatible",
135
+ `Guide manifest requires SDK advisory client ${manifest.minimumSdkVersion}, this client is ${GUIDE_CLIENT_VERSION}.`,
136
+ );
137
+ return { ok: true, value: { keyId: manifest.keyId } };
138
+ }
139
+
140
+ /**
141
+ * Monotonic floor check: a candidate manifest for a channel must strictly
142
+ * advance that channel's sequence, otherwise an older (but still validly
143
+ * signed) manifest is refused as a rollback.
144
+ */
145
+ export function guideRollbackCheck(
146
+ previous: { manifestId: string; sequence: number } | undefined,
147
+ candidate: GuideManifestV1,
148
+ ): GuideVerificationResult<{ monotonic: boolean }> {
149
+ if (previous && previous.manifestId === candidate.manifestId && candidate.sequence <= previous.sequence)
150
+ return guideFailure(
151
+ "rollback",
152
+ `Guide manifest sequence ${candidate.sequence} does not advance the cached floor ${previous.sequence} for ${candidate.manifestId}.`,
153
+ );
154
+ return { ok: true, value: { monotonic: true } };
155
+ }
156
+
157
+ export function guideAdvisoryDigest(text: Uint8Array): string {
158
+ return createHash("sha256").update(text).digest("hex");
159
+ }
160
+
161
+ /**
162
+ * Per-guide SHA-256 binding: the advisory text must match the digest the
163
+ * signed manifest binds for that guide id. This is the tamper check for the
164
+ * advisory payload itself — the manifest signature cannot cover content that
165
+ * is fetched separately, so every advisory is hash-verified against it.
166
+ */
167
+ export function verifyGuideAdvisoryText(
168
+ entry: GuideEntryV1,
169
+ text: Uint8Array,
170
+ ): GuideVerificationResult<{ sha256: string }> {
171
+ const digest = guideAdvisoryDigest(text);
172
+ if (digest !== entry.sha256)
173
+ return guideFailure(
174
+ "hash_mismatch",
175
+ `Advisory ${entry.id} digest ${digest} does not match the manifest binding ${entry.sha256}.`,
176
+ );
177
+ return { ok: true, value: { sha256: digest } };
178
+ }
@@ -3,6 +3,7 @@ import {
3
3
  DEFAULT_MODEL_SELECTION_RECOVERY_MESSAGE,
4
4
  parseDefaultModelSelectionRecovery,
5
5
  } from "../../../session/default-model-selection";
6
+ import { isElevationAllowlisted } from "../../elevation/allowlist";
6
7
  import { OPERATIONS, type Operation } from "../../protocol/operation-registry";
7
8
  import type { ControlInput, ControlSurface, ControlValue } from "./operations";
8
9
 
@@ -13,6 +14,8 @@ export interface ControlRequest {
13
14
  expectedRevision?: string;
14
15
  idempotencyKey?: string;
15
16
  confirm?: boolean;
17
+ /** Top-level elevation claim selector, preserved from the control frame. */
18
+ elevationRequestId?: string;
16
19
  }
17
20
 
18
21
  export type ControlErrorCode = string;
@@ -105,6 +108,22 @@ function failure(
105
108
  function isInput(value: unknown): value is ControlInput {
106
109
  return typeof value === "object" && value !== null && !Array.isArray(value);
107
110
  }
111
+ /**
112
+ * Build the typed control request from a wire frame. Every runtime transport
113
+ * mapper uses this helper so the top-level elevation claim selector cannot
114
+ * drift between callsites.
115
+ */
116
+ export function controlRequestFromFrame(frame: Record<string, unknown>): ControlRequest {
117
+ return {
118
+ id: typeof frame.id === "string" ? frame.id : "",
119
+ operation: typeof frame.operation === "string" ? frame.operation : "",
120
+ input: frame.input,
121
+ expectedRevision: typeof frame.expectedRevision === "string" ? frame.expectedRevision : undefined,
122
+ idempotencyKey: typeof frame.idempotencyKey === "string" ? frame.idempotencyKey : undefined,
123
+ confirm: frame.confirm === true,
124
+ elevationRequestId: typeof frame.elevationRequestId === "string" ? frame.elevationRequestId : undefined,
125
+ };
126
+ }
108
127
 
109
128
  function canonicalize(value: unknown): unknown {
110
129
  if (Array.isArray(value)) return value.map(canonicalize);
@@ -134,6 +153,7 @@ function invoke(
134
153
  input: ControlInput,
135
154
  confirm: boolean | undefined,
136
155
  idempotencyKey: string | undefined,
156
+ elevationRequestId: string | undefined,
137
157
  ): Promise<ControlValue> | ControlValue {
138
158
  switch (operation) {
139
159
  case "turn.prompt":
@@ -154,9 +174,15 @@ function invoke(
154
174
  input.response,
155
175
  input.expectedSessionId as string | undefined,
156
176
  idempotencyKey,
177
+ elevationRequestId,
157
178
  );
158
179
  case "workflow.plan_approve":
159
- return surface.approvePlan(text(input, "id"), input.choice, input.expectedSessionId as string | undefined);
180
+ return surface.approvePlan(
181
+ text(input, "id"),
182
+ input.choice,
183
+ input.expectedSessionId as string | undefined,
184
+ elevationRequestId,
185
+ );
160
186
  case "skill.invoke":
161
187
  return surface.invokeSkill(
162
188
  text(input, "name"),
@@ -303,6 +329,7 @@ async function execute(surface: ControlSurface, row: Operation, request: Control
303
329
  request.input as ControlInput,
304
330
  request.confirm,
305
331
  request.idempotencyKey,
332
+ request.elevationRequestId,
306
333
  ),
307
334
  };
308
335
  } catch (error) {
@@ -379,6 +406,27 @@ export function dispatchControl(
379
406
  );
380
407
  if (!isInput(request.input))
381
408
  return Promise.resolve(failure(request.id, "invalid_input", "Control input must be an object."));
409
+ const elevationEnabled =
410
+ process.env.GJC_SDK_ELEVATION_ENABLED === "1" || process.env.GJC_SDK_ELEVATION_ENABLED === "true";
411
+ const elevationAllowlisted = isElevationAllowlisted("control", row.sdkId);
412
+ const elevationRequired = elevationEnabled && elevationAllowlisted;
413
+ if (elevationRequired && request.elevationRequestId === undefined)
414
+ return Promise.resolve(failure(request.id, "elevation_required", `${row.sdkId} requires an elevation grant.`));
415
+ if (request.elevationRequestId !== undefined) {
416
+ const claim = request.elevationRequestId.trim();
417
+ if (!claim || claim.length > 128)
418
+ return Promise.resolve(
419
+ failure(request.id, "invalid_input", "elevationRequestId must be a non-empty bounded string."),
420
+ );
421
+ if (!elevationAllowlisted)
422
+ return Promise.resolve(
423
+ failure(request.id, "invalid_input", "Only allowlisted elevation operations may carry elevationRequestId."),
424
+ );
425
+ if (!surface.authorizeElevationClaim?.(row.sdkId, request.input, claim))
426
+ return Promise.resolve(
427
+ failure(request.id, "elevation_required", "The elevation claim is not broker-authorized for this control."),
428
+ );
429
+ }
382
430
  if ((row.sdkId === "context.clear" || row.sdkId === "session.delete") && request.confirm !== true)
383
431
  return Promise.resolve(
384
432
  failure(request.id, "invalid_input", "confirm: true is required for this destructive operation."),
@@ -4,6 +4,7 @@ export {
4
4
  type ControlErrorCode,
5
5
  type ControlRequest,
6
6
  type ControlResponse,
7
+ controlRequestFromFrame,
7
8
  dispatchControl,
8
9
  TypedControlError,
9
10
  } from "./dispatch";