@skrr-ai/cli 0.1.48 → 0.1.50
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 +16 -4
- package/dist/commands/agent-worker.d.ts +12 -0
- package/dist/commands/agent-worker.js +140 -0
- package/dist/commands/agents/create.js +14 -7
- package/dist/commands/agents/update.js +11 -5
- 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/effective-policy.js +26 -1
- package/dist/commands/commitments/explain.d.ts +17 -0
- package/dist/commands/commitments/explain.js +40 -0
- package/dist/commands/goals/create.d.ts +1 -0
- package/dist/commands/goals/create.js +22 -0
- 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/tasks/create.js +1 -0
- package/dist/commands/tasks/list.js +1 -0
- package/dist/commands/tasks/update.js +1 -0
- package/dist/lib/agent-config.d.ts +2 -0
- package/dist/lib/agent-config.js +3 -1
- package/dist/lib/api-fetch.js +19 -0
- package/dist/lib/auth-storage.d.ts +14 -0
- package/dist/lib/auth-storage.js +14 -0
- package/dist/lib/commitments.d.ts +17 -2
- package/dist/lib/commitments.js +114 -0
- package/dist/lib/daemon-installer.d.ts +6 -5
- package/dist/lib/daemon-installer.js +4 -17
- package/dist/lib/daemonBroker.d.ts +120 -31
- package/dist/lib/daemonBroker.js +313 -20
- 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/first-party-harness-broker.d.ts +10 -0
- package/dist/lib/first-party-harness-broker.js +9 -0
- 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/hosted-machines.d.ts +10 -1
- package/dist/lib/hosted-machines.js +36 -2
- package/dist/lib/login.js +16 -0
- package/dist/lib/machine-spend-cap.d.ts +23 -0
- package/dist/lib/machine-spend-cap.js +68 -0
- package/dist/lib/node-adapter.js +15 -3
- 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/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 +109 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +171 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +4 -2
- 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.js +8 -9
- 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/releaseManifest.d.ts +38 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.js +66 -1
- 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/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/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 +109 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +164 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
- 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.js +8 -9
- 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/releaseManifest.d.ts +38 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.js +64 -0
- 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/types.d.ts +6 -1
- package/dist/node_modules/@skrr-ai/auth-core/package.json +41 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3413 -3343
- package/oclif.manifest.json +8930 -8466
- package/package.json +4 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ATTESTED_TOOL_APPROVAL_TTL_MS = exports.DAEMON_TOOL_CATEGORIES = exports.DAEMON_TOOL_READ_ONLY_CAPABILITY = exports.DAEMON_TOOL_ATTESTED_APPROVAL_CAPABILITY = void 0;
|
|
4
|
+
exports.categoryForDaemonTool = categoryForDaemonTool;
|
|
5
|
+
exports.canonicalToolInputDigest = canonicalToolInputDigest;
|
|
6
|
+
exports.mintAttestedToolApproval = mintAttestedToolApproval;
|
|
7
|
+
exports.verifyAttestedToolApproval = verifyAttestedToolApproval;
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
/**
|
|
10
|
+
* Server-attested tool approval (OSK-11975 follow-up to the `manual` mode
|
|
11
|
+
* refusal on `daemon:tool:request`).
|
|
12
|
+
*
|
|
13
|
+
* A daemon category in `manual` mode means "a PERSON approves each call". On
|
|
14
|
+
* the harness-session path the daemon asks through the permission bridge; on
|
|
15
|
+
* the daemon-tool path a cloud agent's call used to be refused outright,
|
|
16
|
+
* because the request carried no approval and there was no channel back to a
|
|
17
|
+
* person. The attested-approval protocol closes that gap without weakening
|
|
18
|
+
* the guarantee:
|
|
19
|
+
*
|
|
20
|
+
* 1. The daemon announces `daemon_tool_attested_approval_v1` and its
|
|
21
|
+
* resolved per-category posture (`toolPermissionModes`) at register time.
|
|
22
|
+
* 2. Before proxying a call, the server reads the announced posture for the
|
|
23
|
+
* tool's category. `manual` means it must ask first — through the same
|
|
24
|
+
* permission surface (`canUseTool` → `permission:request`) an in-process
|
|
25
|
+
* SDK turn would have used — and only then dispatch, carrying an
|
|
26
|
+
* `attestedApproval` object on the request.
|
|
27
|
+
* 3. The daemon verifies the attestation's SHAPE and BINDING — it is bound
|
|
28
|
+
* to this exact (toolUseId, toolName, toolInput digest) and expires
|
|
29
|
+
* quickly — and allows the call. A missing or malformed attestation gets
|
|
30
|
+
* the same named refusal as before.
|
|
31
|
+
*
|
|
32
|
+
* What the daemon verifies is the binding, not the server's honesty: the
|
|
33
|
+
* request already arrives over the daemon's authenticated server channel, so
|
|
34
|
+
* the field IS the attestation. What the binding buys is that the model —
|
|
35
|
+
* which authors `toolInput` — cannot mint an approval, and an approval for
|
|
36
|
+
* one call cannot be replayed onto another.
|
|
37
|
+
*/
|
|
38
|
+
/** Announced by a daemon that honours `attestedApproval` on daemon:tool:request. */
|
|
39
|
+
exports.DAEMON_TOOL_ATTESTED_APPROVAL_CAPABILITY = 'daemon_tool_attested_approval_v1';
|
|
40
|
+
/**
|
|
41
|
+
* Announced by a daemon that honours `toolPosture: 'read_only'` on
|
|
42
|
+
* daemon:tool:request — the daemon-tool-path expression of the wire `plan`
|
|
43
|
+
* session mode: reads run, every mutating category is refused with a named
|
|
44
|
+
* reason. Carries INTENT like `browserPolicy: 'read_only'` — the daemon owns
|
|
45
|
+
* what read-only means, so the server cannot widen the fence by shipping a
|
|
46
|
+
* wider value.
|
|
47
|
+
*/
|
|
48
|
+
exports.DAEMON_TOOL_READ_ONLY_CAPABILITY = 'daemon_tool_read_only_v1';
|
|
49
|
+
exports.DAEMON_TOOL_CATEGORIES = [
|
|
50
|
+
'read',
|
|
51
|
+
'write',
|
|
52
|
+
'bash',
|
|
53
|
+
'browser',
|
|
54
|
+
'claude_session',
|
|
55
|
+
'native_ui',
|
|
56
|
+
];
|
|
57
|
+
/** Map a tool name to its category. Unknown tools take the restrictive answer. */
|
|
58
|
+
function categoryForDaemonTool(toolName) {
|
|
59
|
+
switch (String(toolName || '').toLowerCase()) {
|
|
60
|
+
case 'read':
|
|
61
|
+
case 'glob':
|
|
62
|
+
case 'grep':
|
|
63
|
+
case 'filetransferread':
|
|
64
|
+
return 'read';
|
|
65
|
+
case 'write':
|
|
66
|
+
case 'edit':
|
|
67
|
+
case 'multiedit':
|
|
68
|
+
case 'apply_patch':
|
|
69
|
+
case 'filetransferwrite':
|
|
70
|
+
return 'write';
|
|
71
|
+
// Reading or stopping a background command is part of running it.
|
|
72
|
+
case 'bash':
|
|
73
|
+
case 'taskoutput':
|
|
74
|
+
case 'taskstop':
|
|
75
|
+
return 'bash';
|
|
76
|
+
case 'browser':
|
|
77
|
+
return 'browser';
|
|
78
|
+
case 'native':
|
|
79
|
+
return 'native_ui';
|
|
80
|
+
case 'claude_session':
|
|
81
|
+
case 'claude-session':
|
|
82
|
+
return 'claude_session';
|
|
83
|
+
default:
|
|
84
|
+
return 'write';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Attested approval
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
/**
|
|
91
|
+
* How long an attestation stays valid. Short: it answers ONE person, about
|
|
92
|
+
* THIS call, moments ago — it is not a standing grant. The server asks and
|
|
93
|
+
* dispatches back-to-back, so the window only has to outlive publish latency.
|
|
94
|
+
*/
|
|
95
|
+
exports.ATTESTED_TOOL_APPROVAL_TTL_MS = 60 * 1000;
|
|
96
|
+
/**
|
|
97
|
+
* Deterministic serialization for the input digest: objects sort keys
|
|
98
|
+
* recursively, arrays keep order, non-JSON values normalize the way
|
|
99
|
+
* `JSON.stringify` would emit them (undefined/functions in objects drop, in
|
|
100
|
+
* arrays become null). Both runtimes must digest the SAME bytes — the server
|
|
101
|
+
* mints and the daemon verifies — so this lives here, once.
|
|
102
|
+
*/
|
|
103
|
+
function canonicalJson(value) {
|
|
104
|
+
if (value === null || typeof value !== 'object') {
|
|
105
|
+
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
|
|
106
|
+
return 'null';
|
|
107
|
+
}
|
|
108
|
+
return JSON.stringify(value);
|
|
109
|
+
}
|
|
110
|
+
if (Array.isArray(value)) {
|
|
111
|
+
return `[${value.map((entry) => canonicalJson(entry)).join(',')}]`;
|
|
112
|
+
}
|
|
113
|
+
const record = value;
|
|
114
|
+
const keys = Object.keys(record).filter((key) => record[key] !== undefined &&
|
|
115
|
+
typeof record[key] !== 'function' &&
|
|
116
|
+
typeof record[key] !== 'symbol');
|
|
117
|
+
keys.sort();
|
|
118
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
|
|
119
|
+
}
|
|
120
|
+
/** The digest both sides compute over `toolInput`. */
|
|
121
|
+
function canonicalToolInputDigest(toolInput) {
|
|
122
|
+
return (0, node_crypto_1.createHash)('sha256')
|
|
123
|
+
.update(canonicalJson(toolInput ?? {}))
|
|
124
|
+
.digest('hex');
|
|
125
|
+
}
|
|
126
|
+
/** Server side: mint the attestation after a person approved THIS call. */
|
|
127
|
+
function mintAttestedToolApproval(args) {
|
|
128
|
+
const now = args.now ?? Date.now();
|
|
129
|
+
return {
|
|
130
|
+
v: 1,
|
|
131
|
+
toolUseId: args.toolUseId,
|
|
132
|
+
toolName: args.toolName,
|
|
133
|
+
inputSha256: canonicalToolInputDigest(args.toolInput),
|
|
134
|
+
decidedBy: 'user',
|
|
135
|
+
issuedAt: now,
|
|
136
|
+
expiresAt: now + (args.ttlMs ?? exports.ATTESTED_TOOL_APPROVAL_TTL_MS),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Daemon side: verify an attestation against the call it claims to approve.
|
|
141
|
+
* Untrusted wire input — every field is checked, and any mismatch is a plain
|
|
142
|
+
* "no approval", never a partial credit. `reason` is for daemon logs; the
|
|
143
|
+
* refusal text stays generic so a probing caller learns nothing new.
|
|
144
|
+
*/
|
|
145
|
+
function verifyAttestedToolApproval(approval, call) {
|
|
146
|
+
if (approval === undefined || approval === null)
|
|
147
|
+
return { ok: false, reason: 'absent' };
|
|
148
|
+
if (typeof approval !== 'object' || Array.isArray(approval)) {
|
|
149
|
+
return { ok: false, reason: 'malformed' };
|
|
150
|
+
}
|
|
151
|
+
const a = approval;
|
|
152
|
+
if (a.v !== 1 ||
|
|
153
|
+
typeof a.toolUseId !== 'string' ||
|
|
154
|
+
typeof a.toolName !== 'string' ||
|
|
155
|
+
typeof a.inputSha256 !== 'string' ||
|
|
156
|
+
a.decidedBy !== 'user' ||
|
|
157
|
+
typeof a.issuedAt !== 'number' ||
|
|
158
|
+
typeof a.expiresAt !== 'number') {
|
|
159
|
+
return { ok: false, reason: 'malformed' };
|
|
160
|
+
}
|
|
161
|
+
const now = call.now ?? Date.now();
|
|
162
|
+
if (a.expiresAt <= now || a.issuedAt > now + 30_000) {
|
|
163
|
+
return { ok: false, reason: 'expired' };
|
|
164
|
+
}
|
|
165
|
+
if (a.toolUseId !== call.toolUseId ||
|
|
166
|
+
a.toolName.toLowerCase() !== String(call.toolName).toLowerCase() ||
|
|
167
|
+
a.inputSha256 !== canonicalToolInputDigest(call.toolInput)) {
|
|
168
|
+
return { ok: false, reason: 'binding' };
|
|
169
|
+
}
|
|
170
|
+
return { ok: true };
|
|
171
|
+
}
|
|
@@ -223,7 +223,7 @@ export declare function firstPartyHarnessManifestFile(provider?: string): string
|
|
|
223
223
|
* The update feed ORIGIN. Not a product name, and not moved by this identity:
|
|
224
224
|
* a feed host sits inside signed manifests (naming map §2 "Update feed").
|
|
225
225
|
*/
|
|
226
|
-
export declare const FIRST_PARTY_HARNESS_FEED_ORIGIN = "https://updates.
|
|
226
|
+
export declare const FIRST_PARTY_HARNESS_FEED_ORIGIN = "https://updates.skrr.ai";
|
|
227
227
|
/** Object-key prefix for one spelling's feed, e.g. `skrr-code/`. */
|
|
228
228
|
export declare function firstPartyHarnessFeedPrefix(provider?: string): string;
|
|
229
229
|
/**
|
|
@@ -339,7 +339,7 @@ function firstPartyHarnessManifestFile(provider = exports.FIRST_PARTY_HARNESS.pr
|
|
|
339
339
|
* The update feed ORIGIN. Not a product name, and not moved by this identity:
|
|
340
340
|
* a feed host sits inside signed manifests (naming map §2 "Update feed").
|
|
341
341
|
*/
|
|
342
|
-
exports.FIRST_PARTY_HARNESS_FEED_ORIGIN = 'https://updates.
|
|
342
|
+
exports.FIRST_PARTY_HARNESS_FEED_ORIGIN = 'https://updates.skrr.ai';
|
|
343
343
|
/** Object-key prefix for one spelling's feed, e.g. `skrr-code/`. */
|
|
344
344
|
function firstPartyHarnessFeedPrefix(provider = exports.FIRST_PARTY_HARNESS.provider) {
|
|
345
345
|
return `${provider}/`;
|
|
@@ -40,6 +40,6 @@ export * from './firstPartyHarness.js';
|
|
|
40
40
|
export { FIRST_PARTY_HARNESS_PREVIOUS_SUFFIX, FIRST_PARTY_HARNESS_PROVENANCE_SUFFIX, firstPartyHarnessBinDir, firstPartyHarnessHomeDir, firstPartyHarnessInstalledBinaryPath, linkFirstPartyHarnessAliases, migrateFirstPartyHarnessHome, type FirstPartyHarnessHomeFs, type FirstPartyHarnessHomeMigration, } from './firstPartyHarnessHome.js';
|
|
41
41
|
export { DEFAULT_FIRST_PARTY_HARNESS_CHANNEL, FIRST_PARTY_HARNESS_CHANNELS, firstPartyHarnessChannelPrefix, firstPartyHarnessFeedBase, firstPartyHarnessFeedCandidates, isFirstPartyHarnessChannel, resolveChannelManifestUrl, resolveFirstPartyHarnessChannel, type FirstPartyHarnessChannel, type FirstPartyHarnessFeedCandidate, } from './firstPartyHarnessChannels.js';
|
|
42
42
|
export { OVERSKY_RELEASE_PUBLIC_KEYS, type ReleaseKey } from './releaseKeys.js';
|
|
43
|
-
export { MAX_SIGS, buildArtifactMessage, buildManifestMessage, getDeltaFor, verifyArtifact, verifyManifest, type DaemonVersionManifest, type DeltaDescriptor, type ManifestSignature, type PlatformArtifact, type VerifyResult, } from './releaseManifest.js';
|
|
43
|
+
export { MAX_SIGS, TRUSTED_FEED_ORIGINS, buildArtifactMessage, buildManifestMessage, getDeltaFor, isTrustedArtifactOrigin, verifyArtifact, verifyManifest, type DaemonVersionManifest, type DeltaDescriptor, type ManifestSignature, type PlatformArtifact, type VerifyResult, } from './releaseManifest.js';
|
|
44
44
|
export { resolveConfigRoot, configRootPath, isConfigRootOverridden } from './configRoot.js';
|
|
45
45
|
export { PROFILES_SEGMENT, SUPERSEDED_MARKER_SUFFIX, SUPERSEDED_COPY_SUFFIX, profileStateDir, legacyProfileStateDir, resolveProfileStateDir, resolveProfileStateFile, profileStateFileLocations, profileStateFilePathsToClear, consolidateProfileState, type ResolvedProfileStateDir, type ConsolidationSpec, type ConsolidationStatus, type ConsolidationOutcome, } from './profileStateDir.js';
|
|
@@ -25,8 +25,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
25
25
|
exports.generateVerifier = exports.invalidateHeadersHelperCache = exports.runHeadersHelper = exports.SENTINEL_TRIGGERING_FAILURES = exports.__resetAuthHelperFailureReasonForTest = exports.getLastAuthHelperFailureReason = exports.getDefaultHelperTtlMs = exports.invokeAuthHelper = exports.AuthHelperUnavailableError = exports.AuthHelperUntrustedError = exports.CI_TOKEN_PREFIX = exports.classifyTokenKind = exports.invalidateAuthHelperCache = exports.__resetCredentialResolverForTest = exports.CREDENTIAL_PRECEDENCE = exports.CredentialResolver = exports.__setFdReaderForTest = exports.readAuthFromFd = exports.createRefreshScheduler = exports.UNKNOWN_401_BUDGET = exports.__resetUnknown401StreaksForTest = exports.__refreshDiagnostics = exports.installLockExitHandlers = exports.clearReauthState = exports.needsReauthState = exports.persistReauthState = exports.withAuthLock = exports.revokeDaemonRefreshSession = exports.refreshDaemonToken = exports.classifyRefreshResponse = exports.EXIT_NEEDS_REAUTH = exports.reauthCommand = exports.formatReauthMessage = exports.handleMidStreamAuthExpiry = exports.isAuthExpiredSseEvent = exports.decodeJwtPayload = exports.decodeJwtExpiry = exports.TransientAuthFailure = exports.PermanentAuthFailure = exports.emitAuthTelemetry = exports.getAuthBinaryName = exports.getAuthKillSwitches = exports.getAuthConfigDir = exports.getAuthLogger = exports.configureAuthCore = exports.OAUTH_CLI_SCOPE = exports.isDaemonScope = exports.VALID_DAEMON_SCOPE_SET = exports.ALL_DAEMON_SCOPES = exports.SCOPES = void 0;
|
|
26
26
|
exports.publicJwkFromX = exports.normalizeHtu = exports.jwkThumbprint = exports.generateDeviceKeyPair = exports.buildDeviceProof = exports.zeroizeKekCaches = exports.WindowsKek = exports.registerZeroizeHook = exports.MacosKek = exports.LinuxKek = exports.KekUnavailableError = exports.UnavailableKek = exports.InMemoryKek = exports.getKekStrategy = exports.__setKekStrategyForTest = exports.credEnvelopeWrap = exports.credEnvelopeUnwrap = exports.credEnvelopeSerialize = exports.credSealBuffer = exports.credOpenBuffer = exports.CRED_KEY_LEN = exports.isCredEnvelopeString = exports.generateDek = exports.CRED_ENVELOPE_MAGIC = exports.credEnvelopeEncrypt = exports.credEnvelopeDeserialize = exports.credEnvelopeDecrypt = exports.buildCredentialAad = exports.isValidRecoveryCode = exports.normalizeRecoveryCode = exports.parseRecoveryCode = exports.formatRecoveryCode = exports.MAX_TTL_DAYS = exports.MIN_TTL_DAYS = exports.parseTtlDays = exports.LoginExchangeError = exports.LoginInitError = exports.loginWithLocalhost = exports.LocalCallbackMalformedError = exports.LocalCallbackDeniedError = exports.LocalCallbackStateMismatchError = exports.LocalCallbackTimeoutError = exports.LocalCallbackBindError = exports.LocalCallbackError = exports.DEFAULT_LOGIN_TIMEOUT_MS = exports.startLocalCallback = exports.verifyChallenge = exports.safeEqual = exports.generateState = exports.challenge = void 0;
|
|
27
27
|
exports.pairUint8ArrayToBase64Url = exports.pairUint8ArrayToBase64 = exports.parsePairUrl = exports.parsePairPlaintext = exports.parsePairBundle = exports.openPairBundleBase64 = exports.generatePairKeyPair = exports.buildPairUrl = exports.buildPairAad = exports.pairBase64UrlToUint8Array = exports.pairBase64ToUint8Array = exports.assemblePairBundleBase64 = exports.PairBundleError = exports.PAIR_SECRET_LEN = exports.PAIR_PUBKEY_LEN = exports.PAIR_NONCE_LEN = exports.PAIR_BUNDLE_MAX_BASE64_LEN = exports.PAIR_AAD_MAX_LEN = exports.PAIR_AAD_PREFIX = exports.__resetCachedDaemonIdForTest = exports.legacyDaemonUuids = exports.ensureMachineDaemonId = exports.__getDeviceIdentityStateForTest = exports.__setDeviceIdentityStateForTest = exports.__enrollmentBackoffPathForTest = exports.__enrollmentMarkerPathForTest = exports.__privateKeyPathForTest = exports.__publicKeyPathForTest = exports.describeDeviceIdentityState = exports.getDevicePublicKey = exports.isDeviceIdentityActive = exports.clearEnrollmentMarker = exports.enrollWithServer = exports.signServerRequest = exports.resetDeviceIdentity = exports.initDeviceIdentity = exports.__resetShutdownGuardForTest = exports.__getCredEnvelopeStateForTest = exports.__setCredEnvelopeStateForTest = exports.__wrappedDekFilePathForTest = exports.maybeDecryptOnRead = exports.maybeEncryptForWrite = exports.describeCredEnvelopeState = exports.hasCredEnvelopePathConflict = exports.isCredEnvelopeActive = exports.shutdownCredEnvelope = exports.resetCredEnvelope = exports.initCredEnvelope = exports.verifyDeviceProof = exports.tokenHash = void 0;
|
|
28
|
-
exports.
|
|
29
|
-
exports.consolidateProfileState = exports.profileStateFilePathsToClear = exports.profileStateFileLocations = exports.resolveProfileStateFile = exports.resolveProfileStateDir = void 0;
|
|
28
|
+
exports.SUPERSEDED_COPY_SUFFIX = exports.SUPERSEDED_MARKER_SUFFIX = exports.PROFILES_SEGMENT = exports.isConfigRootOverridden = exports.configRootPath = exports.resolveConfigRoot = exports.verifyManifest = exports.verifyArtifact = exports.isTrustedArtifactOrigin = exports.getDeltaFor = exports.buildManifestMessage = exports.buildArtifactMessage = exports.TRUSTED_FEED_ORIGINS = exports.MAX_SIGS = exports.OVERSKY_RELEASE_PUBLIC_KEYS = exports.resolveFirstPartyHarnessChannel = exports.resolveChannelManifestUrl = exports.isFirstPartyHarnessChannel = exports.firstPartyHarnessFeedCandidates = exports.firstPartyHarnessFeedBase = exports.firstPartyHarnessChannelPrefix = exports.FIRST_PARTY_HARNESS_CHANNELS = exports.DEFAULT_FIRST_PARTY_HARNESS_CHANNEL = exports.migrateFirstPartyHarnessHome = exports.linkFirstPartyHarnessAliases = exports.firstPartyHarnessInstalledBinaryPath = exports.firstPartyHarnessHomeDir = exports.firstPartyHarnessBinDir = exports.FIRST_PARTY_HARNESS_PROVENANCE_SUFFIX = exports.FIRST_PARTY_HARNESS_PREVIOUS_SUFFIX = exports.describeLegacyState = exports.findLegacyLocalState = exports.DEFAULT_BINARY_NAME = exports.LEGACY_NATIVE_ID_PREFIXES = exports.NATIVE_ID_PREFIX = exports.LEGACY_CONFIG_DIR_NAMES = exports.CONFIG_DIR_NAME = exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_NAME_TOKENS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = exports.sanitizeSpawnEnv = exports.harnessLoadsClaudeSkillsLayout = exports.claudeSkillsLayoutEvidence = exports.HARNESSES_LOADING_CLAUDE_SKILLS_LAYOUT = exports.tierPermitsConfiguredAutoMode = exports.credentialPolicyForTier = exports.getHarnessTier = exports.HARNESS_TIERS = void 0;
|
|
29
|
+
exports.consolidateProfileState = exports.profileStateFilePathsToClear = exports.profileStateFileLocations = exports.resolveProfileStateFile = exports.resolveProfileStateDir = exports.legacyProfileStateDir = exports.profileStateDir = void 0;
|
|
30
30
|
// Daemon scope vocabulary — single source of truth for non-Mongoose tiers
|
|
31
31
|
// (daemon, desktop, cli). Schema-bound canonical lives in
|
|
32
32
|
// `@skrr-ai/data-schemas/common/daemonScopes`; the two are kept byte-equal
|
|
@@ -361,9 +361,11 @@ var releaseKeys_js_1 = require("./releaseKeys.js");
|
|
|
361
361
|
Object.defineProperty(exports, "OVERSKY_RELEASE_PUBLIC_KEYS", { enumerable: true, get: function () { return releaseKeys_js_1.OVERSKY_RELEASE_PUBLIC_KEYS; } });
|
|
362
362
|
var releaseManifest_js_1 = require("./releaseManifest.js");
|
|
363
363
|
Object.defineProperty(exports, "MAX_SIGS", { enumerable: true, get: function () { return releaseManifest_js_1.MAX_SIGS; } });
|
|
364
|
+
Object.defineProperty(exports, "TRUSTED_FEED_ORIGINS", { enumerable: true, get: function () { return releaseManifest_js_1.TRUSTED_FEED_ORIGINS; } });
|
|
364
365
|
Object.defineProperty(exports, "buildArtifactMessage", { enumerable: true, get: function () { return releaseManifest_js_1.buildArtifactMessage; } });
|
|
365
366
|
Object.defineProperty(exports, "buildManifestMessage", { enumerable: true, get: function () { return releaseManifest_js_1.buildManifestMessage; } });
|
|
366
367
|
Object.defineProperty(exports, "getDeltaFor", { enumerable: true, get: function () { return releaseManifest_js_1.getDeltaFor; } });
|
|
368
|
+
Object.defineProperty(exports, "isTrustedArtifactOrigin", { enumerable: true, get: function () { return releaseManifest_js_1.isTrustedArtifactOrigin; } });
|
|
367
369
|
Object.defineProperty(exports, "verifyArtifact", { enumerable: true, get: function () { return releaseManifest_js_1.verifyArtifact; } });
|
|
368
370
|
Object.defineProperty(exports, "verifyManifest", { enumerable: true, get: function () { return releaseManifest_js_1.verifyManifest; } });
|
|
369
371
|
// The one definition of the skrr root, shared by `skrr` and `skrrd`. See the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type AuthFailureReason = 'REFRESH_INVALID' | 'REFRESH_REUSED' | 'REFRESH_EXPIRED' | 'SESSION_REVOKED' | 'TOKEN_EXPIRED' | 'TOKEN_INVALID' | 'UNKNOWN_401' | 'UNKNOWN';
|
|
1
|
+
export type AuthFailureReason = 'REFRESH_INVALID' | 'REFRESH_REUSED' | 'REFRESH_EXPIRED' | 'SESSION_REVOKED' | 'DEVICE_KEY_MISMATCH' | 'TOKEN_EXPIRED' | 'TOKEN_INVALID' | 'UNKNOWN_401' | 'UNKNOWN';
|
|
2
2
|
/**
|
|
3
3
|
* The command that recovers this process from a re-auth latch.
|
|
4
4
|
*
|
|
@@ -11,8 +11,13 @@ export type AuthFailureReason = 'REFRESH_INVALID' | 'REFRESH_REUSED' | 'REFRESH_
|
|
|
11
11
|
* something the other did not describe (OSK-10224).
|
|
12
12
|
*
|
|
13
13
|
* The binary is whatever the consumer registered with
|
|
14
|
-
* `configureAuthCore({ binaryName })` — the binary the user actually typed
|
|
15
|
-
*
|
|
14
|
+
* `configureAuthCore({ binaryName })` — the binary the user actually typed.
|
|
15
|
+
*
|
|
16
|
+
* No flag. This used to append `--device` whenever stdout was not a TTY, which
|
|
17
|
+
* is always true for a daemon under launchd/systemd — and neither `skrrd login`
|
|
18
|
+
* nor `skrr login` has a `--device` option, so the one command the latch line
|
|
19
|
+
* named failed with `unknown option '--device'` (OSK-12056). Both logins already
|
|
20
|
+
* fall back to a paste-a-code arm when no browser can be opened.
|
|
16
21
|
*/
|
|
17
22
|
export declare function reauthCommand(): string;
|
|
18
23
|
export declare function formatReauthMessage(reason: AuthFailureReason | string): string;
|
|
@@ -17,6 +17,7 @@ const REASON_HUMAN = {
|
|
|
17
17
|
REFRESH_REUSED: 'Your session was ended for security reasons (token replay detected).',
|
|
18
18
|
REFRESH_EXPIRED: 'Your session has expired.',
|
|
19
19
|
SESSION_REVOKED: 'Your session was ended (you signed out elsewhere or your password changed).',
|
|
20
|
+
DEVICE_KEY_MISMATCH: "This computer's device key no longer matches the one skrr registered for it, so its session cannot be renewed.",
|
|
20
21
|
TOKEN_EXPIRED: 'Your access token has expired.',
|
|
21
22
|
TOKEN_INVALID: 'Your saved credentials are no longer valid.',
|
|
22
23
|
UNKNOWN_401: 'skrr needs you to sign in again.',
|
|
@@ -34,12 +35,16 @@ const REASON_HUMAN = {
|
|
|
34
35
|
* something the other did not describe (OSK-10224).
|
|
35
36
|
*
|
|
36
37
|
* The binary is whatever the consumer registered with
|
|
37
|
-
* `configureAuthCore({ binaryName })` — the binary the user actually typed
|
|
38
|
-
*
|
|
38
|
+
* `configureAuthCore({ binaryName })` — the binary the user actually typed.
|
|
39
|
+
*
|
|
40
|
+
* No flag. This used to append `--device` whenever stdout was not a TTY, which
|
|
41
|
+
* is always true for a daemon under launchd/systemd — and neither `skrrd login`
|
|
42
|
+
* nor `skrr login` has a `--device` option, so the one command the latch line
|
|
43
|
+
* named failed with `unknown option '--device'` (OSK-12056). Both logins already
|
|
44
|
+
* fall back to a paste-a-code arm when no browser can be opened.
|
|
39
45
|
*/
|
|
40
46
|
function reauthCommand() {
|
|
41
|
-
|
|
42
|
-
return process.stdout.isTTY ? `${bin} login` : `${bin} login --device`;
|
|
47
|
+
return `${(0, runtime_js_1.getAuthBinaryName)()} login`;
|
|
43
48
|
}
|
|
44
49
|
function formatReauthMessage(reason) {
|
|
45
50
|
const norm = REASON_HUMAN[reason] || REASON_HUMAN.UNKNOWN;
|
|
@@ -41,6 +41,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
41
41
|
const node_path_1 = __importDefault(require("node:path"));
|
|
42
42
|
const proper_lockfile_1 = __importDefault(require("proper-lockfile"));
|
|
43
43
|
const runtime_js_1 = require("./runtime.js");
|
|
44
|
+
const refreshClassification_js_1 = require("./refreshClassification.js");
|
|
44
45
|
const types_js_1 = require("./types.js");
|
|
45
46
|
/**
|
|
46
47
|
* P1-6: Reduced from 30 s to 5 s.
|
|
@@ -820,14 +821,12 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
|
|
|
820
821
|
catch {
|
|
821
822
|
/* ignore */
|
|
822
823
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
];
|
|
830
|
-
if (code && permanent.includes(code)) {
|
|
824
|
+
// The server's code, mapped through the one table the stateless classifier
|
|
825
|
+
// reads too — so a permanent refusal that is not spelled as a reason name
|
|
826
|
+
// (DEVICE_PROOF_THUMBPRINT_MISMATCH) latches instead of riding the
|
|
827
|
+
// unknown-401 budget forever (OSK-12056).
|
|
828
|
+
const code = (0, refreshClassification_js_1.permanentReasonForRefreshCode)(parsedBody.code);
|
|
829
|
+
if (code) {
|
|
831
830
|
// A KNOWN credential-death code is genuinely permanent — latch
|
|
832
831
|
// immediately, exactly as before. It is NOT routed through the
|
|
833
832
|
// unknown-401 budget.
|
|
@@ -857,7 +856,7 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
|
|
|
857
856
|
}
|
|
858
857
|
// Unknown-shape 401 — unknown code, missing code, or unparseable body.
|
|
859
858
|
// By construction this is NOT a daemon's actual refresh-family burn
|
|
860
|
-
// (those arrive as one of the
|
|
859
|
+
// (those arrive as one of the KNOWN codes above); it most likely
|
|
861
860
|
// came from infra OverSky does not fully control — a proxy/middleware
|
|
862
861
|
// regression, a clock-skew replay rejection (DEVICE_PROOF_REPLAYED),
|
|
863
862
|
// or a half-rolled-out server route.
|
|
@@ -36,6 +36,12 @@ export interface ClassifiedRefreshFailure {
|
|
|
36
36
|
failure: PermanentAuthFailure | TransientAuthFailure;
|
|
37
37
|
}
|
|
38
38
|
export type ClassifiedRefreshResult = ClassifiedRefreshSuccess | ClassifiedRefreshFailure;
|
|
39
|
+
/**
|
|
40
|
+
* The permanent reason a 401 refresh refusal's `code` names, or `null` when the
|
|
41
|
+
* code is missing or unrecognised (an unknown-shape 401). The ONE mapping both
|
|
42
|
+
* this classifier and the daemon's `refresh.ts` read.
|
|
43
|
+
*/
|
|
44
|
+
export declare function permanentReasonForRefreshCode(code: string | undefined | null): PermanentAuthReason | null;
|
|
39
45
|
/**
|
|
40
46
|
* Classify an HTTP response from /api/auth/refresh (or /api/daemons/token/refresh)
|
|
41
47
|
* into a discriminated union of success or failure.
|
|
@@ -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
|
|
@@ -159,3 +159,41 @@ export declare function verifyManifest(manifest: DaemonVersionManifest, keys?: r
|
|
|
159
159
|
* against the signed `artifact.sha256`, not from this metadata).
|
|
160
160
|
*/
|
|
161
161
|
export declare function getDeltaFor(artifact: PlatformArtifact, fromVersion: string): DeltaDescriptor | null;
|
|
162
|
+
/**
|
|
163
|
+
* Origins an artifact may be served from, beyond the feed base this run used.
|
|
164
|
+
*
|
|
165
|
+
* WHY A SET AND NOT THE ONE ORIGIN
|
|
166
|
+
* --------------------------------
|
|
167
|
+
* The pin used to require the artifact origin to EQUAL the feed base's
|
|
168
|
+
* origin. That is correct with one host and unsatisfiable with two, which the
|
|
169
|
+
* skrr rename creates:
|
|
170
|
+
*
|
|
171
|
+
* - a shipped daemon/CLI has `updates.oversky.ai` compiled in, so a manifest
|
|
172
|
+
* advertising skrr.ai artifacts is refused and it can never self-update
|
|
173
|
+
* again — including to the build that would have fixed it;
|
|
174
|
+
* - a new build shipping `updates.skrr.ai` refuses the SAME manifest for the
|
|
175
|
+
* mirror-image reason.
|
|
176
|
+
*
|
|
177
|
+
* One manifest of absolute URLs therefore cannot serve both, and the update
|
|
178
|
+
* path is the one thing that must not break: a client that cannot update
|
|
179
|
+
* cannot be repaired remotely.
|
|
180
|
+
*
|
|
181
|
+
* Widening to a small, closed set of origins we publish keeps the property the
|
|
182
|
+
* pin exists for: with keys pinned the ed25519 signature already binds the
|
|
183
|
+
* URL, and this is defence-in-depth for when verification is disabled —
|
|
184
|
+
* stopping a manifest-integrity failure from redirecting a download to an
|
|
185
|
+
* ARBITRARY origin serving a hash-matching payload. An attacker gains nothing
|
|
186
|
+
* from a second origin that is also ours.
|
|
187
|
+
*
|
|
188
|
+
* Both entries are removed down to one when no shipped client uses the old
|
|
189
|
+
* host, per the add -> verify -> remove ordering.
|
|
190
|
+
*/
|
|
191
|
+
export declare const TRUSTED_FEED_ORIGINS: readonly string[];
|
|
192
|
+
/**
|
|
193
|
+
* True when `candidate` may serve an artifact for a feed rooted at `feedBase`.
|
|
194
|
+
*
|
|
195
|
+
* The feed base's own origin always passes, so an override
|
|
196
|
+
* (OVERSKY_DAEMON_UPDATE_FEED_BASE, a canary, a test server) keeps working
|
|
197
|
+
* without being listed here.
|
|
198
|
+
*/
|
|
199
|
+
export declare function isTrustedArtifactOrigin(candidate: string, feedBase: string): boolean;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MAX_SIGS = void 0;
|
|
3
|
+
exports.TRUSTED_FEED_ORIGINS = exports.MAX_SIGS = void 0;
|
|
4
4
|
exports.buildArtifactMessage = buildArtifactMessage;
|
|
5
5
|
exports.buildManifestMessage = buildManifestMessage;
|
|
6
6
|
exports.verifyArtifact = verifyArtifact;
|
|
7
7
|
exports.verifyManifest = verifyManifest;
|
|
8
8
|
exports.getDeltaFor = getDeltaFor;
|
|
9
|
+
exports.isTrustedArtifactOrigin = isTrustedArtifactOrigin;
|
|
9
10
|
/**
|
|
10
11
|
* release-manifest.ts — signed daemon release manifest: types, canonical
|
|
11
12
|
* signature messages, and offline ed25519 verification.
|
|
@@ -233,3 +234,67 @@ function getDeltaFor(artifact, fromVersion) {
|
|
|
233
234
|
}
|
|
234
235
|
return d;
|
|
235
236
|
}
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Trusted artifact origins
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
/**
|
|
241
|
+
* Origins an artifact may be served from, beyond the feed base this run used.
|
|
242
|
+
*
|
|
243
|
+
* WHY A SET AND NOT THE ONE ORIGIN
|
|
244
|
+
* --------------------------------
|
|
245
|
+
* The pin used to require the artifact origin to EQUAL the feed base's
|
|
246
|
+
* origin. That is correct with one host and unsatisfiable with two, which the
|
|
247
|
+
* skrr rename creates:
|
|
248
|
+
*
|
|
249
|
+
* - a shipped daemon/CLI has `updates.oversky.ai` compiled in, so a manifest
|
|
250
|
+
* advertising skrr.ai artifacts is refused and it can never self-update
|
|
251
|
+
* again — including to the build that would have fixed it;
|
|
252
|
+
* - a new build shipping `updates.skrr.ai` refuses the SAME manifest for the
|
|
253
|
+
* mirror-image reason.
|
|
254
|
+
*
|
|
255
|
+
* One manifest of absolute URLs therefore cannot serve both, and the update
|
|
256
|
+
* path is the one thing that must not break: a client that cannot update
|
|
257
|
+
* cannot be repaired remotely.
|
|
258
|
+
*
|
|
259
|
+
* Widening to a small, closed set of origins we publish keeps the property the
|
|
260
|
+
* pin exists for: with keys pinned the ed25519 signature already binds the
|
|
261
|
+
* URL, and this is defence-in-depth for when verification is disabled —
|
|
262
|
+
* stopping a manifest-integrity failure from redirecting a download to an
|
|
263
|
+
* ARBITRARY origin serving a hash-matching payload. An attacker gains nothing
|
|
264
|
+
* from a second origin that is also ours.
|
|
265
|
+
*
|
|
266
|
+
* Both entries are removed down to one when no shipped client uses the old
|
|
267
|
+
* host, per the add -> verify -> remove ordering.
|
|
268
|
+
*/
|
|
269
|
+
exports.TRUSTED_FEED_ORIGINS = [
|
|
270
|
+
'https://updates.oversky.ai',
|
|
271
|
+
'https://updates.skrr.ai',
|
|
272
|
+
];
|
|
273
|
+
/**
|
|
274
|
+
* True when `candidate` may serve an artifact for a feed rooted at `feedBase`.
|
|
275
|
+
*
|
|
276
|
+
* The feed base's own origin always passes, so an override
|
|
277
|
+
* (OVERSKY_DAEMON_UPDATE_FEED_BASE, a canary, a test server) keeps working
|
|
278
|
+
* without being listed here.
|
|
279
|
+
*/
|
|
280
|
+
function isTrustedArtifactOrigin(candidate, feedBase) {
|
|
281
|
+
let feedOrigin;
|
|
282
|
+
try {
|
|
283
|
+
feedOrigin = new URL(feedBase).origin;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
// Callers hand over either a bare origin (the daemon's self-updater) or a
|
|
289
|
+
// full artifact URL (the CLI installer) — both spell the same check.
|
|
290
|
+
let candidateOrigin = candidate;
|
|
291
|
+
try {
|
|
292
|
+
candidateOrigin = new URL(candidate).origin;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
/* not a URL — compare raw; it will not match anything */
|
|
296
|
+
}
|
|
297
|
+
if (candidateOrigin === feedOrigin)
|
|
298
|
+
return true;
|
|
299
|
+
return exports.TRUSTED_FEED_ORIGINS.includes(candidateOrigin);
|
|
300
|
+
}
|
|
@@ -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;
|