@nowcrew/daemon 0.6.88-beta.3 → 0.6.88-beta.30
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/README.md +89 -14
- package/dist/agent-ability/controller.js +11 -1
- package/dist/agent-ability/git-errors.js +22 -0
- package/dist/agent-ability/materializer.js +21 -10
- package/dist/agent-ability/resolver.js +12 -4
- package/dist/agent-environment-contract.js +55 -0
- package/dist/agent-environment-control.js +24 -0
- package/dist/agent-environment-execution.js +98 -0
- package/dist/agent-environments.js +275 -0
- package/dist/agent-memory/policy.js +3 -1
- package/dist/automatic-daemon-installer.js +51 -2
- package/dist/automatic-install-target.js +8 -0
- package/dist/computer-main.js +18 -0
- package/dist/computer-profile.js +17 -4
- package/dist/computer-setup-client.js +318 -0
- package/dist/computer-setup-orchestration.js +203 -0
- package/dist/computer-setup.js +244 -0
- package/dist/config.js +30 -12
- package/dist/console-formatter.js +3 -3
- package/dist/console.js +15 -2
- package/dist/daemon-update-eligibility.js +1 -1
- package/dist/execution-journal.js +9 -6
- package/dist/execution-protocol.js +2 -1
- package/dist/execution-runner.js +16 -10
- package/dist/i18n.js +19 -0
- package/dist/list-models.js +13 -11
- package/dist/local-execution-lease.js +22 -0
- package/dist/local-executor.js +88 -92
- package/dist/local-memory/candidate.js +43 -0
- package/dist/local-memory/index-budget.js +8 -0
- package/dist/local-memory/inventory-cli.js +61 -0
- package/dist/local-memory/inventory-preview.js +159 -0
- package/dist/local-memory/inventory-source.js +159 -0
- package/dist/local-memory/pending-match.js +29 -0
- package/dist/local-memory/pending-query.js +100 -0
- package/dist/local-memory/pending-schema.js +29 -0
- package/dist/local-memory/pending-source.js +126 -0
- package/dist/local-memory/pending.js +34 -84
- package/dist/local-memory/prompt.js +20 -12
- package/dist/local-memory/read-cli.js +97 -12
- package/dist/local-memory/rollout.js +82 -0
- package/dist/local-memory/runtime.js +24 -17
- package/dist/local-memory/work-log-execution.js +31 -0
- package/dist/local-memory/work-log.js +58 -0
- package/dist/local-memory/write.js +27 -58
- package/dist/local-runtime-environment.js +50 -0
- package/dist/machine-environment-cache.js +147 -0
- package/dist/machine-environment-contract.js +10 -0
- package/dist/machine-environment-control.js +27 -0
- package/dist/machine-environment-policy.js +75 -0
- package/dist/machine-environment-process.js +110 -0
- package/dist/machine-info.js +70 -25
- package/dist/machine-runtime-environment.js +189 -0
- package/dist/machine-shell-environment-capture.js +135 -0
- package/dist/machine-windows-environment-capture.js +215 -0
- package/dist/machine-windows-environment-script.js +158 -0
- package/dist/main.js +4 -69
- package/dist/marketplace-controller.js +75 -0
- package/dist/marketplace-protocol.js +38 -0
- package/dist/marketplace-runner.js +234 -0
- package/dist/marketplace-serve.js +17 -0
- package/dist/message-file-preview.js +246 -0
- package/dist/native-thread-context.js +59 -0
- package/dist/pre-admission-rejection.js +64 -0
- package/dist/project-skills/execution-adapter.js +3 -2
- package/dist/project-skills/runtime-launch.js +14 -0
- package/dist/prompt.js +15 -15
- package/dist/provider-env.js +26 -15
- package/dist/runner.js +31 -19
- package/dist/runtime-environment-merge.js +16 -0
- package/dist/runtime-health-handler.js +10 -0
- package/dist/runtime-health.js +3 -1
- package/dist/runtime-path.js +22 -12
- package/dist/runtime-probe.js +35 -6
- package/dist/runtimes/acp-probe-transport.js +69 -0
- package/dist/runtimes/claude-models.js +20 -9
- package/dist/runtimes/claude-session-start-hook.js +24 -0
- package/dist/runtimes/claude-thread-runner.js +266 -0
- package/dist/runtimes/claude.js +23 -4
- package/dist/runtimes/codex-app-server-runner.js +43 -4
- package/dist/runtimes/deepseek-harness-launch.js +10 -0
- package/dist/runtimes/deepseek-harness-profile.js +92 -0
- package/dist/runtimes/deepseek-harness-runner.js +226 -0
- package/dist/runtimes/deepseek-harness.js +76 -15
- package/dist/runtimes/harness-probe-retry.js +42 -0
- package/dist/runtimes/hermes-models.js +2 -1
- package/dist/runtimes/kimi-acp-runner.js +84 -15
- package/dist/runtimes/opencode-runner.js +7 -1
- package/dist/runtimes/opencode.js +2 -2
- package/dist/serve.js +95 -184
- package/dist/server-url.js +25 -0
- package/dist/session.js +4 -0
- package/dist/slog.js +2 -1
- package/dist/storage-identity.js +0 -3
- package/dist/storage-runtime.js +2 -2
- package/dist/storage-serve-adapter.js +6 -72
- package/dist/supervised-runtime.js +28 -18
- package/dist/thread-input-delivery.js +217 -0
- package/dist/token.js +0 -1
- package/dist/workspace-control-handler.js +27 -0
- package/dist/workspace-fs.js +66 -15
- package/dist/workspace.js +1 -1
- package/package.json +5 -4
- package/dist/codex-home-launch.js +0 -7
- package/dist/codex-home-mode-controller.js +0 -68
- package/dist/daemon-migration-controller.js +0 -211
- package/dist/daemon-migration-wiring.js +0 -22
- package/dist/runtimes/codex-home-migration-cli.js +0 -26
- package/dist/runtimes/codex-home-migration.js +0 -112
- package/dist/runtimes/codex-home.js +0 -236
- package/dist/storage-control.js +0 -212
- package/dist/storage-copy.js +0 -628
- package/dist/storage-credential-guard.js +0 -21
- package/dist/storage-doctor.js +0 -228
- package/dist/storage-manifest.js +0 -1027
- package/dist/storage-migration-controller.js +0 -440
- package/dist/storage-migration-manifest.js +0 -105
- package/dist/storage-migration-policy.js +0 -100
- package/dist/storage-runtime-controller.js +0 -381
- package/dist/storage-runtime-readiness.js +0 -240
- package/dist/storage-safe-fs.js +0 -302
- package/dist/storage-status-delivery.js +0 -224
- package/dist/storage-status-replay.js +0 -140
- package/dist/storage-target-confirmation.js +0 -129
- package/dist/storage-worker-command.js +0 -37
- package/dist/storage-worker-executor.js +0 -449
- package/dist/storage-worker-handoff.js +0 -151
- package/dist/storage-worker-operation.js +0 -294
- package/dist/storage-worker-status.js +0 -259
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { cp, lstat, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { isAbsolute, relative, resolve } from "node:path";
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
import { loadProfile } from "./computer-profile.js";
|
|
7
|
-
import { builtDaemonEntry } from "./computer-cli.js";
|
|
8
|
-
import { installDaemonAutomatically } from "./automatic-daemon-installer.js";
|
|
9
|
-
import { buildServiceSpec, captureExactService, serviceStatus, systemCommandRunner } from "./computer-service.js";
|
|
10
|
-
import { readManagedServiceRegistry } from "./managed-service-registry.js";
|
|
11
|
-
import { uninstallManagedService } from "./managed-service-lifecycle.js";
|
|
12
|
-
import { resolveAutomaticInstallTarget } from "./automatic-install-target.js";
|
|
13
|
-
import { buildStorageMigrationMappings, createSafeServiceManifest, CredentialMaterialError, } from "./storage-migration-manifest.js";
|
|
14
|
-
const MigrationMessageSchema = z.object({
|
|
15
|
-
type: z.literal("daemon:migrate"),
|
|
16
|
-
migrationId: z.string().uuid(),
|
|
17
|
-
targetProfile: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,47}$/),
|
|
18
|
-
targetDaemonHome: z.string().min(1),
|
|
19
|
-
targetAgentsRoot: z.string().min(1),
|
|
20
|
-
}).strict();
|
|
21
|
-
function migrationBackupRoot(input, id) {
|
|
22
|
-
return resolve(input.backupRoot ?? resolve(input.userHome ?? homedir(), ".crew", "migration-backups"), id);
|
|
23
|
-
}
|
|
24
|
-
function legacyLocalHome(input) {
|
|
25
|
-
return resolve(input.userHome ?? homedir(), ".crew");
|
|
26
|
-
}
|
|
27
|
-
async function assertEmptyOrMissing(path) {
|
|
28
|
-
try {
|
|
29
|
-
const entries = await readdir(path);
|
|
30
|
-
if (entries.length > 0)
|
|
31
|
-
throw new Error(`target path is not empty: ${path}`);
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
if (error.code !== "ENOENT")
|
|
35
|
-
throw error;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
async function hashedFiles(root, directory = root) {
|
|
39
|
-
const entries = await readdir(directory, { withFileTypes: true });
|
|
40
|
-
const files = [];
|
|
41
|
-
for (const entry of entries) {
|
|
42
|
-
const path = resolve(directory, entry.name);
|
|
43
|
-
const stat = await lstat(path);
|
|
44
|
-
if (stat.isSymbolicLink())
|
|
45
|
-
throw new Error(`backup source contains a symbolic link: ${relative(root, path)}`);
|
|
46
|
-
if (stat.isDirectory())
|
|
47
|
-
files.push(...await hashedFiles(root, path));
|
|
48
|
-
else if (stat.isFile())
|
|
49
|
-
files.push({ path: relative(root, path), sha256: createHash("sha256").update(await readFile(path)).digest("hex") });
|
|
50
|
-
}
|
|
51
|
-
return files;
|
|
52
|
-
}
|
|
53
|
-
async function backupAndCopy(input, id, targetHome, targetAgents, service) {
|
|
54
|
-
// Validate and project service identity before creating any backup bytes. A descriptor containing
|
|
55
|
-
// a token must stop the migration before it can be copied into the local backup root.
|
|
56
|
-
const safeService = createSafeServiceManifest({
|
|
57
|
-
spec: service.spec,
|
|
58
|
-
snapshot: service.snapshot,
|
|
59
|
-
managedRecord: service.managedRecord,
|
|
60
|
-
});
|
|
61
|
-
const backup = migrationBackupRoot(input, id);
|
|
62
|
-
await mkdir(backup, { recursive: true, mode: 0o700 });
|
|
63
|
-
await cp(input.currentDaemonHome, resolve(backup, "daemon-home"), { recursive: true, force: false });
|
|
64
|
-
await cp(input.currentAgentsRoot, resolve(backup, "agents-root"), { recursive: true, force: false });
|
|
65
|
-
await cp(input.currentDaemonHome, targetHome, { recursive: true, force: false });
|
|
66
|
-
await cp(input.currentAgentsRoot, targetAgents, { recursive: true, force: false });
|
|
67
|
-
const daemonFiles = await hashedFiles(resolve(backup, "daemon-home"));
|
|
68
|
-
const agentFiles = await hashedFiles(resolve(backup, "agents-root"));
|
|
69
|
-
const mappings = buildStorageMigrationMappings({ daemonFiles, agentFiles });
|
|
70
|
-
await writeFile(resolve(backup, "manifest.json"), `${JSON.stringify({
|
|
71
|
-
version: 2,
|
|
72
|
-
migrationId: id,
|
|
73
|
-
profile: input.profileName,
|
|
74
|
-
daemonHome: input.currentDaemonHome,
|
|
75
|
-
agentsRoot: input.currentAgentsRoot,
|
|
76
|
-
service: safeService,
|
|
77
|
-
mappings,
|
|
78
|
-
}, null, 2)}\n`, { mode: 0o600 });
|
|
79
|
-
return backup;
|
|
80
|
-
}
|
|
81
|
-
async function cleanupCurrentServiceDefault(spec, input) {
|
|
82
|
-
const userHome = input.userHome ?? homedir();
|
|
83
|
-
const records = await readManagedServiceRegistry({ userHome, localHome: legacyLocalHome(input) });
|
|
84
|
-
const record = records.find((candidate) => candidate.profile === spec.profile && candidate.daemonHome === input.currentDaemonHome);
|
|
85
|
-
if (record !== undefined && (spec.platform === "darwin" || spec.platform === "linux")) {
|
|
86
|
-
await uninstallManagedService({ spec, record, runner: systemCommandRunner, registry: { userHome, localHome: legacyLocalHome(input) } });
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
// Windows and legacy descriptors are handled by the normal CLI/service lifecycle.
|
|
90
|
-
const { uninstallService } = await import("./computer-service.js");
|
|
91
|
-
const status = await serviceStatus(spec, systemCommandRunner);
|
|
92
|
-
if (status.installed)
|
|
93
|
-
await uninstallService(spec, systemCommandRunner);
|
|
94
|
-
}
|
|
95
|
-
export function createDaemonMigrationController(input) {
|
|
96
|
-
const handled = new Set();
|
|
97
|
-
let running = null;
|
|
98
|
-
const sendFailure = (id, errorCode, error) => {
|
|
99
|
-
input.sendStatus({ type: "daemon:migration-status", migrationId: id, status: "failed", errorCode, errorMessage: error instanceof Error ? error.message : String(error) });
|
|
100
|
-
};
|
|
101
|
-
const execute = async (raw) => {
|
|
102
|
-
const parsed = MigrationMessageSchema.safeParse(raw);
|
|
103
|
-
if (!parsed.success)
|
|
104
|
-
return false;
|
|
105
|
-
const message = parsed.data;
|
|
106
|
-
if (handled.has(message.migrationId))
|
|
107
|
-
return true;
|
|
108
|
-
if (running !== null) {
|
|
109
|
-
sendFailure(message.migrationId, "target_conflict", new Error("another migration is already running"));
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
running = (async () => {
|
|
113
|
-
input.sendStatus({ type: "daemon:migration-status", migrationId: message.migrationId, status: "preflight" });
|
|
114
|
-
if (input.isBusy()) {
|
|
115
|
-
sendFailure(message.migrationId, "runtime_busy", new Error("active executions must finish before migration"));
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const platform = input.platform ?? process.platform;
|
|
119
|
-
const userHome = input.userHome ?? homedir();
|
|
120
|
-
let targetPath;
|
|
121
|
-
let targetAgents;
|
|
122
|
-
let currentSpec;
|
|
123
|
-
let currentService;
|
|
124
|
-
let managedRecord = null;
|
|
125
|
-
let currentRegistryUrl;
|
|
126
|
-
try {
|
|
127
|
-
const currentProfile = await (input.loadCurrentProfile ?? loadProfile)(input.profileName, input.currentDaemonHome);
|
|
128
|
-
if (currentProfile.machineToken !== input.machineToken)
|
|
129
|
-
throw new Error("active profile token does not match this daemon");
|
|
130
|
-
currentRegistryUrl = currentProfile.registryUrl;
|
|
131
|
-
const target = resolveAutomaticInstallTarget({
|
|
132
|
-
serverUrl: input.serverUrl,
|
|
133
|
-
requestedProfile: message.targetProfile,
|
|
134
|
-
userHome,
|
|
135
|
-
platform,
|
|
136
|
-
// Layout migration creates the canonical service in the new local-home
|
|
137
|
-
// root. Source registry and rollback material remain under legacy .crew.
|
|
138
|
-
requestedLocalHomeMode: "nowwork",
|
|
139
|
-
});
|
|
140
|
-
if (!isAbsolute(message.targetDaemonHome) || !isAbsolute(message.targetAgentsRoot)
|
|
141
|
-
|| resolve(message.targetDaemonHome) !== resolve(target.daemonHome)
|
|
142
|
-
|| resolve(message.targetAgentsRoot) !== resolve(target.agentsRoot))
|
|
143
|
-
throw new Error("requested target does not match the standard profile layout");
|
|
144
|
-
targetPath = resolve(target.daemonHome);
|
|
145
|
-
targetAgents = resolve(target.agentsRoot);
|
|
146
|
-
await assertEmptyOrMissing(targetPath);
|
|
147
|
-
await assertEmptyOrMissing(targetAgents);
|
|
148
|
-
currentSpec = buildServiceSpec({ platform, profile: input.profileName, userHome, uid: process.getuid?.(), nodePath: process.execPath, entryPath: builtDaemonEntry(), profileHome: input.currentDaemonHome });
|
|
149
|
-
const exact = await (input.captureCurrentService ?? ((spec) => captureExactService(spec, systemCommandRunner)))(currentSpec);
|
|
150
|
-
if (!exact.installed || !exact.running || !exact.exact)
|
|
151
|
-
throw new Error("current managed service identity is not exact and running");
|
|
152
|
-
currentService = { installed: exact.installed, running: exact.running };
|
|
153
|
-
if (platform === "darwin" || platform === "linux") {
|
|
154
|
-
const records = await (input.readManagedRecords ?? (() => readManagedServiceRegistry({ userHome, localHome: legacyLocalHome(input) })))();
|
|
155
|
-
managedRecord = records.find((record) => record.serviceId === currentSpec.id && record.profile === input.profileName && record.daemonHome === input.currentDaemonHome) ?? null;
|
|
156
|
-
if (managedRecord === null)
|
|
157
|
-
throw new Error("current managed service registry record is missing");
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
catch (error) {
|
|
161
|
-
sendFailure(message.migrationId, error instanceof CredentialMaterialError ? "credential_material" : "target_conflict", error);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
input.sendStatus({ type: "daemon:migration-status", migrationId: message.migrationId, status: "backing_up" });
|
|
165
|
-
let backupPath;
|
|
166
|
-
try {
|
|
167
|
-
backupPath = await backupAndCopy(input, message.migrationId, targetPath, targetAgents, { spec: currentSpec, snapshot: currentService, managedRecord });
|
|
168
|
-
input.sendStatus({ type: "daemon:migration-status", migrationId: message.migrationId, status: "installing", backupPath });
|
|
169
|
-
}
|
|
170
|
-
catch (error) {
|
|
171
|
-
await Promise.allSettled([rm(targetPath, { recursive: true, force: true }), rm(targetAgents, { recursive: true, force: true })]);
|
|
172
|
-
sendFailure(message.migrationId, error instanceof CredentialMaterialError ? "credential_material" : "backup_failed", error);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
const install = input.install ?? installDaemonAutomatically;
|
|
176
|
-
try {
|
|
177
|
-
const result = await install({
|
|
178
|
-
serverUrl: input.serverUrl,
|
|
179
|
-
machineToken: input.machineToken,
|
|
180
|
-
requestedProfile: message.targetProfile,
|
|
181
|
-
requestedLocalHomeMode: "nowwork",
|
|
182
|
-
...(currentRegistryUrl === undefined ? {} : { requestedRegistryUrl: currentRegistryUrl }),
|
|
183
|
-
});
|
|
184
|
-
if (result.profile !== message.targetProfile || result.serviceId.length === 0) {
|
|
185
|
-
throw new Error("target service identity did not match the requested profile");
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
catch (error) {
|
|
189
|
-
await Promise.allSettled([rm(targetPath, { recursive: true, force: true }), rm(targetAgents, { recursive: true, force: true })]);
|
|
190
|
-
sendFailure(message.migrationId, "install_failed", error);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
input.sendStatus({ type: "daemon:migration-status", migrationId: message.migrationId, status: "restarting" });
|
|
194
|
-
try {
|
|
195
|
-
await (input.cleanupCurrentService ?? ((spec) => cleanupCurrentServiceDefault(spec, input)))(currentSpec);
|
|
196
|
-
}
|
|
197
|
-
catch (error) {
|
|
198
|
-
const targetSpec = buildServiceSpec({ platform, profile: message.targetProfile, userHome, uid: process.getuid?.(), nodePath: process.execPath, entryPath: builtDaemonEntry(), profileHome: targetPath });
|
|
199
|
-
await (input.cleanupTargetService ?? ((spec) => cleanupCurrentServiceDefault(spec, { ...input, profileName: message.targetProfile, currentDaemonHome: targetPath })))(targetSpec).catch(() => undefined);
|
|
200
|
-
sendFailure(message.migrationId, "service_cleanup_failed", error);
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
})().finally(() => {
|
|
204
|
-
handled.add(message.migrationId);
|
|
205
|
-
running = null;
|
|
206
|
-
});
|
|
207
|
-
await running;
|
|
208
|
-
return true;
|
|
209
|
-
};
|
|
210
|
-
return { handle: execute, drain: async () => { await running; } };
|
|
211
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { daemonHome } from "./computer-profile.js";
|
|
2
|
-
import { createDaemonMigrationController } from "./daemon-migration-controller.js";
|
|
3
|
-
export function createServeMigrationController(input) {
|
|
4
|
-
if (input.profileName === undefined)
|
|
5
|
-
return null;
|
|
6
|
-
return createDaemonMigrationController({
|
|
7
|
-
serverUrl: input.config.serverUrl,
|
|
8
|
-
machineToken: input.config.machineToken,
|
|
9
|
-
profileName: input.profileName,
|
|
10
|
-
currentDaemonHome: daemonHome(),
|
|
11
|
-
currentAgentsRoot: input.config.agentsRoot,
|
|
12
|
-
isBusy: input.isBusy,
|
|
13
|
-
sendStatus: input.sendStatus,
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
export async function handleMigrationControlMessage(controller, decoded) {
|
|
17
|
-
if (controller === null || typeof decoded !== "object" || decoded === null
|
|
18
|
-
|| !("type" in decoded) || decoded.type !== "daemon:migrate")
|
|
19
|
-
return false;
|
|
20
|
-
await controller.handle(decoded);
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { parseArgs } from "node:util";
|
|
3
|
-
import { migrateCodexHome } from "./codex-home-migration.js";
|
|
4
|
-
const { values } = parseArgs({
|
|
5
|
-
args: process.argv.slice(2),
|
|
6
|
-
strict: true,
|
|
7
|
-
options: {
|
|
8
|
-
"agent-home": { type: "string" },
|
|
9
|
-
"source-home": { type: "string" },
|
|
10
|
-
"backup-root": { type: "string" },
|
|
11
|
-
yes: { type: "boolean", default: false },
|
|
12
|
-
},
|
|
13
|
-
});
|
|
14
|
-
function required(value, name) {
|
|
15
|
-
if (!value?.trim())
|
|
16
|
-
throw new Error(`Missing --${name}`);
|
|
17
|
-
return value;
|
|
18
|
-
}
|
|
19
|
-
if (!values.yes)
|
|
20
|
-
throw new Error("Refusing Codex home migration without --yes");
|
|
21
|
-
const result = await migrateCodexHome({
|
|
22
|
-
agentHome: required(values["agent-home"], "agent-home"),
|
|
23
|
-
sourceHome: required(values["source-home"], "source-home"),
|
|
24
|
-
backupRoot: required(values["backup-root"], "backup-root"),
|
|
25
|
-
});
|
|
26
|
-
process.stdout.write(`${result.status}${result.backupDir ? ` backup=${result.backupDir}` : ""}\n`);
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
|
-
import { lstat, mkdir, readlink, rename, rm, symlink, unlink, } from "node:fs/promises";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
5
|
-
import { randomUUID } from "node:crypto";
|
|
6
|
-
import { atomicPrivateWrite } from "../atomic-private-write.js";
|
|
7
|
-
import { defaultCodexHome, materializeDefaultCodexHome } from "./codex-home.js";
|
|
8
|
-
const execFileAsync = promisify(execFile);
|
|
9
|
-
const SQLITE_FILES = ["state_5.sqlite", "state_5.sqlite-wal", "state_5.sqlite-shm"];
|
|
10
|
-
const MARKER_NAME = ".codex-home-migration.json";
|
|
11
|
-
async function exists(path) {
|
|
12
|
-
try {
|
|
13
|
-
await lstat(path);
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
16
|
-
catch (error) {
|
|
17
|
-
if (error.code === "ENOENT")
|
|
18
|
-
return false;
|
|
19
|
-
throw error;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
async function assertNoOpenSqliteFiles(codexHome) {
|
|
23
|
-
for (const name of SQLITE_FILES) {
|
|
24
|
-
const path = join(codexHome, name);
|
|
25
|
-
if (!(await exists(path)))
|
|
26
|
-
continue;
|
|
27
|
-
try {
|
|
28
|
-
const result = await execFileAsync("lsof", ["-t", "--", path], { encoding: "utf8" });
|
|
29
|
-
if (result.stdout.trim()) {
|
|
30
|
-
throw new Error(`Codex SQLite file is still open: ${path}`);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
const code = error.code;
|
|
35
|
-
if (code === 1)
|
|
36
|
-
continue;
|
|
37
|
-
if (code === "ENOENT")
|
|
38
|
-
throw new Error("lsof is required to verify Codex home quiescence");
|
|
39
|
-
throw error;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
async function symlinkTarget(path) {
|
|
44
|
-
try {
|
|
45
|
-
const info = await lstat(path);
|
|
46
|
-
if (!info.isSymbolicLink())
|
|
47
|
-
return null;
|
|
48
|
-
return await readlink(path);
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
if (error.code === "ENOENT")
|
|
52
|
-
return null;
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
export async function migrateCodexHome(input, dependencies = {}) {
|
|
57
|
-
const agentHome = resolve(input.agentHome);
|
|
58
|
-
const sourceHome = resolve(input.sourceHome);
|
|
59
|
-
const backupRoot = resolve(input.backupRoot);
|
|
60
|
-
const codexHome = defaultCodexHome(agentHome);
|
|
61
|
-
const sessions = join(codexHome, "sessions");
|
|
62
|
-
const index = join(codexHome, "session_index.jsonl");
|
|
63
|
-
const expectedSessions = resolve(sourceHome, "sessions");
|
|
64
|
-
const expectedIndex = resolve(sourceHome, "session_index.jsonl");
|
|
65
|
-
const marker = join(codexHome, MARKER_NAME);
|
|
66
|
-
const sessionLink = await symlinkTarget(sessions);
|
|
67
|
-
const indexLink = await symlinkTarget(index);
|
|
68
|
-
if (sessionLink === null && indexLink === null && await exists(marker)) {
|
|
69
|
-
return { status: "already-migrated" };
|
|
70
|
-
}
|
|
71
|
-
if (sessionLink === null || indexLink === null) {
|
|
72
|
-
throw new Error("Refusing migration: Codex home is not in the complete legacy symlink layout");
|
|
73
|
-
}
|
|
74
|
-
if (resolve(codexHome, sessionLink) !== expectedSessions || resolve(codexHome, indexLink) !== expectedIndex) {
|
|
75
|
-
throw new Error("Refusing migration: legacy symlinks do not point to the configured source home");
|
|
76
|
-
}
|
|
77
|
-
await (dependencies.ensureQuiesced ?? assertNoOpenSqliteFiles)(codexHome);
|
|
78
|
-
const stamp = (dependencies.now ?? (() => new Date()))().toISOString().replaceAll(/[^0-9]/g, "").slice(0, 14);
|
|
79
|
-
const backupDir = join(backupRoot, `${stamp}-${dependencies.id?.() ?? randomUUID()}`);
|
|
80
|
-
await mkdir(backupDir, { recursive: true, mode: 0o700 });
|
|
81
|
-
await symlink(sessionLink, join(backupDir, "sessions"), "dir");
|
|
82
|
-
await symlink(indexLink, join(backupDir, "session_index.jsonl"), "file");
|
|
83
|
-
const moved = [];
|
|
84
|
-
try {
|
|
85
|
-
for (const name of SQLITE_FILES) {
|
|
86
|
-
const current = join(codexHome, name);
|
|
87
|
-
if (await exists(current)) {
|
|
88
|
-
await rename(current, join(backupDir, name));
|
|
89
|
-
moved.push(name);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
await materializeDefaultCodexHome(agentHome, sourceHome);
|
|
93
|
-
await atomicPrivateWrite(marker, JSON.stringify({
|
|
94
|
-
version: 1,
|
|
95
|
-
migrated_at: new Date().toISOString(),
|
|
96
|
-
source_home: sourceHome,
|
|
97
|
-
backup_dir: backupDir,
|
|
98
|
-
}) + "\n");
|
|
99
|
-
return { status: "migrated", backupDir };
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
for (const name of moved.reverse()) {
|
|
103
|
-
await rename(join(backupDir, name), join(codexHome, name)).catch(() => undefined);
|
|
104
|
-
}
|
|
105
|
-
await rm(sessions, { recursive: true, force: true });
|
|
106
|
-
await unlink(index).catch(() => undefined);
|
|
107
|
-
await symlink(sessionLink, sessions, "dir").catch(() => undefined);
|
|
108
|
-
await symlink(indexLink, index, "file").catch(() => undefined);
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
export const codexHomeMigrationMarkerName = MARKER_NAME;
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import { chmod, copyFile, lstat, mkdir, open, readdir, readlink, rename, rm, symlink, unlink, link, writeFile, } from "node:fs/promises";
|
|
2
|
-
import { constants } from "node:fs";
|
|
3
|
-
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
-
const PRIVATE_CODEX_HOME = ".codex";
|
|
5
|
-
export function defaultCodexHome(homeDir) {
|
|
6
|
-
return resolve(homeDir, PRIVATE_CODEX_HOME);
|
|
7
|
-
}
|
|
8
|
-
async function linkIfMissing(source, target) {
|
|
9
|
-
try {
|
|
10
|
-
await lstat(target);
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
catch (error) {
|
|
14
|
-
if (error.code !== "ENOENT")
|
|
15
|
-
throw error;
|
|
16
|
-
}
|
|
17
|
-
try {
|
|
18
|
-
await lstat(source);
|
|
19
|
-
}
|
|
20
|
-
catch (error) {
|
|
21
|
-
if (error.code === "ENOENT")
|
|
22
|
-
return;
|
|
23
|
-
throw error;
|
|
24
|
-
}
|
|
25
|
-
try {
|
|
26
|
-
await symlink(source, target, "file");
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
if (error.code !== "EEXIST")
|
|
30
|
-
throw error;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function isBelow(root, candidate) {
|
|
34
|
-
const child = relative(root, candidate);
|
|
35
|
-
return child !== "" && child !== ".." && !child.startsWith(`..${sep}`) && !child.startsWith(sep);
|
|
36
|
-
}
|
|
37
|
-
async function isExactSymlink(target, expectedSource) {
|
|
38
|
-
const linkTarget = await readlink(target);
|
|
39
|
-
return resolve(dirname(target), linkTarget) === resolve(expectedSource);
|
|
40
|
-
}
|
|
41
|
-
async function ensurePrivateSessions(target, legacySource) {
|
|
42
|
-
try {
|
|
43
|
-
const current = await lstat(target);
|
|
44
|
-
if (current.isSymbolicLink()) {
|
|
45
|
-
if (legacySource === undefined || !(await isExactSymlink(target, legacySource))) {
|
|
46
|
-
throw new Error(`Refusing to replace unexpected Codex sessions symlink: ${target}`);
|
|
47
|
-
}
|
|
48
|
-
const replacement = `${target}.private-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
49
|
-
const legacyBackup = `${target}.legacy-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
50
|
-
try {
|
|
51
|
-
await mkdir(replacement, { mode: 0o700 });
|
|
52
|
-
await rename(target, legacyBackup);
|
|
53
|
-
try {
|
|
54
|
-
await rename(replacement, target);
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
await rename(legacyBackup, target).catch(() => undefined);
|
|
58
|
-
throw error;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
finally {
|
|
62
|
-
await rm(replacement, { recursive: true, force: true }).catch(() => undefined);
|
|
63
|
-
await rm(legacyBackup, { recursive: true, force: true }).catch(() => undefined);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
else if (!current.isDirectory()) {
|
|
67
|
-
throw new Error(`Codex sessions target is not a directory: ${target}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
if (error.code !== "ENOENT")
|
|
72
|
-
throw error;
|
|
73
|
-
await mkdir(target, { mode: 0o700 });
|
|
74
|
-
}
|
|
75
|
-
await chmod(target, 0o700);
|
|
76
|
-
}
|
|
77
|
-
async function atomicCopyFile(source, target, mode) {
|
|
78
|
-
const parent = dirname(target);
|
|
79
|
-
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
80
|
-
const temporary = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
81
|
-
try {
|
|
82
|
-
await copyFile(source, temporary, constants.COPYFILE_EXCL);
|
|
83
|
-
await chmod(temporary, mode);
|
|
84
|
-
try {
|
|
85
|
-
await link(temporary, target);
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
if (error.code !== "EEXIST")
|
|
90
|
-
throw error;
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
finally {
|
|
95
|
-
await unlink(temporary).catch(() => undefined);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
async function ensurePrivateIndex(target, source) {
|
|
99
|
-
let current;
|
|
100
|
-
try {
|
|
101
|
-
current = await lstat(target);
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
if (error.code !== "ENOENT")
|
|
105
|
-
throw error;
|
|
106
|
-
}
|
|
107
|
-
if (current?.isSymbolicLink()) {
|
|
108
|
-
if (source === undefined || !(await isExactSymlink(target, source))) {
|
|
109
|
-
throw new Error(`Refusing to replace unexpected Codex session index symlink: ${target}`);
|
|
110
|
-
}
|
|
111
|
-
const replacement = `${target}.private-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
112
|
-
const legacyBackup = `${target}.legacy-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
113
|
-
try {
|
|
114
|
-
try {
|
|
115
|
-
await lstat(source);
|
|
116
|
-
await copyFile(source, replacement, constants.COPYFILE_EXCL);
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
if (error.code !== "ENOENT")
|
|
120
|
-
throw error;
|
|
121
|
-
await writeFile(replacement, "", { encoding: "utf8", flag: "wx" });
|
|
122
|
-
}
|
|
123
|
-
await chmod(replacement, 0o600);
|
|
124
|
-
await rename(target, legacyBackup);
|
|
125
|
-
try {
|
|
126
|
-
await rename(replacement, target);
|
|
127
|
-
}
|
|
128
|
-
catch (error) {
|
|
129
|
-
await rename(legacyBackup, target).catch(() => undefined);
|
|
130
|
-
throw error;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
finally {
|
|
134
|
-
await rm(replacement, { force: true }).catch(() => undefined);
|
|
135
|
-
await rm(legacyBackup, { force: true }).catch(() => undefined);
|
|
136
|
-
}
|
|
137
|
-
current = undefined;
|
|
138
|
-
}
|
|
139
|
-
if (current !== undefined && !current.isFile()) {
|
|
140
|
-
throw new Error(`Codex session index target is not a regular file: ${target}`);
|
|
141
|
-
}
|
|
142
|
-
if (current !== undefined) {
|
|
143
|
-
await chmod(target, 0o600);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
if (source !== undefined) {
|
|
147
|
-
try {
|
|
148
|
-
await lstat(source);
|
|
149
|
-
if (await atomicCopyFile(source, target, 0o600))
|
|
150
|
-
return;
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
if (error.code !== "ENOENT")
|
|
155
|
-
throw error;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
const handle = await open(target, "wx", 0o600).catch((error) => {
|
|
159
|
-
if (error.code === "EEXIST")
|
|
160
|
-
return null;
|
|
161
|
-
throw error;
|
|
162
|
-
});
|
|
163
|
-
await handle?.close();
|
|
164
|
-
}
|
|
165
|
-
async function findRollout(root, sessionId) {
|
|
166
|
-
const entries = await readdir(root, { withFileTypes: true }).catch((error) => {
|
|
167
|
-
if (error.code === "ENOENT")
|
|
168
|
-
return [];
|
|
169
|
-
throw error;
|
|
170
|
-
});
|
|
171
|
-
const matches = [];
|
|
172
|
-
for (const entry of entries) {
|
|
173
|
-
const path = join(root, entry.name);
|
|
174
|
-
if (entry.isDirectory()) {
|
|
175
|
-
const nested = await findRollout(path, sessionId);
|
|
176
|
-
if (nested !== null)
|
|
177
|
-
matches.push(nested);
|
|
178
|
-
}
|
|
179
|
-
else if (entry.isFile() && entry.name.includes(sessionId)) {
|
|
180
|
-
matches.push(path);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
if (matches.length > 1)
|
|
184
|
-
throw new Error(`Multiple Codex rollout files match session ${sessionId}`);
|
|
185
|
-
return matches[0] ?? null;
|
|
186
|
-
}
|
|
187
|
-
async function hydrateResumeRollout(sourceSessions, privateSessions, sessionId) {
|
|
188
|
-
if (sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
189
|
-
throw new Error("Invalid Codex resume session id");
|
|
190
|
-
}
|
|
191
|
-
const sourcePath = await findRollout(sourceSessions, sessionId);
|
|
192
|
-
if (sourcePath === null)
|
|
193
|
-
return;
|
|
194
|
-
const relativePath = relative(sourceSessions, sourcePath);
|
|
195
|
-
const destination = resolve(privateSessions, relativePath);
|
|
196
|
-
if (!isBelow(resolve(privateSessions), destination)) {
|
|
197
|
-
throw new Error("Codex resume rollout escaped the private sessions directory");
|
|
198
|
-
}
|
|
199
|
-
let existing;
|
|
200
|
-
try {
|
|
201
|
-
existing = await lstat(destination);
|
|
202
|
-
}
|
|
203
|
-
catch (error) {
|
|
204
|
-
if (error.code !== "ENOENT")
|
|
205
|
-
throw error;
|
|
206
|
-
}
|
|
207
|
-
if (existing !== undefined) {
|
|
208
|
-
if (!existing.isFile() || existing.isSymbolicLink()) {
|
|
209
|
-
throw new Error(`Refusing to replace existing Codex rollout: ${destination}`);
|
|
210
|
-
}
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
const mode = (await lstat(sourcePath)).mode & 0o777;
|
|
214
|
-
await atomicCopyFile(sourcePath, destination, mode);
|
|
215
|
-
}
|
|
216
|
-
/** Isolate Codex high-volume state per Agent while preserving auth and exact resume. */
|
|
217
|
-
export async function materializeDefaultCodexHome(agentHome, sourceHome, resumeSessionId) {
|
|
218
|
-
const codexHome = defaultCodexHome(agentHome);
|
|
219
|
-
await mkdir(codexHome, { recursive: true, mode: 0o700 });
|
|
220
|
-
await chmod(codexHome, 0o700);
|
|
221
|
-
const source = sourceHome === undefined ? null : resolve(sourceHome);
|
|
222
|
-
if (source !== null && source !== codexHome) {
|
|
223
|
-
await Promise.all([
|
|
224
|
-
linkIfMissing(join(source, "auth.json"), join(codexHome, "auth.json")),
|
|
225
|
-
linkIfMissing(join(source, "config.toml"), join(codexHome, "config.toml")),
|
|
226
|
-
]);
|
|
227
|
-
}
|
|
228
|
-
const sourceSessions = source === null || source === codexHome ? undefined : join(source, "sessions");
|
|
229
|
-
await ensurePrivateSessions(join(codexHome, "sessions"), sourceSessions);
|
|
230
|
-
const sourceIndex = source === null || source === codexHome ? undefined : join(source, "session_index.jsonl");
|
|
231
|
-
await ensurePrivateIndex(join(codexHome, "session_index.jsonl"), sourceIndex);
|
|
232
|
-
if (sourceSessions !== undefined && resumeSessionId) {
|
|
233
|
-
await hydrateResumeRollout(sourceSessions, join(codexHome, "sessions"), resumeSessionId);
|
|
234
|
-
}
|
|
235
|
-
return codexHome;
|
|
236
|
-
}
|