@skrr-ai/cli 0.1.50 → 0.1.52

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 (131) hide show
  1. package/dist/base-command.d.ts +7 -0
  2. package/dist/base-command.js +95 -8
  3. package/dist/commands/agents/chat.d.ts +14 -0
  4. package/dist/commands/agents/chat.js +37 -1
  5. package/dist/commands/agents/create.js +8 -4
  6. package/dist/commands/agents/show.d.ts +9 -0
  7. package/dist/commands/agents/show.js +53 -0
  8. package/dist/commands/agents/update.d.ts +32 -0
  9. package/dist/commands/agents/update.js +81 -9
  10. package/dist/commands/balance/show.d.ts +34 -0
  11. package/dist/commands/balance/show.js +92 -2
  12. package/dist/commands/commitments/doctor.d.ts +1 -0
  13. package/dist/commands/commitments/doctor.js +19 -4
  14. package/dist/commands/commitments/effective-policy.js +15 -1
  15. package/dist/commands/commitments/next-tick.js +12 -2
  16. package/dist/commands/commitments/preflight.js +10 -0
  17. package/dist/commands/commitments/show.js +11 -1
  18. package/dist/commands/context.js +2 -1
  19. package/dist/commands/daemon/config.d.ts +15 -0
  20. package/dist/commands/daemon/config.js +53 -0
  21. package/dist/commands/daemon/index.js +1 -0
  22. package/dist/commands/daemon/status.js +15 -1
  23. package/dist/commands/doctor.js +8 -1
  24. package/dist/commands/harnesses/models.d.ts +54 -0
  25. package/dist/commands/harnesses/models.js +100 -7
  26. package/dist/commands/harnesses/show.d.ts +11 -0
  27. package/dist/commands/harnesses/show.js +35 -0
  28. package/dist/commands/harnesses/update.d.ts +28 -1
  29. package/dist/commands/harnesses/update.js +73 -10
  30. package/dist/commands/login.d.ts +2 -1
  31. package/dist/commands/login.js +26 -7
  32. package/dist/commands/logout.js +3 -0
  33. package/dist/commands/machines/dedicated/show.js +25 -2
  34. package/dist/commands/outbox/list.d.ts +12 -1
  35. package/dist/commands/outbox/list.js +31 -2
  36. package/dist/commands/revoke-daemon.js +9 -0
  37. package/dist/commands/spaces/create.d.ts +1 -0
  38. package/dist/commands/spaces/create.js +17 -0
  39. package/dist/commands/spaces/show.js +5 -0
  40. package/dist/commands/spaces/update.d.ts +1 -0
  41. package/dist/commands/spaces/update.js +16 -0
  42. package/dist/commands/tasks/create.d.ts +6 -0
  43. package/dist/commands/tasks/create.js +31 -6
  44. package/dist/commands/tasks/show.js +45 -0
  45. package/dist/commands/tasks/start.d.ts +14 -0
  46. package/dist/commands/tasks/start.js +31 -1
  47. package/dist/commands/tasks/update.d.ts +1 -0
  48. package/dist/commands/tasks/update.js +29 -1
  49. package/dist/commands/tasks/upsert.d.ts +1 -0
  50. package/dist/commands/tasks/upsert.js +16 -0
  51. package/dist/commands/whoami.js +5 -3
  52. package/dist/lib/agent-config.d.ts +2 -0
  53. package/dist/lib/agent-config.js +14 -10
  54. package/dist/lib/agentic-stream.d.ts +88 -1
  55. package/dist/lib/agentic-stream.js +330 -9
  56. package/dist/lib/auth-core-init.d.ts +2 -10
  57. package/dist/lib/auth-core-init.js +20 -67
  58. package/dist/lib/auth-failure.d.ts +33 -0
  59. package/dist/lib/auth-failure.js +68 -0
  60. package/dist/lib/auth-storage.d.ts +24 -0
  61. package/dist/lib/auth-storage.js +342 -11
  62. package/dist/lib/cli-id.d.ts +22 -0
  63. package/dist/lib/cli-id.js +56 -0
  64. package/dist/lib/cli-identity.d.ts +49 -0
  65. package/dist/lib/cli-identity.js +159 -0
  66. package/dist/lib/commitments.d.ts +25 -1
  67. package/dist/lib/commitments.js +46 -1
  68. package/dist/lib/config.d.ts +17 -0
  69. package/dist/lib/config.js +15 -3
  70. package/dist/lib/daemonBroker.d.ts +37 -1
  71. package/dist/lib/daemonBroker.js +146 -22
  72. package/dist/lib/daemonBrokerRefusal.d.ts +51 -0
  73. package/dist/lib/daemonBrokerRefusal.js +127 -0
  74. package/dist/lib/dedicated-machines.d.ts +33 -2
  75. package/dist/lib/dedicated-machines.js +50 -1
  76. package/dist/lib/delegated-cli.js +20 -1
  77. package/dist/lib/exec-runtime-binary.d.ts +3 -1
  78. package/dist/lib/exec-runtime-binary.js +4 -2
  79. package/dist/lib/first-party-harness-broker.js +10 -5
  80. package/dist/lib/harnesses.d.ts +10 -0
  81. package/dist/lib/keychain.d.ts +39 -0
  82. package/dist/lib/keychain.js +228 -39
  83. package/dist/lib/label-ref.d.ts +34 -0
  84. package/dist/lib/label-ref.js +68 -1
  85. package/dist/lib/live-runtime-binary.d.ts +40 -0
  86. package/dist/lib/live-runtime-binary.js +193 -0
  87. package/dist/lib/login.d.ts +23 -0
  88. package/dist/lib/login.js +67 -21
  89. package/dist/lib/message-intent.js +3 -1
  90. package/dist/lib/oauthLogin.js +2 -8
  91. package/dist/lib/refresh.js +6 -2
  92. package/dist/lib/tasks.d.ts +20 -0
  93. package/dist/lib/tasks.js +87 -8
  94. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +2 -0
  95. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +26 -1
  96. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +28 -4
  97. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +23 -5
  98. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +41 -3
  99. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
  100. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +25 -3
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.d.ts +32 -0
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.js +268 -0
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.d.ts +2 -2
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +11 -10
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +36 -0
  106. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +156 -0
  107. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +8 -0
  108. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +8 -1
  109. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +22 -0
  110. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +61 -2
  111. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +2 -0
  112. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +25 -1
  113. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +28 -4
  114. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +22 -4
  115. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +41 -3
  116. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
  117. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
  118. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.d.ts +32 -0
  119. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.js +257 -0
  120. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.d.ts +2 -2
  121. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +11 -10
  122. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +36 -0
  123. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +147 -0
  124. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +8 -0
  125. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +8 -1
  126. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +22 -0
  127. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +58 -1
  128. package/dist/node_modules/@skrr-ai/auth-core/package.json +21 -1
  129. package/dist/node_modules/@skrr-ai/data-provider/index.js +3139 -3097
  130. package/oclif.manifest.json +28591 -28438
  131. package/package.json +1 -1
