@skrr-ai/cli 0.1.65 → 0.1.67

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.
@@ -17,11 +17,10 @@ exports.__resetPrefetchCliAuthForTest = __resetPrefetchCliAuthForTest;
17
17
  * Runs as a side-effect import from base-command.ts so every command path
18
18
  * (including future ones) gets the same DI setup. Idempotent and cheap.
19
19
  *
20
- * Phase A scope:
20
+ * Runtime scope:
21
21
  * - Logger: a thin shim over `console`. CLI is a one-shot process; we
22
22
  * don't ship winston for this.
23
- * - configDir: still ~/.sky/ in Phase A. Phase D will repoint to
24
- * ~/.skrr/ with a one-shot read-shim migration.
23
+ * - configDir: the shared ~/.skrr/ root, with profile state below it.
25
24
  * - Kill switches: respect OVERSKY_DISABLE_REFRESH_SCHEDULER for parity
26
25
  * with the daemon's rollback knob.
27
26
  * - binaryName: 'skrr' so reauth banners say `skrr login` not `oversky login`.
@@ -42,6 +41,7 @@ const auth_core_1 = require("@skrr-ai/auth-core");
42
41
  const cred_envelope_1 = require("./cred-envelope");
43
42
  const device_identity_1 = require("./device-identity");
44
43
  const auth_storage_1 = require("./auth-storage");
44
+ const keychain_1 = require("./keychain");
45
45
  function envFlag(name) {
46
46
  const v = process.env[name];
47
47
  return v === '1' || v === 'true' || v === 'TRUE';
@@ -165,12 +165,6 @@ const cliLogger = {
165
165
  },
166
166
  error: (...args) => console.error('[auth-core error]', ...args),
167
167
  };
168
- /**
169
- * Profile name in shared on-disk paths. Pre-Phase-K every consumer
170
- * resolves to `'default'`; matches `daemon/src/auth.ts:DEFAULT_PROFILE`
171
- * so cross-binary file paths align byte-for-byte.
172
- */
173
- const DEFAULT_PROFILE = 'default';
174
168
  const DEDICATED_AUTH_GENERATION_MARKER = 'dedicated-runtime-auth-generation';
