@skrr-ai/cli 0.1.44 → 0.1.46

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 (73) hide show
  1. package/dist/base-command.d.ts +13 -1
  2. package/dist/base-command.js +37 -3
  3. package/dist/commands/agents/actions/create.js +11 -4
  4. package/dist/commands/agents/chat.js +3 -4
  5. package/dist/commands/browser/cloud-agents.d.ts +20 -0
  6. package/dist/commands/browser/cloud-agents.js +43 -0
  7. package/dist/commands/code/handover.d.ts +11 -0
  8. package/dist/commands/code/handover.js +105 -1
  9. package/dist/commands/commitments/handover.d.ts +29 -0
  10. package/dist/commands/commitments/handover.js +99 -0
  11. package/dist/commands/followups/list.js +1 -1
  12. package/dist/commands/followups/watch.js +40 -5
  13. package/dist/commands/initiatives/list.d.ts +1 -0
  14. package/dist/commands/initiatives/list.js +11 -2
  15. package/dist/commands/labels/list.d.ts +28 -0
  16. package/dist/commands/labels/list.js +50 -32
  17. package/dist/commands/tasks/fork.d.ts +32 -0
  18. package/dist/commands/tasks/fork.js +171 -0
  19. package/dist/commands/tasks/handover.d.ts +23 -0
  20. package/dist/commands/tasks/handover.js +205 -0
  21. package/dist/commands/tasks/promote.d.ts +23 -0
  22. package/dist/commands/tasks/promote.js +82 -0
  23. package/dist/commands/tasks/runs.d.ts +16 -0
  24. package/dist/commands/tasks/runs.js +46 -1
  25. package/dist/commands/tasks/show.d.ts +23 -0
  26. package/dist/commands/tasks/show.js +59 -0
  27. package/dist/lib/agentic-stream.js +41 -2
  28. package/dist/lib/api-fetch.d.ts +7 -1
  29. package/dist/lib/api-fetch.js +9 -2
  30. package/dist/lib/auth-core-init.js +11 -4
  31. package/dist/lib/code-handover.d.ts +60 -0
  32. package/dist/lib/code-handover.js +120 -0
  33. package/dist/lib/commitments.d.ts +22 -0
  34. package/dist/lib/commitments.js +42 -0
  35. package/dist/lib/dedicated-wait.js +22 -1
  36. package/dist/lib/followups.d.ts +18 -0
  37. package/dist/lib/followups.js +44 -0
  38. package/dist/lib/initiatives.d.ts +15 -3
  39. package/dist/lib/label-ref.d.ts +2 -0
  40. package/dist/lib/node-adapter.js +6 -0
  41. package/dist/lib/task-extras.d.ts +44 -0
  42. package/dist/lib/task-extras.js +73 -0
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +29 -0
  44. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +169 -23
  45. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +91 -5
  46. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +8 -1
  47. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +7 -0
  48. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.js +45 -0
  49. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.d.ts +65 -0
  50. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.js +99 -0
  51. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
  52. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +26 -2
  53. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loginLocalhost.js +177 -26
  54. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.d.ts +119 -0
  55. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.js +346 -0
  56. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +29 -0
  57. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +169 -24
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +93 -7
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +8 -1
  60. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +7 -0
  61. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.js +45 -0
  62. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.d.ts +65 -0
  63. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.js +94 -0
  64. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
  65. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loginLocalhost.js +177 -26
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.d.ts +119 -0
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.js +333 -0
  69. package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
  70. package/dist/node_modules/@skrr-ai/data-provider/index.js +2746 -2711
  71. package/dist/node_modules/@skrr-ai/inference-broker/package.json +1 -1
  72. package/oclif.manifest.json +34300 -33843
  73. package/package.json +2 -2
