@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
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { getAgentDir } from "@gajae-code/utils";
|
|
1
2
|
import { CliParseError } from "@gajae-code/utils/cli";
|
|
2
|
-
import
|
|
3
|
-
import { listSdkSessionEndpoints } from "../client/discovery";
|
|
4
|
-
import { type SdkEndpointSelectionError, selectLiveEndpoint } from "../client/liveness";
|
|
3
|
+
import { readSdkBrokerDiscovery, SdkClient, SdkClientError } from "../client";
|
|
5
4
|
import { DEFAULT_PENDING_CEILING_BYTES, MIN_PENDING_CEILING_BYTES, startSocketServe, startStdioServe } from "./index";
|
|
6
5
|
|
|
6
|
+
const MAX_SESSION_LIST_PAGES = 10_000;
|
|
7
|
+
|
|
7
8
|
type ServeMode = { kind: "stdio" } | { kind: "socket"; socketPath: string };
|
|
8
9
|
|
|
9
10
|
interface ServeArguments {
|
|
@@ -67,11 +68,72 @@ export function resolveServePendingCeiling(flagValue: string | undefined, envVal
|
|
|
67
68
|
return ceiling;
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
type BrokerSessionRow = { sessionId: string; live: boolean; ambiguous: boolean };
|
|
72
|
+
|
|
73
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
74
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Extracts the broker `result` envelope, converting an explicit error frame into a typed throw. */
|
|
78
|
+
function brokerResult(value: unknown): Record<string, unknown> {
|
|
79
|
+
if (isRecord(value) && value.ok === false) {
|
|
80
|
+
const error = isRecord(value.error) ? value.error : {};
|
|
81
|
+
const code = typeof error.code === "string" ? error.code : "unavailable";
|
|
82
|
+
const message = typeof error.message === "string" ? error.message : "SDK broker request failed";
|
|
83
|
+
throw new SdkClientError(code, message, value.error);
|
|
84
|
+
}
|
|
85
|
+
return isRecord(value) && isRecord(value.result) ? value.result : {};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function brokerSessionRows(value: Record<string, unknown>): BrokerSessionRow[] {
|
|
89
|
+
if (!Array.isArray(value.sessions)) return [];
|
|
90
|
+
return value.sessions.flatMap(item => {
|
|
91
|
+
if (!isRecord(item) || typeof item.sessionId !== "string" || !item.sessionId) return [];
|
|
92
|
+
return [{ sessionId: item.sessionId, live: item.live === true, ambiguous: item.ambiguous === true }];
|
|
93
|
+
});
|
|
72
94
|
}
|
|
73
95
|
|
|
74
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* Exhausts broker `session.list` cursor pagination (default page limit 100) to
|
|
98
|
+
* one full snapshot; exported for tests.
|
|
99
|
+
*/
|
|
100
|
+
export async function listBrokerSessions(broker: SdkClient): Promise<BrokerSessionRow[]> {
|
|
101
|
+
const rows: BrokerSessionRow[] = [];
|
|
102
|
+
let cursor: string | undefined;
|
|
103
|
+
for (let pageCount = 0; pageCount < MAX_SESSION_LIST_PAGES; pageCount++) {
|
|
104
|
+
const listed = brokerResult(await broker.global("session.list", cursor === undefined ? {} : { cursor }));
|
|
105
|
+
rows.push(...brokerSessionRows(listed));
|
|
106
|
+
const nextCursor =
|
|
107
|
+
typeof listed.continuationCursor === "string" && listed.continuationCursor.length > 0
|
|
108
|
+
? listed.continuationCursor
|
|
109
|
+
: undefined;
|
|
110
|
+
if (!nextCursor) return rows;
|
|
111
|
+
cursor = nextCursor;
|
|
112
|
+
}
|
|
113
|
+
throw new SdkClientError("protocol_error", "session.list exceeded the page budget.");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Selects the session to serve through broker `session.list` truth (C10); exported for tests. */
|
|
117
|
+
export function selectBrokerSession(sessions: BrokerSessionRow[], explicitSessionId: string | undefined): string {
|
|
118
|
+
if (explicitSessionId !== undefined) {
|
|
119
|
+
const row = sessions.find(session => session.sessionId === explicitSessionId);
|
|
120
|
+
if (!row) throw new Error(`not_found: session ${explicitSessionId} is not indexed by the broker`);
|
|
121
|
+
if (row.ambiguous) throw new Error("ambiguous_session: session id maps to more than one state root");
|
|
122
|
+
if (!row.live) throw new Error(`endpoint_stale: session ${explicitSessionId} endpoint is not live`);
|
|
123
|
+
return row.sessionId;
|
|
124
|
+
}
|
|
125
|
+
const live = sessions.filter(session => session.live && !session.ambiguous);
|
|
126
|
+
if (live.length === 0) throw new Error("no_live_endpoint: no live session endpoint");
|
|
127
|
+
if (live.length > 1) throw new Error("multiple_live_endpoints: more than one live session; specify --session <id>");
|
|
128
|
+
return live[0]!.sessionId;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Attaches a stdio or Unix-socket relay to one live SDK session endpoint.
|
|
133
|
+
* Session targeting is broker-bound (C10): `session.list` resolves the session
|
|
134
|
+
* and `session.get_endpoint` mints the exact credential — never a direct
|
|
135
|
+
* endpoint-file read. A missing or unreachable broker fails closed.
|
|
136
|
+
*/
|
|
75
137
|
export async function runSdkServe(argv: string[]): Promise<void> {
|
|
76
138
|
const parsed = parseServeArguments(argv);
|
|
77
139
|
if (parsed.mode.kind === "socket" && process.platform === "win32")
|
|
@@ -80,26 +142,51 @@ export async function runSdkServe(argv: string[]): Promise<void> {
|
|
|
80
142
|
parsed.pendingCeiling,
|
|
81
143
|
process.env.GJC_SDK_SERVE_PENDING_CEILING_BYTES,
|
|
82
144
|
);
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
145
|
+
const discovery = await readSdkBrokerDiscovery(getAgentDir());
|
|
146
|
+
if (!discovery) throw new Error("broker_unavailable: SDK broker is not running");
|
|
147
|
+
let broker: SdkClient;
|
|
148
|
+
try {
|
|
149
|
+
broker = await SdkClient.connect(discovery.url, discovery.token);
|
|
150
|
+
} catch {
|
|
151
|
+
throw new Error("broker_unavailable: SDK broker is not reachable");
|
|
88
152
|
}
|
|
89
|
-
const options = { url: selected.url, token: selected.token, pendingCeilingBytes };
|
|
90
|
-
const handle =
|
|
91
|
-
parsed.mode.kind === "stdio"
|
|
92
|
-
? await startStdioServe(options)
|
|
93
|
-
: await startSocketServe({ ...options, socketPath: parsed.mode.socketPath });
|
|
94
|
-
const stop = () => {
|
|
95
|
-
void handle.close();
|
|
96
|
-
};
|
|
97
|
-
process.once("SIGINT", stop);
|
|
98
|
-
process.once("SIGTERM", stop);
|
|
99
153
|
try {
|
|
100
|
-
await
|
|
154
|
+
const sessionId = selectBrokerSession(await listBrokerSessions(broker), parsed.sessionId);
|
|
155
|
+
const endpoint = brokerResult(await broker.global("session.get_endpoint", { sessionId }));
|
|
156
|
+
const url = typeof endpoint.url === "string" && endpoint.url ? endpoint.url : undefined;
|
|
157
|
+
const token = typeof endpoint.token === "string" ? endpoint.token : "";
|
|
158
|
+
if (!url) throw new Error("unavailable: broker returned an invalid endpoint record");
|
|
159
|
+
const options = {
|
|
160
|
+
url,
|
|
161
|
+
token,
|
|
162
|
+
pendingCeilingBytes,
|
|
163
|
+
validateDownstreamFrame: (source: string): boolean => {
|
|
164
|
+
try {
|
|
165
|
+
const frame = JSON.parse(source) as { type?: unknown; elevationRequestId?: unknown };
|
|
166
|
+
return frame.type !== "control_request" || frame.elevationRequestId === undefined;
|
|
167
|
+
} catch {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
const handle =
|
|
173
|
+
parsed.mode.kind === "stdio"
|
|
174
|
+
? await startStdioServe(options)
|
|
175
|
+
: await startSocketServe({ ...options, socketPath: parsed.mode.socketPath });
|
|
176
|
+
const stop = () => {
|
|
177
|
+
void handle.close();
|
|
178
|
+
};
|
|
179
|
+
process.once("SIGINT", stop);
|
|
180
|
+
process.once("SIGTERM", stop);
|
|
181
|
+
try {
|
|
182
|
+
await handle.done;
|
|
183
|
+
} finally {
|
|
184
|
+
process.removeListener("SIGINT", stop);
|
|
185
|
+
process.removeListener("SIGTERM", stop);
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
throw error instanceof SdkClientError ? new Error(`${error.code}: ${error.message}`) : error;
|
|
101
189
|
} finally {
|
|
102
|
-
|
|
103
|
-
process.removeListener("SIGTERM", stop);
|
|
190
|
+
await broker.close();
|
|
104
191
|
}
|
|
105
192
|
}
|
|
@@ -21,17 +21,95 @@ export interface SecretEntry {
|
|
|
21
21
|
|
|
22
22
|
const REPLACEMENT_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Domain separator for replace-mode replacement derivation. Distinct from
|
|
26
|
+
* `PLACEHOLDER_DOMAIN` so a value minted under one construction can never be
|
|
27
|
+
* confused with a value minted under the other, even with the same key.
|
|
28
|
+
*/
|
|
29
|
+
const REPLACEMENT_DOMAIN = "gjc.secret-obfuscation.replacement.v1\0";
|
|
30
|
+
|
|
31
|
+
/** One HMAC-SHA256 digest block length in bytes. */
|
|
32
|
+
const REPLACEMENT_DIGEST_BYTES = 32;
|
|
33
|
+
|
|
34
|
+
/** Largest accepted byte for uniform rejection sampling (256 - (256 % 62)). */
|
|
35
|
+
const REPLACEMENT_REJECT_THRESHOLD = 248;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generate a deterministic, keyed, same-length replacement string from a
|
|
39
|
+
* secret value.
|
|
40
|
+
*
|
|
41
|
+
* COMPATIBILITY CONTRACT (required behavior, must never regress):
|
|
42
|
+
* - Deterministic within a process: the same secret under the same key always
|
|
43
|
+
* yields the same replacement, independent of entry order or call order.
|
|
44
|
+
* - Same length: `result.length === secret.length` (UTF-16 code units, i.e.
|
|
45
|
+
* `String.prototype.length` semantics — unchanged from the pre-fix
|
|
46
|
+
* implementation). Astral-plane characters count as two units, exactly as
|
|
47
|
+
* they did before.
|
|
48
|
+
* - Allowed characters: output is restricted to `[A-Za-z0-9]` (62 chars).
|
|
49
|
+
* - Explicit `replacement` values in secrets.yml are authoritative and are
|
|
50
|
+
* never derived; this path runs only when `replacement` is undefined.
|
|
51
|
+
* - Replace mode stays one-way: the derived value is never reversed by
|
|
52
|
+
* `deobfuscate()`.
|
|
53
|
+
*
|
|
54
|
+
* THREAT MODEL (issue #4166):
|
|
55
|
+
* - Attacker model: an observer sees one or more replacements (model context,
|
|
56
|
+
* provider-side logs, saved/shared transcripts) and has full knowledge of
|
|
57
|
+
* the public algorithm. Without the 32-byte obfuscation key they cannot
|
|
58
|
+
* confirm a candidate secret offline and cannot predict or precompute the
|
|
59
|
+
* replacement for any secret. The construction is a keyed PRF
|
|
60
|
+
* (HMAC-SHA256), so output is unpredictable without the key, and keyed
|
|
61
|
+
* domain separation keeps this construction distinct from the placeholder
|
|
62
|
+
* construction.
|
|
63
|
+
* - Accepted residual disclosure: the replacement is same-length by design
|
|
64
|
+
* (required behavior above), so the secret's exact length is still
|
|
65
|
+
* observable. This is inherent to size-preserving substitution and is
|
|
66
|
+
* explicitly out of scope; the fix removes the *keyless confirmation*
|
|
67
|
+
* oracle, not the length signal.
|
|
68
|
+
* - Cross-process stability / key rotation: the process key is generated per
|
|
69
|
+
* process (`PROCESS_SECRET_OBFUSCATION_KEY`), so derived replacements are
|
|
70
|
+
* stable within a process and differ across processes or after a key
|
|
71
|
+
* rotation. This is a deliberate decision, identical in scope to the
|
|
72
|
+
* obfuscate-mode placeholder behavior. Users who need stable values across
|
|
73
|
+
* processes or restarts must set an explicit `replacement` in secrets.yml.
|
|
74
|
+
* - Collision/bias: output is a keyed pseudorandom mapping. Two distinct
|
|
75
|
+
* secrets can in principle collide (birthday bound over the 62^len output
|
|
76
|
+
* space); for realistic secret lengths this is negligible. Character
|
|
77
|
+
* distribution is uniform by construction: digest bytes are rejection-
|
|
78
|
+
* sampled (bytes 248-255 rejected, 256 - (256 % 62) = 248 = 62 * 4), so
|
|
79
|
+
* every character is exactly equally likely, with no modulo bias.
|
|
80
|
+
* - Empty secrets: a zero-length secret yields an empty replacement and the
|
|
81
|
+
* obfuscator treats it as a no-op (load-time validation rejects empty
|
|
82
|
+
* content anyway). Unicode secrets are hashed as UTF-8 bytes, so encoding
|
|
83
|
+
* is canonical. Very long secrets expand via a counter-based HMAC stream
|
|
84
|
+
* in O(length) blocks; generation cost is linear in the secret length.
|
|
85
|
+
* - Overlapping secrets and streaming: derivation is per-secret and does not
|
|
86
|
+
* depend on matching semantics (longest-first overlap handling is
|
|
87
|
+
* unchanged), and `obfuscate()` runs per complete text payload, so chunked
|
|
88
|
+
* output (e.g. streamed LLM messages) sees the same deterministic
|
|
89
|
+
* replacement for the same secret within a process.
|
|
90
|
+
*/
|
|
91
|
+
function generateDeterministicReplacement(secret: string, key: Uint8Array): string {
|
|
92
|
+
const length = secret.length;
|
|
93
|
+
if (length === 0) return "";
|
|
28
94
|
const chars: string[] = [];
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
95
|
+
// CTR-style expansion: HMAC(key, DOMAIN || counter || secret) per 32-byte
|
|
96
|
+
// block. The counter is big-endian and fixed-width, so the stream is
|
|
97
|
+
// unambiguous and never repeats.
|
|
98
|
+
const counter = new Uint8Array(8);
|
|
99
|
+
const counterView = new DataView(counter.buffer);
|
|
100
|
+
let block: Uint8Array | undefined;
|
|
101
|
+
let blockOffset = REPLACEMENT_DIGEST_BYTES;
|
|
102
|
+
let blockIndex = 0;
|
|
103
|
+
while (chars.length < length) {
|
|
104
|
+
if (block === undefined || blockOffset >= REPLACEMENT_DIGEST_BYTES) {
|
|
105
|
+
counterView.setUint32(4, blockIndex, false);
|
|
106
|
+
block = createHmac("sha256", key).update(REPLACEMENT_DOMAIN).update(counter).update(secret, "utf8").digest();
|
|
107
|
+
blockOffset = 0;
|
|
108
|
+
blockIndex++;
|
|
109
|
+
}
|
|
110
|
+
const byte = block[blockOffset++]!;
|
|
111
|
+
if (byte >= REPLACEMENT_REJECT_THRESHOLD) continue;
|
|
112
|
+
chars.push(REPLACEMENT_CHARS[byte % REPLACEMENT_CHARS.length]!);
|
|
35
113
|
}
|
|
36
114
|
return chars.join("");
|
|
37
115
|
}
|
|
@@ -115,7 +193,8 @@ export class SecretObfuscator {
|
|
|
115
193
|
index++;
|
|
116
194
|
} else {
|
|
117
195
|
// replace mode
|
|
118
|
-
const replacement =
|
|
196
|
+
const replacement =
|
|
197
|
+
entry.replacement ?? generateDeterministicReplacement(entry.content, this.#placeholderKey);
|
|
119
198
|
this.#replaceMappings.set(entry.content, replacement);
|
|
120
199
|
}
|
|
121
200
|
} else {
|
|
@@ -168,7 +247,8 @@ export class SecretObfuscator {
|
|
|
168
247
|
|
|
169
248
|
for (const matchValue of matches) {
|
|
170
249
|
if (entry.mode === "replace") {
|
|
171
|
-
const replacement =
|
|
250
|
+
const replacement =
|
|
251
|
+
entry.replacement ?? generateDeterministicReplacement(matchValue, this.#placeholderKey);
|
|
172
252
|
result = replaceAll(result, matchValue, replacement);
|
|
173
253
|
} else {
|
|
174
254
|
// obfuscate mode — get or create stable index
|