@skrr-ai/cli 0.1.49 → 0.1.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +5 -4
  2. package/dist/base-command.js +75 -6
  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 +14 -7
  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 +9 -0
  9. package/dist/commands/agents/update.js +38 -5
  10. package/dist/commands/balance/show.d.ts +34 -0
  11. package/dist/commands/balance/show.js +92 -2
  12. package/dist/commands/code/index.d.ts +1 -0
  13. package/dist/commands/code/index.js +49 -5
  14. package/dist/commands/code/run.js +5 -1
  15. package/dist/commands/commitments/cycles.d.ts +19 -0
  16. package/dist/commands/commitments/cycles.js +46 -0
  17. package/dist/commands/commitments/doctor.d.ts +1 -0
  18. package/dist/commands/commitments/doctor.js +19 -4
  19. package/dist/commands/commitments/effective-policy.js +40 -1
  20. package/dist/commands/commitments/explain.d.ts +17 -0
  21. package/dist/commands/commitments/explain.js +40 -0
  22. package/dist/commands/commitments/next-tick.js +3 -1
  23. package/dist/commands/commitments/preflight.js +10 -0
  24. package/dist/commands/commitments/show.js +11 -1
  25. package/dist/commands/daemon/config.d.ts +15 -0
  26. package/dist/commands/daemon/config.js +38 -0
  27. package/dist/commands/daemon/index.js +1 -0
  28. package/dist/commands/daemon/status.js +15 -1
  29. package/dist/commands/goals/create.d.ts +1 -0
  30. package/dist/commands/goals/create.js +22 -0
  31. package/dist/commands/harnesses/models.d.ts +54 -0
  32. package/dist/commands/harnesses/models.js +100 -7
  33. package/dist/commands/harnesses/show.d.ts +11 -0
  34. package/dist/commands/harnesses/show.js +35 -0
  35. package/dist/commands/harnesses/update.d.ts +28 -1
  36. package/dist/commands/harnesses/update.js +73 -10
  37. package/dist/commands/logout.js +12 -1
  38. package/dist/commands/machines/dedicated/index.js +1 -1
  39. package/dist/commands/machines/dedicated/sign-in.js +4 -1
  40. package/dist/commands/machines/dedicated/terminal/kill.d.ts +21 -0
  41. package/dist/commands/machines/dedicated/terminal/kill.js +55 -0
  42. package/dist/commands/machines/dedicated/terminal/ls.d.ts +22 -0
  43. package/dist/commands/machines/dedicated/terminal/ls.js +70 -0
  44. package/dist/commands/machines/dedicated/terminal/rename.d.ts +23 -0
  45. package/dist/commands/machines/dedicated/terminal/rename.js +64 -0
  46. package/dist/commands/machines/dedicated/terminal.d.ts +9 -0
  47. package/dist/commands/machines/dedicated/terminal.js +27 -2
  48. package/dist/commands/machines/hosted/connect.js +2 -2
  49. package/dist/commands/machines/hosted/destroy.js +1 -1
  50. package/dist/commands/machines/hosted/exec.js +1 -1
  51. package/dist/commands/machines/hosted/list.js +1 -1
  52. package/dist/commands/machines/hosted/pause.js +1 -1
  53. package/dist/commands/machines/hosted/pull.js +1 -1
  54. package/dist/commands/machines/hosted/resume.js +1 -1
  55. package/dist/commands/machines/hosted/start.js +2 -2
  56. package/dist/commands/machines/hosted/status.js +2 -1
  57. package/dist/commands/outbox/list.d.ts +12 -1
  58. package/dist/commands/outbox/list.js +31 -2
  59. package/dist/commands/revoke-daemon.js +9 -0
  60. package/dist/commands/spaces/create.d.ts +1 -0
  61. package/dist/commands/spaces/create.js +17 -0
  62. package/dist/commands/spaces/show.js +5 -0
  63. package/dist/commands/spaces/update.d.ts +1 -0
  64. package/dist/commands/spaces/update.js +16 -0
  65. package/dist/commands/tasks/create.d.ts +1 -0
  66. package/dist/commands/tasks/create.js +6 -0
  67. package/dist/commands/tasks/list.js +1 -0
  68. package/dist/commands/tasks/show.js +45 -0
  69. package/dist/commands/tasks/update.d.ts +1 -0
  70. package/dist/commands/tasks/update.js +22 -0
  71. package/dist/commands/tasks/upsert.d.ts +1 -0
  72. package/dist/commands/tasks/upsert.js +16 -0
  73. package/dist/lib/agent-config.d.ts +2 -0
  74. package/dist/lib/agent-config.js +3 -1
  75. package/dist/lib/agentic-stream.d.ts +68 -1
  76. package/dist/lib/agentic-stream.js +294 -7
  77. package/dist/lib/api-fetch.js +19 -0
  78. package/dist/lib/auth-core-init.d.ts +2 -10
  79. package/dist/lib/auth-core-init.js +20 -67
  80. package/dist/lib/auth-failure.d.ts +33 -0
  81. package/dist/lib/auth-failure.js +65 -0
  82. package/dist/lib/auth-storage.d.ts +14 -0
  83. package/dist/lib/auth-storage.js +14 -0
  84. package/dist/lib/commitments.d.ts +37 -2
  85. package/dist/lib/commitments.js +148 -0
  86. package/dist/lib/daemonBroker.d.ts +134 -31
  87. package/dist/lib/daemonBroker.js +376 -31
  88. package/dist/lib/dedicated-lease-command.d.ts +14 -0
  89. package/dist/lib/dedicated-lease-command.js +30 -1
  90. package/dist/lib/dedicated-machines.js +8 -25
  91. package/dist/lib/dedicated-service-command.d.ts +11 -3
  92. package/dist/lib/dedicated-service-command.js +20 -3
  93. package/dist/lib/dedicated-service.d.ts +46 -0
  94. package/dist/lib/dedicated-service.js +85 -7
  95. package/dist/lib/dedicated-terminal.d.ts +21 -0
  96. package/dist/lib/dedicated-terminal.js +104 -9
  97. package/dist/lib/delegated-cli.js +4 -1
  98. package/dist/lib/exec-runtime-binary.d.ts +3 -1
  99. package/dist/lib/exec-runtime-binary.js +4 -2
  100. package/dist/lib/first-party-harness-broker.d.ts +10 -0
  101. package/dist/lib/first-party-harness-broker.js +13 -1
  102. package/dist/lib/first-party-harness-doctor.js +41 -1
  103. package/dist/lib/first-party-harness-managed.d.ts +4 -3
  104. package/dist/lib/first-party-harness-project-trust.d.ts +125 -0
  105. package/dist/lib/first-party-harness-project-trust.js +364 -0
  106. package/dist/lib/first-party-harness.d.ts +9 -2
  107. package/dist/lib/first-party-harness.js +6 -5
  108. package/dist/lib/harnesses.d.ts +10 -0
  109. package/dist/lib/hosted-machines.d.ts +10 -1
  110. package/dist/lib/hosted-machines.js +36 -2
  111. package/dist/lib/live-runtime-binary.d.ts +40 -0
  112. package/dist/lib/live-runtime-binary.js +193 -0
  113. package/dist/lib/login.d.ts +23 -0
  114. package/dist/lib/login.js +60 -10
  115. package/dist/lib/machine-spend-cap.d.ts +23 -0
  116. package/dist/lib/machine-spend-cap.js +68 -0
  117. package/dist/lib/message-intent.js +3 -1
  118. package/dist/lib/node-adapter.js +15 -3
  119. package/dist/lib/tasks.d.ts +20 -0
  120. package/dist/lib/tasks.js +54 -0
  121. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +90 -0
  122. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +113 -0
  123. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +2 -0
  124. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +26 -1
  125. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.d.ts +47 -0
  126. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.js +69 -0
  127. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +133 -0
  128. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +189 -0
  129. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +41 -3
  130. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
  131. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +25 -3
  132. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.d.ts +32 -0
  133. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.js +268 -0
  134. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.d.ts +2 -2
  135. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +11 -10
  136. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +36 -0
  137. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +156 -0
  138. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.d.ts +8 -3
  139. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +9 -4
  140. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +8 -0
  141. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +16 -10
  142. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.d.ts +6 -0
  143. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.js +32 -9
  144. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.d.ts +81 -0
  145. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.js +87 -0
  146. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +22 -0
  147. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +61 -2
  148. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/types.d.ts +6 -1
  149. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +90 -0
  150. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +105 -0
  151. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +2 -0
  152. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +25 -1
  153. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.d.ts +47 -0
  154. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.js +66 -0
  155. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +133 -0
  156. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +182 -0
  157. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +41 -3
  158. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
  159. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
  160. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.d.ts +32 -0
  161. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.js +257 -0
  162. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.d.ts +2 -2
  163. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +11 -10
  164. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +36 -0
  165. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +147 -0
  166. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.d.ts +8 -3
  167. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +9 -4
  168. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +8 -0
  169. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +16 -10
  170. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.d.ts +6 -0
  171. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.js +31 -9
  172. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.d.ts +81 -0
  173. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.js +80 -0
  174. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +22 -0
  175. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +58 -1
  176. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/types.d.ts +6 -1
  177. package/dist/node_modules/@skrr-ai/auth-core/package.json +61 -1
  178. package/dist/node_modules/@skrr-ai/data-provider/index.js +3418 -3344
  179. package/oclif.manifest.json +24813 -24242
  180. package/package.json +4 -1
