@skrr-ai/cli 0.1.79 → 0.1.80

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 (47) hide show
  1. package/dist/base-command.js +9 -2
  2. package/dist/commands/inbox/index.js +4 -2
  3. package/dist/commands/login.d.ts +8 -3
  4. package/dist/commands/login.js +14 -4
  5. package/dist/commands/spaces/workflows/events.js +9 -2
  6. package/dist/lib/agentic-stream.js +13 -4
  7. package/dist/lib/api-fetch.js +6 -2
  8. package/dist/lib/brokered-dpop.d.ts +34 -0
  9. package/dist/lib/brokered-dpop.js +89 -0
  10. package/dist/lib/daemonBroker.d.ts +71 -2
  11. package/dist/lib/daemonBroker.js +203 -36
  12. package/dist/lib/daemonBrokerRefusal.d.ts +26 -0
  13. package/dist/lib/daemonBrokerRefusal.js +83 -4
  14. package/dist/lib/daemonRestartWait.d.ts +39 -0
  15. package/dist/lib/daemonRestartWait.js +181 -0
  16. package/dist/lib/dedicated-service.js +10 -5
  17. package/dist/lib/dedicated-ssh.js +6 -3
  18. package/dist/lib/dedicated-terminal.js +6 -3
  19. package/dist/lib/dpop-auth.d.ts +35 -0
  20. package/dist/lib/dpop-auth.js +57 -0
  21. package/dist/lib/login.d.ts +6 -0
  22. package/dist/lib/login.js +13 -4
  23. package/dist/lib/node-adapter.js +4 -0
  24. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +42 -1
  25. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +35 -3
  26. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.d.ts +49 -1
  27. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.js +47 -9
  28. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.d.ts +56 -0
  29. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.js +42 -20
  30. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dpop.d.ts +101 -0
  31. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dpop.js +179 -0
  32. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
  33. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +19 -3
  34. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +42 -1
  35. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +32 -2
  36. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.d.ts +49 -1
  37. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.js +47 -9
  38. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.d.ts +56 -0
  39. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.js +39 -20
  40. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dpop.d.ts +101 -0
  41. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dpop.js +138 -0
  42. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +4 -0
  44. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  45. package/dist/node_modules/@skrr-ai/data-provider/index.js +3422 -3407
  46. package/oclif.manifest.json +31930 -31930
  47. package/package.json +2 -2
@@ -399,7 +399,10 @@ class BaseCommand extends core_1.Command {
399
399
  }
400
400
  else if ((this.brokerRefusal =
401
401
  (0, daemonBrokerRefusal_1.describeBrokerRefusal)(autoResult.outcome, this.config.bin) ??
402
- describeAbsentGuestDaemon(autoResult.outcome, this.config.bin))) {
402
+ describeAbsentGuestDaemon(autoResult.outcome, this.config.bin) ??
403
+ // A laptop daemon that is down or mid-restart is not the person
404
+ // being signed out (OSK-13222).
405
+ (0, daemonBrokerRefusal_1.describeLocalDaemonUnavailable)(autoResult.outcome, this.config.bin))) {
403
406
  // Reported by requireAuth(); nothing to print here.
404
407
  }
