@skrr-ai/cli 0.1.79 → 0.1.81

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 (114) hide show
  1. package/dist/base-command.d.ts +4 -0
  2. package/dist/base-command.js +40 -4
  3. package/dist/commands/agents/visibility.d.ts +14 -0
  4. package/dist/commands/agents/visibility.js +52 -0
  5. package/dist/commands/create-token.d.ts +0 -1
  6. package/dist/commands/create-token.js +3 -1
  7. package/dist/commands/inbox/index.js +4 -2
  8. package/dist/commands/list-tokens.d.ts +0 -1
  9. package/dist/commands/list-tokens.js +1 -1
  10. package/dist/commands/login.d.ts +8 -3
  11. package/dist/commands/login.js +14 -4
  12. package/dist/commands/machines/dedicated/ssh-proxy.js +1 -0
  13. package/dist/commands/revoke-token.d.ts +0 -1
  14. package/dist/commands/revoke-token.js +1 -1
  15. package/dist/commands/spaces/workflows/events.js +9 -2
  16. package/dist/commands/store/follow.d.ts +1 -0
  17. package/dist/commands/store/follow.js +7 -1
  18. package/dist/commands/store/follows/list.d.ts +11 -0
  19. package/dist/commands/store/follows/list.js +29 -0
  20. package/dist/commands/store/follows/remove.d.ts +16 -0
  21. package/dist/commands/store/follows/remove.js +37 -0
  22. package/dist/commands/store/publishers/profile/draft.d.ts +17 -0
  23. package/dist/commands/store/publishers/profile/draft.js +54 -0
  24. package/dist/commands/store/publishers/profile/handle.d.ts +14 -0
  25. package/dist/commands/store/publishers/profile/handle.js +39 -0
  26. package/dist/commands/store/publishers/profile/hide.d.ts +12 -0
  27. package/dist/commands/store/publishers/profile/hide.js +39 -0
  28. package/dist/commands/store/publishers/profile/preview.d.ts +11 -0
  29. package/dist/commands/store/publishers/profile/preview.js +39 -0
  30. package/dist/commands/store/publishers/profile/publish.d.ts +12 -0
  31. package/dist/commands/store/publishers/profile/publish.js +41 -0
  32. package/dist/commands/store/publishers/profile/restore.d.ts +15 -0
  33. package/dist/commands/store/publishers/profile/restore.js +49 -0
  34. package/dist/commands/store/publishers/profile/revisions.d.ts +11 -0
  35. package/dist/commands/store/publishers/profile/revisions.js +30 -0
  36. package/dist/commands/store/publishers/profile/show.d.ts +11 -0
  37. package/dist/commands/store/publishers/profile/show.js +39 -0
  38. package/dist/commands/store/review.d.ts +26 -0
  39. package/dist/commands/store/review.js +93 -0
  40. package/dist/commands/store/reviews.d.ts +29 -0
  41. package/dist/commands/store/reviews.js +170 -0
  42. package/dist/commands/store/tip.d.ts +20 -0
  43. package/dist/commands/store/tip.js +57 -0
  44. package/dist/commands/tokens/create.d.ts +28 -0
  45. package/dist/commands/tokens/create.js +136 -0
  46. package/dist/commands/tokens/list.d.ts +11 -0
  47. package/dist/commands/tokens/list.js +53 -0
  48. package/dist/commands/tokens/revoke.d.ts +14 -0
  49. package/dist/commands/tokens/revoke.js +42 -0
  50. package/dist/lib/access-tokens.d.ts +59 -0
  51. package/dist/lib/access-tokens.js +50 -0
  52. package/dist/lib/agentic-stream.js +13 -4
  53. package/dist/lib/api-fetch.js +6 -2
  54. package/dist/lib/auth-core-init.d.ts +6 -0
  55. package/dist/lib/auth-core-init.js +28 -6
  56. package/dist/lib/brokered-dpop.d.ts +34 -0
  57. package/dist/lib/brokered-dpop.js +89 -0
  58. package/dist/lib/credential-resolver.d.ts +1 -1
  59. package/dist/lib/credential-resolver.js +8 -2
  60. package/dist/lib/daemonBroker.d.ts +84 -4
  61. package/dist/lib/daemonBroker.js +209 -40
  62. package/dist/lib/daemonBrokerRefusal.d.ts +26 -0
  63. package/dist/lib/daemonBrokerRefusal.js +83 -4
  64. package/dist/lib/daemonRestartWait.d.ts +39 -0
  65. package/dist/lib/daemonRestartWait.js +181 -0
  66. package/dist/lib/dedicated-service.js +10 -5
  67. package/dist/lib/dedicated-ssh.d.ts +9 -0
  68. package/dist/lib/dedicated-ssh.js +235 -63
  69. package/dist/lib/dedicated-terminal.js +6 -3
  70. package/dist/lib/dpop-auth.d.ts +35 -0
  71. package/dist/lib/dpop-auth.js +57 -0
  72. package/dist/lib/login.d.ts +6 -0
  73. package/dist/lib/login.js +13 -4
  74. package/dist/lib/node-adapter.js +6 -2
  75. package/dist/lib/publisher-profile-flags.d.ts +19 -0
  76. package/dist/lib/publisher-profile-flags.js +48 -0
  77. package/dist/lib/refresh.d.ts +1 -1
  78. package/dist/lib/refresh.js +6 -1
  79. package/dist/lib/ssh-ordered-bytes.d.ts +8 -0
  80. package/dist/lib/ssh-ordered-bytes.js +31 -0
  81. package/dist/lib/usd-micros.d.ts +9 -0
  82. package/dist/lib/usd-micros.js +21 -0
  83. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +42 -1
  84. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +35 -3
  85. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialResolver.d.ts +22 -3
  86. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialResolver.js +29 -5
  87. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.d.ts +49 -1
  88. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.js +47 -9
  89. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +7 -1
  90. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +7 -1
  91. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.d.ts +56 -0
  92. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.js +42 -20
  93. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dpop.d.ts +101 -0
  94. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dpop.js +179 -0
  95. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -1
  96. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +23 -5
  97. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +42 -1
  98. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +32 -2
  99. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialResolver.d.ts +22 -3
  100. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialResolver.js +27 -4
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.d.ts +49 -1
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.js +47 -9
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +7 -1
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +7 -1
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.d.ts +56 -0
  106. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.js +39 -20
  107. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dpop.d.ts +101 -0
  108. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dpop.js +138 -0
  109. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -1
  110. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +5 -1
  111. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  112. package/dist/node_modules/@skrr-ai/data-provider/index.js +3895 -3882
  113. package/oclif.manifest.json +34985 -33643
  114. package/package.json +28 -4
