@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
@@ -77,6 +77,7 @@ exports.resolveBootstrapPath = resolveBootstrapPath;
77
77
  exports.resolveProfileBootstrapPaths = resolveProfileBootstrapPaths;
78
78
  exports.resolveBootstrapCandidatePaths = resolveBootstrapCandidatePaths;
79
79
  exports.resolveCliHandoffCandidatePaths = resolveCliHandoffCandidatePaths;
80
+ exports.resolveExactProfileBootstrapPaths = resolveExactProfileBootstrapPaths;
80
81
  exports.findLiveDaemonBootstrap = findLiveDaemonBootstrap;
81
82
  exports.readBootstrap = readBootstrap;
82
83
  exports.attemptDaemonBrokerLogin = attemptDaemonBrokerLogin;
@@ -91,10 +92,11 @@ const fs = __importStar(require("node:fs"));
91
92
  const path = __importStar(require("node:path"));
92
93
  const os = __importStar(require("node:os"));
93
94
  const auth_core_1 = require("@skrr-ai/auth-core");
95
+ const loopback_http_1 = require("@skrr-ai/auth-core/loopback-http");
94
96
  const cli_handoff_wire_1 = require("@skrr-ai/auth-core/cli-handoff-wire");
95
97
  const auth_storage_1 = require("./auth-storage");
96
98
  const config_1 = require("./config");
97
- const device_id_1 = require("./device-id");
99
+ const cli_identity_1 = require("./cli-identity");
98
100
  const REQUEST_TIMEOUT_MS = 20_000;
99
101
  /**
100
102
  * Normalize a base URL for equality comparison. Mirrors the daemon-side
@@ -118,6 +120,19 @@ function normalizeBaseUrl(u) {
118
120
  return (0, config_1.canonicalizeKnownBaseURL)(trimmed.toLowerCase());
119
121
  }
120
122
  }
123
+ /**
124
+ * Codes the daemon's legacy `/v1/auth/cli-handoff` route writes itself. Every
125
+ * other coded body on that route is the server's, passed through verbatim.
126
+ */
127
+ const DAEMON_OWN_HANDOFF_CODES = new Set([
128
+ cli_handoff_wire_1.CLI_HANDOFF_ERROR_BROKER_ONLY,
129
+ 'BODY_TOO_LARGE',
130
+ 'BODY_PARSE_FAILED',
131
+ 'CLI_ID_INVALID',
132
+ 'BASE_URL_MISMATCH',
133
+ 'DAEMON_NOT_AUTHED',
134
+ 'UPSTREAM_UNREACHABLE',
135
+ ]);
121
136
  /**
122
137
  * Compute the bootstrap file path. Exported for tests so they can assert
123
138
  * the platform fork without monkeypatching `os.platform()`.
@@ -139,6 +154,16 @@ function resolveProfileBootstrapPaths(primary, entryNames) {
139
154
  .map((name) => path.join(path.dirname(primary), name));
140
155
  return [primary, ...extras];
141
156
  }
157
+ /**
158
+ * Legacy bootstrap directory (darwin): pre-rename daemons wrote under
159
+ * `~/Library/Application Support/OverSky/` while this reader looked under
160
+ * `skrr/` — a drift that silently killed island bootstrapping on macOS
161
+ * (OSK-12057). New daemons dual-write both; old daemons keep writing only
162
+ * `OverSky/`, so these stay consulted as fallbacks.
163
+ */
164
+ function legacyDarwinBootstrapDir() {
165
+ return path.join(os.homedir(), 'Library', 'Application Support', 'OverSky');
166
+ }
142
167
  /**
143
168
  * List the bootstrap files that a Sky CLI may use for a local daemon
144
169
  * handoff. `default` stays first for backwards compatibility; any other
@@ -150,16 +175,31 @@ function resolveProfileBootstrapPaths(primary, entryNames) {
150
175
  */
