@claudexor/cli 3.5.0 → 3.8.0
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.
- package/dist/accounts-projection.d.ts +22 -14
- package/dist/accounts-projection.d.ts.map +1 -1
- package/dist/accounts-projection.js +112 -60
- package/dist/accounts-projection.js.map +1 -1
- package/dist/accounts-services.d.ts +140 -128
- package/dist/accounts-services.d.ts.map +1 -1
- package/dist/accounts-services.js +22 -9
- package/dist/accounts-services.js.map +1 -1
- package/dist/accounts-unified-migration.d.ts +110 -0
- package/dist/accounts-unified-migration.d.ts.map +1 -0
- package/dist/accounts-unified-migration.js +422 -0
- package/dist/accounts-unified-migration.js.map +1 -0
- package/dist/agy-quota-source.d.ts +1 -0
- package/dist/agy-quota-source.d.ts.map +1 -1
- package/dist/agy-quota-source.js +61 -108
- package/dist/agy-quota-source.js.map +1 -1
- package/dist/auth-login-harnesses.d.ts +7 -5
- package/dist/auth-login-harnesses.d.ts.map +1 -1
- package/dist/auth-login-harnesses.js +10 -9
- package/dist/auth-login-harnesses.js.map +1 -1
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +6 -1
- package/dist/capabilities.js.map +1 -1
- package/dist/claude-oauth-usage.d.ts.map +1 -1
- package/dist/claude-oauth-usage.js +9 -3
- package/dist/claude-oauth-usage.js.map +1 -1
- package/dist/claude-statusline.d.ts.map +1 -1
- package/dist/claude-statusline.js +14 -1
- package/dist/claude-statusline.js.map +1 -1
- package/dist/claudexord-entry.d.ts +7 -2
- package/dist/claudexord-entry.d.ts.map +1 -1
- package/dist/claudexord-entry.js +33 -11
- package/dist/claudexord-entry.js.map +1 -1
- package/dist/claudexord.d.ts.map +1 -1
- package/dist/claudexord.js +25 -37
- package/dist/claudexord.js.map +1 -1
- package/dist/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/dist/codex-quota-source.d.ts.map +1 -1
- package/dist/codex-quota-source.js +8 -3
- package/dist/codex-quota-source.js.map +1 -1
- package/dist/command-flags.d.ts.map +1 -1
- package/dist/command-flags.js +1 -0
- package/dist/command-flags.js.map +1 -1
- package/dist/control-services.d.ts +206 -188
- package/dist/control-services.d.ts.map +1 -1
- package/dist/control-services.js +54 -122
- package/dist/control-services.js.map +1 -1
- package/dist/credential-commands.d.ts +16 -0
- package/dist/credential-commands.d.ts.map +1 -1
- package/dist/credential-commands.js +93 -39
- package/dist/credential-commands.js.map +1 -1
- package/dist/credential-profile-mutations.d.ts +70 -0
- package/dist/credential-profile-mutations.d.ts.map +1 -0
- package/dist/credential-profile-mutations.js +219 -0
- package/dist/credential-profile-mutations.js.map +1 -0
- package/dist/credential-status-invalidation.d.ts +11 -0
- package/dist/credential-status-invalidation.d.ts.map +1 -0
- package/dist/credential-status-invalidation.js +36 -0
- package/dist/credential-status-invalidation.js.map +1 -0
- package/dist/daemon-admission-runtime.d.ts +5 -0
- package/dist/daemon-admission-runtime.d.ts.map +1 -1
- package/dist/daemon-admission-runtime.js +1 -0
- package/dist/daemon-admission-runtime.js.map +1 -1
- package/dist/delegation-belt-descriptor.d.ts +2 -1
- package/dist/delegation-belt-descriptor.d.ts.map +1 -1
- package/dist/delegation-belt-descriptor.js +2 -1
- package/dist/delegation-belt-descriptor.js.map +1 -1
- package/dist/harness-command-specs.d.ts.map +1 -1
- package/dist/harness-command-specs.js +4 -4
- package/dist/harness-command-specs.js.map +1 -1
- package/dist/harness-command.d.ts +5 -3
- package/dist/harness-command.d.ts.map +1 -1
- package/dist/harness-command.js +5 -3
- package/dist/harness-command.js.map +1 -1
- package/dist/harness-install-lease.d.ts +11 -0
- package/dist/harness-install-lease.d.ts.map +1 -0
- package/dist/harness-install-lease.js +98 -0
- package/dist/harness-install-lease.js.map +1 -0
- package/dist/harness-install-proof.d.ts +36 -0
- package/dist/harness-install-proof.d.ts.map +1 -0
- package/dist/harness-install-proof.js +148 -0
- package/dist/harness-install-proof.js.map +1 -0
- package/dist/harness-install-recipes.d.ts +80 -0
- package/dist/harness-install-recipes.d.ts.map +1 -0
- package/dist/harness-install-recipes.js +130 -0
- package/dist/harness-install-recipes.js.map +1 -0
- package/dist/harness-installer.d.ts +45 -46
- package/dist/harness-installer.d.ts.map +1 -1
- package/dist/harness-installer.js +248 -156
- package/dist/harness-installer.js.map +1 -1
- package/dist/mcp-run-projections.d.ts +1 -1
- package/dist/native-login.d.ts +7 -0
- package/dist/native-login.d.ts.map +1 -1
- package/dist/native-login.js +40 -17
- package/dist/native-login.js.map +1 -1
- package/dist/profile-registration.d.ts +15 -1
- package/dist/profile-registration.d.ts.map +1 -1
- package/dist/profile-registration.js +124 -7
- package/dist/profile-registration.js.map +1 -1
- package/dist/quota-services.d.ts +4 -4
- package/dist/quota-subject-universe.d.ts.map +1 -1
- package/dist/quota-subject-universe.js +6 -0
- package/dist/quota-subject-universe.js.map +1 -1
- package/dist/remote-command.d.ts +0 -2
- package/dist/remote-command.d.ts.map +1 -1
- package/dist/remote-command.js +3 -74
- package/dist/remote-command.js.map +1 -1
- package/dist/run-orchestrator.d.ts +28 -0
- package/dist/run-orchestrator.d.ts.map +1 -0
- package/dist/run-orchestrator.js +43 -0
- package/dist/run-orchestrator.js.map +1 -0
- package/dist/settings-service.d.ts +1 -1
- package/dist/settings-service.js +3 -3
- package/dist/settings-service.js.map +1 -1
- package/dist/setup-attach-command.d.ts +30 -0
- package/dist/setup-attach-command.d.ts.map +1 -0
- package/dist/setup-attach-command.js +118 -0
- package/dist/setup-attach-command.js.map +1 -0
- package/dist/setup-job-store.d.ts +2 -2
- package/dist/setup-job-support.d.ts +36 -9
- package/dist/setup-job-support.d.ts.map +1 -1
- package/dist/setup-job-support.js +81 -10
- package/dist/setup-job-support.js.map +1 -1
- package/dist/setup-jobs.d.ts +4 -4
- package/dist/setup-jobs.d.ts.map +1 -1
- package/dist/setup-jobs.js +50 -47
- package/dist/setup-jobs.js.map +1 -1
- package/dist/setup-login-capability.d.ts +37 -0
- package/dist/setup-login-capability.d.ts.map +1 -0
- package/dist/setup-login-capability.js +96 -0
- package/dist/setup-login-capability.js.map +1 -0
- package/dist/setup-login-io.d.ts +4 -1
- package/dist/setup-login-io.d.ts.map +1 -1
- package/dist/setup-login-io.js +22 -3
- package/dist/setup-login-io.js.map +1 -1
- package/dist/setup-login-pty.d.ts +64 -22
- package/dist/setup-login-pty.d.ts.map +1 -1
- package/dist/setup-login-pty.js +289 -48
- package/dist/setup-login-pty.js.map +1 -1
- package/dist/setup-login-runner-support.d.ts +12 -0
- package/dist/setup-login-runner-support.d.ts.map +1 -0
- package/dist/setup-login-runner-support.js +65 -0
- package/dist/setup-login-runner-support.js.map +1 -0
- package/dist/setup-login-runner.d.ts +2 -0
- package/dist/setup-login-runner.d.ts.map +1 -1
- package/dist/setup-login-runner.js +119 -116
- package/dist/setup-login-runner.js.map +1 -1
- package/dist/setup-runner-outcome.d.ts +10 -0
- package/dist/setup-runner-outcome.d.ts.map +1 -0
- package/dist/setup-runner-outcome.js +49 -0
- package/dist/setup-runner-outcome.js.map +1 -0
- package/dist/thread-continuity-context.d.ts +24 -0
- package/dist/thread-continuity-context.d.ts.map +1 -1
- package/dist/thread-continuity-context.js +17 -0
- package/dist/thread-continuity-context.js.map +1 -1
- package/package.json +24 -24
|
@@ -6,12 +6,14 @@ import { noProjectRepoRoot } from "@claudexor/util";
|
|
|
6
6
|
import { validateModel } from "@claudexor/core";
|
|
7
7
|
import { withQuotaAvailability } from "@claudexor/schema";
|
|
8
8
|
import { vendorVerifiedProfileStatus } from "@claudexor/orchestrator";
|
|
9
|
-
import {
|
|
10
|
-
import { buildGateway, harnessModels } from "./registry.js";
|
|
9
|
+
import { accountPoolsProjection, profileAccountProjection } from "./accounts-projection.js";
|
|
10
|
+
import { buildGateway, buildRegistry, harnessModels } from "./registry.js";
|
|
11
11
|
import { delegationCapabilityFor } from "./delegation-capability.js";
|
|
12
|
+
import { effectiveSetupLoginCapability } from "./setup-login-capability.js";
|
|
12
13
|
const NO_PROJECT_ROOT = noProjectRepoRoot();
|
|
13
14
|
export async function projectHarnessStatuses(statuses) {
|
|
14
15
|
const cfg = loadConfig(NO_PROJECT_ROOT);
|
|
16
|
+
const adapters = buildRegistry({ includeFakes: false });
|
|
15
17
|
return Promise.all(statuses.map(async (status) => {
|
|
16
18
|
const configured = cfg.global.harnesses[status.id]?.default_model ?? null;
|
|
17
19
|
let check = null;
|
|
@@ -24,6 +26,9 @@ export async function projectHarnessStatuses(statuses) {
|
|
|
24
26
|
configuredModel: configured,
|
|
25
27
|
configuredModelCheck: check,
|
|
26
28
|
delegation: delegationCapabilityFor(status.manifest),
|
|
29
|
+
setupLogin: await effectiveSetupLoginCapability(status.id, {
|
|
30
|
+
getAdapter: (id) => adapters.get(id),
|
|
31
|
+
}),
|
|
27
32
|
readiness: normalizeReadiness({
|
|
28
33
|
checks: status.checks,
|
|
29
34
|
authSources: status.authSources,
|
|
@@ -35,11 +40,13 @@ export async function projectHarnessStatuses(statuses) {
|
|
|
35
40
|
}
|
|
36
41
|
/** One server-owned Accounts response builder. The opt-in form refreshes quota
|
|
37
42
|
* first and then derives next_up from that exact returned response; no client
|
|
38
|
-
* can accidentally pair a newer quota card with an older routing identity.
|
|
43
|
+
* can accidentally pair a newer quota card with an older routing identity.
|
|
44
|
+
* Returns the listing service plus the pool-authority read
|
|
45
|
+
* (`GET /v2/account-pools`) so both share one cached projection. */
|
|
39
46
|
export function createCredentialProfilesService(quotaRegistry) {
|
|
40
47
|
const projectProfiles = () => {
|
|
41
48
|
const profiles = loadConfig(NO_PROJECT_ROOT).global.credential_profiles;
|
|
42
|
-
return Promise.all(profiles.map(profileAccountProjection));
|
|
49
|
+
return Promise.all(profiles.map((profile) => profileAccountProjection(profile, profiles)));
|
|
43
50
|
};
|
|
44
51
|
// The plain (non-snapshot) form is the UI's poll target: without a cache it
|
|
45
52
|
// ran a doctor probe per registered profile plus a full harness sweep
|
|
@@ -53,7 +60,10 @@ export function createCredentialProfilesService(quotaRegistry) {
|
|
|
53
60
|
const out = withVendorVerification(await projectProfiles(), quota);
|
|
54
61
|
return {
|
|
55
62
|
profiles: out,
|
|
56
|
-
|
|
63
|
+
// Unified account model: the legacy carrier stays PRESENT and empty for
|
|
64
|
+
// strict old clients; routing facts ride accountPools.
|
|
65
|
+
harnessAccounts: [],
|
|
66
|
+
accountPools: await accountPoolsProjection(NO_PROJECT_ROOT, quota.snapshots, {
|
|
57
67
|
profiles: out,
|
|
58
68
|
}),
|
|
59
69
|
};
|
|
@@ -61,7 +71,7 @@ export function createCredentialProfilesService(quotaRegistry) {
|
|
|
61
71
|
const pollCache = new StatusProjectionCache({
|
|
62
72
|
versionOf: globalConfigVersion,
|
|
63
73
|
});
|
|
64
|
-
|
|
74
|
+
const credentialProfiles = async (input) => {
|
|
65
75
|
if (input?.snapshot === true) {
|
|
66
76
|
const [probed, accountStatuses, git, fencedQuota] = await Promise.all([
|
|
67
77
|
projectProfiles(),
|
|
@@ -73,7 +83,6 @@ export function createCredentialProfilesService(quotaRegistry) {
|
|
|
73
83
|
quotaRegistry().refreshWithCursor(),
|
|
74
84
|
]);
|
|
75
85
|
const statuses = accountStatuses.map((receipt) => receipt.status);
|
|
76
|
-
const accountIdentities = new Map(accountStatuses.map((receipt) => [receipt.status.id, receipt.identity]));
|
|
77
86
|
const rawQuota = fencedQuota.response;
|
|
78
87
|
const out = withVendorVerification(probed, rawQuota);
|
|
79
88
|
// The explicit refresh proved the live state — drop the stale poll
|
|
@@ -82,10 +91,10 @@ export function createCredentialProfilesService(quotaRegistry) {
|
|
|
82
91
|
pollCache.invalidate();
|
|
83
92
|
return {
|
|
84
93
|
profiles: out,
|
|
85
|
-
harnessAccounts:
|
|
94
|
+
harnessAccounts: [],
|
|
95
|
+
accountPools: await accountPoolsProjection(NO_PROJECT_ROOT, rawQuota.snapshots, {
|
|
86
96
|
profiles: out,
|
|
87
97
|
statuses,
|
|
88
|
-
accountIdentities,
|
|
89
98
|
}),
|
|
90
99
|
harnesses: await projectHarnessStatuses(statuses),
|
|
91
100
|
git,
|
|
@@ -95,6 +104,10 @@ export function createCredentialProfilesService(quotaRegistry) {
|
|
|
95
104
|
}
|
|
96
105
|
return pollCache.read(buildPollResponse);
|
|
97
106
|
};
|
|
107
|
+
const accountPools = async () => ({
|
|
108
|
+
accountPools: (await pollCache.read(buildPollResponse)).accountPools,
|
|
109
|
+
});
|
|
110
|
+
return { credentialProfiles, accountPools };
|
|
98
111
|
}
|
|
99
112
|
/**
|
|
100
113
|
* Replace each profile's LOCAL verification verdict with the vendor's, wherever
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accounts-services.js","sourceRoot":"","sources":["../src/accounts-services.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE/C,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAC1F,OAAO,EAAE,kBAAkB,EAAsB,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAOhD,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,2BAA2B,EAAE,MAAM,yBAAyB,CAAC;AACtE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"accounts-services.js","sourceRoot":"","sources":["../src/accounts-services.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { AccountsUnifiedMigrationFile } from "@claudexor/schema";
|
|
2
|
+
/** The harnesses whose legacy default stores auto-register (cursor's host
|
|
3
|
+
* Keychain login is deliberately NOT migrated — owner decision D-U3: host
|
|
4
|
+
* logins disappear; new cursor logins are ordinary isolated file-store rows). */
|
|
5
|
+
export declare const ACCOUNTS_MIGRATION_HARNESSES: readonly ["claude", "codex"];
|
|
6
|
+
export type AccountsMigrationHarness = (typeof ACCOUNTS_MIGRATION_HARNESSES)[number];
|
|
7
|
+
export declare function accountsMigrationFilePath(): string;
|
|
8
|
+
/** Lenient read for projection/mutation consumers: any unreadable state reads
|
|
9
|
+
* as "no records" (those surfaces fail safe on their own rules). The RUN GATE
|
|
10
|
+
* and the migration itself use the strict variant below — a corrupt phase
|
|
11
|
+
* file must fail CLOSED there, never silently pass runs against unknown
|
|
12
|
+
* migration state or overwrite the evidence with a fresh reservation. */
|
|
13
|
+
export declare function readAccountsMigrationFile(): AccountsUnifiedMigrationFile;
|
|
14
|
+
/** ENOENT/ENOTDIR = honestly no record ({}); anything else — an unreadable
|
|
15
|
+
* file, a JSON parse failure, a schema violation — THROWS a typed error so
|
|
16
|
+
* the gate and the migration fail closed instead of treating corrupt state
|
|
17
|
+
* as absent. */
|
|
18
|
+
export declare function readAccountsMigrationFileStrict(): AccountsUnifiedMigrationFile;
|
|
19
|
+
/**
|
|
20
|
+
* The daemon startup wrapper: run (or crash-resume) the migration, invalidate
|
|
21
|
+
* status projections when anything changed, and NEVER crash startup — a
|
|
22
|
+
* failed pass leaves the phase file mid-state, the affected harness refuses
|
|
23
|
+
* runs typed (accountsMigrationGate), and the next start resumes the
|
|
24
|
+
* remaining phases.
|
|
25
|
+
*/
|
|
26
|
+
export declare function runStartupAccountsMigration(threads: AccountsMigrationStores["threads"], quota: AccountsMigrationStores["quota"] & {
|
|
27
|
+
noteCredentialChange(): void;
|
|
28
|
+
}, log: (message: string) => void): void;
|
|
29
|
+
/** Retire one harness's migration record (its row was deleted with its
|
|
30
|
+
* material, so a later start detects no legacy login and fabricates nothing). */
|
|
31
|
+
export declare function removeAccountsMigrationRecord(harnessId: string): void;
|
|
32
|
+
/**
|
|
33
|
+
* Typed per-harness run refusal while this harness's migration is incomplete
|
|
34
|
+
* (plan §L.3): a crash between phases must not let runs route against
|
|
35
|
+
* half-migrated continuity state. Complete (or absent) records never block.
|
|
36
|
+
*/
|
|
37
|
+
export declare function accountsMigrationGate(harnessId: string): {
|
|
38
|
+
reason: string;
|
|
39
|
+
} | null;
|
|
40
|
+
/** The harness's exact Claudexor-owned default native store dir, or null for
|
|
41
|
+
* harnesses without one (D-U3: cursor has no default store; agy is named-only). */
|
|
42
|
+
export declare function defaultNativeStoreLocator(harnessId: string): string | null;
|
|
43
|
+
/**
|
|
44
|
+
* The ONE structural "this row IS the harness's default native store" rule,
|
|
45
|
+
* shared by the delete fence and the `native_credentials_enabled` downgrade
|
|
46
|
+
* mirror: true when the locator resolves to the harness's exact
|
|
47
|
+
* defaultNative*Dir() — the same rule the startup migration matches rows by.
|
|
48
|
+
* Bootstrap rows (ensureBootstrapProfile) sit at this locator BEFORE any
|
|
49
|
+
* migration record exists, so record presence must never be the only key.
|
|
50
|
+
*/
|
|
51
|
+
export declare function locatorIsDefaultNativeStore(harnessId: string, locator: string | null | undefined): boolean;
|
|
52
|
+
/** The stores the migration mutates, injected so tests drive temp roots. */
|
|
53
|
+
export interface AccountsMigrationStores {
|
|
54
|
+
threads: {
|
|
55
|
+
migrateNullProfileContinuity(harnessId: string, rowId: string): {
|
|
56
|
+
sessions: number;
|
|
57
|
+
checkpoints: number;
|
|
58
|
+
skippedPartitions: string[];
|
|
59
|
+
};
|
|
60
|
+
rollbackProfileContinuity(harnessId: string, rowId: string): {
|
|
61
|
+
sessions: number;
|
|
62
|
+
checkpoints: number;
|
|
63
|
+
skippedPartitions: string[];
|
|
64
|
+
};
|
|
65
|
+
listThreads(): Array<{
|
|
66
|
+
repo?: {
|
|
67
|
+
root: string;
|
|
68
|
+
} | null;
|
|
69
|
+
}>;
|
|
70
|
+
};
|
|
71
|
+
quota: {
|
|
72
|
+
removeSubject(harness: string, subjectId: string | null): number;
|
|
73
|
+
};
|
|
74
|
+
log?: (message: string) => void;
|
|
75
|
+
}
|
|
76
|
+
export interface AccountsMigrationReceipt {
|
|
77
|
+
harness_id: string;
|
|
78
|
+
row_id: string;
|
|
79
|
+
outcome: "migrated" | "resumed" | "already_completed";
|
|
80
|
+
sessions: number;
|
|
81
|
+
checkpoints: number;
|
|
82
|
+
lanes: number;
|
|
83
|
+
skipped_partitions: string[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Run (or crash-resume) the unified-accounts migration for every harness with
|
|
87
|
+
* a detected legacy login. Idempotent: the second and later starts find
|
|
88
|
+
* completed records (or nothing to detect) and mutate nothing (§K.6 case 6).
|
|
89
|
+
*/
|
|
90
|
+
export declare function runAccountsUnifiedMigration(stores: AccountsMigrationStores): AccountsMigrationReceipt[];
|
|
91
|
+
export interface AccountsRollbackReceipt {
|
|
92
|
+
harness_id: string;
|
|
93
|
+
row_id: string;
|
|
94
|
+
sessions: number;
|
|
95
|
+
checkpoints: number;
|
|
96
|
+
lanes: number;
|
|
97
|
+
skipped_partitions: string[];
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The supported downgrade path (run BEFORE installing 3.5.0): surgically
|
|
101
|
+
* reverses every migration phase — sessions/checkpoints/lane homes return to
|
|
102
|
+
* the null engine-default keys, the auto-registered row leaves the registry
|
|
103
|
+
* (its `enabled` state returns to the `native_credentials_enabled` mirror),
|
|
104
|
+
* the row's quota subject retires, and the harness's migration record is
|
|
105
|
+
* removed so a later new-engine start re-migrates cleanly. The pre-migration
|
|
106
|
+
* config backup (`backup_ref`) stays on disk as manual-recovery evidence.
|
|
107
|
+
* Credential bytes were never moved, so nothing is copied back.
|
|
108
|
+
*/
|
|
109
|
+
export declare function rollbackAccountsUnifiedMigration(stores: AccountsMigrationStores, harnessId?: string): AccountsRollbackReceipt[];
|
|
110
|
+
//# sourceMappingURL=accounts-unified-migration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accounts-unified-migration.d.ts","sourceRoot":"","sources":["../src/accounts-unified-migration.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified-accounts startup migration (INV-135 rewrite, plan §L.3).
|
|
3
|
+
*
|
|
4
|
+
* At daemon start, a DETECTED legacy default-store login (claude/codex) is
|
|
5
|
+
* auto-registered as an ordinary registry row — reserved machine id
|
|
6
|
+
* `claude-default` / `codex-default`, locator = the existing Claudexor-owned
|
|
7
|
+
* native dir, bytes never move — and the null engine-default subject's
|
|
8
|
+
* continuity state (thread sessions, lane checkpoints, durable lane homes)
|
|
9
|
+
* migrates onto that row id as ONE unit (INV-137). Progress is a
|
|
10
|
+
* crash-recoverable per-harness state machine persisted OUTSIDE config.yaml
|
|
11
|
+
* (`<config>/migration/accounts-unified.json`; a downgraded 3.5.0 engine never
|
|
12
|
+
* reads it): `reserved → registry_written → continuity_migrated → completed`,
|
|
13
|
+
* every phase restart-idempotent. While a harness's record is incomplete, that
|
|
14
|
+
* harness's runs refuse typed (`accountsMigrationGate`) — other harnesses keep
|
|
15
|
+
* working. `rollbackAccountsUnifiedMigration` is the supported downgrade
|
|
16
|
+
* path's first step: it surgically reverses every phase (run it BEFORE
|
|
17
|
+
* installing an engine whose canonicalizers refuse the native locator).
|
|
18
|
+
*/
|
|
19
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { loadConfig, updateGlobalConfig } from "@claudexor/config";
|
|
22
|
+
import { normalizeThroughExistingAncestor } from "@claudexor/core";
|
|
23
|
+
import { claudeAccountIdentity, defaultNativeClaudeConfigDir } from "@claudexor/harness-claude";
|
|
24
|
+
import { codexAccountIdentity, defaultNativeCodexHome } from "@claudexor/harness-codex";
|
|
25
|
+
import { AccountsUnifiedMigrationFile as MigrationFileSchema } from "@claudexor/schema";
|
|
26
|
+
import { migrateDefaultLanes, rollbackProfileLanes } from "@claudexor/workspace";
|
|
27
|
+
import { invalidateStatusProjections } from "./status-projection-cache.js";
|
|
28
|
+
import { noProjectRepoRoot, nowIso, userConfigDir } from "@claudexor/util";
|
|
29
|
+
/** The harnesses whose legacy default stores auto-register (cursor's host
|
|
30
|
+
* Keychain login is deliberately NOT migrated — owner decision D-U3: host
|
|
31
|
+
* logins disappear; new cursor logins are ordinary isolated file-store rows). */
|
|
32
|
+
export const ACCOUNTS_MIGRATION_HARNESSES = ["claude", "codex"];
|
|
33
|
+
export function accountsMigrationFilePath() {
|
|
34
|
+
return join(userConfigDir(), "migration", "accounts-unified.json");
|
|
35
|
+
}
|
|
36
|
+
/** Lenient read for projection/mutation consumers: any unreadable state reads
|
|
37
|
+
* as "no records" (those surfaces fail safe on their own rules). The RUN GATE
|
|
38
|
+
* and the migration itself use the strict variant below — a corrupt phase
|
|
39
|
+
* file must fail CLOSED there, never silently pass runs against unknown
|
|
40
|
+
* migration state or overwrite the evidence with a fresh reservation. */
|
|
41
|
+
export function readAccountsMigrationFile() {
|
|
42
|
+
try {
|
|
43
|
+
return readAccountsMigrationFileStrict();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** ENOENT/ENOTDIR = honestly no record ({}); anything else — an unreadable
|
|
50
|
+
* file, a JSON parse failure, a schema violation — THROWS a typed error so
|
|
51
|
+
* the gate and the migration fail closed instead of treating corrupt state
|
|
52
|
+
* as absent. */
|
|
53
|
+
export function readAccountsMigrationFileStrict() {
|
|
54
|
+
const path = accountsMigrationFilePath();
|
|
55
|
+
let raw;
|
|
56
|
+
try {
|
|
57
|
+
raw = readFileSync(path, "utf8");
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const code = error.code;
|
|
61
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
62
|
+
return {};
|
|
63
|
+
throw Object.assign(new Error(`unified-accounts migration record ${path} is unreadable (${code ?? "io_error"})`), { code: "accounts_migration_record_unreadable" });
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return MigrationFileSchema.parse(JSON.parse(raw));
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw Object.assign(new Error(`unified-accounts migration record ${path} is corrupt: ${error instanceof Error ? error.message : String(error)}; fix or remove the file before runs of migrated harnesses can proceed`), { code: "accounts_migration_record_unreadable" });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function writeAccountsMigrationFile(file) {
|
|
73
|
+
const path = accountsMigrationFilePath();
|
|
74
|
+
mkdirSync(join(path, ".."), { recursive: true, mode: 0o700 });
|
|
75
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
76
|
+
writeFileSync(tmp, `${JSON.stringify(file, null, 2)}\n`, { mode: 0o600 });
|
|
77
|
+
renameSync(tmp, path);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The daemon startup wrapper: run (or crash-resume) the migration, invalidate
|
|
81
|
+
* status projections when anything changed, and NEVER crash startup — a
|
|
82
|
+
* failed pass leaves the phase file mid-state, the affected harness refuses
|
|
83
|
+
* runs typed (accountsMigrationGate), and the next start resumes the
|
|
84
|
+
* remaining phases.
|
|
85
|
+
*/
|
|
86
|
+
export function runStartupAccountsMigration(threads, quota, log) {
|
|
87
|
+
try {
|
|
88
|
+
if (runAccountsUnifiedMigration({ threads, quota, log }).length > 0) {
|
|
89
|
+
invalidateStatusProjections();
|
|
90
|
+
quota.noteCredentialChange();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
log(`unified-accounts migration pass failed (will resume on the next start): ${error instanceof Error ? error.message : String(error)}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Retire one harness's migration record (its row was deleted with its
|
|
98
|
+
* material, so a later start detects no legacy login and fabricates nothing). */
|
|
99
|
+
export function removeAccountsMigrationRecord(harnessId) {
|
|
100
|
+
const file = { ...readAccountsMigrationFile() };
|
|
101
|
+
if (!(harnessId in file))
|
|
102
|
+
return;
|
|
103
|
+
delete file[harnessId];
|
|
104
|
+
writeAccountsMigrationFile(file);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Typed per-harness run refusal while this harness's migration is incomplete
|
|
108
|
+
* (plan §L.3): a crash between phases must not let runs route against
|
|
109
|
+
* half-migrated continuity state. Complete (or absent) records never block.
|
|
110
|
+
*/
|
|
111
|
+
export function accountsMigrationGate(harnessId) {
|
|
112
|
+
let record;
|
|
113
|
+
try {
|
|
114
|
+
record = readAccountsMigrationFileStrict()[harnessId];
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
// A CORRUPT phase file fails the gate CLOSED: runs must not route
|
|
118
|
+
// against unknown migration state (an absent file passes above via the
|
|
119
|
+
// strict reader's ENOENT branch).
|
|
120
|
+
return { reason: error instanceof Error ? error.message : String(error) };
|
|
121
|
+
}
|
|
122
|
+
if (!record || record.phase === "completed")
|
|
123
|
+
return null;
|
|
124
|
+
if (record.phase === "rolling_back") {
|
|
125
|
+
return {
|
|
126
|
+
reason: `${harnessId} is blocked by an interrupted unified-accounts rollback; ` +
|
|
127
|
+
`re-run the rollback command (\`claudexor accounts-migration-rollback\`) to finish it`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
reason: `${harnessId} is blocked by an incomplete unified-accounts migration ` +
|
|
132
|
+
`(phase: ${record.phase}); restart the daemon (\`claudexor daemon stop\`, then any command) ` +
|
|
133
|
+
`to finish it, or roll it back (\`claudexor accounts-migration-rollback\`)`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Only ACTUALLY DETECTED logins register — an empty default store writes no
|
|
137
|
+
* record and fabricates no row (§K.6 case 1). */
|
|
138
|
+
function detectLegacyLogin(harnessId) {
|
|
139
|
+
if (harnessId === "claude") {
|
|
140
|
+
const dir = defaultNativeClaudeConfigDir();
|
|
141
|
+
const identity = claudeAccountIdentity(dir);
|
|
142
|
+
// macOS keeps the token in the Keychain (identity in .claude.json);
|
|
143
|
+
// Linux/Windows keep .credentials.json in the dir. Either proves a login.
|
|
144
|
+
if (identity === null && !existsSync(join(dir, ".credentials.json")))
|
|
145
|
+
return null;
|
|
146
|
+
return { locator: dir, displayName: identity?.email ?? null };
|
|
147
|
+
}
|
|
148
|
+
const home = defaultNativeCodexHome();
|
|
149
|
+
try {
|
|
150
|
+
const parsed = JSON.parse(readFileSync(join(home, "auth.json"), "utf8"));
|
|
151
|
+
// Only a subscription login migrates; an api-key auth.json can not exist
|
|
152
|
+
// in the native store (ensureCodexApiAuth refuses to write it there).
|
|
153
|
+
if (parsed.auth_mode !== "chatgpt")
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return { locator: home, displayName: codexAccountIdentity(home)?.email ?? null };
|
|
160
|
+
}
|
|
161
|
+
function sameLocator(a, b) {
|
|
162
|
+
if (!a)
|
|
163
|
+
return false;
|
|
164
|
+
try {
|
|
165
|
+
return normalizeThroughExistingAncestor(a) === normalizeThroughExistingAncestor(b);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return a === b;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** The harness's exact Claudexor-owned default native store dir, or null for
|
|
172
|
+
* harnesses without one (D-U3: cursor has no default store; agy is named-only). */
|
|
173
|
+
export function defaultNativeStoreLocator(harnessId) {
|
|
174
|
+
if (harnessId === "claude")
|
|
175
|
+
return defaultNativeClaudeConfigDir();
|
|
176
|
+
if (harnessId === "codex")
|
|
177
|
+
return defaultNativeCodexHome();
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The ONE structural "this row IS the harness's default native store" rule,
|
|
182
|
+
* shared by the delete fence and the `native_credentials_enabled` downgrade
|
|
183
|
+
* mirror: true when the locator resolves to the harness's exact
|
|
184
|
+
* defaultNative*Dir() — the same rule the startup migration matches rows by.
|
|
185
|
+
* Bootstrap rows (ensureBootstrapProfile) sit at this locator BEFORE any
|
|
186
|
+
* migration record exists, so record presence must never be the only key.
|
|
187
|
+
*/
|
|
188
|
+
export function locatorIsDefaultNativeStore(harnessId, locator) {
|
|
189
|
+
const native = defaultNativeStoreLocator(harnessId);
|
|
190
|
+
return native !== null && sameLocator(locator, native);
|
|
191
|
+
}
|
|
192
|
+
/** Reserved machine id (§L.3): `<harness>-default`, deterministic `-2`, `-3`…
|
|
193
|
+
* suffix on collision. CROSS-HARNESS unique — the wire's scalar
|
|
194
|
+
* `credentialProfileId` must never match rows of two harnesses. */
|
|
195
|
+
function reserveRowId(registry, harnessId) {
|
|
196
|
+
const base = `${harnessId}-default`;
|
|
197
|
+
let candidate = base;
|
|
198
|
+
for (let i = 2; registry.some((p) => p.profile_id === candidate); i += 1) {
|
|
199
|
+
candidate = `${base}-${i}`;
|
|
200
|
+
}
|
|
201
|
+
return candidate;
|
|
202
|
+
}
|
|
203
|
+
function backupConfig(harnessId) {
|
|
204
|
+
try {
|
|
205
|
+
const source = join(userConfigDir(), "config.yaml");
|
|
206
|
+
if (!existsSync(source))
|
|
207
|
+
return null;
|
|
208
|
+
const dir = join(userConfigDir(), "migration", `backup-${harnessId}-${Date.now()}`);
|
|
209
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
210
|
+
copyFileSync(source, join(dir, "config.yaml"));
|
|
211
|
+
return dir;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// Fail-soft: the backup is the manual-recovery half of the downgrade
|
|
215
|
+
// path; the supported rollback command reverses phases surgically and
|
|
216
|
+
// does not depend on it. A null backup_ref is disclosed in the record.
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function laneRootsOf(stores) {
|
|
221
|
+
const roots = new Set([noProjectRepoRoot()]);
|
|
222
|
+
try {
|
|
223
|
+
for (const thread of stores.threads.listThreads()) {
|
|
224
|
+
if (thread.repo?.root)
|
|
225
|
+
roots.add(thread.repo.root);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
/* listing failure bounds the rename to the no-project root; sessions are
|
|
230
|
+
* already migrated by the journal step, so an unrenamed lane degrades to
|
|
231
|
+
* a fresh session with the standard continuity disclosure */
|
|
232
|
+
}
|
|
233
|
+
return [...roots];
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Run (or crash-resume) the unified-accounts migration for every harness with
|
|
237
|
+
* a detected legacy login. Idempotent: the second and later starts find
|
|
238
|
+
* completed records (or nothing to detect) and mutate nothing (§K.6 case 6).
|
|
239
|
+
*/
|
|
240
|
+
export function runAccountsUnifiedMigration(stores) {
|
|
241
|
+
const receipts = [];
|
|
242
|
+
for (const harnessId of ACCOUNTS_MIGRATION_HARNESSES) {
|
|
243
|
+
// STRICT read: a corrupt phase file must never be treated as "no record"
|
|
244
|
+
// here — a fresh reservation would overwrite the crash evidence. The
|
|
245
|
+
// throw is caught by runStartupAccountsMigration; the run gate keeps the
|
|
246
|
+
// affected harnesses refusing typed until the file is fixed or removed.
|
|
247
|
+
const file = readAccountsMigrationFileStrict();
|
|
248
|
+
let record = file[harnessId];
|
|
249
|
+
const resumed = record !== undefined && record.phase !== "completed";
|
|
250
|
+
if (record?.phase === "completed")
|
|
251
|
+
continue;
|
|
252
|
+
if (record?.phase === "rolling_back") {
|
|
253
|
+
// An interrupted ROLLBACK never resumes forward — re-running the
|
|
254
|
+
// rollback command finishes it; the run gate names that instruction.
|
|
255
|
+
stores.log?.(`unified-accounts migration: ${harnessId} has an interrupted rollback; ` +
|
|
256
|
+
`re-run \`claudexor accounts-migration-rollback\` to finish it`);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (record?.phase === "reserved" && detectLegacyLogin(harnessId) === null) {
|
|
260
|
+
// Crash-resume of a RESERVED record whose login has since vanished:
|
|
261
|
+
// the reserved phase mutated nothing but the record itself, and
|
|
262
|
+
// registering a row now would fabricate a cold account for a login
|
|
263
|
+
// that no longer exists. Skip (and clear the record) — logged.
|
|
264
|
+
const remaining = { ...file };
|
|
265
|
+
delete remaining[harnessId];
|
|
266
|
+
writeAccountsMigrationFile(remaining);
|
|
267
|
+
stores.log?.(`unified-accounts migration skipped: ${harnessId} reserved row "${record.row_id}" ` +
|
|
268
|
+
`has no detectable legacy login anymore; the reservation was cleared`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (!record) {
|
|
272
|
+
const detected = detectLegacyLogin(harnessId);
|
|
273
|
+
if (!detected)
|
|
274
|
+
continue;
|
|
275
|
+
const registry = loadConfig(noProjectRepoRoot()).global.credential_profiles;
|
|
276
|
+
const sameLocatorRow = registry.find((p) => p.harness_id === harnessId && sameLocator(p.isolation_locator, detected.locator));
|
|
277
|
+
// Phase RESERVED: id + backup persisted BEFORE any mutation, so a crash
|
|
278
|
+
// can never re-reserve a different id (§K.3). A row already registered
|
|
279
|
+
// at this exact locator is reused as-is (D-U2 no-op reuse).
|
|
280
|
+
record = {
|
|
281
|
+
phase: "reserved",
|
|
282
|
+
row_id: sameLocatorRow?.profile_id ?? reserveRowId(registry, harnessId),
|
|
283
|
+
legacy_aliases: [null],
|
|
284
|
+
locator: detected.locator,
|
|
285
|
+
backup_ref: backupConfig(harnessId),
|
|
286
|
+
};
|
|
287
|
+
writeAccountsMigrationFile({ ...file, [harnessId]: record });
|
|
288
|
+
}
|
|
289
|
+
const rowId = record.row_id;
|
|
290
|
+
if (record.phase === "reserved") {
|
|
291
|
+
registerMigratedRow(harnessId, record);
|
|
292
|
+
record = { ...record, phase: "registry_written" };
|
|
293
|
+
writeAccountsMigrationFile({ ...readAccountsMigrationFileStrict(), [harnessId]: record });
|
|
294
|
+
}
|
|
295
|
+
let continuity = { sessions: 0, checkpoints: 0, skippedPartitions: [] };
|
|
296
|
+
let lanes = 0;
|
|
297
|
+
if (record.phase === "registry_written") {
|
|
298
|
+
continuity = stores.threads.migrateNullProfileContinuity(harnessId, rowId);
|
|
299
|
+
for (const root of laneRootsOf(stores))
|
|
300
|
+
lanes += migrateDefaultLanes(root, harnessId, rowId);
|
|
301
|
+
record = { ...record, phase: "continuity_migrated" };
|
|
302
|
+
writeAccountsMigrationFile({ ...readAccountsMigrationFileStrict(), [harnessId]: record });
|
|
303
|
+
}
|
|
304
|
+
if (record.phase === "continuity_migrated") {
|
|
305
|
+
// No replay alias (§K.3): the legacy null subject is retired here; the
|
|
306
|
+
// new row's subject refreshes fresh on the next quota cycle.
|
|
307
|
+
stores.quota.removeSubject(harnessId, null);
|
|
308
|
+
record = { ...record, phase: "completed" };
|
|
309
|
+
writeAccountsMigrationFile({ ...readAccountsMigrationFileStrict(), [harnessId]: record });
|
|
310
|
+
}
|
|
311
|
+
const receipt = {
|
|
312
|
+
harness_id: harnessId,
|
|
313
|
+
row_id: rowId,
|
|
314
|
+
outcome: resumed ? "resumed" : "migrated",
|
|
315
|
+
sessions: continuity.sessions,
|
|
316
|
+
checkpoints: continuity.checkpoints,
|
|
317
|
+
lanes,
|
|
318
|
+
skipped_partitions: continuity.skippedPartitions,
|
|
319
|
+
};
|
|
320
|
+
receipts.push(receipt);
|
|
321
|
+
stores.log?.(`unified-accounts migration ${receipt.outcome}: ${harnessId} → row "${rowId}" ` +
|
|
322
|
+
`(${receipt.sessions} sessions, ${receipt.checkpoints} checkpoints, ${receipt.lanes} lane homes` +
|
|
323
|
+
(receipt.skipped_partitions.length > 0
|
|
324
|
+
? `; SKIPPED quarantined partition(s) ${receipt.skipped_partitions.join(", ")} — their sessions migrate after recovery`
|
|
325
|
+
: "") +
|
|
326
|
+
`)`);
|
|
327
|
+
}
|
|
328
|
+
return receipts;
|
|
329
|
+
}
|
|
330
|
+
/** Phase REGISTRY_WRITTEN's one locked config write. Idempotent: an existing
|
|
331
|
+
* (harness, row_id) row — or a row already holding the locator — is adopted
|
|
332
|
+
* untouched; `native_credentials_enabled=false` maps to `enabled=false`
|
|
333
|
+
* (the deprecated key stays in config as the downgrade-window mirror). */
|
|
334
|
+
function registerMigratedRow(harnessId, record) {
|
|
335
|
+
const detected = detectLegacyLogin(harnessId);
|
|
336
|
+
updateGlobalConfig((config) => {
|
|
337
|
+
const existing = config.credential_profiles.find((p) => (p.harness_id === harnessId && p.profile_id === record.row_id) ||
|
|
338
|
+
(p.harness_id === harnessId && sameLocator(p.isolation_locator, record.locator)));
|
|
339
|
+
if (existing)
|
|
340
|
+
return config;
|
|
341
|
+
const nativeEnabled = config.harnesses[harnessId]?.native_credentials_enabled !== false;
|
|
342
|
+
const entry = {
|
|
343
|
+
profile_id: record.row_id,
|
|
344
|
+
harness_id: harnessId,
|
|
345
|
+
display_name: detected?.displayName ?? `${harnessId} default login`,
|
|
346
|
+
credential_kind: "config_dir_login",
|
|
347
|
+
isolation_locator: record.locator,
|
|
348
|
+
secret_ref: null,
|
|
349
|
+
enabled: nativeEnabled,
|
|
350
|
+
created_at: nowIso(),
|
|
351
|
+
};
|
|
352
|
+
return { ...config, credential_profiles: [...config.credential_profiles, entry] };
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* The supported downgrade path (run BEFORE installing 3.5.0): surgically
|
|
357
|
+
* reverses every migration phase — sessions/checkpoints/lane homes return to
|
|
358
|
+
* the null engine-default keys, the auto-registered row leaves the registry
|
|
359
|
+
* (its `enabled` state returns to the `native_credentials_enabled` mirror),
|
|
360
|
+
* the row's quota subject retires, and the harness's migration record is
|
|
361
|
+
* removed so a later new-engine start re-migrates cleanly. The pre-migration
|
|
362
|
+
* config backup (`backup_ref`) stays on disk as manual-recovery evidence.
|
|
363
|
+
* Credential bytes were never moved, so nothing is copied back.
|
|
364
|
+
*/
|
|
365
|
+
export function rollbackAccountsUnifiedMigration(stores, harnessId) {
|
|
366
|
+
const receipts = [];
|
|
367
|
+
const targets = harnessId ? [harnessId] : Object.keys(readAccountsMigrationFileStrict());
|
|
368
|
+
for (const target of targets) {
|
|
369
|
+
const file = readAccountsMigrationFileStrict();
|
|
370
|
+
const record = file[target];
|
|
371
|
+
if (!record)
|
|
372
|
+
continue;
|
|
373
|
+
const rowId = record.row_id;
|
|
374
|
+
// Phase ROLLING_BACK persists BEFORE the first reverse step: a crash
|
|
375
|
+
// mid-rollback must not leave a record whose phase claims a completed
|
|
376
|
+
// forward migration while continuity is half-reversed. The startup
|
|
377
|
+
// migration never resumes a rolling_back record forward; every reverse
|
|
378
|
+
// step below is idempotent, so re-running this command finishes the job.
|
|
379
|
+
if (record.phase !== "rolling_back") {
|
|
380
|
+
writeAccountsMigrationFile({ ...file, [target]: { ...record, phase: "rolling_back" } });
|
|
381
|
+
}
|
|
382
|
+
const continuity = stores.threads.rollbackProfileContinuity(target, rowId);
|
|
383
|
+
let lanes = 0;
|
|
384
|
+
for (const root of laneRootsOf(stores))
|
|
385
|
+
lanes += rollbackProfileLanes(root, target, rowId);
|
|
386
|
+
stores.quota.removeSubject(target, rowId);
|
|
387
|
+
updateGlobalConfig((config) => {
|
|
388
|
+
const row = config.credential_profiles.find((p) => p.harness_id === target && p.profile_id === rowId);
|
|
389
|
+
// Only the row this migration created leaves the registry — matched by
|
|
390
|
+
// the record's exact locator, so a later hand-registered same-id row
|
|
391
|
+
// pointing elsewhere is never silently deleted.
|
|
392
|
+
if (!row || !sameLocator(row.isolation_locator, record.locator))
|
|
393
|
+
return config;
|
|
394
|
+
return {
|
|
395
|
+
...config,
|
|
396
|
+
harnesses: {
|
|
397
|
+
...config.harnesses,
|
|
398
|
+
[target]: {
|
|
399
|
+
...config.harnesses[target],
|
|
400
|
+
native_credentials_enabled: row.enabled,
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
credential_profiles: config.credential_profiles.filter((p) => p !== row),
|
|
404
|
+
};
|
|
405
|
+
});
|
|
406
|
+
const remaining = { ...readAccountsMigrationFileStrict() };
|
|
407
|
+
delete remaining[target];
|
|
408
|
+
writeAccountsMigrationFile(remaining);
|
|
409
|
+
receipts.push({
|
|
410
|
+
harness_id: target,
|
|
411
|
+
row_id: rowId,
|
|
412
|
+
sessions: continuity.sessions,
|
|
413
|
+
checkpoints: continuity.checkpoints,
|
|
414
|
+
lanes,
|
|
415
|
+
skipped_partitions: continuity.skippedPartitions,
|
|
416
|
+
});
|
|
417
|
+
stores.log?.(`unified-accounts migration ROLLED BACK: ${target} row "${rowId}" ` +
|
|
418
|
+
`(${continuity.sessions} sessions, ${continuity.checkpoints} checkpoints, ${lanes} lane homes returned to the engine-default keys)`);
|
|
419
|
+
}
|
|
420
|
+
return receipts;
|
|
421
|
+
}
|
|
422
|
+
//# sourceMappingURL=accounts-unified-migration.js.map
|