405
408
  else if (autoResult.outcome.reason === 'rate_limited') {
@@ -760,7 +763,11 @@ class BaseCommand extends core_1.Command {
760
763
  // Same code as the plain case — scripts branch on NOT_SIGNED_IN — with
761
764
  // the daemon's answer in the message and in `details.broker`.
762
765
  this.failWithCliError({
763
- message: `Not signed in. ${refusal.headline}\n→ ${refusal.remedy}`,
766
+ // A daemon that is down or restarting holds the person's sign-in; it
767
+ // has not been lost, so the sentence must not say it has (OSK-13222).
768
+ message: refusal.daemonUnavailable
769
+ ? `No credential from the skrr background service. ${refusal.headline}\n→ ${refusal.remedy}`
770
+ : `Not signed in. ${refusal.headline}\n→ ${refusal.remedy}`,
764
771
  code: 'NOT_SIGNED_IN',
765
772
  exit: 2,
766
773
  retryable: refusal.transient,
@@ -8,6 +8,7 @@ const base_command_1 = require("../../base-command");
8
8
  const inbox_1 = require("../../lib/inbox");
9
9
  const format_1 = require("../../lib/format");
10
10
  const agentic_stream_1 = require("../../lib/agentic-stream");
11
+ const dpop_auth_1 = require("../../lib/dpop-auth");
11
12
  /**
12
13
  * Stop `--follow` on Ctrl-C or SIGTERM, and end with the status a shell
13
14
  * reports for that signal (130 / 143).
@@ -241,11 +242,12 @@ class Inbox extends base_command_1.BaseCommand {
241
242
  const wsURL = (0, agentic_stream_1.toWebSocketOrigin)(this.cliConfig.baseURL);
242
243
  const socket = (0, socket_io_client_1.io)(wsURL, {
243
244
  path: '/ws/agentic',
244
- auth: {
245
+ // OSK-12020 — a DPoP-bound token proves possession on every handshake.
246
+ auth: (0, dpop_auth_1.socketAuthWithDpop)({
245
247
  token: this.resolvedCredential.token,
246
248
  ...(0, data_provider_1.getClientIdentityAuth)(),
247
249
  clientPlatform: 'cli',
248
- },
250
+ }, wsURL),
249
251
  extraHeaders: process.env.OVERSKY_WORKSPACE_ID
250
252
  ? { 'X-Workspace-Id': process.env.OVERSKY_WORKSPACE_ID }
251
253
  : undefined,
@@ -12,9 +12,14 @@ import { BaseCommand } from '../base-command';
12
12
  * Refresh, rotation, family revoke, and the cross-process auth.lock are
13
13
  * all the same substrate the daemon uses (@skrr-ai/auth-core).
14
14
  *
15
- * CI callers should NOT use this command — use
16
- * skrr create-token --name <name> --ttl 365
17
- * and set `OVERSKY_TOKEN=osk_ci_...` in the CI environment.
15
+ * CI. There is no long-lived token that `skrr` commands accept yet (OSK-13208).
16
+ * `skrr create-token` mints an `osk_ci_*` token, and that token only
17
+ * bootstraps a DAEMON (`OVERSKY_TOKEN=osk_ci_... skrrd start`): every `skrr`
18
+ * command rejects it with 401 (see create-token.ts and base-command.ts). What
19
+ * works today is a session minted for the machine with
20
+ * `skrr token mint --for <id>`, restored by the `OVERSKY_REFRESH_TOKEN` path
21
+ * below — whose refresh token rotates on use — or `OVERSKY_AUTH_HELPER`.
22
+ * docs/cli/SKRR_AUTH.md ("CI today") states the caveats.
18
23
  *
19
24
  * L13 — non-interactive restore from a refresh token. When
20
25
  * `OVERSKY_REFRESH_TOKEN` is set in the environment, the browser /
@@ -27,9 +27,14 @@ const prompt_1 = require("../lib/prompt");
27
27
  * Refresh, rotation, family revoke, and the cross-process auth.lock are
28
28
  * all the same substrate the daemon uses (@skrr-ai/auth-core).
29
29
  *
30
- * CI callers should NOT use this command — use
31
- * skrr create-token --name <name> --ttl 365
32
- * and set `OVERSKY_TOKEN=osk_ci_...` in the CI environment.
30
+ * CI. There is no long-lived token that `skrr` commands accept yet (OSK-13208).
31
+ * `skrr create-token` mints an `osk_ci_*` token, and that token only
32
+ * bootstraps a DAEMON (`OVERSKY_TOKEN=osk_ci_... skrrd start`): every `skrr`
33
+ * command rejects it with 401 (see create-token.ts and base-command.ts). What
34
+ * works today is a session minted for the machine with
35
+ * `skrr token mint --for <id>`, restored by the `OVERSKY_REFRESH_TOKEN` path
36
+ * below — whose refresh token rotates on use — or `OVERSKY_AUTH_HELPER`.
37
+ * docs/cli/SKRR_AUTH.md ("CI today") states the caveats.
33
38
  *
34
39
  * L13 — non-interactive restore from a refresh token. When
35
40
  * `OVERSKY_REFRESH_TOKEN` is set in the environment, the browser /
@@ -60,7 +65,12 @@ class Login extends base_command_1.BaseCommand {
60
65
  'Set OVERSKY_SKIP_DAEMON_BROKER=1 to skip step 1 (--device-code skips it too). After signing ' +
61
66
  'in, skrr also signs the background service in, and on an interactive first login on a ' +
62
67
  'computer without it, offers to install it. Set SKRR_SKIP_DAEMON_HANDOFF=1 to skip that ' +
63
- 'optional setup; it does not skip step 1.';
68
+ 'optional setup; it does not skip step 1.\n\n' +
69
+ 'With no person to approve a sign-in (CI, a provisioning script): mint a session for that ' +
70
+ 'machine with `skrr token mint --for <id>` and run this command there with ' +
71
+ 'OVERSKY_REFRESH_TOKEN and OVERSKY_CLI_ID set. The refresh token rotates when used, so one ' +
72
+ 'minted session serves one machine. Tokens from `skrr create-token` only bring up a ' +
73
+ 'background service (`skrrd`); skrr commands reject them.';
64
74
  static examples = [
65
75
  '<%= config.bin %> login',
66
76
  '<%= config.bin %> login --env prod',
@@ -4,6 +4,7 @@ exports.workflowEventStreamHeaders = workflowEventStreamHeaders;
4
4
  const core_1 = require("@oclif/core");
5
5
  const base_command_1 = require("../../../base-command");
6
6
  const workspace_header_1 = require("../../../lib/workspace-header");
7
+ const dpop_auth_1 = require("../../../lib/dpop-auth");
7
8
  function workflowEventStreamHeaders(token, env = process.env) {
8
9
  return (0, workspace_header_1.withWorkspaceHeader)({
9
10
  Accept: 'text/event-stream',
@@ -31,8 +32,14 @@ class SpacesWorkflowsEvents extends base_command_1.BaseCommand {
31
32
  this.error('--after must be an ISO-8601 timestamp', { exit: 2 });
32
33
  }
33
34
  const path = `/api/spaces/${encodeURIComponent(args.spaceId)}/workflows/runs/${encodeURIComponent(args.runId)}/events/stream${flags.after ? `?after=${encodeURIComponent(flags.after)}` : ''}`;
34
- const response = await fetch(`${this.cliConfig.baseURL.replace(/\/+$/, '')}${path}`, {
35
- headers: workflowEventStreamHeaders(this.resolvedCredential.token),
35
+ const streamUrl = `${this.cliConfig.baseURL.replace(/\/+$/, '')}${path}`;
36
+ const token = this.resolvedCredential.token;
37
+ const response = await fetch(streamUrl, {
38
+ headers: {
39
+ ...workflowEventStreamHeaders(token),
40
+ // OSK-12020 — a DPoP-bound hand-off token proves possession here too.
41
+ ...(await (0, dpop_auth_1.dpopHeadersFor)(token, 'GET', streamUrl)),
42
+ },
36
43
  });
37
44
  if (!response.ok || !response.body) {
38
45
  const body = await response.text();
@@ -26,6 +26,7 @@ const socket_io_client_1 = require("socket.io-client");
26
26
  const data_provider_1 = require("@skrr-ai/data-provider");
27
27
  const config_1 = require("./config");
28
28
  const publicEndpoints_generated_1 = require("./publicEndpoints.generated");
29
+ const dpop_auth_1 = require("./dpop-auth");
29
30
  /**
30
31
  * Why a question was dismissed when no terminal exists to ask it on. It is the
31
32
  * REASON for a dismissal, never an answer: it travels as the value of the
@@ -709,12 +710,13 @@ class AgenticStreamClient {
709
710
  : undefined;
710
711
  this.socket = (0, socket_io_client_1.io)(wsURL, {
711
712
  path: '/ws/agentic',
712
- auth: {
713
+ // OSK-12020 — a DPoP-bound token proves possession on every handshake.
714
+ auth: (0, dpop_auth_1.socketAuthWithDpop)({
713
715
  token: this.options.token,
714
716
  ...(0, data_provider_1.getClientIdentityAuth)(),
715
717
  clientPlatform: 'cli',
716
718
  clientOs: process.platform === 'darwin' ? 'macos' : process.platform,
717
- },
719
+ }, wsURL),
718
720
  extraHeaders,
719
721
  transports: ['websocket'],
720
722
  reconnection: true,
@@ -948,12 +950,19 @@ class AgenticStreamClient {
948
950
  if (this.closed || this.settled || generation !== this.replayGeneration)
949
951
  return;
950
952
  this.replayFailures = 0;
951
- this.receiveSessionOutput({ type: 'session.output', sessionId: this.options.sessionId, source: 'replay', envelopes: result.envelopes });
953
+ this.receiveSessionOutput({
954
+ type: 'session.output',
955
+ sessionId: this.options.sessionId,
956
+ source: 'replay',
957
+ envelopes: result.envelopes,
958
+ });
952
959
  // The canonical endpoint explicitly identifies retention gaps. Only
953
960
  // then may later frames pass with the existing honest gap notice.
954
961
  if (result.gap) {
955
962
  // A retention gap proves only this page, never a later live frame.
956
- const pageSeqs = result.envelopes.map((e) => Number(e?.__seq)).filter(Number.isSafeInteger);
963
+ const pageSeqs = result.envelopes
964
+ .map((e) => Number(e?.__seq))
965
+ .filter(Number.isSafeInteger);
957
966
  if (pageSeqs.length)
958
967
  this.flushPendingEnvelopes(true, Math.max(...pageSeqs));
959
968
  this.flushPendingEnvelopes(false);
@@ -44,6 +44,7 @@ const node_adapter_1 = require("./node-adapter");
44
44
  const refresh_1 = require("./refresh");
45
45
  const delegated_cli_1 = require("./delegated-cli");
46
46
  const daemonBroker_1 = require("./daemonBroker");
47
+ const dpop_auth_1 = require("./dpop-auth");
47
48
  const workspace_header_1 = require("./workspace-header");
48
49
  const non_json_response_1 = require("./non-json-response");
49
50
  class ApiFetchError extends Error {
@@ -162,7 +163,7 @@ async function apiFetch(pathOrUrl, opts = {}) {
162
163
  (0, node_adapter_1.getAdapterCredentialOverride)() ??
163
164
  (await (0, credential_resolver_1.resolveCredential)());
164
165
  }
165
- const fetchWithCredential = (credential) => {
166
+ const fetchWithCredential = async (credential) => {
166
167
  const headers = {
167
168
  Accept: 'application/json',
168
169
  'X-Oversky-Origin': (0, refresh_1.getCliOrigin)(),
@@ -178,10 +179,13 @@ async function apiFetch(pathOrUrl, opts = {}) {
178
179
  // key is additive metadata, not a way to re-auth the request.
179
180
  for (const [k, v] of Object.entries(callerHeaders)) {
180
181
  const lower = k.toLowerCase();
181
- if (lower === 'authorization' || lower === 'content-type')
182
+ if (lower === 'authorization' || lower === 'content-type' || lower === 'dpop')
182
183
  continue;
183
184
  headers[k] = v;
184
185
  }
186
+ // OSK-12020 — a fresh proof per attempt for a DPoP-bound hand-off token
187
+ // (the 401 retry below re-enters here and gets its own). No-op otherwise.
188
+ Object.assign(headers, await (0, dpop_auth_1.dpopHeadersFor)(credential.token, method, url));
185
189
  return fetch(url, {
186
190
  method,
187
191
  headers: (0, workspace_header_1.withWorkspaceHeader)(headers),
@@ -0,0 +1,34 @@
1
+ /**
2
+ * brokered-dpop.ts — this process's DPoP-bound hand-off token, and the proof
3
+ * fetch for it (OSK-12020).
4
+ *
5
+ * Kept apart from `daemonBroker.ts` on purpose: every transport consults it on
6
+ * every request (`dpop-auth.ts`), so it must stay a leaf with no I/O at import
7
+ * and no dependency on the broker's own module graph. `daemonBroker` registers
8
+ * the binding after a redeem whose answer said `tokenType: 'DPoP'`, and clears
9
+ * it after one that did not.
10
+ */
11
+ /** Where to ask for proofs. Re-resolved per call so a rotated secret heals. */
12
+ export interface DpopOracleEndpoint {
13
+ host: string;
14
+ port: number;
15
+ secret: string;
16
+ }
17
+ export declare function registerDpopBinding(accessToken: string, resolveEndpoint: () => DpopOracleEndpoint): void;
18
+ export declare function clearDpopBinding(): void;
19
+ /** True when `token` is this process's DPoP-bound hand-off token. */
20
+ export declare function isDpopBoundHandoffToken(token: string | null | undefined): boolean;
21
+ /**
22
+ * A fresh RFC 9449 proof for one request made with `token`, from the local
23
+ * daemon that holds the key the token is bound to. Null when `token` is not the
24
+ * bound one (nothing to prove) or the daemon would not sign — the request then
25
+ * goes out without a proof and the server's `DPOP_PROOF_REQUIRED` names the
26
+ * problem, rather than this client inventing a second error for it.
27
+ *
28
+ * The token itself is never sent: the daemon gets its SHA-256 (`ath`) and signs
29
+ * only for hashes of tokens it issued. Proofs are single-use, so this runs per
30
+ * request and per retry.
31
+ */
32
+ export declare function brokeredDpopProof(token: string | null | undefined, htm: string, htu: string): Promise<string | null>;
33
+ /** @internal test seam. */
34
+ export declare function __resetDpopBindingForTest(): void;
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ /**
3
+ * brokered-dpop.ts — this process's DPoP-bound hand-off token, and the proof
4
+ * fetch for it (OSK-12020).
5
+ *
6
+ * Kept apart from `daemonBroker.ts` on purpose: every transport consults it on
7
+ * every request (`dpop-auth.ts`), so it must stay a leaf with no I/O at import
8
+ * and no dependency on the broker's own module graph. `daemonBroker` registers
9
+ * the binding after a redeem whose answer said `tokenType: 'DPoP'`, and clears
10
+ * it after one that did not.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.registerDpopBinding = registerDpopBinding;
14
+ exports.clearDpopBinding = clearDpopBinding;
15
+ exports.isDpopBoundHandoffToken = isDpopBoundHandoffToken;
16
+ exports.brokeredDpopProof = brokeredDpopProof;
17
+ exports.__resetDpopBindingForTest = __resetDpopBindingForTest;
18
+ const node_crypto_1 = require("node:crypto");
19
+ const loopback_http_1 = require("@skrr-ai/auth-core/loopback-http");
20
+ const cli_handoff_wire_1 = require("@skrr-ai/auth-core/cli-handoff-wire");
21
+ let binding = null;
22
+ /** Bound the proof fetch: it sits in front of every request a bound token makes. */
23
+ const DPOP_PROOF_TIMEOUT_MS = 5_000;
24
+ function registerDpopBinding(accessToken, resolveEndpoint) {
25
+ binding = { accessToken, resolveEndpoint };
26
+ }
27
+ function clearDpopBinding() {
28
+ binding = null;
29
+ }
30
+ /** True when `token` is this process's DPoP-bound hand-off token. */
31
+ function isDpopBoundHandoffToken(token) {
32
+ return Boolean(token && binding && token === binding.accessToken);
33
+ }
34
+ /**
35
+ * A fresh RFC 9449 proof for one request made with `token`, from the local
36
+ * daemon that holds the key the token is bound to. Null when `token` is not the
37
+ * bound one (nothing to prove) or the daemon would not sign — the request then
38
+ * goes out without a proof and the server's `DPOP_PROOF_REQUIRED` names the
39
+ * problem, rather than this client inventing a second error for it.
40
+ *
41
+ * The token itself is never sent: the daemon gets its SHA-256 (`ath`) and signs
42
+ * only for hashes of tokens it issued. Proofs are single-use, so this runs per
43
+ * request and per retry.
44
+ */
45
+ async function brokeredDpopProof(token, htm, htu) {
46
+ const state = binding;
47
+ if (!token || !state || token !== state.accessToken)
48
+ return null;
49
+ let endpoint;
50
+ try {
51
+ endpoint = state.resolveEndpoint();
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ const controller = new AbortController();
57
+ const timer = setTimeout(() => controller.abort(), DPOP_PROOF_TIMEOUT_MS);
58
+ try {
59
+ // The descriptor secret is loopback-only: never global `fetch`, which can
60
+ // route 127.0.0.1 through HTTP_PROXY (OSK-12134).
61
+ const response = await (0, loopback_http_1.loopbackFetch)(`http://${endpoint.host}:${endpoint.port}${cli_handoff_wire_1.CLI_HANDOFF_DPOP_PROOF_PATH}`, {
62
+ method: 'POST',
63
+ headers: {
64
+ Authorization: `Bearer ${endpoint.secret}`,
65
+ 'Content-Type': 'application/json',
66
+ Accept: 'application/json',
67
+ },
68
+ body: JSON.stringify({
69
+ htm: htm.toUpperCase(),
70
+ htu,
71
+ ath: (0, node_crypto_1.createHash)('sha256').update(token, 'utf8').digest('base64url'),
72
+ }),
73
+ signal: controller.signal,
74
+ });
75
+ if (!response.ok)
76
+ return null;
77
+ return (0, cli_handoff_wire_1.parseCliHandoffDpopProofResponse)(await response.json())?.proof ?? null;
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ finally {
83
+ clearTimeout(timer);
84
+ }
85
+ }
86
+ /** @internal test seam. */
87
+ function __resetDpopBindingForTest() {
88
+ binding = null;
89
+ }
@@ -39,6 +39,7 @@
39
39
  import { type CliHandoffDescriptor, type CliHandoffTokenResponse } from '@skrr-ai/auth-core/cli-handoff-wire';
40
40
  import { type AuthBackend } from './auth-storage';
41
41
  import { type CliConfig } from './config';
42
+ import { type AwaitRestartedDaemonOptions } from './daemonRestartWait';
42
43
  /**
43
44
  * Wire-shape of the bootstrap file — the shared declaration in
44
45
  * `@skrr-ai/auth-core/cli-handoff-wire` (`CliHandoffDescriptor`), which the
@@ -173,6 +174,23 @@ export interface DaemonBrokerFailure {
173
174
  * the repair for a refusal is the machine, never a login.
174
175
  */
175
176
  dedicatedGuest?: boolean;
177
+ /**
178
+ * For `no_bootstrap`: what the descriptor file on disk actually was, when
179
+ * one existed. `dead_pid` is a daemon that crashed or is mid-restart and left
180
+ * its hand-off behind; `unreadable` / `invalid` a file that could not be
181
+ * used. Absent when no candidate file existed at all (OSK-13222).
182
+ */
183
+ descriptorState?: 'dead_pid' | 'unreadable' | 'invalid';
184
+ /** The pid the descriptor named (a dead one, for `dead_pid`). */
185
+ descriptorPid?: number;
186
+ /** The loopback port the descriptor named — for `network`, the refused one. */
187
+ descriptorPort?: number;
188
+ /**
189
+ * How long the CLI waited for a restarting daemon before giving up. Set only
190
+ * when it waited: the local service was starting or is supervised, so a
191
+ * restart was the likely state (OSK-13222).
192
+ */
193
+ restartWaitedMs?: number;
176
194
  }
177
195
  export type DaemonBrokerOutcome = (DaemonBrokerResult & {
178
196
  ok: true;
@@ -215,7 +233,12 @@ export interface DaemonBrokerOptions {
215
233
  * - `'access_token'` — broker mode: redeem a short-lived access token
216
234
  * and hold it in memory only. Used by `maybeAutoBroker`: nothing
217
235
  * credential-shaped lands on disk and each `skrr` process re-redeems
218
- * over loopback.
236
+ * over loopback. When the descriptor advertises `access_token`, the
237
+ * token route is the ONLY door for this caller: a failed redeem is
238
+ * reported, never retried as a durable mint (OSK-12018). The automatic
239
+ * path must not mint and persist a refresh family the human never asked
240
+ * for — a durable login is the human's to hold, and only an explicit
241
+ * `skrr login` takes it.
219
242
  * - `'refresh_family'` (default) — the legacy durable mint. Used by
220
243
  * explicit `skrr login`: a human login still writes a real,
221
244
  * user-owned refresh family. A descriptor that advertises ONLY
@@ -223,6 +246,12 @@ export interface DaemonBrokerOptions {
223
246
  * regardless — its secret cannot mint a family at all.
224
247
  */
225
248
  preferHandoffMode?: 'access_token' | 'refresh_family';
249
+ /**
250
+ * Overrides for the bounded wait on a restarting local daemon (OSK-13222):
251
+ * budgets, the clock and the supervisor probe. Tests use it; production
252
+ * leaves it unset.
253
+ */
254
+ restartWait?: Omit<AwaitRestartedDaemonOptions, 'profile' | 'ready'>;
226
255
  }
227
256
  /**
228
257
  * Compute the bootstrap file path. Exported for tests so they can assert
@@ -257,6 +286,17 @@ export declare function resolveBootstrapCandidatePaths(profile?: string): string
257
286
  * user configures can point the broker somewhere else.
258
287
  */
259
288
  export declare const DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR = "/run/skrr-dedicated-runtime/cli-handoff/descriptor.json";
289
+ /**
290
+ * A Hosted Machine's workload hand-off descriptor, published only on a box
291
+ * booted under the identity split (OSK-11981, OSK-12019), where the daemon runs
292
+ * as `oversky-daemon` and its island file is unreadable to the workload. Same
293
+ * format and same broker-only contract as the Dedicated descriptor
294
+ * (`daemon/src/dedicated-cli-handoff.ts`, `workloadCliHandoffTarget`), in a
295
+ * DIFFERENT directory so a Hosted Machine is never mistaken for a Dedicated
296
+ * Runtime guest (`runningOnDedicatedRuntimeGuest` keys on that directory). The
297
+ * parent is created by the box's launcher as root and owned by the daemon.
298
+ */
299
+ export declare const HOSTED_MACHINE_CLI_HANDOFF_DESCRIPTOR = "/run/skrr-hosted-machine/cli-handoff/descriptor.json";
260
300
  /** Where the cli-handoff broker looks, in order. */
261
301
  export declare function resolveCliHandoffCandidatePaths(profile?: string): string[];
262
302
  /**
@@ -283,6 +323,32 @@ export declare function findLiveDaemonBootstrap({ profile, bootstrapPathOverride
283
323
  * file is malformed, the CLI shouldn't try to repair it).
284
324
  */
285
325
  export declare function readBootstrap(filePath: string): LocalBootstrap | null;
326
+ /**
327
+ * Why a descriptor file did or did not yield a usable bootstrap (OSK-13222).
328
+ *
329
+ * `readBootstrap` answers null for all of these alike, which is right for a
330
+ * caller that only wants "is there a daemon to ask", and wrong for the one
331
+ * that has to explain a failure: a descriptor naming a pid that has exited is
332
+ * a daemon that crashed or is restarting, not a person who is signed out, and
333
+ * the two have opposite repairs.
334
+ */
335
+ export type BootstrapInspection = {
336
+ state: 'ok';
337
+ bootstrap: LocalBootstrap;
338
+ } | {
339
+ state: 'absent';
340
+ } | {
341
+ state: 'unreadable';
342
+ detail: string;
343
+ } | {
344
+ state: 'invalid';
345
+ detail: string;
346
+ } | {
347
+ state: 'dead_pid';
348
+ pid: number;
349
+ bootstrap: LocalBootstrap;
350
+ };
351
+ export declare function inspectBootstrap(filePath: string): BootstrapInspection;
286
352
  /**
287
353
  * Try the daemon-as-broker handoff. Returns a tagged outcome so callers
288
354
  * can decide between fall-through and surface-to-user without parsing
@@ -431,7 +497,10 @@ export interface MaybeAutoBrokerResult {
431
497
  * 'access_token'`. On a descriptor that advertises broker mode the
432
498
  * outcome is `brokered` — an in-memory access token, nothing written;
433
499
  * the caller holds it for this process and the next `skrr` re-redeems.
434
- * On a legacy/v1 descriptor the durable mint is persisted to the
500
+ * A failed redeem on such a descriptor is returned as the outcome and
501
+ * never retried as a durable mint (OSK-12018). Only on a legacy/v1
502
+ * descriptor (no `access_token` advertised — the compatibility window
503
+ * documented in `brokerThrough`) is the durable mint persisted to the
435
504
  * keychain / cli-auth.json exactly as before.
436
505
  * - May generate a new `cliId` if the config doesn't have one. The
437
506
  * caller MUST persist `updatedConfig` so the next invocation