@@ -0,0 +1,346 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SUPERSEDED_COPY_SUFFIX = exports.SUPERSEDED_MARKER_SUFFIX = exports.PROFILES_SEGMENT = void 0;
7
+ exports.profileStateDir = profileStateDir;
8
+ exports.legacyProfileStateDir = legacyProfileStateDir;
9
+ exports.resolveProfileStateDir = resolveProfileStateDir;
10
+ exports.resolveProfileStateFile = resolveProfileStateFile;
11
+ exports.profileStateFileLocations = profileStateFileLocations;
12
+ exports.consolidateProfileState = consolidateProfileState;
13
+ exports.profileStateFilePathsToClear = profileStateFilePathsToClear;
14
+ /**
15
+ * profileStateDir.ts — the ONE definition of where a profile's auth state lives
16
+ * on disk, and the migration for a machine that ended up with two.
17
+ *
18
+ * A profile is this product's unit of credential isolation, and its state has
19
+ * exactly one home:
20
+ *
21
+ * <machine root>/profiles/<profile>/cred-dek-wrapped
22
+ * <machine root>/profiles/<profile>/cred-envelope.lock
23
+ * <machine root>/profiles/<profile>/device-private-key
24
+ * …
25
+ *
26
+ * It lives here, next to `configRoot.ts`, for the same reason that file exists:
27
+ * both binaries need the same answer and neither owns it. `configRoot.ts` made
28
+ * "where is the skrr root" one function; this makes "where is a profile's auth
29
+ * state" one function, because the layer above it had the identical defect one
30
+ * level down.
31
+ *
32
+ * WHAT WENT WRONG (OSK-10314). `credentialEnvelopeBridge` and
33
+ * `deviceIdentityBridge` each wrote `path.join(getAuthConfigDir(), 'profiles',
34
+ * profile)` — two independent spellings of one rule — and `configDir` means
35
+ * different things to the two consumers:
36
+ *
37
+ * - the CLI passes the MACHINE ROOT, so it landed on the layout above;
38
+ * - the daemon passes `getConfigDir()`, which is ALREADY `<root>/profiles/<p>`,
39
+ * so it landed on `<root>/profiles/<p>/profiles/<p>/…`.
40
+ *
41
+ * One profile therefore had two wrapped DEKs, two device keypairs and two
42
+ * lockfiles, and the lockfile the shared module exists to share served neither
43
+ * of the two processes it was meant to serialize. Nothing cross-read, so
44
+ * nothing broke — it was a split-brain waiting for the first cross-read.
45
+ *
46
+ * The fix is to derive the directory from `getAuthMachineConfigDir()`, which
47
+ * BOTH consumers already wire explicitly (daemon: `getMachineConfigDir()`; CLI:
48
+ * the skrr root). Note what is deliberately NOT done: the daemon is not asked to
49
+ * pass a different `configDir`. It has one knob and auth-core needs two answers
50
+ * from it — `refresh.ts` reads `getAuthConfigDir()` raw for `auth.lock` and
51
+ * `needs-reauth.json`, so repointing the daemon's `configDir` at the machine
52
+ * root would quietly move its reauth latch from per-profile to machine-wide and
53
+ * let one profile latch every profile. Profile-scoped and root-scoped artifacts
54
+ * are only distinguishable INSIDE auth-core, so that is where the line is drawn.
55
+ *
56
+ * WHY A BARE PATH SWAP IS NOT THE FIX. A daemon with a nested wrapped DEK has
57
+ * sealed real credentials under it. Pointing it at the canonical path hands it a
58
+ * DIFFERENT DEK and every one of those credentials stops decrypting — a silent,
59
+ * fleet-wide forced re-login, to repair a bug that had so far harmed nobody. So
60
+ * the resolver PREFERS THE FILE A PROCESS IS ALREADY USING until a migration has
61
+ * proved the canonical one is equivalent, and the migration COPIES.
62
+ *
63
+ * Resolution order for `<filename>` under `<profile>` (stateless — no ordering
64
+ * requirement between this and any init):
65
+ *
66
+ * 1. legacy dir IS the canonical dir (the CLI) → canonical
67
+ * 2. no legacy copy of the file → canonical
68
+ * 3. a `<filename>.superseded.json` marker beside it → canonical (consolidated)
69
+ * 4. no canonical copy of the file → legacy (not yet consolidated)
70
+ * 5. both copies, no marker → legacy (unresolved: stay put)
71
+ *
72
+ * The migration writes the marker, never deletes, and refuses case 5 when both
73
+ * sides hold a usable and DIFFERENT key — because merging those would require
74
+ * re-sealing content that lives in the OS keychain and is not auth-core's to
75
+ * re-seal. A human resolves that one, and the loud signal is the deliverable.
76
+ *
77
+ * ROLLBACK. The legacy file is left exactly where it was, under its own name.
78
+ * Older code looks there, finds it, and works — no operator step. The marker is
79
+ * a sidecar older code never reads.
80
+ */
81
+ const node_crypto_1 = require("node:crypto");
82
+ const node_fs_1 = __importDefault(require("node:fs"));
83
+ const node_path_1 = __importDefault(require("node:path"));
84
+ const runtime_js_1 = require("./runtime.js");
85
+ /** Directory segment that separates per-profile state from machine-scoped state. */
86
+ exports.PROFILES_SEGMENT = 'profiles';
87
+ /**
88
+ * Sidecar written beside a legacy file once it has been consolidated. Its
89
+ * PRESENCE is what flips resolution to the canonical copy; its contents are for
90
+ * a human reading the directory afterwards.
91
+ */
92
+ exports.SUPERSEDED_MARKER_SUFFIX = '.superseded.json';
93
+ /**
94
+ * Suffix for a canonical file moved aside to make room. Only used when the
95
+ * canonical copy existed and was NOT usable — a usable one is never replaced.
96
+ */
97
+ exports.SUPERSEDED_COPY_SUFFIX = '.superseded';
98
+ /** The canonical directory for a profile's auth state. */
99
+ function profileStateDir(profile) {
100
+ assertProfile(profile);
101
+ return node_path_1.default.join((0, runtime_js_1.getAuthMachineConfigDir)(), exports.PROFILES_SEGMENT, profile);
102
+ }
103
+ /**
104
+ * Where a consumer whose `configDir` is already profile-scoped used to write.
105
+ *
106
+ * `null` when it resolves to the canonical directory — i.e. the consumer passes
107
+ * the machine root and never had a second location. Callers branch on `null`
108
+ * rather than comparing strings so the "one root" case costs one check.
109
+ */
110
+ function legacyProfileStateDir(profile) {
111
+ assertProfile(profile);
112
+ const legacy = node_path_1.default.join((0, runtime_js_1.getAuthConfigDir)(), exports.PROFILES_SEGMENT, profile);
113
+ const canonical = profileStateDir(profile);
114
+ return node_path_1.default.resolve(legacy) === node_path_1.default.resolve(canonical) ? null : legacy;
115
+ }
116
+ /**
117
+ * Resolve the directory holding `primaryFilename` for `profile`.
118
+ *
119
+ * `primaryFilename` is the file whose presence decides where a group of related
120
+ * files lives. Device identity has four files that must be read from ONE
121
+ * directory — a public JWK from one copy and a private key from the other is a
122
+ * keypair mismatch — so the group anchors on the private key rather than each
123
+ * file answering for itself.
124
+ */
125
+ function resolveProfileStateDir(profile, primaryFilename) {
126
+ const canonicalDir = profileStateDir(profile);
127
+ const legacyDir = legacyProfileStateDir(profile);
128
+ if (legacyDir === null) {
129
+ return { dir: canonicalDir, canonicalDir, legacyDir: null, usingLegacy: false };
130
+ }
131
+ const legacyFile = node_path_1.default.join(legacyDir, primaryFilename);
132
+ const stayOnLegacy = existsFile(legacyFile) && !existsFile(`${legacyFile}${exports.SUPERSEDED_MARKER_SUFFIX}`);
133
+ return {
134
+ dir: stayOnLegacy ? legacyDir : canonicalDir,
135
+ canonicalDir,
136
+ legacyDir,
137
+ usingLegacy: stayOnLegacy,
138
+ };
139
+ }
140
+ /** `resolveProfileStateDir` for a single self-anchoring file. */
141
+ function resolveProfileStateFile(profile, filename) {
142
+ const resolved = resolveProfileStateDir(profile, filename);
143
+ return {
144
+ ...resolved,
145
+ path: node_path_1.default.join(resolved.dir, filename),
146
+ canonicalPath: node_path_1.default.join(resolved.canonicalDir, filename),
147
+ legacyPath: resolved.legacyDir === null ? null : node_path_1.default.join(resolved.legacyDir, filename),
148
+ };
149
+ }
150
+ /** Every location `filename` may occupy for `profile`, canonical first. */
151
+ function profileStateFileLocations(profile, filename) {
152
+ const canonicalDir = profileStateDir(profile);
153
+ const legacyDir = legacyProfileStateDir(profile);
154
+ const out = [node_path_1.default.join(canonicalDir, filename)];
155
+ if (legacyDir !== null)
156
+ out.push(node_path_1.default.join(legacyDir, filename));
157
+ return out;
158
+ }
159
+ /**
160
+ * Consolidate a profile's state onto the canonical path. Idempotent, additive,
161
+ * and refuses rather than guesses.
162
+ *
163
+ * The one case it will not decide is two usable, different keys for one profile:
164
+ * each side has sealed content under its own, that content lives in the OS
165
+ * keychain, and nothing here can re-seal it. Picking a winner there would
166
+ * destroy the loser's credentials silently, which is the exact failure this
167
+ * whole module exists to avoid.
168
+ */
169
+ async function consolidateProfileState(profile, spec) {
170
+ assertProfile(profile);
171
+ const dryRun = spec.dryRun === true;
172
+ const canonicalDir = profileStateDir(profile);
173
+ const legacyDir = legacyProfileStateDir(profile);
174
+ const base = { profile, canonicalDir, legacyDir, written: [], supersededAside: [], dryRun };
175
+ if (legacyDir === null)
176
+ return { ...base, status: 'not_applicable' };
177
+ const legacyPrimary = node_path_1.default.join(legacyDir, spec.primary);
178
+ if (!existsFile(legacyPrimary))
179
+ return { ...base, status: 'nothing_to_migrate' };
180
+ const markerPath = `${legacyPrimary}${exports.SUPERSEDED_MARKER_SUFFIX}`;
181
+ if (existsFile(markerPath))
182
+ return { ...base, status: 'already_consolidated' };
183
+ const canonicalPrimary = node_path_1.default.join(canonicalDir, spec.primary);
184
+ if (!existsFile(canonicalPrimary)) {
185
+ const moved = adopt(legacyDir, canonicalDir, spec.files, dryRun);
186
+ if (!dryRun)
187
+ writeMarker(markerPath, canonicalPrimary, 'adopted');
188
+ emitOutcome(spec, 'adopted', { profile, from: legacyDir, to: canonicalDir });
189
+ return { ...base, status: 'adopted', ...moved };
190
+ }
191
+ const identify = spec.identify ?? ((dir) => sha256OfFile(node_path_1.default.join(dir, spec.primary)));
192
+ const legacyIdentity = await identify(legacyDir);
193
+ const canonicalIdentity = await identify(canonicalDir);
194
+ const identities = { canonical: canonicalIdentity, legacy: legacyIdentity };
195
+ // Same state in both places, or the legacy copy is unreadable while the
196
+ // canonical one is fine. Either way the canonical copy is authoritative and
197
+ // the legacy one has nothing to contribute — record that and move on.
198
+ const sameState = legacyIdentity !== null && canonicalIdentity !== null && legacyIdentity === canonicalIdentity;
199
+ const legacyHasNothingToOffer = legacyIdentity === null && canonicalIdentity !== null;
200
+ const bothUnreadableButIdentical = legacyIdentity === null &&
201
+ canonicalIdentity === null &&
202
+ sha256OfFile(legacyPrimary) !== null &&
203
+ sha256OfFile(legacyPrimary) === sha256OfFile(canonicalPrimary);
204
+ if (sameState || legacyHasNothingToOffer || bothUnreadableButIdentical) {
205
+ if (!dryRun)
206
+ writeMarker(markerPath, canonicalPrimary, 'already_consolidated');
207
+ emitOutcome(spec, 'already_consolidated', { profile, identities });
208
+ return { ...base, status: 'already_consolidated', identities };
209
+ }
210
+ // The legacy copy is usable and the canonical one is not. Nothing is sealed
211
+ // under an unreadable key that was not already lost, so replacing it costs
212
+ // nothing — but keep it, because "unreadable here, today" is not "worthless".
213
+ if (legacyIdentity !== null && canonicalIdentity === null) {
214
+ const aside = dryRun
215
+ ? plannedAside(canonicalDir, spec.files)
216
+ : moveAside(canonicalDir, spec.files);
217
+ const moved = adopt(legacyDir, canonicalDir, spec.files, dryRun);
218
+ if (!dryRun)
219
+ writeMarker(markerPath, canonicalPrimary, 'superseded');
220
+ emitOutcome(spec, 'superseded', { profile, identities, supersededAside: aside });
221
+ return {
222
+ ...base,
223
+ status: 'superseded',
224
+ written: moved.written,
225
+ supersededAside: aside,
226
+ identities,
227
+ };
228
+ }
229
+ emitOutcome(spec, 'conflict', { profile, identities, canonicalDir, legacyDir });
230
+ return { ...base, status: 'conflict', identities };
231
+ }
232
+ /**
233
+ * Every path a profile's copy of `filename` can occupy, plus its marker —
234
+ * what a genuine logout must remove. Deliberately separate from the migration:
235
+ * this module never deletes, and logout is the one caller that must.
236
+ */
237
+ function profileStateFilePathsToClear(profile, filename) {
238
+ const out = [];
239
+ for (const location of profileStateFileLocations(profile, filename)) {
240
+ out.push(location);
241
+ out.push(`${location}${exports.SUPERSEDED_MARKER_SUFFIX}`);
242
+ }
243
+ return out;
244
+ }
245
+ // ---------------------------------------------------------------------------
246
+ // internals
247
+ // ---------------------------------------------------------------------------
248
+ function assertProfile(profile) {
249
+ if (typeof profile !== 'string' || profile.length === 0) {
250
+ throw new Error('profileStateDir: profile must be a non-empty string');
251
+ }
252
+ }
253
+ function existsFile(target) {
254
+ try {
255
+ return node_fs_1.default.statSync(target).isFile();
256
+ }
257
+ catch {
258
+ return false;
259
+ }
260
+ }
261
+ function sha256OfFile(target) {
262
+ try {
263
+ return (0, node_crypto_1.createHash)('sha256').update(node_fs_1.default.readFileSync(target)).digest('hex');
264
+ }
265
+ catch {
266
+ return null;
267
+ }
268
+ }
269
+ /**
270
+ * tmp + fsync + rename + dir-fsync, 0600. The same shape the two bridges
271
+ * already use, so a crash mid-migration leaves either the old file or the new
272
+ * one and never a half-written key.
273
+ */
274
+ function atomicWrite0600(target, value) {
275
+ const dir = node_path_1.default.dirname(target);
276
+ node_fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
277
+ const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
278
+ const fd = node_fs_1.default.openSync(tmp, 'w', 0o600);
279
+ try {
280
+ node_fs_1.default.writeSync(fd, value);
281
+ node_fs_1.default.fsyncSync(fd);
282
+ }
283
+ finally {
284
+ node_fs_1.default.closeSync(fd);
285
+ }
286
+ node_fs_1.default.renameSync(tmp, target);
287
+ try {
288
+ const dirFd = node_fs_1.default.openSync(dir, 'r');
289
+ try {
290
+ node_fs_1.default.fsyncSync(dirFd);
291
+ }
292
+ finally {
293
+ node_fs_1.default.closeSync(dirFd);
294
+ }
295
+ }
296
+ catch {
297
+ /* best-effort — Windows / some BSDs reject dir fsync */
298
+ }
299
+ }
300
+ function adopt(fromDir, toDir, files, dryRun) {
301
+ const written = [];
302
+ for (const name of files) {
303
+ const source = node_path_1.default.join(fromDir, name);
304
+ if (!existsFile(source))
305
+ continue;
306
+ const target = node_path_1.default.join(toDir, name);
307
+ if (!dryRun)
308
+ atomicWrite0600(target, node_fs_1.default.readFileSync(source));
309
+ written.push(target);
310
+ }
311
+ return { written, supersededAside: [] };
312
+ }
313
+ function moveAside(dir, files) {
314
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
315
+ const aside = [];
316
+ for (const name of files) {
317
+ const target = node_path_1.default.join(dir, name);
318
+ if (!existsFile(target))
319
+ continue;
320
+ node_fs_1.default.renameSync(target, `${target}${exports.SUPERSEDED_COPY_SUFFIX}.${stamp}`);
321
+ aside.push(target);
322
+ }
323
+ return aside;
324
+ }
325
+ function plannedAside(dir, files) {
326
+ return files.map((name) => node_path_1.default.join(dir, name)).filter((target) => existsFile(target));
327
+ }
328
+ function writeMarker(markerPath, supersededBy, reason) {
329
+ const payload = {
330
+ supersededBy,
331
+ reason,
332
+ sha256: sha256OfFile(supersededBy),
333
+ at: new Date().toISOString(),
334
+ note: 'Kept deliberately. This file is the pre-consolidation copy (OSK-10314); ' +
335
+ 'older binaries read it and it is the only rollback that needs no operator step. ' +
336
+ 'Remove it only through logout.',
337
+ };
338
+ atomicWrite0600(markerPath, Buffer.from(`${JSON.stringify(payload, null, 2)}\n`, 'utf-8'));
339
+ }
340
+ function emitOutcome(spec, status, meta) {
341
+ spec.onEvent?.(`path_consolidation.${status}`, status !== 'conflict', {
342
+ ...meta,
343
+ primary: spec.primary,
344
+ ...(spec.dryRun === true ? { dryRun: true } : {}),
345
+ });
346
+ }
@@ -36,6 +36,14 @@ export declare function initCredEnvelope(profile: string): Promise<void>;
36
36
  * Reset state. With `clearOnDisk` deletes the wrapped DEK file too —