@@ -0,0 +1,268 @@
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.loopbackFetch = exports.LoopbackTargetRefusedError = exports.LOOPBACK_DEFAULT_MAX_BODY_BYTES = void 0;
7
+ exports.isLoopbackAddress = isLoopbackAddress;
8
+ exports.isLoopbackHostname = isLoopbackHostname;
9
+ exports.assertLoopbackHttpUrl = assertLoopbackHttpUrl;
10
+ /**
11
+ * OSK-12134 — the ONE HTTP client for talking to a process on this machine.
12
+ *
13
+ * ── Why this is not `fetch` ────────────────────────────────────────────────
14
+ *
15
+ * The compiled daemon runs on Bun, and Bun's `fetch` routes EVERY URL through
16
+ * `HTTP_PROXY` / `HTTPS_PROXY` unless `NO_PROXY` names it — a `127.0.0.1`
17
+ * target included. Node's `fetch` does the same once `NODE_USE_ENV_PROXY=1` is
18
+ * set (Node 22.21+/24). On a laptop with a local proxy exported in the shell
19
+ * (Clash, Surge, a corporate agent, …) every loopback call the daemon, a
20
+ * wrapper, or the CLI made with `fetch` was delivered to that proxy instead —
21
+ * WITH its credential: the daemon's local secret, the CLI-handoff descriptor
22
+ * secret, the wrapper fan-out bearer, the browser bridge token, the extension
23
+ * relay token. OSK-12087 found this on the `/health` probe and moved that one
24
+ * caller to `node:http`; this module is that fix made the only way to do it.
25
+ *
26
+ * `node:http` never consults proxy environment variables, on Node or on Bun, so
27
+ * a request made through it cannot be routed off the machine by environment.
28
+ *
29
+ * ── What it refuses ────────────────────────────────────────────────────────
30
+ *
31
+ * Only `http:` URLs whose host is a loopback name or address are accepted
32
+ * (`127.0.0.0/8`, `::1`, `localhost`). Anything else throws BEFORE a socket is
33
+ * opened: a caller that builds its URL from a descriptor or an environment
34
+ * variable must not be turned into an SSRF primitive by a hostile value, and a
35
+ * credential that is only meant for this machine must not be sent anywhere
36
+ * else. `localhost` is resolved through a lookup that rejects any non-loopback
37
+ * answer, so a rewritten hosts file cannot redirect it either.
38
+ *
39
+ * ── Shape ──────────────────────────────────────────────────────────────────
40
+ *
41
+ * `loopbackFetch` is a drop-in for the subset of `fetch` these callers use —
42
+ * method, headers, a string/bytes body, an `AbortSignal` — and returns a real
43
+ * `Response`, so existing callers (and their `fetchImpl` test seams) change one
44
+ * identifier. Transport failures reject with `TypeError('fetch failed')` whose
45
+ * `cause` carries the socket error (`ECONNREFUSED`, …), and aborts reject with
46
+ * the signal's reason, exactly as `fetch` does, so existing error handling keeps
47
+ * working. The response body is buffered (bounded by `maxBodyBytes`); nothing
48
+ * on loopback streams through this client.
49
+ */
50
+ const node_dns_1 = __importDefault(require("node:dns"));
51
+ const node_http_1 = __importDefault(require("node:http"));
52
+ const node_net_1 = __importDefault(require("node:net"));
53
+ /** Loopback host names this client will connect to, besides 127/8 and ::1 literals. */
54
+ const LOOPBACK_NAMES = new Set(['localhost']);
55
+ /** Upper bound on a buffered response body. Loopback control-plane bodies are small. */
56
+ exports.LOOPBACK_DEFAULT_MAX_BODY_BYTES = 16 * 1024 * 1024;
57
+ /** Statuses a `Response` must be constructed with a null body for. */
58
+ const NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);
59
+ /** Thrown (synchronously surfaced as a rejection) for a URL that is not loopback HTTP. */
60
+ class LoopbackTargetRefusedError extends Error {
61
+ code = 'LOOPBACK_TARGET_REFUSED';
62
+ constructor(message) {
63
+ super(message);
64
+ this.name = 'LoopbackTargetRefusedError';
65
+ }
66
+ }
67
+ exports.LoopbackTargetRefusedError = LoopbackTargetRefusedError;
68
+ function stripBrackets(hostname) {
69
+ return hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;
70
+ }
71
+ /** True for an IP literal in 127.0.0.0/8, `::1`, or an IPv4-mapped 127/8 address. */
72
+ function isLoopbackAddress(address) {
73
+ const value = stripBrackets(address.trim().toLowerCase());
74
+ if (node_net_1.default.isIPv4(value))
75
+ return value.split('.')[0] === '127';
76
+ if (node_net_1.default.isIPv6(value)) {
77
+ if (value === '::1' || value === '0:0:0:0:0:0:0:1')
78
+ return true;
79
+ // IPv4-mapped (::ffff:127.0.0.1, which WHATWG URL normalizes to ::ffff:7f00:1).
80
+ if (value.startsWith('::ffff:')) {
81
+ const rest = value.slice('::ffff:'.length);
82
+ if (node_net_1.default.isIPv4(rest))
83
+ return isLoopbackAddress(rest);
84
+ const high = /^([0-9a-f]{1,4}):[0-9a-f]{1,4}$/.exec(rest);
85
+ return high ? parseInt(high[1], 16) >> 8 === 127 : false;
86
+ }
87
+ return false;
88
+ }
89
+ return false;
90
+ }
91
+ /** True for a host name or address this client accepts as "this machine". */
92
+ function isLoopbackHostname(hostname) {
93
+ const value = stripBrackets(hostname.trim().toLowerCase());
94
+ return LOOPBACK_NAMES.has(value) || isLoopbackAddress(value);
95
+ }
96
+ /**
97
+ * Parse and validate a loopback HTTP URL. Throws {@link LoopbackTargetRefusedError}
98
+ * for anything that is not `http:` to a loopback host.
99
+ */
100
+ function assertLoopbackHttpUrl(input) {
101
+ let url;
102
+ try {
103
+ url = new URL(String(input));
104
+ }
105
+ catch {
106
+ throw new LoopbackTargetRefusedError('Loopback request refused: not a valid URL.');
107
+ }
108
+ if (url.protocol !== 'http:') {
109
+ throw new LoopbackTargetRefusedError(`Loopback request refused: only http: is allowed on loopback (got ${url.protocol}).`);
110
+ }
111
+ if (!isLoopbackHostname(url.hostname)) {
112
+ throw new LoopbackTargetRefusedError(`Loopback request refused: ${url.hostname} is not a loopback host.`);
113
+ }
114
+ if (url.username || url.password) {
115
+ throw new LoopbackTargetRefusedError('Loopback request refused: URL must not carry userinfo.');
116
+ }
117
+ return url;
118
+ }
119
+ /** A `dns.lookup` that only ever answers with a loopback address. */
120
+ const loopbackOnlyLookup = (hostname, options, callback) => {
121
+ node_dns_1.default.lookup(hostname, { ...options, all: true }, (err, addresses) => {
122
+ if (err) {
123
+ callback(err, '', 0);
124
+ return;
125
+ }
126
+ const list = (Array.isArray(addresses) ? addresses : []);
127
+ const loopback = list.filter((entry) => isLoopbackAddress(entry.address));
128
+ if (loopback.length === 0) {
129
+ const refused = Object.assign(new Error(`${hostname} did not resolve to a loopback address`), { code: 'LOOPBACK_TARGET_REFUSED' });
130
+ callback(refused, '', 0);
131
+ return;
132
+ }
133
+ if (options?.all) {
134
+ callback(null, loopback);
135
+ return;
136
+ }
137
+ callback(null, loopback[0].address, loopback[0].family);
138
+ });
139
+ };
140
+ function abortReason(signal) {
141
+ return signal.reason !== undefined
142
+ ? signal.reason
143
+ : new DOMException('This operation was aborted', 'AbortError');
144
+ }
145
+ function fetchFailed(cause) {
146
+ return new TypeError('fetch failed', { cause });
147
+ }
148
+ function toBodyBuffer(body) {
149
+ if (body === undefined || body === null)
150
+ return null;
151
+ if (typeof body === 'string')
152
+ return Buffer.from(body, 'utf8');
153
+ if (body instanceof ArrayBuffer)
154
+ return Buffer.from(body);
155
+ if (ArrayBuffer.isView(body))
156
+ return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
157
+ throw new TypeError('loopbackFetch supports only string or byte bodies');
158
+ }
159
+ /**
160
+ * `fetch` for a loopback URL, over `node:http`, never through a proxy.
161
+ * Refuses (rejects with {@link LoopbackTargetRefusedError}) any other target.
162
+ */
163
+ const loopbackFetch = (input, init = {}) => {
164
+ let url;
165
+ let payload;
166
+ try {
167
+ url = assertLoopbackHttpUrl(input);
168
+ payload = toBodyBuffer(init.body);
169
+ }
170
+ catch (err) {
171
+ return Promise.reject(err);
172
+ }
173
+ const signal = init.signal ?? undefined;
174
+ if (signal?.aborted)
175
+ return Promise.reject(abortReason(signal));
176
+ const maxBodyBytes = init.maxBodyBytes ?? exports.LOOPBACK_DEFAULT_MAX_BODY_BYTES;
177
+ const method = (init.method ?? 'GET').toUpperCase();
178
+ const headers = {};
179
+ new Headers(init.headers ?? undefined).forEach((value, name) => {
180
+ headers[name] = value;
181
+ });
182
+ if (payload && headers['content-length'] === undefined) {
183
+ headers['content-length'] = String(payload.length);
184
+ }
185
+ return new Promise((resolve, reject) => {
186
+ let settled = false;
187
+ let req;
188
+ const onAbort = () => {
189
+ if (settled)
190
+ return;
191
+ settled = true;
192
+ req?.destroy();
193
+ reject(abortReason(signal));
194
+ };
195
+ const finish = (fn) => {
196
+ if (settled)
197
+ return;
198
+ settled = true;
199
+ signal?.removeEventListener('abort', onAbort);
200
+ fn();
201
+ };
202
+ signal?.addEventListener('abort', onAbort, { once: true });
203
+ try {
204
+ req = node_http_1.default.request({
205
+ protocol: 'http:',
206
+ host: stripBrackets(url.hostname),
207
+ port: url.port ? Number(url.port) : 80,
208
+ path: `${url.pathname}${url.search}`,
209
+ method,
210
+ headers,
211
+ // No pooled agent: a short-lived command must be able to exit as soon
212
+ // as it has its answer, and no socket outlives its request.
213
+ agent: false,
214
+ lookup: loopbackOnlyLookup,
215
+ }, (res) => {
216
+ const chunks = [];
217
+ let size = 0;
218
+ res.on('data', (chunk) => {
219
+ size += chunk.length;
220
+ if (size > maxBodyBytes) {
221
+ res.destroy();
222
+ finish(() => reject(fetchFailed(new Error(`response larger than ${maxBodyBytes} bytes`))));
223
+ return;
224
+ }
225
+ chunks.push(chunk);
226
+ });
227
+ res.on('error', (err) => finish(() => reject(fetchFailed(err))));
228
+ res.on('aborted', () => finish(() => reject(fetchFailed(new Error('response aborted')))));
229
+ res.on('end', () => {
230
+ finish(() => {
231
+ const status = res.statusCode ?? 0;
232
+ const responseHeaders = new Headers();
233
+ for (const [name, value] of Object.entries(res.headers)) {
234
+ if (value === undefined)
235
+ continue;
236
+ if (Array.isArray(value)) {
237
+ for (const v of value)
238
+ responseHeaders.append(name, v);
239
+ }
240
+ else {
241
+ responseHeaders.set(name, String(value));
242
+ }
243
+ }
244
+ const body = NULL_BODY_STATUSES.has(status) ? null : Buffer.concat(chunks);
245
+ try {
246
+ resolve(new Response(body, {
247
+ status,
248
+ statusText: res.statusMessage ?? '',
249
+ headers: responseHeaders,
250
+ }));
251
+ }
252
+ catch (err) {
253
+ reject(fetchFailed(err));
254
+ }
255
+ });
256
+ });
257
+ });
258
+ req.on('error', (err) => finish(() => reject(fetchFailed(err))));
259
+ if (payload)
260
+ req.write(payload);
261
+ req.end();
262
+ }
263
+ catch (err) {
264
+ finish(() => reject(fetchFailed(err)));
265
+ }
266
+ });
267
+ };
268
+ exports.loopbackFetch = loopbackFetch;
@@ -10,8 +10,8 @@
10
10
  * with a real UUIDv7 downstream.
