@skrr-ai/cli 0.1.81 → 0.1.82

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 (30) 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 +187 -11
  13. package/dist/lib/daemonBrokerRefusal.d.ts +14 -0
  14. package/dist/lib/daemonBrokerRefusal.js +18 -1
  15. package/dist/lib/harness-context.d.ts +8 -0
  16. package/dist/lib/harness-context.js +55 -0
  17. package/dist/lib/login.js +4 -1
  18. package/dist/lib/refresh.js +4 -0
  19. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  20. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +4 -2
  21. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +25 -0
  22. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +35 -1
  23. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  24. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
  25. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +25 -0
  26. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +33 -0
  27. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  28. package/dist/node_modules/@skrr-ai/data-provider/index.js +3856 -3801
  29. package/oclif.manifest.json +15792 -15792
  30. package/package.json +2 -2
@@ -70,6 +70,9 @@ var __importStar = (this && this.__importStar) || (function () {
70
70
  return result;
71
71
  };
72
72
  })();
73
+ var __importDefault = (this && this.__importDefault) || function (mod) {
74
+ return (mod && mod.__esModule) ? mod : { "default": mod };
75
+ };
73
76
  Object.defineProperty(exports, "__esModule", { value: true });
74
77
  exports.HOSTED_MACHINE_CLI_HANDOFF_DESCRIPTOR = exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR = void 0;
75
78
  exports.normalizeBaseUrl = normalizeBaseUrl;
@@ -89,16 +92,19 @@ exports.findBrokeredHandoffDescriptor = findBrokeredHandoffDescriptor;
89
92
  exports.maybeRetireStoredCliAuthFile = maybeRetireStoredCliAuthFile;
90
93
  exports.shouldBrokerBeforeStoredCredential = shouldBrokerBeforeStoredCredential;
91
94
  exports.attemptDaemonBrokerLoginAndPersist = attemptDaemonBrokerLoginAndPersist;
95
+ exports.cliSessionAdoptionBlocker = cliSessionAdoptionBlocker;
92
96
  exports.maybeAutoBroker = maybeAutoBroker;
93
97
  const fs = __importStar(require("node:fs"));
94
98
  const path = __importStar(require("node:path"));
95
99
  const os = __importStar(require("node:os"));
100
+ const proper_lockfile_1 = __importDefault(require("proper-lockfile"));
96
101
  const auth_core_1 = require("@skrr-ai/auth-core");
97
102
  const loopback_http_1 = require("@skrr-ai/auth-core/loopback-http");
98
103
  const cli_handoff_wire_1 = require("@skrr-ai/auth-core/cli-handoff-wire");
99
104
  const brokered_dpop_1 = require("./brokered-dpop");
100
105
  const auth_storage_1 = require("./auth-storage");
101
106
  const config_1 = require("./config");
107
+ const harness_context_1 = require("./harness-context");
102
108
  const cli_identity_1 = require("./cli-identity");
103
109
  const daemonRestartWait_1 = require("./daemonRestartWait");
104
110
  const keychain_1 = require("./keychain");
