@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.
Files changed (147) hide show
  1. package/README.md +21 -0
  2. package/dist/cjs/aead.d.ts +24 -0
  3. package/dist/cjs/aead.js +65 -0
  4. package/dist/cjs/authExpiredDetector.d.ts +65 -0
  5. package/dist/cjs/authExpiredDetector.js +89 -0
  6. package/dist/cjs/authHelper.d.ts +51 -0
  7. package/dist/cjs/authHelper.js +321 -0
  8. package/dist/cjs/credentialEnvelope.d.ts +85 -0
  9. package/dist/cjs/credentialEnvelope.js +230 -0
  10. package/dist/cjs/credentialEnvelopeBridge.d.ts +116 -0
  11. package/dist/cjs/credentialEnvelopeBridge.js +637 -0
  12. package/dist/cjs/credentialResolver.d.ts +278 -0
  13. package/dist/cjs/credentialResolver.js +423 -0
  14. package/dist/cjs/daemonPair.d.ts +172 -0
  15. package/dist/cjs/daemonPair.js +495 -0
  16. package/dist/cjs/daemonScopes.d.ts +74 -0
  17. package/dist/cjs/daemonScopes.js +84 -0
  18. package/dist/cjs/deviceIdentityBridge.d.ts +113 -0
  19. package/dist/cjs/deviceIdentityBridge.js +591 -0
  20. package/dist/cjs/deviceKey.d.ts +167 -0
  21. package/dist/cjs/deviceKey.js +332 -0
  22. package/dist/cjs/fdToken.d.ts +17 -0
  23. package/dist/cjs/fdToken.js +238 -0
  24. package/dist/cjs/harnessTrust.d.ts +72 -0
  25. package/dist/cjs/harnessTrust.js +103 -0
  26. package/dist/cjs/headers-helper.d.ts +25 -0
  27. package/dist/cjs/headers-helper.js +323 -0
  28. package/dist/cjs/index.d.ts +37 -0
  29. package/dist/cjs/index.js +305 -0
  30. package/dist/cjs/jwtUtils.d.ts +35 -0
  31. package/dist/cjs/jwtUtils.js +61 -0
  32. package/dist/cjs/kek/index.d.ts +89 -0
  33. package/dist/cjs/kek/index.js +208 -0
  34. package/dist/cjs/kek/linux.d.ts +92 -0
  35. package/dist/cjs/kek/linux.js +596 -0
  36. package/dist/cjs/kek/macos.d.ts +112 -0
  37. package/dist/cjs/kek/macos.js +599 -0
  38. package/dist/cjs/kek/types.d.ts +111 -0
  39. package/dist/cjs/kek/types.js +43 -0
  40. package/dist/cjs/kek/windows.d.ts +71 -0
  41. package/dist/cjs/kek/windows.js +350 -0
  42. package/dist/cjs/kek/zeroize-registry.d.ts +44 -0
  43. package/dist/cjs/kek/zeroize-registry.js +63 -0
  44. package/dist/cjs/legacyStatePreflight.d.ts +67 -0
  45. package/dist/cjs/legacyStatePreflight.js +78 -0
  46. package/dist/cjs/localIdentity.d.ts +54 -0
  47. package/dist/cjs/localIdentity.js +57 -0
  48. package/dist/cjs/loginLocalhost.d.ts +115 -0
  49. package/dist/cjs/loginLocalhost.js +368 -0
  50. package/dist/cjs/loginWithLocalhost.d.ts +131 -0
  51. package/dist/cjs/loginWithLocalhost.js +359 -0
  52. package/dist/cjs/machineId.d.ts +35 -0
  53. package/dist/cjs/machineId.js +239 -0
  54. package/dist/cjs/messages.d.ts +9 -0
  55. package/dist/cjs/messages.js +44 -0
  56. package/dist/cjs/package.json +3 -0
  57. package/dist/cjs/pkce.d.ts +62 -0
  58. package/dist/cjs/pkce.js +158 -0
  59. package/dist/cjs/recoveryCode.d.ts +61 -0
  60. package/dist/cjs/recoveryCode.js +213 -0
  61. package/dist/cjs/refresh.d.ts +215 -0
  62. package/dist/cjs/refresh.js +877 -0
  63. package/dist/cjs/refreshClassification.d.ts +53 -0
  64. package/dist/cjs/refreshClassification.js +105 -0
  65. package/dist/cjs/refreshScheduler.d.ts +53 -0
  66. package/dist/cjs/refreshScheduler.js +332 -0
  67. package/dist/cjs/runtime.d.ts +149 -0
  68. package/dist/cjs/runtime.js +136 -0
  69. package/dist/cjs/spawnEnv.d.ts +76 -0
  70. package/dist/cjs/spawnEnv.js +153 -0
  71. package/dist/cjs/ttlParser.d.ts +54 -0
  72. package/dist/cjs/ttlParser.js +87 -0
  73. package/dist/cjs/types.d.ts +91 -0
  74. package/dist/cjs/types.js +26 -0
  75. package/dist/esm/aead.d.ts +24 -0
  76. package/dist/esm/aead.js +57 -0
  77. package/dist/esm/authExpiredDetector.d.ts +65 -0
  78. package/dist/esm/authExpiredDetector.js +85 -0
  79. package/dist/esm/authHelper.d.ts +51 -0
  80. package/dist/esm/authHelper.js +311 -0
  81. package/dist/esm/credentialEnvelope.d.ts +85 -0
  82. package/dist/esm/credentialEnvelope.js +213 -0
  83. package/dist/esm/credentialEnvelopeBridge.d.ts +116 -0
  84. package/dist/esm/credentialEnvelopeBridge.js +620 -0
  85. package/dist/esm/credentialResolver.d.ts +278 -0
  86. package/dist/esm/credentialResolver.js +414 -0
  87. package/dist/esm/daemonPair.d.ts +172 -0
  88. package/dist/esm/daemonPair.js +476 -0
  89. package/dist/esm/daemonScopes.d.ts +74 -0
  90. package/dist/esm/daemonScopes.js +80 -0
  91. package/dist/esm/deviceIdentityBridge.d.ts +113 -0
  92. package/dist/esm/deviceIdentityBridge.js +572 -0
  93. package/dist/esm/deviceKey.d.ts +167 -0
  94. package/dist/esm/deviceKey.js +320 -0
  95. package/dist/esm/fdToken.d.ts +17 -0
  96. package/dist/esm/fdToken.js +231 -0
  97. package/dist/esm/harnessTrust.d.ts +72 -0
  98. package/dist/esm/harnessTrust.js +97 -0
  99. package/dist/esm/headers-helper.d.ts +25 -0
  100. package/dist/esm/headers-helper.js +316 -0
  101. package/dist/esm/index.d.ts +37 -0
  102. package/dist/esm/index.js +144 -0
  103. package/dist/esm/jwtUtils.d.ts +35 -0
  104. package/dist/esm/jwtUtils.js +57 -0
  105. package/dist/esm/kek/index.d.ts +89 -0
  106. package/dist/esm/kek/index.js +162 -0
  107. package/dist/esm/kek/linux.d.ts +92 -0
  108. package/dist/esm/kek/linux.js +585 -0
  109. package/dist/esm/kek/macos.d.ts +112 -0
  110. package/dist/esm/kek/macos.js +586 -0
  111. package/dist/esm/kek/types.d.ts +111 -0
  112. package/dist/esm/kek/types.js +39 -0
  113. package/dist/esm/kek/windows.d.ts +71 -0
  114. package/dist/esm/kek/windows.js +344 -0
  115. package/dist/esm/kek/zeroize-registry.d.ts +44 -0
  116. package/dist/esm/kek/zeroize-registry.js +58 -0
  117. package/dist/esm/legacyStatePreflight.d.ts +67 -0
  118. package/dist/esm/legacyStatePreflight.js +71 -0
  119. package/dist/esm/localIdentity.d.ts +54 -0
  120. package/dist/esm/localIdentity.js +54 -0
  121. package/dist/esm/loginLocalhost.d.ts +115 -0
  122. package/dist/esm/loginLocalhost.js +355 -0
  123. package/dist/esm/loginWithLocalhost.d.ts +131 -0
  124. package/dist/esm/loginWithLocalhost.js +353 -0
  125. package/dist/esm/machineId.d.ts +35 -0
  126. package/dist/esm/machineId.js +231 -0
  127. package/dist/esm/messages.d.ts +9 -0
  128. package/dist/esm/messages.js +40 -0
  129. package/dist/esm/pkce.d.ts +62 -0
  130. package/dist/esm/pkce.js +148 -0
  131. package/dist/esm/recoveryCode.d.ts +61 -0
  132. package/dist/esm/recoveryCode.js +207 -0
  133. package/dist/esm/refresh.d.ts +215 -0
  134. package/dist/esm/refresh.js +863 -0
  135. package/dist/esm/refreshClassification.d.ts +53 -0
  136. package/dist/esm/refreshClassification.js +106 -0
  137. package/dist/esm/refreshScheduler.d.ts +53 -0
  138. package/dist/esm/refreshScheduler.js +329 -0
  139. package/dist/esm/runtime.d.ts +149 -0
  140. package/dist/esm/runtime.js +121 -0
  141. package/dist/esm/spawnEnv.d.ts +76 -0
  142. package/dist/esm/spawnEnv.js +149 -0
  143. package/dist/esm/ttlParser.d.ts +54 -0
  144. package/dist/esm/ttlParser.js +83 -0
  145. package/dist/esm/types.d.ts +91 -0
  146. package/dist/esm/types.js +21 -0
  147. 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>>;