@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,284 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join, posix, win32 } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { atomicPrivateWrite } from "./atomic-private-write.js";
|
|
7
|
+
import { assertStorageControlMaterialTokenFree } from "./storage-credential-guard.js";
|
|
8
|
+
import { storageLayout } from "./storage-layout.js";
|
|
9
|
+
import { StorageMigrationWarningCodeSchema } from "./storage-migration-policy.js";
|
|
10
|
+
const CONTROL = /[\0\r\n]/u;
|
|
11
|
+
const PROFILE = /^[a-z0-9][a-z0-9_-]{0,47}$/u;
|
|
12
|
+
const ENVIRONMENT = /^[a-z0-9][a-z0-9_-]{0,31}$/u;
|
|
13
|
+
const HASH = /^[0-9a-f]{64}$/u;
|
|
14
|
+
const AbsolutePathSchema = z.string().min(1).max(32768)
|
|
15
|
+
.refine((value) => !CONTROL.test(value), "path must be a single line")
|
|
16
|
+
.refine((value) => posix.isAbsolute(value) || win32.isAbsolute(value), "path must be absolute");
|
|
17
|
+
const RelativePathSchema = z.string().min(1).max(32768)
|
|
18
|
+
.refine((value) => !CONTROL.test(value), "path must be a single line")
|
|
19
|
+
.refine((value) => !posix.isAbsolute(value) && !win32.isAbsolute(value), "path must be relative")
|
|
20
|
+
.refine((value) => !value.split(/[\\/]/u).some((segment) => segment === "" || segment === "." || segment === ".."), "path must be normalized");
|
|
21
|
+
const SourceKindSchema = z.enum(["daemon-home", "agents-root", "legacy-agents-root"]);
|
|
22
|
+
const PolicySchema = z.object({
|
|
23
|
+
exclusions: z.array(z.object({
|
|
24
|
+
sourceKind: SourceKindSchema,
|
|
25
|
+
sourcePath: RelativePathSchema,
|
|
26
|
+
reason: z.string().min(1).max(256),
|
|
27
|
+
}).strict()).max(100_000),
|
|
28
|
+
transformations: z.array(z.object({
|
|
29
|
+
sourceKind: SourceKindSchema,
|
|
30
|
+
sourcePath: RelativePathSchema,
|
|
31
|
+
logicalDestination: RelativePathSchema,
|
|
32
|
+
sourceSha256: z.string().regex(HASH),
|
|
33
|
+
targetSha256: z.string().regex(HASH),
|
|
34
|
+
targetMode: z.string().regex(/^[0-7]{4}$/u),
|
|
35
|
+
transform: z.literal("redact_live_like_tokens_v1"),
|
|
36
|
+
}).strict()).max(100_000),
|
|
37
|
+
}).strict();
|
|
38
|
+
const DoctorInputSchema = z.object({
|
|
39
|
+
migrationId: z.string().uuid(),
|
|
40
|
+
generatedAt: z.string().datetime({ offset: true }),
|
|
41
|
+
environment: z.string().regex(ENVIRONMENT),
|
|
42
|
+
workspaceId: z.string().uuid(),
|
|
43
|
+
profile: z.string().regex(PROFILE),
|
|
44
|
+
targetWorkspaceRoot: AbsolutePathSchema,
|
|
45
|
+
sources: z.array(z.object({
|
|
46
|
+
kind: SourceKindSchema,
|
|
47
|
+
root: AbsolutePathSchema,
|
|
48
|
+
logicalPrefix: RelativePathSchema,
|
|
49
|
+
}).strict()).min(1).max(3),
|
|
50
|
+
policy: PolicySchema,
|
|
51
|
+
manifestStoreRoot: AbsolutePathSchema.optional(),
|
|
52
|
+
state: z.object({
|
|
53
|
+
profileIdentityExact: z.boolean(),
|
|
54
|
+
duplicateWorkspaceMappings: z.number().int().nonnegative(),
|
|
55
|
+
activeAgentExecutions: z.number().int().nonnegative(),
|
|
56
|
+
activeScheduledRuns: z.number().int().nonnegative(),
|
|
57
|
+
}).strict(),
|
|
58
|
+
freeSpaceMultiplier: z.number().finite().min(1).max(10).optional(),
|
|
59
|
+
}).strict();
|
|
60
|
+
const ManagedServiceRecordSchema = z.object({
|
|
61
|
+
version: z.literal(1),
|
|
62
|
+
generation: z.string().uuid(),
|
|
63
|
+
platform: z.enum(["darwin", "linux"]),
|
|
64
|
+
serviceId: z.string().min(1).max(256),
|
|
65
|
+
profile: z.string().regex(PROFILE),
|
|
66
|
+
daemonHome: AbsolutePathSchema,
|
|
67
|
+
agentsRoot: AbsolutePathSchema,
|
|
68
|
+
nodePath: AbsolutePathSchema,
|
|
69
|
+
entryPath: AbsolutePathSchema,
|
|
70
|
+
packageRoot: AbsolutePathSchema,
|
|
71
|
+
npmPrefix: AbsolutePathSchema,
|
|
72
|
+
descriptorPath: AbsolutePathSchema,
|
|
73
|
+
descriptorSha256: z.string().regex(HASH),
|
|
74
|
+
createdAt: z.string().datetime({ offset: true }),
|
|
75
|
+
}).strict();
|
|
76
|
+
const InstallationRecordSchema = z.object({
|
|
77
|
+
version: z.literal(1),
|
|
78
|
+
installationId: z.string().uuid(),
|
|
79
|
+
profile: z.string().regex(PROFILE),
|
|
80
|
+
serverUrl: z.string().url().max(2048).refine((value) => {
|
|
81
|
+
const url = new URL(value);
|
|
82
|
+
return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password;
|
|
83
|
+
}, "serverUrl must be HTTP(S) without credentials"),
|
|
84
|
+
environment: z.string().regex(ENVIRONMENT),
|
|
85
|
+
workspaceId: z.string().uuid(),
|
|
86
|
+
storageId: z.string().min(1).max(128).regex(/^[A-Za-z0-9_-]+$/u),
|
|
87
|
+
nowworkHome: AbsolutePathSchema,
|
|
88
|
+
runtimeRoot: AbsolutePathSchema,
|
|
89
|
+
credentialRef: z.string().regex(/^profile:[a-z0-9][a-z0-9_-]{0,47}$/u),
|
|
90
|
+
storageMigrationId: z.string().uuid(),
|
|
91
|
+
createdAt: z.string().datetime({ offset: true }),
|
|
92
|
+
updatedAt: z.string().datetime({ offset: true }),
|
|
93
|
+
}).strict();
|
|
94
|
+
const StorageServiceEnvironmentSchema = z.object({
|
|
95
|
+
NOWWORK_HOME: AbsolutePathSchema.optional(),
|
|
96
|
+
NOWWORK_ENV_TAG: z.string().regex(ENVIRONMENT).optional(),
|
|
97
|
+
NOWWORK_STORAGE_POLICY_PATH: AbsolutePathSchema.optional(),
|
|
98
|
+
}).strict();
|
|
99
|
+
const OperationSchema = z.object({
|
|
100
|
+
version: z.literal(1),
|
|
101
|
+
migrationId: z.string().uuid(),
|
|
102
|
+
createdAt: z.string().datetime({ offset: true }),
|
|
103
|
+
environment: z.string().regex(ENVIRONMENT),
|
|
104
|
+
workspaceId: z.string().uuid(),
|
|
105
|
+
profile: z.string().regex(PROFILE),
|
|
106
|
+
expectedManifestDigest: z.string().regex(HASH),
|
|
107
|
+
doctorInput: DoctorInputSchema,
|
|
108
|
+
migration: z.object({
|
|
109
|
+
sourceRoots: z.object({
|
|
110
|
+
"daemon-home": AbsolutePathSchema.optional(),
|
|
111
|
+
"agents-root": AbsolutePathSchema.optional(),
|
|
112
|
+
"legacy-agents-root": AbsolutePathSchema.optional(),
|
|
113
|
+
}).strict(),
|
|
114
|
+
stagingWorkspaceRoot: AbsolutePathSchema,
|
|
115
|
+
targetWorkspaceRoot: AbsolutePathSchema,
|
|
116
|
+
backupsRoot: AbsolutePathSchema,
|
|
117
|
+
backupArtifacts: z.array(z.object({
|
|
118
|
+
name: z.string().regex(/^[a-z0-9][a-z0-9_.-]{0,127}$/u),
|
|
119
|
+
path: AbsolutePathSchema,
|
|
120
|
+
}).strict()).max(32),
|
|
121
|
+
acceptedWarnings: z.array(StorageMigrationWarningCodeSchema)
|
|
122
|
+
.max(StorageMigrationWarningCodeSchema.options.length),
|
|
123
|
+
}).strict(),
|
|
124
|
+
service: z.object({
|
|
125
|
+
platform: z.enum(["darwin", "linux"]),
|
|
126
|
+
userHome: AbsolutePathSchema,
|
|
127
|
+
uid: z.number().int().nonnegative().optional(),
|
|
128
|
+
serviceEnvironment: StorageServiceEnvironmentSchema.optional(),
|
|
129
|
+
sourceWasRunning: z.boolean(),
|
|
130
|
+
sourceRecord: ManagedServiceRecordSchema,
|
|
131
|
+
targetRecord: ManagedServiceRecordSchema,
|
|
132
|
+
}).strict(),
|
|
133
|
+
installation: z.object({
|
|
134
|
+
configRoot: AbsolutePathSchema,
|
|
135
|
+
target: InstallationRecordSchema,
|
|
136
|
+
}).strict(),
|
|
137
|
+
}).strict();
|
|
138
|
+
const pathsFor = (_platform) => posix;
|
|
139
|
+
function canonicalPath(value, platform) {
|
|
140
|
+
const paths = pathsFor(platform);
|
|
141
|
+
const absolute = paths.resolve(value);
|
|
142
|
+
try {
|
|
143
|
+
return realpathSync.native(absolute);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
const code = error.code;
|
|
147
|
+
if (code === "ENOTDIR")
|
|
148
|
+
return absolute;
|
|
149
|
+
if (code !== "ENOENT")
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
const missing = [];
|
|
153
|
+
let ancestor = absolute;
|
|
154
|
+
for (;;) {
|
|
155
|
+
const parent = paths.dirname(ancestor);
|
|
156
|
+
if (parent === ancestor)
|
|
157
|
+
return absolute;
|
|
158
|
+
missing.unshift(paths.basename(ancestor));
|
|
159
|
+
ancestor = parent;
|
|
160
|
+
try {
|
|
161
|
+
return paths.resolve(realpathSync.native(ancestor), ...missing);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
const code = error.code;
|
|
165
|
+
if (code === "ENOTDIR")
|
|
166
|
+
return absolute;
|
|
167
|
+
if (code !== "ENOENT")
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function samePath(left, right, platform) {
|
|
173
|
+
return canonicalPath(left, platform) === canonicalPath(right, platform);
|
|
174
|
+
}
|
|
175
|
+
function isInside(root, candidate, platform) {
|
|
176
|
+
const paths = pathsFor(platform);
|
|
177
|
+
const child = paths.relative(canonicalPath(root, platform), canonicalPath(candidate, platform));
|
|
178
|
+
return child === "" || (!child.startsWith("..") && !paths.isAbsolute(child));
|
|
179
|
+
}
|
|
180
|
+
function assertSemanticIdentity(operation) {
|
|
181
|
+
const identity = [operation.doctorInput, operation.installation.target];
|
|
182
|
+
if (identity.some((value) => value.workspaceId !== operation.workspaceId
|
|
183
|
+
|| value.environment !== operation.environment || value.profile !== operation.profile)
|
|
184
|
+
|| operation.doctorInput.migrationId !== operation.migrationId
|
|
185
|
+
|| operation.installation.target.storageMigrationId !== operation.migrationId
|
|
186
|
+
|| operation.installation.target.credentialRef !== `profile:${operation.profile}`) {
|
|
187
|
+
throw new Error("storage worker operation identity does not match");
|
|
188
|
+
}
|
|
189
|
+
if (operation.doctorInput.state.profileIdentityExact !== true
|
|
190
|
+
|| operation.doctorInput.state.duplicateWorkspaceMappings !== 0
|
|
191
|
+
|| operation.doctorInput.state.activeAgentExecutions !== 0
|
|
192
|
+
|| operation.doctorInput.state.activeScheduledRuns !== 0) {
|
|
193
|
+
throw new Error("storage worker operation doctor state is not safe");
|
|
194
|
+
}
|
|
195
|
+
const { platform, sourceRecord, targetRecord } = operation.service;
|
|
196
|
+
if (sourceRecord.platform !== platform || targetRecord.platform !== platform
|
|
197
|
+
|| sourceRecord.profile !== operation.profile || targetRecord.profile !== operation.profile
|
|
198
|
+
|| sourceRecord.serviceId === targetRecord.serviceId
|
|
199
|
+
|| !samePath(sourceRecord.nodePath, targetRecord.nodePath, platform)
|
|
200
|
+
|| !samePath(sourceRecord.entryPath, targetRecord.entryPath, platform)
|
|
201
|
+
|| !samePath(sourceRecord.packageRoot, targetRecord.packageRoot, platform)
|
|
202
|
+
|| !samePath(sourceRecord.npmPrefix, targetRecord.npmPrefix, platform)
|
|
203
|
+
|| !samePath(targetRecord.daemonHome, operation.installation.configRoot, platform)) {
|
|
204
|
+
throw new Error("storage worker service identity does not match");
|
|
205
|
+
}
|
|
206
|
+
const sourceKinds = new Set();
|
|
207
|
+
for (const source of operation.doctorInput.sources) {
|
|
208
|
+
if (sourceKinds.has(source.kind) || !samePath(operation.migration.sourceRoots[source.kind] ?? "", source.root, platform)) {
|
|
209
|
+
throw new Error("storage worker source roots do not match doctor input");
|
|
210
|
+
}
|
|
211
|
+
sourceKinds.add(source.kind);
|
|
212
|
+
}
|
|
213
|
+
const configuredSourceKinds = Object.entries(operation.migration.sourceRoots)
|
|
214
|
+
.filter(([, value]) => value !== undefined)
|
|
215
|
+
.map(([kind]) => kind);
|
|
216
|
+
if (configuredSourceKinds.length !== sourceKinds.size || configuredSourceKinds.some((kind) => !sourceKinds.has(kind))
|
|
217
|
+
|| !samePath(operation.migration.sourceRoots["daemon-home"] ?? "", sourceRecord.daemonHome, platform)
|
|
218
|
+
|| !samePath(operation.migration.sourceRoots["agents-root"] ?? "", sourceRecord.agentsRoot, platform)) {
|
|
219
|
+
throw new Error("storage worker source service roots do not match");
|
|
220
|
+
}
|
|
221
|
+
const projected = storageLayout({
|
|
222
|
+
nowworkHome: operation.installation.target.nowworkHome,
|
|
223
|
+
environment: operation.environment,
|
|
224
|
+
workspaceId: operation.workspaceId,
|
|
225
|
+
runtimeBase: join(operation.service.userHome, ".nowwork-worker-runtime-validation"),
|
|
226
|
+
platform,
|
|
227
|
+
});
|
|
228
|
+
if (!samePath(projected.workspaceRoot, operation.migration.targetWorkspaceRoot, platform)
|
|
229
|
+
|| !samePath(projected.workspaceRoot, operation.doctorInput.targetWorkspaceRoot, platform)
|
|
230
|
+
|| !samePath(projected.agentItemsRoot, targetRecord.agentsRoot, platform)) {
|
|
231
|
+
throw new Error("storage worker target layout does not match installation identity");
|
|
232
|
+
}
|
|
233
|
+
if (dirname(operation.migration.stagingWorkspaceRoot) !== dirname(operation.migration.targetWorkspaceRoot)
|
|
234
|
+
|| !basename(operation.migration.stagingWorkspaceRoot).includes(operation.migrationId)) {
|
|
235
|
+
throw new Error("storage worker staging identity does not match target");
|
|
236
|
+
}
|
|
237
|
+
if (isInside(operation.installation.target.nowworkHome, operation.migration.backupsRoot, platform)
|
|
238
|
+
|| isInside(operation.installation.target.nowworkHome, operation.installation.configRoot, platform)) {
|
|
239
|
+
throw new Error("storage worker local state must remain outside NOWWORK_HOME");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function parseOperation(raw) {
|
|
243
|
+
const operation = OperationSchema.parse(raw);
|
|
244
|
+
assertSemanticIdentity(operation);
|
|
245
|
+
return operation;
|
|
246
|
+
}
|
|
247
|
+
function sha256(value) {
|
|
248
|
+
return createHash("sha256").update(value).digest("hex");
|
|
249
|
+
}
|
|
250
|
+
export async function writeStorageWorkerOperation(raw, input) {
|
|
251
|
+
try {
|
|
252
|
+
const operation = parseOperation(raw);
|
|
253
|
+
const stateRoot = AbsolutePathSchema.parse(input.stateRoot);
|
|
254
|
+
const path = join(stateRoot, "migrations", operation.migrationId, "operation.json");
|
|
255
|
+
const serialized = `${JSON.stringify(operation, null, 2)}\n`;
|
|
256
|
+
assertStorageControlMaterialTokenFree(serialized, "storage worker operation");
|
|
257
|
+
await atomicPrivateWrite(path, serialized);
|
|
258
|
+
return Object.freeze({ path, sha256: sha256(serialized) });
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
throw new Error("Invalid storage worker operation", { cause: error });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
export async function readStorageWorkerOperation(path, expectedSha256) {
|
|
265
|
+
try {
|
|
266
|
+
AbsolutePathSchema.parse(path);
|
|
267
|
+
z.string().regex(HASH).parse(expectedSha256);
|
|
268
|
+
const raw = await readFile(path);
|
|
269
|
+
assertStorageControlMaterialTokenFree(raw.toString("utf8"), "storage worker operation");
|
|
270
|
+
const actual = Buffer.from(sha256(raw), "hex");
|
|
271
|
+
const expected = Buffer.from(expectedSha256, "hex");
|
|
272
|
+
if (!timingSafeEqual(actual, expected))
|
|
273
|
+
throw new Error("storage worker operation digest mismatch");
|
|
274
|
+
const operation = parseOperation(JSON.parse(raw.toString("utf8")));
|
|
275
|
+
if (basename(path) !== "operation.json" || basename(dirname(path)) !== operation.migrationId
|
|
276
|
+
|| basename(dirname(dirname(path))) !== "migrations") {
|
|
277
|
+
throw new Error("storage worker operation path does not match migration identity");
|
|
278
|
+
}
|
|
279
|
+
return operation;
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
throw new Error("Invalid storage worker operation", { cause: error });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { readFile, unlink } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
3
|
+
import { Worker } from "node:worker_threads";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { atomicPrivateWrite } from "./atomic-private-write.js";
|
|
6
|
+
const WorkerFailureSchema = z.object({
|
|
7
|
+
version: z.literal(1),
|
|
8
|
+
migrationId: z.string().uuid(),
|
|
9
|
+
status: z.literal("failed"),
|
|
10
|
+
errorCode: z.string().regex(/^[a-z0-9._-]{1,128}$/u),
|
|
11
|
+
errorMessage: z.string().min(1).max(512),
|
|
12
|
+
at: z.string().datetime({ offset: true }),
|
|
13
|
+
}).strict();
|
|
14
|
+
const WorkerHeartbeatSchema = z.object({
|
|
15
|
+
version: z.literal(1),
|
|
16
|
+
migrationId: z.string().uuid(),
|
|
17
|
+
pid: z.number().int().positive(),
|
|
18
|
+
at: z.string().datetime({ offset: true }),
|
|
19
|
+
}).strict();
|
|
20
|
+
const TOKEN = /(?:\bsk_(?:machine|user|agent)_[A-Za-z0-9_-]{4,}\b|\bbearer\s+[^\s]+|(?:password|secret|token)\s*[:=]\s*[^\s]+)/giu;
|
|
21
|
+
const HEARTBEAT_WORKER_KIND = "nowwork-storage-worker-heartbeat-v1";
|
|
22
|
+
async function unlinkHeartbeat(path) {
|
|
23
|
+
await unlink(path).catch((error) => {
|
|
24
|
+
if (error.code !== "ENOENT")
|
|
25
|
+
throw error;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Keep the liveness writer in a worker with its own event loop. The source is
|
|
30
|
+
* intentionally plain JavaScript because a worker module cannot resolve the
|
|
31
|
+
* source tree's `.js` specifiers under tsx/Vitest; production builds and tests
|
|
32
|
+
* therefore exercise the same isolated execution boundary.
|
|
33
|
+
*/
|
|
34
|
+
const HEARTBEAT_WORKER_SOURCE = String.raw `
|
|
35
|
+
const { parentPort, workerData } = require("node:worker_threads");
|
|
36
|
+
const { mkdir, chmod, rename, rm, unlink, writeFile } = require("node:fs/promises");
|
|
37
|
+
const { dirname } = require("node:path");
|
|
38
|
+
const { randomUUID } = require("node:crypto");
|
|
39
|
+
|
|
40
|
+
const atomicPrivateWrite = async (path, content) => {
|
|
41
|
+
const directory = dirname(path);
|
|
42
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
43
|
+
await chmod(directory, 0o700);
|
|
44
|
+
const temporary = path + "." + randomUUID() + ".tmp";
|
|
45
|
+
try {
|
|
46
|
+
await writeFile(temporary, content, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
47
|
+
await rename(temporary, path);
|
|
48
|
+
await chmod(path, 0o600);
|
|
49
|
+
} finally {
|
|
50
|
+
await rm(temporary, { force: true });
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const removeHeartbeat = async () => {
|
|
54
|
+
try { await unlink(workerData.path); } catch (error) {
|
|
55
|
+
if (error && error.code !== "ENOENT") throw error;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
let pending = Promise.resolve();
|
|
59
|
+
let stopping = false;
|
|
60
|
+
let firstWrite = true;
|
|
61
|
+
const write = (requestedAt) => {
|
|
62
|
+
const initial = firstWrite;
|
|
63
|
+
firstWrite = false;
|
|
64
|
+
pending = pending.catch(() => undefined).then(() => atomicPrivateWrite(workerData.path, JSON.stringify({
|
|
65
|
+
version: 1,
|
|
66
|
+
migrationId: workerData.migrationId,
|
|
67
|
+
pid: process.pid,
|
|
68
|
+
at: initial && workerData.initialAt ? workerData.initialAt : requestedAt || new Date().toISOString(),
|
|
69
|
+
}, null, 2) + "\n"));
|
|
70
|
+
return pending;
|
|
71
|
+
};
|
|
72
|
+
const report = (message) => { try { parentPort.postMessage(message); } catch {} };
|
|
73
|
+
(async () => {
|
|
74
|
+
try { await write(); report({ type: "ready" }); }
|
|
75
|
+
catch (error) { report({ type: "error", message: error instanceof Error ? error.message : String(error) }); return; }
|
|
76
|
+
const timer = workerData.useParentClock ? null : setInterval(() => {
|
|
77
|
+
if (!stopping) void write().catch((error) => report({ type: "error", message: error instanceof Error ? error.message : String(error) }));
|
|
78
|
+
}, workerData.intervalMs);
|
|
79
|
+
timer?.unref?.();
|
|
80
|
+
parentPort.on("message", (message) => {
|
|
81
|
+
if (stopping || !message) return;
|
|
82
|
+
if (message.type === "tick") {
|
|
83
|
+
if (!workerData.useParentClock) return;
|
|
84
|
+
void write(message.at).catch((error) => report({ type: "error", message: error instanceof Error ? error.message : String(error) }));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (message.type !== "stop") return;
|
|
88
|
+
stopping = true;
|
|
89
|
+
if (timer !== null) clearInterval(timer);
|
|
90
|
+
void pending.then(async () => { await removeHeartbeat(); report({ type: "stopped" }); }, async () => {
|
|
91
|
+
await removeHeartbeat().catch(() => undefined); report({ type: "stopped" });
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
})();
|
|
95
|
+
`;
|
|
96
|
+
function identityFromHandoffPath(handoffPath) {
|
|
97
|
+
if (!isAbsolute(handoffPath) || basename(handoffPath) !== "handoff.json") {
|
|
98
|
+
throw new Error("storage worker handoff path is invalid");
|
|
99
|
+
}
|
|
100
|
+
const migrationDirectory = dirname(handoffPath);
|
|
101
|
+
const migrationsDirectory = dirname(migrationDirectory);
|
|
102
|
+
if (basename(migrationsDirectory) !== "migrations")
|
|
103
|
+
throw new Error("storage worker handoff path is invalid");
|
|
104
|
+
return {
|
|
105
|
+
stateRoot: dirname(migrationsDirectory),
|
|
106
|
+
migrationId: z.string().uuid().parse(basename(migrationDirectory)).toLowerCase(),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function storageWorkerFailurePath(stateRoot, migrationId) {
|
|
110
|
+
if (!isAbsolute(stateRoot))
|
|
111
|
+
throw new Error("storage worker state root must be absolute");
|
|
112
|
+
return join(stateRoot, "migrations", z.string().uuid().parse(migrationId), "worker-status.json");
|
|
113
|
+
}
|
|
114
|
+
export function storageWorkerHeartbeatPath(stateRoot, migrationId) {
|
|
115
|
+
if (!isAbsolute(stateRoot))
|
|
116
|
+
throw new Error("storage worker state root must be absolute");
|
|
117
|
+
return join(stateRoot, "migrations", z.string().uuid().parse(migrationId), "worker-heartbeat.json");
|
|
118
|
+
}
|
|
119
|
+
export async function startStorageWorkerHeartbeat(handoffPath, input = {}) {
|
|
120
|
+
const identity = identityFromHandoffPath(handoffPath);
|
|
121
|
+
const intervalMs = z.number().int().min(100).max(15_000).parse(input.intervalMs ?? 5_000);
|
|
122
|
+
const path = storageWorkerHeartbeatPath(identity.stateRoot, identity.migrationId);
|
|
123
|
+
const initialAt = input.now?.().toISOString();
|
|
124
|
+
// The worker owns heartbeat I/O so a synchronous CPU-heavy scan in the
|
|
125
|
+
// migration process cannot delay the monitor's 30-second freshness window.
|
|
126
|
+
const worker = new Worker(HEARTBEAT_WORKER_SOURCE, {
|
|
127
|
+
eval: true,
|
|
128
|
+
// `--input-type=module` is inherited by workers launched from an eval
|
|
129
|
+
// smoke test; strip it so the plain CommonJS worker source can use require.
|
|
130
|
+
execArgv: process.execArgv.filter((arg) => !arg.startsWith("--input-type=")),
|
|
131
|
+
workerData: {
|
|
132
|
+
kind: HEARTBEAT_WORKER_KIND,
|
|
133
|
+
path,
|
|
134
|
+
migrationId: identity.migrationId,
|
|
135
|
+
intervalMs,
|
|
136
|
+
useParentClock: input.now !== undefined,
|
|
137
|
+
...(initialAt === undefined ? {} : { initialAt }),
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
worker.unref();
|
|
141
|
+
let startupSettled = false;
|
|
142
|
+
let heartbeatError = null;
|
|
143
|
+
let stopResolve = null;
|
|
144
|
+
const onWorkerMessage = (message) => {
|
|
145
|
+
if (message.type === "error") {
|
|
146
|
+
heartbeatError = new Error(`storage worker heartbeat failed: ${message.message}`);
|
|
147
|
+
if (!startupSettled)
|
|
148
|
+
startupReject?.(heartbeatError);
|
|
149
|
+
}
|
|
150
|
+
else if (message.type === "stopped") {
|
|
151
|
+
stopResolve?.();
|
|
152
|
+
stopResolve = null;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
let startupReject = null;
|
|
156
|
+
worker.on("message", onWorkerMessage);
|
|
157
|
+
// Keep this listener for the entire worker lifetime. A heartbeat write may
|
|
158
|
+
// fail after startup; dropping the listener would turn that operational
|
|
159
|
+
// error into an unhandled Worker 'error' event and crash the parent.
|
|
160
|
+
const onWorkerError = (error) => {
|
|
161
|
+
heartbeatError = error;
|
|
162
|
+
if (!startupSettled)
|
|
163
|
+
startupReject?.(error);
|
|
164
|
+
};
|
|
165
|
+
worker.on("error", onWorkerError);
|
|
166
|
+
await new Promise((resolvePromise, reject) => {
|
|
167
|
+
startupReject = reject;
|
|
168
|
+
const onReady = (message) => {
|
|
169
|
+
if (message.type !== "ready")
|
|
170
|
+
return;
|
|
171
|
+
startupSettled = true;
|
|
172
|
+
worker.off("message", onReady);
|
|
173
|
+
resolvePromise();
|
|
174
|
+
};
|
|
175
|
+
worker.on("message", onReady);
|
|
176
|
+
}).catch(async (error) => {
|
|
177
|
+
startupSettled = true;
|
|
178
|
+
await worker.terminate().catch(() => undefined);
|
|
179
|
+
worker.off("message", onWorkerMessage);
|
|
180
|
+
worker.off("error", onWorkerError);
|
|
181
|
+
await unlinkHeartbeat(path).catch(() => undefined);
|
|
182
|
+
throw error;
|
|
183
|
+
});
|
|
184
|
+
const parentTimer = input.now === undefined ? null : setInterval(() => {
|
|
185
|
+
try {
|
|
186
|
+
worker.postMessage({ type: "tick", at: input.now().toISOString() });
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
heartbeatError = error instanceof Error ? error : new Error(String(error));
|
|
190
|
+
}
|
|
191
|
+
}, intervalMs);
|
|
192
|
+
parentTimer?.unref?.();
|
|
193
|
+
let stopped = false;
|
|
194
|
+
const stop = async () => {
|
|
195
|
+
if (stopped)
|
|
196
|
+
return;
|
|
197
|
+
stopped = true;
|
|
198
|
+
if (parentTimer !== null)
|
|
199
|
+
clearInterval(parentTimer);
|
|
200
|
+
await new Promise((resolvePromise) => {
|
|
201
|
+
const onExit = () => {
|
|
202
|
+
worker.off("exit", onExit);
|
|
203
|
+
stopResolve = null;
|
|
204
|
+
resolvePromise();
|
|
205
|
+
};
|
|
206
|
+
stopResolve = resolvePromise;
|
|
207
|
+
worker.once("exit", onExit);
|
|
208
|
+
worker.postMessage({ type: "stop" });
|
|
209
|
+
});
|
|
210
|
+
await worker.terminate().catch(() => undefined);
|
|
211
|
+
worker.off("message", onWorkerMessage);
|
|
212
|
+
worker.off("error", onWorkerError);
|
|
213
|
+
await unlinkHeartbeat(path);
|
|
214
|
+
};
|
|
215
|
+
Object.defineProperty(stop, "assertHealthy", {
|
|
216
|
+
enumerable: false,
|
|
217
|
+
value: () => {
|
|
218
|
+
if (heartbeatError !== null)
|
|
219
|
+
throw heartbeatError;
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
return stop;
|
|
223
|
+
}
|
|
224
|
+
export async function writeStorageWorkerFailureStatus(handoffPath, error) {
|
|
225
|
+
const identity = identityFromHandoffPath(handoffPath);
|
|
226
|
+
const rawCode = error instanceof Error && "code" in error ? String(error.code) : "worker_failed";
|
|
227
|
+
const errorCode = /^[a-z0-9._-]{1,128}$/u.test(rawCode) ? rawCode : "worker_failed";
|
|
228
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
229
|
+
const errorMessage = rawMessage.replaceAll(TOKEN, "<redacted>").slice(0, 512) || "storage worker failed";
|
|
230
|
+
const status = WorkerFailureSchema.parse({
|
|
231
|
+
version: 1,
|
|
232
|
+
migrationId: identity.migrationId,
|
|
233
|
+
status: "failed",
|
|
234
|
+
errorCode,
|
|
235
|
+
errorMessage,
|
|
236
|
+
at: new Date().toISOString(),
|
|
237
|
+
});
|
|
238
|
+
await atomicPrivateWrite(storageWorkerFailurePath(identity.stateRoot, identity.migrationId), `${JSON.stringify(status, null, 2)}\n`);
|
|
239
|
+
}
|
|
240
|
+
export async function readStorageWorkerFailureStatus(stateRoot, migrationId) {
|
|
241
|
+
try {
|
|
242
|
+
return WorkerFailureSchema.parse(JSON.parse(await readFile(storageWorkerFailurePath(stateRoot, migrationId), "utf8")));
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (error.code === "ENOENT")
|
|
246
|
+
return null;
|
|
247
|
+
throw new Error("Invalid storage worker failure status", { cause: error });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
export async function readStorageWorkerHeartbeat(stateRoot, migrationId) {
|
|
251
|
+
try {
|
|
252
|
+
return WorkerHeartbeatSchema.parse(JSON.parse(await readFile(storageWorkerHeartbeatPath(stateRoot, migrationId), "utf8")));
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (error.code === "ENOENT")
|
|
256
|
+
return null;
|
|
257
|
+
throw new Error("Invalid storage worker heartbeat", { cause: error });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -46,7 +46,6 @@ export function supervisorLaunch(request) {
|
|
|
46
46
|
effectivePermission: request.effectivePermission,
|
|
47
47
|
...(request.model === undefined ? {} : { model: request.model }),
|
|
48
48
|
...(request.reasoning === undefined ? {} : { reasoning: request.reasoning }),
|
|
49
|
-
...(request.networkAccess === undefined ? {} : { networkAccess: request.networkAccess }),
|
|
50
49
|
...(request.sessionId === undefined ? {} : { sessionId: request.sessionId }),
|
|
51
50
|
...(request.imagePaths === undefined ? {} : { imagePaths: request.imagePaths }),
|
|
52
51
|
...(request.agentRoot === undefined ? {} : {
|
|
@@ -59,6 +58,9 @@ export function supervisorLaunch(request) {
|
|
|
59
58
|
...(request.codexSkillRoots === undefined ? {} : {
|
|
60
59
|
skillRoots: request.codexSkillRoots,
|
|
61
60
|
}),
|
|
61
|
+
...(request.codexCollaborationTools === undefined ? {} : {
|
|
62
|
+
codexCollaborationTools: request.codexCollaborationTools,
|
|
63
|
+
}),
|
|
62
64
|
resume: request.resume,
|
|
63
65
|
}),
|
|
64
66
|
};
|
package/dist/workspace.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* spawn runtime 时把 <ws>/.crew 置于 PATH 最前,于是 agent 的 `crew ...` 命中 wrapper。
|
|
10
10
|
*/
|
|
11
|
-
import { mkdir, writeFile, readFile, chmod, access } from "node:fs/promises";
|
|
11
|
+
import { mkdir, writeFile, readFile, chmod, access, lstat } from "node:fs/promises";
|
|
12
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
14
|
import { dslog } from "./slog.js";
|
|
@@ -34,17 +34,37 @@ async function exists(p) {
|
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
async function hasLocalMemoryV2Marker(agentDir) {
|
|
38
|
+
const crewDir = join(agentDir, ".crew");
|
|
39
|
+
const memoryDir = join(crewDir, "memory");
|
|
40
|
+
const markerPath = join(agentDir, ".crew", "memory", "v2-enabled");
|
|
41
|
+
try {
|
|
42
|
+
const crewStat = await lstat(crewDir);
|
|
43
|
+
const memoryStat = await lstat(memoryDir);
|
|
44
|
+
if (!crewStat.isDirectory() || crewStat.isSymbolicLink()
|
|
45
|
+
|| !memoryStat.isDirectory() || memoryStat.isSymbolicLink())
|
|
46
|
+
return false;
|
|
47
|
+
const stat = await lstat(markerPath);
|
|
48
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
49
|
+
return false;
|
|
50
|
+
return (await readFile(markerPath, "utf8")) === "1\n";
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
37
56
|
export async function prepareWorkspace(input) {
|
|
38
57
|
const dir = join(input.agentsRoot, input.handle);
|
|
58
|
+
const agentHomePreexisted = await exists(dir);
|
|
39
59
|
const crewDir = join(dir, ".crew");
|
|
40
60
|
await mkdir(crewDir, { recursive: true });
|
|
41
61
|
// 预先创建明细目录,让收尾流程可以按功能创建 notes/<topic>.md。
|
|
42
|
-
|
|
43
|
-
await mkdir(join(dir, "notes"), { recursive: true });
|
|
62
|
+
await mkdir(join(dir, "notes"), { recursive: true });
|
|
44
63
|
// MEMORY.md:首次创建"持久身份 + 知识索引"骨架,之后由 agent 自己维护
|
|
45
64
|
const memoryPath = join(dir, "MEMORY.md");
|
|
46
|
-
const memorySeedCreated = !
|
|
47
|
-
|
|
65
|
+
const memorySeedCreated = !(await exists(memoryPath));
|
|
66
|
+
const localMemoryV2Initialized = await hasLocalMemoryV2Marker(dir);
|
|
67
|
+
if (memorySeedCreated) {
|
|
48
68
|
try {
|
|
49
69
|
await writeFile(memoryPath, memorySeed(input.handle, input.description), "utf8");
|
|
50
70
|
}
|
|
@@ -53,15 +73,13 @@ export async function prepareWorkspace(input) {
|
|
|
53
73
|
throw error;
|
|
54
74
|
}
|
|
55
75
|
}
|
|
56
|
-
let memory
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
throw error;
|
|
64
|
-
}
|
|
76
|
+
let memory;
|
|
77
|
+
try {
|
|
78
|
+
memory = await readFile(memoryPath, "utf8");
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
logMemoryPrepareFailure(input, "memory_read", error);
|
|
82
|
+
throw error;
|
|
65
83
|
}
|
|
66
84
|
// 系统提示词按 execution 隔离;省略 identity 的兼容调用每次使用随机路径。
|
|
67
85
|
const promptDir = join(crewDir, "prompts");
|
|
@@ -127,7 +145,9 @@ export async function prepareWorkspace(input) {
|
|
|
127
145
|
crewDir,
|
|
128
146
|
systemPromptPath,
|
|
129
147
|
memory,
|
|
148
|
+
agentHomePreexisted,
|
|
130
149
|
memorySeedCreated,
|
|
150
|
+
localMemoryV2Initialized,
|
|
131
151
|
homeDir,
|
|
132
152
|
runDir,
|
|
133
153
|
workLogPath,
|
package/package.json
CHANGED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { rm } from "node:fs/promises";
|
|
2
|
-
import { isAbsolute, relative, sep } from "node:path";
|
|
3
|
-
/** Delete only a concrete child of this Agent's tasks root; never follow an ambiguous target. */
|
|
4
|
-
export async function cleanupSelfTrainingRunDir(tasksRoot, runDir) {
|
|
5
|
-
const child = relative(tasksRoot, runDir);
|
|
6
|
-
if (child === "" || isAbsolute(child) || child === ".." || child.startsWith(`..${sep}`))
|
|
7
|
-
return;
|
|
8
|
-
try {
|
|
9
|
-
await rm(runDir, { recursive: true, force: true });
|
|
10
|
-
}
|
|
11
|
-
catch (error) {
|
|
12
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
13
|
-
process.stderr.write(`[execution] failed to remove self-training run dir ${runDir}: ${detail}\n`);
|
|
14
|
-
}
|
|
15
|
-
}
|