@@ -562,8 +568,19 @@ async function brokerThrough(bootstrap, opts, sourcePath) {
562
568
  // would either break explicit login or be bypassed by anyone who calls it
563
569
  // the way login does. It is not a boundary, so it is not attempted. What the
564
570
  // CLI CAN guarantee is that its own automatic path never mints and persists
565
- // a refresh family: when `access_token` is advertised, a failed redeem is
566
- // the answer, and the daemon's outage is reported with its repair.
571
+ // a refresh family on a borrower's behalf: when `access_token` is advertised,
572
+ // a failed redeem is the answer, and the daemon's outage is reported with its
573
+ // repair.
574
+ //
575
+ // OSK-13254 narrowed "the automatic path" to mean a BORROWER. A person's
576
+ // first `skrr` on a profile with nothing stored now ADOPTS a CLI session of
577
+ // its own (`maybeAutoBroker` with `adopt`, `preferHandoffMode:
578
+ // 'refresh_family'`), because a sign-in that lived only as long as the daemon
579
+ // read as "signed out" every time the daemon restarted. What OSK-12018 was
580
+ // guarding against — an agent-originated implicit sign-in persisting a family
581
+ // — is now refused by name: a process the daemon started for an agent or tool
582
+ // carries `SKRR_DAEMON_CHILD` / `OVERSKY_AUTONOMOUS_SESSION`
583
+ // (`harness-context.ts`), sets `borrowOnly`, and `writeToBackend` refuses it.
567
584
  //
568
585
  // COMPATIBILITY WINDOW: a v1 descriptor (no `handoffModes`, so no
569
586
  // `access_token`) still takes the durable mint on the automatic path below,
@@ -578,7 +595,17 @@ async function brokerThrough(bootstrap, opts, sourcePath) {
578
595
  const offersAccessToken = (0, cli_handoff_wire_1.descriptorOffersAccessToken)(bootstrap);
579
596
  const offersRefreshFamily = modes.includes('refresh_family');
580
597
  const preferAccessToken = opts.preferHandoffMode === 'access_token';
581
- if (offersAccessToken && (preferAccessToken || !offersRefreshFamily)) {
598
+ if (opts.borrowOnly && !offersAccessToken) {
599
+ return {
600
+ ok: false,
601
+ reason: 'unknown',
602
+ refusedBy: 'daemon',
603
+ code: 'HANDOFF_BORROW_UNAVAILABLE',
604
+ detail: 'The skrr background service on this computer is too old to lend this agent process a ' +
605
+ 'short-lived token, and a process it started for an agent never stores a sign-in. Update skrrd.',
606
+ };
607
+ }
608
+ if (offersAccessToken && (preferAccessToken || opts.borrowOnly || !offersRefreshFamily)) {
582
609
  const redeem = await redeemCliHandoffToken(bootstrap, {
583
610
  forceRefresh: false,
584
611
  timeoutMs: opts.timeoutMs,
@@ -1150,6 +1177,16 @@ async function attemptDaemonBrokerLoginAndPersist(opts) {
1150
1177
  // Callers use the token in-memory for this process only.
1151
1178
  if (outcome.brokered)
1152
1179
  return outcome;
1180
+ // Unreachable through `brokerThrough`, which never mints for a borrower;
1181
+ // kept so a future route cannot persist on a borrower's behalf.
1182
+ if (opts.borrowOnly) {
1183
+ return {
1184
+ ok: false,
1185
+ reason: 'unknown',
1186
+ code: 'HANDOFF_BORROW_UNAVAILABLE',
1187
+ detail: 'A borrower was handed a durable credential; it was not stored.',
1188
+ };
1189
+ }
1153
1190
  const serverOrigin = normalizeBaseUrl(opts.baseURL);
1154
1191
  if (!serverOrigin) {
1155
1192
  throw new Error('Daemon-broker credential persistence requires a valid CLI baseURL.');
@@ -1199,6 +1236,123 @@ const AUTH_SELF_MANAGED_COMMANDS = new Set([
1199
1236
  'pair',
1200
1237
  'generate-recovery-code',
1201
1238
  ]);
1239
+ /**
1240
+ * Why this invocation must borrow instead of adopting a CLI session of its
1241
+ * own, or null when it may adopt (OSK-13254).
1242
+ */
1243
+ function cliSessionAdoptionBlocker(cliConfig, env = process.env) {
1244
+ if ((0, harness_context_1.isHarnessCliContext)(env))
1245
+ return 'harness_context';
1246
+ if (env.SKRR_NO_CLI_SESSION_ADOPTION === '1')
1247
+ return 'opted_out';
1248
+ // `skrr logout` said "this CLI is signed out". The daemon may still LEND
1249
+ // tokens (logout says so), but a durable session does not reappear behind
1250
+ // that statement; the next `skrr login` clears the flag.
1251
+ if (cliConfig.adoptDaemonSession === false)
1252
+ return 'signed_out';
1253
+ return null;
1254
+ }
1255
+ /**
1256
+ * Failures that mean no daemon answered (or answered for another server) —
1257
+ * a second attempt on the token route cannot do better and would only wait
1258
+ * out the restart window twice.
1259
+ */
1260
+ function daemonDidNotAnswer(failure) {
1261
+ switch (failure.reason) {
1262
+ case 'no_bootstrap':
1263
+ case 'stale_bootstrap':
1264
+ case 'bootstrap_parse':
1265
+ case 'base_url_mismatch':
1266
+ case 'network':
1267
+ case 'timeout':
1268
+ return true;
1269
+ default:
1270
+ return false;
1271
+ }
1272
+ }
1273
+ /** The adoption lock: one per config root and profile. */
1274
+ function adoptionLockPath(profile) {
1275
+ const safe = profile.replace(/[^A-Za-z0-9._-]/g, '_') || 'default';
1276
+ return path.join((0, config_1.configRoot)(), `cli-session-adopt.${safe}.lock`);
1277
+ }
1278
+ function storedAccessToken(baseURL) {
1279
+ try {
1280
+ return (0, auth_storage_1.readFromBackend)(baseURL).bundle?.token ?? null;
1281
+ }
1282
+ catch {
1283
+ return null;
1284
+ }
1285
+ }
1286
+ /**
1287
+ * Adopt a CLI session through the local daemon (OSK-13254): the durable mint
1288
+ * (`/v1/auth/cli-handoff` → `POST /api/daemons/cli-handoff`), persisted like a
1289
+ * browser login, so that the next `skrr` reads it from the Keychain/file and
1290
+ * renews it itself — the daemon restarting no longer reads as signed out.
1291
+ *
1292
+ * Serialized across processes. The confirm that follows a mint retires every
1293
+ * OTHER live family of this cliId, so two concurrent adopters would each
1294
+ * revoke the other's session mid-command. Under the lock a process that
1295
+ * finds a session a sibling stored while it waited uses that one instead.
1296
+ *
1297
+ * Never worse than borrowing: if the lock cannot be taken in time, or the
1298
+ * daemon answered but would not mint (rate limit, a server refusal, a
1299
+ * broker-only descriptor), this borrows an access token for this process
1300
+ * exactly as before.
1301
+ */
1302
+ async function adoptCliSessionThroughDaemon(base, lockRetries) {
1303
+ const profile = base.profile ?? 'default';
1304
+ const baseURL = base.baseURL ?? '';
1305
+ const borrow = () => attemptDaemonBrokerLoginAndPersist({
1306
+ ...base,
1307
+ preferHandoffMode: 'access_token',
1308
+ borrowOnly: true,
1309
+ });
1310
+ // Adopt only from a live descriptor that can mint a family. One that
1311
+ // cannot (the Dedicated guest's access-token-only shape), or none at all (a
1312
+ // daemon that is down or restarting), has nothing to adopt: borrow without
1313
+ // taking the lock, so concurrent commands never queue behind it and the
1314
+ // borrow path's own restart wait and outage reporting apply unchanged.
1315
+ const usable = listUsableBootstraps(base);
1316
+ if (!usable.some(({ bootstrap }) => (0, cli_handoff_wire_1.advertisedHandoffModes)(bootstrap).includes('refresh_family'))) {
1317
+ return { outcome: await borrow() };
1318
+ }
1319
+ const before = storedAccessToken(baseURL);
1320
+ const lockPath = adoptionLockPath(profile);
1321
+ let release = null;
1322
+ try {
1323
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 0o700 });
1324
+ if (!fs.existsSync(lockPath))
1325
+ fs.writeFileSync(lockPath, '', { mode: 0o600 });
1326
+ release = await proper_lockfile_1.default.lock(lockPath, {
1327
+ // Longer than one mint (20s loopback + 10s confirm), so a holder that
1328
+ // is still working is never judged stale.
1329
+ stale: 60_000,
1330
+ retries: { retries: lockRetries, minTimeout: 250, maxTimeout: 1_000, factor: 1.5 },
1331
+ });
1332
+ }
1333
+ catch {
1334
+ // Could not serialize: do not race a sibling's mint. Borrow.
1335
+ return { outcome: await borrow() };
1336
+ }
1337
+ let durable;
1338
+ try {
1339
+ const now = storedAccessToken(baseURL);
1340
+ if (now && now !== before)
1341
+ return { adoptedBySibling: true };
1342
+ durable = await attemptDaemonBrokerLoginAndPersist({
1343
+ ...base,
1344
+ preferHandoffMode: 'refresh_family',
1345
+ });
1346
+ }
1347
+ finally {
1348
+ await release().catch(() => undefined);
1349
+ }
1350
+ if (durable.ok || daemonDidNotAnswer(durable))
1351
+ return { outcome: durable };
1352
+ // The daemon answered but would not mint a family. The token route is a
1353
+ // different door with different limits; take it for this process.
1354
+ return { outcome: await borrow() };
1355
+ }
1202
1356
  /**
1203
1357
  * Auto-broker pre-flight for `BaseCommand.init()`. When the credential
1204
1358
  * resolver returns no token, this attempts a silent daemon broker so
@@ -1244,21 +1398,43 @@ async function maybeAutoBroker(opts) {
1244
1398
  // confirm cannot retire another profile's session. Persisted by the caller on
1245
1399
  // success so the rotation row stays bound across processes.
1246
1400
  const { cliId, updatedConfig } = (0, cli_identity_1.planLoginCliId)(opts.cliConfig);
1247
- const outcome = await attemptDaemonBrokerLoginAndPersist({
1401
+ const base = {
1248
1402
  cliId,
1249
1403
  baseURL: opts.cliConfig.baseURL,
1250
- // Auto-broker prefers the brokered access-token mode when the descriptor
1251
- // advertises it: nothing credential-shaped is persisted, and the next
1252
- // `skrr` process simply re-redeems over loopback. The legacy refresh-
1253
- // family mint remains the fallback for v1 descriptors and the redeem
1254
- // path for explicit `skrr login`.
1255
- preferHandoffMode: 'access_token',
1256
1404
  profile: opts.profile ?? (0, keychain_1.getActiveProfile)(),
1257
1405
  ...(opts.bootstrapPathOverride ? { bootstrapPathOverride: opts.bootstrapPathOverride } : {}),
1406
+ };
1407
+ const harness = (0, harness_context_1.isHarnessCliContext)();
1408
+ // OSK-13254 — a person's first `skrr` on a machine whose daemon is signed in
1409
+ // ADOPTS a CLI session of its own: a separate cli-scope family, minted by the
1410
+ // daemon's hand-off and stored like a browser login, so later commands do
1411
+ // not depend on the daemon being up. Everything else borrows a short-lived
1412
+ // token for this process, which is what the automatic path always did
1413
+ // (OSK-12018): a process the daemon started for an agent or tool, a 401
1414
+ // recovery, a profile signed out with `skrr logout`, and the opt-out.
1415
+ if (opts.adopt && !cliSessionAdoptionBlocker(opts.cliConfig)) {
1416
+ const adopted = await adoptCliSessionThroughDaemon(base, opts.adoptionLockRetries ?? 40);
1417
+ if (adopted.adoptedBySibling)
1418
+ return { triggered: true, adoptedBySibling: true };
1419
+ const outcome = adopted.outcome;
1420
+ return {
1421
+ triggered: true,
1422
+ outcome,
1423
+ ...(updatedConfig && outcome.ok ? { updatedConfig } : {}),
1424
+ };
1425
+ }
1426
+ const outcome = await attemptDaemonBrokerLoginAndPersist({
1427
+ ...base,
1428
+ // Borrow: the access-token mode, held in memory for this process; the next
1429
+ // `skrr` re-redeems over loopback. A v1 descriptor still takes the durable
1430
+ // mint here (the compatibility window in `brokerThrough`), except for a
1431
+ // harness-context process, which never stores a sign-in.
1432
+ preferHandoffMode: 'access_token',
1433
+ ...(harness ? { borrowOnly: true } : {}),
1258
1434
  });
1259
1435
  return {
1260
1436
  triggered: true,
1261
1437
  outcome,
1262
- ...(updatedConfig && outcome.ok ? { updatedConfig } : {}),
1438
+ ...(updatedConfig && outcome.ok && !harness ? { updatedConfig } : {}),
1263
1439
  };
1264
1440
  }
@@ -96,3 +96,17 @@ export declare function describeGuestDaemonUnavailable(failure: DaemonBrokerFail
96
96
  * nothing waited for): "Not signed in" is then the true sentence.
97
97
  */
98
98
  export declare function describeLocalDaemonUnavailable(failure: DaemonBrokerFailure, bin?: string): BrokerRefusalDescription | null;
99
+ /**
100
+ * The one description of a laptop or guest broker failure for every surface
101
+ * that has no separate guest probe of its own: a daemon that answered and
102
+ * refused (`describeBrokerRefusal`), else a daemon whose descriptor is on disk
103
+ * but which did not answer — down, crashed or mid-restart
104
+ * (`describeLocalDaemonUnavailable`).
105
+ *
106
+ * `requireAuth` composed these two by hand; `skrr login`'s progress line and
107
+ * headless refusal, and the mid-command 401 paths, consulted only the first,
108
+ * so a daemon that was restarting read there as "no daemon at all" — the
109
+ * generic browser refusal, or a bare "Run `skrr login`" (OSK-13099). Returns
110
+ * null only when there is genuinely nothing to say about a daemon.
111
+ */
112
+ export declare function describeBrokerFailure(failure: DaemonBrokerFailure, bin?: string): BrokerRefusalDescription | null;
@@ -4,6 +4,7 @@ exports.isAnsweredBrokerRefusal = isAnsweredBrokerRefusal;
4
4
  exports.describeBrokerRefusal = describeBrokerRefusal;
5
5
  exports.describeGuestDaemonUnavailable = describeGuestDaemonUnavailable;
6
6
  exports.describeLocalDaemonUnavailable = describeLocalDaemonUnavailable;
7
+ exports.describeBrokerFailure = describeBrokerFailure;
7
8
  /**
8
9
  * True when a daemon ANSWERED the hand-off and refused it — as opposed to no
9
10
  * daemon being there to ask (`no_bootstrap`, a stale descriptor nobody listens
@@ -261,7 +262,7 @@ function describeLocalDaemonUnavailable(failure, bin = 'skrr') {
261
262
  if (!headline)
262
263
  return null;
263
264
  const remedy = 'You are not signed out: your sign-in is held by that service, which normally restarts on its own. ' +
264
- `Retry in a moment. \`${bin} daemon status\` and \`${bin} daemon logs\` show the service's side, ` +
265
+ `Retry in a few seconds, or start it with \`${bin} daemon start\` if it stays down. \`${bin} daemon status\` and \`${bin} daemon logs\` show the service's side, ` +
265
266
  `and \`${bin} login\` signs this CLI in without it.`;
266
267
  return {
267
268
  headline: `${headline}${waited}`,
@@ -289,3 +290,19 @@ function describeLocalDaemonUnavailable(failure, bin = 'skrr') {
289
290
  },
290
291
  };
291
292
  }
293
+ /**
294
+ * The one description of a laptop or guest broker failure for every surface
295
+ * that has no separate guest probe of its own: a daemon that answered and
296
+ * refused (`describeBrokerRefusal`), else a daemon whose descriptor is on disk
297
+ * but which did not answer — down, crashed or mid-restart
298
+ * (`describeLocalDaemonUnavailable`).
299
+ *
300
+ * `requireAuth` composed these two by hand; `skrr login`'s progress line and
301
+ * headless refusal, and the mid-command 401 paths, consulted only the first,
302
+ * so a daemon that was restarting read there as "no daemon at all" — the
303
+ * generic browser refusal, or a bare "Run `skrr login`" (OSK-13099). Returns
304
+ * null only when there is genuinely nothing to say about a daemon.
305
+ */
306
+ function describeBrokerFailure(failure, bin = 'skrr') {
307
+ return describeBrokerRefusal(failure, bin) ?? describeLocalDaemonUnavailable(failure, bin);
308
+ }
@@ -0,0 +1,8 @@
1
+ export declare function isHarnessCliContext(env?: Readonly<Record<string, string | undefined>>): boolean;
2
+ /** Thrown by `writeToBackend` when a harness-context process tries to persist. */
3
+ export declare class HarnessCredentialPersistRefused extends Error {
4
+ readonly code = "HARNESS_CONTEXT_PERSIST_REFUSED";
5
+ constructor();
6
+ }
7
+ /** The commands a harness-context process may not run (they own the stored session). */
8
+ export declare const HARNESS_REFUSED_AUTH_COMMANDS: ReadonlySet<string>;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HARNESS_REFUSED_AUTH_COMMANDS = exports.HarnessCredentialPersistRefused = void 0;
4
+ exports.isHarnessCliContext = isHarnessCliContext;
5
+ /**
6
+ * harness-context.ts — is this `skrr` running for an agent the daemon started?
7
+ * (OSK-13254)
8
+ *
9
+ * A person's `skrr` may adopt a CLI refresh session of its own through the
10
+ * local daemon's hand-off, so that signing in survives the daemon restarting
11
+ * (`daemonBroker.ts`, `maybeAutoBroker`). A `skrr` the daemon spawned for an
12
+ * agent or a tool must not: it borrows short-lived tokens from the daemon for
13
+ * each process, and never reads, writes, rotates or revokes the person's
14
+ * stored CLI session.
15
+ *
16
+ * Two daemon-set markers say so, and either is enough:
17
+ *
18
+ * - `OVERSKY_AUTONOMOUS_SESSION=1` — a harness session (`session-wrapper.ts`,
19
+ * `api-local-session-client.ts`). Such a process already never reaches the
20
+ * stored-credential chain (`BaseCommand.init` returns early on it); it is
21
+ * listed here so every guard below answers the same question.
22
+ * - `SKRR_DAEMON_CHILD` — every other agent/tool child: one-shot Bash,
23
+ * background Bash, agent terminals, harness backends
24
+ * (`daemon/src/env-sanitize.ts`, `agent-terminal-env.ts`). It survives the
25
+ * shared sanitizer, so grandchildren keep it.
26
+ *
27
+ * What this is and is not, stated so nobody leans on it harder than it holds:
28
+ * on a laptop the daemon, the agent and the person share one uid, so this is
29
+ * POLICY — a child that deletes the marker can do what the person's shell can.
30
+ * Where a uid boundary exists (Dedicated Runtime, identity-split Hosted
31
+ * Machines) the guest descriptor offers only the access-token mode, so a
32
+ * durable family cannot be minted there whatever the environment says. This
33
+ * is the same scope invariant I1 has today
34
+ * (`docs/architecture/managed-inference-execution-shapes-2026-08-12.md` §15).
35
+ */
36
+ const auth_core_1 = require("@skrr-ai/auth-core");
37
+ function isHarnessCliContext(env = process.env) {
38
+ return env.OVERSKY_AUTONOMOUS_SESSION === '1' || (0, auth_core_1.isDaemonChildEnv)(env);
39
+ }
40
+ /** Thrown by `writeToBackend` when a harness-context process tries to persist. */
41
+ class HarnessCredentialPersistRefused extends Error {
42
+ code = 'HARNESS_CONTEXT_PERSIST_REFUSED';
43
+ constructor() {
44
+ super('Refusing to store a skrr sign-in from a process the skrr background service started for an ' +
45
+ 'agent or tool. skrr here borrows short-lived tokens from the service; sign in from your own terminal.');
46
+ this.name = 'HarnessCredentialPersistRefused';
47
+ }
48
+ }
49
+ exports.HarnessCredentialPersistRefused = HarnessCredentialPersistRefused;
50
+ /** The commands a harness-context process may not run (they own the stored session). */
51
+ exports.HARNESS_REFUSED_AUTH_COMMANDS = new Set([
52
+ 'login',
53
+ 'logout',
54
+ 'pair',
55
+ ]);
package/dist/lib/login.js CHANGED
@@ -202,7 +202,10 @@ async function cliLogin(opts = {}) {
202
202
  // The daemon answered and refused. Say so before whatever comes next —
203
203
  // a browser that opens with no stated cause, or a refusal about terminals
204
204
  // that says nothing about the daemon, both hid the real reason.
205
- brokerRefusal = (0, daemonBrokerRefusal_1.describeBrokerRefusal)(brokerResult);
205
+ // A daemon whose descriptor is on disk but which did not answer (down or
206
+ // mid-restart) is described too: without it a headless `skrr login` during
207
+ // a restart said only that this shell has no terminal (OSK-13099).
208
+ brokerRefusal = (0, daemonBrokerRefusal_1.describeBrokerFailure)(brokerResult);
206
209
  if (brokerRefusal && !(0, ssh_detect_1.isNonInteractive)()) {
207
210
  console.log('');
208
211
  console.log(` Asked the local skrr service for a session first. ${brokerRefusal.headline}`);
@@ -30,6 +30,7 @@ const config_1 = require("./config");
30
30
  const cli_id_1 = require("./cli-id");
31
31
  const credential_resolver_1 = require("./credential-resolver");
32
32
  const device_identity_1 = require("./device-identity");
33
+ const harness_context_1 = require("./harness-context");
33
34
  exports.CLI_REFRESH_EARLY_MS = 5 * 60 * 1000;
34
35
  /**
35
36
  * Stable origin header so the server's benign-grace matcher can identify
@@ -96,6 +97,9 @@ function canMutatePersistedCredential(credential, bareMode) {
96
97
  return false;
97
98
  if ((0, auth_core_1.isStaticTokenKind)(credential.kind))
98
99
  return false;
100
+ // A borrower never rotates the person's stored family (OSK-13254).
101
+ if ((0, harness_context_1.isHarnessCliContext)())
102
+ return false;
99
103
  // These sources are operator-owned or generated on demand. Never rotate
100
104
  // them into the local keychain/file store behind the operator's back.
101
105
  return credential.source === 'keychain' || credential.source === 'file';
@@ -35,7 +35,7 @@ export { ensureMachineDaemonId, legacyDaemonUuids, __resetCachedDaemonIdForTest,
35
35
  export { PAIR_AAD_PREFIX, PAIR_AAD_MAX_LEN, PAIR_BUNDLE_MAX_BASE64_LEN, PAIR_NONCE_LEN, PAIR_PUBKEY_LEN, PAIR_SECRET_LEN, PairBundleError, assemblePairBundleBase64, base64ToUint8Array as pairBase64ToUint8Array, base64UrlToUint8Array as pairBase64UrlToUint8Array, buildPairAad, buildPairUrl, generatePairKeyPair, openPairBundleBase64, parsePairBundle, parsePairPlaintext, parsePairUrl, uint8ArrayToBase64 as pairUint8ArrayToBase64, uint8ArrayToBase64Url as pairUint8ArrayToBase64Url, type AssembleBundleOptions as PairAssembleBundleOptions, type OpenBundleOptions as PairOpenBundleOptions, type PairBundle, type PairBundleErrorCode, type PairKeyPair, type PairPlaintextV1, type PairUrlFields, } from './daemonPair.js';
36
36
  export { HARNESS_TIERS, getHarnessTier, credentialPolicyForTier, tierPermitsConfiguredAutoMode, type HarnessTrustTier, type CredentialDeliveryPolicy, type CredentialMode, } from './harnessTrust.js';
37
37
  export { HARNESSES_LOADING_CLAUDE_SKILLS_LAYOUT, claudeSkillsLayoutEvidence, harnessLoadsClaudeSkillsLayout, } from './harnessSkillLayout.js';
38
- export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_NAME_TOKENS, SENSITIVE_ENV_PREFIX_EXCEPTIONS, LOOPBACK_NO_PROXY_HOSTS, envHasProxy, withLoopbackNoProxy, } from './spawnEnv.js';
38
+ export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_NAME_TOKENS, SENSITIVE_ENV_PREFIX_EXCEPTIONS, DAEMON_CHILD_ENV, isDaemonChildEnv, LOOPBACK_NO_PROXY_HOSTS, envHasProxy, withLoopbackNoProxy, } from './spawnEnv.js';
39
39
  export { loopbackFetch, assertLoopbackHttpUrl, isLoopbackHostname, isLoopbackAddress, LoopbackTargetRefusedError, LOOPBACK_DEFAULT_MAX_BODY_BYTES, type LoopbackFetch, type LoopbackFetchInit, } from './loopbackHttp.js';
40
40
  export { CONFIG_DIR_NAME, LEGACY_CONFIG_DIR_NAMES, NATIVE_ID_PREFIX, LEGACY_NATIVE_ID_PREFIXES, DEFAULT_BINARY_NAME, } from './localIdentity.js';
41
41
  export { findLegacyLocalState, describeLegacyState, type LegacyStateFinding, type LegacyStateReport, } from './legacyStatePreflight.js';
@@ -25,8 +25,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
25
25
  exports.runHeadersHelper = exports.SENTINEL_TRIGGERING_FAILURES = exports.__resetAuthHelperFailureReasonForTest = exports.getLastAuthHelperFailureReason = exports.getDefaultHelperTtlMs = exports.invokeAuthHelper = exports.AuthHelperUnavailableError = exports.AuthHelperUntrustedError = exports.isStaticTokenKind = exports.PERSONAL_ACCESS_TOKEN_PREFIX = exports.CI_TOKEN_PREFIX = exports.classifyTokenKind = exports.invalidateAuthHelperCache = exports.__resetCredentialResolverForTest = exports.CREDENTIAL_PRECEDENCE = exports.CredentialResolver = exports.__setFdReaderForTest = exports.readAuthFromFd = exports.createRefreshScheduler = exports.UNKNOWN_401_BUDGET = exports.__resetUnknown401StreaksForTest = exports.__refreshDiagnostics = exports.installLockExitHandlers = exports.clearReauthState = exports.needsReauthState = exports.persistReauthState = exports.withAuthLock = exports.revokeDaemonRefreshSession = exports.refreshDaemonToken = exports.classifyRefreshResponse = exports.EXIT_NEEDS_REAUTH = exports.reauthCommand = exports.formatReauthMessage = exports.handleMidStreamAuthExpiry = exports.isAuthExpiredSseEvent = exports.decodeJwtPayload = exports.decodeJwtExpiry = exports.TransientAuthFailure = exports.PermanentAuthFailure = exports.emitAuthTelemetry = exports.getAuthBinaryName = exports.getAuthKillSwitches = exports.getAuthConfigDir = exports.getAuthLogger = exports.configureAuthCore = exports.OAUTH_CLI_SCOPE = exports.isDaemonScope = exports.VALID_DAEMON_SCOPE_SET = exports.ALL_DAEMON_SCOPES = exports.SCOPES = void 0;
26
26
  exports.jwkThumbprint = exports.generateDeviceKeyPair = exports.buildDeviceProof = exports.zeroizeKekCaches = exports.WindowsKek = exports.registerZeroizeHook = exports.MacosKek = exports.LinuxKek = exports.KekUnavailableError = exports.UnavailableKek = exports.InMemoryKek = exports.getKekStrategy = exports.__setKekStrategyForTest = exports.credEnvelopeWrap = exports.credEnvelopeUnwrap = exports.credEnvelopeSerialize = exports.credSealBuffer = exports.credOpenBuffer = exports.CRED_KEY_LEN = exports.isCredEnvelopeString = exports.generateDek = exports.CRED_ENVELOPE_MAGIC = exports.credEnvelopeEncrypt = exports.credEnvelopeDeserialize = exports.credEnvelopeDecrypt = exports.buildCredentialAad = exports.isValidRecoveryCode = exports.normalizeRecoveryCode = exports.parseRecoveryCode = exports.formatRecoveryCode = exports.MAX_TTL_DAYS = exports.MIN_TTL_DAYS = exports.parseTtlDays = exports.LoginExchangeError = exports.LoginInitError = exports.loginWithLocalhost = exports.LocalCallbackMalformedError = exports.LocalCallbackDeniedError = exports.LocalCallbackStateMismatchError = exports.LocalCallbackTimeoutError = exports.LocalCallbackBindError = exports.LocalCallbackError = exports.DEFAULT_LOGIN_TIMEOUT_MS = exports.startLocalCallback = exports.verifyChallenge = exports.safeEqual = exports.generateState = exports.challenge = exports.generateVerifier = exports.invalidateHeadersHelperCache = void 0;
27
27
  exports.ensureMachineDaemonId = exports.__resetMachineUuidCacheForTest = exports.HOSTED_RUNTIME_MACHINE_SOURCES = exports.hostedRuntimeIdOverride = exports.getMachineUuid = exports.__getDeviceIdentityStateForTest = exports.__setDeviceIdentityStateForTest = exports.__enrollmentBackoffPathForTest = exports.__enrollmentMarkerPathForTest = exports.__privateKeyPathForTest = exports.__publicKeyPathForTest = exports.describeDeviceKeySplit = exports.describeDeviceIdentityState = exports.getDevicePublicKey = exports.isDeviceIdentityActive = exports.clearEnrollmentMarker = exports.enrollWithServer = exports.signServerRequest = exports.resetDeviceIdentity = exports.initDeviceIdentity = exports.__resetShutdownGuardForTest = exports.__getCredEnvelopeStateForTest = exports.__setCredEnvelopeStateForTest = exports.__wrappedDekFilePathForTest = exports.DEFAULT_UNREADABLE_CREDENTIAL_HINT = exports.maybeDecryptOnRead = exports.maybeEncryptForWrite = exports.splitKeyConvergencePolicy = exports.describeCredEnvelopeState = exports.hasCredEnvelopePathConflict = exports.isCredEnvelopeActive = exports.shutdownCredEnvelope = exports.resetCredEnvelope = exports.initCredEnvelope = exports.verifyDpopProof = exports.normalizeDpopHtu = exports.isJwkThumbprint = exports.isAth = exports.createDpopKey = exports.accessTokenHash = exports.DPOP_PROOF_VERIFY_UNAVAILABLE = exports.DPOP_PROOF_TYP = exports.DPOP_PROOF_REQUIRED = exports.DPOP_PROOF_INVALID = exports.DPOP_MAX_AGE_SECONDS = exports.DPOP_HEADER = exports.verifyDeviceProof = exports.tokenHash = exports.publicJwkFromX = exports.normalizeHtu = void 0;
28
- exports.describeLegacyState = exports.findLegacyLocalState = exports.DEFAULT_BINARY_NAME = exports.LEGACY_NATIVE_ID_PREFIXES = exports.NATIVE_ID_PREFIX = exports.LEGACY_CONFIG_DIR_NAMES = exports.CONFIG_DIR_NAME = exports.LOOPBACK_DEFAULT_MAX_BODY_BYTES = exports.LoopbackTargetRefusedError = exports.isLoopbackAddress = exports.isLoopbackHostname = exports.assertLoopbackHttpUrl = exports.loopbackFetch = exports.withLoopbackNoProxy = exports.envHasProxy = exports.LOOPBACK_NO_PROXY_HOSTS = exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_NAME_TOKENS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = exports.sanitizeSpawnEnv = exports.harnessLoadsClaudeSkillsLayout = exports.claudeSkillsLayoutEvidence = exports.HARNESSES_LOADING_CLAUDE_SKILLS_LAYOUT = exports.tierPermitsConfiguredAutoMode = exports.credentialPolicyForTier = exports.getHarnessTier = exports.HARNESS_TIERS = exports.pairUint8ArrayToBase64Url = exports.pairUint8ArrayToBase64 = exports.parsePairUrl = exports.parsePairPlaintext = exports.parsePairBundle = exports.openPairBundleBase64 = exports.generatePairKeyPair = exports.buildPairUrl = exports.buildPairAad = exports.pairBase64UrlToUint8Array = exports.pairBase64ToUint8Array = exports.assemblePairBundleBase64 = exports.PairBundleError = exports.PAIR_SECRET_LEN = exports.PAIR_PUBKEY_LEN = exports.PAIR_NONCE_LEN = exports.PAIR_BUNDLE_MAX_BASE64_LEN = exports.PAIR_AAD_MAX_LEN = exports.PAIR_AAD_PREFIX = exports.__resetCachedDaemonIdForTest = exports.legacyDaemonUuids = void 0;
29
- exports.consolidateProfileState = exports.profileStateFilePathsToClear = exports.profileStateFileLocations = exports.resolveProfileStateFile = exports.resolveProfileStateDir = exports.legacyProfileStateDir = exports.profileStateDir = exports.SUPERSEDED_COPY_SUFFIX = exports.SUPERSEDED_MARKER_SUFFIX = exports.PROFILES_SEGMENT = exports.isConfigRootOverridden = exports.configRootPath = exports.resolveConfigRoot = exports.verifyManifest = exports.verifyArtifact = exports.isTrustedArtifactOrigin = exports.getDeltaFor = exports.buildManifestMessage = exports.buildArtifactMessage = exports.TRUSTED_FEED_ORIGINS = exports.MAX_SIGS = exports.OVERSKY_RELEASE_PUBLIC_KEYS = exports.resolveFirstPartyHarnessChannel = exports.resolveChannelManifestUrl = exports.isFirstPartyHarnessChannel = exports.firstPartyHarnessFeedCandidates = exports.firstPartyHarnessFeedBase = exports.firstPartyHarnessChannelPrefix = exports.FIRST_PARTY_HARNESS_CHANNELS = exports.DEFAULT_FIRST_PARTY_HARNESS_CHANNEL = exports.migrateFirstPartyHarnessHome = exports.linkFirstPartyHarnessAliases = exports.firstPartyHarnessInstalledBinaryPath = exports.firstPartyHarnessHomeDir = exports.firstPartyHarnessBinDir = exports.FIRST_PARTY_HARNESS_PROVENANCE_SUFFIX = exports.FIRST_PARTY_HARNESS_PREVIOUS_SUFFIX = void 0;
28
+ exports.DEFAULT_BINARY_NAME = exports.LEGACY_NATIVE_ID_PREFIXES = exports.NATIVE_ID_PREFIX = exports.LEGACY_CONFIG_DIR_NAMES = exports.CONFIG_DIR_NAME = exports.LOOPBACK_DEFAULT_MAX_BODY_BYTES = exports.LoopbackTargetRefusedError = exports.isLoopbackAddress = exports.isLoopbackHostname = exports.assertLoopbackHttpUrl = exports.loopbackFetch = exports.withLoopbackNoProxy = exports.envHasProxy = exports.LOOPBACK_NO_PROXY_HOSTS = exports.isDaemonChildEnv = exports.DAEMON_CHILD_ENV = exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_NAME_TOKENS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = exports.sanitizeSpawnEnv = exports.harnessLoadsClaudeSkillsLayout = exports.claudeSkillsLayoutEvidence = exports.HARNESSES_LOADING_CLAUDE_SKILLS_LAYOUT = exports.tierPermitsConfiguredAutoMode = exports.credentialPolicyForTier = exports.getHarnessTier = exports.HARNESS_TIERS = exports.pairUint8ArrayToBase64Url = exports.pairUint8ArrayToBase64 = exports.parsePairUrl = exports.parsePairPlaintext = exports.parsePairBundle = exports.openPairBundleBase64 = exports.generatePairKeyPair = exports.buildPairUrl = exports.buildPairAad = exports.pairBase64UrlToUint8Array = exports.pairBase64ToUint8Array = exports.assemblePairBundleBase64 = exports.PairBundleError = exports.PAIR_SECRET_LEN = exports.PAIR_PUBKEY_LEN = exports.PAIR_NONCE_LEN = exports.PAIR_BUNDLE_MAX_BASE64_LEN = exports.PAIR_AAD_MAX_LEN = exports.PAIR_AAD_PREFIX = exports.__resetCachedDaemonIdForTest = exports.legacyDaemonUuids = void 0;
29
+ exports.consolidateProfileState = exports.profileStateFilePathsToClear = exports.profileStateFileLocations = exports.resolveProfileStateFile = exports.resolveProfileStateDir = exports.legacyProfileStateDir = exports.profileStateDir = exports.SUPERSEDED_COPY_SUFFIX = exports.SUPERSEDED_MARKER_SUFFIX = exports.PROFILES_SEGMENT = exports.isConfigRootOverridden = exports.configRootPath = exports.resolveConfigRoot = exports.verifyManifest = exports.verifyArtifact = exports.isTrustedArtifactOrigin = exports.getDeltaFor = exports.buildManifestMessage = exports.buildArtifactMessage = exports.TRUSTED_FEED_ORIGINS = exports.MAX_SIGS = exports.OVERSKY_RELEASE_PUBLIC_KEYS = exports.resolveFirstPartyHarnessChannel = exports.resolveChannelManifestUrl = exports.isFirstPartyHarnessChannel = exports.firstPartyHarnessFeedCandidates = exports.firstPartyHarnessFeedBase = exports.firstPartyHarnessChannelPrefix = exports.FIRST_PARTY_HARNESS_CHANNELS = exports.DEFAULT_FIRST_PARTY_HARNESS_CHANNEL = exports.migrateFirstPartyHarnessHome = exports.linkFirstPartyHarnessAliases = exports.firstPartyHarnessInstalledBinaryPath = exports.firstPartyHarnessHomeDir = exports.firstPartyHarnessBinDir = exports.FIRST_PARTY_HARNESS_PROVENANCE_SUFFIX = exports.FIRST_PARTY_HARNESS_PREVIOUS_SUFFIX = exports.describeLegacyState = exports.findLegacyLocalState = void 0;
30
30
  // Daemon scope vocabulary — single source of truth for non-Mongoose tiers
31
31
  // (daemon, desktop, cli). Schema-bound canonical lives in
32
32
  // `@skrr-ai/data-schemas/common/daemonScopes`; the two are kept byte-equal
@@ -346,6 +346,8 @@ Object.defineProperty(exports, "SENSITIVE_ENV_PREFIXES", { enumerable: true, get
346
346
  Object.defineProperty(exports, "SENSITIVE_ENV_SUFFIXES", { enumerable: true, get: function () { return spawnEnv_js_1.SENSITIVE_ENV_SUFFIXES; } });
347
347
  Object.defineProperty(exports, "SENSITIVE_ENV_NAME_TOKENS", { enumerable: true, get: function () { return spawnEnv_js_1.SENSITIVE_ENV_NAME_TOKENS; } });
348
348
  Object.defineProperty(exports, "SENSITIVE_ENV_PREFIX_EXCEPTIONS", { enumerable: true, get: function () { return spawnEnv_js_1.SENSITIVE_ENV_PREFIX_EXCEPTIONS; } });
349
+ Object.defineProperty(exports, "DAEMON_CHILD_ENV", { enumerable: true, get: function () { return spawnEnv_js_1.DAEMON_CHILD_ENV; } });
350
+ Object.defineProperty(exports, "isDaemonChildEnv", { enumerable: true, get: function () { return spawnEnv_js_1.isDaemonChildEnv; } });
349
351
  Object.defineProperty(exports, "LOOPBACK_NO_PROXY_HOSTS", { enumerable: true, get: function () { return spawnEnv_js_1.LOOPBACK_NO_PROXY_HOSTS; } });
350
352
  Object.defineProperty(exports, "envHasProxy", { enumerable: true, get: function () { return spawnEnv_js_1.envHasProxy; } });
351
353
  Object.defineProperty(exports, "withLoopbackNoProxy", { enumerable: true, get: function () { return spawnEnv_js_1.withLoopbackNoProxy; } });
@@ -77,6 +77,31 @@ export declare const SENSITIVE_ENV_SUFFIXES: readonly string[];
77
77
  export declare const SENSITIVE_ENV_NAME_TOKENS: readonly string[];
78
78
  /** Specific names to keep — the prefix sweeps are broad on purpose. */
79
79
  export declare const SENSITIVE_ENV_PREFIX_EXCEPTIONS: ReadonlySet<string>;
80
+ /**
81
+ * OSK-13254 — the marker the daemon stamps on every agent- or tool-driven child
82
+ * (`daemon/src/env-sanitize.ts`, `daemon/src/terminal/agent-terminal-env.ts`).
83
+ *
84
+ * A `skrr` that sees it is a borrower: it redeems short-lived tokens from the
85
+ * daemon's loopback broker, never adopts a CLI refresh session of its own, and
86
+ * never reads the person's stored one (`cli/src/lib/harness-context.ts`). A
87
+ * person's own terminal — including the web terminal, which is a human surface
88
+ * (`humanTerminalEnv`) — does not carry it.
89
+ *
90
+ * The marker can only take authority away, so a process that forges it hurts
91
+ * nobody but itself. On a laptop it is POLICY, not a boundary: a same-uid child
92
+ * that deletes it can do whatever the person's own shell can. Where a uid
93
+ * boundary exists (Dedicated Runtime, identity-split Hosted Machines), the
94
+ * guest descriptor already offers only the access-token mode, so a durable CLI
95
+ * family cannot be minted there whatever the environment says.
96
+ */
97
+ export declare const DAEMON_CHILD_ENV = "SKRR_DAEMON_CHILD";
98
+ /**
99
+ * True when `env` carries the daemon-child marker with ANY non-empty value.
100
+ * Deliberately no "0 means off": the marker only narrows authority, so the
101
+ * one value that could switch it off is the one a request-carried environment
102
+ * would forge.
103
+ */
104
+ export declare function isDaemonChildEnv(env: Readonly<Record<string, string | undefined>>): boolean;
80
105
  /**
81
106
  * Build a sanitized snapshot of the parent's own env for a spawned
82
107
  * child, stripping credentials and library-injection vectors while
@@ -40,7 +40,8 @@
40
40
  * serve that case: a deny-list with a hole is not a deny-list.
41
41
  */
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
- exports.LOOPBACK_NO_PROXY_HOSTS = exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_NAME_TOKENS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = void 0;
43
+ exports.LOOPBACK_NO_PROXY_HOSTS = exports.DAEMON_CHILD_ENV = exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_NAME_TOKENS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = void 0;
44
+ exports.isDaemonChildEnv = isDaemonChildEnv;
44
45
  exports.sanitizeSpawnEnv = sanitizeSpawnEnv;
45
46
  exports.envHasProxy = envHasProxy;
46
47
  exports.withLoopbackNoProxy = withLoopbackNoProxy;
@@ -154,7 +155,40 @@ exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = new Set([
154
155
  // engine) must keep it to detect they already run as the workload uid. Not
155
156
  // a secret — it authorizes nothing without the matching real uid.
156
157
  'SKRR_WORKLOAD_UID',
158
+ // OSK-13254 — "this process descends from a skrrd agent/tool spawn". It only
159
+ // ever REMOVES authority (a `skrr` that sees it borrows from the daemon and
160
+ // never reads or writes a stored CLI session), so it must survive every later
161
+ // re-sanitization: a Bash tool that runs `skrr code`, whose engine runs `skrr`,
162
+ // is still the agent's process. See `DAEMON_CHILD_ENV` below.
163
+ 'SKRR_DAEMON_CHILD',
157
164
  ]);
165
+ /**
166
+ * OSK-13254 — the marker the daemon stamps on every agent- or tool-driven child
167
+ * (`daemon/src/env-sanitize.ts`, `daemon/src/terminal/agent-terminal-env.ts`).
168
+ *
169
+ * A `skrr` that sees it is a borrower: it redeems short-lived tokens from the
170
+ * daemon's loopback broker, never adopts a CLI refresh session of its own, and
171
+ * never reads the person's stored one (`cli/src/lib/harness-context.ts`). A
172
+ * person's own terminal — including the web terminal, which is a human surface
173
+ * (`humanTerminalEnv`) — does not carry it.
174
+ *
175
+ * The marker can only take authority away, so a process that forges it hurts
176
+ * nobody but itself. On a laptop it is POLICY, not a boundary: a same-uid child
177
+ * that deletes it can do whatever the person's own shell can. Where a uid
178
+ * boundary exists (Dedicated Runtime, identity-split Hosted Machines), the
179
+ * guest descriptor already offers only the access-token mode, so a durable CLI
180
+ * family cannot be minted there whatever the environment says.
181
+ */
182
+ exports.DAEMON_CHILD_ENV = 'SKRR_DAEMON_CHILD';
183
+ /**
184
+ * True when `env` carries the daemon-child marker with ANY non-empty value.
185
+ * Deliberately no "0 means off": the marker only narrows authority, so the
186
+ * one value that could switch it off is the one a request-carried environment
187
+ * would forge.
188
+ */
189
+ function isDaemonChildEnv(env) {
190
+ return Boolean(env[exports.DAEMON_CHILD_ENV]?.trim());
191
+ }
158
192
  /**
159
193
  * Build a sanitized snapshot of the parent's own env for a spawned
160
194
  * child, stripping credentials and library-injection vectors while
@@ -35,7 +35,7 @@ export { ensureMachineDaemonId, legacyDaemonUuids, __resetCachedDaemonIdForTest,
35
35
  export { PAIR_AAD_PREFIX, PAIR_AAD_MAX_LEN, PAIR_BUNDLE_MAX_BASE64_LEN, PAIR_NONCE_LEN, PAIR_PUBKEY_LEN, PAIR_SECRET_LEN, PairBundleError, assemblePairBundleBase64, base64ToUint8Array as pairBase64ToUint8Array, base64UrlToUint8Array as pairBase64UrlToUint8Array, buildPairAad, buildPairUrl, generatePairKeyPair, openPairBundleBase64, parsePairBundle, parsePairPlaintext, parsePairUrl, uint8ArrayToBase64 as pairUint8ArrayToBase64, uint8ArrayToBase64Url as pairUint8ArrayToBase64Url, type AssembleBundleOptions as PairAssembleBundleOptions, type OpenBundleOptions as PairOpenBundleOptions, type PairBundle, type PairBundleErrorCode, type PairKeyPair, type PairPlaintextV1, type PairUrlFields, } from './daemonPair.js';
36
36
  export { HARNESS_TIERS, getHarnessTier, credentialPolicyForTier, tierPermitsConfiguredAutoMode, type HarnessTrustTier, type CredentialDeliveryPolicy, type CredentialMode, } from './harnessTrust.js';
37
37
  export { HARNESSES_LOADING_CLAUDE_SKILLS_LAYOUT, claudeSkillsLayoutEvidence, harnessLoadsClaudeSkillsLayout, } from './harnessSkillLayout.js';
38
- export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_NAME_TOKENS, SENSITIVE_ENV_PREFIX_EXCEPTIONS, LOOPBACK_NO_PROXY_HOSTS, envHasProxy, withLoopbackNoProxy, } from './spawnEnv.js';
38
+ export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_NAME_TOKENS, SENSITIVE_ENV_PREFIX_EXCEPTIONS, DAEMON_CHILD_ENV, isDaemonChildEnv, LOOPBACK_NO_PROXY_HOSTS, envHasProxy, withLoopbackNoProxy, } from './spawnEnv.js';
39
39
  export { loopbackFetch, assertLoopbackHttpUrl, isLoopbackHostname, isLoopbackAddress, LoopbackTargetRefusedError, LOOPBACK_DEFAULT_MAX_BODY_BYTES, type LoopbackFetch, type LoopbackFetchInit, } from './loopbackHttp.js';
40
40
  export { CONFIG_DIR_NAME, LEGACY_CONFIG_DIR_NAMES, NATIVE_ID_PREFIX, LEGACY_NATIVE_ID_PREFIXES, DEFAULT_BINARY_NAME, } from './localIdentity.js';
41
41
  export { findLegacyLocalState, describeLegacyState, type LegacyStateFinding, type LegacyStateReport, } from './legacyStatePreflight.js';
@@ -149,7 +149,7 @@ export { HARNESSES_LOADING_CLAUDE_SKILLS_LAYOUT, claudeSkillsLayoutEvidence, har
149
149
  // spawns the SAME engine and did not). `daemon/src/env-sanitize.ts` re-exports
150
150
  // these so its own importers are unchanged, and keeps `filterCustomEnv` — that
151
151
  // governs SERVER-PROVIDED env, which no other process is handed.
152
- export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_NAME_TOKENS, SENSITIVE_ENV_PREFIX_EXCEPTIONS, LOOPBACK_NO_PROXY_HOSTS, envHasProxy, withLoopbackNoProxy, } from './spawnEnv.js';
152
+ export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_NAME_TOKENS, SENSITIVE_ENV_PREFIX_EXCEPTIONS, DAEMON_CHILD_ENV, isDaemonChildEnv, LOOPBACK_NO_PROXY_HOSTS, envHasProxy, withLoopbackNoProxy, } from './spawnEnv.js';
153
153
  // OSK-12134 — the one proxy-free client for loopback HTTP. Bun's fetch (and
154
154
  // Node's under NODE_USE_ENV_PROXY) sends 127.0.0.1 requests to HTTP_PROXY,
155
155
  // credentials and all; every loopback caller goes through this instead.
@@ -77,6 +77,31 @@ export declare const SENSITIVE_ENV_SUFFIXES: readonly string[];
77
77
  export declare const SENSITIVE_ENV_NAME_TOKENS: readonly string[];
78
78
  /** Specific names to keep — the prefix sweeps are broad on purpose. */
79
79
  export declare const SENSITIVE_ENV_PREFIX_EXCEPTIONS: ReadonlySet<string>;
80
+ /**
81
+ * OSK-13254 — the marker the daemon stamps on every agent- or tool-driven child
82
+ * (`daemon/src/env-sanitize.ts`, `daemon/src/terminal/agent-terminal-env.ts`).
83
+ *
84
+ * A `skrr` that sees it is a borrower: it redeems short-lived tokens from the
85
+ * daemon's loopback broker, never adopts a CLI refresh session of its own, and
86
+ * never reads the person's stored one (`cli/src/lib/harness-context.ts`). A
87
+ * person's own terminal — including the web terminal, which is a human surface
88
+ * (`humanTerminalEnv`) — does not carry it.
89
+ *
90
+ * The marker can only take authority away, so a process that forges it hurts
91
+ * nobody but itself. On a laptop it is POLICY, not a boundary: a same-uid child
92
+ * that deletes it can do whatever the person's own shell can. Where a uid
93
+ * boundary exists (Dedicated Runtime, identity-split Hosted Machines), the
94
+ * guest descriptor already offers only the access-token mode, so a durable CLI
95
+ * family cannot be minted there whatever the environment says.
96
+ */
97
+ export declare const DAEMON_CHILD_ENV = "SKRR_DAEMON_CHILD";
98
+ /**
99
+ * True when `env` carries the daemon-child marker with ANY non-empty value.
100
+ * Deliberately no "0 means off": the marker only narrows authority, so the
101
+ * one value that could switch it off is the one a request-carried environment
102
+ * would forge.
103
+ */
104
+ export declare function isDaemonChildEnv(env: Readonly<Record<string, string | undefined>>): boolean;
80
105
  /**
81
106
  * Build a sanitized snapshot of the parent's own env for a spawned
82
107
  * child, stripping credentials and library-injection vectors while