@@ -23,11 +23,13 @@ exports.__resetCachedDaemonIdForTest = __resetCachedDaemonIdForTest;
23
23
  * directory survives), and a user whose machineUuid changes can
24
24
  * still be re-anchored via the legacy-id list.
25
25
  *
26
- * Auth-core does NOT know about platform-level OS UUIDs (those still
27
- * live in `daemon/src/registry.ts::getMachineUuid()`). The fallback is
28
- * supplied by the consumer via the `machineUuidFallback` parameter so
29
- * a daemon can pass its richer detector while a CLI uses a hostname
30
- * fingerprint.
26
+ * This id is NOT the `machineUuid` the server keys Daemon rows on. That
27
+ * is the hardware id from `getMachineUuid()` (`machineUuid.ts`), and the
28
+ * two must never be substituted for each other on the wire: OSK-12063
29
+ * was the CLI sending this id as `machineUuid` on device-key enrollment,
30
+ * which matched no Daemon row and 404'd forever. The fallback used when
31
+ * this file cannot be persisted is supplied by the consumer via the
32
+ * `machineUuidFallback` parameter (the daemon passes `getMachineUuid`).
31
33
  */
32
34
  const node_crypto_1 = __importDefault(require("node:crypto"));
33
35
  const node_fs_1 = __importDefault(require("node:fs"));
