@skrr-ai/cli 0.1.58 → 0.1.60

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 (39) hide show
  1. package/dist/base-command.d.ts +0 -16
  2. package/dist/base-command.js +65 -11
  3. package/dist/commands/followups/checkin.js +16 -3
  4. package/dist/commands/logout.d.ts +0 -46
  5. package/dist/commands/logout.js +21 -3
  6. package/dist/lib/agentic-stream.d.ts +49 -0
  7. package/dist/lib/agentic-stream.js +140 -15
  8. package/dist/lib/api-fetch.d.ts +11 -0
  9. package/dist/lib/api-fetch.js +98 -1
  10. package/dist/lib/daemonBrokerRefusal.d.ts +21 -0
  11. package/dist/lib/daemonBrokerRefusal.js +87 -2
  12. package/dist/lib/dedicated-guest.d.ts +13 -0
  13. package/dist/lib/dedicated-guest.js +22 -0
  14. package/dist/lib/dedicated-machines.js +17 -1
  15. package/dist/lib/dedicated-service.d.ts +1 -13
  16. package/dist/lib/dedicated-service.js +7 -19
  17. package/dist/lib/followups.d.ts +7 -0
  18. package/dist/lib/followups.js +17 -4
  19. package/dist/lib/login.js +10 -1
  20. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +41 -0
  21. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +147 -3
  22. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +17 -0
  23. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +38 -0
  24. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -2
  25. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +5 -3
  26. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.d.ts +21 -1
  27. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.js +40 -7
  28. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +41 -0
  29. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +147 -4
  30. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +17 -0
  31. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +38 -1
  32. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -2
  33. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -2
  34. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.d.ts +21 -1
  35. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.js +39 -6
  36. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  37. package/dist/node_modules/@skrr-ai/data-provider/index.js +4397 -4363
  38. package/oclif.manifest.json +17475 -17475
  39. package/package.json +1 -1
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ApiFetchError = void 0;
4
4
  exports.apiFetch = apiFetch;
5
+ exports.__resetDaemonHandoffFallbackForTest = __resetDaemonHandoffFallbackForTest;
5
6
  exports.hasHumanSession = hasHumanSession;
6
7
  /**
7
8
  * api-fetch.ts — authenticated fetch helper for endpoints not yet in
@@ -61,6 +62,14 @@ class ApiFetchError extends Error {
61
62
  * attributed by guesswork — see `node-adapter.ts`.
62
63
  */
63
64
  requestId;
65
+ /**
66
+ * Set on a 401 when this CLI's own refresh was refused AND the local
67
+ * daemon, asked for a hand-off, refused too (OSK-12474). The daemon's reason
68
+ * decides the repair, so the 401 message carries it rather than a bare
69
+ * "Run `skrr login`" (OSK-12149, OSK-12151); `describeBrokerRefusal` turns
70
+ * it into words. Absent when the daemon was never asked.
71
+ */
72
+ brokerFailure;
64
73
  constructor(message, status, body, code, method, requestId) {
65
74
  super(message);
66
75
  this.name = 'ApiFetchError';
@@ -181,6 +190,7 @@ async function apiFetch(pathOrUrl, opts = {}) {
181
190
  });
182
191
  };
183
192
  let res = await fetchWithCredential(cred);
193
+ let brokerFailure = null;
184
194
  if (res.status === 401 && delegatedCli) {
185
195
  // Deliberately terminal on broker failure: an Agent whose daemon cannot
186
196
  // rotate its family must surface the 401, never fall through to
@@ -217,6 +227,25 @@ async function apiFetch(pathOrUrl, opts = {}) {
217
227
  (0, node_adapter_1.setAdapterCredentialOverride)(cred);
218
228
  res = await fetchWithCredential(cred);
219
229
  }
230
+ else if (fresh.skippedReason === 'refresh_failed' ||
231
+ fresh.skippedReason === 'no_refresh_token') {
232
+ // OSK-12474 — this CLI's STORED sign-in could not be renewed (e.g.
233
+ // DEVICE_KEY_MISMATCH after a key split). The node-adapter asks the local
234
+ // daemon for a hand-off at this point; this transport did not, so a
235
+ // healthy daemon was ignored and every apiFetch command demanded
236
+ // `skrr login`. Gated on the stored-credential skip reasons only: a
237
+ // `--token`, OVERSKY_TOKEN, CI token or bare-mode credential is the
238
+ // operator's, and must never be swapped for the daemon's principal.
239
+ const brokered = await daemonHandoffAfterRefusedRefresh();
240
+ if (brokered.credential) {
241
+ cred = brokered.credential;
242
+ (0, node_adapter_1.setAdapterCredentialOverride)(cred);
243
+ res = await fetchWithCredential(cred);
244
+ }
245
+ else {
246
+ brokerFailure = brokered.failure;
247
+ }
248
+ }
220
249
  }
