@skrr-ai/auth-core 0.1.2
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 +21 -0
- package/dist/cjs/aead.d.ts +24 -0
- package/dist/cjs/aead.js +65 -0
- package/dist/cjs/authExpiredDetector.d.ts +65 -0
- package/dist/cjs/authExpiredDetector.js +89 -0
- package/dist/cjs/authHelper.d.ts +51 -0
- package/dist/cjs/authHelper.js +321 -0
- package/dist/cjs/credentialEnvelope.d.ts +85 -0
- package/dist/cjs/credentialEnvelope.js +230 -0
- package/dist/cjs/credentialEnvelopeBridge.d.ts +116 -0
- package/dist/cjs/credentialEnvelopeBridge.js +637 -0
- package/dist/cjs/credentialResolver.d.ts +278 -0
- package/dist/cjs/credentialResolver.js +423 -0
- package/dist/cjs/daemonPair.d.ts +172 -0
- package/dist/cjs/daemonPair.js +495 -0
- package/dist/cjs/daemonScopes.d.ts +74 -0
- package/dist/cjs/daemonScopes.js +84 -0
- package/dist/cjs/deviceIdentityBridge.d.ts +113 -0
- package/dist/cjs/deviceIdentityBridge.js +591 -0
- package/dist/cjs/deviceKey.d.ts +167 -0
- package/dist/cjs/deviceKey.js +332 -0
- package/dist/cjs/fdToken.d.ts +17 -0
- package/dist/cjs/fdToken.js +238 -0
- package/dist/cjs/harnessTrust.d.ts +72 -0
- package/dist/cjs/harnessTrust.js +103 -0
- package/dist/cjs/headers-helper.d.ts +25 -0
- package/dist/cjs/headers-helper.js +323 -0
- package/dist/cjs/index.d.ts +37 -0
- package/dist/cjs/index.js +305 -0
- package/dist/cjs/jwtUtils.d.ts +35 -0
- package/dist/cjs/jwtUtils.js +61 -0
- package/dist/cjs/kek/index.d.ts +89 -0
- package/dist/cjs/kek/index.js +208 -0
- package/dist/cjs/kek/linux.d.ts +92 -0
- package/dist/cjs/kek/linux.js +596 -0
- package/dist/cjs/kek/macos.d.ts +112 -0
- package/dist/cjs/kek/macos.js +599 -0
- package/dist/cjs/kek/types.d.ts +111 -0
- package/dist/cjs/kek/types.js +43 -0
- package/dist/cjs/kek/windows.d.ts +71 -0
- package/dist/cjs/kek/windows.js +350 -0
- package/dist/cjs/kek/zeroize-registry.d.ts +44 -0
- package/dist/cjs/kek/zeroize-registry.js +63 -0
- package/dist/cjs/legacyStatePreflight.d.ts +67 -0
- package/dist/cjs/legacyStatePreflight.js +78 -0
- package/dist/cjs/localIdentity.d.ts +54 -0
- package/dist/cjs/localIdentity.js +57 -0
- package/dist/cjs/loginLocalhost.d.ts +115 -0
- package/dist/cjs/loginLocalhost.js +368 -0
- package/dist/cjs/loginWithLocalhost.d.ts +131 -0
- package/dist/cjs/loginWithLocalhost.js +359 -0
- package/dist/cjs/machineId.d.ts +35 -0
- package/dist/cjs/machineId.js +239 -0
- package/dist/cjs/messages.d.ts +9 -0
- package/dist/cjs/messages.js +44 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/pkce.d.ts +62 -0
- package/dist/cjs/pkce.js +158 -0
- package/dist/cjs/recoveryCode.d.ts +61 -0
- package/dist/cjs/recoveryCode.js +213 -0
- package/dist/cjs/refresh.d.ts +215 -0
- package/dist/cjs/refresh.js +877 -0
- package/dist/cjs/refreshClassification.d.ts +53 -0
- package/dist/cjs/refreshClassification.js +105 -0
- package/dist/cjs/refreshScheduler.d.ts +53 -0
- package/dist/cjs/refreshScheduler.js +332 -0
- package/dist/cjs/runtime.d.ts +149 -0
- package/dist/cjs/runtime.js +136 -0
- package/dist/cjs/spawnEnv.d.ts +76 -0
- package/dist/cjs/spawnEnv.js +153 -0
- package/dist/cjs/ttlParser.d.ts +54 -0
- package/dist/cjs/ttlParser.js +87 -0
- package/dist/cjs/types.d.ts +91 -0
- package/dist/cjs/types.js +26 -0
- package/dist/esm/aead.d.ts +24 -0
- package/dist/esm/aead.js +57 -0
- package/dist/esm/authExpiredDetector.d.ts +65 -0
- package/dist/esm/authExpiredDetector.js +85 -0
- package/dist/esm/authHelper.d.ts +51 -0
- package/dist/esm/authHelper.js +311 -0
- package/dist/esm/credentialEnvelope.d.ts +85 -0
- package/dist/esm/credentialEnvelope.js +213 -0
- package/dist/esm/credentialEnvelopeBridge.d.ts +116 -0
- package/dist/esm/credentialEnvelopeBridge.js +620 -0
- package/dist/esm/credentialResolver.d.ts +278 -0
- package/dist/esm/credentialResolver.js +414 -0
- package/dist/esm/daemonPair.d.ts +172 -0
- package/dist/esm/daemonPair.js +476 -0
- package/dist/esm/daemonScopes.d.ts +74 -0
- package/dist/esm/daemonScopes.js +80 -0
- package/dist/esm/deviceIdentityBridge.d.ts +113 -0
- package/dist/esm/deviceIdentityBridge.js +572 -0
- package/dist/esm/deviceKey.d.ts +167 -0
- package/dist/esm/deviceKey.js +320 -0
- package/dist/esm/fdToken.d.ts +17 -0
- package/dist/esm/fdToken.js +231 -0
- package/dist/esm/harnessTrust.d.ts +72 -0
- package/dist/esm/harnessTrust.js +97 -0
- package/dist/esm/headers-helper.d.ts +25 -0
- package/dist/esm/headers-helper.js +316 -0
- package/dist/esm/index.d.ts +37 -0
- package/dist/esm/index.js +144 -0
- package/dist/esm/jwtUtils.d.ts +35 -0
- package/dist/esm/jwtUtils.js +57 -0
- package/dist/esm/kek/index.d.ts +89 -0
- package/dist/esm/kek/index.js +162 -0
- package/dist/esm/kek/linux.d.ts +92 -0
- package/dist/esm/kek/linux.js +585 -0
- package/dist/esm/kek/macos.d.ts +112 -0
- package/dist/esm/kek/macos.js +586 -0
- package/dist/esm/kek/types.d.ts +111 -0
- package/dist/esm/kek/types.js +39 -0
- package/dist/esm/kek/windows.d.ts +71 -0
- package/dist/esm/kek/windows.js +344 -0
- package/dist/esm/kek/zeroize-registry.d.ts +44 -0
- package/dist/esm/kek/zeroize-registry.js +58 -0
- package/dist/esm/legacyStatePreflight.d.ts +67 -0
- package/dist/esm/legacyStatePreflight.js +71 -0
- package/dist/esm/localIdentity.d.ts +54 -0
- package/dist/esm/localIdentity.js +54 -0
- package/dist/esm/loginLocalhost.d.ts +115 -0
- package/dist/esm/loginLocalhost.js +355 -0
- package/dist/esm/loginWithLocalhost.d.ts +131 -0
- package/dist/esm/loginWithLocalhost.js +353 -0
- package/dist/esm/machineId.d.ts +35 -0
- package/dist/esm/machineId.js +231 -0
- package/dist/esm/messages.d.ts +9 -0
- package/dist/esm/messages.js +40 -0
- package/dist/esm/pkce.d.ts +62 -0
- package/dist/esm/pkce.js +148 -0
- package/dist/esm/recoveryCode.d.ts +61 -0
- package/dist/esm/recoveryCode.js +207 -0
- package/dist/esm/refresh.d.ts +215 -0
- package/dist/esm/refresh.js +863 -0
- package/dist/esm/refreshClassification.d.ts +53 -0
- package/dist/esm/refreshClassification.js +106 -0
- package/dist/esm/refreshScheduler.d.ts +53 -0
- package/dist/esm/refreshScheduler.js +329 -0
- package/dist/esm/runtime.d.ts +149 -0
- package/dist/esm/runtime.js +121 -0
- package/dist/esm/spawnEnv.d.ts +76 -0
- package/dist/esm/spawnEnv.js +149 -0
- package/dist/esm/ttlParser.d.ts +54 -0
- package/dist/esm/ttlParser.js +83 -0
- package/dist/esm/types.d.ts +91 -0
- package/dist/esm/types.js +21 -0
- package/package.json +82 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fdToken.ts — one-shot credential handoff over an inherited pipe fd.
|
|
3
|
+
*
|
|
4
|
+
* When Electron's `DaemonSupervisor` (or any parent process) spawns a
|
|
5
|
+
* foreground daemon, it can stream the access (and optionally refresh)
|
|
6
|
+
* token over an inherited pipe fd instead of writing the token to disk and
|
|
7
|
+
* waiting for the daemon to find it. This eliminates:
|
|
8
|
+
* - The Keychain-propagation race on macOS (write → security DB index
|
|
9
|
+
* update → daemon read) that bites cold-start foreground spawns.
|
|
10
|
+
* - The `auth.json` filesystem mtime-watch race on non-darwin platforms.
|
|
11
|
+
*
|
|
12
|
+
* Wire protocol — parent writes a single JSON object (newline tolerated
|
|
13
|
+
* but not required) and closes the write side so the child sees EOF:
|
|
14
|
+
*
|
|
15
|
+
* { "accessToken": "...",
|
|
16
|
+
* "accessExpiresAt"?: 1234567890000,
|
|
17
|
+
* "refreshToken"?: "...",
|
|
18
|
+
* "refreshExpiresAt"?: 1234567890000 }
|
|
19
|
+
*
|
|
20
|
+
* The child reads once, parses, and closes the fd — regardless of outcome.
|
|
21
|
+
*
|
|
22
|
+
* LaunchAgent-spawned daemons (different process tree from Electron) do NOT
|
|
23
|
+
* get this handoff because launchd/systemd/schtasks don't propagate pipe
|
|
24
|
+
* fds across the boundary. Those keep using the keychain / file backend.
|
|
25
|
+
*
|
|
26
|
+
* Safety invariants:
|
|
27
|
+
* - `OVERSKY_AUTH_FD` absent → null (caller falls back).
|
|
28
|
+
* - fd not parseable / not readable → null.
|
|
29
|
+
* - Read exceeds 64 KB → bail (real payloads ~1 KB; defensive cap).
|
|
30
|
+
* - Read times out after 2 s (parent crashed mid-handoff) → null.
|
|
31
|
+
* - Malformed JSON or missing accessToken → null.
|
|
32
|
+
* - Always close the fd after the read, regardless of outcome.
|
|
33
|
+
* - After the first successful consume we SCRUB the env var so a later
|
|
34
|
+
* getToken() that misses any cache doesn't try to re-read a fd that
|
|
35
|
+
* has already been closed.
|
|
36
|
+
*/
|
|
37
|
+
import fs from 'node:fs';
|
|
38
|
+
import { getAuthKillSwitches } from './runtime.js';
|
|
39
|
+
const MAX_PAYLOAD_BYTES = 64 * 1024;
|
|
40
|
+
const READ_TIMEOUT_MS = 2_000;
|
|
41
|
+
const FD_ENV_VAR = 'OVERSKY_AUTH_FD';
|
|
42
|
+
/**
|
|
43
|
+
* Test seam — lets unit tests stub the actual fd read without opening a
|
|
44
|
+
* real pipe. Default: null → real bounded-async `createReadStream(fd)` path.
|
|
45
|
+
*/
|
|
46
|
+
let _fdReader = null;
|
|
47
|
+
/** @internal test-only — replace the fd reader. Pass null to restore. */
|
|
48
|
+
export function __setFdReaderForTest(fn) {
|
|
49
|
+
_fdReader = fn;
|
|
50
|
+
}
|
|
51
|
+
function parseFd(envValue) {
|
|
52
|
+
if (!envValue)
|
|
53
|
+
return null;
|
|
54
|
+
const fd = Number.parseInt(envValue, 10);
|
|
55
|
+
if (!Number.isFinite(fd) || fd < 0)
|
|
56
|
+
return null;
|
|
57
|
+
return fd;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Bounded async read of the pipe fd with a 2 s deadline and a 64 KB cap.
|
|
61
|
+
*
|
|
62
|
+
* Uses `fs.createReadStream(fd)` with an `AbortController` so a hung parent
|
|
63
|
+
* writer (never closes its write side, never signals EOF) cannot stall the
|
|
64
|
+
* daemon. On timeout we abort the stream and return null. The fd is closed
|
|
65
|
+
* exactly once — either by the stream itself on 'close', or explicitly by
|
|
66
|
+
* `closeSync` on the error/timeout paths.
|
|
67
|
+
*/
|
|
68
|
+
function readFdBounded(fd) {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
const controller = new AbortController();
|
|
71
|
+
let settled = false;
|
|
72
|
+
let bytes = 0;
|
|
73
|
+
const chunks = [];
|
|
74
|
+
let streamClosedFd = false;
|
|
75
|
+
const finish = (result) => {
|
|
76
|
+
if (settled)
|
|
77
|
+
return;
|
|
78
|
+
settled = true;
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
if (!streamClosedFd) {
|
|
81
|
+
try {
|
|
82
|
+
fs.closeSync(fd);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
/* fd may already be closed by the stream — ignore */
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
resolve(result);
|
|
89
|
+
};
|
|
90
|
+
const timer = setTimeout(() => {
|
|
91
|
+
try {
|
|
92
|
+
controller.abort();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
/* ignore */
|
|
96
|
+
}
|
|
97
|
+
console.warn(`[auth/fdToken] read fd ${fd} timed out after ${READ_TIMEOUT_MS} ms — parent writer may have crashed`);
|
|
98
|
+
finish(null);
|
|
99
|
+
}, READ_TIMEOUT_MS);
|
|
100
|
+
if (typeof timer.unref === 'function')
|
|
101
|
+
timer.unref();
|
|
102
|
+
let stream;
|
|
103
|
+
try {
|
|
104
|
+
stream = fs.createReadStream('', {
|
|
105
|
+
fd,
|
|
106
|
+
autoClose: true,
|
|
107
|
+
signal: controller.signal,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
console.warn(`[auth/fdToken] failed to open read stream on fd ${fd}: ${err instanceof Error ? err.message : String(err)}`);
|
|
112
|
+
finish(null);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
stream.on('data', (chunk) => {
|
|
116
|
+
const buf = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk;
|
|
117
|
+
bytes += buf.length;
|
|
118
|
+
if (bytes > MAX_PAYLOAD_BYTES) {
|
|
119
|
+
try {
|
|
120
|
+
controller.abort();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
/* ignore */
|
|
124
|
+
}
|
|
125
|
+
console.warn(`[auth/fdToken] payload size exceeds ${MAX_PAYLOAD_BYTES} — discarding`);
|
|
126
|
+
finish(null);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
chunks.push(buf);
|
|
130
|
+
});
|
|
131
|
+
stream.on('error', (err) => {
|
|
132
|
+
if (err && (err.name === 'AbortError' || err.code === 'ABORT_ERR')) {
|
|
133
|
+
finish(null);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
console.warn(`[auth/fdToken] failed to read fd ${fd}: ${err instanceof Error ? err.message : String(err)}`);
|
|
137
|
+
finish(null);
|
|
138
|
+
});
|
|
139
|
+
stream.on('close', () => {
|
|
140
|
+
streamClosedFd = true;
|
|
141
|
+
if (settled)
|
|
142
|
+
return;
|
|
143
|
+
if (bytes === 0) {
|
|
144
|
+
finish(null);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const raw = Buffer.concat(chunks, bytes).toString('utf8');
|
|
148
|
+
finish(raw);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Attempt a one-shot read of the handoff fd. Returns the parsed payload on
|
|
154
|
+
* success, or null on any failure (missing env var, bad fd, bad JSON, etc.).
|
|
155
|
+
*
|
|
156
|
+
* Side effect: on the first successful read the env var is scrubbed so a
|
|
157
|
+
* later caller that misses every in-process cache doesn't retry the now-
|
|
158
|
+
* closed fd.
|
|
159
|
+
*/
|
|
160
|
+
export async function readAuthFromFd() {
|
|
161
|
+
// Kill switch: ignore the fd handoff entirely and fall back to the
|
|
162
|
+
// keychain / file backend (pre-Phase-4 path).
|
|
163
|
+
if (getAuthKillSwitches().fdTokenHandoff)
|
|
164
|
+
return null;
|
|
165
|
+
const fd = parseFd(process.env[FD_ENV_VAR]);
|
|
166
|
+
if (fd === null)
|
|
167
|
+
return null;
|
|
168
|
+
let raw = null;
|
|
169
|
+
try {
|
|
170
|
+
if (_fdReader) {
|
|
171
|
+
const ret = _fdReader(fd);
|
|
172
|
+
raw =
|
|
173
|
+
typeof ret?.then === 'function'
|
|
174
|
+
? await ret
|
|
175
|
+
: ret;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
raw = await readFdBounded(fd);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
console.warn(`[auth/fdToken] failed to read fd ${fd}: ${err instanceof Error ? err.message : String(err)}`);
|
|
183
|
+
raw = null;
|
|
184
|
+
if (_fdReader) {
|
|
185
|
+
try {
|
|
186
|
+
fs.closeSync(fd);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
/* ignore */
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (raw === null)
|
|
194
|
+
return null;
|
|
195
|
+
if (raw.length > MAX_PAYLOAD_BYTES) {
|
|
196
|
+
console.warn(`[auth/fdToken] payload size ${raw.length} exceeds ${MAX_PAYLOAD_BYTES} — discarding`);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
let parsed;
|
|
200
|
+
try {
|
|
201
|
+
parsed = JSON.parse(raw.trim());
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
if (!parsed || typeof parsed !== 'object')
|
|
207
|
+
return null;
|
|
208
|
+
const obj = parsed;
|
|
209
|
+
if (typeof obj.accessToken !== 'string' || obj.accessToken.length === 0) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const out = { accessToken: obj.accessToken };
|
|
213
|
+
if (typeof obj.accessExpiresAt === 'number' && Number.isFinite(obj.accessExpiresAt)) {
|
|
214
|
+
out.accessExpiresAt = obj.accessExpiresAt;
|
|
215
|
+
}
|
|
216
|
+
if (typeof obj.refreshToken === 'string' && obj.refreshToken.length > 0) {
|
|
217
|
+
out.refreshToken = obj.refreshToken;
|
|
218
|
+
}
|
|
219
|
+
if (typeof obj.refreshExpiresAt === 'number' && Number.isFinite(obj.refreshExpiresAt)) {
|
|
220
|
+
out.refreshExpiresAt = obj.refreshExpiresAt;
|
|
221
|
+
}
|
|
222
|
+
// Scrub the env var so a later getToken() that misses caches doesn't try
|
|
223
|
+
// to re-read the now-closed fd.
|
|
224
|
+
try {
|
|
225
|
+
delete process.env[FD_ENV_VAR];
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Some hardened runtimes make process.env partially immutable. Best-effort.
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OSK-3897 — harness trust TIERS, shared by the daemon and the CLI.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this lives in auth-core ────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* `sky code doctor` must tell a user what tier their harness is and what that
|
|
7
|
+
* means for credentials and permission prompts. The daemon is the authority for
|
|
8
|
+
* that answer, but the CLI cannot import `daemon/src` — separate packages, and
|
|
9
|
+
* the daemon already depends on the CLI, so the reverse edge would be a cycle.
|
|
10
|
+
*
|
|
11
|
+
* `auth-core` is the one package BOTH already resolve: it is a daemon dependency
|
|
12
|
+
* and it is copied verbatim into the published CLI by
|
|
13
|
+
* `cli/scripts/vendor-monorepo-deps.js`, which keeps its `exports` map intact and
|
|
14
|
+
* preserves a single physical instance. So sharing through here changes nothing
|
|
15
|
+
* in the build or the release pipeline.
|
|
16
|
+
*
|
|
17
|
+
* Before this, the CLI carried a MIRROR of the table with a static test that
|
|
18
|
+
* parsed the daemon's source to catch drift. That guard worked — and it was still
|
|
19
|
+
* only a guard. The regression it existed to catch had already shipped once: the
|
|
20
|
+
* doctor described Tier 2 behaviour for a day after the owner decision of
|
|
21
|
+
* 2026-08-11 moved `sky-code` to Tier 1, and a unit test asserting the stale
|
|
22
|
+
* string made the wrong claim look load-bearing.
|
|
23
|
+
*
|
|
24
|
+
* ── What deliberately did NOT move ────────────────────────────────────────
|
|
25
|
+
*
|
|
26
|
+
* The PROVENANCE half stays in the daemon: `OVERSKY_DISTRIBUTED_HARNESSES`,
|
|
27
|
+
* `PROVENANCE_ELEVATABLE_TIERS`, `ProvenanceEvidence`, `effectiveHarnessTier`,
|
|
28
|
+
* `credentialTierForSpawn`, and the signature verification behind them.
|
|
29
|
+
*
|
|
30
|
+
* That split is the point. A tier is a PUBLISHED FACT — safe for any surface to
|
|
31
|
+
* read and state. Provenance is EVIDENCE ABOUT BYTES ON DISK, which only the
|
|
32
|
+
* process that spawns the binary can gather. Moving it here would let the CLI
|
|
33
|
+
* appear to speak about binary identity it has no way to verify, which is a worse
|
|
34
|
+
* failure than the drift this file fixes: a confident wrong answer beats an
|
|
35
|
+
* honestly stale one for causing harm.
|
|
36
|
+
*/
|
|
37
|
+
/** A harness's trust tier. 3 is the safe default for anything unrecognised. */
|
|
38
|
+
export type HarnessTrustTier = 1 | 2 | 3;
|
|
39
|
+
/**
|
|
40
|
+
* Trust tier by backend name.
|
|
41
|
+
*
|
|
42
|
+
* Tier 1 — first-party / audited. Tier 2 — reviewed open-source community
|
|
43
|
+
* harness. Tier 3 has NO entries on purpose: it is what any name absent from this
|
|
44
|
+
* map resolves to, so adding trust is always a deliberate, reviewable edit and
|
|
45
|
+
* never an omission.
|
|
46
|
+
*
|
|
47
|
+
* `sky-code` is Tier 1 by OWNER DECISION (2026-08-11). The full reasoning, and
|
|
48
|
+
* the measured cost of the Tier 2 posture it replaced, is recorded in
|
|
49
|
+
* `daemon/src/harness-trust.ts` beside the gates that read it — the argument
|
|
50
|
+
* belongs where the enforcement is, not in a shared vocabulary module.
|
|
51
|
+
*/
|
|
52
|
+
export declare const HARNESS_TIERS: Readonly<Record<string, HarnessTrustTier>>;
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the trust tier for a backend name.
|
|
55
|
+
*
|
|
56
|
+
* Returns `3` for any name that is not first-party or reviewed-community —
|
|
57
|
+
* including empty, whitespace-only, and non-string inputs. Unknown == untrusted.
|
|
58
|
+
*/
|
|
59
|
+
export declare function getHarnessTier(backendName: string): HarnessTrustTier;
|
|
60
|
+
/** What a tier permits in terms of credential DELIVERY. */
|
|
61
|
+
export type CredentialDeliveryPolicy = 'direct-or-gateway' | 'gateway-only' | 'byo-only';
|
|
62
|
+
export declare function credentialPolicyForTier(tier: HarnessTrustTier): CredentialDeliveryPolicy;
|
|
63
|
+
/** How a credential reaches a spawned harness. */
|
|
64
|
+
export type CredentialMode = 'direct-key' | 'gateway' | 'env-map' | 'none';
|
|
65
|
+
/**
|
|
66
|
+
* May an operator-configured `auto` permission mode SURVIVE for this tier?
|
|
67
|
+
*
|
|
68
|
+
* This never approves anything. It answers only whether a mode the operator
|
|
69
|
+
* already chose is kept or downgraded to per-call adjudication — which is the
|
|
70
|
+
* distinction most often lost when someone reads "Tier 1" as "pre-approved".
|
|
71
|
+
*/
|
|
72
|
+
export declare function tierPermitsConfiguredAutoMode(tier: HarnessTrustTier): boolean;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OSK-3897 — harness trust TIERS, shared by the daemon and the CLI.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this lives in auth-core ────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* `sky code doctor` must tell a user what tier their harness is and what that
|
|
7
|
+
* means for credentials and permission prompts. The daemon is the authority for
|
|
8
|
+
* that answer, but the CLI cannot import `daemon/src` — separate packages, and
|
|
9
|
+
* the daemon already depends on the CLI, so the reverse edge would be a cycle.
|
|
10
|
+
*
|
|
11
|
+
* `auth-core` is the one package BOTH already resolve: it is a daemon dependency
|
|
12
|
+
* and it is copied verbatim into the published CLI by
|
|
13
|
+
* `cli/scripts/vendor-monorepo-deps.js`, which keeps its `exports` map intact and
|
|
14
|
+
* preserves a single physical instance. So sharing through here changes nothing
|
|
15
|
+
* in the build or the release pipeline.
|
|
16
|
+
*
|
|
17
|
+
* Before this, the CLI carried a MIRROR of the table with a static test that
|
|
18
|
+
* parsed the daemon's source to catch drift. That guard worked — and it was still
|
|
19
|
+
* only a guard. The regression it existed to catch had already shipped once: the
|
|
20
|
+
* doctor described Tier 2 behaviour for a day after the owner decision of
|
|
21
|
+
* 2026-08-11 moved `sky-code` to Tier 1, and a unit test asserting the stale
|
|
22
|
+
* string made the wrong claim look load-bearing.
|
|
23
|
+
*
|
|
24
|
+
* ── What deliberately did NOT move ────────────────────────────────────────
|
|
25
|
+
*
|
|
26
|
+
* The PROVENANCE half stays in the daemon: `OVERSKY_DISTRIBUTED_HARNESSES`,
|
|
27
|
+
* `PROVENANCE_ELEVATABLE_TIERS`, `ProvenanceEvidence`, `effectiveHarnessTier`,
|
|
28
|
+
* `credentialTierForSpawn`, and the signature verification behind them.
|
|
29
|
+
*
|
|
30
|
+
* That split is the point. A tier is a PUBLISHED FACT — safe for any surface to
|
|
31
|
+
* read and state. Provenance is EVIDENCE ABOUT BYTES ON DISK, which only the
|
|
32
|
+
* process that spawns the binary can gather. Moving it here would let the CLI
|
|
33
|
+
* appear to speak about binary identity it has no way to verify, which is a worse
|
|
34
|
+
* failure than the drift this file fixes: a confident wrong answer beats an
|
|
35
|
+
* honestly stale one for causing harm.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Trust tier by backend name.
|
|
39
|
+
*
|
|
40
|
+
* Tier 1 — first-party / audited. Tier 2 — reviewed open-source community
|
|
41
|
+
* harness. Tier 3 has NO entries on purpose: it is what any name absent from this
|
|
42
|
+
* map resolves to, so adding trust is always a deliberate, reviewable edit and
|
|
43
|
+
* never an omission.
|
|
44
|
+
*
|
|
45
|
+
* `sky-code` is Tier 1 by OWNER DECISION (2026-08-11). The full reasoning, and
|
|
46
|
+
* the measured cost of the Tier 2 posture it replaced, is recorded in
|
|
47
|
+
* `daemon/src/harness-trust.ts` beside the gates that read it — the argument
|
|
48
|
+
* belongs where the enforcement is, not in a shared vocabulary module.
|
|
49
|
+
*/
|
|
50
|
+
export const HARNESS_TIERS = Object.freeze({
|
|
51
|
+
// --- Tier 1: first-party / audited ---
|
|
52
|
+
claude: 1,
|
|
53
|
+
codex: 1,
|
|
54
|
+
opencode: 1,
|
|
55
|
+
'opencode-acp': 1,
|
|
56
|
+
gemini: 1,
|
|
57
|
+
'gemini-acp': 1,
|
|
58
|
+
'sky-code': 1,
|
|
59
|
+
// --- Tier 2: reviewed open-source community harnesses ---
|
|
60
|
+
goose: 2,
|
|
61
|
+
aider: 2,
|
|
62
|
+
cline: 2,
|
|
63
|
+
openhands: 2,
|
|
64
|
+
kimi: 2,
|
|
65
|
+
hermes: 2,
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the trust tier for a backend name.
|
|
69
|
+
*
|
|
70
|
+
* Returns `3` for any name that is not first-party or reviewed-community —
|
|
71
|
+
* including empty, whitespace-only, and non-string inputs. Unknown == untrusted.
|
|
72
|
+
*/
|
|
73
|
+
export function getHarnessTier(backendName) {
|
|
74
|
+
if (!backendName || typeof backendName !== 'string')
|
|
75
|
+
return 3;
|
|
76
|
+
const key = backendName.trim();
|
|
77
|
+
if (!key)
|
|
78
|
+
return 3;
|
|
79
|
+
return HARNESS_TIERS[key] ?? 3;
|
|
80
|
+
}
|
|
81
|
+
export function credentialPolicyForTier(tier) {
|
|
82
|
+
if (tier === 1)
|
|
83
|
+
return 'direct-or-gateway';
|
|
84
|
+
if (tier === 2)
|
|
85
|
+
return 'gateway-only';
|
|
86
|
+
return 'byo-only';
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* May an operator-configured `auto` permission mode SURVIVE for this tier?
|
|
90
|
+
*
|
|
91
|
+
* This never approves anything. It answers only whether a mode the operator
|
|
92
|
+
* already chose is kept or downgraded to per-call adjudication — which is the
|
|
93
|
+
* distinction most often lost when someone reads "Tier 1" as "pre-approved".
|
|
94
|
+
*/
|
|
95
|
+
export function tierPermitsConfiguredAutoMode(tier) {
|
|
96
|
+
return tier === 1;
|
|
97
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invalidate the headers-helper cache.
|
|
3
|
+
* - `invalidateHeadersHelperCache('foo')` clears only the `foo` server.
|
|
4
|
+
* - `invalidateHeadersHelperCache()` clears all servers.
|
|
5
|
+
*/
|
|
6
|
+
export declare function invalidateHeadersHelperCache(serverName?: string): void;
|
|
7
|
+
export interface RunHeadersHelperOptions {
|
|
8
|
+
serverName: string;
|
|
9
|
+
serverUrl: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Invoke the headers helper for the named MCP server and return the
|
|
13
|
+
* resulting headers. Results are cached per `serverName` for 5 minutes.
|
|
14
|
+
*
|
|
15
|
+
* On cold-cache failure the function throws (fail closed — better to
|
|
16
|
+
* surface the misconfiguration than to silently send unauthenticated
|
|
17
|
+
* requests to an MCP server expecting credentials).
|
|
18
|
+
*
|
|
19
|
+
* On warm-cache helper failure the stale cached value is returned with
|
|
20
|
+
* a warning so ongoing requests are not disrupted during a transient
|
|
21
|
+
* helper outage.
|
|
22
|
+
*
|
|
23
|
+
* Returns `{}` (empty object) when no helper is configured.
|
|
24
|
+
*/
|
|
25
|
+
export declare function runHeadersHelper(opts: RunHeadersHelperOptions): Promise<Record<string, string>>;
|