@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,554 @@
1
+ import { installGuideCache, readGuideCache, type VerifiedGuideCache } from "./cache";
2
+ import {
3
+ GUIDE_ADVISORY_MAX_BYTES,
4
+ GUIDE_MANIFEST_MAX_BYTES,
5
+ type GuideEntryV1,
6
+ type GuideManifestV1,
7
+ parseGuideManifest,
8
+ } from "./manifest";
9
+ import { GUIDE_PINNED_KEYS, guideAdvisoryDigest, verifyGuideAdvisoryText, verifyGuideManifest } from "./verify";
10
+
11
+ /**
12
+ * HTTPS-only, allowlisted, credential-free fetch boundary for advisory guides.
13
+ *
14
+ * The catalog refuses any URL that is not https, whose host is not in the
15
+ * allowlist, whose path is outside the allowlisted prefix, or that carries
16
+ * userinfo. Every request uses `credentials: "omit"`, no authorization or
17
+ * cookie headers, and `redirect: "error"` so a redirect can never escape the
18
+ * allowlist. Responses are bounded by byte caps and a timeout. This boundary
19
+ * is the only network surface of the guide subsystem — advisories are fetched
20
+ * as data, verified against the signed manifest, and never executed.
21
+ */
22
+ export interface GuideFetchAllowlistEntry {
23
+ host: string;
24
+ /** Every fetched path must start with this prefix. */
25
+ pathPrefix: string;
26
+ }
27
+
28
+ export const GUIDE_FETCH_ALLOWLIST: readonly GuideFetchAllowlistEntry[] = [
29
+ { host: "guides.gajae-code.com", pathPrefix: "/" },
30
+ ];
31
+ export const GUIDE_FETCH_TIMEOUT_MS = 10_000;
32
+ export const GUIDE_FETCH_MAX_MANIFEST_BYTES = GUIDE_MANIFEST_MAX_BYTES;
33
+ export const GUIDE_FETCH_MAX_SIGNATURE_BYTES = 1024;
34
+ export const GUIDE_FETCH_USER_AGENT = "gajae-code-sdk-guides/1";
35
+
36
+ export interface GuideFetchPolicy {
37
+ httpsOnly: true;
38
+ credentials: "omit";
39
+ redirect: "error";
40
+ allowlist: readonly GuideFetchAllowlistEntry[];
41
+ maxManifestBytes: number;
42
+ maxSignatureBytes: number;
43
+ timeoutMs: number;
44
+ userAgent: string;
45
+ }
46
+
47
+ export function guideFetchPolicy(): GuideFetchPolicy {
48
+ return {
49
+ httpsOnly: true,
50
+ credentials: "omit",
51
+ redirect: "error",
52
+ allowlist: GUIDE_FETCH_ALLOWLIST,
53
+ maxManifestBytes: GUIDE_FETCH_MAX_MANIFEST_BYTES,
54
+ maxSignatureBytes: GUIDE_FETCH_MAX_SIGNATURE_BYTES,
55
+ timeoutMs: GUIDE_FETCH_TIMEOUT_MS,
56
+ userAgent: GUIDE_FETCH_USER_AGENT,
57
+ };
58
+ }
59
+
60
+ /** True when a URL satisfies the HTTPS allowlist boundary. */
61
+ export function isGuideFetchUrlAllowed(url: string): boolean {
62
+ let parsed: URL;
63
+ try {
64
+ parsed = new URL(url);
65
+ } catch {
66
+ return false;
67
+ }
68
+ if (parsed.protocol !== "https:") return false;
69
+ if (parsed.port !== "" || parsed.username !== "" || parsed.password !== "") return false;
70
+ return GUIDE_FETCH_ALLOWLIST.some(
71
+ entry => parsed.hostname === entry.host && parsed.pathname.startsWith(entry.pathPrefix),
72
+ );
73
+ }
74
+
75
+ export type GuideFetchErrorCode =
76
+ | "fetch_forbidden"
77
+ | "fetch_failed"
78
+ | "oversize"
79
+ | "timeout"
80
+ | "network_error"
81
+ | "invalid_manifest"
82
+ | "unsupported_version"
83
+ | (string & {});
84
+
85
+ export interface GuideOnlineManifest {
86
+ url: string;
87
+ manifest: GuideManifestV1;
88
+ signatureBytes: Uint8Array;
89
+ fetchedAt: number;
90
+ }
91
+
92
+ export type GuideFetchResult =
93
+ | { ok: true; value: GuideOnlineManifest }
94
+ | { ok: false; error: { code: GuideFetchErrorCode; message: string; status?: number } };
95
+
96
+ function fetchFailure(code: GuideFetchErrorCode, message: string, status?: number): GuideFetchResult {
97
+ return { ok: false, error: { code, message, ...(status === undefined ? {} : { status }) } };
98
+ }
99
+
100
+ const guideUtf8Decoder = new TextDecoder("utf-8", { fatal: true });
101
+
102
+ function parseFatalUtf8Json(bytes: Uint8Array): unknown {
103
+ return JSON.parse(guideUtf8Decoder.decode(bytes));
104
+ }
105
+
106
+ type BoundedFetchResult =
107
+ | { ok: true; value: { bytes: Uint8Array } }
108
+ | {
109
+ ok: false;
110
+ error: { code: "fetch_failed" | "oversize" | "timeout" | "network_error"; message: string; status?: number };
111
+ };
112
+
113
+ function isFetchTimeout(error: unknown): boolean {
114
+ if (!(error instanceof Error)) return false;
115
+ if (error.name === "TimeoutError" || error.name === "AbortError") return true;
116
+ return /timed ?out|aborted|timeout/i.test(error.message);
117
+ }
118
+
119
+ async function boundedFetch(
120
+ fetchImpl: typeof fetch,
121
+ url: string,
122
+ policy: GuideFetchPolicy,
123
+ maxBytes: number,
124
+ ): Promise<BoundedFetchResult> {
125
+ let response: Response;
126
+ try {
127
+ response = await fetchImpl(url, {
128
+ method: "GET",
129
+ credentials: "omit",
130
+ redirect: "error",
131
+ signal: AbortSignal.timeout(policy.timeoutMs),
132
+ headers: { "user-agent": policy.userAgent },
133
+ });
134
+ } catch (error) {
135
+ if (isFetchTimeout(error))
136
+ return {
137
+ ok: false,
138
+ error: { code: "timeout", message: `Fetch timed out after ${policy.timeoutMs}ms: ${url}` },
139
+ };
140
+ return {
141
+ ok: false,
142
+ error: { code: "network_error", message: error instanceof Error ? error.message : String(error) },
143
+ };
144
+ }
145
+ if (!response.ok)
146
+ return {
147
+ ok: false,
148
+ error: {
149
+ code: "fetch_failed",
150
+ message: `Fetch failed with HTTP ${response.status}: ${url}`,
151
+ status: response.status,
152
+ },
153
+ };
154
+ const declared = response.headers.get("content-length");
155
+ if (declared !== null) {
156
+ const declaredBytes = Number(declared);
157
+ if (Number.isFinite(declaredBytes) && declaredBytes > maxBytes)
158
+ return {
159
+ ok: false,
160
+ error: { code: "oversize", message: `Response exceeds the ${maxBytes}-byte fetch cap: ${url}` },
161
+ };
162
+ }
163
+ let bytes: ArrayBuffer;
164
+ try {
165
+ bytes = await response.arrayBuffer();
166
+ } catch (error) {
167
+ if (isFetchTimeout(error))
168
+ return {
169
+ ok: false,
170
+ error: { code: "timeout", message: `Fetch timed out after ${policy.timeoutMs}ms: ${url}` },
171
+ };
172
+ return {
173
+ ok: false,
174
+ error: { code: "network_error", message: error instanceof Error ? error.message : String(error) },
175
+ };
176
+ }
177
+ if (bytes.byteLength > maxBytes)
178
+ return {
179
+ ok: false,
180
+ error: { code: "oversize", message: `Response exceeds the ${maxBytes}-byte fetch cap: ${url}` },
181
+ };
182
+ return { ok: true, value: { bytes: new Uint8Array(bytes) } };
183
+ }
184
+
185
+ /**
186
+ * Fetches the manifest at `url` and its detached signature at `<url>.sig`,
187
+ * bounded by the HTTPS allowlist policy. The manifest is parsed and shape
188
+ * validated here; signature verification and advisory hash checks happen in
189
+ * the caller so a rejected refresh can fall back to cache/bundled.
190
+ */
191
+ export async function fetchGuideManifestOnline(params: {
192
+ url: string;
193
+ timeoutMs?: number;
194
+ fetchImpl?: typeof fetch;
195
+ now?: number;
196
+ }): Promise<GuideFetchResult> {
197
+ const policy = guideFetchPolicy();
198
+ if (!isGuideFetchUrlAllowed(params.url))
199
+ return fetchFailure("fetch_forbidden", `URL ${params.url} is outside the HTTPS allowlist.`);
200
+ const timeoutMs = params.timeoutMs ?? policy.timeoutMs;
201
+ const fetchImpl = params.fetchImpl ?? fetch;
202
+ const effectivePolicy: GuideFetchPolicy = { ...policy, timeoutMs };
203
+
204
+ const manifestResponse = await boundedFetch(fetchImpl, params.url, effectivePolicy, policy.maxManifestBytes);
205
+ if (!manifestResponse.ok) return { ok: false, error: manifestResponse.error };
206
+ const signatureResponse = await boundedFetch(
207
+ fetchImpl,
208
+ `${params.url}.sig`,
209
+ effectivePolicy,
210
+ policy.maxSignatureBytes,
211
+ );
212
+ if (!signatureResponse.ok) return { ok: false, error: signatureResponse.error };
213
+
214
+ let manifestValue: unknown;
215
+ try {
216
+ manifestValue = parseFatalUtf8Json(manifestResponse.value.bytes);
217
+ } catch {
218
+ return fetchFailure("invalid_manifest", `Manifest at ${params.url} is not valid UTF-8 JSON.`);
219
+ }
220
+ const parsed = parseGuideManifest(manifestValue);
221
+ if (!parsed.ok)
222
+ return fetchFailure(parsed.error.code, `Manifest at ${params.url} is invalid: ${parsed.error.message}`);
223
+ return {
224
+ ok: true,
225
+ value: {
226
+ url: params.url,
227
+ manifest: parsed.manifest,
228
+ signatureBytes: signatureResponse.value.bytes,
229
+ fetchedAt: params.now ?? Date.now(),
230
+ },
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Trusted-by-compilation bundled manifests. The seed manifest is signed by the
236
+ * bundled pinned key (`verify.ts`), and every bundled advisory ships its text
237
+ * so fresh-install `list`/`show` work offline with no cache. The seed is
238
+ * selected only when no valid online or cached manifest exists; because the
239
+ * bundled manifest is itself signature-verified at module load, bundling the
240
+ * signature here preserves the normal verify path for bundled content.
241
+ */
242
+ const BUNDLED_GUIDE_MANIFEST_ID = "gajae-code-advisory-bundled";
243
+ const BUNDLED_GUIDE_SIGNATURE_HEX =
244
+ "682028a6834dfd2b39db57fb6cc185b9e2bdf21a3256d907fbbb8ddb20a0ce285f3a21b970df7d4055acfb2aede13d8f69931249c6d27fc092bfa2b60e936303";
245
+
246
+ const bundledGuideAdvisoryTexts: Readonly<Record<string, string>> = {
247
+ "getting-started":
248
+ "GJC ships a small set of signed advisory guides with the client so `gjc sdk guides list` and `gjc sdk guides show <guideId>` work offline on a fresh install. Guides are advisory text only: they are rendered for reading and never executed or applied as configuration. To receive newer guides, run `gjc sdk guides refresh --url <https manifest url>`. The manifest must come from the allowlisted HTTPS host and must be signed by a pinned Ed25519 key; a rejected refresh falls back to the last verified cache, then to the bundled seed, and any rejection is reported in the warnings list.",
249
+ "sdk/session-cli":
250
+ "`gjc sdk session` is the broker-bound command family for operating live GJC SDK sessions from the terminal: `list` enumerates managed sessions, `inspect` shows session details, `send` submits a prompt turn, `status` reports session health, and `tail` follows the event stream. The explicit `raw` hatch dispatches one SDK operation as `control`, `query`, or `global`. Authority resolves through the local broker and endpoint credentials are never rendered by the CLI.",
251
+ "troubleshooting/sdk-connection":
252
+ "When an SDK connection fails, first check that the session host is alive and healthy: `gjc sdk session status` reports readiness and liveness. If the broker is gone, restart it and re-list sessions; detached hosts are reaped after a bounded absence grace. Fetch-boundary failures (offline host, allowlisted URL violations, signature rejections) are reported as typed errors with exit code 1 so scripts can fail closed instead of silently serving unverified content.",
253
+ };
254
+
255
+ function bundledGuideSeedEntry(id: string, title: string): GuideEntryV1 {
256
+ const text = bundledGuideAdvisoryTexts[id];
257
+ if (text === undefined) throw new Error(`Bundled guide ${id} has no advisory text.`);
258
+ return { id, title, sha256: guideAdvisoryDigest(new TextEncoder().encode(text)) };
259
+ }
260
+
261
+ /**
262
+ * Trusted-by-compilation bundled manifests. The seed manifest is signed by the
263
+ * bundled pinned key (`verify.ts`), and every bundled advisory ships its text
264
+ * so fresh-install `list`/`show` work offline with no cache. The seed is
265
+ * selected only when no valid online or cached manifest exists; because the
266
+ * bundled manifest is itself signature-verified at module load, bundling the
267
+ * signature here preserves the normal verify path for bundled content.
268
+ */
269
+ export const BUNDLED_GUIDE_MANIFESTS: readonly GuideManifestV1[] = [
270
+ {
271
+ version: 1,
272
+ manifestId: BUNDLED_GUIDE_MANIFEST_ID,
273
+ keyId: GUIDE_PINNED_KEYS[0].keyId,
274
+ sequence: 1,
275
+ issuedAt: Date.UTC(2026, 0, 1),
276
+ expiresAt: Date.UTC(2036, 0, 1),
277
+ minimumSdkVersion: 1,
278
+ guides: [
279
+ bundledGuideSeedEntry("getting-started", "Getting started with the SDK advisory catalog"),
280
+ bundledGuideSeedEntry("sdk/session-cli", "Using the SDK session CLI"),
281
+ bundledGuideSeedEntry("troubleshooting/sdk-connection", "Troubleshooting SDK connection failures"),
282
+ ],
283
+ },
284
+ ];
285
+ for (const manifest of BUNDLED_GUIDE_MANIFESTS) {
286
+ for (const entry of manifest.guides) Object.freeze(entry);
287
+ Object.freeze(manifest.guides);
288
+ Object.freeze(manifest);
289
+ }
290
+ Object.freeze(BUNDLED_GUIDE_MANIFESTS);
291
+ {
292
+ const selfVerification = verifyGuideManifest({
293
+ manifest: BUNDLED_GUIDE_MANIFESTS[0],
294
+ signatureBytes: Buffer.from(BUNDLED_GUIDE_SIGNATURE_HEX, "hex"),
295
+ now: Date.UTC(2026, 0, 2),
296
+ });
297
+ if (!selfVerification.ok)
298
+ throw new Error(
299
+ `Bundled guide manifest failed self-verification: ${selfVerification.error.code} ${selfVerification.error.message}`,
300
+ );
301
+ }
302
+
303
+ export type GuideCatalogSource = "online" | "cache" | "bundled";
304
+
305
+ export interface GuideCatalogGuideV1 {
306
+ id: string;
307
+ title: string;
308
+ sha256: string;
309
+ /** Advisory text; undefined when the bundled manifest does not ship content for this guide. */
310
+ text: string | undefined;
311
+ }
312
+
313
+ export interface GuideCatalogSelection {
314
+ source: GuideCatalogSource;
315
+ manifest: GuideManifestV1;
316
+ guides: GuideCatalogGuideV1[];
317
+ /** Present when the selection is served from (or was installed into) the verified cache. */
318
+ cache?: VerifiedGuideCache;
319
+ /** Online provenance. */
320
+ url?: string;
321
+ fetchedAt?: number;
322
+ /** Non-fatal provenance notes (e.g. an online refresh was rejected and selection fell back). */
323
+ warnings: string[];
324
+ }
325
+
326
+ export type GuideCatalogResult =
327
+ | { ok: true; value: GuideCatalogSelection }
328
+ | { ok: false; error: { code: string; message: string } };
329
+
330
+ function catalogFailure(code: string, message: string): GuideCatalogResult {
331
+ return { ok: false, error: { code, message } };
332
+ }
333
+
334
+ export interface GuideCatalogOptions {
335
+ agentDir: string;
336
+ /** Online manifest URL; undefined disables the network entirely. */
337
+ onlineUrl?: string;
338
+ now?: () => number;
339
+ fetchImpl?: typeof fetch;
340
+ timeoutMs?: number;
341
+ /** Trusted-by-compilation bundled manifests; defaults to `BUNDLED_GUIDE_MANIFESTS`. */
342
+ disableBundled?: boolean;
343
+ }
344
+
345
+ /**
346
+ * Advisory guide catalog service.
347
+ *
348
+ * Selection precedence is `valid online -> valid cache -> bundled`: a freshly
349
+ * fetched, verified manifest wins; otherwise the verified cache; otherwise the
350
+ * bundled seed. Verification failures never throw — they surface as warnings
351
+ * (for online) or as selection fallback, and the cache is never destroyed.
352
+ */
353
+ export class GuideCatalog {
354
+ #agentDir: string;
355
+ #onlineUrl: string | undefined;
356
+ #now: () => number;
357
+ #fetchImpl: typeof fetch | undefined;
358
+ #timeoutMs: number | undefined;
359
+ #bundled: readonly GuideManifestV1[];
360
+
361
+ constructor(options: GuideCatalogOptions) {
362
+ this.#agentDir = options.agentDir;
363
+ this.#onlineUrl = options.onlineUrl;
364
+ this.#now = options.now ?? Date.now;
365
+ this.#fetchImpl = options.fetchImpl;
366
+ this.#timeoutMs =
367
+ options.timeoutMs === undefined
368
+ ? undefined
369
+ : Math.min(GUIDE_FETCH_TIMEOUT_MS, Math.max(1, Math.floor(options.timeoutMs)));
370
+ this.#bundled = options.disableBundled === true ? [] : BUNDLED_GUIDE_MANIFESTS;
371
+ }
372
+
373
+ /**
374
+ * Fetches, verifies, and installs the online manifest when `onlineUrl` is
375
+ * configured, then selects per the online/cache/bundled precedence. The
376
+ * verified cache is always the source of advisory text for online and
377
+ * cached selections, so a single verified read path serves both.
378
+ */
379
+ async refresh(): Promise<GuideCatalogResult> {
380
+ const warnings: string[] = [];
381
+ if (this.#onlineUrl !== undefined) {
382
+ const fetched = await fetchGuideManifestOnline({
383
+ url: this.#onlineUrl,
384
+ timeoutMs: this.#timeoutMs,
385
+ fetchImpl: this.#fetchImpl,
386
+ now: this.#now(),
387
+ });
388
+ if (!fetched.ok) {
389
+ warnings.push(`Online refresh rejected: ${fetched.error.code} ${fetched.error.message}`);
390
+ } else {
391
+ const verified = verifyGuideManifest({
392
+ manifest: fetched.value.manifest,
393
+ signatureBytes: fetched.value.signatureBytes,
394
+ now: this.#now(),
395
+ });
396
+ if (!verified.ok) {
397
+ warnings.push(`Online refresh rejected: ${verified.error.code} ${verified.error.message}`);
398
+ } else {
399
+ const advisories = await this.#fetchAdvisories(fetched.value);
400
+ if (!advisories.ok) {
401
+ warnings.push(`Online refresh rejected: ${advisories.error.code} ${advisories.error.message}`);
402
+ } else {
403
+ const installed = await installGuideCache({
404
+ agentDir: this.#agentDir,
405
+ manifest: fetched.value.manifest,
406
+ signatureBytes: fetched.value.signatureBytes,
407
+ advisories: advisories.value,
408
+ now: this.#now(),
409
+ });
410
+ if (!installed.ok) {
411
+ warnings.push(`Cache install rejected: ${installed.error.code} ${installed.error.message}`);
412
+ } else {
413
+ return this.#selectionFromCache(
414
+ installed.value,
415
+ "online",
416
+ fetched.value.url,
417
+ fetched.value.fetchedAt,
418
+ warnings,
419
+ );
420
+ }
421
+ }
422
+ }
423
+ }
424
+ }
425
+ return this.#selectFromCacheOrBundled(
426
+ warnings,
427
+ "No verified online, cached, or bundled guide manifest is available.",
428
+ );
429
+ }
430
+
431
+ /** Offline selection: verified cache first, then bundled. Never touches the network. */
432
+ async load(): Promise<GuideCatalogResult> {
433
+ return this.#selectFromCacheOrBundled([], "No verified cached or bundled guide manifest is available.");
434
+ }
435
+
436
+ /**
437
+ * Returns the advisory text for a guide in the currently selected
438
+ * manifest. Advisory text is data only: it is rendered and never executed
439
+ * or applied as configuration.
440
+ */
441
+ async advisory(
442
+ guideId: string,
443
+ ): Promise<
444
+ | { ok: true; value: { source: GuideCatalogSource; guideId: string; title: string; text: string } }
445
+ | { ok: false; error: { code: string; message: string } }
446
+ > {
447
+ const selection = await this.load();
448
+ if (!selection.ok) return { ok: false, error: selection.error };
449
+ const guide = selection.value.guides.find(candidate => candidate.id === guideId);
450
+ if (!guide)
451
+ return {
452
+ ok: false,
453
+ error: { code: "not_found", message: `Guide ${guideId} is not in the selected manifest.` },
454
+ };
455
+ if (guide.text === undefined)
456
+ return {
457
+ ok: false,
458
+ error: {
459
+ code: "unavailable",
460
+ message: `Guide ${guideId} is bundled-only; no advisory text is shipped with the client.`,
461
+ },
462
+ };
463
+ return {
464
+ ok: true,
465
+ value: { source: selection.value.source, guideId, title: guide.title, text: guide.text },
466
+ };
467
+ }
468
+
469
+ async #selectFromCacheOrBundled(warnings: string[], unavailableMessage: string): Promise<GuideCatalogResult> {
470
+ const cache = await readGuideCache({ agentDir: this.#agentDir, now: this.#now() });
471
+ if (cache.ok) return this.#selectionFromCache(cache.value, "cache", undefined, undefined, warnings);
472
+ if (cache.error.code !== "missing_cache")
473
+ warnings.push(`Cache rejected: ${cache.error.code} ${cache.error.message}`);
474
+ const bundled = this.#bundled[0];
475
+ if (bundled)
476
+ return {
477
+ ok: true,
478
+ value: {
479
+ source: "bundled",
480
+ manifest: bundled,
481
+ guides: bundled.guides.map(entry => ({
482
+ id: entry.id,
483
+ title: entry.title,
484
+ sha256: entry.sha256,
485
+ text: bundledGuideAdvisoryTexts[entry.id],
486
+ })),
487
+ warnings,
488
+ },
489
+ };
490
+ return catalogFailure(
491
+ "unavailable",
492
+ warnings.length === 0 ? unavailableMessage : `${unavailableMessage} ${warnings.join("; ")}`,
493
+ );
494
+ }
495
+
496
+ async #fetchAdvisories(
497
+ online: GuideOnlineManifest,
498
+ ): Promise<
499
+ | { ok: true; value: { entry: GuideEntryV1; text: Uint8Array }[] }
500
+ | { ok: false; error: { code: string; message: string; status?: number } }
501
+ > {
502
+ const policy = guideFetchPolicy();
503
+ const effectivePolicy: GuideFetchPolicy = { ...policy, timeoutMs: this.#timeoutMs ?? policy.timeoutMs };
504
+ const fetchImpl = this.#fetchImpl ?? fetch;
505
+ const advisories: { entry: GuideEntryV1; text: Uint8Array }[] = [];
506
+ for (const entry of online.manifest.guides) {
507
+ let url: string;
508
+ try {
509
+ url = new URL(`guides/${entry.id}`, online.url).href;
510
+ } catch {
511
+ return {
512
+ ok: false,
513
+ error: { code: "invalid_manifest", message: `Advisory URL for ${entry.id} could not be resolved.` },
514
+ };
515
+ }
516
+ if (!isGuideFetchUrlAllowed(url))
517
+ return {
518
+ ok: false,
519
+ error: { code: "fetch_forbidden", message: `Advisory URL ${url} is outside the HTTPS allowlist.` },
520
+ };
521
+ const fetched = await boundedFetch(fetchImpl, url, effectivePolicy, GUIDE_ADVISORY_MAX_BYTES);
522
+ if (!fetched.ok) return fetched;
523
+ const binding = verifyGuideAdvisoryText(entry, fetched.value.bytes);
524
+ if (!binding.ok) return binding;
525
+ advisories.push({ entry, text: fetched.value.bytes });
526
+ }
527
+ return { ok: true, value: advisories };
528
+ }
529
+
530
+ #selectionFromCache(
531
+ cache: VerifiedGuideCache,
532
+ source: "online" | "cache",
533
+ url: string | undefined,
534
+ fetchedAt: number | undefined,
535
+ warnings: string[],
536
+ ): GuideCatalogResult {
537
+ return {
538
+ ok: true,
539
+ value: {
540
+ source,
541
+ manifest: cache.manifest,
542
+ guides: cache.guides.map(guide => ({
543
+ id: guide.id,
544
+ title: guide.title,
545
+ sha256: guide.sha256,
546
+ text: guide.text,
547
+ })),
548
+ cache,
549
+ ...(url === undefined ? {} : { url, fetchedAt }),
550
+ warnings,
551
+ },
552
+ };
553
+ }
554
+ }