@@ -108,9 +110,8 @@ function writeDaemonIdFile(target, id) {
108
110
  * SHA-256 of `os.hostname()` + `os.platform()` — better than just
109
111
  * hostname (includes platform) but worse than a true hardware UUID.
110
112
  *
111
- * The daemon overrides this with its richer detector
112
- * (`registry.ts::getMachineUuid()`) when available. The CLI today has
113
- * no native deps for OS UUIDs and so accepts this default.
113
+ * The daemon overrides this with the hardware detector
114
+ * (`getMachineUuid()`, `machineUuid.ts`).
114
115
  */
115
116
  function defaultMachineUuidFallback() {
116
117
  const host = node_os_1.default.hostname() || 'unknown-host';
@@ -129,8 +130,8 @@ function defaultMachineUuidFallback() {
129
130
  * with a real UUIDv7 downstream.
130
131
  *
131
132
  * The caller may pass `machineUuidFallback` to override the default
132
- * hostname+platform fingerprint with a richer detector (e.g. the
133
- * daemon's `getMachineUuid()` from `registry.ts`).
133
+ * hostname+platform fingerprint with a richer detector (e.g.
134
+ * `getMachineUuid()` from `machineUuid.ts`).
134
135
  */
135
136
  function ensureMachineDaemonId(machineUuidFallback) {
136
137
  if (cachedStableDaemonId !== null)
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Daemon sources whose identity is ISSUED by the control plane rather than
3
+ * derived from the host: the provisioner mints the daemon id and machine uuid
4
+ * before the daemon exists, and every binary on the runtime must use exactly
5
+ * those values.
6
+ *
7
+ * `dedicated-runtime` is here because the server's generation fence parses the
8
+ * lease generation out of the machine uuid (`dedicated-runtime:<lease>:<gen>`).
9
+ * A guest that fell back to `/etc/machine-id` would register under a uuid the
10
+ * fence cannot read, and every dispatch to it would be refused.
11
+ */
12
+ export declare const HOSTED_RUNTIME_MACHINE_SOURCES: ReadonlySet<string>;
13
+ /**
14
+ * The control-plane-issued value of `name` (e.g. `OVERSKY_MACHINE_UUID`), or
15
+ * null when this process is not on a hosted runtime or the value is absent /
16
+ * malformed. Only honoured under a hosted `OVERSKY_DAEMON_SOURCE`, so a stray
17
+ * variable on a laptop cannot rename the machine.
18
+ */
19
+ export declare function hostedRuntimeIdOverride(name: string, env?: NodeJS.ProcessEnv): string | null;
20
+ /**
21
+ * Return a stable, machine-scoped UUID. Cached for the process lifetime.
22
+ *
23
+ * Callers should treat this as opaque: the format differs per platform
24
+ * (macOS IOPlatformUUID, Linux systemd machine-id, Windows MachineGuid).
25
+ * The guarantee is only: same machine -> same value across reboots, and the
26
+ * same value from every binary that calls this function.
27
+ *
28
+ * On a hosted runtime the control-plane-issued `OVERSKY_MACHINE_UUID` wins.
29
+ *
30
+ * Falls back to a deterministic hostname+username hash if the platform
31
+ * probe fails. This preserves server-side record continuity even on
32
+ * locked-down machines that refuse ioreg / registry access.
33
+ */
34
+ export declare function getMachineUuid(): string;
35
+ /** @internal test seam — forget the cached value so the next call re-probes. */
36
+ export declare function __resetMachineUuidCacheForTest(): void;
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HOSTED_RUNTIME_MACHINE_SOURCES = void 0;
7
+ exports.hostedRuntimeIdOverride = hostedRuntimeIdOverride;
8
+ exports.getMachineUuid = getMachineUuid;
9
+ exports.__resetMachineUuidCacheForTest = __resetMachineUuidCacheForTest;
10
+ /**
11
+ * machineUuid.ts — the ONE hardware machine identity sent to the server as
12
+ * `machineUuid`, shared by the daemon and the CLI.
13
+ *
14
+ * Server Daemon rows are keyed on this value (daemon register, device-key
15
+ * enrollment, token mint, pairing), so every binary on one machine must
16
+ * compute it identically. Until OSK-12063 the CLI sent its own UUIDv7 from
17
+ * `~/.skrr/daemon.id` (`ensureMachineDaemonId()`, a DIFFERENT identity) as
18
+ * `machineUuid` on device-key enrollment while the daemon sent the hardware
19
+ * UUID computed here, so every CLI enrollment matched no Daemon row, got
20
+ * 404 DAEMON_NOT_FOUND and backed off forever. Two implementations of one
21
+ * identity can only agree by being edited together; this file is the one
22
+ * implementation, and `daemon/src/registry-lite.ts` re-exports it.
23
+ *
24
+ * Deliberately dependency-free (node builtins only) and exposed as the
25
+ * `@skrr-ai/auth-core/machine-uuid` subpath: the packaged desktop imports the
26
+ * daemon's `registry-lite.js` directly, and loading the full auth-core index
27
+ * there would drag in the credential stack for a one-function need.
28
+ *
29
+ * Not to be confused with `ensureMachineDaemonId()` (`machineId.ts`), the
30
+ * minted per-home-dir UUIDv7 that anchors the daemon's `stableDaemonId`. That
31
+ * one survives an OS reinstall; this one survives a home-dir wipe. They are
32
+ * different facts and neither may stand in for the other on the wire.
33
+ */
34
+ const node_crypto_1 = __importDefault(require("node:crypto"));
35
+ const node_fs_1 = __importDefault(require("node:fs"));
36
+ const node_os_1 = __importDefault(require("node:os"));
37
+ const node_child_process_1 = require("node:child_process");
38
+ /**
39
+ * Daemon sources whose identity is ISSUED by the control plane rather than
40
+ * derived from the host: the provisioner mints the daemon id and machine uuid
41
+ * before the daemon exists, and every binary on the runtime must use exactly
42
+ * those values.
43
+ *
44
+ * `dedicated-runtime` is here because the server's generation fence parses the
45
+ * lease generation out of the machine uuid (`dedicated-runtime:<lease>:<gen>`).
46
+ * A guest that fell back to `/etc/machine-id` would register under a uuid the
47
+ * fence cannot read, and every dispatch to it would be refused.
48
+ */
49
+ exports.HOSTED_RUNTIME_MACHINE_SOURCES = new Set([
50
+ 'hosted-machine',
51
+ 'cloud-coding',
52
+ 'dedicated-runtime',
53
+ ]);
54
+ const HOSTED_RUNTIME_ID_RE = /^[A-Za-z0-9_.:-]{1,160}$/;
55
+ /**
56
+ * The control-plane-issued value of `name` (e.g. `OVERSKY_MACHINE_UUID`), or
57
+ * null when this process is not on a hosted runtime or the value is absent /
58
+ * malformed. Only honoured under a hosted `OVERSKY_DAEMON_SOURCE`, so a stray
59
+ * variable on a laptop cannot rename the machine.
60
+ */
61
+ function hostedRuntimeIdOverride(name, env = process.env) {
62
+ if (!exports.HOSTED_RUNTIME_MACHINE_SOURCES.has(env.OVERSKY_DAEMON_SOURCE ?? '')) {
63
+ return null;
64
+ }
65
+ const value = env[name]?.trim();
66
+ if (!value || !HOSTED_RUNTIME_ID_RE.test(value))
67
+ return null;
68
+ return value;
69
+ }
70
+ let cachedMachineUuid = null;
71
+ function readMachineUuidDarwin() {
72
+ try {
73
+ const out = (0, node_child_process_1.execFileSync)('ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice'], {
74
+ encoding: 'utf-8',
75
+ timeout: 2_000,
76
+ stdio: ['ignore', 'pipe', 'ignore'],
77
+ });
78
+ const match = out.match(/IOPlatformUUID"?\s*=\s*"?([0-9A-F-]{36})/i);
79
+ return match ? match[1].toLowerCase() : null;
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
85
+ function readMachineUuidLinux() {
86
+ for (const p of ['/etc/machine-id', '/var/lib/dbus/machine-id']) {
87
+ try {
88
+ const raw = node_fs_1.default.readFileSync(p, 'utf-8').trim();
89
+ if (raw)
90
+ return raw.toLowerCase();
91
+ }
92
+ catch {
93
+ // Try the next candidate.
94
+ }
95
+ }
96
+ return null;
97
+ }
98
+ function readMachineUuidWindows() {
99
+ try {
100
+ const out = (0, node_child_process_1.execFileSync)('reg', ['query', 'HKLM\\SOFTWARE\\Microsoft\\Cryptography', '/v', 'MachineGuid'], { encoding: 'utf-8', timeout: 2_000, stdio: ['ignore', 'pipe', 'ignore'] });
101
+ const match = out.match(/MachineGuid\s+REG_SZ\s+([0-9A-Fa-f-]{36})/);
102
+ return match ? match[1].toLowerCase() : null;
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ /**
109
+ * Return a stable, machine-scoped UUID. Cached for the process lifetime.
110
+ *
111
+ * Callers should treat this as opaque: the format differs per platform
112
+ * (macOS IOPlatformUUID, Linux systemd machine-id, Windows MachineGuid).
113
+ * The guarantee is only: same machine -> same value across reboots, and the
114
+ * same value from every binary that calls this function.
115
+ *
116
+ * On a hosted runtime the control-plane-issued `OVERSKY_MACHINE_UUID` wins.
117
+ *
118
+ * Falls back to a deterministic hostname+username hash if the platform
119
+ * probe fails. This preserves server-side record continuity even on
120
+ * locked-down machines that refuse ioreg / registry access.
121
+ */
122
+ function getMachineUuid() {
123
+ const hostedMachineUuid = hostedRuntimeIdOverride('OVERSKY_MACHINE_UUID');
124
+ if (hostedMachineUuid)
125
+ return hostedMachineUuid;
126
+ if (cachedMachineUuid !== null)
127
+ return cachedMachineUuid;
128
+ let detected = null;
129
+ switch (process.platform) {
130
+ case 'darwin':
131
+ detected = readMachineUuidDarwin();
132
+ break;
133
+ case 'linux':
134
+ detected = readMachineUuidLinux();
135
+ break;
136
+ case 'win32':
137
+ detected = readMachineUuidWindows();
138
+ break;
139
+ }
140
+ if (detected) {
141
+ cachedMachineUuid = detected;
142
+ return detected;
143
+ }
144
+ const fallback = 'fallback-' +
145
+ node_crypto_1.default
146
+ .createHash('sha256')
147
+ .update(`${node_os_1.default.hostname()}:${node_os_1.default.userInfo().username}`)
148
+ .digest('hex')
149
+ .slice(0, 32);
150
+ cachedMachineUuid = fallback;
151
+ return fallback;
152
+ }
153
+ /** @internal test seam — forget the cached value so the next call re-probes. */
154
+ function __resetMachineUuidCacheForTest() {
155
+ cachedMachineUuid = null;
156
+ }
@@ -274,6 +274,14 @@ export declare function persistReauthState(state: ReauthState): void;
274
274
  export declare function needsReauthState(): {
275
275
  needed: boolean;
276
276
  reason?: string;
277
+ /**
278
+ * The server's own words for the refusal (`Refresh token bound to a different
279
+ * daemon`). The code alone collapses several distinct causes into one
280
+ * (`REFRESH_INVALID` is also "not recognized" and "issued without a
281
+ * daemonId"), and a surface that names only the code cannot tell the reader
282
+ * which one they hit (OSK-12116).
283
+ */
284
+ message?: string;
277
285
  since?: number;
278
286
  };
279
287
  export declare function clearReauthState(): void;
@@ -969,7 +969,14 @@ function needsReauthState() {
969
969
  return { needed: false };
970
970
  const raw = node_fs_1.default.readFileSync(flagPath, 'utf-8');
971
971
  const state = JSON.parse(raw);
972
- return { needed: true, reason: state.code, since: state.at };
972
+ return {
973
+ needed: true,
974
+ reason: state.code,
975
+ ...(typeof state.message === 'string' && state.message.length > 0
976
+ ? { message: state.message }
977
+ : {}),
978
+ since: state.at,
979
+ };
973
980
  }
974
981
  catch {
975
982
  return { needed: false };
@@ -84,3 +84,25 @@ export declare const SENSITIVE_ENV_PREFIX_EXCEPTIONS: ReadonlySet<string>;
84
84
  * etc.) so legitimate tools keep working.
85
85
  */
86
86
  export declare function sanitizeSpawnEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
87
+ /**
88
+ * OSK-12134 — loopback names a spawned child must reach directly, never through
89
+ * a proxy. A harness talks to this machine constantly: the daemon's local
90
+ * bridges (browser, delegated CLI, message intents), the inference broker, and
91
+ * the engine's own loopback services (skrr Code's `session/new` reaches a local
92
+ * directory service and failed with "service failure" behind a shell proxy).
93
+ */
94
+ export declare const LOOPBACK_NO_PROXY_HOSTS: readonly string[];
95
+ /** True when `env` names a proxy some client in the child might honour. */
96
+ export declare function envHasProxy(env: Readonly<Record<string, string | undefined>>): boolean;
97
+ /**
98
+ * Return `env` with the loopback hosts appended to BOTH `NO_PROXY` and
99
+ * `no_proxy`, when (and only when) it names a proxy. Tools disagree on which
100
+ * spelling they read and whether they fall back to the other, so an unset
101
+ * spelling is seeded from the other one before the hosts are appended —
102
+ * seeding only the hosts would silently drop every existing exemption for a
103
+ * tool that prefers the unset spelling. Existing entries keep their order.
104
+ *
105
+ * Exported for a parent that layers a proxy onto the child's env AFTER
106
+ * {@link sanitizeSpawnEnv}: call it again on the final map.
107
+ */
108
+ export declare function withLoopbackNoProxy<T extends Record<string, string | undefined>>(env: T): T;
@@ -40,8 +40,10 @@
40
40
  * serve that case: a deny-list with a hole is not a deny-list.
41
41
  */
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
- exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_NAME_TOKENS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = void 0;
43
+ exports.LOOPBACK_NO_PROXY_HOSTS = exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_NAME_TOKENS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = void 0;
44
44
  exports.sanitizeSpawnEnv = sanitizeSpawnEnv;
45
+ exports.envHasProxy = envHasProxy;
46
+ exports.withLoopbackNoProxy = withLoopbackNoProxy;
45
47
  /**
46
48
  * Env vars that must NEVER reach a spawned subprocess.
47
49
  *
@@ -187,5 +189,62 @@ function sanitizeSpawnEnv(env) {
187
189
  continue;
188
190
  out[key] = value;
189
191
  }
190
- return out;
192
+ return withLoopbackNoProxy(out);
193
+ }
194
+ /**
195
+ * OSK-12134 — loopback names a spawned child must reach directly, never through
196
+ * a proxy. A harness talks to this machine constantly: the daemon's local
197
+ * bridges (browser, delegated CLI, message intents), the inference broker, and
198
+ * the engine's own loopback services (skrr Code's `session/new` reaches a local
199
+ * directory service and failed with "service failure" behind a shell proxy).
200
+ */
201
+ exports.LOOPBACK_NO_PROXY_HOSTS = ['127.0.0.1', 'localhost', '::1'];
202
+ const PROXY_ENV_VARS = [
203
+ 'HTTP_PROXY',
204
+ 'http_proxy',
205
+ 'HTTPS_PROXY',
206
+ 'https_proxy',
207
+ 'ALL_PROXY',
208
+ 'all_proxy',
209
+ ];
210
+ /** True when `env` names a proxy some client in the child might honour. */
211
+ function envHasProxy(env) {
212
+ return PROXY_ENV_VARS.some((name) => typeof env[name] === 'string' && env[name].trim() !== '');
213
+ }
214
+ function appendNoProxyHosts(list, hosts) {
215
+ const entries = list
216
+ .split(',')
217
+ .map((entry) => entry.trim().toLowerCase())
218
+ .filter(Boolean);
219
+ // `*` already exempts everything, and some clients honour it only as the
220
+ // entire value, so appending to it would break them.
221
+ if (entries.includes('*'))
222
+ return list;
223
+ const missing = hosts.filter((host) => !entries.includes(host));
224
+ if (missing.length === 0)
225
+ return list;
226
+ const kept = list.replace(/[\s,]+$/, '');
227
+ return kept ? `${kept},${missing.join(',')}` : missing.join(',');
228
+ }
229
+ /**
230
+ * Return `env` with the loopback hosts appended to BOTH `NO_PROXY` and
231
+ * `no_proxy`, when (and only when) it names a proxy. Tools disagree on which
232
+ * spelling they read and whether they fall back to the other, so an unset
233
+ * spelling is seeded from the other one before the hosts are appended —
234
+ * seeding only the hosts would silently drop every existing exemption for a
235
+ * tool that prefers the unset spelling. Existing entries keep their order.
236
+ *
237
+ * Exported for a parent that layers a proxy onto the child's env AFTER
238
+ * {@link sanitizeSpawnEnv}: call it again on the final map.
239
+ */
240
+ function withLoopbackNoProxy(env) {
241
+ if (!envHasProxy(env))
242
+ return env;
243
+ const upper = env.NO_PROXY?.trim() ? env.NO_PROXY : undefined;
244
+ const lower = env.no_proxy?.trim() ? env.no_proxy : undefined;
245
+ return {
246
+ ...env,
247
+ NO_PROXY: appendNoProxyHosts(upper ?? lower ?? '', exports.LOOPBACK_NO_PROXY_HOSTS),
248
+ no_proxy: appendNoProxyHosts(lower ?? upper ?? '', exports.LOOPBACK_NO_PROXY_HOSTS),
249
+ };
191
250
  }
@@ -21,6 +21,8 @@ type DisabledReason = 'kek_unavailable' | 'init_failed' | 'platform_unsupported'
21
21
  export declare function __setStateForTest(next: CredEnvelopeState): CredEnvelopeState;
22
22
  /** @internal Test seam — read state for assertions. */
23
23
  export declare function __getStateForTest(): CredEnvelopeState;
24
+ /** @internal Test seam — each spec owns its warn-dedupe horizon. */
25
+ export declare function __clearWarnDedupeForTest(): void;
24
26
  /**
25
27
  * Initialize the envelope path. Idempotent — second call when state is
26
28
  * non-uninit returns immediately.
@@ -123,7 +123,21 @@ function atomicWrite0600(target, value) {
123
123
  * The event name passed in is the FULLY-QUALIFIED `cred_envelope.*` form
124
124
  * because daemon-side telemetry consumers (`event-log.ts`, dashboards,
125
125
  * spec assertions) key off the prefixed name. Do not strip the prefix.
126
+ *
127
+ * Identical failure warnings hit the logger only once per process.
128
+ * Telemetry still records every occurrence — the dedupe exists because a
129
+ * single unreadable envelope produced the same warning once per stored
130
+ * field (token, refresh token, expiry…), which read to users as three
131
+ * crashes rather than one stale credential (OSK-12074). The signature is
132
+ * event + kek identity + message, so a genuinely different failure mode
133
+ * still gets its own line.
126
134
  */
135
+ const warnedSignatures = new Set();
136
+ const MAX_WARNED_SIGNATURES = 64;
137
+ /** @internal Test seam — each spec owns its warn-dedupe horizon. */
138
+ export function __clearWarnDedupeForTest() {
139
+ warnedSignatures.clear();
140
+ }
127
141
  function emit(eventName, success, metadata) {
128
142
  emitAuthTelemetry({
129
143
  timestamp: new Date().toISOString(),
@@ -132,7 +146,13 @@ function emit(eventName, success, metadata) {
132
146
  metadata: { event: eventName, ...metadata },
133
147
  });
134
148
  if (!success) {
135
- getAuthLogger().warn(`[credEnvelope] ${eventName}`, metadata);
149
+ const signature = `${eventName}:${metadata.kekId ?? ''}:${metadata.kekKind ?? ''}:${metadata.message ?? ''}`;
150
+ if (!warnedSignatures.has(signature)) {
151
+ if (warnedSignatures.size >= MAX_WARNED_SIGNATURES)
152
+ warnedSignatures.clear();
153
+ warnedSignatures.add(signature);
154
+ getAuthLogger().warn(`[credEnvelope] ${eventName}`, metadata);
155
+ }
136
156
  }
137
157
  }
138
158
  // ---------------------------------------------------------------------------
@@ -745,6 +765,10 @@ export function maybeDecryptOnRead(stored) {
745
765
  kekId: _state.kekId,
746
766
  kekKind: _state.kekKind,
747
767
  message: err?.message ?? 'unknown',
768
+ // The crypto error alone reads as a crash; the actionable truth is
769
+ // that this stored credential is unreadable by THIS binary (usually
770
+ // a signing-identity or profile drift) and re-login rebuilds it.
771
+ hint: 'stored credential is unreadable on this machine — signing in again rebuilds it',
748
772
  });
749
773
  return { plaintext: null, needsMigration: false };
750
774
  }
@@ -23,9 +23,14 @@
23
23
  *
24
24
  * What the daemon verifies is the binding, not the server's honesty: the
25
25
  * request already arrives over the daemon's authenticated server channel, so
26
- * the field IS the attestation. What the binding buys is that the model —
27
- * which authors `toolInput` — cannot mint an approval, and an approval for
28
- * one call cannot be replayed onto another.
26
+ * the field IS the attestation — it is unsigned, and `decidedBy` is a literal.
27
+ * What the binding buys is that the model — which authors `toolInput` —
28
+ * cannot mint an approval, and an approval for one call cannot be replayed
29
+ * onto another. The verifier also bounds the validity window itself (never
30
+ * trusting the issuer's `expiresAt` beyond `ATTESTED_TOOL_APPROVAL_TTL_MS`),
31
+ * and the daemon spends each approval once: a second delivery of the same
32
+ * attestation inside its window is refused as `replayed`. That ledger lives
33
+ * in daemon memory, so a daemon restart inside the (≤60s) window forgets it.
29
34
  */
30
35
  /** Announced by a daemon that honours `attestedApproval` on daemon:tool:request. */
31
36
  export declare const DAEMON_TOOL_ATTESTED_APPROVAL_CAPABILITY = "daemon_tool_attested_approval_v1";
@@ -89,12 +94,31 @@ export declare function mintAttestedToolApproval(args: {
89
94
  now?: number;
90
95
  ttlMs?: number;
91
96
  }): AttestedToolApproval;
97
+ /**
98
+ * Why an attestation did not approve a call — logged by the daemon, never
99
+ * shown to the caller beyond "absent" vs "did not verify".
100
+ * - `absent` — no attestation arrived (the server did not ask anyone)
101
+ * - `malformed` — not the v1 shape
102
+ * - `expired` — past `expiresAt`, or issued implausibly far in the future
103
+ * - `window` — the issuer claimed a validity window longer than
104
+ * `ATTESTED_TOOL_APPROVAL_TTL_MS` (or an empty/negative one)
105
+ * - `binding` — minted for a different call (toolUseId, tool, or input)
106
+ * - `replayed` — this exact approval was already spent on this daemon
107
+ * (the ledger is the verifier's caller's, see the daemon's
108
+ * `permissions/attestation-ledger.ts`)
109
+ */
110
+ export type AttestedToolApprovalRefusalReason = 'absent' | 'malformed' | 'binding' | 'expired' | 'window' | 'replayed';
92
111
  export type AttestedToolApprovalVerdict = {
93
112
  ok: true;
94
113
  } | {
95
114
  ok: false;
96
- reason: 'absent' | 'malformed' | 'binding' | 'expired';
115
+ reason: AttestedToolApprovalRefusalReason;
97
116
  };
117
+ /**
118
+ * Clock skew tolerated on `issuedAt`: a server clock slightly ahead of the
119
+ * daemon's must not refuse a fresh approval.
120
+ */
121
+ export declare const ATTESTED_TOOL_APPROVAL_CLOCK_SKEW_MS: number;
98
122
  /**
99
123
  * Daemon side: verify an attestation against the call it claims to approve.
100
124
  * Untrusted wire input — every field is checked, and any mismatch is a plain
@@ -24,9 +24,14 @@ import { createHash } from 'node:crypto';
24
24
  *
25
25
  * What the daemon verifies is the binding, not the server's honesty: the
26
26
  * request already arrives over the daemon's authenticated server channel, so
27
- * the field IS the attestation. What the binding buys is that the model —
28
- * which authors `toolInput` — cannot mint an approval, and an approval for
29
- * one call cannot be replayed onto another.
27
+ * the field IS the attestation — it is unsigned, and `decidedBy` is a literal.
28
+ * What the binding buys is that the model — which authors `toolInput` —
29
+ * cannot mint an approval, and an approval for one call cannot be replayed
30
+ * onto another. The verifier also bounds the validity window itself (never
31
+ * trusting the issuer's `expiresAt` beyond `ATTESTED_TOOL_APPROVAL_TTL_MS`),
32
+ * and the daemon spends each approval once: a second delivery of the same
33
+ * attestation inside its window is refused as `replayed`. That ledger lives
34
+ * in daemon memory, so a daemon restart inside the (≤60s) window forgets it.
30
35
  */
31
36
  /** Announced by a daemon that honours `attestedApproval` on daemon:tool:request. */
32
37
  export const DAEMON_TOOL_ATTESTED_APPROVAL_CAPABILITY = 'daemon_tool_attested_approval_v1';
@@ -129,6 +134,11 @@ export function mintAttestedToolApproval(args) {
129
134
  expiresAt: now + (args.ttlMs ?? ATTESTED_TOOL_APPROVAL_TTL_MS),
130
135
  };
131
136
  }
137
+ /**
138
+ * Clock skew tolerated on `issuedAt`: a server clock slightly ahead of the
139
+ * daemon's must not refuse a fresh approval.
140
+ */
141
+ export const ATTESTED_TOOL_APPROVAL_CLOCK_SKEW_MS = 30 * 1000;
132
142
  /**
133
143
  * Daemon side: verify an attestation against the call it claims to approve.
134
144
  * Untrusted wire input — every field is checked, and any mismatch is a plain
@@ -152,9 +162,17 @@ export function verifyAttestedToolApproval(approval, call) {
152
162
  return { ok: false, reason: 'malformed' };
153
163
  }
154
164
  const now = call.now ?? Date.now();
155
- if (a.expiresAt <= now || a.issuedAt > now + 30_000) {
165
+ if (a.expiresAt <= now || a.issuedAt > now + ATTESTED_TOOL_APPROVAL_CLOCK_SKEW_MS) {
156
166
  return { ok: false, reason: 'expired' };
157
167
  }
168
+ // The TTL is the VERIFIER's bound, not the issuer's claim (OSK-12108): an
169
+ // attestation whose own window is longer than one approval may stay valid
170
+ // is refused whatever `expiresAt` says, so "answers one person about this
171
+ // call, moments ago" holds even against an issuer that set a year.
172
+ const window = a.expiresAt - a.issuedAt;
173
+ if (!(window > 0) || window > ATTESTED_TOOL_APPROVAL_TTL_MS) {
174
+ return { ok: false, reason: 'window' };
175
+ }
158
176
  if (a.toolUseId !== call.toolUseId ||
159
177
  a.toolName.toLowerCase() !== String(call.toolName).toLowerCase() ||
160
178
  a.inputSha256 !== canonicalToolInputDigest(call.toolInput)) {
@@ -585,11 +585,49 @@ export async function enrollWithServer(opts) {
585
585
  // Enrollment is the CLI's (JWT) job; the daemon holding an opaque
586
586
  // token should not enroll. Guarded at the daemon call site, this is
587
587
  // defense-in-depth for any other opaque caller.
588
- // • 409 already-enrolled — a concurrent binary won the enrollment race
589
- // for the shared keypair; idempotent, nothing to do.
588
+ // • 409 ALREADY_ENROLLED with OUR thumbprint — a concurrent binary won
589
+ // the enrollment race for this keypair; idempotent, nothing to do.
590
+ //
591
+ // A 409 is NOT benign when the row is pinned to a DIFFERENT key
592
+ // (OSK-12063). The server keeps one pin per (user, machine) row while
593
+ // every local profile has its own key, so a 409 can mean "every refresh
594
+ // this key signs will be refused with DEVICE_PROOF_THUMBPRINT_MISMATCH".
595
+ // The server names the pinned thumbprint (`enrolledThumbprint`) so the
596
+ // two cases can be told apart. An older server that omits it keeps the
597
+ // old reading, because there is nothing to compare. A 409
598
+ // ENROLLMENT_DUPLICATE_KEY — this key is pinned on another active row —
599
+ // is never benign either.
600
+ let conflictCode = null;
601
+ let enrolledThumbprint = null;
602
+ if (response.status === 409) {
603
+ try {
604
+ const parsed = JSON.parse(responseText);
605
+ conflictCode = typeof parsed.code === 'string' ? parsed.code : null;
606
+ enrolledThumbprint =
607
+ typeof parsed.enrolledThumbprint === 'string' ? parsed.enrolledThumbprint : null;
608
+ }
609
+ catch {
610
+ /* non-JSON 409 — treat as the legacy benign race */
611
+ }
612
+ }
613
+ const pinnedToOtherKey = response.status === 409 &&
614
+ enrolledThumbprint !== null &&
615
+ enrolledThumbprint !== keypair.thumbprint;
616
+ const keyPinnedElsewhere = response.status === 409 && conflictCode === 'ENROLLMENT_DUPLICATE_KEY';
590
617
  const isBenignRejection = (response.status === 404 && responseText.includes('DAEMON_NOT_FOUND')) ||
591
618
  (response.status === 403 && responseText.includes('OPAQUE_AUTH_REFUSED')) ||
592
- response.status === 409;
619
+ (response.status === 409 && !pinnedToOtherKey && !keyPinnedElsewhere);
620
+ if (pinnedToOtherKey) {
621
+ emit('enrollment_key_mismatch', false, {
622
+ profile,
623
+ thumbprint: keypair.thumbprint,
624
+ enrolledThumbprint,
625
+ serverUrl,
626
+ message: 'this machine is registered with a different device key than this profile signs ' +
627
+ 'with; its refreshes will be refused. An interactive daemon login (skrrd login) ' +
628
+ 're-registers this key.',
629
+ }, { logLevel: 'warn' });
630
+ }
593
631
  if (response.status === 404 && responseText.includes('DAEMON_NOT_FOUND')) {
594
632
  try {
595
633
  atomicWrite0600(backoffPath, JSON.stringify({
@@ -29,11 +29,13 @@ export { __setKekStrategyForTest, getKekStrategy, InMemoryKek, UnavailableKek, K
29
29
  export { buildProof as buildDeviceProof, generateDeviceKeyPair, jwkThumbprint, normalizeHtu, publicJwkFromX, tokenHash, verifyProof as verifyDeviceProof, type DeviceKeyPair, type DeviceProofClaims, type DeviceProofHeader, type Ed25519PrivateJwk, type Ed25519PublicJwk, type VerifyFailureReason as DeviceProofVerifyFailureReason, type VerifyFailure as DeviceProofVerifyFailure, type VerifyOptions as DeviceProofVerifyOptions, type VerifySuccess as DeviceProofVerifySuccess, } from './deviceKey.js';
30
30
  export { initCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, maybeEncryptForWrite, maybeDecryptOnRead, __wrappedDekFilePathForTest, __setStateForTest as __setCredEnvelopeStateForTest, __getStateForTest as __getCredEnvelopeStateForTest, __resetShutdownGuardForTest, type ReadTransformResult, } from './credentialEnvelopeBridge.js';
31
31
  export { initDeviceIdentity, resetDeviceIdentity, signServerRequest, enrollWithServer, clearEnrollmentMarker, isDeviceIdentityActive, getDevicePublicKey, describeDeviceIdentityState, __publicKeyPathForTest, __privateKeyPathForTest, __enrollmentMarkerPathForTest, __enrollmentBackoffPathForTest, __setStateForTest as __setDeviceIdentityStateForTest, __getStateForTest as __getDeviceIdentityStateForTest, type EnrollmentResult, } from './deviceIdentityBridge.js';
32
+ export { getMachineUuid, hostedRuntimeIdOverride, HOSTED_RUNTIME_MACHINE_SOURCES, __resetMachineUuidCacheForTest, } from './machineUuid.js';
32
33
  export { ensureMachineDaemonId, legacyDaemonUuids, __resetCachedDaemonIdForTest, } from './machineId.js';
33
34
  export { PAIR_AAD_PREFIX, PAIR_AAD_MAX_LEN, PAIR_BUNDLE_MAX_BASE64_LEN, PAIR_NONCE_LEN, PAIR_PUBKEY_LEN, PAIR_SECRET_LEN, PairBundleError, assemblePairBundleBase64, base64ToUint8Array as pairBase64ToUint8Array, base64UrlToUint8Array as pairBase64UrlToUint8Array, buildPairAad, buildPairUrl, generatePairKeyPair, openPairBundleBase64, parsePairBundle, parsePairPlaintext, parsePairUrl, uint8ArrayToBase64 as pairUint8ArrayToBase64, uint8ArrayToBase64Url as pairUint8ArrayToBase64Url, type AssembleBundleOptions as PairAssembleBundleOptions, type OpenBundleOptions as PairOpenBundleOptions, type PairBundle, type PairBundleErrorCode, type PairKeyPair, type PairPlaintextV1, type PairUrlFields, } from './daemonPair.js';
34
35
  export { HARNESS_TIERS, getHarnessTier, credentialPolicyForTier, tierPermitsConfiguredAutoMode, type HarnessTrustTier, type CredentialDeliveryPolicy, type CredentialMode, } from './harnessTrust.js';
35
36
  export { HARNESSES_LOADING_CLAUDE_SKILLS_LAYOUT, claudeSkillsLayoutEvidence, harnessLoadsClaudeSkillsLayout, } from './harnessSkillLayout.js';
36
- export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_NAME_TOKENS, SENSITIVE_ENV_PREFIX_EXCEPTIONS, } from './spawnEnv.js';
37
+ export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_NAME_TOKENS, SENSITIVE_ENV_PREFIX_EXCEPTIONS, LOOPBACK_NO_PROXY_HOSTS, envHasProxy, withLoopbackNoProxy, } from './spawnEnv.js';
38
+ export { loopbackFetch, assertLoopbackHttpUrl, isLoopbackHostname, isLoopbackAddress, LoopbackTargetRefusedError, LOOPBACK_DEFAULT_MAX_BODY_BYTES, type LoopbackFetch, type LoopbackFetchInit, } from './loopbackHttp.js';
37
39
  export { CONFIG_DIR_NAME, LEGACY_CONFIG_DIR_NAMES, NATIVE_ID_PREFIX, LEGACY_NATIVE_ID_PREFIXES, DEFAULT_BINARY_NAME, } from './localIdentity.js';
38
40
  export { findLegacyLocalState, describeLegacyState, type LegacyStateFinding, type LegacyStateReport, } from './legacyStatePreflight.js';
39
41
  export * from './firstPartyHarness.js';
@@ -118,7 +118,12 @@ export {
118
118
  initDeviceIdentity, resetDeviceIdentity, signServerRequest, enrollWithServer, clearEnrollmentMarker, isDeviceIdentityActive, getDevicePublicKey, describeDeviceIdentityState,
119
119
  // Test seams — file path resolvers + state setters for spec files.
120
120
  __publicKeyPathForTest, __privateKeyPathForTest, __enrollmentMarkerPathForTest, __enrollmentBackoffPathForTest, __setStateForTest as __setDeviceIdentityStateForTest, __getStateForTest as __getDeviceIdentityStateForTest, } from './deviceIdentityBridge.js';
121
+ // Hardware machine identity sent as `machineUuid` (IOPlatformUUID /
122
+ // machine-id / MachineGuid). ONE implementation for daemon and CLI —
123
+ // OSK-12063. Also exposed dependency-free as `@skrr-ai/auth-core/machine-uuid`.
124
+ export { getMachineUuid, hostedRuntimeIdOverride, HOSTED_RUNTIME_MACHINE_SOURCES, __resetMachineUuidCacheForTest, } from './machineUuid.js';
121
125
  // Stable per-machine UUIDv7 identity persisted at <configDir>/daemon.id.
126
+ // NOT a `machineUuid` — see machineUuid.ts.
122
127
  export { ensureMachineDaemonId, legacyDaemonUuids, __resetCachedDaemonIdForTest, } from './machineId.js';
123
128
  // Tier 1 #2 — daemon QR pairing primitives. Pure crypto + AAD framing,
124
129
  // shared between the daemon CLI orchestrator (`daemon/src/pair.ts`) and
@@ -140,7 +145,11 @@ export { HARNESSES_LOADING_CLAUDE_SKILLS_LAYOUT, claudeSkillsLayoutEvidence, har
140
145
  // spawns the SAME engine and did not). `daemon/src/env-sanitize.ts` re-exports
141
146
  // these so its own importers are unchanged, and keeps `filterCustomEnv` — that
142
147
  // governs SERVER-PROVIDED env, which no other process is handed.
143
- export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_NAME_TOKENS, SENSITIVE_ENV_PREFIX_EXCEPTIONS, } from './spawnEnv.js';
148
+ export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_NAME_TOKENS, SENSITIVE_ENV_PREFIX_EXCEPTIONS, LOOPBACK_NO_PROXY_HOSTS, envHasProxy, withLoopbackNoProxy, } from './spawnEnv.js';
149
+ // OSK-12134 — the one proxy-free client for loopback HTTP. Bun's fetch (and
150
+ // Node's under NODE_USE_ENV_PROXY) sends 127.0.0.1 requests to HTTP_PROXY,
151
+ // credentials and all; every loopback caller goes through this instead.
152
+ export { loopbackFetch, assertLoopbackHttpUrl, isLoopbackHostname, isLoopbackAddress, LoopbackTargetRefusedError, LOOPBACK_DEFAULT_MAX_BODY_BYTES, } from './loopbackHttp.js';
144
153
  /* Local-machine identity — the names this product leaves on a disk. Exported so
145
154
  the CLI, daemon and Desktop share one definition instead of 47 literals. */
146
155
  export { CONFIG_DIR_NAME, LEGACY_CONFIG_DIR_NAMES, NATIVE_ID_PREFIX, LEGACY_NATIVE_ID_PREFIXES, DEFAULT_BINARY_NAME, } from './localIdentity.js';