@skrr-ai/cli 0.1.81 → 0.1.83

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 (32) hide show
  1. package/dist/base-command.d.ts +16 -0
  2. package/dist/base-command.js +124 -9
  3. package/dist/commands/followups/checkin.js +1 -1
  4. package/dist/commands/login.js +10 -0
  5. package/dist/commands/logout.js +4 -0
  6. package/dist/commands/store/installs.js +6 -1
  7. package/dist/lib/auth-storage.d.ts +5 -0
  8. package/dist/lib/auth-storage.js +8 -0
  9. package/dist/lib/config.d.ts +8 -0
  10. package/dist/lib/credential-resolver.js +7 -1
  11. package/dist/lib/daemonBroker.d.ts +35 -0
  12. package/dist/lib/daemonBroker.js +191 -11
  13. package/dist/lib/daemonBrokerRefusal.d.ts +14 -0
  14. package/dist/lib/daemonBrokerRefusal.js +18 -1
  15. package/dist/lib/first-party-harness-broker.d.ts +2 -0
  16. package/dist/lib/first-party-harness-broker.js +77 -3
  17. package/dist/lib/harness-context.d.ts +8 -0
  18. package/dist/lib/harness-context.js +55 -0
  19. package/dist/lib/login.js +4 -1
  20. package/dist/lib/refresh.js +4 -0
  21. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  22. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +4 -2
  23. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +25 -0
  24. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +35 -1
  25. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  26. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
  27. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +25 -0
  28. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +33 -0
  29. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  30. package/dist/node_modules/@skrr-ai/data-provider/index.js +3856 -3801
  31. package/oclif.manifest.json +19574 -19574
  32. package/package.json +2 -2