221
250
  if (!res.ok) {
222
251
  let text = '';
@@ -235,7 +264,10 @@ async function apiFetch(pathOrUrl, opts = {}) {
235
264
  catch {
236
265
  /* ignore body read failures */
237
266
  }
238
- throw new ApiFetchError(`HTTP ${res.status} ${res.statusText} — ${method} ${pathOrUrl}`, res.status, text, code, method, res.headers.get('x-request-id') || undefined);
267
+ const error = new ApiFetchError(`HTTP ${res.status} ${res.statusText} — ${method} ${pathOrUrl}`, res.status, text, code, method, res.headers.get('x-request-id') || undefined);
268
+ if (brokerFailure)
269
+ error.brokerFailure = brokerFailure;
270
+ throw error;
239
271
  }
240
272
  // 204/205 carry no body by definition; an empty body has no content-type
241
273
  // worth judging. Neither is an error.
@@ -255,6 +287,71 @@ async function apiFetch(pathOrUrl, opts = {}) {
255
287
  }
256
288
  return (await res.json());
257
289
  }
290
+ /**
291
+ * One daemon hand-off attempt per process (OSK-12474). Memoized, including a
292
+ * failure: a command that makes many requests must not ask the daemon again
293
+ * on every 401, and a hand-off token that is itself refused is renewed by the
294
+ * brokered re-redeem branch above, never by coming back here — so this cannot
295
+ * loop.
296
+ */
297
+ let daemonHandoffFallback = null;
298
+ function daemonHandoffAfterRefusedRefresh() {
299
+ daemonHandoffFallback ??= attemptDaemonHandoffFallback();
300
+ return daemonHandoffFallback;
301
+ }
302
+ async function attemptDaemonHandoffFallback() {
303
+ const cliConfig = (0, config_1.loadConfig)();
304
+ let result;
305
+ try {
306
+ // `maybeAutoBroker` applies the same opt-outs as the node-adapter's
307
+ // recovery: OVERSKY_TOKEN/OVERSKY_REFRESH_TOKEN and
308
+ // OVERSKY_SKIP_DAEMON_BROKER=1 leave it untriggered.
309
+ result = await (0, daemonBroker_1.maybeAutoBroker)({ cliConfig, bareMode: false });
310
+ }
311
+ catch {
312
+ return { credential: null, failure: null };
313
+ }
314
+ const outcome = result.outcome;
315
+ if (!result.triggered || !outcome)
316
+ return { credential: null, failure: null };
317
+ if (!outcome.ok)
318
+ return { credential: null, failure: outcome };
319
+ if (result.updatedConfig) {
320
+ try {
321
+ (0, config_1.saveConfig)(result.updatedConfig);
322
+ }
323
+ catch {
324
+ /* non-fatal: the next process mints a new cliId (see BaseCommand.init) */
325
+ }
326
+ }
327
+ const accessToken = outcome.accessToken;
328
+ // Where a durable mint was written; a brokered hand-off is never stored.
329
+ const storedSource = !outcome.brokered && outcome.storedIn === 'keychain' ? 'keychain' : 'file';
330
+ // Say where the credential now comes from, once. The adapter slot records a
331
+ // brokered token as `env-token` (for refresh semantics), which a reader would
332
+ // take for OVERSKY_TOKEN; the label must name the descriptor (OSK-12185).
333
+ const label = outcome.brokered
334
+ ? (0, credential_resolver_1.describeCredentialSource)('env-token', {
335
+ kind: 'daemon-handoff',
336
+ ...(outcome.descriptorPath ? { descriptorPath: outcome.descriptorPath } : {}),
337
+ })
338
+ : `a new sign-in brokered by the local daemon, saved to ${(0, credential_resolver_1.describeCredentialSource)(storedSource)}`;
339
+ process.stderr.write(`[skrr] This CLI's own sign-in could not be renewed; using ${label} instead.\n`);
340
+ return {
341
+ credential: {
342
+ token: accessToken,
343
+ // A brokered hand-off is held in memory for this process only; the
344
+ // durable mint was written to the backend the broker reported.
345
+ source: outcome.brokered ? 'env-token' : storedSource,
346
+ kind: (0, credential_resolver_1.kindOf)(accessToken),
347
+ },
348
+ failure: null,
349
+ };
350
+ }
351
+ /** @internal test seam — forget this process's daemon hand-off attempt. */
352
+ function __resetDaemonHandoffFallbackForTest() {
353
+ daemonHandoffFallback = null;
354
+ }
258
355
  function trustedAutonomousBaseUrl() {
259
356
  // Only the daemon-owned alias is accepted. OVERSKY_BASE_URL is a normal
260
357
  // user/CLI override and may originate in request-carried provider env.
@@ -49,3 +49,24 @@ export interface BrokerRefusalDescription {
49
49
  * (`base_url_mismatch`, `rate_limited`).
50
50
  */
51
51
  export declare function describeBrokerRefusal(failure: DaemonBrokerFailure, bin?: string): BrokerRefusalDescription | null;
52
+ /**
53
+ * What to tell a person on a Dedicated Runtime guest when NO daemon answered
54
+ * the hand-off at all (OSK-12473).
55
+ *
56
+ * `describeBrokerRefusal` speaks only for a daemon that answered. On a guest
57
+ * the realistic broken state is a daemon that is absent: its credential was
58
+ * revoked and it exited by design, or systemd is cycling it, or it crashed and
59
+ * left its descriptor behind. The CLI then saw `no_bootstrap` / `network`, and
60
+ * fell to "Not signed in. Run `skrr login` first." — a login nobody performs
61
+ * on a guest, and which would only put a personal credential on a machine the
62
+ * daemon is meant to hold the account for. Say the machine's service is not
63
+ * running and name the owner's repair instead.
64
+ *
65
+ * Returns null off a guest, or for a daemon that answered (that is
66
+ * `describeBrokerRefusal`'s case).
67
+ */
68
+ export declare function describeGuestDaemonUnavailable(failure: DaemonBrokerFailure | null, opts: {
69
+ bin?: string;
70
+ onGuest: boolean;
71
+ descriptorPresent?: boolean;
72
+ }): BrokerRefusalDescription | null;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isAnsweredBrokerRefusal = isAnsweredBrokerRefusal;
4
4
  exports.describeBrokerRefusal = describeBrokerRefusal;
5
+ exports.describeGuestDaemonUnavailable = describeGuestDaemonUnavailable;
5
6
  /**
6
7
  * True when a daemon ANSWERED the hand-off and refused it — as opposed to no
7
8
  * daemon being there to ask (`no_bootstrap`, a stale descriptor nobody listens
@@ -81,8 +82,8 @@ function describeBrokerRefusal(failure, bin = 'skrr') {
81
82
  `if it persists, check the machine from your own computer with \`${bin} machines dedicated health-check <lease-id>\` ` +
82
83
  `(\`${bin} machines dedicated list\` shows the lease id).`
83
84
  : '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.`;
85
+ `\`${bin} machines dedicated health-check <lease-id>\` (\`${bin} machines dedicated list\` shows the lease id). ` +
86
+ guestReissueRemedy(bin, 'If it stays refused');
86
87
  }
87
88
  else if (code === 'DEVICE_PROOF_PIN_MISMATCH') {
88
89
  remedy =
@@ -125,3 +126,87 @@ function describeBrokerRefusal(failure, bin = 'skrr') {
125
126
  },
126
127
  };
127
128
  }
129
+ /**
130
+ * The owner-side repair that issues a guest's daemon a fresh credential.
131
+ *
132
+ * `stop` then `start`, not `restart`: a restart keeps the instance, so its
133
+ * daemon re-enrolls through the attested re-issue, which the server refuses
134
+ * for a credential a person revoked (`REISSUE_REVOKE_REASON`). Observed on a
135
+ * revoked guest: `restart` sat in `restarting` for 16 minutes and ended
136
+ * `dedicated_runtime_guest_not_ready_after_restart`, while stop + start
137
+ * recovered it (OSK-12473).
138
+ */
139
+ function guestReissueRemedy(bin, when) {
140
+ return (`${when}, \`${bin} machines dedicated stop <lease-id>\` then \`${bin} machines dedicated start <lease-id>\` ` +
141
+ 'gives the machine a fresh credential (files under /workspace are kept; a restart does not re-issue one).');
142
+ }
143
+ /**
144
+ * What to tell a person on a Dedicated Runtime guest when NO daemon answered
145
+ * the hand-off at all (OSK-12473).
146
+ *
147
+ * `describeBrokerRefusal` speaks only for a daemon that answered. On a guest
148
+ * the realistic broken state is a daemon that is absent: its credential was
149
+ * revoked and it exited by design, or systemd is cycling it, or it crashed and
150
+ * left its descriptor behind. The CLI then saw `no_bootstrap` / `network`, and
151
+ * fell to "Not signed in. Run `skrr login` first." — a login nobody performs
152
+ * on a guest, and which would only put a personal credential on a machine the
153
+ * daemon is meant to hold the account for. Say the machine's service is not
154
+ * running and name the owner's repair instead.
155
+ *
156
+ * Returns null off a guest, or for a daemon that answered (that is
157
+ * `describeBrokerRefusal`'s case).
158
+ */
159
+ function describeGuestDaemonUnavailable(failure, opts) {
160
+ const bin = opts.bin ?? 'skrr';
161
+ if (!opts.onGuest && failure?.dedicatedGuest !== true)
162
+ return null;
163
+ if (failure && isAnsweredBrokerRefusal(failure))
164
+ return null;
165
+ if (failure && (failure.reason === 'base_url_mismatch' || failure.reason === 'rate_limited')) {
166
+ return null;
167
+ }
168
+ const who = `This Dedicated Runtime's skrr service${failure?.daemonId ? ` (daemon ${failure.daemonId})` : ''}`;
169
+ let headline;
170
+ let transient = false;
171
+ const reason = failure?.reason ?? 'no_bootstrap';
172
+ if (reason === 'network') {
173
+ headline = `${who} is not running: its sign-in hand-off names a port nothing is listening on.`;
174
+ }
175
+ else if (reason === 'bootstrap_parse') {
176
+ headline = `${who} is not available: its sign-in hand-off file could not be read.`;
177
+ }
178
+ else if (reason === 'stale_bootstrap') {
179
+ // An uncoded 401 on the descriptor secret: the service rotated it, which
180
+ // it does when it restarts. Re-reading the file next time heals it.
181
+ headline = `${who} refused this CLI's hand-off secret, which it rotates when it restarts.`;
182
+ transient = true;
183
+ }
184
+ else if (opts.descriptorPresent) {
185
+ headline = `${who} is not running: its sign-in hand-off was left behind by a process that has exited.`;
186
+ }
187
+ else {
188
+ headline = `${who} is not running, so nothing on this machine can sign this CLI in.`;
189
+ }
190
+ const remedy = transient
191
+ ? 'Retry in a moment. Nobody signs in on a Dedicated Runtime, so `' +
192
+ `${bin} login\` is not the fix; if it persists, the machine's owner can run ` +
193
+ `\`${bin} machines dedicated health-check <lease-id>\` from their own computer.`
194
+ : `Nobody signs in on a Dedicated Runtime — its service holds the account — so \`${bin} login\` is not the fix. ` +
195
+ "If the machine is still starting, retry in a minute. Otherwise, from the owner's own computer: " +
196
+ `\`${bin} machines dedicated health-check <lease-id>\` shows why (\`${bin} machines dedicated list\` shows the lease id). ` +
197
+ guestReissueRemedy(bin, 'If the service stays down');
198
+ return {
199
+ headline,
200
+ remedy,
201
+ transient,
202
+ dedicatedGuest: true,
203
+ broker: {
204
+ reason,
205
+ ...(typeof failure?.status === 'number' ? { status: failure.status } : {}),
206
+ ...(failure?.code ? { code: failure.code } : {}),
207
+ ...(failure?.detail ? { detail: failure.detail } : {}),
208
+ ...(failure?.descriptorPath ? { descriptorPath: failure.descriptorPath } : {}),
209
+ ...(failure?.daemonId ? { daemonId: failure.daemonId } : {}),
210
+ },
211
+ };
212
+ }
@@ -0,0 +1,13 @@
1
+ /** The daemon's workload-facing service socket on a Dedicated Runtime guest. */
2
+ export declare const GUEST_SERVICE_SOCKET_PATH = "/run/skrr-dedicated-runtime/service-propose/service-propose.sock";
3
+ /**
4
+ * True when this process is running on a Dedicated Runtime guest — the
5
+ * workload-uid test, or the guest markers the image/daemon own. An SSH
6
+ * session may not carry the wrapper's env stamp, so the filesystem markers
7
+ * count too; the socket itself remains the authority.
8
+ */
9
+ export declare function runningOnDedicatedRuntimeGuest(env?: NodeJS.ProcessEnv, getuid?: (() => number) | undefined, paths?: {
10
+ socketPath?: string;
11
+ attestationPath?: string;
12
+ runtimeDir?: string;
13
+ }): boolean;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GUEST_SERVICE_SOCKET_PATH = void 0;
4
+ exports.runningOnDedicatedRuntimeGuest = runningOnDedicatedRuntimeGuest;
5
+ const node_fs_1 = require("node:fs");
6
+ const first_party_harness_1 = require("./first-party-harness");
7
+ /** The daemon's workload-facing service socket on a Dedicated Runtime guest. */
8
+ exports.GUEST_SERVICE_SOCKET_PATH = '/run/skrr-dedicated-runtime/service-propose/service-propose.sock';
9
+ /**
10
+ * True when this process is running on a Dedicated Runtime guest — the
11
+ * workload-uid test, or the guest markers the image/daemon own. An SSH
12
+ * session may not carry the wrapper's env stamp, so the filesystem markers
13
+ * count too; the socket itself remains the authority.
14
+ */
15
+ function runningOnDedicatedRuntimeGuest(env = process.env, getuid = process.getuid?.bind(process), paths = {}) {
16
+ if ((0, first_party_harness_1.isDedicatedRuntimeWorkload)(env, getuid))
17
+ return true;
18
+ const socketPath = paths.socketPath ?? exports.GUEST_SERVICE_SOCKET_PATH;
19
+ const attestation = paths.attestationPath ?? '/etc/skrr-dedicated-runtime/attestation.json';
20
+ const runtimeDir = paths.runtimeDir ?? '/run/skrr-dedicated-runtime';
21
+ return (0, node_fs_1.existsSync)(socketPath) || (0, node_fs_1.existsSync)(attestation) || (0, node_fs_1.existsSync)(runtimeDir);
22
+ }
@@ -1280,7 +1280,23 @@ function describeRefusalReasons(err, context = {}) {
1280
1280
  if (spend)
1281
1281
  return spend;
1282
1282
  const sentences = (0, harnesses_1.describeHealthReasonSentences)(details?.reasons, details?.state);
1283
- return sentences ? ` Reasons: ${sentences}` : '';
1283
+ // A refusal whose public `reasons` came back empty used to print the bare
1284
+ // sentence, which reads as "no reason" when the server said more: that it
1285
+ // withheld reasons it does not publish, or that its own check did not finish
1286
+ // (OSK-12473). Say which, so the person knows whether to retry.
1287
+ const extra = [];
1288
+ const incomplete = Array.isArray(details?.checkIncompleteReasons)
1289
+ ? details.checkIncompleteReasons.length
1290
+ : 0;
1291
+ if (details?.checkIncomplete === true || incomplete > 0) {
1292
+ extra.push('skrr could not finish checking the machine, so run the check again.');
1293
+ }
1294
+ const omitted = Number(details?.omittedReasonCount);
1295
+ if (Number.isInteger(omitted) && omitted > 0) {
1296
+ extra.push(`${omitted} further reason${omitted === 1 ? ' was' : 's were'} not published by the server.`);
1297
+ }
1298
+ const tail = [sentences, ...extra].filter(Boolean).join(' ');
1299
+ return tail ? ` Reasons: ${tail}` : '';
1284
1300
  }
1285
1301
  /**
1286
1302
  * The numbers behind a cap refusal: the cap, this month's spend so far, and
@@ -373,16 +373,4 @@ export declare function mergeServiceTemplate(template: DedicatedServiceTemplate,
373
373
  * guest-local status commands so both read the same shape.
374
374
  */
375
375
  export declare function serviceDetailText(detail: unknown): string | undefined;
376
- /** The daemon's workload-facing service socket on a Dedicated Runtime guest. */
377
- export declare const GUEST_SERVICE_SOCKET_PATH = "/run/skrr-dedicated-runtime/service-propose/service-propose.sock";
378
- /**
379
- * True when this process is running on a Dedicated Runtime guest — the
380
- * workload-uid test, or the guest markers the image/daemon own. An SSH
381
- * session may not carry the wrapper's env stamp, so the filesystem markers
382
- * count too; the socket itself remains the authority.
383
- */
384
- export declare function runningOnDedicatedRuntimeGuest(env?: NodeJS.ProcessEnv, getuid?: (() => number) | undefined, paths?: {
385
- socketPath?: string;
386
- attestationPath?: string;
387
- runtimeDir?: string;
388
- }): boolean;
376
+ export { GUEST_SERVICE_SOCKET_PATH, runningOnDedicatedRuntimeGuest } from './dedicated-guest';
@@ -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.GUEST_SERVICE_SOCKET_PATH = exports.DedicatedServiceChannel = exports.DedicatedServiceError = exports.SERVICE_ACTIONS = exports.SERVICE_RESTART_POLICIES = exports.SERVICE_RESERVED_NAMES = exports.SERVICE_SECRET_NAME_PATTERN = exports.SERVICE_NAME_PATTERN = exports.SERVICE_CALL_TIMEOUT_MS = exports.DEDICATED_PERSISTENT_TERMINAL_CAPABILITY = exports.DEDICATED_TERMINAL_NAME_PATTERN = exports.DEDICATED_TERMINAL_EVENTS = exports.DEDICATED_SERVICE_EVENTS = void 0;
6
+ exports.runningOnDedicatedRuntimeGuest = exports.GUEST_SERVICE_SOCKET_PATH = exports.DedicatedServiceChannel = exports.DedicatedServiceError = exports.SERVICE_ACTIONS = exports.SERVICE_RESTART_POLICIES = exports.SERVICE_RESERVED_NAMES = exports.SERVICE_SECRET_NAME_PATTERN = exports.SERVICE_NAME_PATTERN = exports.SERVICE_CALL_TIMEOUT_MS = exports.DEDICATED_PERSISTENT_TERMINAL_CAPABILITY = exports.DEDICATED_TERMINAL_NAME_PATTERN = exports.DEDICATED_TERMINAL_EVENTS = exports.DEDICATED_SERVICE_EVENTS = void 0;
7
7
  exports.isServiceUnsupportedCode = isServiceUnsupportedCode;
8
8
  exports.describeServiceRefusal = describeServiceRefusal;
9
9
  exports.describeTerminalRefusal = describeTerminalRefusal;
@@ -19,7 +19,6 @@ exports.serviceTemplatesDir = serviceTemplatesDir;
19
19
  exports.loadLocalServiceTemplate = loadLocalServiceTemplate;
20
20
  exports.mergeServiceTemplate = mergeServiceTemplate;
21
21
  exports.serviceDetailText = serviceDetailText;
22
- exports.runningOnDedicatedRuntimeGuest = runningOnDedicatedRuntimeGuest;
23
22
  const node_crypto_1 = require("node:crypto");
24
23
  const node_fs_1 = require("node:fs");
25
24
  const node_os_1 = require("node:os");
@@ -27,7 +26,6 @@ const node_path_1 = __importDefault(require("node:path"));
27
26
  const socket_io_client_1 = require("socket.io-client");
28
27
  const data_provider_1 = require("@skrr-ai/data-provider");
29
28
  const agentic_stream_1 = require("./agentic-stream");
30
- const first_party_harness_1 = require("./first-party-harness");
31
29
  const format_1 = require("./format");
32
30
  /**
33
31
  * Declared services on a Dedicated Runtime — the CLI half of
@@ -801,19 +799,9 @@ function serviceDetailText(detail) {
801
799
  parts.push(d.detail);
802
800
  return parts.join(', ') || undefined;
803
801
  }
804
- /** The daemon's workload-facing service socket on a Dedicated Runtime guest. */
805
- exports.GUEST_SERVICE_SOCKET_PATH = '/run/skrr-dedicated-runtime/service-propose/service-propose.sock';
806
- /**
807
- * True when this process is running on a Dedicated Runtime guest — the
808
- * workload-uid test, or the guest markers the image/daemon own. An SSH
809
- * session may not carry the wrapper's env stamp, so the filesystem markers
810
- * count too; the socket itself remains the authority.
811
- */
812
- function runningOnDedicatedRuntimeGuest(env = process.env, getuid = process.getuid?.bind(process), paths = {}) {
813
- if ((0, first_party_harness_1.isDedicatedRuntimeWorkload)(env, getuid))
814
- return true;
815
- const socketPath = paths.socketPath ?? exports.GUEST_SERVICE_SOCKET_PATH;
816
- const attestation = paths.attestationPath ?? '/etc/skrr-dedicated-runtime/attestation.json';
817
- const runtimeDir = paths.runtimeDir ?? '/run/skrr-dedicated-runtime';
818
- return (0, node_fs_1.existsSync)(socketPath) || (0, node_fs_1.existsSync)(attestation) || (0, node_fs_1.existsSync)(runtimeDir);
819
- }
802
+ // Guest detection lives in its own light module so the auth path (base-command,
803
+ // login) can ask "is this a guest?" without loading socket.io and the stream
804
+ // client this module needs (OSK-12473). Re-exported for existing callers.
805
+ var dedicated_guest_1 = require("./dedicated-guest");
806
+ Object.defineProperty(exports, "GUEST_SERVICE_SOCKET_PATH", { enumerable: true, get: function () { return dedicated_guest_1.GUEST_SERVICE_SOCKET_PATH; } });
807
+ Object.defineProperty(exports, "runningOnDedicatedRuntimeGuest", { enumerable: true, get: function () { return dedicated_guest_1.runningOnDedicatedRuntimeGuest; } });
@@ -120,6 +120,13 @@ export type ResolvedFollowUpAgent = {
120
120
  */
121
121
  boundToSession: boolean;
122
122
  };
123
+ /**
124
+ * A trigger subject becomes a durable concurrency key, so it cannot be prose.
125
+ * Keep this grammar aligned with TriggerEngine's subject validation and the
126
+ * FollowUpService storage limit.
127
+ */
128
+ export declare const FOLLOWUP_SUBJECT_EXTERNAL_KEY_RE: RegExp;
129
+ export declare const FOLLOWUP_SUBJECT_FLAG_HELP = "Subject ref: type:externalKey (externalKey: letters, digits, _ - . : / # @ + ~; no spaces)";
123
130
  /**
124
131
  * Which agent a follow-up belongs to.
125
132
  *
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.followupsApi = exports.WATCH_WAIT_KINDS = exports.FOLLOWUP_RESOLVE_OUTCOMES = exports.FOLLOWUP_STATES = exports.FOLLOWUP_KINDS = exports.followUpIdArg = void 0;
3
+ exports.followupsApi = exports.FOLLOWUP_SUBJECT_FLAG_HELP = exports.FOLLOWUP_SUBJECT_EXTERNAL_KEY_RE = exports.WATCH_WAIT_KINDS = exports.FOLLOWUP_RESOLVE_OUTCOMES = exports.FOLLOWUP_STATES = exports.FOLLOWUP_KINDS = exports.followUpIdArg = void 0;
4
4
  exports.settledLine = settledLine;
5
5
  exports.recipeWatchBoundsRefusal = recipeWatchBoundsRefusal;
6
6
  exports.resolveFollowUpAgent = resolveFollowUpAgent;
@@ -115,6 +115,13 @@ function recipeWatchBoundsRefusal(waitKind, passed) {
115
115
  : 'A recipe watch ends through its --wait.',
116
116
  ].join(' ');
117
117
  }
118
+ /**
119
+ * A trigger subject becomes a durable concurrency key, so it cannot be prose.
120
+ * Keep this grammar aligned with TriggerEngine's subject validation and the
121
+ * FollowUpService storage limit.
122
+ */
123
+ exports.FOLLOWUP_SUBJECT_EXTERNAL_KEY_RE = /^[A-Za-z0-9_\-.:/#@+~]{1,300}$/;
124
+ exports.FOLLOWUP_SUBJECT_FLAG_HELP = 'Subject ref: type:externalKey (externalKey: letters, digits, _ - . : / # @ + ~; no spaces)';
118
125
  /**
119
126
  * Which agent a follow-up belongs to.
120
127
  *
@@ -154,11 +161,17 @@ function resolveFollowUpAgent(explicitAgentId, env = process.env) {
154
161
  }
155
162
  /** A `type:externalKey` subject ref → the subject block the API wants. */
156
163
  function parseSubjectRef(ref) {
157
- const idx = ref.indexOf(':');
158
- if (idx <= 0 || idx === ref.length - 1) {
164
+ const value = ref.trim();
165
+ const idx = value.indexOf(':');
166
+ if (idx <= 0 || idx === value.length - 1) {
159
167
  throw new Error(`--subject must be "type:externalKey" (e.g. task:OSK-42, conversation:conv_123), got "${ref}"`);
160
168
  }
161
- return { type: ref.slice(0, idx), externalKey: ref.slice(idx + 1) };
169
+ const type = value.slice(0, idx);
170
+ const externalKey = value.slice(idx + 1);
171
+ if (!exports.FOLLOWUP_SUBJECT_EXTERNAL_KEY_RE.test(externalKey)) {
172
+ throw new Error('--subject has an invalid external key. Use type:externalKey; the key may contain only letters, digits, _ - . : / # @ + ~ (no spaces). Put descriptive text in --why.');
173
+ }
174
+ return { type, externalKey };
162
175
  }
163
176
  /* ------------------------------------------------------------------ *
164
177
  * API *
package/dist/lib/login.js CHANGED
@@ -37,6 +37,7 @@ const device_code_1 = require("./device-code");
37
37
  const device_identity_1 = require("./device-identity");
38
38
  const oauthLogin_1 = require("./oauthLogin");
39
39
  const daemonBroker_1 = require("./daemonBroker");
40
+ const dedicated_guest_1 = require("./dedicated-guest");
40
41
  const daemonBrokerRefusal_1 = require("./daemonBrokerRefusal");
41
42
  /**
42
43
  * What a shell with no terminal can do instead (OSK-12117). The refusal used to
@@ -119,8 +120,16 @@ async function cliLogin(opts = {}) {
119
120
  // — the token lives for this process and the next `skrr` re-redeems
120
121
  // over loopback. An explicit PKCE/device-code login further down
121
122
  // still writes a real user-owned family, unchanged.
123
+ //
124
+ // Only a Dedicated guest's hand-off is the PLATFORM's. A laptop
125
+ // daemon's island file advertises `access_token` too, and there this
126
+ // sentence claimed a platform-managed machine that does not exist —
127
+ // the logout half of the same bug was OSK-12467 (c951446486).
122
128
  console.log('');
123
- console.log(" This machine is signed in through the platform's managed hand-off.");
129
+ console.log((0, dedicated_guest_1.runningOnDedicatedRuntimeGuest)()
130
+ ? " This machine is signed in through the platform's managed hand-off."
131
+ : ' Signed in through the skrr background service on this computer (no browser needed). ' +
132
+ 'It lends this CLI short-lived tokens, so nothing was stored for this profile.');
124
133
  return {
125
134
  token: brokerResult.accessToken,
126
135
  ...(brokerResult.accessExpiresAt !== undefined
@@ -23,6 +23,37 @@ export declare function __setStateForTest(next: CredEnvelopeState): CredEnvelope
23
23
  export declare function __getStateForTest(): CredEnvelopeState;
24
24
  /** @internal Test seam — each spec owns its warn-dedupe horizon. */
25
25
  export declare function __clearWarnDedupeForTest(): void;
26
+ /**
27
+ * Run the OSK-10314 consolidation for this profile's wrapped DEK.
28
+ *
29
+ * Under the canonical lock, because the window it closes is real: a daemon
30
+ * copying its legacy DEK into an empty canonical slot must not race a CLI
31
+ * enrolling a fresh one there — the loser's key would be overwritten and
32
+ * anything already sealed under it lost. That is the same race the lock was
33
+ * introduced for, one layer up.
34
+ *
35
+ * Never fatal. A migration that cannot run leaves the process exactly where it
36
+ * was, which is the state it has been operating in all along.
37
+ */
38
+ /**
39
+ * Whether a split profile may be converged onto the legacy (daemon) key.
40
+ *
41
+ * Only the daemon ever has a legacy location, so only the daemon runs this, and
42
+ * the legacy key is the one it signs with. Since OSK-12063 the CLI enrolls no
43
+ * device key and only an interactive daemon login re-pins, so the key the server
44
+ * pins for this machine is the daemon's, UNLESS the daemon has itself been
45
+ * refused over its device key. That is the one fact that proves otherwise, and
46
+ * then nothing here knows which key the server holds, so it refuses as before.
47
+ *
48
+ * Why converging matters (OSK-12462): a CLI credential brokered by the daemon
49
+ * is tied to the daemon's machine row, and the server checks its refresh proof
50
+ * against the key pinned there. A CLI signing with a different key has that
51
+ * refresh refused as DEVICE_PROOF_THUMBPRINT_MISMATCH at its first renewal.
52
+ */
53
+ export declare function splitKeyConvergencePolicy(): {
54
+ converge: boolean;
55
+ reason: 'daemon_key_pinned' | 'daemon_key_refused';
56
+ };
26
57
  /**
27
58
  * Initialize the envelope path. Idempotent — second call when state is
28
59
  * non-uninit returns immediately.
@@ -96,6 +127,16 @@ export declare function describeCredEnvelopeState(): {
96
127
  canonicalPath: string;
97
128
  legacyPath: string;
98
129
  };
130
+ /**
131
+ * Set when THIS process converged a split profile onto the legacy (daemon)
132
+ * key (OSK-12462). `canonicalPath` held the key that is now retired.
133
+ */
134
+ convergedFrom?: {
135
+ canonicalPath: string;
136
+ legacyPath: string;
137
+ };
138
+ /** How many retired DEKs this process can still open old values with. */
139
+ retiredKeys?: number;
99
140
  };
100
141
  /**
101
142
  * Sync write transform. Returns the wrapped serialized form when active.