175
169
  /**
176
170
  * A Dedicated Runtime's workspace survives replacement but `/etc/machine-id`
@@ -192,7 +186,11 @@ async function prepareDedicatedRuntimeAuthGeneration(options = {}) {
192
186
  return false;
193
187
  const configDir = options.configDir ?? (0, auth_core_1.resolveConfigRoot)();
194
188
  node_fs_1.default.mkdirSync(configDir, { recursive: true, mode: 0o700 });
195
- const markerPath = node_path_1.default.join(configDir, DEDICATED_AUTH_GENERATION_MARKER);
189
+ const profile = (0, keychain_1.getActiveProfile)();
190
+ const markerName = profile === keychain_1.DEFAULT_PROFILE
191
+ ? DEDICATED_AUTH_GENERATION_MARKER
192
+ : `${DEDICATED_AUTH_GENERATION_MARKER}.${profile}`;
193
+ const markerPath = node_path_1.default.join(configDir, markerName);
196
194
  const release = await proper_lockfile_1.default.lock(configDir, {
197
195
  lockfilePath: `${markerPath}.lock`,
198
196
  realpath: false,
@@ -212,12 +210,12 @@ async function prepareDedicatedRuntimeAuthGeneration(options = {}) {
212
210
  (options.clearStoredAuth ?? auth_storage_1.clearAllBackends)();
213
211
  await (options.clearDeviceIdentity ??
214
212
  (() => (0, device_identity_1.resetDeviceIdentity)({
215
- profile: DEFAULT_PROFILE,
213
+ profile,
216
214
  clearOnDisk: true,
217
215
  })))();
218
216
  await (options.clearCredentialEnvelope ??
219
217
  (() => (0, cred_envelope_1.resetCredEnvelope)({
220
- profile: DEFAULT_PROFILE,
218
+ profile,
221
219
  clearOnDisk: true,
222
220
  })))();
223
221
  const temporary = `${markerPath}.tmp.${process.pid}.${Date.now()}`;
@@ -298,6 +296,7 @@ function ensureAuthCoreConfigured() {
298
296
  let _prefetchPromise = null;
299
297
  let _credEnvelopeInitPromise = null;
300
298
  let _credEnvelopeInitStartedAt = 0;
299
+ let _prefetchProfile = null;
301
300
  class CliCredentialInitializationError extends Error {
302
301
  code = 'CREDENTIAL_INITIALIZATION_UNAVAILABLE';
303
302
  exitCode = 75;
@@ -342,15 +341,21 @@ function encryptedCredentialInitError(timedOut, cause) {
342
341
  'The credential remains protected; unlock or repair the OS credential store and retry.');
343
342
  }
344
343
  function ensureCliAuthPrefetchStarted() {
345
- if (_prefetchPromise)
344
+ const profile = (0, keychain_1.getActiveProfile)();
345
+ if (_prefetchPromise) {
346
+ if (_prefetchProfile !== profile) {
347
+ throw new CliCredentialInitializationError(`Credential initialization already belongs to profile ${_prefetchProfile}; start a new skrr process for profile ${profile}.`);
348
+ }
346
349
  return;
350
+ }
351
+ _prefetchProfile = profile;
347
352
  _credEnvelopeInitStartedAt = Date.now();
348
- const envelopeInit = (0, cred_envelope_1.initCredEnvelope)(DEFAULT_PROFILE);
353
+ const envelopeInit = (0, cred_envelope_1.initCredEnvelope)(profile);
349
354
  _credEnvelopeInitPromise = envelopeInit;
350
355
  _prefetchPromise = (async () => {
351
356
  try {
352
357
  await envelopeInit;
353
- await (0, device_identity_1.initDeviceIdentity)(DEFAULT_PROFILE);
358
+ await (0, device_identity_1.initDeviceIdentity)(profile);
354
359
  // The CLI deliberately does NOT enroll a device key with the server
355
360
  // (OSK-12063). Enrollment pins the FIRST key presented on a machine's
356
361
  // Daemon row, and every later refresh signed by a different key is
@@ -431,6 +436,7 @@ function __resetPrefetchCliAuthForTest() {
431
436
  _prefetchPromise = null;
432
437
  _credEnvelopeInitPromise = null;
433
438
  _credEnvelopeInitStartedAt = 0;
439
+ _prefetchProfile = null;
434
440
  }
435
441
  // Eager-configure on first import. Cheap and avoids ordering pitfalls in
436
442
  // any future command that calls auth-core helpers from outside a Command.
@@ -1,4 +1,4 @@
1
- /** Pre-Phase-K namespace; reserved for a future migration helper. */
1
+ /** Default profile when no profile flag or environment override is present. */
2
2
  export declare const DEFAULT_PROFILE = "default";
3
3
  export declare function setActiveProfile(profile: string): void;
4
4
  /**
@@ -102,11 +102,13 @@ export declare function deleteRefreshToken(): void;
102
102
  * Returns the services it actually removed something from, so the caller can
103
103
  * say what happened instead of destroying credentials silently.
104
104
  *
105
- * NOTE this also signs out the retired `oversky` binary, if one is still
106
- * installed. That is the intent — same account, same machine — and the cutover
107
- * runbook already lists these items for removal.
105
+ * Ordinary logout removes only retired CLI slots. Machine credentials and
106
+ * KEKs are shared with the daemon and are removed only by --all-devices.
107
+ * That broader mode also signs out an installed retired `oversky` daemon.
108
108
  */
