@skrr-ai/cli 0.1.80 → 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 (96) hide show
  1. package/dist/base-command.d.ts +20 -0
  2. package/dist/base-command.js +151 -7
  3. package/dist/commands/agents/visibility.d.ts +14 -0
  4. package/dist/commands/agents/visibility.js +52 -0
  5. package/dist/commands/create-token.d.ts +0 -1
  6. package/dist/commands/create-token.js +3 -1
  7. package/dist/commands/followups/checkin.js +1 -1
  8. package/dist/commands/list-tokens.d.ts +0 -1
  9. package/dist/commands/list-tokens.js +1 -1
  10. package/dist/commands/login.js +10 -0
  11. package/dist/commands/logout.js +4 -0
  12. package/dist/commands/machines/dedicated/ssh-proxy.js +1 -0
  13. package/dist/commands/revoke-token.d.ts +0 -1
  14. package/dist/commands/revoke-token.js +1 -1
  15. package/dist/commands/store/follow.d.ts +1 -0
  16. package/dist/commands/store/follow.js +7 -1
  17. package/dist/commands/store/follows/list.d.ts +11 -0
  18. package/dist/commands/store/follows/list.js +29 -0
  19. package/dist/commands/store/follows/remove.d.ts +16 -0
  20. package/dist/commands/store/follows/remove.js +37 -0
  21. package/dist/commands/store/installs.js +6 -1
  22. package/dist/commands/store/publishers/profile/draft.d.ts +17 -0
  23. package/dist/commands/store/publishers/profile/draft.js +54 -0
  24. package/dist/commands/store/publishers/profile/handle.d.ts +14 -0
  25. package/dist/commands/store/publishers/profile/handle.js +39 -0
  26. package/dist/commands/store/publishers/profile/hide.d.ts +12 -0
  27. package/dist/commands/store/publishers/profile/hide.js +39 -0
  28. package/dist/commands/store/publishers/profile/preview.d.ts +11 -0
  29. package/dist/commands/store/publishers/profile/preview.js +39 -0
  30. package/dist/commands/store/publishers/profile/publish.d.ts +12 -0
  31. package/dist/commands/store/publishers/profile/publish.js +41 -0
  32. package/dist/commands/store/publishers/profile/restore.d.ts +15 -0
  33. package/dist/commands/store/publishers/profile/restore.js +49 -0
  34. package/dist/commands/store/publishers/profile/revisions.d.ts +11 -0
  35. package/dist/commands/store/publishers/profile/revisions.js +30 -0
  36. package/dist/commands/store/publishers/profile/show.d.ts +11 -0
  37. package/dist/commands/store/publishers/profile/show.js +39 -0
  38. package/dist/commands/store/review.d.ts +26 -0
  39. package/dist/commands/store/review.js +93 -0
  40. package/dist/commands/store/reviews.d.ts +29 -0
  41. package/dist/commands/store/reviews.js +170 -0
  42. package/dist/commands/store/tip.d.ts +20 -0
  43. package/dist/commands/store/tip.js +57 -0
  44. package/dist/commands/tokens/create.d.ts +28 -0
  45. package/dist/commands/tokens/create.js +136 -0
  46. package/dist/commands/tokens/list.d.ts +11 -0
  47. package/dist/commands/tokens/list.js +53 -0
  48. package/dist/commands/tokens/revoke.d.ts +14 -0
  49. package/dist/commands/tokens/revoke.js +42 -0
  50. package/dist/lib/access-tokens.d.ts +59 -0
  51. package/dist/lib/access-tokens.js +50 -0
  52. package/dist/lib/auth-core-init.d.ts +6 -0
  53. package/dist/lib/auth-core-init.js +28 -6
  54. package/dist/lib/auth-storage.d.ts +5 -0
  55. package/dist/lib/auth-storage.js +8 -0
  56. package/dist/lib/config.d.ts +8 -0
  57. package/dist/lib/credential-resolver.d.ts +1 -1
  58. package/dist/lib/credential-resolver.js +15 -3
  59. package/dist/lib/daemonBroker.d.ts +48 -2
  60. package/dist/lib/daemonBroker.js +193 -15
  61. package/dist/lib/daemonBrokerRefusal.d.ts +14 -0
  62. package/dist/lib/daemonBrokerRefusal.js +18 -1
  63. package/dist/lib/dedicated-ssh.d.ts +9 -0
  64. package/dist/lib/dedicated-ssh.js +232 -63
  65. package/dist/lib/harness-context.d.ts +8 -0
  66. package/dist/lib/harness-context.js +55 -0
  67. package/dist/lib/login.js +4 -1
  68. package/dist/lib/node-adapter.js +2 -2
  69. package/dist/lib/publisher-profile-flags.d.ts +19 -0
  70. package/dist/lib/publisher-profile-flags.js +48 -0
  71. package/dist/lib/refresh.d.ts +1 -1
  72. package/dist/lib/refresh.js +10 -1
  73. package/dist/lib/ssh-ordered-bytes.d.ts +8 -0
  74. package/dist/lib/ssh-ordered-bytes.js +31 -0
  75. package/dist/lib/usd-micros.d.ts +9 -0
  76. package/dist/lib/usd-micros.js +21 -0
  77. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialResolver.d.ts +22 -3
  78. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialResolver.js +29 -5
  79. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +7 -1
  80. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +7 -1
  81. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -2
  82. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +9 -5
  83. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +25 -0
  84. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +35 -1
  85. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialResolver.d.ts +22 -3
  86. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialResolver.js +27 -4
  87. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +7 -1
  88. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +7 -1
  89. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -2
  90. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -2
  91. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +25 -0
  92. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +33 -0
  93. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  94. package/dist/node_modules/@skrr-ai/data-provider/index.js +4302 -4249
  95. package/oclif.manifest.json +41314 -39972
  96. package/package.json +28 -4
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ACCESS_TOKEN_REVOKED_REASON_LABELS = void 0;
4
+ exports.createAccessToken = createAccessToken;
5
+ exports.listAccessTokens = listAccessTokens;
6
+ exports.revokeAccessToken = revokeAccessToken;
7
+ exports.describeAccessTokenState = describeAccessTokenState;
8
+ const api_fetch_1 = require("./api-fetch");
9
+ /**
10
+ * The words for each stored revocation reason. Every member of the schema's
11
+ * `revokedReason` enum appears here, so the list shows WHY a token died —
12
+ * "revoked" alone cannot tell a deliberate revoke from a sign-out sweep.
13
+ */
14
+ exports.ACCESS_TOKEN_REVOKED_REASON_LABELS = {
15
+ user: 'revoked by you',
16
+ sign_out_everywhere: 'revoked by sign-out everywhere / password or 2FA change',
17
+ admin: 'revoked by an administrator',
18
+ compromised: 'revoked as compromised',
19
+ };
20
+ async function createAccessToken(input) {
21
+ return (0, api_fetch_1.apiFetch)('/api/user/access-tokens', {
22
+ method: 'POST',
23
+ body: {
24
+ name: input.name,
25
+ ...(input.scopes ? { scopes: input.scopes } : {}),
26
+ ...(input.expiresInDays !== undefined ? { expiresInDays: input.expiresInDays } : {}),
27
+ },
28
+ });
29
+ }
30
+ /** Every token the caller owns, newest first, including revoked and expired rows. */
31
+ async function listAccessTokens() {
32
+ const res = await (0, api_fetch_1.apiFetch)('/api/user/access-tokens');
33
+ return Array.isArray(res?.accessTokens) ? res.accessTokens : [];
34
+ }
35
+ /** Revoke one token. Irreversible: a revoked token is replaced, never re-enabled. */
36
+ async function revokeAccessToken(id, opts = {}) {
37
+ return (0, api_fetch_1.apiFetch)(`/api/user/access-tokens/${encodeURIComponent(id)}`, {
38
+ method: 'DELETE',
39
+ ...(opts.compromised ? { body: { reason: 'compromised' } } : {}),
40
+ });
41
+ }
42
+ /** One-line status for a listed token. */
43
+ function describeAccessTokenState(token) {
44
+ if (token.state === 'revoked') {
45
+ return exports.ACCESS_TOKEN_REVOKED_REASON_LABELS[token.revokedReason ?? ''] ?? 'revoked';
46
+ }
47
+ if (token.state === 'expired')
48
+ return 'expired';
49
+ return 'active';
50
+ }
@@ -36,6 +36,12 @@ export declare function plainCredentialEnvelopeWarning(args: readonly unknown[])
36
36
  * own when a local daemon can hand a credential over.
