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