11
11
  *
12
12
  * The caller may pass `machineUuidFallback` to override the default
13
- * hostname+platform fingerprint with a richer detector (e.g. the
14
- * daemon's `getMachineUuid()` from `registry.ts`).
13
+ * hostname+platform fingerprint with a richer detector (e.g.
14
+ * `getMachineUuid()` from `machineUuid.ts`).
15
15
  */
16
16
  export declare function ensureMachineDaemonId(machineUuidFallback?: () => string): string;
17
17
  /**
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- export type AuthFailureReason = 'REFRESH_INVALID' | 'REFRESH_REUSED' | 'REFRESH_EXPIRED' | 'SESSION_REVOKED' | 'TOKEN_EXPIRED' | 'TOKEN_INVALID' | 'UNKNOWN_401' | 'UNKNOWN';
1
+ export type AuthFailureReason = 'REFRESH_INVALID' | 'REFRESH_REUSED' | 'REFRESH_EXPIRED' | 'SESSION_REVOKED' | 'DEVICE_KEY_MISMATCH' | 'TOKEN_EXPIRED' | 'TOKEN_INVALID' | 'UNKNOWN_401' | 'UNKNOWN';
2
2
  /**
3
3
  * The command that recovers this process from a re-auth latch.
4
4
  *
@@ -11,8 +11,13 @@ export type AuthFailureReason = 'REFRESH_INVALID' | 'REFRESH_REUSED' | 'REFRESH_
11
11
  * something the other did not describe (OSK-10224).
12
12
  *
13
13
  * The binary is whatever the consumer registered with
14
- * `configureAuthCore({ binaryName })` — the binary the user actually typed —
15
- * and `--device` is added only when there is no TTY to open a browser from.
14
+ * `configureAuthCore({ binaryName })` — the binary the user actually typed.
15
+ *
16
+ * No flag. This used to append `--device` whenever stdout was not a TTY, which
17
+ * is always true for a daemon under launchd/systemd — and neither `skrrd login`
18
+ * nor `skrr login` has a `--device` option, so the one command the latch line
19
+ * named failed with `unknown option '--device'` (OSK-12056). Both logins already
20
+ * fall back to a paste-a-code arm when no browser can be opened.
16
21
  */
17
22
  export declare function reauthCommand(): string;
18
23
  export declare function formatReauthMessage(reason: AuthFailureReason | string): string;
@@ -17,6 +17,7 @@ const REASON_HUMAN = {
17
17
  REFRESH_REUSED: 'Your session was ended for security reasons (token replay detected).',
18
18
  REFRESH_EXPIRED: 'Your session has expired.',
19
19
  SESSION_REVOKED: 'Your session was ended (you signed out elsewhere or your password changed).',
20
+ DEVICE_KEY_MISMATCH: "This computer's device key no longer matches the one skrr registered for it, so its session cannot be renewed.",
20
21
  TOKEN_EXPIRED: 'Your access token has expired.',
21
22
  TOKEN_INVALID: 'Your saved credentials are no longer valid.',
22
23
  UNKNOWN_401: 'skrr needs you to sign in again.',
@@ -34,12 +35,16 @@ const REASON_HUMAN = {
34
35
  * something the other did not describe (OSK-10224).
35
36
  *
36
37
  * The binary is whatever the consumer registered with
37
- * `configureAuthCore({ binaryName })` — the binary the user actually typed —
38
- * and `--device` is added only when there is no TTY to open a browser from.
38
+ * `configureAuthCore({ binaryName })` — the binary the user actually typed.
39
+ *
40
+ * No flag. This used to append `--device` whenever stdout was not a TTY, which
41
+ * is always true for a daemon under launchd/systemd — and neither `skrrd login`
42
+ * nor `skrr login` has a `--device` option, so the one command the latch line
43
+ * named failed with `unknown option '--device'` (OSK-12056). Both logins already
44
+ * fall back to a paste-a-code arm when no browser can be opened.
39
45
  */
40
46
  function reauthCommand() {
41
- const bin = (0, runtime_js_1.getAuthBinaryName)();
42
- return process.stdout.isTTY ? `${bin} login` : `${bin} login --device`;
47
+ return `${(0, runtime_js_1.getAuthBinaryName)()} login`;
43
48
  }
44
49
  function formatReauthMessage(reason) {
45
50
  const norm = REASON_HUMAN[reason] || REASON_HUMAN.UNKNOWN;
@@ -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;
@@ -41,6 +41,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
41
41
  const node_path_1 = __importDefault(require("node:path"));
42
42
  const proper_lockfile_1 = __importDefault(require("proper-lockfile"));
43
43
  const runtime_js_1 = require("./runtime.js");
44
+ const refreshClassification_js_1 = require("./refreshClassification.js");
44
45
  const types_js_1 = require("./types.js");
45
46
  /**
46
47
  * P1-6: Reduced from 30 s to 5 s.
@@ -820,14 +821,12 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
820
821
  catch {
821
822
  /* ignore */
822
823
  }
823
- const code = parsedBody.code;
824
- const permanent = [
825
- 'REFRESH_INVALID',
826
- 'REFRESH_REUSED',
827
- 'REFRESH_EXPIRED',
828
- 'SESSION_REVOKED',
829
- ];
830
- if (code && permanent.includes(code)) {
824
+ // The server's code, mapped through the one table the stateless classifier
825
+ // reads too — so a permanent refusal that is not spelled as a reason name
826
+ // (DEVICE_PROOF_THUMBPRINT_MISMATCH) latches instead of riding the
827
+ // unknown-401 budget forever (OSK-12056).
828
+ const code = (0, refreshClassification_js_1.permanentReasonForRefreshCode)(parsedBody.code);
829
+ if (code) {
831
830
  // A KNOWN credential-death code is genuinely permanent — latch
832
831
  // immediately, exactly as before. It is NOT routed through the
833
832
  // unknown-401 budget.
@@ -857,7 +856,7 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
857
856
  }
858
857
  // Unknown-shape 401 — unknown code, missing code, or unparseable body.
859
858
  // By construction this is NOT a daemon's actual refresh-family burn
860
- // (those arrive as one of the four KNOWN codes above); it most likely
859
+ // (those arrive as one of the KNOWN codes above); it most likely
861
860
  // came from infra OverSky does not fully control — a proxy/middleware
862
861
  // regression, a clock-skew replay rejection (DEVICE_PROOF_REPLAYED),
863
862
  // or a half-rolled-out server route.
@@ -970,7 +969,14 @@ function needsReauthState() {
970
969
  return { needed: false };
971
970
  const raw = node_fs_1.default.readFileSync(flagPath, 'utf-8');
972
971
  const state = JSON.parse(raw);
973
- 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
+ };
974
980
  }
975
981
  catch {
976
982
  return { needed: false };
@@ -36,6 +36,12 @@ export interface ClassifiedRefreshFailure {
36
36
  failure: PermanentAuthFailure | TransientAuthFailure;
37
37
  }
38
38
  export type ClassifiedRefreshResult = ClassifiedRefreshSuccess | ClassifiedRefreshFailure;
39
+ /**
40
+ * The permanent reason a 401 refresh refusal's `code` names, or `null` when the
41
+ * code is missing or unrecognised (an unknown-shape 401). The ONE mapping both
42
+ * this classifier and the daemon's `refresh.ts` read.
43
+ */
44
+ export declare function permanentReasonForRefreshCode(code: string | undefined | null): PermanentAuthReason | null;
39
45
  /**
40
46
  * Classify an HTTP response from /api/auth/refresh (or /api/daemons/token/refresh)
41
47
  * into a discriminated union of success or failure.