109
- export declare function deleteLegacyBrandEntries(): string[];
109
+ export declare function deleteLegacyBrandEntries(options?: {
110
+ includeMachineCredentials?: boolean;
111
+ }): string[];
110
112
  /**
111
113
  * Remove every CLI keychain slot for the active profile. Used by `skrr logout`.
112
114
  * Safe to call on a clean system — missing items are not an error.
@@ -67,10 +67,8 @@ exports.resolveProfileFromArgv = resolveProfileFromArgv;
67
67
  * Mirrors `daemon/src/keychain.ts` but simpler:
68
68
  * - No prefetch: the CLI is a short-lived foreground process; parallelizing
69
69
  * the keychain probe with other startup work isn't worth the code.
70
- * - Namespaced `ai.skrr.cli.<profile>` from day one. Before Phase K
71
- * ships profile switching, `<profile>` is always `default`, but baking
72
- * the suffix in now makes the future migration a rename instead of a
73
- * schema change.
70
+ * - Namespaced `ai.skrr.cli.<profile>` so each selected profile keeps
71
+ * its own CLI refresh family.
74
72
  * - Distinct service from the daemon (`ai.skrr.cli.*` vs.
75
73
  * `ai.skrr.daemon.*`). The CLI holds `scope: 'cli'` tokens; the
76
74
  * daemon holds `scope: 'daemon'`. They must never read each other's
@@ -121,7 +119,7 @@ const cred_envelope_1 = require("./cred-envelope");
121
119
  const auth_core_1 = require("@skrr-ai/auth-core");
122
120
  /** Service-name prefix. Always followed by `.<profile>`. */
123
121
  const SERVICE_BASE = 'ai.skrr.cli';
124
- /** Pre-Phase-K namespace; reserved for a future migration helper. */
122
+ /** Default profile when no profile flag or environment override is present. */
125
123
  exports.DEFAULT_PROFILE = 'default';
126
124
  const ACCOUNT_TOKEN = 'cli-token';
127
125
  const ACCOUNT_EXPIRES = 'cli-token-expires';