@@ -471,5 +471,21 @@ export declare function describeDaemonScopedCredential(bin: string, scope?: Inco
471
471
  * question. Merging the two is the mistake its own comment warns about.
472
472
  */
473
473
  export declare function isTypedServerRefusal(body: Record<string, unknown> | null): boolean;
474
+ /**
475
+ * Did the API itself answer this gateway-class status (502/503/504) with a
476
+ * typed code?
477
+ *
478
+ * Those statuses mean "outage" only when something IN FRONT of the API wrote
479
+ * them — the load balancer or CDN, whose bodies are HTML or empty, and which
480
+ * `parseErrorBody` therefore reads as null. A JSON body carrying `code` or
481
+ * `errorType` was written by a route or middleware that ran and chose the
482
+ * status: the API was up, and the request did not time out at the gateway.
483
+ *
484
+ * Checked AFTER `isTypedServerRefusal`, which handles the subset that also
485
+ * explains itself; this covers the coded rest — including production bodies
486
+ * whose message the 5xx redaction boundary replaced. A JSON body with no code
487
+ * stays in the outage branch: it says nothing a reader could act on.
488
+ */
489
+ export declare function isApplicationCodedUnavailable(status: number | undefined, body: Record<string, unknown> | null): boolean;
474
490
  export declare function isRetryableFailure(status: number | undefined, body: Record<string, unknown> | null): boolean;
475
491
  export { Flags };
@@ -15,6 +15,7 @@ exports.describeForbidden = describeForbidden;
15
15
  exports.isDaemonScopedCredential = isDaemonScopedCredential;
16
16
  exports.describeDaemonScopedCredential = describeDaemonScopedCredential;
17
17
  exports.isTypedServerRefusal = isTypedServerRefusal;
18
+ exports.isApplicationCodedUnavailable = isApplicationCodedUnavailable;
18
19
  exports.isRetryableFailure = isRetryableFailure;
19
20
  const node_fs_1 = require("node:fs");
20
21
  const core_1 = require("@oclif/core");
@@ -54,6 +55,7 @@ const edge_error_page_1 = require("./lib/edge-error-page");
54
55
  const step_up_1 = require("./lib/step-up");
55
56
  const delegated_cli_1 = require("./lib/delegated-cli");
56
57
  const dedicated_guest_1 = require("./lib/dedicated-guest");
58
+ const harness_context_1 = require("./lib/harness-context");
57
59
  const tasks_1 = require("./lib/tasks");
58
60
  function assigneePayloadNeedsResolution(payload) {
59
61
  const fields = [
@@ -163,12 +165,17 @@ class BaseCommand extends core_1.Command {
163
165
  const result = await (0, daemonBroker_1.redeemBrokeredHandoffToken)(true);
164
166
  if (!result?.accessToken)
165
167
  throw new Error('The local daemon could not renew CLI authentication.');
166
- this.resolvedCredential = { token: result.accessToken, source: 'env-token',
167
- kind: (0, auth_core_1.classifyTokenKind)(result.accessToken) };
168
+ this.resolvedCredential = {
169
+ token: result.accessToken,
170
+ source: 'env-token',
171
+ kind: (0, auth_core_1.classifyTokenKind)(result.accessToken),
172
+ };
168
173
  }
169
174
  else {
170
- this.resolvedCredential = (await (0, refresh_1.ensureFreshCliCredential)({ credential: this.resolvedCredential,
171
- bareMode: this.bareMode })).credential;
175
+ this.resolvedCredential = (await (0, refresh_1.ensureFreshCliCredential)({
176
+ credential: this.resolvedCredential,
177
+ bareMode: this.bareMode,
178
+ })).credential;
172
179
  }
173
180
  if (!this.resolvedCredential.token)
174
181
  throw new Error('CLI authentication is unavailable.');
@@ -272,6 +279,21 @@ class BaseCommand extends core_1.Command {
272
279
  (0, data_provider_1.setHttpAdapter)((0, node_adapter_1.createNodeAdapter)());
273
280
  return;
274
281
  }
282
+ // OSK-13254 — a process the daemon started for an agent or tool borrows
283
+ // from the daemon and never owns the person's stored session, so the
284
+ // commands that create, replace or end that session are not its to run.
285
+ const harnessContext = (0, harness_context_1.isHarnessCliContext)();
286
+ if (harnessContext && this.id && harness_context_1.HARNESS_REFUSED_AUTH_COMMANDS.has(this.id)) {
287
+ this.failWithCliError({
288
+ message: `\`${this.config.bin} ${this.id}\` is not available here: this process was started by the skrr ` +
289
+ 'background service for an agent or tool, and skrr in it borrows short-lived tokens from ' +
290
+ 'that service instead of holding a sign-in. Run it in your own terminal.',
291
+ code: 'HARNESS_CONTEXT_AUTH_COMMAND',
292
+ exit: 2,
293
+ retryable: false,
294
+ });
295
+ return;
296
+ }
275
297
  // A replacement guest keeps its workspace but receives a fresh Linux
276
298
  // machine-id. Retire the prior instance's machine-bound CLI envelopes
277
299
  // before migration/prefetch reads them; the current daemon handoff below
@@ -280,7 +302,9 @@ class BaseCommand extends core_1.Command {
280
302
  // One-shot migration: legacy token/refreshToken in cli-config.json
281
303
  // → keychain (macOS) or cli-auth.json (elsewhere). Idempotent.
282
304
  try {
283
- (0, migrate_1.migrateAuthOutOfConfig)();
305
+ // A borrower never writes the person's stored session (OSK-13254).
306
+ if (!harnessContext)
307
+ (0, migrate_1.migrateAuthOutOfConfig)();
284
308
  }
285
309
  catch {
286
310
  // Never block a command on migration failure; the resolver still
@@ -336,9 +360,18 @@ class BaseCommand extends core_1.Command {
336
360
  allowUnreadableStoredCredential: this.id === 'login' || this.id === 'logout' || this.id === 'pair',
337
361
  });
338
362
  let ignoredStoredCredential = null;
339
- if (!this.resolvedCredential.token && !this.bareMode && !parsed.token) {
363
+ if (!this.resolvedCredential.token && !this.bareMode && !parsed.token && !harnessContext) {
340
364
  ignoredStoredCredential = (0, auth_storage_1.readFromBackend)(this.cliConfig.baseURL);
341
365
  }
366
+ // OSK-13254 — nothing at all is stored for this profile: a first sign-in,
367
+ // which may ADOPT a CLI session of its own through the daemon instead of
368
+ // borrowing per process. Anything stored — a refused session, another
369
+ // server's, another config root's — is left alone and the daemon lends.
370
+ const nothingStored = !harnessContext &&
371
+ !!ignoredStoredCredential &&
372
+ !ignoredStoredCredential.bundle &&
373
+ ignoredStoredCredential.ownerStatus !== 'foreign' &&
374
+ (ignoredStoredCredential.originStatus ?? 'none') === 'none';
342
375
  // Daemon-as-broker auto-mint. When the resolver came up empty AND
343
376
  // a local `oversky` daemon is already authenticated for this user,
344
377
  // silently mint a sibling cli-scope refresh family via the daemon's
@@ -366,8 +399,14 @@ class BaseCommand extends core_1.Command {
366
399
  cliConfig: this.cliConfig,
367
400
  bareMode: this.bareMode,
368
401
  commandId: this.id,
402
+ adopt: nothingStored,
369
403
  }));
370
- if (autoResult.triggered && autoResult.outcome) {
404
+ if (autoResult.adoptedBySibling) {
405
+ // Another `skrr` adopted this profile's session while this one waited
406
+ // for the adoption lock: read it like any stored session.
407
+ this.resolvedCredential = await (0, credential_resolver_1.resolveCredential)({ bareMode: this.bareMode });
408
+ }
409
+ else if (autoResult.triggered && autoResult.outcome) {
371
410
  if (autoResult.outcome.ok) {
372
411
  // Persist the cliId now so a sibling skrr process spawned
373
412
  // immediately after sees the same identity.
@@ -415,6 +454,14 @@ class BaseCommand extends core_1.Command {
415
454
  : {}),
416
455
  }
417
456
  : null;
457
+ if (!autoResult.outcome.brokered && nothingStored) {
458
+ // Said once, on stderr, the moment it happens: a session now exists
459
+ // that the person did not type a command to create (OSK-13254).
460
+ process.stderr.write(`[skrr] Signed this CLI in through the skrr background service on this computer. ` +
461
+ `The session is saved to ${(0, credential_resolver_1.describeCredentialSource)(this.resolvedCredential.source)} ` +
462
+ `and renews itself, so skrr keeps working while the service restarts. ` +
463
+ `It is listed under Active Sessions; \`${this.config.bin} logout\` ends it.\n`);
464
+ }
418
465
  }
419
466
  else if ((this.brokerRefusal =
420
467
  (0, daemonBrokerRefusal_1.describeBrokerRefusal)(autoResult.outcome, this.config.bin) ??
@@ -1215,7 +1262,7 @@ class BaseCommand extends core_1.Command {
1215
1262
  // login to run, so its remedy replaces the generic "Run login".
1216
1263
  const brokerRefusal = presentedCiToken || presentedPat || !e.brokerFailure
1217
1264
  ? null
1218
- : (0, daemonBrokerRefusal_1.describeBrokerRefusal)(e.brokerFailure, this.config.bin);
1265
+ : (0, daemonBrokerRefusal_1.describeBrokerFailure)(e.brokerFailure, this.config.bin);
1219
1266
  const renewal = refreshRefusal
1220
1267
  ? refreshRefusal.message.replace(/\s*Run `[^`]+` to sign in again\.$/, '')
1221
1268
  : "This CLI's own sign-in could not be renewed.";
@@ -1361,6 +1408,45 @@ class BaseCommand extends core_1.Command {
1361
1408
  requestId: stringProp(body, 'requestId') || e.requestId,
1362
1409
  });
1363
1410
  }
1411
+ else if (isApplicationCodedUnavailable(e.status, body)) {
1412
+ // A 502/503/504 the API ITSELF wrote, with a typed code — not a page
1413
+ // from the load balancer or CDN. The outage branch below would say the
1414
+ // platform is down, absolve this command, and (for a write) call the
1415
+ // outcome unknown because "the gateway may have timed out after the
1416
+ // server committed it". None of that holds when the API answered.
1417
+ //
1418
+ // Observed 2026-10-01: `skrr agents chat --daemon <id>` printed exactly
1419
+ // that outage paragraph for `POST /api/agentic/chat` → 503
1420
+ // `preferred_daemon_unavailable` — "The selected local daemon is not
1421
+ // connected" — a synchronous refusal before anything was enqueued,
1422
+ // while skrr.ai served every other request. The reader went looking
1423
+ // for a platform outage.
1424
+ //
1425
+ // No incident is recorded: one route refusing in its own words is not
1426
+ // evidence the control plane is failing. `isOutageFailure` is left
1427
+ // alone because it also decides whether the outbox queues a write.
1428
+ const retryable = isRetryableFailure(e.status, body);
1429
+ const authored = bodyMessage && !isRedactedServerMessage(bodyMessage) ? bodyMessage : '';
1430
+ const requestId = stringProp(body, 'requestId') || e.requestId;
1431
+ this.failWithCliError({
1432
+ message: say(`${this.cliConfig.baseURL} refused this request (HTTP ${e.status}, ${bodyCode})` +
1433
+ `${authored ? `: ${authored}` : '.'} ` +
1434
+ 'The API answered this itself, so the request did not time out at the gateway. ' +
1435
+ (authored ? '' : 'It did not say why in this response. ') +
1436
+ (retryable
1437
+ ? 'Retrying may succeed once that condition clears.'
1438
+ : 'Retrying the same request will not help until that condition changes.') +
1439
+ (requestId && !authored
1440
+ ? ` Request id ${requestId} — quote it when reporting this.`
1441
+ : '')),
1442
+ code: bodyCode,
1443
+ status: e.status,
1444
+ exit: retryable ? 5 : 1,
1445
+ retryable,
1446
+ details: body ?? undefined,
1447
+ ...(requestId ? { requestId } : {}),
1448
+ });
1449
+ }
1364
1450
  else if ((0, outbox_1.isOutageFailure)(e)) {
1365
1451
  // A gateway/unavailable status is an OUTAGE, and saying so is the whole
1366
1452
  // point. During the OSK-2806 incident these surfaced as bare
@@ -1541,7 +1627,7 @@ class BaseCommand extends core_1.Command {
1541
1627
  cliId: this.cliConfig ? (0, cli_id_1.resolveCliId)(this.cliConfig) : undefined,
1542
1628
  });
1543
1629
  const brokerRefusal = e.brokerFailure
1544
- ? (0, daemonBrokerRefusal_1.describeBrokerRefusal)(e.brokerFailure, this.config.bin)
1630
+ ? (0, daemonBrokerRefusal_1.describeBrokerFailure)(e.brokerFailure, this.config.bin)
1545
1631
  : null;
1546
1632
  const why = (refusal
1547
1633
  ? refusal.message.replace(/\s*Run `[^`]+` to sign in again\.$/, '')
@@ -2059,6 +2145,35 @@ function isTypedServerRefusal(body) {
2059
2145
  return false;
2060
2146
  return (typeof body.cause === 'string' || typeof body.remedy === 'string' || body.retryable === false);
2061
2147
  }
2148
+ /**
2149
+ * Did the API itself answer this gateway-class status (502/503/504) with a
2150
+ * typed code?
2151
+ *
2152
+ * Those statuses mean "outage" only when something IN FRONT of the API wrote
2153
+ * them — the load balancer or CDN, whose bodies are HTML or empty, and which
2154
+ * `parseErrorBody` therefore reads as null. A JSON body carrying `code` or
2155
+ * `errorType` was written by a route or middleware that ran and chose the
2156
+ * status: the API was up, and the request did not time out at the gateway.
2157
+ *
2158
+ * Checked AFTER `isTypedServerRefusal`, which handles the subset that also
2159
+ * explains itself; this covers the coded rest — including production bodies
2160
+ * whose message the 5xx redaction boundary replaced. A JSON body with no code
2161
+ * stays in the outage branch: it says nothing a reader could act on.
2162
+ */
2163
+ function isApplicationCodedUnavailable(status, body) {
2164
+ if (status !== 502 && status !== 503 && status !== 504)
2165
+ return false;
2166
+ return Boolean(stringProp(body, 'code') || stringProp(body, 'errorType'));
2167
+ }
2168
+ /**
2169
+ * The sentence `redactServerErrorResponses` substitutes for a 5xx message in
2170
+ * production. It is the boundary's, not the route's, so it is never relayed as
2171
+ * if it explained anything.
2172
+ */
2173
+ const REDACTED_SERVER_MESSAGE = 'An unexpected error occurred.';
2174
+ function isRedactedServerMessage(message) {
2175
+ return message.trim() === REDACTED_SERVER_MESSAGE;
2176
+ }
2062
2177
  function isRetryableFailure(status, body) {
2063
2178
  const declared = body?.retryable;
2064
2179
  if (typeof declared === 'boolean')
@@ -49,7 +49,7 @@ class FollowupsCheckin extends base_command_1.BaseCommand {
49
49
  catch (err) {
50
50
  this.failWithCliError({
51
51
  message: err instanceof Error ? err.message : String(err),
52
- code: 'FOLLOWUP_SUBJECT_INVALID',
52
+ code: 'FOLLOWUP_INVALID_FIELD',
53
53
  exit: 2,
54
54
  retryable: false,
55
55
  });
@@ -126,6 +126,16 @@ class Login extends base_command_1.BaseCommand {
126
126
  `(${explicitBaseUrl}). Pass one of them.`, { exit: 2 });
127
127
  }
128
128
  const requestedBaseUrl = envBaseUrl ?? explicitBaseUrl;
129
+ // OSK-13254 — an explicit sign-in lifts what `skrr logout` set: from here a
130
+ // command that finds no stored session may adopt one through the daemon
131
+ // again. Cleared up front, because asking to sign in is the decision.
132
+ {
133
+ const current = (0, config_1.loadConfig)();
134
+ if (current.adoptDaemonSession === false) {
135
+ const { adoptDaemonSession: _cleared, ...rest } = current;
136
+ (0, config_1.saveConfig)(rest);
137
+ }
138
+ }
129
139
  /**
130
140
  * Undo the base-URL write if this login does not complete.
131
141
  *
@@ -260,6 +260,10 @@ class Logout extends base_command_1.BaseCommand {
260
260
  ...(agentSetting.value ? { [first_party_harness_managed_1.AGENT_CONFIG_KEY]: agentSetting.value } : {}),
261
261
  ...(cfg.credentials ? { credentials: cfg.credentials } : {}),
262
262
  migrated: { ...(cfg.migrated ?? {}), authOutOfConfig: true },
263
+ // OSK-13254 — signed out means no durable session reappears behind this
264
+ // command. The daemon may still LEND this CLI short-lived tokens (said
265
+ // below); `skrr login` clears the flag.
266
+ adoptDaemonSession: false,
263
267
  });
264
268
  const finalMessage = flags['all-devices']
265
269
  ? allDevicesError
@@ -44,7 +44,12 @@ class StoreInstalls extends base_command_1.BaseCommand {
44
44
  release: install.installedReleaseId ?? '-',
45
45
  version: install.installedVersionLabel ?? '-',
46
46
  state: install.copyMissing ? 'copy missing' : install.stopReason ? 'stopped' : 'installed',
47
- reason: install.stopReason ?? '-',
47
+ // The server's projection decides both (OSK-13290): `stopReason` is
48
+ // the shared stop vocabulary (`recalled`, `revoked`, …). An advisory
49
+ // that does not stop the copy outright (a `revoke` recall still lets
50
+ // its owner chat with it) is named rather than shown as '-'.
51
+ reason: install.stopReason ??
52
+ (install.advisory?.remedy ? `advisory: ${install.advisory.remedy}` : '-'),
48
53
  })), [
49
54
  { key: 'agent', header: 'LISTING' },
50
55
  { key: 'copy', header: 'YOUR COPY' },
@@ -124,6 +124,11 @@ export declare function describeForeignCredential(result: ReadResult): string |
124
124
  * a specific backend (bare mode, store-mode overrides) should call the
125
125
  * lower-level write helpers directly — not exported here to keep the
126
126
  * default path honest.
127
+ *
128
+ * Refuses in a process the daemon started for an agent or tool (OSK-13254):
129
+ * the person's stored session is theirs, and a borrower never writes it. This
130
+ * is the one choke point every persisting path goes through, so the policy
131
+ * cannot be missed by a new caller.
127
132
  */
128
133
  export declare function writeToBackend(bundle: AuthBundle, options?: WriteOptions): AuthBackend;
129
134
  /**
@@ -102,6 +102,7 @@ const keychain = __importStar(require("./keychain"));
102
102
  const cred_envelope_1 = require("./cred-envelope");
103
103
  const config_1 = require("./config");
104
104
  const cli_id_1 = require("./cli-id");
105
+ const harness_context_1 = require("./harness-context");
105
106
  /** Filename for the non-darwin file backend. Parallel to the daemon's
106
107
  * `auth.json`, deliberately distinct so the two binaries never clobber
107
108
  * each other's tokens (CLI holds scope=cli, daemon holds scope=daemon). */
@@ -759,8 +760,15 @@ function describeForeignCredential(result) {
759
760
  * a specific backend (bare mode, store-mode overrides) should call the
760
761
  * lower-level write helpers directly — not exported here to keep the
761
762
  * default path honest.
763
+ *
764
+ * Refuses in a process the daemon started for an agent or tool (OSK-13254):
765
+ * the person's stored session is theirs, and a borrower never writes it. This
766
+ * is the one choke point every persisting path goes through, so the policy
767
+ * cannot be missed by a new caller.
762
768
  */
763
769
  function writeToBackend(bundle, options = {}) {
770
+ if ((0, harness_context_1.isHarnessCliContext)())
771
+ throw new harness_context_1.HarnessCredentialPersistRefused();
764
772
  const requestedOrigin = options.serverOrigin ?? bundle.serverOrigin;
765
773
  let persistedBundle = bundle;
766
774
  if (requestedOrigin !== undefined) {
@@ -46,6 +46,14 @@ export interface CliConfig {
46
46
  * `resolveCliId()` (`cli-id.ts`), the one resolver every caller goes through.
47
47
  */
48
48
  profileCliId?: string;
49
+ /**
50
+ * OSK-13254 — `false` once `skrr logout` has signed this profile out: from
51
+ * then on a command that finds no stored session BORROWS a short-lived token
52
+ * from the local daemon for that process and does not adopt a durable CLI
53
+ * session of its own. Absent means adoption is allowed. `skrr login` clears
54
+ * it. Read through `cliSessionAdoptionBlocker` (`daemonBroker.ts`).
55
+ */
56
+ adoptDaemonSession?: boolean;
49
57
  /**
50
58
  * OSK-3892 — the agent a locally-hosted `skrr code` session is attributed to.
51
59
  *
@@ -31,6 +31,7 @@ const auth_core_init_1 = require("./auth-core-init");
31
31
  const config_1 = require("./config");
32
32
  const auth_storage_1 = require("./auth-storage");
33
33
  const cred_envelope_1 = require("./cred-envelope");
34
+ const harness_context_1 = require("./harness-context");
34
35
  /**
35
36
  * Return the only credential an Agent-owned CLI process is allowed to use.
36
37
  *
@@ -68,7 +69,12 @@ async function resolveCredential(opts = {}) {
68
69
  return undefined;
69
70
  }
70
71
  })();
71
- const storeMode = opts.storeModeOverride ?? cfg?.credentials?.store ?? 'auto';
72
+ // A process the daemon started for an agent or tool never reads the
73
+ // person's stored CLI session (OSK-13254): it borrows from the daemon, or
74
+ // uses a token it was explicitly given.
75
+ const storeMode = (0, harness_context_1.isHarnessCliContext)()
76
+ ? 'env-only'
77
+ : (opts.storeModeOverride ?? cfg?.credentials?.store ?? 'auto');
72
78
  const bareMode = opts.bareMode || ['1', 'true', 'TRUE', 'yes'].includes(process.env.OVERSKY_BARE_MODE ?? '');
73
79
  // Clear an observation left by a prior, unrelated direct backend probe in
74
80
  // this process. Only a blocked read made by THIS resolver should trigger the
@@ -246,6 +246,14 @@ export interface DaemonBrokerOptions {
246
246
  * regardless — its secret cannot mint a family at all.
247
247
  */
248
248
  preferHandoffMode?: 'access_token' | 'refresh_family';
249
+ /**
250
+ * Never take the durable mint, whatever the descriptor offers (OSK-13254).
251
+ * Set for a process the daemon started for an agent or tool
252
+ * (`harness-context.ts`): it borrows an access token or nothing. A v1
253
+ * descriptor, which offers only the durable mint, is reported as unable to
254
+ * lend rather than minting a family nobody would hold.
255
+ */
256
+ borrowOnly?: boolean;
249
257
  /**
250
258
  * Overrides for the bounded wait on a restarting local daemon (OSK-13222):
251
259
  * budgets, the clock and the supervisor probe. Tests use it; production
@@ -472,6 +480,22 @@ export interface MaybeAutoBrokerOptions {
472
480
  * profile, and otherwise reports NOT_SIGNED_IN for the profile it was given.
473
481
  */
474
482
  profile?: string;
483
+ /**
484
+ * Adopt a CLI session of this profile's own instead of borrowing (OSK-13254).
485
+ *
486
+ * Set by `BaseCommand.init` ONLY when the resolver found no stored
487
+ * credential at all — a first sign-in. The 401-recovery callers leave it
488
+ * unset: a stored session that was refused may have been revoked on purpose
489
+ * (Active Sessions, a password change), and re-adopting a new one behind
490
+ * that decision would undo it. They borrow, as before.
491
+ *
492
+ * Ignored (borrow instead) in a harness context, after `skrr logout`
493
+ * (`cliConfig.adoptDaemonSession === false`), and under
494
+ * `SKRR_NO_CLI_SESSION_ADOPTION=1`. See {@link cliSessionAdoptionBlocker}.
495
+ */
496
+ adopt?: boolean;
497
+ /** Test seam: lock-acquisition budget for the adoption lock. */
498
+ adoptionLockRetries?: number;
475
499
  }
476
500
  export interface MaybeAutoBrokerResult {
477
501
  /**
@@ -489,7 +513,18 @@ export interface MaybeAutoBrokerResult {
489
513
  * and end up with a divergent refresh family.
490
514
  */
491
515
  updatedConfig?: CliConfig;
516
+ /**
517
+ * True when this process waited on the adoption lock and found that a
518
+ * sibling `skrr` had adopted a session for this profile meanwhile. Nothing
519
+ * was minted; the caller re-reads the stored credential (OSK-13254).
520
+ */
521
+ adoptedBySibling?: boolean;
492
522
  }
523
+ /**
524
+ * Why this invocation must borrow instead of adopting a CLI session of its
525
+ * own, or null when it may adopt (OSK-13254).
526
+ */
527
+ export declare function cliSessionAdoptionBlocker(cliConfig: Pick<CliConfig, 'adoptDaemonSession'>, env?: NodeJS.ProcessEnv): 'harness_context' | 'signed_out' | 'opted_out' | null;
493
528
  /**
494
529
  * Auto-broker pre-flight for `BaseCommand.init()`. When the credential
495
530
  * resolver returns no token, this attempts a silent daemon broker so