37
37
  * used by `oversky logout` / `sky logout`. Zeroes the in-memory DEK
38
38
  * before dropping state for forward secrecy on the active credential.
39
+ *
40
+ * It deletes EVERY location this profile's key can occupy, plus the
41
+ * consolidation marker. `profileStateDir.ts` never deletes anything and says so;
42
+ * this is the one caller that must, and the reason is not symmetry — a logout
43
+ * that removed only the canonical copy would leave the pre-consolidation copy
44
+ * behind, and the next init would adopt it straight back. That is a deleted
45
+ * credential key resurrecting itself, which is a worse bug than the one this
46
+ * whole change is fixing.
39
47
  */
40
48
  export declare function resetCredEnvelope(opts?: {
41
49
  profile?: string;
@@ -49,6 +57,17 @@ export declare function __resetShutdownGuardForTest(): void;
49
57
  * predicate used by sync transforms; never throws.
50
58
  */
51
59
  export declare function isCredEnvelopeActive(): boolean;
60
+ /**
61
+ * True while this profile holds two usable and different wrapped DEKs.
62
+ *
63
+ * Read by `deviceIdentityBridge` before it migrates anything of its own. The
64
+ * device private key is sealed with whichever DEK is active, so moving it to the
65
+ * canonical directory while the two binaries are still on DIFFERENT DEKs hands
66
+ * the other one a key it cannot open — it regenerates, overwrites, and the two
67
+ * processes then destroy each other's device identity on every start. Unresolved
68
+ * conflicts freeze the whole profile's layout, not just the DEK's.
69
+ */
70
+ export declare function hasCredEnvelopePathConflict(): boolean;
52
71
  /**
53
72
  * Operator-readable summary of envelope state. Used by `oversky status`
54
73
  * and CLI debug commands. Never includes the DEK or any key material.
@@ -65,6 +84,16 @@ export declare function describeCredEnvelopeState(): {
65
84
  kekKind?: string | null;
66
85
  profile?: string;
67
86
  kekRotated?: boolean;
87
+ /**
88
+ * Set when this profile holds two usable and DIFFERENT wrapped DEKs and the
89
+ * consolidation refused to pick one (OSK-10314). The envelope is fully
90
+ * operational — this process reads the key it has always read — but the other
91
+ * binary is holding a different one and cannot read what this one seals.
92
+ */
93
+ pathConflict?: {
94
+ canonicalPath: string;
95
+ legacyPath: string;
96
+ };
68
97
  };
69
98
  /**
70
99
  * Sync write transform. Returns the wrapped serialized form when active.