@nowcrew/daemon 0.6.68 → 0.6.70
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/agent-ability/materializer.js +30 -1
- package/dist/agent-ability/resolver.js +27 -16
- package/dist/agent-ability/runtime.js +3 -0
- package/dist/agent-ability/types.js +6 -11
- package/dist/automatic-install-target.js +15 -10
- package/dist/background-job.js +50 -0
- package/dist/computer-cli.js +10 -1
- package/dist/computer-service.js +41 -1
- package/dist/config.js +44 -0
- package/dist/console-formatter.js +76 -1
- package/dist/console.js +23 -4
- package/dist/daemon-migration-controller.js +16 -18
- package/dist/execution-protocol.js +0 -1
- package/dist/execution-runner.js +60 -11
- package/dist/i18n.js +2 -0
- package/dist/local-execution-prompt.js +32 -13
- package/dist/local-executor.js +107 -55
- package/dist/local-memory/index-budget.js +74 -0
- package/dist/local-memory/pending.js +111 -0
- package/dist/local-memory/runtime.js +87 -0
- package/dist/local-memory/types.js +1 -0
- package/dist/local-memory/write.js +448 -0
- package/dist/machine-info.js +3 -4
- package/dist/main.js +83 -0
- package/dist/managed-service-lifecycle.js +47 -2
- package/dist/managed-service-startup.js +2 -0
- package/dist/profile-layout.js +10 -4
- package/dist/project-dependencies.js +139 -124
- package/dist/project-dependency-lock.js +87 -0
- package/dist/project-dependency-manifest.js +2 -12
- package/dist/project-registry-wiring.js +1 -4
- package/dist/project-skills/controller.js +2 -0
- package/dist/project-skills/runtime-launch.js +10 -7
- package/dist/prompt.js +19 -1
- package/dist/provider-env.js +3 -21
- package/dist/repository-identity.js +45 -0
- package/dist/runner.js +4 -2
- package/dist/runtimes/codex-app-server-runner.js +55 -23
- package/dist/runtimes/codex-command-safety.js +75 -0
- package/dist/runtimes/codex-contract.js +20 -5
- package/dist/runtimes/codex-deepseek-config.js +7 -17
- package/dist/runtimes/codex-turn-retry.js +69 -0
- package/dist/runtimes/codex.js +4 -3
- package/dist/serve.js +68 -3
- package/dist/storage-config.js +175 -0
- package/dist/storage-control.js +193 -0
- package/dist/storage-copy.js +628 -0
- package/dist/storage-credential-guard.js +21 -0
- package/dist/storage-doctor.js +228 -0
- package/dist/storage-identity.js +29 -0
- package/dist/storage-layout.js +143 -0
- package/dist/storage-manifest.js +855 -0
- package/dist/storage-migration-controller.js +420 -0
- package/dist/storage-migration-manifest.js +105 -0
- package/dist/storage-migration-policy.js +100 -0
- package/dist/storage-runtime-controller.js +339 -0
- package/dist/storage-runtime-readiness.js +184 -0
- package/dist/storage-runtime.js +39 -0
- package/dist/storage-safe-fs.js +302 -0
- package/dist/storage-serve-adapter.js +65 -0
- package/dist/storage-status-delivery.js +209 -0
- package/dist/storage-status-replay.js +125 -0
- package/dist/storage-target-confirmation.js +129 -0
- package/dist/storage-worker-command.js +37 -0
- package/dist/storage-worker-executor.js +430 -0
- package/dist/storage-worker-handoff.js +151 -0
- package/dist/storage-worker-operation.js +284 -0
- package/dist/storage-worker-status.js +259 -0
- package/dist/supervised-runtime.js +3 -1
- package/dist/workspace.js +34 -14
- package/package.json +1 -1
- package/dist/self-training-workspace.js +0 -15
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, lstat, readdir, realpath, statfs } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import { generateStorageManifest, persistStorageManifestChunks, verifyStorageManifestChunkStore, parseStorageManifest, storageManifestDigest, } from "./storage-manifest.js";
|
|
5
|
+
import { validateStorageEnvironment } from "./storage-layout.js";
|
|
6
|
+
export class StorageDoctorAbortError extends Error {
|
|
7
|
+
code = "preflight_aborted";
|
|
8
|
+
scannedEntries;
|
|
9
|
+
constructor(scannedEntries, cause) {
|
|
10
|
+
super("storage doctor preflight was aborted", { cause });
|
|
11
|
+
this.name = "StorageDoctorAbortError";
|
|
12
|
+
this.scannedEntries = scannedEntries;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function discoverLegacyStorageSources(input) {
|
|
16
|
+
if (!isAbsolute(input.legacyRoot))
|
|
17
|
+
throw new Error("legacy root must be absolute");
|
|
18
|
+
for (const value of [input.daemonDirectory, input.agentsDirectory]) {
|
|
19
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,127}$/u.test(value))
|
|
20
|
+
throw new Error("legacy directory name is invalid");
|
|
21
|
+
}
|
|
22
|
+
return Object.freeze([
|
|
23
|
+
Object.freeze({ kind: "daemon-home", root: join(input.legacyRoot, input.daemonDirectory), logicalPrefix: "daemon" }),
|
|
24
|
+
Object.freeze({ kind: "agents-root", root: join(input.legacyRoot, input.agentsDirectory), logicalPrefix: "agent-items" }),
|
|
25
|
+
]);
|
|
26
|
+
}
|
|
27
|
+
async function nearestExistingAncestor(path) {
|
|
28
|
+
let candidate = path;
|
|
29
|
+
for (;;) {
|
|
30
|
+
try {
|
|
31
|
+
await lstat(candidate);
|
|
32
|
+
return candidate;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error.code !== "ENOENT")
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
const parent = dirname(candidate);
|
|
39
|
+
if (parent === candidate)
|
|
40
|
+
throw new Error("target storage has no existing ancestor");
|
|
41
|
+
candidate = parent;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function canonicalPotentialPath(path) {
|
|
45
|
+
const absolute = resolve(path);
|
|
46
|
+
const ancestor = await nearestExistingAncestor(absolute);
|
|
47
|
+
const canonicalAncestor = await realpath(ancestor);
|
|
48
|
+
return resolve(canonicalAncestor, relative(ancestor, absolute));
|
|
49
|
+
}
|
|
50
|
+
async function pathsOverlap(left, right) {
|
|
51
|
+
const [canonicalLeft, canonicalRight] = await Promise.all([
|
|
52
|
+
canonicalPotentialPath(left),
|
|
53
|
+
canonicalPotentialPath(right),
|
|
54
|
+
]);
|
|
55
|
+
const leftToRight = relative(canonicalLeft, canonicalRight);
|
|
56
|
+
const rightToLeft = relative(canonicalRight, canonicalLeft);
|
|
57
|
+
const contained = (value) => value === "" || (!value.startsWith("..") && !isAbsolute(value));
|
|
58
|
+
return contained(leftToRight) || contained(rightToLeft);
|
|
59
|
+
}
|
|
60
|
+
async function defaultProbeTarget(targetWorkspaceRoot) {
|
|
61
|
+
try {
|
|
62
|
+
const ancestor = await nearestExistingAncestor(targetWorkspaceRoot);
|
|
63
|
+
await access(ancestor, constants.R_OK | constants.W_OK);
|
|
64
|
+
let targetState = "absent";
|
|
65
|
+
try {
|
|
66
|
+
const target = await lstat(targetWorkspaceRoot);
|
|
67
|
+
targetState = target.isDirectory() && (await readdir(targetWorkspaceRoot)).length === 0 ? "empty" : "non_empty";
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error.code !== "ENOENT")
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
const filesystem = await statfs(ancestor, { bigint: true });
|
|
74
|
+
const available = filesystem.bavail * filesystem.bsize;
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
status: "available",
|
|
77
|
+
targetState,
|
|
78
|
+
availableBytes: available > BigInt(Number.MAX_SAFE_INTEGER) ? Number.MAX_SAFE_INTEGER : Number(available),
|
|
79
|
+
writable: true,
|
|
80
|
+
// Staging and final roots are siblings on this filesystem. A destructive rename probe belongs
|
|
81
|
+
// to the separately approved write preflight, not this read-only doctor.
|
|
82
|
+
atomicRenameSupported: true,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const code = error.code;
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
status: code === "EACCES" || code === "EPERM" ? "permission_denied" : "unavailable",
|
|
89
|
+
targetState: "absent",
|
|
90
|
+
availableBytes: 0,
|
|
91
|
+
writable: false,
|
|
92
|
+
atomicRenameSupported: false,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const issue = (code, detail) => Object.freeze({ code, detail });
|
|
97
|
+
function inspectManifestLinks(manifest) {
|
|
98
|
+
const warnings = [];
|
|
99
|
+
const blockers = [];
|
|
100
|
+
const credentialLinks = manifest.entries.filter((entry) => entry.link?.classification === "external" && entry.reason === "credential_material").length;
|
|
101
|
+
const unapprovedLinks = manifest.entries.filter((entry) => entry.link?.classification === "external" && entry.reason !== "credential_material" && entry.reason !== "local_runtime").length;
|
|
102
|
+
const brokenLinks = manifest.entries.filter((entry) => entry.link?.classification === "broken").length;
|
|
103
|
+
const unsupportedItems = manifest.entries.filter((entry) => entry.type === "unsupported").length;
|
|
104
|
+
if (credentialLinks > 0)
|
|
105
|
+
warnings.push(issue("credential_link_excluded", `${credentialLinks} credential link(s) excluded from migration`));
|
|
106
|
+
if (unapprovedLinks > 0)
|
|
107
|
+
blockers.push(issue("external_symlink_unapproved", `${unapprovedLinks} external link(s) require an explicit exclusion or portable mapping`));
|
|
108
|
+
if (brokenLinks > 0)
|
|
109
|
+
blockers.push(issue("broken_symlink", `${brokenLinks} broken link(s) cannot be migrated safely`));
|
|
110
|
+
if (unsupportedItems > 0)
|
|
111
|
+
blockers.push(issue("unsupported_file_type", `${unsupportedItems} unsupported filesystem item(s) found`));
|
|
112
|
+
if (manifest.summary.transformations > 0) {
|
|
113
|
+
warnings.push(issue("target_transformations_required", `${manifest.summary.transformations} target-only transformation(s) must be verified during apply`));
|
|
114
|
+
}
|
|
115
|
+
return { warnings, blockers };
|
|
116
|
+
}
|
|
117
|
+
export async function runStorageDoctor(input, dependencies = {}) {
|
|
118
|
+
const environment = validateStorageEnvironment(input.environment);
|
|
119
|
+
let scannedEntries = 0;
|
|
120
|
+
const onProgress = (progress) => {
|
|
121
|
+
scannedEntries = progress.scannedEntries;
|
|
122
|
+
input.onProgress?.(progress);
|
|
123
|
+
};
|
|
124
|
+
if (input.signal?.aborted)
|
|
125
|
+
throw new StorageDoctorAbortError(scannedEntries);
|
|
126
|
+
const manifest = typeof input.manifest === "string"
|
|
127
|
+
? parseStorageManifest(input.manifest)
|
|
128
|
+
: input.manifest === undefined
|
|
129
|
+
? await (input.manifestStoreRoot === undefined
|
|
130
|
+
? generateStorageManifest({
|
|
131
|
+
migrationId: input.migrationId,
|
|
132
|
+
generatedAt: input.generatedAt,
|
|
133
|
+
environment,
|
|
134
|
+
workspaceId: input.workspaceId,
|
|
135
|
+
targetWorkspaceRoot: input.targetWorkspaceRoot,
|
|
136
|
+
sources: input.sources,
|
|
137
|
+
policy: input.policy,
|
|
138
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
139
|
+
onProgress,
|
|
140
|
+
...(input.progressIntervalEntries === undefined ? {} : { progressIntervalEntries: input.progressIntervalEntries }),
|
|
141
|
+
})
|
|
142
|
+
: persistStorageManifestChunks({
|
|
143
|
+
migrationId: input.migrationId,
|
|
144
|
+
generatedAt: input.generatedAt,
|
|
145
|
+
environment,
|
|
146
|
+
workspaceId: input.workspaceId,
|
|
147
|
+
targetWorkspaceRoot: input.targetWorkspaceRoot,
|
|
148
|
+
sources: input.sources,
|
|
149
|
+
policy: input.policy,
|
|
150
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
151
|
+
onProgress,
|
|
152
|
+
...(input.progressIntervalEntries === undefined ? {} : { progressIntervalEntries: input.progressIntervalEntries }),
|
|
153
|
+
}, { root: input.manifestStoreRoot })).catch((error) => {
|
|
154
|
+
if (error.code === "preflight_aborted") {
|
|
155
|
+
throw new StorageDoctorAbortError(scannedEntries, error);
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
})
|
|
159
|
+
: parseStorageManifest(JSON.stringify(input.manifest));
|
|
160
|
+
const target = await (dependencies.probeTarget ?? defaultProbeTarget)(input.targetWorkspaceRoot);
|
|
161
|
+
const warnings = [];
|
|
162
|
+
const blockers = [];
|
|
163
|
+
try {
|
|
164
|
+
await verifyStorageManifestChunkStore(manifest);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
blockers.push(issue("manifest_chunk_store_invalid", error instanceof Error ? error.message : "manifest chunk store is invalid"));
|
|
168
|
+
}
|
|
169
|
+
if (manifest.migrationId !== input.migrationId || manifest.workspaceId !== input.workspaceId.toLowerCase()
|
|
170
|
+
|| manifest.environment !== environment || manifest.targetWorkspaceRoot !== input.targetWorkspaceRoot) {
|
|
171
|
+
blockers.push(issue("manifest_identity_mismatch", "manifest identity does not match the requested migration"));
|
|
172
|
+
}
|
|
173
|
+
if (!input.state.profileIdentityExact)
|
|
174
|
+
blockers.push(issue("profile_identity_drift", "profile, service or managed registry identity drifted"));
|
|
175
|
+
if ((await Promise.all(input.sources.map((source) => pathsOverlap(source.root, input.targetWorkspaceRoot))))
|
|
176
|
+
.some(Boolean)) {
|
|
177
|
+
blockers.push(issue("source_target_overlap", "target workspace and source roots must not contain one another"));
|
|
178
|
+
}
|
|
179
|
+
if (input.state.duplicateWorkspaceMappings > 0)
|
|
180
|
+
blockers.push(issue("duplicate_workspace_mapping", "more than one profile maps to this workspace"));
|
|
181
|
+
if (input.state.activeAgentExecutions > 0 || input.state.activeScheduledRuns > 0) {
|
|
182
|
+
blockers.push(issue("active_execution", "active agent execution or scheduled run must finish before migration"));
|
|
183
|
+
}
|
|
184
|
+
if (target.status === "unavailable")
|
|
185
|
+
blockers.push(issue("target_unavailable", "target storage is unavailable"));
|
|
186
|
+
if (target.status === "permission_denied" || !target.writable)
|
|
187
|
+
blockers.push(issue("target_permission_denied", "target storage is not writable by the daemon user"));
|
|
188
|
+
if (target.targetState === "non_empty")
|
|
189
|
+
blockers.push(issue("target_non_empty", "target workspace root is not empty"));
|
|
190
|
+
const multiplier = input.freeSpaceMultiplier ?? 1.2;
|
|
191
|
+
if (!Number.isFinite(multiplier) || multiplier < 1)
|
|
192
|
+
throw new Error("free space multiplier must be at least 1");
|
|
193
|
+
const requiredBytes = Math.ceil(manifest.summary.bytesToCopy * multiplier);
|
|
194
|
+
if (target.status === "available" && target.availableBytes < requiredBytes) {
|
|
195
|
+
blockers.push(issue("insufficient_space", `target requires at least ${requiredBytes} available bytes`));
|
|
196
|
+
}
|
|
197
|
+
if (!target.atomicRenameSupported)
|
|
198
|
+
blockers.push(issue("atomic_rename_unsupported", "target filesystem rename semantics are not verified"));
|
|
199
|
+
const links = inspectManifestLinks(manifest);
|
|
200
|
+
warnings.push(...links.warnings);
|
|
201
|
+
blockers.push(...links.blockers);
|
|
202
|
+
if (manifest.chunkStore !== undefined) {
|
|
203
|
+
if (manifest.chunkStore.credentialLinks > 0)
|
|
204
|
+
warnings.push(issue("credential_link_excluded", `${manifest.chunkStore.credentialLinks} credential link(s) excluded from migration`));
|
|
205
|
+
if (manifest.chunkStore.unapprovedLinks > 0)
|
|
206
|
+
blockers.push(issue("external_symlink_unapproved", `${manifest.chunkStore.unapprovedLinks} external link(s) require an explicit exclusion or portable mapping`));
|
|
207
|
+
if (manifest.chunkStore.brokenLinks > 0)
|
|
208
|
+
blockers.push(issue("broken_symlink", `${manifest.chunkStore.brokenLinks} broken link(s) cannot be migrated safely`));
|
|
209
|
+
if (manifest.chunkStore.unsupportedItems > 0)
|
|
210
|
+
blockers.push(issue("unsupported_file_type", `${manifest.chunkStore.unsupportedItems} unsupported filesystem item(s) found`));
|
|
211
|
+
}
|
|
212
|
+
const applyAllowed = blockers.length === 0;
|
|
213
|
+
const status = !applyAllowed ? "blocked" : warnings.length > 0 ? "warnings" : "safe";
|
|
214
|
+
const exitCode = status === "safe" ? 0 : status === "warnings" ? 2 : 3;
|
|
215
|
+
return Object.freeze({
|
|
216
|
+
status,
|
|
217
|
+
exitCode,
|
|
218
|
+
applyAllowed,
|
|
219
|
+
environment,
|
|
220
|
+
workspaceId: input.workspaceId.toLowerCase(),
|
|
221
|
+
profile: input.profile,
|
|
222
|
+
manifest,
|
|
223
|
+
manifestDigest: storageManifestDigest(manifest),
|
|
224
|
+
target,
|
|
225
|
+
warnings: Object.freeze(warnings),
|
|
226
|
+
blockers: Object.freeze(blockers),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { STORAGE_LAYOUT_VERSION } from "./storage-layout.js";
|
|
3
|
+
function fingerprint(namespace, value) {
|
|
4
|
+
return `sha256:${createHash("sha256").update(`${namespace}\0${value}`).digest("hex")}`;
|
|
5
|
+
}
|
|
6
|
+
export function storageIdFingerprint(storageId) {
|
|
7
|
+
return fingerprint("storage", storageId);
|
|
8
|
+
}
|
|
9
|
+
export function storageHelloIdentity(input) {
|
|
10
|
+
const { installation, layout } = input;
|
|
11
|
+
if (installation.environment !== layout.environment
|
|
12
|
+
|| installation.workspaceId !== layout.workspaceId
|
|
13
|
+
|| installation.nowworkHome !== layout.persistentRoot
|
|
14
|
+
|| installation.runtimeRoot !== layout.runtimeRoot) {
|
|
15
|
+
throw new Error("installation storage identity does not match resolved layout");
|
|
16
|
+
}
|
|
17
|
+
return Object.freeze({
|
|
18
|
+
storageLayoutVersion: STORAGE_LAYOUT_VERSION,
|
|
19
|
+
storageEnvironment: layout.environment,
|
|
20
|
+
storageWorkspaceId: layout.workspaceId,
|
|
21
|
+
storageIdFingerprint: storageIdFingerprint(installation.storageId),
|
|
22
|
+
workspaceRootFingerprint: fingerprint("workspace-root", layout.workspaceRoot),
|
|
23
|
+
daemonRootFingerprint: fingerprint("daemon-root", layout.daemonRoot),
|
|
24
|
+
agentRootFingerprint: fingerprint("agent-root", layout.agentItemsRoot),
|
|
25
|
+
...(installation.storageMigrationId === undefined
|
|
26
|
+
? {}
|
|
27
|
+
: { storageMigrationId: installation.storageMigrationId }),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { posix, win32 } from "node:path";
|
|
3
|
+
export const STORAGE_LAYOUT_VERSION = 2;
|
|
4
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
5
|
+
const ENVIRONMENT = /^[a-z0-9][a-z0-9_-]{0,31}$/u;
|
|
6
|
+
const CONTROL = /[\0\r\n]/u;
|
|
7
|
+
const pathsFor = (platform) => platform === "win32" ? win32 : posix;
|
|
8
|
+
function canonicalExistingAncestor(value, platform) {
|
|
9
|
+
if (platform !== process.platform)
|
|
10
|
+
return value;
|
|
11
|
+
const paths = pathsFor(platform);
|
|
12
|
+
const missing = [];
|
|
13
|
+
let ancestor = value;
|
|
14
|
+
for (;;) {
|
|
15
|
+
try {
|
|
16
|
+
return paths.join(realpathSync.native(ancestor), ...missing);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
const code = error.code;
|
|
20
|
+
if (code !== "ENOENT" && code !== "ENOTDIR")
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
const parent = paths.dirname(ancestor);
|
|
24
|
+
if (parent === ancestor)
|
|
25
|
+
return value;
|
|
26
|
+
missing.unshift(paths.basename(ancestor));
|
|
27
|
+
ancestor = parent;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function assertSafeText(value, label) {
|
|
31
|
+
if (value.length === 0 || CONTROL.test(value))
|
|
32
|
+
throw new Error(`${label} contains empty or control characters`);
|
|
33
|
+
}
|
|
34
|
+
function normalizedAbsolutePath(value, userHome, platform) {
|
|
35
|
+
assertSafeText(value, "NOWWORK_HOME");
|
|
36
|
+
const paths = pathsFor(platform);
|
|
37
|
+
let expanded = value;
|
|
38
|
+
if (value === "~") {
|
|
39
|
+
if (userHome === undefined)
|
|
40
|
+
throw new Error("NOWWORK_HOME '~' requires user home");
|
|
41
|
+
expanded = userHome;
|
|
42
|
+
}
|
|
43
|
+
else if (value.startsWith("~/") || (platform === "win32" && value.startsWith("~\\"))) {
|
|
44
|
+
if (userHome === undefined)
|
|
45
|
+
throw new Error("NOWWORK_HOME '~' requires user home");
|
|
46
|
+
expanded = paths.join(userHome, value.slice(2));
|
|
47
|
+
}
|
|
48
|
+
if (!paths.isAbsolute(expanded))
|
|
49
|
+
throw new Error("NOWWORK_HOME must be absolute or start with '~/'");
|
|
50
|
+
return canonicalExistingAncestor(paths.normalize(expanded), platform);
|
|
51
|
+
}
|
|
52
|
+
export function validateStorageEnvironment(value) {
|
|
53
|
+
assertSafeText(value, "storage environment");
|
|
54
|
+
const normalized = value.trim().toLowerCase();
|
|
55
|
+
if (normalized === "in" || normalized === "production")
|
|
56
|
+
return "production";
|
|
57
|
+
if (!ENVIRONMENT.test(normalized) || normalized.includes("..")) {
|
|
58
|
+
throw new Error("storage environment must use lowercase letters, digits, '_' or '-'");
|
|
59
|
+
}
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
function validateOpaqueId(value, label) {
|
|
63
|
+
if (!UUID.test(value))
|
|
64
|
+
throw new Error(`${label} id must be a UUID`);
|
|
65
|
+
return value.toLowerCase();
|
|
66
|
+
}
|
|
67
|
+
function assertRuntimeIsLocal(persistentRoot, runtimeRoot, platform) {
|
|
68
|
+
const paths = pathsFor(platform);
|
|
69
|
+
const relative = paths.relative(persistentRoot, runtimeRoot);
|
|
70
|
+
if (relative === "" || (!relative.startsWith("..") && !paths.isAbsolute(relative))) {
|
|
71
|
+
throw new Error("runtime root must not be inside NOWWORK_HOME");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function resolveNowworkHome(input) {
|
|
75
|
+
const selected = input.explicit !== undefined
|
|
76
|
+
? { value: input.explicit, source: "explicit" }
|
|
77
|
+
: input.environment !== undefined
|
|
78
|
+
? { value: input.environment, source: "environment" }
|
|
79
|
+
: null;
|
|
80
|
+
if (selected !== null) {
|
|
81
|
+
return Object.freeze({
|
|
82
|
+
mode: "standard",
|
|
83
|
+
root: normalizedAbsolutePath(selected.value, input.userHome, input.platform),
|
|
84
|
+
source: selected.source,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
mode: "legacy",
|
|
89
|
+
root: normalizedAbsolutePath(pathsFor(input.platform).join(input.userHome, ".crew"), undefined, input.platform),
|
|
90
|
+
source: "legacy_default",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
export function resolveEnvTag(input) {
|
|
94
|
+
if (input.explicit !== undefined) {
|
|
95
|
+
return Object.freeze({ environment: validateStorageEnvironment(input.explicit), source: "explicit" });
|
|
96
|
+
}
|
|
97
|
+
if (input.environment !== undefined) {
|
|
98
|
+
return Object.freeze({ environment: validateStorageEnvironment(input.environment), source: "environment" });
|
|
99
|
+
}
|
|
100
|
+
return Object.freeze({ environment: "production", source: "default" });
|
|
101
|
+
}
|
|
102
|
+
export function storageLayout(input) {
|
|
103
|
+
const paths = pathsFor(input.platform);
|
|
104
|
+
const persistentRoot = normalizedAbsolutePath(input.nowworkHome, undefined, input.platform);
|
|
105
|
+
const runtimeBase = normalizedAbsolutePath(input.runtimeBase, undefined, input.platform);
|
|
106
|
+
const environment = validateStorageEnvironment(input.environment);
|
|
107
|
+
const workspaceId = validateOpaqueId(input.workspaceId, "workspace");
|
|
108
|
+
const environmentRoot = environment === "production"
|
|
109
|
+
? persistentRoot
|
|
110
|
+
: paths.join(persistentRoot, "environments", environment);
|
|
111
|
+
const workspaceRoot = paths.join(environmentRoot, "workspaces", workspaceId);
|
|
112
|
+
const runtimeRoot = paths.join(runtimeBase, environment, workspaceId);
|
|
113
|
+
assertRuntimeIsLocal(persistentRoot, runtimeRoot, input.platform);
|
|
114
|
+
return Object.freeze({
|
|
115
|
+
layoutVersion: STORAGE_LAYOUT_VERSION,
|
|
116
|
+
environment,
|
|
117
|
+
workspaceId,
|
|
118
|
+
persistentRoot,
|
|
119
|
+
environmentRoot,
|
|
120
|
+
workspaceRoot,
|
|
121
|
+
agentItemsRoot: paths.join(workspaceRoot, "agent-items"),
|
|
122
|
+
daemonRoot: paths.join(workspaceRoot, "daemon"),
|
|
123
|
+
repoRoot: paths.join(workspaceRoot, "repos"),
|
|
124
|
+
suiteToolsRoot: paths.join(workspaceRoot, "suite-tools"),
|
|
125
|
+
migrationsRoot: paths.join(persistentRoot, "migrations"),
|
|
126
|
+
runtimeRoot,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
export function agentStorageRoot(layout, agentId) {
|
|
130
|
+
const platform = layout.workspaceRoot.includes("\\") ? "win32" : "linux";
|
|
131
|
+
return pathsFor(platform).join(layout.agentItemsRoot, validateOpaqueId(agentId, "agent"));
|
|
132
|
+
}
|
|
133
|
+
const COMPARABLE_FIELDS = [
|
|
134
|
+
"layoutVersion", "environment", "workspaceId", "persistentRoot", "environmentRoot", "workspaceRoot",
|
|
135
|
+
"agentItemsRoot", "daemonRoot", "repoRoot", "suiteToolsRoot", "migrationsRoot", "runtimeRoot",
|
|
136
|
+
];
|
|
137
|
+
export function compareStorageLayout(current, expected) {
|
|
138
|
+
if (current === null || current.layoutVersion !== STORAGE_LAYOUT_VERSION)
|
|
139
|
+
return "unknown";
|
|
140
|
+
return COMPARABLE_FIELDS.every((field) => current[field] === expected[field])
|
|
141
|
+
? "standard"
|
|
142
|
+
: "migration_required";
|
|
143
|
+
}
|