@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.
- package/CHANGELOG.md +67 -2
- package/dist/types/cli/args.d.ts +5 -2
- package/dist/types/cli/root-flags.d.ts +246 -0
- package/dist/types/cli.d.ts +11 -19
- package/dist/types/commands/daemon.d.ts +0 -30
- package/dist/types/commands/launch.d.ts +11 -19
- package/dist/types/commands/quick-lane.d.ts +32 -0
- package/dist/types/commands/sdk.d.ts +21 -15
- package/dist/types/config/file-lock-gc.d.ts +2 -0
- package/dist/types/config/model-equivalence.d.ts +20 -0
- package/dist/types/config/model-profile-activation.d.ts +62 -17
- package/dist/types/config/model-profiles.d.ts +10 -0
- package/dist/types/config/model-registry.d.ts +68 -1
- package/dist/types/config/model-resolver.d.ts +15 -4
- package/dist/types/config/models-config-schema.d.ts +7 -2
- package/dist/types/config/provider-selection-policy.d.ts +62 -0
- package/dist/types/config/settings-schema.d.ts +36 -78
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/edit/renderer.d.ts +5 -2
- package/dist/types/edit/streaming.d.ts +13 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
- package/dist/types/extensibility/extensions/compact-handler.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +19 -5
- package/dist/types/internal-urls/local-protocol.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +0 -2
- package/dist/types/modes/components/provider-order-context.d.ts +45 -0
- package/dist/types/modes/components/provider-order-editor.d.ts +24 -0
- package/dist/types/modes/components/settings-defs.d.ts +5 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/quick-lane/classify.d.ts +40 -0
- package/dist/types/runtime/stt-settings-setup.d.ts +18 -0
- package/dist/types/sdk/broker/broker.d.ts +56 -2
- package/dist/types/sdk/broker/operation-receipt-ledger.d.ts +25 -0
- package/dist/types/sdk/broker/session-index.d.ts +83 -2
- package/dist/types/sdk/broker/state-version.d.ts +1 -1
- package/dist/types/sdk/bus/notification-service.d.ts +6 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +8 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +26 -0
- package/dist/types/sdk/cli/rows.d.ts +98 -0
- package/dist/types/sdk/cli/session-cli.d.ts +35 -3
- package/dist/types/sdk/elevation/allowlist.d.ts +20 -0
- package/dist/types/sdk/elevation/audit.d.ts +93 -0
- package/dist/types/sdk/elevation/capability.d.ts +3 -0
- package/dist/types/sdk/elevation/digest.d.ts +72 -0
- package/dist/types/sdk/elevation/dispatch-receipt.d.ts +85 -0
- package/dist/types/sdk/elevation/grant-ledger.d.ts +163 -0
- package/dist/types/sdk/elevation/owner.d.ts +48 -0
- package/dist/types/sdk/guides/cache.d.ts +98 -0
- package/dist/types/sdk/guides/catalog.d.ts +156 -0
- package/dist/types/sdk/guides/cli.d.ts +39 -0
- package/dist/types/sdk/guides/manifest.d.ts +66 -0
- package/dist/types/sdk/guides/verify.d.ts +66 -0
- package/dist/types/sdk/host/control/dispatch.d.ts +8 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +3 -2
- package/dist/types/sdk/host/query/cursor.d.ts +10 -0
- package/dist/types/sdk/host/query/handlers.d.ts +21 -0
- package/dist/types/sdk/host/session-runtime.d.ts +9 -0
- package/dist/types/sdk/mcp/server.d.ts +2 -2
- package/dist/types/sdk/prompt-status.d.ts +11 -0
- package/dist/types/sdk/session.d.ts +4 -0
- package/dist/types/sdk/transport/index.d.ts +1 -0
- package/dist/types/sdk/transport/relay.d.ts +1 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +20 -1
- package/dist/types/session/agent-session.d.ts +32 -18
- package/dist/types/session/blob-store.d.ts +13 -7
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +25 -1
- package/dist/types/session/internal/session-memory-sidecar.d.ts +5 -5
- package/dist/types/session/session-manager.d.ts +139 -10
- package/dist/types/session/session-storage.d.ts +48 -0
- package/dist/types/setup/provider-onboarding.d.ts +7 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +14 -0
- package/dist/types/stt/setup.d.ts +2 -0
- package/dist/types/task/executor.d.ts +10 -2
- package/dist/types/tools/browser/runtime-diagnostics.d.ts +65 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/browser.d.ts +2 -0
- package/dist/types/tools/image-gen.d.ts +14 -18
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resource-gc.d.ts +2 -0
- package/dist/types/utils/pasted-image-path.d.ts +6 -0
- package/package.json +9 -9
- package/scripts/generate-sdk-operation-inventory.ts +8 -0
- package/scripts/run-test-manifest.ts +61 -21
- package/scripts/verify-gjc-sdk-canonicalization.ts +9 -4
- package/src/cli/args.ts +62 -9
- package/src/cli/completion-cli.ts +1 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/root-flags.ts +100 -0
- package/src/cli/setup-cli.ts +6 -2
- package/src/cli.ts +42 -65
- package/src/commands/acp.ts +1 -1
- package/src/commands/daemon.ts +2 -31
- package/src/commands/gc.ts +1 -1
- package/src/commands/launch.ts +8 -129
- package/src/commands/quick-lane.ts +51 -0
- package/src/commands/sdk.ts +200 -26
- package/src/commands/setup.ts +4 -2
- package/src/config/file-lock-gc.ts +4 -1
- package/src/config/model-equivalence.ts +76 -7
- package/src/config/model-profile-activation.ts +1115 -265
- package/src/config/model-profiles.ts +134 -2
- package/src/config/model-registry.ts +352 -50
- package/src/config/model-resolver.ts +289 -38
- package/src/config/models-config-schema.ts +11 -4
- package/src/config/provider-selection-policy.ts +146 -0
- package/src/config/settings-schema.ts +50 -77
- package/src/config/settings.ts +40 -0
- package/src/coordinator-mcp/server.ts +32 -2
- package/src/edit/renderer.ts +175 -223
- package/src/edit/streaming.ts +108 -1
- package/src/export/html/index.ts +57 -7
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/extensions/compact-handler.ts +3 -2
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/gjc-runtime/gc-runtime.ts +379 -20
- package/src/gjc-runtime/launch-worktree.ts +5 -1
- package/src/gjc-runtime/team-runtime.ts +59 -2
- package/src/gjc-runtime/tmux-owner-isolation.ts +19 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +17 -13
- package/src/internal-urls/local-protocol.ts +1 -0
- package/src/main.ts +40 -3
- package/src/memories/index.ts +4 -2
- package/src/modes/acp/acp-agent.ts +235 -91
- package/src/modes/components/model-selector.ts +206 -42
- package/src/modes/components/provider-order-context.ts +197 -0
- package/src/modes/components/provider-order-editor.ts +441 -0
- package/src/modes/components/settings-defs.ts +21 -1
- package/src/modes/components/settings-selector.ts +45 -2
- package/src/modes/components/tool-execution.ts +47 -15
- package/src/modes/controllers/command-controller.ts +6 -3
- package/src/modes/controllers/extension-ui-controller.ts +2 -0
- package/src/modes/controllers/input-controller.ts +73 -2
- package/src/modes/controllers/selector-controller.ts +260 -153
- package/src/modes/prompt-suggestion-controller.ts +1 -1
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +18 -0
- package/src/prompts/tools/browser.md +1 -0
- package/src/quick-lane/classify.ts +257 -0
- package/src/rlm/index.ts +1 -1
- package/src/runtime/stt-settings-setup.ts +57 -0
- package/src/sdk/acp/adapter.ts +2 -1
- package/src/sdk/broker/broker.ts +780 -12
- package/src/sdk/broker/lifecycle.ts +70 -29
- package/src/sdk/broker/operation-receipt-ledger.ts +144 -0
- package/src/sdk/broker/session-index.ts +463 -48
- package/src/sdk/broker/state-version.ts +13 -9
- package/src/sdk/broker/transport.ts +14 -1
- package/src/sdk/bus/index.ts +34 -13
- package/src/sdk/bus/kind-aware-reconciliation.ts +21 -2
- package/src/sdk/bus/notification-service.ts +47 -1
- package/src/sdk/bus/prompt-reconciliation.ts +22 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
- package/src/sdk/bus/telegram-daemon.ts +54 -3
- package/src/sdk/cli/rows.ts +247 -0
- package/src/sdk/cli/session-cli.ts +930 -79
- package/src/sdk/elevation/allowlist.ts +54 -0
- package/src/sdk/elevation/audit.ts +208 -0
- package/src/sdk/elevation/capability.ts +41 -0
- package/src/sdk/elevation/digest.ts +129 -0
- package/src/sdk/elevation/dispatch-receipt.ts +184 -0
- package/src/sdk/elevation/grant-ledger.ts +1142 -0
- package/src/sdk/elevation/owner.ts +109 -0
- package/src/sdk/guides/cache.ts +363 -0
- package/src/sdk/guides/catalog.ts +554 -0
- package/src/sdk/guides/cli.ts +235 -0
- package/src/sdk/guides/manifest.ts +152 -0
- package/src/sdk/guides/verify.ts +178 -0
- package/src/sdk/host/control/dispatch.ts +49 -1
- package/src/sdk/host/control/index.ts +1 -0
- package/src/sdk/host/control/operations.ts +8 -1
- package/src/sdk/host/query/cursor.ts +47 -3
- package/src/sdk/host/query/handlers.ts +155 -2
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +391 -1
- package/src/sdk/host/session-runtime.ts +308 -73
- package/src/sdk/host/surface-policy.ts +3 -0
- package/src/sdk/host/websocket-transport.ts +7 -1
- package/src/sdk/mcp/server.ts +88 -15
- package/src/sdk/prompt-status.ts +8 -0
- package/src/sdk/protocol/operation-inventory.generated.json +62 -0
- package/src/sdk/protocol/operation-registry.ts +3 -2
- package/src/sdk/session.ts +87 -39
- package/src/sdk/transport/index.ts +1 -0
- package/src/sdk/transport/relay.ts +3 -0
- package/src/sdk/transport/serve-cli.ts +111 -24
- package/src/secrets/obfuscator.ts +92 -12
- package/src/session/agent-session.ts +322 -93
- package/src/session/blob-store.ts +60 -8
- package/src/session/fallback-chain-controller.ts +35 -0
- package/src/session/internal/managed-session-scope.ts +25 -3
- package/src/session/internal/managed-session-storage.ts +574 -73
- package/src/session/internal/session-memory-sidecar.ts +19 -29
- package/src/session/session-manager.ts +2166 -743
- package/src/session/session-storage.ts +715 -48
- package/src/setup/provider-onboarding.ts +19 -3
- package/src/setup/provider-presets.json +3 -1
- package/src/slash-commands/builtin-registry.ts +85 -3
- package/src/stt/downloader.ts +2 -3
- package/src/stt/setup.ts +29 -4
- package/src/task/executor.ts +22 -18
- package/src/task/index.ts +24 -1
- package/src/tools/archive-reader.ts +11 -2
- package/src/tools/browser/runtime-diagnostics.ts +249 -0
- package/src/tools/browser/tab-protocol.ts +4 -0
- package/src/tools/browser/tab-supervisor.ts +5 -0
- package/src/tools/browser/tab-worker.ts +36 -1
- package/src/tools/browser.ts +7 -0
- package/src/tools/image-gen.ts +110 -166
- package/src/tools/index.ts +2 -0
- package/src/tools/read.ts +1 -1
- package/src/tools/resource-gc.ts +6 -1
- package/src/tools/tool-catalog.generated.ts +5 -1
- package/src/utils/pasted-image-path.ts +93 -0
- 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
|
+
}
|