@@ -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),
@@ -36,6 +36,12 @@ export declare function plainCredentialEnvelopeWarning(args: readonly unknown[])
36
36
  * own when a local daemon can hand a credential over.
37
37
  */
38
38
  export declare const CREDENTIAL_KEY_CHANGED_NOTICE: string;
39
+ /**
40
+ * The one line an unreadable device key prints when no credential-key change
41
+ * explains it. The CLI never enrolls or requires a device key (OSK-12063), so
42
+ * this degrades a proof, not access — say exactly that.
43
+ */
44
+ export declare const DEVICE_KEY_RECOVERY_REQUIRED_NOTICE: string;
39
45
  export declare function createCredentialEnvelopeWarningPresenter(): (args: readonly unknown[]) => string | null;
40
46
  type DedicatedGenerationPreparationOptions = {
41
47
  machineUuid?: string;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.CliCredentialInitializationError = exports.CREDENTIAL_KEY_CHANGED_NOTICE = void 0;
6
+ exports.CliCredentialInitializationError = exports.DEVICE_KEY_RECOVERY_REQUIRED_NOTICE = exports.CREDENTIAL_KEY_CHANGED_NOTICE = void 0;
7
7
  exports.plainCredentialEnvelopeWarning = plainCredentialEnvelopeWarning;
8
8
  exports.createCredentialEnvelopeWarningPresenter = createCredentialEnvelopeWarningPresenter;
9
9
  exports.prepareDedicatedRuntimeAuthGeneration = prepareDedicatedRuntimeAuthGeneration;
@@ -108,6 +108,20 @@ exports.CREDENTIAL_KEY_CHANGED_NOTICE = "note: this machine's credential key cha
108
108
  */
109
109
  /** auth-core's device-identity warning for a private key it could not open. */
110
110
  const DEVICE_KEY_DECRYPT_FAILED = '[deviceIdentity] private_key_decrypt_failed';
111
+ /**
112
+ * auth-core's device-identity warning for a stored key it will NOT replace on
113
+ * ordinary boot (`b5e683280b`): minting a new key here would make the next
114
+ * refresh sign with a key the server never pinned. Only an explicit login with
115
+ * `recoverUnreadable` replaces it.
116
+ */
117
+ const DEVICE_KEY_RECOVERY_REQUIRED = '[deviceIdentity] recovery_required';
118
+ /**
119
+ * The one line an unreadable device key prints when no credential-key change
120
+ * explains it. The CLI never enrolls or requires a device key (OSK-12063), so
121
+ * this degrades a proof, not access — say exactly that.
122
+ */
123
+ exports.DEVICE_KEY_RECOVERY_REQUIRED_NOTICE = "warning: this machine's stored device key could not be read, so skrr sends requests " +
124
+ 'without a device proof until the local daemon signs in again. (DEBUG=1 shows the underlying error.)';
111
125
  function createCredentialEnvelopeWarningPresenter() {
112
126
  let keyChanged = false;
113
127
  return (args) => {
@@ -133,12 +147,20 @@ function createCredentialEnvelopeWarningPresenter() {
133
147
  if (keyChanged && event === 'cred_envelope.read.decrypt_failed')
134
148
  return '';
135
149
  // The device private key is one of those values: auth-core's device
136
- // identity loads it next, cannot open it, and mints a new keypair. It
137
- // logs that through its own `[deviceIdentity]` prefix, which reached the
138
- // terminal as a raw `[auth-core warn]` line right under the notice. The
139
- // notice covers it; without a key change it is a real fault and prints.
140
- if (keyChanged && message === DEVICE_KEY_DECRYPT_FAILED)
150
+ // identity loads it next and cannot open it. Since `b5e683280b` it does
151
+ // NOT mint a replacement on ordinary boot — it parks in
152
+ // `recovery_required` until an explicit login recovers it. Both warnings
153
+ // come through the `[deviceIdentity]` prefix and reached the terminal as
154
+ // raw `[auth-core warn]` lines right under the notice, which already says
155
+ // the old key is gone and what replaces it.
156
+ if (keyChanged &&
157
+ (message === DEVICE_KEY_DECRYPT_FAILED || message === DEVICE_KEY_RECOVERY_REQUIRED)) {
141
158
  return '';
159
+ }
160
+ // Without a key change an unreadable device key is a real fault: one
161
+ // plain line instead of an object dump.
162
+ if (message === DEVICE_KEY_RECOVERY_REQUIRED)
163
+ return exports.DEVICE_KEY_RECOVERY_REQUIRED_NOTICE;
142
164
  return plainCredentialEnvelopeWarning(args);
143
165
  };
144
166
  }
@@ -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
+ }
@@ -5,7 +5,7 @@
5
5
  * CLI-appropriate backends. The resolver walks the published chain:
6
6
  *
7
7
  * 1. flag — --token <jwt> (debugging; BaseCommand parses)
8
- * 2. env-token — OVERSKY_TOKEN (CI / static; kind='ci' detected)
8
+ * 2. env-token — OVERSKY_TOKEN, then SKRR_TOKEN (CI / static; kind 'ci' or 'pat' detected)
9
9
  * 3. auth-helper — OVERSKY_AUTH_HELPER script output
10
10
  * 4. fd-handoff — N/A for CLI (skipped; no parent fd)
11
11
  * 5. keychain — ai.skrr.cli.<profile>
@@ -12,7 +12,7 @@ exports.formatCredentialSource = formatCredentialSource;
12
12
  * CLI-appropriate backends. The resolver walks the published chain:
13
13
  *
14
14
  * 1. flag — --token <jwt> (debugging; BaseCommand parses)
15
- * 2. env-token — OVERSKY_TOKEN (CI / static; kind='ci' detected)
15
+ * 2. env-token — OVERSKY_TOKEN, then SKRR_TOKEN (CI / static; kind 'ci' or 'pat' detected)
16
16
  * 3. auth-helper — OVERSKY_AUTH_HELPER script output
17
17
  * 4. fd-handoff — N/A for CLI (skipped; no parent fd)
18
18
  * 5. keychain — ai.skrr.cli.<profile>
@@ -78,6 +78,9 @@ async function resolveCredential(opts = {}) {
78
78
  flagToken: opts.flagToken,
79
79
  bareMode,
80
80
  storeMode,
81
+ // SKRR_TOKEN is the name the CI recipe uses (OSK-13208); OVERSKY_TOKEN still
82
+ // wins when both are set, as in the config overlay (config.ts).
83
+ envTokenNames: ['OVERSKY_TOKEN', 'SKRR_TOKEN'],
81
84
  // CLI never has an interactive slot — users run `skrr login` explicitly.
82
85
  isHeadless: () => true,
83
86
  // Stored credentials are only usable against the origin that issued
@@ -160,7 +163,10 @@ function formatCredentialSource(source) {
160
163
  case 'flag':
161
164
  return 'CLI --token flag';
162
165
  case 'env-token':
163
- return 'OVERSKY_TOKEN';
166
+ // Name the variable that actually supplied it (OVERSKY_TOKEN wins).
167
+ return !process.env.OVERSKY_TOKEN?.trim() && process.env.SKRR_TOKEN?.trim()
168
+ ? 'SKRR_TOKEN'
169
+ : 'OVERSKY_TOKEN';
164
170
  case 'auth-helper':
165
171
  return 'OVERSKY_AUTH_HELPER';
166
172
  case 'fd-handoff':
@@ -1,5 +1,5 @@
1
1
  /**
2
- * daemonBroker.ts — Sky CLI client for the local `oversky` daemon's
2
+ * daemonBroker.ts — skrr CLI client for the local `skrrd` daemon's
3
3
  * cli-handoff endpoint.
4
4
  *
5
5
  * When the daemon is already authenticated for the user on this machine,
@@ -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;
@@ -181,7 +199,7 @@ export interface DaemonBrokerOptions {
181
199
  /** Stable per-machine cliId the CLI wants the new refresh row bound to. */
182
200
  cliId: string;
183
201
  /**
184
- * The Sky CLI's currently-configured baseURL (e.g. `https://oversky.ai`).
202
+ * The skrr CLI's currently-configured baseURL (e.g. `https://skrr.ai`).
185
203
  * Two roles:
186
204
  * 1. Pre-flight check vs `bootstrap.serverUrl` — when they don't
187
205
  * match, we refuse to call the daemon so we never mint a token
@@ -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
@@ -395,6 +461,17 @@ export interface MaybeAutoBrokerOptions {
395
461
  * everywhere else. Production callers omit it and behave exactly as before.
396
462
  */
397
463
  bootstrapPathOverride?: string;
464
+ /**
465
+ * The profile whose daemon may be borrowed. Defaults to the active profile.
466
+ *
467
+ * A silent hand-off must never cross profiles (OSK-13257). The `default`
468
+ * candidate list falls through to any running daemon, and every daemon is
469
+ * signed in as whoever started it — so `--profile buyer`, with its own
470
+ * session refused, used to run as the machine owner after one stderr line.
471
+ * A named profile now borrows only a daemon started under that same
472
+ * profile, and otherwise reports NOT_SIGNED_IN for the profile it was given.
473
+ */
474
+ profile?: string;
398
475
  }
399
476
  export interface MaybeAutoBrokerResult {
400
477
  /**
@@ -431,7 +508,10 @@ export interface MaybeAutoBrokerResult {
431
508
  * 'access_token'`. On a descriptor that advertises broker mode the
432
509
  * outcome is `brokered` — an in-memory access token, nothing written;
433
510
  * 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
511
+ * A failed redeem on such a descriptor is returned as the outcome and
512
+ * never retried as a durable mint (OSK-12018). Only on a legacy/v1
513
+ * descriptor (no `access_token` advertised — the compatibility window
514
+ * documented in `brokerThrough`) is the durable mint persisted to the
435
515
  * keychain / cli-auth.json exactly as before.
436
516
  * - May generate a new `cliId` if the config doesn't have one. The
437
517
  * caller MUST persist `updatedConfig` so the next invocation