151
176
  function resolveBootstrapCandidatePaths(profile = 'default') {
152
177
  const primary = resolveBootstrapPath(profile);
153
- if (profile !== 'default')
154
- return [primary];
178
+ if (profile !== 'default') {
179
+ const legacy = process.platform === 'darwin'
180
+ ? [path.join(legacyDarwinBootstrapDir(), `daemon-island.${profile}.json`)]
181
+ : [];
182
+ return [primary, ...legacy];
183
+ }
155
184
  try {
156
185
  if (process.platform === 'darwin') {
157
- const dir = path.dirname(primary);
158
- const entryNames = fs
159
- .readdirSync(dir, { withFileTypes: true })
160
- .filter((entry) => entry.isFile())
161
- .map((entry) => entry.name);
162
- return resolveProfileBootstrapPaths(primary, entryNames);
186
+ const candidates = [primary];
187
+ for (const dir of [path.dirname(primary), legacyDarwinBootstrapDir()]) {
188
+ let entryNames = [];
189
+ try {
190
+ entryNames = fs
191
+ .readdirSync(dir, { withFileTypes: true })
192
+ .filter((entry) => entry.isFile())
193
+ .map((entry) => entry.name);
194
+ }
195
+ catch {
196
+ continue;
197
+ }
198
+ for (const p of resolveProfileBootstrapPaths(path.join(dir, 'daemon-island.default.json'), entryNames)) {
199
+ candidates.push(p);
200
+ }
201
+ }
202
+ return [...new Set(candidates)];
163
203
  }
164
204
  const profilesDir = path.dirname(path.dirname(primary));
165
205
  const extras = fs
@@ -208,6 +248,20 @@ function resolveCliHandoffCandidatePaths(profile = 'default') {
208
248
  function legacyBootstrapPath() {
209
249
  return path.join(os.homedir(), '.skrr', 'daemon-island.json');
210
250
  }
251
+ /**
252
+ * The island files that belong to exactly `profile` — its canonical path and
253
+ * the pre-rename darwin location — and never another profile's. The broker's
254
+ * candidate list for `default` deliberately falls through to any running
255
+ * profile, which is right for borrowing a credential and wrong for naming
256
+ * which daemon a profile's status describes (OSK-12102).
257
+ */
258
+ function resolveExactProfileBootstrapPaths(profile = 'default') {
259
+ const primary = resolveBootstrapPath(profile);
260
+ if (process.platform === 'darwin') {
261
+ return [primary, path.join(legacyDarwinBootstrapDir(), `daemon-island.${profile}.json`)];
262
+ }
263
+ return profile === 'default' ? [primary, legacyBootstrapPath()] : [primary];
264
+ }
211
265
  /**
212
266
  * Resolve the first live daemon bootstrap for a profile. Message-intent
213
267
  * submission reuses the same authenticated loopback discovery as the login
@@ -301,6 +355,7 @@ async function attemptDaemonBrokerLogin(opts) {
301
355
  const usable = [];
302
356
  const unbound = [];
303
357
  let mismatchedBootstrap = null;
358
+ let mismatchedPath = null;
304
359
  for (const candidate of candidates) {
305
360
  const candidateBootstrap = readBootstrap(candidate);
306
361
  if (!candidateBootstrap)
@@ -310,7 +365,10 @@ async function attemptDaemonBrokerLogin(opts) {
310
365
  typeof opts.baseURL === 'string' &&
311
366
  opts.baseURL.length > 0 &&
312
367
  normalizeBaseUrl(opts.baseURL) !== normalizeBaseUrl(candidateBootstrap.serverUrl)) {
313
- mismatchedBootstrap ??= candidateBootstrap;
368
+ if (!mismatchedBootstrap) {
369
+ mismatchedBootstrap = candidateBootstrap;
370
+ mismatchedPath = candidate;
371
+ }
314
372
  continue;
315
373
  }
316
374
  if (typeof candidateBootstrap.serverUrl !== 'string' ||
@@ -326,8 +384,11 @@ async function attemptDaemonBrokerLogin(opts) {
326
384
  return {
327
385
  ok: false,
328
386
  reason: 'base_url_mismatch',
329
- detail: `The skrr background service on this computer is not logged in to ${opts.baseURL}; it is logged in to ${mismatchedBootstrap.serverUrl}.`,
387
+ // What was READ, not a claim about the daemon: the descriptor may be
388
+ // another profile's (OSK-12117).
389
+ detail: `The daemon descriptor ${mismatchedPath ?? ''} is for ${mismatchedBootstrap.serverUrl}, not ${opts.baseURL}.`,
330
390
  daemonServerUrl: mismatchedBootstrap.serverUrl,
391
+ ...(mismatchedPath ? { descriptorPath: mismatchedPath } : {}),
331
392
  };
332
393
  }
333
394
  return {
@@ -346,6 +407,17 @@ async function attemptDaemonBrokerLogin(opts) {
346
407
  let outcome = null;
347
408
  for (const { bootstrap, sourcePath } of usable) {
348
409
  outcome = await brokerThrough(bootstrap, opts, sourcePath);
410
+ if (!outcome.ok) {
411
+ // Say which daemon answered: a guest's refusal has a different repair.
412
+ outcome = {
413
+ ...outcome,
414
+ descriptorPath: outcome.descriptorPath ?? sourcePath,
415
+ ...(bootstrap.daemonId ? { daemonId: bootstrap.daemonId } : {}),
416
+ ...(sourcePath === exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR
417
+ ? { dedicatedGuest: true }
418
+ : {}),
419
+ };
420
+ }
349
421
  if (outcome.ok || outcome.reason !== 'network')
350
422
  return outcome;
351
423
  }
@@ -400,6 +472,13 @@ async function brokerThrough(bootstrap, opts, sourcePath) {
400
472
  // 403 HANDOFF_BROKER_ONLY anyway.
401
473
  if (!offersRefreshFamily)
402
474
  return redeem;
475
+ const minted = await mintRefreshFamilyThrough(bootstrap, opts);
476
+ // Both doors refused: report the one that says more. The legacy route
477
+ // usually carries the server's code itself, but an older daemon's token
478
+ // route may be the only one that named it.
479
+ if (!minted.ok && !minted.code && redeem.code && redeem.reason !== 'network')
480
+ return redeem;
481
+ return minted;
403
482
  }
404
483
  return mintRefreshFamilyThrough(bootstrap, opts);
405
484
  }
@@ -415,7 +494,9 @@ async function redeemCliHandoffToken(bootstrap, opts = {}) {
415
494
  const abortTimer = setTimeout(() => controller.abort(), timeoutMs);
416
495
  let response;
417
496
  try {
418
- response = await fetch(url, {
497
+ // The descriptor secret is a loopback-only credential: never `fetch`, which
498
+ // sends 127.0.0.1 to HTTP_PROXY under Bun and NODE_USE_ENV_PROXY (OSK-12134).
499
+ response = await (0, loopback_http_1.loopbackFetch)(url, {
419
500
  method: 'POST',
420
501
  headers: {
421
502
  Authorization: `Bearer ${bootstrap.secret}`,
@@ -467,6 +548,28 @@ async function redeemCliHandoffToken(bootstrap, opts = {}) {
467
548
  ...(code ? { code } : {}),
468
549
  };
469
550
  }
551
+ // A daemon that names the SERVER's refusal of its own mint carries
552
+ // `upstreamCode` / `upstreamStatus` beside the 503 (older daemons omit them). Surface the
553
+ // server's code — `DEVICE_PROOF_PIN_MISMATCH` has a specific repair that
554
+ // `HANDOFF_TOKEN_UNAVAILABLE` cannot point at (OSK-12149).
555
+ const upstreamCode = typeof parsedBody?.upstreamCode === 'string'
556
+ ? parsedBody.upstreamCode
557
+ : undefined;
558
+ const upstreamStatus = typeof parsedBody?.upstreamStatus === 'number'
559
+ ? parsedBody.upstreamStatus
560
+ : undefined;
561
+ if (upstreamCode || upstreamStatus !== undefined) {
562
+ return {
563
+ ok: false,
564
+ reason: 'upstream_refused',
565
+ refusedBy: 'server',
566
+ detail: message,
567
+ ...(upstreamStatus !== undefined
568
+ ? { status: upstreamStatus }
569
+ : { status: response.status }),
570
+ ...(upstreamCode ? { code: upstreamCode } : code ? { code } : {}),
571
+ };
572
+ }
470
573
  return {
471
574
  ok: false,
472
575
  // 503 HANDOFF_TOKEN_UNAVAILABLE lands in server_error with its code
@@ -474,6 +577,7 @@ async function redeemCliHandoffToken(bootstrap, opts = {}) {
474
577
  // upstream unreachable, mint cool-down). That is a daemon outage, not a
475
578
  // decline, and the code keeps it diagnosable.
476
579
  reason: response.status >= 500 ? 'server_error' : 'unknown',
580
+ refusedBy: 'daemon',
477
581
  detail: message,
478
582
  status: response.status,
479
583
  ...(code ? { code } : {}),
@@ -497,7 +601,9 @@ async function mintRefreshFamilyThrough(bootstrap, opts) {
497
601
  const abortTimer = setTimeout(() => controller.abort(), timeoutMs);
498
602
  let response;
499
603
  try {
500
- response = await fetch(url, {
604
+ // The descriptor secret is a loopback-only credential: never `fetch`, which
605
+ // sends 127.0.0.1 to HTTP_PROXY under Bun and NODE_USE_ENV_PROXY (OSK-12134).
606
+ response = await (0, loopback_http_1.loopbackFetch)(url, {
501
607
  method: 'POST',
502
608
  headers: {
503
609
  Authorization: `Bearer ${bootstrap.secret}`,
@@ -558,7 +664,14 @@ async function mintRefreshFamilyThrough(bootstrap, opts) {
558
664
  };
559
665
  }
560
666
  if (response.status === 503 && code === 'DAEMON_NOT_AUTHED') {
561
- return { ok: false, reason: 'daemon_not_authed', detail: message, status: 503, code };
667
+ return {
668
+ ok: false,
669
+ reason: 'daemon_not_authed',
670
+ refusedBy: 'daemon',
671
+ detail: message,
672
+ status: 503,
673
+ code,
674
+ };
562
675
  }
563
676
  // 429 is TRANSIENT and its remedy is time, which makes it the one failure
564
677
  // here that a fall-through cannot substitute for. Left in the `unknown`
@@ -592,12 +705,26 @@ async function mintRefreshFamilyThrough(bootstrap, opts) {
592
705
  ...(daemonServerUrl ? { daemonServerUrl } : {}),
593
706
  };
594
707
  }
708
+ // A coded body the daemon did not write is the server's refusal of the
709
+ // daemon's mint, passed through verbatim (`cliHandoff.ts`). A 4xx one —
710
+ // observed: 401 DEVICE_PROOF_PIN_MISMATCH — is a decision with a named
711
+ // repair, and used to land in `unknown`, where no caller could classify it.
712
+ const refusedBy = code
713
+ ? DAEMON_OWN_HANDOFF_CODES.has(code)
714
+ ? 'daemon'
715
+ : 'server'
716
+ : undefined;
595
717
  return {
596
718
  ok: false,
597
- reason: response.status >= 500 ? 'server_error' : 'unknown',
719
+ reason: response.status >= 500
720
+ ? 'server_error'
721
+ : refusedBy === 'server'
722
+ ? 'upstream_refused'
723
+ : 'unknown',
598
724
  detail: message,
599
725
  status: response.status,
600
726
  ...(code ? { code } : {}),
727
+ ...(refusedBy ? { refusedBy } : {}),
601
728
  };
602
729
  }
603
730
  // Happy path — validate the response shape minimally.
@@ -894,14 +1021,11 @@ async function maybeAutoBroker(opts) {
894
1021
  if (opts.commandId && AUTH_SELF_MANAGED_COMMANDS.has(opts.commandId)) {
895
1022
  return { triggered: false };
896
1023
  }
897
- // Mint a cliId on the fly when missing. Persisted by the caller on
1024
+ // The same decision an interactive login makes (OSK-12156): a named profile
1025
+ // with no stored credential is issued its own id, so a durable hand-off's
1026
+ // confirm cannot retire another profile's session. Persisted by the caller on
898
1027
  // success so the rotation row stays bound across processes.
899
- let cliId = opts.cliConfig.cliId;
900
- let updatedConfig;
901
- if (!cliId) {
902
- cliId = (0, device_id_1.generateCliId)();
903
- updatedConfig = { ...opts.cliConfig, cliId };
904
- }
1028
+ const { cliId, updatedConfig } = (0, cli_identity_1.planLoginCliId)(opts.cliConfig);
905
1029
  const outcome = await attemptDaemonBrokerLoginAndPersist({
906
1030
  cliId,
907
1031
  baseURL: opts.cliConfig.baseURL,
@@ -0,0 +1,51 @@
1
+ /**
2
+ * What to tell a person whose local daemon ANSWERED the CLI hand-off and
3
+ * refused it (OSK-12149, OSK-12151).
4
+ *
5
+ * The auto-broker used to be "fail-quiet": every refusal except a rate limit
6
+ * and a base-URL mismatch was dropped, and the command then said only "Not
7
+ * signed in. Run `skrr login` first." That sentence is true and points at the
8
+ * wrong repair whenever the daemon's refusal is specific — observed: the server
9
+ * refused the daemon's own mint with `401 DEVICE_PROOF_PIN_MISMATCH`, which
10
+ * `skrr daemon login` repairs (an interactive daemon login re-pins its device
11
+ * key), and which on a Dedicated Runtime guest no login the tenant can run
12
+ * repairs at all.
13
+ *
14
+ * One function decides the headline and the remedy so the NOT_SIGNED_IN error,
15
+ * `skrr login`'s progress line and its headless refusal cannot drift apart.
16
+ */
17
+ import type { DaemonBrokerFailure } from './daemonBroker';
18
+ /**
19
+ * True when a daemon ANSWERED the hand-off and refused it — as opposed to no
20
+ * daemon being there to ask (`no_bootstrap`, a stale descriptor nobody listens
21
+ * behind). This is the case the CLI must explain rather than reduce to "Not
22
+ * signed in": the person has a daemon, it said something specific, and that
23
+ * decides whether `skrr login` would even help.
24
+ */
25
+ export declare function isAnsweredBrokerRefusal(outcome: DaemonBrokerFailure): boolean;
26
+ export interface BrokerRefusalDescription {
27
+ /** One sentence: who refused and what they said, with status and code. */
28
+ headline: string;
29
+ /** What to do about it — never a login the reader cannot perform. */
30
+ remedy: string;
31
+ /** True when waiting and retrying is the repair. */
32
+ transient: boolean;
33
+ /** True when the refusing daemon is a Dedicated Runtime guest's. */
34
+ dedicatedGuest: boolean;
35
+ /** Machine-readable twin for `--json` error details. */
36
+ broker: {
37
+ reason: DaemonBrokerFailure['reason'];
38
+ refusedBy?: 'daemon' | 'server';
39
+ status?: number;
40
+ code?: string;
41
+ detail?: string;
42
+ descriptorPath?: string;
43
+ daemonId?: string;
44
+ };
45
+ }
46
+ /**
47
+ * Describe a broker failure for a person, or null when there is nothing to
48
+ * say: no daemon was there to ask, or the failure already has its own message
49
+ * (`base_url_mismatch`, `rate_limited`).
50
+ */
51
+ export declare function describeBrokerRefusal(failure: DaemonBrokerFailure, bin?: string): BrokerRefusalDescription | null;
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isAnsweredBrokerRefusal = isAnsweredBrokerRefusal;
4
+ exports.describeBrokerRefusal = describeBrokerRefusal;
5
+ /**
6
+ * True when a daemon ANSWERED the hand-off and refused it — as opposed to no
7
+ * daemon being there to ask (`no_bootstrap`, a stale descriptor nobody listens
8
+ * behind). This is the case the CLI must explain rather than reduce to "Not
9
+ * signed in": the person has a daemon, it said something specific, and that
10
+ * decides whether `skrr login` would even help.
11
+ */
12
+ function isAnsweredBrokerRefusal(outcome) {
13
+ switch (outcome.reason) {
14
+ case 'no_bootstrap':
15
+ case 'bootstrap_parse':
16
+ case 'network':
17
+ return false;
18
+ case 'stale_bootstrap':
19
+ // A bare 401 is the daemon refusing a descriptor secret that rotated;
20
+ // re-reading the file next time heals it. A coded one is a real answer.
21
+ return Boolean(outcome.code);
22
+ default:
23
+ return true;
24
+ }
25
+ }
26
+ /** Codes whose repair is signing the DAEMON in again (laptop only). */
27
+ const DAEMON_RELOGIN_CODES = new Set([
28
+ 'DEVICE_PROOF_PIN_MISMATCH',
29
+ 'SESSION_REVOKED',
30
+ 'PRINCIPAL_UNRESOLVED',
31
+ 'DAEMON_NOT_AUTHED',
32
+ ]);
33
+ /** Codes whose repair is time. */
34
+ const TRANSIENT_CODES = new Set([
35
+ 'DEVICE_PROOF_VERIFY_UNAVAILABLE',
36
+ 'SERVICE_UNAVAILABLE',
37
+ 'HANDOFF_RATE_LIMITED',
38
+ 'RATE_LIMITED',
39
+ 'UPSTREAM_UNREACHABLE',
40
+ ]);
41
+ function statusAndCode(f) {
42
+ const parts = [typeof f.status === 'number' ? `HTTP ${f.status}` : '', f.code ?? ''].filter(Boolean);
43
+ return parts.length > 0 ? ` (${parts.join(' ')})` : '';
44
+ }
45
+ function sentence(detail) {
46
+ const d = (detail ?? '').trim();
47
+ if (!d)
48
+ return '';
49
+ return /[.!?]$/.test(d) ? ` ${d}` : ` ${d}.`;
50
+ }
51
+ /**
52
+ * Describe a broker failure for a person, or null when there is nothing to
53
+ * say: no daemon was there to ask, or the failure already has its own message
54
+ * (`base_url_mismatch`, `rate_limited`).
55
+ */
56
+ function describeBrokerRefusal(failure, bin = 'skrr') {
57
+ if (!isAnsweredBrokerRefusal(failure))
58
+ return null;
59
+ if (failure.reason === 'base_url_mismatch' || failure.reason === 'rate_limited')
60
+ return null;
61
+ const dedicatedGuest = failure.dedicatedGuest === true;
62
+ const code = failure.code ?? '';
63
+ const who = dedicatedGuest
64
+ ? `This Dedicated Runtime's skrr service${failure.daemonId ? ` (daemon ${failure.daemonId})` : ''}`
65
+ : 'The skrr background service on this computer';
66
+ const refusal = failure.reason === 'timeout'
67
+ ? `${who} did not answer the sign-in hand-off in time.`
68
+ : failure.refusedBy === 'server' || failure.reason === 'upstream_refused'
69
+ ? `${who} asked the skrr server for a credential for this CLI, and the server refused${statusAndCode(failure)}.${sentence(failure.detail)}`
70
+ : `${who} could not hand this CLI a credential${statusAndCode(failure)}.${sentence(failure.detail)}`;
71
+ const transient = failure.reason === 'timeout' ||
72
+ TRANSIENT_CODES.has(code) ||
73
+ (failure.reason === 'server_error' && !DAEMON_RELOGIN_CODES.has(code));
74
+ let remedy;
75
+ if (dedicatedGuest) {
76
+ // Nobody signs in on a guest: the daemon's credential comes from the
77
+ // machine's attested bootstrap, and a tenant `skrr login` would only put a
78
+ // personal credential on a shared machine. Name the machine instead.
79
+ remedy = transient
80
+ ? 'This is usually temporary; retry in a minute. Nobody signs in on a Dedicated Runtime — ' +
81
+ `if it persists, check the machine from your own computer with \`${bin} machines dedicated health-check <lease-id>\` ` +
82
+ `(\`${bin} machines dedicated list\` shows the lease id).`
83
+ : 'Nobody signs in on a Dedicated Runtime: its daemon holds the account. From your own computer, run ' +
84
+ `\`${bin} machines dedicated health-check <lease-id>\` (\`${bin} machines dedicated list\` shows the lease id), ` +
85
+ `and \`${bin} machines dedicated restart <lease-id>\` if the hand-off stays refused.`;
86
+ }
87
+ else if (code === 'DEVICE_PROOF_PIN_MISMATCH') {
88
+ remedy =
89
+ `Run \`${bin} daemon login\` to sign the background service in again — an interactive daemon login re-pins its device key — then retry. ` +
90
+ `\`${bin} login\` signs this CLI in on its own instead.`;
91
+ }
92
+ else if (DAEMON_RELOGIN_CODES.has(code) || failure.reason === 'daemon_not_authed') {
93
+ remedy =
94
+ `Run \`${bin} daemon login\` to sign the background service in again, then retry. ` +
95
+ `\`${bin} login\` signs this CLI in on its own instead.`;
96
+ }
97
+ else if (failure.reason === 'ci_token_refused') {
98
+ remedy =
99
+ `The background service is signed in with a CI token, which cannot sign a person in. Use \`${bin} login\`, ` +
100
+ `or \`${bin} create-token\` for a CI credential.`;
101
+ }
102
+ else if (transient) {
103
+ remedy =
104
+ `This is usually temporary; retry in a minute. \`${bin} daemon status\` and \`${bin} daemon logs\` show the service's side, ` +
105
+ `and \`${bin} login\` signs this CLI in without it.`;
106
+ }
107
+ else {
108
+ remedy =
109
+ `\`${bin} daemon status\` and \`${bin} daemon logs\` show the service's side. ` +
110
+ `\`${bin} login\` signs this CLI in without it.`;
111
+ }
112
+ return {
113
+ headline: refusal,
114
+ remedy,
115
+ transient,
116
+ dedicatedGuest,
117
+ broker: {
118
+ reason: failure.reason,
119
+ ...(failure.refusedBy ? { refusedBy: failure.refusedBy } : {}),
120
+ ...(typeof failure.status === 'number' ? { status: failure.status } : {}),
121
+ ...(failure.code ? { code: failure.code } : {}),
122
+ ...(failure.detail ? { detail: failure.detail } : {}),
123
+ ...(failure.descriptorPath ? { descriptorPath: failure.descriptorPath } : {}),
124
+ ...(failure.daemonId ? { daemonId: failure.daemonId } : {}),
125
+ },
126
+ };
127
+ }
@@ -111,8 +111,17 @@ export declare function describeDedicatedCheckpoint(lease: DedicatedLease | unde
111
111
  * machine to the previous image. Files under the workspace survive both.
112
112
  */
113
113
  export interface DedicatedImageStanding {
114
- /** A newer image exists that has not already failed on this machine. */
114
+ /**
115
+ * A newer APPROVED image exists that has not already failed on this machine.
116
+ * False is not "running the latest daemon": a release no approved image
117
+ * carries yet is not an update this machine can move to (OSK-12153).
118
+ */
115
119
  updateAvailable?: boolean;
120
+ /**
121
+ * The daemon the approved image carries, when the server can say: this
122
+ * machine runs that image and its daemon reported a version. Null otherwise.
123
+ */
124
+ approvedImageDaemonVersion?: string | null;
116
125
  /** When the older image stops being supported; null when there is no deadline. */
117
126
  updateBy?: string | null;
118
127
  lastUpdate?: {
@@ -121,6 +130,26 @@ export interface DedicatedImageStanding {
121
130
  settledAt?: string | null;
122
131
  } | null;
123
132
  }
133
+ /**
134
+ * The latest daemon release, as `show` read it from the signed release feed:
135
+ * a version, or why it could not be read. Never inferred from the machine.
136
+ */
137
+ export type LatestDaemonRelease = {
138
+ version: string;
139
+ } | {
140
+ error: string;
141
+ };
142
+ /** -1 / 0 / 1, or null when either side is not a version. */
143
+ export declare function compareDaemonVersions(a: string, b: string): number | null;
144
+ /**
145
+ * Which daemon the approved image carries against the latest daemon release
146
+ * (OSK-12153). `image update: current` used to be the whole answer, while the
147
+ * daemon on the same machine said a newer release existed: "current" means no
148
+ * newer APPROVED image, and a release that no approved image carries yet is not
149
+ * something this machine can move to. Both halves are said, and an unknown
150
+ * half is said to be unknown rather than left out.
151
+ */
152
+ export declare function describeDedicatedDaemonRelease(image: DedicatedImageStanding, latest: LatestDaemonRelease | null | undefined): string | null;
124
153
  /** One line saying where a machine's image stands and what the owner can do. */
125
154
  export declare function describeDedicatedImageStanding(image: DedicatedImageStanding, leaseId?: string): string;
126
155
  /**
@@ -527,7 +556,9 @@ export declare function dedicatedSpendSummary(lease: DedicatedLease): string;
527
556
  */
528
557
  /** One line saying what a deletion still allows. */
529
558
  export declare function describeDedicatedDeletion(deletion: DedicatedLeaseDeletion, leaseId?: string): string;
530
- export declare function describeDedicatedLease(lease: DedicatedLease, harnesses?: readonly Harness[]): string[];
559
+ export declare function describeDedicatedLease(lease: DedicatedLease, harnesses?: readonly Harness[], opts?: {
560
+ latestDaemonRelease?: LatestDaemonRelease | null;
561
+ }): string[];
531
562
  /** "2026-09-30 14:00 UTC (in 17 days)" — a deadline readable without converting it. */
532
563
  export declare function formatDedicatedDeadline(value: string | null | undefined, now?: Date): string;
533
564
  /**
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DEDICATED_FILE_MAX_BYTES = exports.DEDICATED_FILE_CHUNK_BYTES = exports.DEDICATED_EXEC_DEFAULT_TIMEOUT_MS = exports.DEDICATED_EXEC_MAX_TIMEOUT_MS = exports.DEDICATED_MACHINE_PRODUCT = exports.DEDICATED_LEASE_ACTIONS = void 0;
4
4
  exports.describeDedicatedCheckpoint = describeDedicatedCheckpoint;
5
+ exports.compareDaemonVersions = compareDaemonVersions;
6
+ exports.describeDedicatedDaemonRelease = describeDedicatedDaemonRelease;
5
7
  exports.describeDedicatedImageStanding = describeDedicatedImageStanding;
6
8
  exports.dedicatedRuntimeCreateInput = dedicatedRuntimeCreateInput;
7
9
  exports.isDedicatedRepositoryRef = isDedicatedRepositoryRef;
@@ -73,6 +75,52 @@ function describeDedicatedCheckpoint(lease) {
73
75
  return null;
74
76
  return (0, data_provider_1.describeMachineCheckpointOutcome)(operation.checkpoint, { action: operation.action });
75
77
  }
78
+ function versionParts(version) {
79
+ const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(String(version || '').trim());
80
+ return match ? match.slice(1, 4).map(Number) : null;
81
+ }
82
+ /** -1 / 0 / 1, or null when either side is not a version. */
83
+ function compareDaemonVersions(a, b) {
84
+ const left = versionParts(a);
85
+ const right = versionParts(b);
86
+ if (!left || !right)
87
+ return null;
88
+ for (let i = 0; i < 3; i += 1) {
89
+ if (left[i] !== right[i])
90
+ return left[i] < right[i] ? -1 : 1;
91
+ }
92
+ return 0;
93
+ }
94
+ /**
95
+ * Which daemon the approved image carries against the latest daemon release
96
+ * (OSK-12153). `image update: current` used to be the whole answer, while the
97
+ * daemon on the same machine said a newer release existed: "current" means no
98
+ * newer APPROVED image, and a release that no approved image carries yet is not
99
+ * something this machine can move to. Both halves are said, and an unknown
100
+ * half is said to be unknown rather than left out.
101
+ */
102
+ function describeDedicatedDaemonRelease(image, latest) {
103
+ if (!latest)
104
+ return null;
105
+ const carried = image.approvedImageDaemonVersion || null;
106
+ const latestText = 'version' in latest
107
+ ? `latest release skrrd ${latest.version}`
108
+ : `latest release unknown (${latest.error})`;
109
+ if (!carried) {
110
+ return image.updateAvailable ? latestText : `approved image's daemon unknown · ${latestText}`;
111
+ }
112
+ if (!('version' in latest)) {
113
+ return `approved image carries skrrd ${carried} · ${latestText}`;
114
+ }
115
+ const compared = compareDaemonVersions(carried, latest.version);
116
+ if (compared === 0) {
117
+ return `approved image carries skrrd ${carried}, the latest release`;
118
+ }
119
+ if (compared === -1) {
120
+ return `approved image carries skrrd ${carried} · latest release skrrd ${latest.version} is not in an approved image yet, so there is nothing to update to`;
121
+ }
122
+ return `approved image carries skrrd ${carried} · ${latestText}`;
123
+ }
76
124
  /** One line saying where a machine's image stands and what the owner can do. */
77
125
  function describeDedicatedImageStanding(image, leaseId) {
78
126
  const last = image.lastUpdate;
@@ -547,7 +595,7 @@ function describeDedicatedDeletion(deletion, leaseId) {
547
595
  }
548
596
  return 'nothing to restore; every copy is deleted when the window closes';
549
597
  }
550
- function describeDedicatedLease(lease, harnesses = []) {
598
+ function describeDedicatedLease(lease, harnesses = [], opts = {}) {
551
599
  const lines = [dedicatedLeaseDisplayName(lease)];
552
600
  const row = (label, value) => {
553
601
  if (value === undefined || value === null || value === '')
@@ -584,6 +632,7 @@ function describeDedicatedLease(lease, harnesses = []) {
584
632
  row('image', image);
585
633
  if (lease.health?.image) {
586
634
  row('image update', describeDedicatedImageStanding(lease.health.image, dedicatedLeaseId(lease)));
635
+ row('image daemon', describeDedicatedDaemonRelease(lease.health.image, opts.latestDaemonRelease));
587
636
  }
588
637
  if (lease.daemon?.version || lease.daemon?.lastHeartbeatAt) {
589
638
  row('daemon', [
@@ -49,6 +49,7 @@ const fs = __importStar(require("node:fs"));
49
49
  const os = __importStar(require("node:os"));
50
50
  const path = __importStar(require("node:path"));
51
51
  const auth_core_1 = require("@skrr-ai/auth-core");
52
+ const loopback_http_1 = require("@skrr-ai/auth-core/loopback-http");
52
53
  function isDelegatedCliSession(env = process.env) {
53
54
  return Boolean(env.OVERSKY_AUTONOMOUS_SESSION === '1' &&
54
55
  env.OVERSKY_DELEGATED_CLI_BROKER_URL?.trim() &&
@@ -213,7 +214,9 @@ async function fetchDelegatedCliCredential(env = process.env, options = {}) {
213
214
  try {
214
215
  let res;
215
216
  try {
216
- res = await fetch(brokerUrl, {
217
+ // Proxy-free and loopback-only (OSK-12134): the capability and the
218
+ // credential it returns must never reach HTTP_PROXY.
219
+ res = await (0, loopback_http_1.loopbackFetch)(brokerUrl, {
217
220
  method: 'POST',
218
221
  headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
219
222
  body: JSON.stringify({
@@ -257,6 +260,22 @@ async function fetchDelegatedCliCredential(env = process.env, options = {}) {
257
260
  const body = (await res.json().catch(() => null));
258
261
  if (res.status === 403 && body?.code === 'DELEGATED_CLI_CAPABILITY_REJECTED')
259
262
  return null;
263
+ if ((res.status === 503 || body?.code === 'DELEGATED_CLI_CREDENTIAL_UNAVAILABLE') &&
264
+ (typeof body?.upstreamCode === 'string' || typeof body?.upstreamStatus === 'number')) {
265
+ // The daemon named the server's refusal of its own mint (OSK-12151):
266
+ // say that, with the code, rather than guess at three possible causes.
267
+ const named = [
268
+ typeof body?.upstreamStatus === 'number' ? `HTTP ${body.upstreamStatus}` : '',
269
+ typeof body?.upstreamCode === 'string' ? body.upstreamCode : '',
270
+ ]
271
+ .filter(Boolean)
272
+ .join(' ');
273
+ // The daemon's `message` already names the refusal; fall back to the
274
+ // status and code when an older daemon sent none.
275
+ throw new Error(typeof body?.message === 'string' && body.message
276
+ ? `the daemon's delegated CLI broker could not mint a credential: ${body.message}`
277
+ : `the skrr server refused the daemon's request for this session's credential (${named}).`);
278
+ }
260
279
  if (res.status === 503 || body?.code === 'DELEGATED_CLI_CREDENTIAL_UNAVAILABLE') {
261
280
  throw new Error(`the daemon's delegated CLI broker is alive but cannot mint a credential ` +
262
281
  `(HTTP ${res.status}). The daemon cannot reach the auth upstream, holds ` +
@@ -71,7 +71,9 @@ export declare function notInstalledMessage(): string;
71
71
  * preserving "exit code = the daemon's exit code" semantics that scripts
72
72
  * may depend on.
73
73
  */
74
- export declare function execRuntimeBinary(args: string[]): Promise<number>;
74
+ export declare function execRuntimeBinary(args: string[], opts?: {
75
+ binary?: string;
76
+ }): Promise<number>;
75
77
  /** Output from a short-lived runtime probe such as `skrrd status --json`. */
76
78
  export type RuntimeBinaryOutput = {
77
79
  code: number;