@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,109 @@
1
+ import * as fs from "node:fs/promises";
2
+ import { isPidAlive } from "../broker/discovery";
3
+ import { isProcessIncarnation, processIncarnation } from "../broker/process-incarnation";
4
+
5
+ /**
6
+ * Broker-owner principal.
7
+ *
8
+ * The elevation authority is the broker itself under the existing loopback
9
+ * trust model: one local OS-user/broker authority derived entirely from
10
+ * broker-owned state, never from caller input. `agentDirUid` is the uid that
11
+ * owns the agent directory, `brokerPid`/`brokerIncarnation` identify the
12
+ * broker process tenure so a restarted broker cannot attest the same
13
+ * authority.
14
+ */
15
+ export interface BrokerOwnerPrincipal {
16
+ kind: "broker_owner";
17
+ owner: {
18
+ agentDirUid: number;
19
+ brokerPid: number;
20
+ brokerIncarnation: string;
21
+ };
22
+ }
23
+
24
+ export function isBrokerOwnerPrincipal(value: unknown): value is BrokerOwnerPrincipal {
25
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
26
+ const principal = value as { kind?: unknown; owner?: unknown };
27
+ if (principal.kind !== "broker_owner") return false;
28
+ if (typeof principal.owner !== "object" || principal.owner === null) return false;
29
+ const owner = principal.owner as { agentDirUid?: unknown; brokerPid?: unknown; brokerIncarnation?: unknown };
30
+ return (
31
+ Number.isSafeInteger(owner.agentDirUid) &&
32
+ (owner.agentDirUid as number) >= 0 &&
33
+ Number.isSafeInteger(owner.brokerPid) &&
34
+ (owner.brokerPid as number) > 0 &&
35
+ isProcessIncarnation(owner.brokerIncarnation)
36
+ );
37
+ }
38
+
39
+ export function sameBrokerOwnerPrincipal(left: BrokerOwnerPrincipal, right: BrokerOwnerPrincipal): boolean {
40
+ return (
41
+ left.kind === right.kind &&
42
+ left.owner.agentDirUid === right.owner.agentDirUid &&
43
+ left.owner.brokerPid === right.owner.brokerPid &&
44
+ left.owner.brokerIncarnation === right.owner.brokerIncarnation
45
+ );
46
+ }
47
+
48
+ /**
49
+ * Reads the broker-owner principal from broker-owned state. Throws when the
50
+ * broker cannot attest a canonical process incarnation (the broker refuses to
51
+ * start in that case anyway), so callers treat a throw as "authority
52
+ * unavailable".
53
+ */
54
+ export async function readBrokerOwnerPrincipal(
55
+ agentDir: string,
56
+ brokerPid: number,
57
+ brokerIncarnation: string,
58
+ ): Promise<BrokerOwnerPrincipal> {
59
+ if (!Number.isSafeInteger(brokerPid) || brokerPid <= 0)
60
+ throw new Error("Broker owner principal requires a live broker pid");
61
+ if (!isProcessIncarnation(brokerIncarnation))
62
+ throw new Error("Broker owner principal requires a canonical broker process incarnation");
63
+ const stat = await fs.stat(agentDir);
64
+ return {
65
+ kind: "broker_owner",
66
+ owner: {
67
+ agentDirUid: stat.uid,
68
+ brokerPid,
69
+ brokerIncarnation,
70
+ },
71
+ };
72
+ }
73
+
74
+ export type ProcessIncarnationLiveness = "alive" | "dead" | "unknown";
75
+
76
+ /**
77
+ * Tri-state liveness of a process incarnation.
78
+ *
79
+ * - `alive`: the pid is running and its current incarnation matches the
80
+ * stored one, so the original process tenure is still live.
81
+ * - `dead`: ESRCH (the pid is provably gone — independent of any persisted
82
+ * incarnation text), or the pid is running under a *different* canonical
83
+ * incarnation (PID reuse), which provably means the original process
84
+ * ended.
85
+ * - `unknown`: the pid is malformed, the persisted or observed incarnation
86
+ * is malformed/undefined, or the current incarnation cannot be read.
87
+ * Unknown never implies death: elevation claims must fail closed with
88
+ * `elevation_claim_in_progress` instead of terminalizing state on
89
+ * unprovable liveness.
90
+ *
91
+ * ESRCH is independent positive death proof: a pid that provably does not
92
+ * exist is dead regardless of the persisted incarnation text, so a corrupt
93
+ * persisted incarnation can never stall recovery forever.
94
+ */
95
+ export function classifyProcessIncarnationLiveness(pid: unknown, incarnation: unknown): ProcessIncarnationLiveness {
96
+ if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0) return "unknown";
97
+ // ESRCH is positive death proof even with malformed persisted
98
+ // incarnation text (AC-L6): the pid no longer exists, so the original
99
+ // tenure provably ended and recovery may proceed.
100
+ if (!isPidAlive(pid)) return "dead";
101
+ // The pid is alive (or EPERM): a malformed/undefined persisted
102
+ // incarnation cannot be compared, and a malformed/undefined observed
103
+ // incarnation cannot be trusted, so liveness is unprovable. Unknown
104
+ // never authorizes mutation.
105
+ if (!isProcessIncarnation(incarnation)) return "unknown";
106
+ const current = processIncarnation(pid);
107
+ if (!isProcessIncarnation(current)) return "unknown";
108
+ return current === incarnation ? "alive" : "dead";
109
+ }
@@ -0,0 +1,363 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import * as fsSync from "node:fs";
3
+ import * as fs from "node:fs/promises";
4
+ import * as path from "node:path";
5
+ import { withFileLock } from "../../config/file-lock";
6
+ import { assertSupportedStateVersion, SDK_STATE_VERSION } from "../broker/state-version";
7
+ import {
8
+ canonicalGuideManifestBytes,
9
+ GUIDE_ADVISORY_MAX_BYTES,
10
+ GUIDE_MANIFEST_MAX_BYTES,
11
+ type GuideEntryV1,
12
+ type GuideManifestV1,
13
+ parseGuideManifest,
14
+ } from "./manifest";
15
+ import {
16
+ type GuideVerificationErrorCode,
17
+ guideRollbackCheck,
18
+ verifyGuideAdvisoryText,
19
+ verifyGuideManifest,
20
+ } from "./verify";
21
+
22
+ /**
23
+ * Verified on-disk cache for trusted advisory guides.
24
+ *
25
+ * Layout under `<agentDir>/sdk/guides/cache/` (mode 0700):
26
+ * meta.json — atomic commit pointer {version, manifestId, sequence, generation, installedAt}
27
+ * meta.json.lock — cross-process install lock (project `withFileLock` convention)
28
+ * generations/<generation>/manifest.json — canonical manifest bytes
29
+ * generations/<generation>/manifest.sig — detached Ed25519 signature
30
+ * generations/<generation>/guides/<sha256> — immutable advisory text
31
+ *
32
+ * A complete generation is written and fsynced before `meta.json` is atomically
33
+ * replaced under the install lock. Readers follow only the committed
34
+ * generation, so an interrupted install cannot expose a new manifest with
35
+ * stale metadata or disturb the previously valid cache.
36
+ */
37
+ export interface GuideCacheMetaV1 {
38
+ version: typeof SDK_STATE_VERSION;
39
+ manifestId: string;
40
+ sequence: number;
41
+ installedAt: number;
42
+ generation: string;
43
+ }
44
+
45
+ export interface GuideCacheGuideV1 {
46
+ id: string;
47
+ title: string;
48
+ sha256: string;
49
+ text: string;
50
+ }
51
+
52
+ export interface VerifiedGuideCache {
53
+ manifest: GuideManifestV1;
54
+ signatureBytes: Uint8Array;
55
+ meta: GuideCacheMetaV1;
56
+ guides: GuideCacheGuideV1[];
57
+ }
58
+
59
+ export type GuideCacheReadResult =
60
+ | { ok: true; value: VerifiedGuideCache }
61
+ | {
62
+ ok: false;
63
+ error: {
64
+ code: "missing_cache" | "corrupt_cache" | "expired_cache" | "unsupported_state_version";
65
+ message: string;
66
+ };
67
+ };
68
+
69
+ export type GuideCacheInstallResult =
70
+ | { ok: true; value: VerifiedGuideCache }
71
+ | { ok: false; error: { code: GuideVerificationErrorCode | "invalid_input" | "io_error"; message: string } };
72
+
73
+ const GUIDE_CACHE_META_MAX_BYTES = 64 * 1024;
74
+
75
+ const guideUtf8Decoder = new TextDecoder("utf-8", { fatal: true });
76
+
77
+ export function guideCacheDir(agentDir: string): string {
78
+ return path.join(agentDir, "sdk", "guides", "cache");
79
+ }
80
+
81
+ function cacheFailure(
82
+ code: "missing_cache" | "corrupt_cache" | "expired_cache" | "unsupported_state_version",
83
+ message: string,
84
+ ): GuideCacheReadResult {
85
+ return { ok: false, error: { code, message } };
86
+ }
87
+
88
+ function isGuideCacheMetaV1(value: unknown): value is GuideCacheMetaV1 {
89
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
90
+ const record = value as Partial<GuideCacheMetaV1>;
91
+ return (
92
+ record.version === SDK_STATE_VERSION &&
93
+ typeof record.manifestId === "string" &&
94
+ record.manifestId.length > 0 &&
95
+ Number.isSafeInteger(record.sequence) &&
96
+ (record.sequence as number) >= 1 &&
97
+ Number.isSafeInteger(record.installedAt) &&
98
+ (record.installedAt as number) >= 0 &&
99
+ /^[a-zA-Z0-9._-]+$/.test(record.generation ?? "")
100
+ );
101
+ }
102
+
103
+ /** Reads a file with O_NOFOLLOW, bounds its size, and returns raw bytes; undefined when absent. */
104
+ async function readBoundedBytes(file: string, maxBytes: number): Promise<Uint8Array | undefined> {
105
+ let handle: fs.FileHandle | undefined;
106
+ try {
107
+ handle = await fs.open(file, fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW);
108
+ const stat = await handle.stat({ bigint: true });
109
+ if (!stat.isFile()) throw new Error(`Cache target is not a regular file: ${file}`);
110
+ if (stat.size > BigInt(maxBytes)) throw new Error(`Cache file exceeds the maximum byte length: ${file}`);
111
+ const bytes = Buffer.alloc(Number(stat.size) + 1);
112
+ const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
113
+ return bytes.subarray(0, bytesRead);
114
+ } catch (error) {
115
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
116
+ throw error;
117
+ } finally {
118
+ if (handle) await handle.close();
119
+ }
120
+ }
121
+
122
+ function parseFatalUtf8Json(bytes: Uint8Array): unknown {
123
+ return JSON.parse(guideUtf8Decoder.decode(bytes));
124
+ }
125
+
126
+ function corruptCache(message: string): GuideCacheReadResult {
127
+ return cacheFailure("corrupt_cache", message);
128
+ }
129
+
130
+ /**
131
+ * Reads and re-verifies the cache end to end: manifest parse, meta commit
132
+ * record, detached signature, expiry against `now`, and every advisory hash.
133
+ * Any failure is reported as `corrupt_cache` (or `expired_cache` when the
134
+ * signature is valid but stale) and the cache is left untouched — a corrupt
135
+ * entry never destroys the data a future, still-valid install may replace.
136
+ */
137
+ export async function readGuideCache(params: { agentDir: string; now: number }): Promise<GuideCacheReadResult> {
138
+ const dir = guideCacheDir(params.agentDir);
139
+ try {
140
+ const metaRaw = await readBoundedBytes(path.join(dir, "meta.json"), GUIDE_CACHE_META_MAX_BYTES);
141
+ if (metaRaw === undefined) return cacheFailure("missing_cache", "No guide cache has been installed.");
142
+ let metaValue: unknown;
143
+ try {
144
+ metaValue = parseFatalUtf8Json(metaRaw);
145
+ } catch {
146
+ return corruptCache("Guide cache meta.json is not valid UTF-8 JSON.");
147
+ }
148
+ try {
149
+ assertSupportedStateVersion(path.join(dir, "meta.json"), metaValue);
150
+ } catch (error) {
151
+ if (error instanceof Error && "code" in error && error.code === "unsupported_state_version")
152
+ return cacheFailure("unsupported_state_version", error.message);
153
+ throw error;
154
+ }
155
+ if (!isGuideCacheMetaV1(metaValue)) return corruptCache("Guide cache meta.json is malformed.");
156
+ const meta = metaValue;
157
+ const generationDir = path.join(dir, "generations", meta.generation);
158
+
159
+ const manifestRaw = await readBoundedBytes(path.join(generationDir, "manifest.json"), GUIDE_MANIFEST_MAX_BYTES);
160
+ if (manifestRaw === undefined) return corruptCache("Guide cache manifest.json is missing.");
161
+ let manifestValue: unknown;
162
+ try {
163
+ manifestValue = parseFatalUtf8Json(manifestRaw);
164
+ } catch {
165
+ return corruptCache("Guide cache manifest.json is not valid UTF-8 JSON.");
166
+ }
167
+ const parsed = parseGuideManifest(manifestValue);
168
+ if (!parsed.ok) return corruptCache(`Guide cache manifest is invalid: ${parsed.error.message}`);
169
+ const manifest = parsed.manifest;
170
+ if (manifest.manifestId !== meta.manifestId)
171
+ return corruptCache("Guide cache manifest manifestId does not match meta.json.");
172
+
173
+ const signatureRaw = await readBoundedBytes(path.join(generationDir, "manifest.sig"), 1024);
174
+ if (signatureRaw === undefined) return corruptCache("Guide cache manifest.sig is missing.");
175
+
176
+ const verified = verifyGuideManifest({ manifest, signatureBytes: signatureRaw, now: params.now });
177
+ if (!verified.ok) {
178
+ if (verified.error.code === "expired")
179
+ return cacheFailure("expired_cache", `Guide cache is expired: ${verified.error.message}`);
180
+ return corruptCache(`Guide cache failed verification: ${verified.error.message}`);
181
+ }
182
+
183
+ const guides: GuideCacheGuideV1[] = [];
184
+ for (const entry of manifest.guides) {
185
+ const advisoryRaw = await readBoundedBytes(
186
+ path.join(generationDir, "guides", entry.sha256),
187
+ GUIDE_ADVISORY_MAX_BYTES,
188
+ );
189
+ if (advisoryRaw === undefined) return corruptCache(`Guide cache is missing advisory content for ${entry.id}.`);
190
+ const binding = verifyGuideAdvisoryText(entry, advisoryRaw);
191
+ if (!binding.ok) return corruptCache(`Guide cache advisory failed verification: ${binding.error.message}`);
192
+ let text: string;
193
+ try {
194
+ text = guideUtf8Decoder.decode(advisoryRaw);
195
+ } catch {
196
+ return corruptCache(`Guide cache advisory ${entry.id} is not valid UTF-8.`);
197
+ }
198
+ guides.push({ id: entry.id, title: entry.title, sha256: entry.sha256, text });
199
+ }
200
+ return { ok: true, value: { manifest, signatureBytes: signatureRaw, meta, guides } };
201
+ } catch (error) {
202
+ return corruptCache(`Guide cache could not be read: ${error instanceof Error ? error.message : String(error)}`);
203
+ }
204
+ }
205
+
206
+ function installFailure(
207
+ code: GuideVerificationErrorCode | "invalid_input" | "io_error",
208
+ message: string,
209
+ ): GuideCacheInstallResult {
210
+ return { ok: false, error: { code, message } };
211
+ }
212
+
213
+ async function writeVerifiedTemp(file: string, bytes: Uint8Array): Promise<void> {
214
+ const temporary = `${file}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
215
+ try {
216
+ const handle = await fs.open(
217
+ temporary,
218
+ fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL | fsSync.constants.O_NOFOLLOW,
219
+ 0o600,
220
+ );
221
+ try {
222
+ await handle.writeFile(bytes);
223
+ await handle.sync();
224
+ } finally {
225
+ await handle.close();
226
+ }
227
+ await fs.rename(temporary, file);
228
+ } catch (error) {
229
+ await fs.unlink(temporary).catch(() => {});
230
+ throw error;
231
+ }
232
+ }
233
+
234
+ async function syncDirectory(dir: string): Promise<void> {
235
+ const directory = await fs.open(dir, fsSync.constants.O_RDONLY);
236
+ try {
237
+ await directory.sync();
238
+ } finally {
239
+ await directory.close();
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Installs a verified manifest plus its advisory content into the cache.
245
+ *
246
+ * Order of operations:
247
+ * 1. Verify the manifest signature, expiry, and every advisory hash before
248
+ * any write — a tampered or expired payload never touches disk.
249
+ * 2. Under the cache's cross-process install lock, enforce the monotonic
250
+ * version floor against the committed meta record (`rollback` when the
251
+ * candidate does not advance the channel).
252
+ * 3. Write and fsync a complete immutable generation, then atomically replace
253
+ * `meta.json` as the only commit pointer.
254
+ *
255
+ * Steps 2–3 are serialized with `<cacheDir>/meta.json.lock` (the project's
256
+ * `withFileLock` convention): the floor is re-read under the same lock that
257
+ * guards the commit pointer replacement, so a concurrent install can never
258
+ * read an older floor, observe a newer commit, and then downgrade the channel.
259
+ * Any failure aborts before the commit, leaving the prior valid cache fully
260
+ * intact and readable.
261
+ */
262
+ export async function installGuideCache(params: {
263
+ agentDir: string;
264
+ manifest: GuideManifestV1;
265
+ signatureBytes: Uint8Array;
266
+ advisories: readonly { entry: GuideEntryV1; text: Uint8Array }[];
267
+ now: number;
268
+ }): Promise<GuideCacheInstallResult> {
269
+ const { manifest, signatureBytes, advisories, now } = params;
270
+ const verified = verifyGuideManifest({ manifest, signatureBytes, now });
271
+ if (!verified.ok) return installFailure(verified.error.code, verified.error.message);
272
+
273
+ if (advisories.length !== manifest.guides.length)
274
+ return installFailure(
275
+ "invalid_input",
276
+ `Advisory count ${advisories.length} does not match the manifest guide count ${manifest.guides.length}.`,
277
+ );
278
+ const advisoryById = new Map(advisories.map(advisory => [advisory.entry.id, advisory]));
279
+ for (const entry of manifest.guides) {
280
+ const advisory = advisoryById.get(entry.id);
281
+ if (!advisory) return installFailure("invalid_input", `Advisory text is missing for manifest entry ${entry.id}.`);
282
+ if (advisory.text.byteLength > GUIDE_ADVISORY_MAX_BYTES)
283
+ return installFailure("invalid_input", `Advisory ${entry.id} exceeds the maximum byte length.`);
284
+ const binding = verifyGuideAdvisoryText(entry, advisory.text);
285
+ if (!binding.ok) return installFailure(binding.error.code, binding.error.message);
286
+ }
287
+
288
+ const dir = guideCacheDir(params.agentDir);
289
+ const metaPath = path.join(dir, "meta.json");
290
+ try {
291
+ return await withFileLock(metaPath, async () => {
292
+ let priorMeta: GuideCacheMetaV1 | undefined;
293
+ try {
294
+ const metaRaw = await readBoundedBytes(metaPath, GUIDE_CACHE_META_MAX_BYTES);
295
+ if (metaRaw !== undefined) {
296
+ let metaValue: unknown;
297
+ try {
298
+ metaValue = parseFatalUtf8Json(metaRaw);
299
+ } catch {
300
+ return installFailure("corrupt_cache", "Existing guide cache meta.json is not valid UTF-8 JSON.");
301
+ }
302
+ try {
303
+ assertSupportedStateVersion(metaPath, metaValue);
304
+ } catch (error) {
305
+ if (error instanceof Error && "code" in error && error.code === "unsupported_state_version")
306
+ return installFailure("unsupported_state_version", error.message);
307
+ throw error;
308
+ }
309
+ if (!isGuideCacheMetaV1(metaValue))
310
+ return installFailure("corrupt_cache", "Existing guide cache meta.json is malformed.");
311
+ priorMeta = metaValue;
312
+ }
313
+ } catch (error) {
314
+ return installFailure(
315
+ "io_error",
316
+ `Guide cache meta could not be read: ${error instanceof Error ? error.message : String(error)}`,
317
+ );
318
+ }
319
+
320
+ const floor = guideRollbackCheck(priorMeta, manifest);
321
+ if (!floor.ok) return installFailure(floor.error.code, floor.error.message);
322
+
323
+ const generation = `${manifest.sequence}-${randomUUID()}`;
324
+ const generationDir = path.join(dir, "generations", generation);
325
+ const guidesDir = path.join(generationDir, "guides");
326
+ await fs.mkdir(guidesDir, { recursive: true, mode: 0o700 });
327
+ for (const advisory of advisories) {
328
+ await writeVerifiedTemp(path.join(guidesDir, advisory.entry.sha256), advisory.text);
329
+ }
330
+ await writeVerifiedTemp(path.join(generationDir, "manifest.json"), canonicalGuideManifestBytes(manifest));
331
+ await writeVerifiedTemp(path.join(generationDir, "manifest.sig"), Buffer.from(signatureBytes));
332
+ await syncDirectory(generationDir);
333
+ const meta: GuideCacheMetaV1 = {
334
+ version: SDK_STATE_VERSION,
335
+ manifestId: manifest.manifestId,
336
+ sequence: manifest.sequence,
337
+ installedAt: now,
338
+ generation,
339
+ };
340
+ await writeVerifiedTemp(metaPath, Buffer.from(`${JSON.stringify(meta)}\n`));
341
+ await syncDirectory(dir);
342
+ return {
343
+ ok: true,
344
+ value: {
345
+ manifest,
346
+ signatureBytes,
347
+ meta,
348
+ guides: advisories.map(advisory => ({
349
+ id: advisory.entry.id,
350
+ title: advisory.entry.title,
351
+ sha256: advisory.entry.sha256,
352
+ text: guideUtf8Decoder.decode(advisory.text),
353
+ })),
354
+ },
355
+ };
356
+ });
357
+ } catch (error) {
358
+ return installFailure(
359
+ "io_error",
360
+ `Guide cache install failed: ${error instanceof Error ? error.message : String(error)}`,
361
+ );
362
+ }
363
+ }