37
37
  */
38
38
  export declare const CREDENTIAL_KEY_CHANGED_NOTICE: string;
39
+ /**
40
+ * The one line an unreadable device key prints when no credential-key change
41
+ * explains it. The CLI never enrolls or requires a device key (OSK-12063), so
42
+ * this degrades a proof, not access — say exactly that.
43
+ */
44
+ export declare const DEVICE_KEY_RECOVERY_REQUIRED_NOTICE: string;
39
45
  export declare function createCredentialEnvelopeWarningPresenter(): (args: readonly unknown[]) => string | null;
40
46
  type DedicatedGenerationPreparationOptions = {
41
47
  machineUuid?: string;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.CliCredentialInitializationError = exports.CREDENTIAL_KEY_CHANGED_NOTICE = void 0;
6
+ exports.CliCredentialInitializationError = exports.DEVICE_KEY_RECOVERY_REQUIRED_NOTICE = exports.CREDENTIAL_KEY_CHANGED_NOTICE = void 0;
7
7
  exports.plainCredentialEnvelopeWarning = plainCredentialEnvelopeWarning;
8
8
  exports.createCredentialEnvelopeWarningPresenter = createCredentialEnvelopeWarningPresenter;
9
9
  exports.prepareDedicatedRuntimeAuthGeneration = prepareDedicatedRuntimeAuthGeneration;
@@ -108,6 +108,20 @@ exports.CREDENTIAL_KEY_CHANGED_NOTICE = "note: this machine's credential key cha
108
108
  */
109
109
  /** auth-core's device-identity warning for a private key it could not open. */
110
110
  const DEVICE_KEY_DECRYPT_FAILED = '[deviceIdentity] private_key_decrypt_failed';
111
+ /**
112
+ * auth-core's device-identity warning for a stored key it will NOT replace on
113
+ * ordinary boot (`b5e683280b`): minting a new key here would make the next
114
+ * refresh sign with a key the server never pinned. Only an explicit login with
115
+ * `recoverUnreadable` replaces it.
116
+ */
117
+ const DEVICE_KEY_RECOVERY_REQUIRED = '[deviceIdentity] recovery_required';
118
+ /**
119
+ * The one line an unreadable device key prints when no credential-key change
120
+ * explains it. The CLI never enrolls or requires a device key (OSK-12063), so
121
+ * this degrades a proof, not access — say exactly that.
122
+ */
123
+ exports.DEVICE_KEY_RECOVERY_REQUIRED_NOTICE = "warning: this machine's stored device key could not be read, so skrr sends requests " +
124
+ 'without a device proof until the local daemon signs in again. (DEBUG=1 shows the underlying error.)';
111
125
  function createCredentialEnvelopeWarningPresenter() {
112
126
  let keyChanged = false;
113
127
  return (args) => {
@@ -133,12 +147,20 @@ function createCredentialEnvelopeWarningPresenter() {
133
147
  if (keyChanged && event === 'cred_envelope.read.decrypt_failed')
134
148
  return '';
135
149
  // The device private key is one of those values: auth-core's device
136
- // identity loads it next, cannot open it, and mints a new keypair. It
137
- // logs that through its own `[deviceIdentity]` prefix, which reached the
138
- // terminal as a raw `[auth-core warn]` line right under the notice. The
139
- // notice covers it; without a key change it is a real fault and prints.
140
- if (keyChanged && message === DEVICE_KEY_DECRYPT_FAILED)
150
+ // identity loads it next and cannot open it. Since `b5e683280b` it does
151
+ // NOT mint a replacement on ordinary boot — it parks in
152
+ // `recovery_required` until an explicit login recovers it. Both warnings
153
+ // come through the `[deviceIdentity]` prefix and reached the terminal as
154
+ // raw `[auth-core warn]` lines right under the notice, which already says
155
+ // the old key is gone and what replaces it.
156
+ if (keyChanged &&
157
+ (message === DEVICE_KEY_DECRYPT_FAILED || message === DEVICE_KEY_RECOVERY_REQUIRED)) {
141
158
  return '';
159
+ }
160
+ // Without a key change an unreadable device key is a real fault: one
161
+ // plain line instead of an object dump.
162
+ if (message === DEVICE_KEY_RECOVERY_REQUIRED)
163
+ return exports.DEVICE_KEY_RECOVERY_REQUIRED_NOTICE;
142
164
  return plainCredentialEnvelopeWarning(args);
143
165
  };
144
166
  }
@@ -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
  *
@@ -5,7 +5,7 @@
5
5
  * CLI-appropriate backends. The resolver walks the published chain:
6
6
  *
7
7
  * 1. flag — --token <jwt> (debugging; BaseCommand parses)
8
- * 2. env-token — OVERSKY_TOKEN (CI / static; kind='ci' detected)
8
+ * 2. env-token — OVERSKY_TOKEN, then SKRR_TOKEN (CI / static; kind 'ci' or 'pat' detected)
9
9
  * 3. auth-helper — OVERSKY_AUTH_HELPER script output
10
10
  * 4. fd-handoff — N/A for CLI (skipped; no parent fd)
11
11
  * 5. keychain — ai.skrr.cli.<profile>
@@ -12,7 +12,7 @@ exports.formatCredentialSource = formatCredentialSource;
12
12
  * CLI-appropriate backends. The resolver walks the published chain:
13
13
  *
14
14
  * 1. flag — --token <jwt> (debugging; BaseCommand parses)
15
- * 2. env-token — OVERSKY_TOKEN (CI / static; kind='ci' detected)
15
+ * 2. env-token — OVERSKY_TOKEN, then SKRR_TOKEN (CI / static; kind 'ci' or 'pat' detected)
16
16
  * 3. auth-helper — OVERSKY_AUTH_HELPER script output
17
17
  * 4. fd-handoff — N/A for CLI (skipped; no parent fd)
18
18
  * 5. keychain — ai.skrr.cli.<profile>
@@ -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
@@ -78,6 +84,9 @@ async function resolveCredential(opts = {}) {
78
84
  flagToken: opts.flagToken,
79
85
  bareMode,
80
86
  storeMode,
87
+ // SKRR_TOKEN is the name the CI recipe uses (OSK-13208); OVERSKY_TOKEN still
88
+ // wins when both are set, as in the config overlay (config.ts).
89
+ envTokenNames: ['OVERSKY_TOKEN', 'SKRR_TOKEN'],
81
90
  // CLI never has an interactive slot — users run `skrr login` explicitly.
82
91
  isHeadless: () => true,
83
92
  // Stored credentials are only usable against the origin that issued
@@ -160,7 +169,10 @@ function formatCredentialSource(source) {
160
169
  case 'flag':
161
170
  return 'CLI --token flag';
162
171
  case 'env-token':
163
- return 'OVERSKY_TOKEN';
172
+ // Name the variable that actually supplied it (OVERSKY_TOKEN wins).
173
+ return !process.env.OVERSKY_TOKEN?.trim() && process.env.SKRR_TOKEN?.trim()
174
+ ? 'SKRR_TOKEN'
175
+ : 'OVERSKY_TOKEN';
164
176
  case 'auth-helper':
165
177
  return 'OVERSKY_AUTH_HELPER';
166
178
  case 'fd-handoff':
@@ -1,5 +1,5 @@
1
1
  /**
2
- * daemonBroker.ts — Sky CLI client for the local `oversky` daemon's
2
+ * daemonBroker.ts — skrr CLI client for the local `skrrd` daemon's
3
3
  * cli-handoff endpoint.
4
4
  *
5
5
  * When the daemon is already authenticated for the user on this machine,
@@ -199,7 +199,7 @@ export interface DaemonBrokerOptions {
199
199
  /** Stable per-machine cliId the CLI wants the new refresh row bound to. */
200
200
  cliId: string;
201
201
  /**
202
- * The Sky CLI's currently-configured baseURL (e.g. `https://oversky.ai`).
202
+ * The skrr CLI's currently-configured baseURL (e.g. `https://skrr.ai`).
203
203
  * Two roles:
204
204
  * 1. Pre-flight check vs `bootstrap.serverUrl` — when they don't
205
205
  * match, we refuse to call the daemon so we never mint a token
@@ -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
@@ -461,6 +469,33 @@ export interface MaybeAutoBrokerOptions {
461
469
  * everywhere else. Production callers omit it and behave exactly as before.
462
470
  */
463
471
  bootstrapPathOverride?: string;
472
+ /**
473
+ * The profile whose daemon may be borrowed. Defaults to the active profile.
474
+ *
475
+ * A silent hand-off must never cross profiles (OSK-13257). The `default`
476
+ * candidate list falls through to any running daemon, and every daemon is
477
+ * signed in as whoever started it — so `--profile buyer`, with its own
478
+ * session refused, used to run as the machine owner after one stderr line.
479
+ * A named profile now borrows only a daemon started under that same
480
+ * profile, and otherwise reports NOT_SIGNED_IN for the profile it was given.
481
+ */
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;
464
499
  }
465
500
  export interface MaybeAutoBrokerResult {
466
501
  /**
@@ -478,7 +513,18 @@ export interface MaybeAutoBrokerResult {
478
513
  * and end up with a divergent refresh family.
479
514
  */
480
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;
481
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;
482
528
  /**
483
529
  * Auto-broker pre-flight for `BaseCommand.init()`. When the credential
484
530
  * resolver returns no token, this attempts a silent daemon broker so
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * daemonBroker.ts — Sky CLI client for the local `oversky` daemon's
3
+ * daemonBroker.ts — skrr CLI client for the local `skrrd` daemon's
4
4
  * cli-handoff endpoint.
5
5
  *
6
6
  * When the daemon is already authenticated for the user on this machine,
@@ -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,18 +92,22 @@ 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");
110
+ const keychain_1 = require("./keychain");
104
111
  const REQUEST_TIMEOUT_MS = 20_000;
105
112
  /**
106
113
  * Normalize a base URL for equality comparison. Mirrors the daemon-side
@@ -247,9 +254,9 @@ exports.HOSTED_MACHINE_CLI_HANDOFF_DESCRIPTOR = '/run/skrr-hosted-machine/cli-ha
247
254
  /** Where the cli-handoff broker looks, in order. */
248
255
  function resolveCliHandoffCandidatePaths(profile = 'default') {
249
256
  const candidates = resolveBootstrapCandidatePaths(profile);
250
- // Any profile: a guest runs one daemon, and the auto-broker resolves the
251
- // default profile regardless of `--profile`, so a narrower condition here would
252
- // only describe behaviour production never reaches.
257
+ // Any profile: a guest runs one daemon holding the one account the guest
258
+ // serves, so every profile on it is that account. On a laptop a named profile
259
+ // reaches only its own island file above (OSK-13257).
253
260
  if (process.platform === 'linux') {
254
261
  candidates.push(exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR, exports.HOSTED_MACHINE_CLI_HANDOFF_DESCRIPTOR);
255
262
  }
@@ -561,8 +568,19 @@ async function brokerThrough(bootstrap, opts, sourcePath) {
561
568
  // would either break explicit login or be bypassed by anyone who calls it
562
569
  // the way login does. It is not a boundary, so it is not attempted. What the
563
570
  // CLI CAN guarantee is that its own automatic path never mints and persists
564
- // a refresh family: when `access_token` is advertised, a failed redeem is
565
- // 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.
566
584
  //
567
585
  // COMPATIBILITY WINDOW: a v1 descriptor (no `handoffModes`, so no
568
586
  // `access_token`) still takes the durable mint on the automatic path below,
@@ -577,7 +595,17 @@ async function brokerThrough(bootstrap, opts, sourcePath) {
577
595
  const offersAccessToken = (0, cli_handoff_wire_1.descriptorOffersAccessToken)(bootstrap);
578
596
  const offersRefreshFamily = modes.includes('refresh_family');
579
597
  const preferAccessToken = opts.preferHandoffMode === 'access_token';
580
- 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)) {
581
609
  const redeem = await redeemCliHandoffToken(bootstrap, {
582
610
  forceRefresh: false,
583
611
  timeoutMs: opts.timeoutMs,
@@ -1149,6 +1177,16 @@ async function attemptDaemonBrokerLoginAndPersist(opts) {
1149
1177
  // Callers use the token in-memory for this process only.
1150
1178
  if (outcome.brokered)
1151
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
+ }
1152
1190
  const serverOrigin = normalizeBaseUrl(opts.baseURL);
1153
1191
  if (!serverOrigin) {
1154
1192
  throw new Error('Daemon-broker credential persistence requires a valid CLI baseURL.');
@@ -1198,6 +1236,123 @@ const AUTH_SELF_MANAGED_COMMANDS = new Set([
1198
1236
  'pair',
1199
1237
  'generate-recovery-code',
1200
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
+ }
1201
1356
  /**
1202
1357
  * Auto-broker pre-flight for `BaseCommand.init()`. When the credential
1203
1358
  * resolver returns no token, this attempts a silent daemon broker so
@@ -1243,20 +1398,43 @@ async function maybeAutoBroker(opts) {
1243
1398
  // confirm cannot retire another profile's session. Persisted by the caller on
1244
1399
  // success so the rotation row stays bound across processes.
1245
1400
  const { cliId, updatedConfig } = (0, cli_identity_1.planLoginCliId)(opts.cliConfig);
1246
- const outcome = await attemptDaemonBrokerLoginAndPersist({
1401
+ const base = {
1247
1402
  cliId,
1248
1403
  baseURL: opts.cliConfig.baseURL,
1249
- // Auto-broker prefers the brokered access-token mode when the descriptor
1250
- // advertises it: nothing credential-shaped is persisted, and the next
1251
- // `skrr` process simply re-redeems over loopback. The legacy refresh-
1252
- // family mint remains the fallback for v1 descriptors and the redeem
1253
- // path for explicit `skrr login`.
1254
- preferHandoffMode: 'access_token',
1404
+ profile: opts.profile ?? (0, keychain_1.getActiveProfile)(),
1255
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 } : {}),
1256
1434
  });
1257
1435
  return {
1258
1436
  triggered: true,
1259
1437
  outcome,
1260
- ...(updatedConfig && outcome.ok ? { updatedConfig } : {}),
1438
+ ...(updatedConfig && outcome.ok && !harness ? { updatedConfig } : {}),
1261
1439
  };
1262
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;