@skrr-ai/cli 0.1.49 → 0.1.51
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/README.md +5 -4
- package/dist/base-command.js +75 -6
- package/dist/commands/agents/chat.d.ts +14 -0
- package/dist/commands/agents/chat.js +37 -1
- package/dist/commands/agents/create.js +14 -7
- package/dist/commands/agents/show.d.ts +9 -0
- package/dist/commands/agents/show.js +53 -0
- package/dist/commands/agents/update.d.ts +9 -0
- package/dist/commands/agents/update.js +38 -5
- package/dist/commands/balance/show.d.ts +34 -0
- package/dist/commands/balance/show.js +92 -2
- package/dist/commands/code/index.d.ts +1 -0
- package/dist/commands/code/index.js +49 -5
- package/dist/commands/code/run.js +5 -1
- package/dist/commands/commitments/cycles.d.ts +19 -0
- package/dist/commands/commitments/cycles.js +46 -0
- package/dist/commands/commitments/doctor.d.ts +1 -0
- package/dist/commands/commitments/doctor.js +19 -4
- package/dist/commands/commitments/effective-policy.js +40 -1
- package/dist/commands/commitments/explain.d.ts +17 -0
- package/dist/commands/commitments/explain.js +40 -0
- package/dist/commands/commitments/next-tick.js +3 -1
- package/dist/commands/commitments/preflight.js +10 -0
- package/dist/commands/commitments/show.js +11 -1
- package/dist/commands/daemon/config.d.ts +15 -0
- package/dist/commands/daemon/config.js +38 -0
- package/dist/commands/daemon/index.js +1 -0
- package/dist/commands/daemon/status.js +15 -1
- package/dist/commands/goals/create.d.ts +1 -0
- package/dist/commands/goals/create.js +22 -0
- package/dist/commands/harnesses/models.d.ts +54 -0
- package/dist/commands/harnesses/models.js +100 -7
- package/dist/commands/harnesses/show.d.ts +11 -0
- package/dist/commands/harnesses/show.js +35 -0
- package/dist/commands/harnesses/update.d.ts +28 -1
- package/dist/commands/harnesses/update.js +73 -10
- package/dist/commands/logout.js +12 -1
- package/dist/commands/machines/dedicated/index.js +1 -1
- package/dist/commands/machines/dedicated/sign-in.js +4 -1
- package/dist/commands/machines/dedicated/terminal/kill.d.ts +21 -0
- package/dist/commands/machines/dedicated/terminal/kill.js +55 -0
- package/dist/commands/machines/dedicated/terminal/ls.d.ts +22 -0
- package/dist/commands/machines/dedicated/terminal/ls.js +70 -0
- package/dist/commands/machines/dedicated/terminal/rename.d.ts +23 -0
- package/dist/commands/machines/dedicated/terminal/rename.js +64 -0
- package/dist/commands/machines/dedicated/terminal.d.ts +9 -0
- package/dist/commands/machines/dedicated/terminal.js +27 -2
- package/dist/commands/machines/hosted/connect.js +2 -2
- package/dist/commands/machines/hosted/destroy.js +1 -1
- package/dist/commands/machines/hosted/exec.js +1 -1
- package/dist/commands/machines/hosted/list.js +1 -1
- package/dist/commands/machines/hosted/pause.js +1 -1
- package/dist/commands/machines/hosted/pull.js +1 -1
- package/dist/commands/machines/hosted/resume.js +1 -1
- package/dist/commands/machines/hosted/start.js +2 -2
- package/dist/commands/machines/hosted/status.js +2 -1
- package/dist/commands/outbox/list.d.ts +12 -1
- package/dist/commands/outbox/list.js +31 -2
- package/dist/commands/revoke-daemon.js +9 -0
- package/dist/commands/spaces/create.d.ts +1 -0
- package/dist/commands/spaces/create.js +17 -0
- package/dist/commands/spaces/show.js +5 -0
- package/dist/commands/spaces/update.d.ts +1 -0
- package/dist/commands/spaces/update.js +16 -0
- package/dist/commands/tasks/create.d.ts +1 -0
- package/dist/commands/tasks/create.js +6 -0
- package/dist/commands/tasks/list.js +1 -0
- package/dist/commands/tasks/show.js +45 -0
- package/dist/commands/tasks/update.d.ts +1 -0
- package/dist/commands/tasks/update.js +22 -0
- package/dist/commands/tasks/upsert.d.ts +1 -0
- package/dist/commands/tasks/upsert.js +16 -0
- package/dist/lib/agent-config.d.ts +2 -0
- package/dist/lib/agent-config.js +3 -1
- package/dist/lib/agentic-stream.d.ts +68 -1
- package/dist/lib/agentic-stream.js +294 -7
- package/dist/lib/api-fetch.js +19 -0
- package/dist/lib/auth-core-init.d.ts +2 -10
- package/dist/lib/auth-core-init.js +20 -67
- package/dist/lib/auth-failure.d.ts +33 -0
- package/dist/lib/auth-failure.js +65 -0
- package/dist/lib/auth-storage.d.ts +14 -0
- package/dist/lib/auth-storage.js +14 -0
- package/dist/lib/commitments.d.ts +37 -2
- package/dist/lib/commitments.js +148 -0
- package/dist/lib/daemonBroker.d.ts +134 -31
- package/dist/lib/daemonBroker.js +376 -31
- package/dist/lib/dedicated-lease-command.d.ts +14 -0
- package/dist/lib/dedicated-lease-command.js +30 -1
- package/dist/lib/dedicated-machines.js +8 -25
- package/dist/lib/dedicated-service-command.d.ts +11 -3
- package/dist/lib/dedicated-service-command.js +20 -3
- package/dist/lib/dedicated-service.d.ts +46 -0
- package/dist/lib/dedicated-service.js +85 -7
- package/dist/lib/dedicated-terminal.d.ts +21 -0
- package/dist/lib/dedicated-terminal.js +104 -9
- package/dist/lib/delegated-cli.js +4 -1
- package/dist/lib/exec-runtime-binary.d.ts +3 -1
- package/dist/lib/exec-runtime-binary.js +4 -2
- package/dist/lib/first-party-harness-broker.d.ts +10 -0
- package/dist/lib/first-party-harness-broker.js +13 -1
- package/dist/lib/first-party-harness-doctor.js +41 -1
- package/dist/lib/first-party-harness-managed.d.ts +4 -3
- package/dist/lib/first-party-harness-project-trust.d.ts +125 -0
- package/dist/lib/first-party-harness-project-trust.js +364 -0
- package/dist/lib/first-party-harness.d.ts +9 -2
- package/dist/lib/first-party-harness.js +6 -5
- package/dist/lib/harnesses.d.ts +10 -0
- package/dist/lib/hosted-machines.d.ts +10 -1
- package/dist/lib/hosted-machines.js +36 -2
- package/dist/lib/live-runtime-binary.d.ts +40 -0
- package/dist/lib/live-runtime-binary.js +193 -0
- package/dist/lib/login.d.ts +23 -0
- package/dist/lib/login.js +60 -10
- package/dist/lib/machine-spend-cap.d.ts +23 -0
- package/dist/lib/machine-spend-cap.js +68 -0
- package/dist/lib/message-intent.js +3 -1
- package/dist/lib/node-adapter.js +15 -3
- package/dist/lib/tasks.d.ts +20 -0
- package/dist/lib/tasks.js +54 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +90 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +113 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +26 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.d.ts +47 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.js +69 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +133 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +189 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +41 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +25 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.js +268 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +11 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +36 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +156 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.d.ts +8 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +9 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +16 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.d.ts +6 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.js +32 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.d.ts +81 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.js +87 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +61 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/types.d.ts +6 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +90 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +105 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +25 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.d.ts +47 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.js +66 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +133 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +182 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +41 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.js +257 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +11 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +36 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +147 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.d.ts +8 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +9 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +16 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.d.ts +6 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.js +31 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.d.ts +81 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.js +80 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +58 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/types.d.ts +6 -1
- package/dist/node_modules/@skrr-ai/auth-core/package.json +61 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3418 -3344
- package/oclif.manifest.json +24813 -24242
- package/package.json +4 -1
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.TransientAuthFailure = exports.PermanentAuthFailure = void 0;
|
|
19
|
+
exports.permanentReasonForRefreshCode = permanentReasonForRefreshCode;
|
|
19
20
|
exports.classifyRefreshResponse = classifyRefreshResponse;
|
|
20
21
|
const types_js_1 = require("./types.js");
|
|
21
22
|
Object.defineProperty(exports, "PermanentAuthFailure", { enumerable: true, get: function () { return types_js_1.PermanentAuthFailure; } });
|
|
@@ -32,6 +33,33 @@ const PERMANENT_CODE_SET = new Set([
|
|
|
32
33
|
'REFRESH_EXPIRED',
|
|
33
34
|
'SESSION_REVOKED',
|
|
34
35
|
]);
|
|
36
|
+
/**
|
|
37
|
+
* Server refusal codes that are permanent but are not themselves reason names.
|
|
38
|
+
*
|
|
39
|
+
* `DEVICE_PROOF_THUMBPRINT_MISMATCH` (`daemonRefreshTokens.verifyDeviceProofPinForRow`)
|
|
40
|
+
* means the proof was signed by a key other than the one enrolled on the daemon's
|
|
41
|
+
* row. Every retry is signed by the same key, so it can never succeed — but
|
|
42
|
+
* because it was not in the set above it fell through to the unknown-401 path,
|
|
43
|
+
* where a daemon retrying on a five-minute cadence never exhausted its budget:
|
|
44
|
+
* one laptop logged 741 "transient, retrying" refusals over four days, and the
|
|
45
|
+
* reason it eventually surfaced was `UNKNOWN_401` (OSK-12056).
|
|
46
|
+
*/
|
|
47
|
+
const PERMANENT_CODE_ALIASES = {
|
|
48
|
+
REFRESH_MISSING: 'REFRESH_INVALID',
|
|
49
|
+
DEVICE_PROOF_THUMBPRINT_MISMATCH: 'DEVICE_KEY_MISMATCH',
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The permanent reason a 401 refresh refusal's `code` names, or `null` when the
|
|
53
|
+
* code is missing or unrecognised (an unknown-shape 401). The ONE mapping both
|
|
54
|
+
* this classifier and the daemon's `refresh.ts` read.
|
|
55
|
+
*/
|
|
56
|
+
function permanentReasonForRefreshCode(code) {
|
|
57
|
+
if (!code)
|
|
58
|
+
return null;
|
|
59
|
+
if (PERMANENT_CODE_SET.has(code))
|
|
60
|
+
return code;
|
|
61
|
+
return PERMANENT_CODE_ALIASES[code] ?? null;
|
|
62
|
+
}
|
|
35
63
|
/**
|
|
36
64
|
* Classify an HTTP response from /api/auth/refresh (or /api/daemons/token/refresh)
|
|
37
65
|
* into a discriminated union of success or failure.
|
|
@@ -64,17 +92,12 @@ function classifyRefreshResponse(status, body) {
|
|
|
64
92
|
}
|
|
65
93
|
// 401 — auth failure; check for known permanent code
|
|
66
94
|
if (status === 401) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
failure: new types_js_1.PermanentAuthFailure('REFRESH_INVALID', body.message ?? 'Refresh token missing'),
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
const code = body?.code;
|
|
74
|
-
if (code && PERMANENT_CODE_SET.has(code)) {
|
|
95
|
+
const reason = permanentReasonForRefreshCode(body?.code);
|
|
96
|
+
if (reason) {
|
|
97
|
+
const fallback = body?.code === 'REFRESH_MISSING' ? 'Refresh token missing' : reason;
|
|
75
98
|
return {
|
|
76
99
|
kind: 'failure',
|
|
77
|
-
failure: new types_js_1.PermanentAuthFailure(
|
|
100
|
+
failure: new types_js_1.PermanentAuthFailure(reason, body?.message ?? fallback),
|
|
78
101
|
};
|
|
79
102
|
}
|
|
80
103
|
// Unknown-shape 401 (no code, or a code we don't recognise). This is NOT a
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session permission authority — the ONE rule that decides whether a session's
|
|
3
|
+
* execution contract governs the run's permissions, shared by the server and
|
|
4
|
+
* the daemon (OSK-11975 introduced it; it was implemented twice — the server
|
|
5
|
+
* read `kind !== 'commitment'`, the daemon read the `permissionSource` marker —
|
|
6
|
+
* which is exactly how a new contract kind or a new source value would drift
|
|
7
|
+
* them apart).
|
|
8
|
+
*
|
|
9
|
+
* The wire model:
|
|
10
|
+
*
|
|
11
|
+
* - The contract is closed (`hasOnlyKeys` on the daemon), so "whose
|
|
12
|
+
* permissions govern" can never ride INSIDE it. It is a top-level session
|
|
13
|
+
* field: `permissionSource`.
|
|
14
|
+
* - `'agent'` means the Agent's own configured permission mode, tool policy,
|
|
15
|
+
* Grants, session env, MCP servers and machine consent govern the run,
|
|
16
|
+
* exactly as for an unattended Task of the same Agent. Today only a
|
|
17
|
+
* `commitment` contract may carry it.
|
|
18
|
+
* - A `commitment` contract WITHOUT the marker (an older server mid-deploy)
|
|
19
|
+
* still governs permissions — the marker is the version signal, so its
|
|
20
|
+
* absence must keep the legacy fence, not silently lift it.
|
|
21
|
+
* - `task_delivery` contracts always govern, marker or not.
|
|
22
|
+
*
|
|
23
|
+
* Server vs daemon asymmetry, on purpose: the server STAMPS the marker
|
|
24
|
+
* (`permissionSourceForContract`), so on its own side the predicate reduces to
|
|
25
|
+
* "a commitment never governs" — `contractGovernsPermissions(c,
|
|
26
|
+
* permissionSourceForContract(c))` is exactly `kind !== 'commitment'`. The
|
|
27
|
+
* daemon reads the marker it received, because it cannot distinguish "new
|
|
28
|
+
* server, agent permissions" from "old server, contract fence" any other way.
|
|
29
|
+
* Both call the same function; only the input differs.
|
|
30
|
+
*/
|
|
31
|
+
/** The wire vocabulary for `permissionSource`. Top-level session field. */
|
|
32
|
+
export declare const SESSION_PERMISSION_SOURCES: readonly ["agent"];
|
|
33
|
+
export type SessionPermissionSource = (typeof SESSION_PERMISSION_SOURCES)[number];
|
|
34
|
+
/**
|
|
35
|
+
* The daemon capability that proves a build honours `permissionSource`. The
|
|
36
|
+
* server announces it in preflight and refuses to dispatch a Commitment Run to
|
|
37
|
+
* a daemon without it (`COMMITMENT_AGENT_PERMISSIONS_RUNTIME_UNSUPPORTED`), so
|
|
38
|
+
* the marker never reaches a daemon that would ignore it.
|
|
39
|
+
*/
|
|
40
|
+
export declare const COMMITMENT_AGENT_PERMISSIONS_CAPABILITY = "commitment_execution_contract_v2_agent_permissions";
|
|
41
|
+
/**
|
|
42
|
+
* The minimal contract shape this rule reads. The predicate only ever needs
|
|
43
|
+
* `kind` — keeping the input structural means this module never imports the
|
|
44
|
+
* full `CommitmentExecutionContract` type, which lives daemon-side
|
|
45
|
+
* (`daemon/src/commitment-execution-contract.ts`) and server-side in the
|
|
46
|
+
* commitment services.
|
|
47
|
+
*/
|
|
48
|
+
export type PermissionContractKind = {
|
|
49
|
+
kind?: unknown;
|
|
50
|
+
} | null | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Untrusted wire input: anything but an exact vocabulary literal reads as
|
|
53
|
+
* absent. Never widen by coercion — an unrecognized future value must degrade
|
|
54
|
+
* to "contract governs", the conservative answer.
|
|
55
|
+
*/
|
|
56
|
+
export declare function normalizeSessionPermissionSource(value: unknown): SessionPermissionSource | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* What the server stamps on the wire for this contract. A `commitment`
|
|
59
|
+
* contract no longer carries permissions (OSK-11975); every other contract
|
|
60
|
+
* kind keeps its own authority and stamps nothing.
|
|
61
|
+
*/
|
|
62
|
+
export declare function permissionSourceForContract(contract: PermissionContractKind): SessionPermissionSource | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Whether the contract is the run's PERMISSION authority — the single place
|
|
65
|
+
* this rule lives.
|
|
66
|
+
*
|
|
67
|
+
* True for a non-commitment contract regardless of marker, and for a
|
|
68
|
+
* commitment whose server did not declare agent permissions. False for no
|
|
69
|
+
* contract at all, and for a commitment carrying `permissionSource: 'agent'`,
|
|
70
|
+
* which then follows the Agent's own permissions exactly as a Task does while
|
|
71
|
+
* the contract keeps its non-permission roles (normalized shape, ephemeral
|
|
72
|
+
* worktree, result validation, `GIT_OPTIONAL_LOCKS`, provider restriction).
|
|
73
|
+
*/
|
|
74
|
+
export declare function contractGovernsPermissions(contract: PermissionContractKind, permissionSource: unknown): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* The contract when it governs permissions, else `undefined` — for the sites
|
|
77
|
+
* that used `executionContract` as "is this run permission-fenced".
|
|
78
|
+
*/
|
|
79
|
+
export declare function permissionGoverningContract<T extends {
|
|
80
|
+
kind?: unknown;
|
|
81
|
+
}>(contract: T | null | undefined, permissionSource: unknown): T | undefined;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Session permission authority — the ONE rule that decides whether a session's
|
|
4
|
+
* execution contract governs the run's permissions, shared by the server and
|
|
5
|
+
* the daemon (OSK-11975 introduced it; it was implemented twice — the server
|
|
6
|
+
* read `kind !== 'commitment'`, the daemon read the `permissionSource` marker —
|
|
7
|
+
* which is exactly how a new contract kind or a new source value would drift
|
|
8
|
+
* them apart).
|
|
9
|
+
*
|
|
10
|
+
* The wire model:
|
|
11
|
+
*
|
|
12
|
+
* - The contract is closed (`hasOnlyKeys` on the daemon), so "whose
|
|
13
|
+
* permissions govern" can never ride INSIDE it. It is a top-level session
|
|
14
|
+
* field: `permissionSource`.
|
|
15
|
+
* - `'agent'` means the Agent's own configured permission mode, tool policy,
|
|
16
|
+
* Grants, session env, MCP servers and machine consent govern the run,
|
|
17
|
+
* exactly as for an unattended Task of the same Agent. Today only a
|
|
18
|
+
* `commitment` contract may carry it.
|
|
19
|
+
* - A `commitment` contract WITHOUT the marker (an older server mid-deploy)
|
|
20
|
+
* still governs permissions — the marker is the version signal, so its
|
|
21
|
+
* absence must keep the legacy fence, not silently lift it.
|
|
22
|
+
* - `task_delivery` contracts always govern, marker or not.
|
|
23
|
+
*
|
|
24
|
+
* Server vs daemon asymmetry, on purpose: the server STAMPS the marker
|
|
25
|
+
* (`permissionSourceForContract`), so on its own side the predicate reduces to
|
|
26
|
+
* "a commitment never governs" — `contractGovernsPermissions(c,
|
|
27
|
+
* permissionSourceForContract(c))` is exactly `kind !== 'commitment'`. The
|
|
28
|
+
* daemon reads the marker it received, because it cannot distinguish "new
|
|
29
|
+
* server, agent permissions" from "old server, contract fence" any other way.
|
|
30
|
+
* Both call the same function; only the input differs.
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.COMMITMENT_AGENT_PERMISSIONS_CAPABILITY = exports.SESSION_PERMISSION_SOURCES = void 0;
|
|
34
|
+
exports.normalizeSessionPermissionSource = normalizeSessionPermissionSource;
|
|
35
|
+
exports.permissionSourceForContract = permissionSourceForContract;
|
|
36
|
+
exports.contractGovernsPermissions = contractGovernsPermissions;
|
|
37
|
+
exports.permissionGoverningContract = permissionGoverningContract;
|
|
38
|
+
/** The wire vocabulary for `permissionSource`. Top-level session field. */
|
|
39
|
+
exports.SESSION_PERMISSION_SOURCES = ['agent'];
|
|
40
|
+
/**
|
|
41
|
+
* The daemon capability that proves a build honours `permissionSource`. The
|
|
42
|
+
* server announces it in preflight and refuses to dispatch a Commitment Run to
|
|
43
|
+
* a daemon without it (`COMMITMENT_AGENT_PERMISSIONS_RUNTIME_UNSUPPORTED`), so
|
|
44
|
+
* the marker never reaches a daemon that would ignore it.
|
|
45
|
+
*/
|
|
46
|
+
exports.COMMITMENT_AGENT_PERMISSIONS_CAPABILITY = 'commitment_execution_contract_v2_agent_permissions';
|
|
47
|
+
/**
|
|
48
|
+
* Untrusted wire input: anything but an exact vocabulary literal reads as
|
|
49
|
+
* absent. Never widen by coercion — an unrecognized future value must degrade
|
|
50
|
+
* to "contract governs", the conservative answer.
|
|
51
|
+
*/
|
|
52
|
+
function normalizeSessionPermissionSource(value) {
|
|
53
|
+
return exports.SESSION_PERMISSION_SOURCES.includes(value)
|
|
54
|
+
? value
|
|
55
|
+
: undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* What the server stamps on the wire for this contract. A `commitment`
|
|
59
|
+
* contract no longer carries permissions (OSK-11975); every other contract
|
|
60
|
+
* kind keeps its own authority and stamps nothing.
|
|
61
|
+
*/
|
|
62
|
+
function permissionSourceForContract(contract) {
|
|
63
|
+
return contract?.kind === 'commitment' ? 'agent' : undefined;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether the contract is the run's PERMISSION authority — the single place
|
|
67
|
+
* this rule lives.
|
|
68
|
+
*
|
|
69
|
+
* True for a non-commitment contract regardless of marker, and for a
|
|
70
|
+
* commitment whose server did not declare agent permissions. False for no
|
|
71
|
+
* contract at all, and for a commitment carrying `permissionSource: 'agent'`,
|
|
72
|
+
* which then follows the Agent's own permissions exactly as a Task does while
|
|
73
|
+
* the contract keeps its non-permission roles (normalized shape, ephemeral
|
|
74
|
+
* worktree, result validation, `GIT_OPTIONAL_LOCKS`, provider restriction).
|
|
75
|
+
*/
|
|
76
|
+
function contractGovernsPermissions(contract, permissionSource) {
|
|
77
|
+
if (!contract)
|
|
78
|
+
return false;
|
|
79
|
+
return !(contract.kind === 'commitment' && normalizeSessionPermissionSource(permissionSource) === 'agent');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The contract when it governs permissions, else `undefined` — for the sites
|
|
83
|
+
* that used `executionContract` as "is this run permission-fenced".
|
|
84
|
+
*/
|
|
85
|
+
function permissionGoverningContract(contract, permissionSource) {
|
|
86
|
+
return contract && contractGovernsPermissions(contract, permissionSource) ? contract : undefined;
|
|
87
|
+
}
|
|
@@ -84,3 +84,25 @@ export declare const SENSITIVE_ENV_PREFIX_EXCEPTIONS: ReadonlySet<string>;
|
|
|
84
84
|
* etc.) so legitimate tools keep working.
|
|
85
85
|
*/
|
|
86
86
|
export declare function sanitizeSpawnEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
87
|
+
/**
|
|
88
|
+
* OSK-12134 — loopback names a spawned child must reach directly, never through
|
|
89
|
+
* a proxy. A harness talks to this machine constantly: the daemon's local
|
|
90
|
+
* bridges (browser, delegated CLI, message intents), the inference broker, and
|
|
91
|
+
* the engine's own loopback services (skrr Code's `session/new` reaches a local
|
|
92
|
+
* directory service and failed with "service failure" behind a shell proxy).
|
|
93
|
+
*/
|
|
94
|
+
export declare const LOOPBACK_NO_PROXY_HOSTS: readonly string[];
|
|
95
|
+
/** True when `env` names a proxy some client in the child might honour. */
|
|
96
|
+
export declare function envHasProxy(env: Readonly<Record<string, string | undefined>>): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Return `env` with the loopback hosts appended to BOTH `NO_PROXY` and
|
|
99
|
+
* `no_proxy`, when (and only when) it names a proxy. Tools disagree on which
|
|
100
|
+
* spelling they read and whether they fall back to the other, so an unset
|
|
101
|
+
* spelling is seeded from the other one before the hosts are appended —
|
|
102
|
+
* seeding only the hosts would silently drop every existing exemption for a
|
|
103
|
+
* tool that prefers the unset spelling. Existing entries keep their order.
|
|
104
|
+
*
|
|
105
|
+
* Exported for a parent that layers a proxy onto the child's env AFTER
|
|
106
|
+
* {@link sanitizeSpawnEnv}: call it again on the final map.
|
|
107
|
+
*/
|
|
108
|
+
export declare function withLoopbackNoProxy<T extends Record<string, string | undefined>>(env: T): T;
|
|
@@ -40,8 +40,10 @@
|
|
|
40
40
|
* serve that case: a deny-list with a hole is not a deny-list.
|
|
41
41
|
*/
|
|
42
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
-
exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_NAME_TOKENS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = void 0;
|
|
43
|
+
exports.LOOPBACK_NO_PROXY_HOSTS = exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_NAME_TOKENS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = void 0;
|
|
44
44
|
exports.sanitizeSpawnEnv = sanitizeSpawnEnv;
|
|
45
|
+
exports.envHasProxy = envHasProxy;
|
|
46
|
+
exports.withLoopbackNoProxy = withLoopbackNoProxy;
|
|
45
47
|
/**
|
|
46
48
|
* Env vars that must NEVER reach a spawned subprocess.
|
|
47
49
|
*
|
|
@@ -187,5 +189,62 @@ function sanitizeSpawnEnv(env) {
|
|
|
187
189
|
continue;
|
|
188
190
|
out[key] = value;
|
|
189
191
|
}
|
|
190
|
-
return out;
|
|
192
|
+
return withLoopbackNoProxy(out);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* OSK-12134 — loopback names a spawned child must reach directly, never through
|
|
196
|
+
* a proxy. A harness talks to this machine constantly: the daemon's local
|
|
197
|
+
* bridges (browser, delegated CLI, message intents), the inference broker, and
|
|
198
|
+
* the engine's own loopback services (skrr Code's `session/new` reaches a local
|
|
199
|
+
* directory service and failed with "service failure" behind a shell proxy).
|
|
200
|
+
*/
|
|
201
|
+
exports.LOOPBACK_NO_PROXY_HOSTS = ['127.0.0.1', 'localhost', '::1'];
|
|
202
|
+
const PROXY_ENV_VARS = [
|
|
203
|
+
'HTTP_PROXY',
|
|
204
|
+
'http_proxy',
|
|
205
|
+
'HTTPS_PROXY',
|
|
206
|
+
'https_proxy',
|
|
207
|
+
'ALL_PROXY',
|
|
208
|
+
'all_proxy',
|
|
209
|
+
];
|
|
210
|
+
/** True when `env` names a proxy some client in the child might honour. */
|
|
211
|
+
function envHasProxy(env) {
|
|
212
|
+
return PROXY_ENV_VARS.some((name) => typeof env[name] === 'string' && env[name].trim() !== '');
|
|
213
|
+
}
|
|
214
|
+
function appendNoProxyHosts(list, hosts) {
|
|
215
|
+
const entries = list
|
|
216
|
+
.split(',')
|
|
217
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
218
|
+
.filter(Boolean);
|
|
219
|
+
// `*` already exempts everything, and some clients honour it only as the
|
|
220
|
+
// entire value, so appending to it would break them.
|
|
221
|
+
if (entries.includes('*'))
|
|
222
|
+
return list;
|
|
223
|
+
const missing = hosts.filter((host) => !entries.includes(host));
|
|
224
|
+
if (missing.length === 0)
|
|
225
|
+
return list;
|
|
226
|
+
const kept = list.replace(/[\s,]+$/, '');
|
|
227
|
+
return kept ? `${kept},${missing.join(',')}` : missing.join(',');
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Return `env` with the loopback hosts appended to BOTH `NO_PROXY` and
|
|
231
|
+
* `no_proxy`, when (and only when) it names a proxy. Tools disagree on which
|
|
232
|
+
* spelling they read and whether they fall back to the other, so an unset
|
|
233
|
+
* spelling is seeded from the other one before the hosts are appended —
|
|
234
|
+
* seeding only the hosts would silently drop every existing exemption for a
|
|
235
|
+
* tool that prefers the unset spelling. Existing entries keep their order.
|
|
236
|
+
*
|
|
237
|
+
* Exported for a parent that layers a proxy onto the child's env AFTER
|
|
238
|
+
* {@link sanitizeSpawnEnv}: call it again on the final map.
|
|
239
|
+
*/
|
|
240
|
+
function withLoopbackNoProxy(env) {
|
|
241
|
+
if (!envHasProxy(env))
|
|
242
|
+
return env;
|
|
243
|
+
const upper = env.NO_PROXY?.trim() ? env.NO_PROXY : undefined;
|
|
244
|
+
const lower = env.no_proxy?.trim() ? env.no_proxy : undefined;
|
|
245
|
+
return {
|
|
246
|
+
...env,
|
|
247
|
+
NO_PROXY: appendNoProxyHosts(upper ?? lower ?? '', exports.LOOPBACK_NO_PROXY_HOSTS),
|
|
248
|
+
no_proxy: appendNoProxyHosts(lower ?? upper ?? '', exports.LOOPBACK_NO_PROXY_HOSTS),
|
|
249
|
+
};
|
|
191
250
|
}
|
|
@@ -62,6 +62,11 @@ export interface TokenPair {
|
|
|
62
62
|
* - TOKEN_* — access-token specific (server pushes 4402 close;
|
|
63
63
|
* daemon classifies as TOKEN_INVALID/TOKEN_EXPIRED).
|
|
64
64
|
* - SESSION_REVOKED — admin/forensic revoke; whole family burned.
|
|
65
|
+
* - DEVICE_KEY_MISMATCH — the refresh carried a device proof signed by a key
|
|
66
|
+
* other than the one enrolled on this daemon's row
|
|
67
|
+
* (server code `DEVICE_PROOF_THUMBPRINT_MISMATCH`).
|
|
68
|
+
* The daemon signs every retry with the same key, so
|
|
69
|
+
* retrying can never succeed.
|
|
65
70
|
* - UNKNOWN_401 — server returned 401 with a code we don't
|
|
66
71
|
* recognize. Treated as permanent (better to prompt
|
|
67
72
|
* re-auth than to loop forever) but tagged
|
|
@@ -74,7 +79,7 @@ export interface TokenPair {
|
|
|
74
79
|
* Without this, the client `describeAuthFailure` exhaustiveness check
|
|
75
80
|
* was silently broken for the non-REFRESH branches.
|
|
76
81
|
*/
|
|
77
|
-
export type PermanentAuthReason = 'REFRESH_INVALID' | 'REFRESH_REUSED' | 'REFRESH_EXPIRED' | 'SESSION_REVOKED' | 'TOKEN_INVALID' | 'TOKEN_EXPIRED' | 'UNKNOWN_401';
|
|
82
|
+
export type PermanentAuthReason = 'REFRESH_INVALID' | 'REFRESH_REUSED' | 'REFRESH_EXPIRED' | 'SESSION_REVOKED' | 'DEVICE_KEY_MISMATCH' | 'TOKEN_INVALID' | 'TOKEN_EXPIRED' | 'UNKNOWN_401';
|
|
78
83
|
export declare class PermanentAuthFailure extends Error {
|
|
79
84
|
readonly code: PermanentAuthReason;
|
|
80
85
|
constructor(code: PermanentAuthReason, message: string);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cliHandoffWire.ts — the ONE declaration of the CLI↔daemon hand-off wire
|
|
3
|
+
* contracts, shared by the daemon (writer/server) and the CLI (reader).
|
|
4
|
+
*
|
|
5
|
+
* Two contracts live here:
|
|
6
|
+
*
|
|
7
|
+
* 1. The hand-off descriptor file (`descriptor.json` on Dedicated guests,
|
|
8
|
+
* `daemon-island.<profile>.json` on laptops). Until now the shape was
|
|
9
|
+
* declared twice — `DedicatedCliHandoffDescriptor` daemon-side and
|
|
10
|
+
* `LocalBootstrap` CLI-side — two type declarations for one file format,
|
|
11
|
+
* the same drift disease `sessionPermissionAuthority` fixed.
|
|
12
|
+
* 2. The broker-mode redeem exchange: `POST /v1/auth/cli-handoff-token`
|
|
13
|
+
* request/response plus its error codes.
|
|
14
|
+
*
|
|
15
|
+
* Readers stay loose (unknown fields ignored, absent `handoffModes` reads as
|
|
16
|
+
* refresh-family-only) so a new field never breaks an older peer; writers
|
|
17
|
+
* must produce the full shape.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* What a caller may redeem the descriptor secret for.
|
|
21
|
+
* - `refresh_family`: the legacy durable mint (90-day cli-scope family).
|
|
22
|
+
* - `access_token`: broker mode — a short-lived access token from a
|
|
23
|
+
* daemon-owned family; nothing durable leaves the daemon.
|
|
24
|
+
*/
|
|
25
|
+
export declare const CLI_HANDOFF_MODES: readonly ["refresh_family", "access_token"];
|
|
26
|
+
export type CliHandoffMode = (typeof CLI_HANDOFF_MODES)[number];
|
|
27
|
+
/** Broker-mode redeem route on the daemon's loopback server. */
|
|
28
|
+
export declare const CLI_HANDOFF_TOKEN_PATH = "/v1/auth/cli-handoff-token";
|
|
29
|
+
/** Error codes this contract emits. String literals so both ends compare
|
|
30
|
+
* without importing a symbol they may not have. */
|
|
31
|
+
export declare const CLI_HANDOFF_TOKEN_ERROR_UNAVAILABLE = "HANDOFF_TOKEN_UNAVAILABLE";
|
|
32
|
+
/** Emitted by the LEGACY /v1/auth/cli-handoff route when a hand-off-secret
|
|
33
|
+
* caller asks for a refresh family the daemon no longer mints for it. */
|
|
34
|
+
export declare const CLI_HANDOFF_ERROR_BROKER_ONLY = "HANDOFF_BROKER_ONLY";
|
|
35
|
+
/**
|
|
36
|
+
* The descriptor file format. `version` is absent on the oldest island files
|
|
37
|
+
* and `1` on the first dedicated-guest descriptors; `handoffModes` first
|
|
38
|
+
* appears at version 2. Every field the CLI needs to dial is validated; the
|
|
39
|
+
* rest ride along untyped.
|
|
40
|
+
*/
|
|
41
|
+
export interface CliHandoffDescriptor {
|
|
42
|
+
version?: number;
|
|
43
|
+
pid?: number;
|
|
44
|
+
daemonId?: string;
|
|
45
|
+
host?: string;
|
|
46
|
+
port?: number;
|
|
47
|
+
/** Base64 handshake secret — hand-off-only on guests, island on laptops. */
|
|
48
|
+
secret?: string;
|
|
49
|
+
/** Server URL the daemon is authenticated against. Older daemons omit it;
|
|
50
|
+
* readers refuse to broker without it (never mint against an unknown
|
|
51
|
+
* server). */
|
|
52
|
+
serverUrl?: string;
|
|
53
|
+
/** Daemon-authored epoch counter; bumped when the descriptor is republished. */
|
|
54
|
+
epoch?: number;
|
|
55
|
+
handoffModes?: CliHandoffMode[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Parse an untrusted descriptor JSON value. Returns null when the fields the
|
|
59
|
+
* caller cannot proceed without — host, port, secret — are absent or
|
|
60
|
+
* mistyped. Unknown fields and future `version` values are tolerated: the
|
|
61
|
+
* reader ignores them, which is what makes additive evolution safe.
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseCliHandoffDescriptor(value: unknown): CliHandoffDescriptor | null;
|
|
64
|
+
/**
|
|
65
|
+
* Untrusted `handoffModes` input → known literals only, deduped. An
|
|
66
|
+
* unrecognized future mode is dropped rather than widening by coercion.
|
|
67
|
+
*/
|
|
68
|
+
export declare function normalizeHandoffModes(value: unknown): CliHandoffMode[];
|
|
69
|
+
/**
|
|
70
|
+
* The versioned-default rule: a descriptor that never declares modes offers
|
|
71
|
+
* only the durable mint it has always offered. Mode negotiation is purely
|
|
72
|
+
* additive — a daemon advertises the new mode, it never un-advertises the
|
|
73
|
+
* old one by accident.
|
|
74
|
+
*/
|
|
75
|
+
export declare function advertisedHandoffModes(d: CliHandoffDescriptor): CliHandoffMode[];
|
|
76
|
+
export declare function descriptorOffersAccessToken(d: CliHandoffDescriptor): boolean;
|
|
77
|
+
export interface CliHandoffTokenRequest {
|
|
78
|
+
/** Bypass the daemon broker's cached token (the 401-recovery path). */
|
|
79
|
+
forceRefresh?: boolean;
|
|
80
|
+
}
|
|
81
|
+
export interface CliHandoffTokenResponse {
|
|
82
|
+
accessToken: string;
|
|
83
|
+
/** Absolute expiry in ms since epoch, when the daemon knows it. */
|
|
84
|
+
accessExpiresAt?: number;
|
|
85
|
+
/** Server the minted credential belongs to — lets the caller verify it is
|
|
86
|
+
* about to use a token for the API it thinks it is talking to. */
|
|
87
|
+
serverUrl?: string;
|
|
88
|
+
}
|
|
89
|
+
/** Parse the redeem response; null when the token itself is absent. */
|
|
90
|
+
export declare function parseCliHandoffTokenResponse(value: unknown): CliHandoffTokenResponse | null;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cliHandoffWire.ts — the ONE declaration of the CLI↔daemon hand-off wire
|
|
3
|
+
* contracts, shared by the daemon (writer/server) and the CLI (reader).
|
|
4
|
+
*
|
|
5
|
+
* Two contracts live here:
|
|
6
|
+
*
|
|
7
|
+
* 1. The hand-off descriptor file (`descriptor.json` on Dedicated guests,
|
|
8
|
+
* `daemon-island.<profile>.json` on laptops). Until now the shape was
|
|
9
|
+
* declared twice — `DedicatedCliHandoffDescriptor` daemon-side and
|
|
10
|
+
* `LocalBootstrap` CLI-side — two type declarations for one file format,
|
|
11
|
+
* the same drift disease `sessionPermissionAuthority` fixed.
|
|
12
|
+
* 2. The broker-mode redeem exchange: `POST /v1/auth/cli-handoff-token`
|
|
13
|
+
* request/response plus its error codes.
|
|
14
|
+
*
|
|
15
|
+
* Readers stay loose (unknown fields ignored, absent `handoffModes` reads as
|
|
16
|
+
* refresh-family-only) so a new field never breaks an older peer; writers
|
|
17
|
+
* must produce the full shape.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* What a caller may redeem the descriptor secret for.
|
|
21
|
+
* - `refresh_family`: the legacy durable mint (90-day cli-scope family).
|
|
22
|
+
* - `access_token`: broker mode — a short-lived access token from a
|
|
23
|
+
* daemon-owned family; nothing durable leaves the daemon.
|
|
24
|
+
*/
|
|
25
|
+
export const CLI_HANDOFF_MODES = ['refresh_family', 'access_token'];
|
|
26
|
+
/** Broker-mode redeem route on the daemon's loopback server. */
|
|
27
|
+
export const CLI_HANDOFF_TOKEN_PATH = '/v1/auth/cli-handoff-token';
|
|
28
|
+
/** Error codes this contract emits. String literals so both ends compare
|
|
29
|
+
* without importing a symbol they may not have. */
|
|
30
|
+
export const CLI_HANDOFF_TOKEN_ERROR_UNAVAILABLE = 'HANDOFF_TOKEN_UNAVAILABLE';
|
|
31
|
+
/** Emitted by the LEGACY /v1/auth/cli-handoff route when a hand-off-secret
|
|
32
|
+
* caller asks for a refresh family the daemon no longer mints for it. */
|
|
33
|
+
export const CLI_HANDOFF_ERROR_BROKER_ONLY = 'HANDOFF_BROKER_ONLY';
|
|
34
|
+
/**
|
|
35
|
+
* Parse an untrusted descriptor JSON value. Returns null when the fields the
|
|
36
|
+
* caller cannot proceed without — host, port, secret — are absent or
|
|
37
|
+
* mistyped. Unknown fields and future `version` values are tolerated: the
|
|
38
|
+
* reader ignores them, which is what makes additive evolution safe.
|
|
39
|
+
*/
|
|
40
|
+
export function parseCliHandoffDescriptor(value) {
|
|
41
|
+
if (value == null || typeof value !== 'object')
|
|
42
|
+
return null;
|
|
43
|
+
const v = value;
|
|
44
|
+
if (typeof v.host !== 'string' || v.host.length === 0)
|
|
45
|
+
return null;
|
|
46
|
+
if (typeof v.port !== 'number' || !Number.isInteger(v.port) || v.port <= 0)
|
|
47
|
+
return null;
|
|
48
|
+
if (typeof v.secret !== 'string' || v.secret.length === 0)
|
|
49
|
+
return null;
|
|
50
|
+
return {
|
|
51
|
+
...(typeof v.version === 'number' ? { version: v.version } : {}),
|
|
52
|
+
...(typeof v.pid === 'number' ? { pid: v.pid } : {}),
|
|
53
|
+
...(typeof v.daemonId === 'string' ? { daemonId: v.daemonId } : {}),
|
|
54
|
+
host: v.host,
|
|
55
|
+
port: v.port,
|
|
56
|
+
secret: v.secret,
|
|
57
|
+
...(typeof v.serverUrl === 'string' ? { serverUrl: v.serverUrl } : {}),
|
|
58
|
+
...(typeof v.epoch === 'number' ? { epoch: v.epoch } : {}),
|
|
59
|
+
...(Array.isArray(v.handoffModes)
|
|
60
|
+
? { handoffModes: normalizeHandoffModes(v.handoffModes) }
|
|
61
|
+
: {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Untrusted `handoffModes` input → known literals only, deduped. An
|
|
66
|
+
* unrecognized future mode is dropped rather than widening by coercion.
|
|
67
|
+
*/
|
|
68
|
+
export function normalizeHandoffModes(value) {
|
|
69
|
+
if (!Array.isArray(value))
|
|
70
|
+
return [];
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const m of value) {
|
|
73
|
+
if (CLI_HANDOFF_MODES.includes(m) &&
|
|
74
|
+
!out.includes(m)) {
|
|
75
|
+
out.push(m);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The versioned-default rule: a descriptor that never declares modes offers
|
|
82
|
+
* only the durable mint it has always offered. Mode negotiation is purely
|
|
83
|
+
* additive — a daemon advertises the new mode, it never un-advertises the
|
|
84
|
+
* old one by accident.
|
|
85
|
+
*/
|
|
86
|
+
export function advertisedHandoffModes(d) {
|
|
87
|
+
const declared = normalizeHandoffModes(d.handoffModes);
|
|
88
|
+
return declared.length > 0 ? declared : ['refresh_family'];
|
|
89
|
+
}
|
|
90
|
+
export function descriptorOffersAccessToken(d) {
|
|
91
|
+
return advertisedHandoffModes(d).includes('access_token');
|
|
92
|
+
}
|
|
93
|
+
/** Parse the redeem response; null when the token itself is absent. */
|
|
94
|
+
export function parseCliHandoffTokenResponse(value) {
|
|
95
|
+
if (value == null || typeof value !== 'object')
|
|
96
|
+
return null;
|
|
97
|
+
const v = value;
|
|
98
|
+
if (typeof v.accessToken !== 'string' || v.accessToken.length === 0)
|
|
99
|
+
return null;
|
|
100
|
+
return {
|
|
101
|
+
accessToken: v.accessToken,
|
|
102
|
+
...(typeof v.accessExpiresAt === 'number' ? { accessExpiresAt: v.accessExpiresAt } : {}),
|
|
103
|
+
...(typeof v.serverUrl === 'string' ? { serverUrl: v.serverUrl } : {}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -21,6 +21,8 @@ type DisabledReason = 'kek_unavailable' | 'init_failed' | 'platform_unsupported'
|
|
|
21
21
|
export declare function __setStateForTest(next: CredEnvelopeState): CredEnvelopeState;
|
|
22
22
|
/** @internal Test seam — read state for assertions. */
|
|
23
23
|
export declare function __getStateForTest(): CredEnvelopeState;
|
|
24
|
+
/** @internal Test seam — each spec owns its warn-dedupe horizon. */
|
|
25
|
+
export declare function __clearWarnDedupeForTest(): void;
|
|
24
26
|
/**
|
|
25
27
|
* Initialize the envelope path. Idempotent — second call when state is
|
|
26
28
|
* non-uninit returns immediately.
|
|
@@ -123,7 +123,21 @@ function atomicWrite0600(target, value) {
|
|
|
123
123
|
* The event name passed in is the FULLY-QUALIFIED `cred_envelope.*` form
|
|
124
124
|
* because daemon-side telemetry consumers (`event-log.ts`, dashboards,
|
|
125
125
|
* spec assertions) key off the prefixed name. Do not strip the prefix.
|
|
126
|
+
*
|
|
127
|
+
* Identical failure warnings hit the logger only once per process.
|
|
128
|
+
* Telemetry still records every occurrence — the dedupe exists because a
|
|
129
|
+
* single unreadable envelope produced the same warning once per stored
|
|
130
|
+
* field (token, refresh token, expiry…), which read to users as three
|
|
131
|
+
* crashes rather than one stale credential (OSK-12074). The signature is
|
|
132
|
+
* event + kek identity + message, so a genuinely different failure mode
|
|
133
|
+
* still gets its own line.
|
|
126
134
|
*/
|
|
135
|
+
const warnedSignatures = new Set();
|
|
136
|
+
const MAX_WARNED_SIGNATURES = 64;
|
|
137
|
+
/** @internal Test seam — each spec owns its warn-dedupe horizon. */
|
|
138
|
+
export function __clearWarnDedupeForTest() {
|
|
139
|
+
warnedSignatures.clear();
|
|
140
|
+
}
|
|
127
141
|
function emit(eventName, success, metadata) {
|
|
128
142
|
emitAuthTelemetry({
|
|
129
143
|
timestamp: new Date().toISOString(),
|
|
@@ -132,7 +146,13 @@ function emit(eventName, success, metadata) {
|
|
|
132
146
|
metadata: { event: eventName, ...metadata },
|
|
133
147
|
});
|
|
134
148
|
if (!success) {
|
|
135
|
-
|
|
149
|
+
const signature = `${eventName}:${metadata.kekId ?? ''}:${metadata.kekKind ?? ''}:${metadata.message ?? ''}`;
|
|
150
|
+
if (!warnedSignatures.has(signature)) {
|
|
151
|
+
if (warnedSignatures.size >= MAX_WARNED_SIGNATURES)
|
|
152
|
+
warnedSignatures.clear();
|
|
153
|
+
warnedSignatures.add(signature);
|
|
154
|
+
getAuthLogger().warn(`[credEnvelope] ${eventName}`, metadata);
|
|
155
|
+
}
|
|
136
156
|
}
|
|
137
157
|
}
|
|
138
158
|
// ---------------------------------------------------------------------------
|
|
@@ -745,6 +765,10 @@ export function maybeDecryptOnRead(stored) {
|
|
|
745
765
|
kekId: _state.kekId,
|
|
746
766
|
kekKind: _state.kekKind,
|
|
747
767
|
message: err?.message ?? 'unknown',
|
|
768
|
+
// The crypto error alone reads as a crash; the actionable truth is
|
|
769
|
+
// that this stored credential is unreadable by THIS binary (usually
|
|
770
|
+
// a signing-identity or profile drift) and re-login rebuilds it.
|
|
771
|
+
hint: 'stored credential is unreadable on this machine — signing in again rebuilds it',
|
|
748
772
|
});
|
|
749
773
|
return { plaintext: null, needsMigration: false };
|
|
750
774
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* credentialSession.ts — the shared credential-lifecycle kernel (policy half).
|
|
3
|
+
*
|
|
4
|
+
* A credential lifecycle is policy × mechanism. The policy — serve a cached
|
|
5
|
+
* token until its expiry-skew window, collapse concurrent acquires onto one
|
|
6
|
+
* in-flight operation, invalidate on a resource-side 401 — is identical for
|
|
7
|
+
* every surface and was being re-implemented per consumer (task token-broker,
|
|
8
|
+
* delegated-cli redemption, the human CLI refresh path). The mechanism —
|
|
9
|
+
* WHERE the durable secret lives and HOW a token is acquired — legitimately
|
|
10
|
+
* differs (in-memory daemon family vs file+envelope+lock vs loopback redeem)
|
|
11
|
+
* and stays behind the `acquire` seam.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately thin: no persistence, no retry policy, no logging. Callers
|
|
14
|
+
* own their store and their error surfaces; this owns the cache/single-
|
|
15
|
+
* flight/invalidate invariants so they cannot drift between surfaces.
|
|
16
|
+
*/
|
|
17
|
+
/** What an acquire returns. `expiresAtMs` absent means "no known expiry" —
|
|
18
|
+
* the token is served until `invalidate()` or an acquire replaces it. */
|
|
19
|
+
export interface AcquiredCredential {
|
|
20
|
+
accessToken: string;
|
|
21
|
+
/** Absolute expiry in ms since epoch, when the issuer tells us. */
|
|
22
|
+
expiresAtMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface CredentialSessionOptions {
|
|
25
|
+
/**
|
|
26
|
+
* The mechanism. Called with `forceRefresh: true` when the caller asked to
|
|
27
|
+
* bypass the cache (the 401-recovery path) — implementations should renew
|
|
28
|
+
* rather than serve their own cache. A rejected promise propagates to every
|
|
29
|
+
* waiter and is never cached.
|
|
30
|
+
*/
|
|
31
|
+
acquire: (forceRefresh: boolean) => Promise<AcquiredCredential>;
|
|
32
|
+
/** Serve the cached token until this many ms before expiry. Default 30s. */
|
|
33
|
+
skewMs?: number;
|
|
34
|
+
/** Injectable clock for tests. */
|
|
35
|
+
now?: () => number;
|
|
36
|
+
}
|
|
37
|
+
export interface CredentialSession {
|
|
38
|
+
/**
|
|
39
|
+
* Resolves a usable access token: the cached one while it is inside its
|
|
40
|
+
* freshness window, otherwise a single shared acquire. `forceRefresh`
|
|
41
|
+
* discards the cache first (use after a resource 401).
|
|
42
|
+
*/
|
|
43
|
+
getToken(forceRefresh?: boolean): Promise<string>;
|
|
44
|
+
/** Drop the cached token; the next getToken() acquires. */
|
|
45
|
+
invalidate(): void;
|
|
46
|
+
}
|
|
47
|
+
export declare function createCredentialSession(opts: CredentialSessionOptions): CredentialSession;
|