@@ -619,14 +617,16 @@ function deleteEntireService(service) {
619
617
  * Returns the services it actually removed something from, so the caller can
620
618
  * say what happened instead of destroying credentials silently.
621
619
  *
622
- * NOTE this also signs out the retired `oversky` binary, if one is still
623
- * installed. That is the intent — same account, same machine — and the cutover
624
- * runbook already lists these items for removal.
620
+ * Ordinary logout removes only retired CLI slots. Machine credentials and
621
+ * KEKs are shared with the daemon and are removed only by --all-devices.
622
+ * That broader mode also signs out an installed retired `oversky` daemon.
625
623
  */
626
- function deleteLegacyBrandEntries() {
624
+ function deleteLegacyBrandEntries(options = {}) {
627
625
  if (!isAvailable())
628
626
  return [];
629
- return discoverLegacyBrandServices().filter((service) => deleteEntireService(service));
627
+ return discoverLegacyBrandServices()
628
+ .filter((service) => options.includeMachineCredentials === true || /^(?:ai|com)\.oversky\.cli\./.test(service))
629
+ .filter((service) => deleteEntireService(service));
630
630
  }
631
631
  /**
632
632
  * Remove every CLI keychain slot for the active profile. Used by `skrr logout`.
@@ -45,6 +45,8 @@ export interface EnsureFreshCliCredentialResult {
45
45
  /** True when we adopted a fresher token another process already wrote. */
46
46
  adoptedStoredToken?: boolean;
47
47
  pair?: TokenPair;
48
+ /** Stable diagnostic code; never contains token or server response text. */
49
+ refreshFailureCode?: string;
48
50
  skippedReason?: 'no_token' | 'ci_token' | 'external_source' | 'env_override' | 'bare_mode' | 'fresh' | 'no_refresh_token' | 'refresh_failed';
49
51
  }
50
52
  /**
@@ -58,6 +58,7 @@ class CliRefreshUnavailableError extends Error {
58
58
  }
59
59
  exports.CliRefreshUnavailableError = CliRefreshUnavailableError;
60
60
  let ensureInFlight = null;
61
+ let reportedPermanentRefreshFailure = false;
61
62
  function expiryFromToken(token) {
62
63
  if (!token)
63
64
  return undefined;
@@ -158,8 +159,25 @@ async function ensureFreshCliCredential(opts = {}) {
158
159
  pair,
159
160
  };
160
161
  }
161
- catch {
162
- return { credential, refreshed: false, skippedReason: 'refresh_failed' };
162
+ catch (err) {
163
+ const refreshFailureCode = err instanceof auth_core_1.PermanentAuthFailure
164
+ ? err.code
165
+ : err instanceof auth_core_1.TransientAuthFailure
166
+ ? 'TRANSIENT_REFRESH_FAILURE'
167
+ : err instanceof CliRefreshUnavailableError
168
+ ? 'LOCAL_REFRESH_UNAVAILABLE'
169
+ : 'LOCAL_REFRESH_ERROR';
170
+ if (err instanceof auth_core_1.PermanentAuthFailure && !reportedPermanentRefreshFailure) {
171
+ reportedPermanentRefreshFailure = true;
172
+ console.error(`[skrr/auth] Stored CLI session refresh was rejected (${refreshFailureCode}). ` +
173
+ 'The background service may still provide access; run `skrr login` if it cannot.');
174
+ }
175
+ return {
176
+ credential,
177
+ refreshed: false,
178
+ skippedReason: 'refresh_failed',
179
+ refreshFailureCode,
180
+ };
163
181
  }
164
182
  finally {
165
183
  ensureInFlight = null;
@@ -1,10 +1,12 @@
1
- import { type MemoryMount as SharedMemoryMount, type MemoryStore as SharedMemoryStore } from '@skrr-ai/data-schemas';
1
+ import type { MemoryMount as SharedMemoryMount, MemoryStore as SharedMemoryStore } from '@skrr-ai/data-schemas';
2
2
  import { BaseCommand } from '../base-command';
3
3
  import { ApiFetchError, type ApiFetchOptions } from './api-fetch';
4
4
  export type MemoryMount = SharedMemoryMount;
5
5
  export type MemoryStore = SharedMemoryStore & {
6
- mount?: SharedMemoryMount;
6
+ mount: SharedMemoryMount;
7
7
  };
8
+ export declare function parseMemoryMount(value: unknown): MemoryMount;
9
+ export declare function parseMountedMemoryStore(value: unknown): MemoryStore;
8
10
  export declare const scopedMemoryFlags: {
9
11
  agent: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
10
12
  space: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -1,12 +1,36 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ScopedMemoryCommand = exports.versionedMutationFlags = exports.mutationFlags = exports.scopedMemoryFlags = void 0;
4
+ exports.parseMemoryMount = parseMemoryMount;
5
+ exports.parseMountedMemoryStore = parseMountedMemoryStore;
4
6
  exports.parseMemoryApiError = parseMemoryApiError;
5
7
  const core_1 = require("@oclif/core");
6
- const data_schemas_1 = require("@skrr-ai/data-schemas");
7
8
  const node_crypto_1 = require("node:crypto");
8
9
  const base_command_1 = require("../base-command");
9
10
  const api_fetch_1 = require("./api-fetch");
11
+ // The public CLI must not load the private data-schemas package at runtime.
12
+ // The API validates the full wire contract; check the fields used by CLI commands
13
+ // before treating a response as a mounted Store or mount.
14
+ function parseMemoryMount(value) {
15
+ const mount = value;
16
+ if (!mount || typeof mount !== 'object' || typeof mount.storeId !== 'string' ||
17
+ typeof mount.uri !== 'string' || typeof mount.authorizationGeneration !== 'number' ||
18
+ !['read_only', 'read_write'].includes(String(mount.access)) ||
19
+ !['agent', 'current_space', 'ancestor_space'].includes(String(mount.source))) {
20
+ throw new Error('Invalid Memory mount response from server');
21
+ }
22
+ return mount;
23
+ }
24
+ function parseMountedMemoryStore(value) {
25
+ const store = value;
26
+ if (!store || typeof store !== 'object' || typeof store.id !== 'string' ||
27
+ typeof store.revision !== 'number' || typeof store.authorizationGeneration !== 'number' ||
28
+ !store.owner || !['agent', 'space'].includes(String(store.owner.kind)) ||
29
+ typeof store.owner.id !== 'string') {
30
+ throw new Error('Invalid Memory Store response from server');
31
+ }
32
+ return { ...store, mount: parseMemoryMount(store.mount) };
33
+ }
10
34
  exports.scopedMemoryFlags = {
11
35
  agent: core_1.Flags.string({ description: 'Act through this Agent', required: true }),
12
36
  space: core_1.Flags.string({ description: 'Current Space context (required for Space scopes)' }),
@@ -126,7 +150,7 @@ class ScopedMemoryCommand extends base_command_1.BaseCommand {
126
150
  async resolveStore(flags, { write = false } = {}) {
127
151
  if (flags.store) {
128
152
  const rawStore = await this.memoryApi(flags.agent, `/stores/${encodeURIComponent(flags.store)}${this.query(flags)}`);
129
- const store = data_schemas_1.MountedMemoryStoreSchema.parse(rawStore);
153
+ const store = parseMountedMemoryStore(rawStore);
130
154
  if (write && store.mount?.access !== 'read_write') {
131
155
  this.error('MEMORY_STORE_READ_ONLY: selected Store is read-only', { exit: 5 });
132
156
  }
@@ -143,7 +167,7 @@ class ScopedMemoryCommand extends base_command_1.BaseCommand {
143
167
  this.error(`MEMORY_SPACE_CONTEXT_REQUIRED: --space ${id} is required when selecting ${flags.scope}.`, { exit: 2, code: 'MEMORY_SPACE_CONTEXT_REQUIRED' });
144
168
  }
145
169
  const response = await this.memoryApi(flags.agent, `/stores${this.query(flags)}`);
146
- const stores = response.stores.map((store) => data_schemas_1.MountedMemoryStoreSchema.parse(store));
170
+ const stores = response.stores.map(parseMountedMemoryStore);
147
171
  const store = stores.find((candidate) => candidate.owner.kind === kind && candidate.owner.id === id);
148
172
  if (!store)
149
173
  this.error(`MEMORY_NOT_MOUNTED: ${flags.scope}`, { exit: 5 });
@@ -10,7 +10,7 @@ type DeviceIdentityState = {
10
10
  profile: string;
11
11
  envelopeProtected: boolean;
12
12
  };
13
- type DisabledReason = 'init_failed' | 'load_failed' | 'persist_failed';
13
+ type DisabledReason = 'init_failed' | 'load_failed' | 'persist_failed' | 'unreadable_key';
14
14
  /** @internal Test seam — force-set state. */
15
15
  export declare function __setStateForTest(next: DeviceIdentityState): DeviceIdentityState;
16
16
  /** @internal Test seam — read state. */
@@ -42,11 +42,13 @@ export declare function describeDeviceKeySplit(profile: string): {
42
42
  * 2. Generate fresh keypair and persist. Failure to persist is a hard
43
43
  * error landing in disabled+persist_failed.
44
44
  */
45
- export declare function initDeviceIdentity(profile: string): Promise<void>;
45
+ export declare function initDeviceIdentity(profile: string, options?: {
46
+ recoverUnreadable?: boolean;
47
+ }): Promise<void>;
46
48
  /**
47
- * Reset state. With `clearOnDisk=true` deletes both keypair files —
48
- * used by `oversky logout` / `sky logout` so a freshly-logged-in user
49
- * gets a fresh device identity that the daemon will see on its next start.
49
+ * Reset in-memory state. With `clearOnDisk=true` deletes both keypair files.
50
+ * Machine-wide sign-out may use that option; ordinary CLI sign-out preserves
51
+ * the daemon's pinned identity.
50
52
  */
51
53
  export declare function resetDeviceIdentity(opts?: {
52
54
  profile?: string;
@@ -100,6 +100,26 @@ function publicKeyPath(profile) {
100
100
  function privateKeyPath(profile) {
101
101
  return node_path_1.default.join(profileConfigDir(profile), PRIVATE_KEY_FILENAME);
102
102
  }
103
+ function storedKeyMaterialExists(profile) {
104
+ let files;
105
+ try {
106
+ files = [publicKeyPath(profile), privateKeyPath(profile), enrollmentMarkerPath(profile)];
107
+ }
108
+ catch {
109
+ return true;
110
+ }
111
+ for (const file of files) {
112
+ try {
113
+ node_fs_1.default.lstatSync(file);
114
+ return true;
115
+ }
116
+ catch (err) {
117
+ if (err.code !== 'ENOENT')
118
+ return true;
119
+ }
120
+ }
121
+ return false;
122
+ }
103
123
  function enrollmentMarkerPath(profile) {
104
124
  return node_path_1.default.join(profileConfigDir(profile), ENROLLMENT_MARKER_FILENAME);
105
125
  }
@@ -339,7 +359,7 @@ async function consolidateDeviceIdentity(profile) {
339
359
  * 2. Generate fresh keypair and persist. Failure to persist is a hard
340
360
  * error landing in disabled+persist_failed.
341
361
  */
342
- async function initDeviceIdentity(profile) {
362
+ async function initDeviceIdentity(profile, options = {}) {
343
363
  if (_state.kind !== 'uninit')
344
364
  return;
345
365
  if (typeof profile !== 'string' || profile.length === 0) {
@@ -374,6 +394,18 @@ async function initDeviceIdentity(profile) {
374
394
  });
375
395
  return;
376
396
  }
397
+ // A first install has neither file. If either file already exists, a null
398
+ // load is a lost, corrupt or temporarily unreadable identity. Replacing it
399
+ // during ordinary boot would make the next refresh sign with a key the
400
+ // server has never pinned, turning a local read failure into an unexpected
401
+ // DEVICE_KEY_MISMATCH. Only an explicit interactive login may replace it;
402
+ // that login proves the new key and re-pins it before issuing credentials.
403
+ const hasStoredKey = storedKeyMaterialExists(profile);
404
+ if (hasStoredKey && !options.recoverUnreadable) {
405
+ _state = { kind: 'disabled', reason: 'unreadable_key' };
406
+ emit('recovery_required', false, { profile, reason: 'stored_key_unreadable' });
407
+ return;
408
+ }
377
409
  let fresh;
378
410
  try {
379
411
  fresh = (0, deviceKey_js_1.generateDeviceKeyPair)();
@@ -413,9 +445,9 @@ async function initDeviceIdentity(profile) {
413
445
  });
414
446
  }
415
447
  /**
416
- * Reset state. With `clearOnDisk=true` deletes both keypair files —
417
- * used by `oversky logout` / `sky logout` so a freshly-logged-in user
418
- * gets a fresh device identity that the daemon will see on its next start.
448
+ * Reset in-memory state. With `clearOnDisk=true` deletes both keypair files.
449
+ * Machine-wide sign-out may use that option; ordinary CLI sign-out preserves
450
+ * the daemon's pinned identity.
419
451
  */
420
452
  async function resetDeviceIdentity(opts) {
421
453
  if (opts?.clearOnDisk && opts.profile) {
@@ -10,7 +10,7 @@ type DeviceIdentityState = {
10
10
  profile: string;
11
11
  envelopeProtected: boolean;
12
12
  };
13
- type DisabledReason = 'init_failed' | 'load_failed' | 'persist_failed';
13
+ type DisabledReason = 'init_failed' | 'load_failed' | 'persist_failed' | 'unreadable_key';
14
14
  /** @internal Test seam — force-set state. */
15
15
  export declare function __setStateForTest(next: DeviceIdentityState): DeviceIdentityState;
16
16
  /** @internal Test seam — read state. */
@@ -42,11 +42,13 @@ export declare function describeDeviceKeySplit(profile: string): {
42
42
  * 2. Generate fresh keypair and persist. Failure to persist is a hard
43
43
  * error landing in disabled+persist_failed.
44
44
  */
45
- export declare function initDeviceIdentity(profile: string): Promise<void>;
45
+ export declare function initDeviceIdentity(profile: string, options?: {
46
+ recoverUnreadable?: boolean;
47
+ }): Promise<void>;
46
48
  /**
47
- * Reset state. With `clearOnDisk=true` deletes both keypair files —
48
- * used by `oversky logout` / `sky logout` so a freshly-logged-in user
49
- * gets a fresh device identity that the daemon will see on its next start.
49
+ * Reset in-memory state. With `clearOnDisk=true` deletes both keypair files.
50
+ * Machine-wide sign-out may use that option; ordinary CLI sign-out preserves
51
+ * the daemon's pinned identity.
50
52
  */
51
53
  export declare function resetDeviceIdentity(opts?: {
52
54
  profile?: string;
@@ -80,6 +80,26 @@ function publicKeyPath(profile) {
80
80
  function privateKeyPath(profile) {
81
81
  return path.join(profileConfigDir(profile), PRIVATE_KEY_FILENAME);
82
82
  }
83
+ function storedKeyMaterialExists(profile) {
84
+ let files;
85
+ try {
86
+ files = [publicKeyPath(profile), privateKeyPath(profile), enrollmentMarkerPath(profile)];
87
+ }
88
+ catch {
89
+ return true;
90
+ }
91
+ for (const file of files) {
92
+ try {
93
+ fs.lstatSync(file);
94
+ return true;
95
+ }
96
+ catch (err) {
97
+ if (err.code !== 'ENOENT')
98
+ return true;
99
+ }
100
+ }
101
+ return false;
102
+ }
83
103
  function enrollmentMarkerPath(profile) {
84
104
  return path.join(profileConfigDir(profile), ENROLLMENT_MARKER_FILENAME);
85
105
  }
@@ -319,7 +339,7 @@ async function consolidateDeviceIdentity(profile) {
319
339
  * 2. Generate fresh keypair and persist. Failure to persist is a hard
320
340
  * error landing in disabled+persist_failed.
321
341
  */
322
- export async function initDeviceIdentity(profile) {
342
+ export async function initDeviceIdentity(profile, options = {}) {
323
343
  if (_state.kind !== 'uninit')
324
344
  return;
325
345
  if (typeof profile !== 'string' || profile.length === 0) {
@@ -354,6 +374,18 @@ export async function initDeviceIdentity(profile) {
354
374
  });
355
375
  return;
356
376
  }
377
+ // A first install has neither file. If either file already exists, a null
378
+ // load is a lost, corrupt or temporarily unreadable identity. Replacing it
379
+ // during ordinary boot would make the next refresh sign with a key the
380
+ // server has never pinned, turning a local read failure into an unexpected
381
+ // DEVICE_KEY_MISMATCH. Only an explicit interactive login may replace it;
382
+ // that login proves the new key and re-pins it before issuing credentials.
383
+ const hasStoredKey = storedKeyMaterialExists(profile);
384
+ if (hasStoredKey && !options.recoverUnreadable) {
385
+ _state = { kind: 'disabled', reason: 'unreadable_key' };
386
+ emit('recovery_required', false, { profile, reason: 'stored_key_unreadable' });
387
+ return;
388
+ }
357
389
  let fresh;
358
390
  try {
359
391
  fresh = generateDeviceKeyPair();
@@ -393,9 +425,9 @@ export async function initDeviceIdentity(profile) {
393
425
  });
394
426
  }
395
427
  /**
396
- * Reset state. With `clearOnDisk=true` deletes both keypair files —
397
- * used by `oversky logout` / `sky logout` so a freshly-logged-in user
398
- * gets a fresh device identity that the daemon will see on its next start.
428
+ * Reset in-memory state. With `clearOnDisk=true` deletes both keypair files.
429
+ * Machine-wide sign-out may use that option; ordinary CLI sign-out preserves
430
+ * the daemon's pinned identity.
399
431
  */
400
432
  export async function resetDeviceIdentity(opts) {
401
433
  if (opts?.clearOnDisk && opts.profile) {
@@ -44752,6 +44752,10 @@ var gR = Object.freeze({ __proto__: null, acceptAgentIntuition: function(e2, t2,
44752
44752
  return oR.get(e2 ? "".concat(vr(), "?workspaceId=").concat(encodeURIComponent(e2)) : vr());
44753
44753
  }, getStorePublisherFollowOverlay: function(e2) {
44754
44754
  return oR.get(xr(e2));
44755
+ }, getStorePublisherFollows: function(e2) {
44756
+ return oR.get((function(e3) {
44757
+ return "".concat(er(), "/publisher-follows").concat(e3 ? "?cursor=".concat(encodeURIComponent(e3)) : "");
44758
+ })(e2));
44755
44759
  }, getStorePublisherNamespaces: function(e2) {
44756
44760
  return oR.get(hr(e2));
44757
44761
  }, getStorePublisherProfileEditor: function(e2) {
@@ -44767,7 +44771,7 @@ var gR = Object.freeze({ __proto__: null, acceptAgentIntuition: function(e2, t2,
44767
44771
  }, getStoreReleaseAdmissions: function(e2) {
44768
44772
  return oR.get((function(e3) {
44769
44773
  var t2 = new URLSearchParams();
44770
- (null == e3 ? void 0 : e3.state) && t2.set("state", e3.state), (null == e3 ? void 0 : e3.limit) && t2.set("limit", String(e3.limit));
44774
+ (null == e3 ? void 0 : e3.state) && t2.set("state", e3.state), (null == e3 ? void 0 : e3.limit) && t2.set("limit", String(e3.limit)), (null == e3 ? void 0 : e3.artifactAfter) && t2.set("artifactAfter", e3.artifactAfter);
44771
44775
  var n2 = t2.toString();
44772
44776
  return "".concat(er(), "/admissions").concat(n2 ? "?".concat(n2) : "");
44773
44777
  })(e2));
@@ -46542,10 +46546,10 @@ var gR = Object.freeze({ __proto__: null, acceptAgentIntuition: function(e2, t2,
46542
46546
  }, setAgentUsageCap: function(e2, t2) {
46543
46547
  return oR.put(Nn(e2), t2);
46544
46548
  }, setAgentVisibility: function(e2) {
46545
- var t2 = e2.agentId, n2 = e2.isPublic, o2 = e2.price, r2 = e2.shareMode, a2 = e2.changelog, i2 = e2.versionMajor, s2 = e2.versionMinor, c2 = e2.capabilityRationales, l2 = e2.supportedLanguages, u2 = e2.license, d2 = e2.sourceVisibility, p2 = { isPublic: n2 };
46546
- return void 0 !== o2 && (p2.price = o2), void 0 !== r2 && (p2.shareMode = r2), void 0 !== a2 && (p2.changelog = a2), void 0 !== i2 && (p2.versionMajor = i2), void 0 !== s2 && (p2.versionMinor = s2), void 0 !== c2 && (p2.capabilityRationales = c2), void 0 !== l2 && (p2.supportedLanguages = l2), void 0 !== u2 && (p2.license = u2), void 0 !== d2 && (p2.sourceVisibility = d2), oR.post((function(e3) {
46549
+ var t2 = e2.agentId, n2 = e2.isPublic, o2 = e2.price, r2 = e2.shareMode, a2 = e2.changelog, i2 = e2.versionMajor, s2 = e2.versionMinor, c2 = e2.capabilityRationales, l2 = e2.supportedLanguages, u2 = e2.dependencies, d2 = e2.license, p2 = e2.sourceVisibility, m2 = { isPublic: n2 };
46550
+ return void 0 !== o2 && (m2.price = o2), void 0 !== r2 && (m2.shareMode = r2), void 0 !== a2 && (m2.changelog = a2), void 0 !== i2 && (m2.versionMajor = i2), void 0 !== s2 && (m2.versionMinor = s2), void 0 !== c2 && (m2.capabilityRationales = c2), void 0 !== l2 && (m2.supportedLanguages = l2), void 0 !== u2 && (m2.dependencies = u2), void 0 !== d2 && (m2.license = d2), void 0 !== p2 && (m2.sourceVisibility = p2), oR.post((function(e3) {
46547
46551
  return "".concat(en, "/api/agents/").concat(e3, "/visibility");
46548
- })(t2), p2);
46552
+ })(t2), m2);
46549
46553
  }, setAlwaysOnDesired: function(e2, t2) {
46550
46554
  return oR.put((n2 = e2, "".concat(en, "/api/agents/").concat(encodeURIComponent(n2), "/always-on")), t2);
46551
46555
  var n2;
@@ -46890,6 +46894,10 @@ var gR = Object.freeze({ __proto__: null, acceptAgentIntuition: function(e2, t2,
46890
46894
  return oR.patch(Aa(e2), { unarchive: true });
46891
46895
  }, unfollowStorePublisherByHandle: function(e2) {
46892
46896
  return oR.delete(xr(e2));
46897
+ }, unfollowStorePublisherById: function(e2) {
46898
+ return oR.delete((function(e3) {
46899
+ return "".concat(er(), "/publisher-follows/").concat(encodeURIComponent(e3));
46900
+ })(e2));
46893
46901
  }, uninstallStoreAgent: function(e2) {
46894
46902
  var t2 = e2.agentId, n2 = e2.erase;
46895
46903
  return oR.post((function(e3) {