@nowcrew/daemon 0.6.0 → 0.6.1
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.
|
@@ -3,8 +3,8 @@ import { chmod, cp, lstat, mkdir, readFile, readdir, readlink, rename, rm, symli
|
|
|
3
3
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { parse } from "yaml";
|
|
5
5
|
import { dslog } from "../slog.js";
|
|
6
|
-
import { abilitySha256, abilityTreeDigest, collectAbilityFiles, safeAbilityName, } from "./resolver.js";
|
|
7
|
-
import { DaemonAbilityLockSchema, DaemonAbilityManifestSchema } from "./types.js";
|
|
6
|
+
import { abilitySha256, abilityTreeDigest, collectAbilityFiles, resolveAbilityRelease, safeAbilityName, } from "./resolver.js";
|
|
7
|
+
import { abilityWorkspaceProjectionPath, DaemonAbilityLockSchema, DaemonAbilityManifestSchema } from "./types.js";
|
|
8
8
|
const exists = (path) => lstat(path).then(() => true, () => false);
|
|
9
9
|
const repositoryKey = (url) => createHash("sha256").update(url).digest("hex");
|
|
10
10
|
const posixPath = (value) => value.split(sep).join("/");
|
|
@@ -48,18 +48,20 @@ const switchDirectory = async (target, staging, backup) => {
|
|
|
48
48
|
if (hadPrevious)
|
|
49
49
|
await rename(target, backup);
|
|
50
50
|
await rename(staging, target);
|
|
51
|
-
if (hadPrevious)
|
|
51
|
+
if (hadPrevious) {
|
|
52
|
+
await writableTree(backup);
|
|
52
53
|
await rm(backup, { recursive: true, force: true });
|
|
54
|
+
}
|
|
53
55
|
}
|
|
54
56
|
catch (error) {
|
|
55
57
|
if (await exists(target))
|
|
56
|
-
await
|
|
58
|
+
await removeManagedTree(target).catch(() => { });
|
|
57
59
|
if (hadPrevious && await exists(backup))
|
|
58
60
|
await rename(backup, target).catch(() => { });
|
|
59
61
|
throw error;
|
|
60
62
|
}
|
|
61
63
|
};
|
|
62
|
-
const copyExistingSkills = async (target, staging,
|
|
64
|
+
const copyExistingSkills = async (target, staging, managedRoots) => {
|
|
63
65
|
const names = new Set();
|
|
64
66
|
if (!await exists(target))
|
|
65
67
|
return names;
|
|
@@ -70,7 +72,7 @@ const copyExistingSkills = async (target, staging, managedReleaseRoot) => {
|
|
|
70
72
|
if (info.isSymbolicLink()) {
|
|
71
73
|
const link = await readlink(source);
|
|
72
74
|
const resolvedLink = resolve(dirname(source), link);
|
|
73
|
-
if (resolvedLink ===
|
|
75
|
+
if (managedRoots.some((root) => resolvedLink === root || resolvedLink.startsWith(`${root}${sep}`)))
|
|
74
76
|
continue;
|
|
75
77
|
await symlink(link, destination, "dir");
|
|
76
78
|
}
|
|
@@ -80,10 +82,10 @@ const copyExistingSkills = async (target, staging, managedReleaseRoot) => {
|
|
|
80
82
|
}
|
|
81
83
|
return names;
|
|
82
84
|
};
|
|
83
|
-
const projectNativeSkills = async (agentRoot,
|
|
85
|
+
const projectNativeSkills = async (agentRoot, trainingRoot, skills) => {
|
|
84
86
|
const nativeSkills = [];
|
|
85
87
|
for (const skill of skills) {
|
|
86
|
-
const source = join(
|
|
88
|
+
const source = join(trainingRoot, "skills", safeAbilityName(skill.name));
|
|
87
89
|
if (await exists(join(source, "SKILL.md"))) {
|
|
88
90
|
nativeSkills.push({ name: skill.name, source });
|
|
89
91
|
continue;
|
|
@@ -99,14 +101,14 @@ const projectNativeSkills = async (agentRoot, releaseRoot, skills) => {
|
|
|
99
101
|
join(agentRoot, ".agents", "skills"),
|
|
100
102
|
join(agentRoot, ".crew", "claude-skills", ".claude", "skills"),
|
|
101
103
|
];
|
|
102
|
-
const
|
|
104
|
+
const managedRoots = [trainingRoot, join(agentRoot, ".nowcrew", "ability", "releases")];
|
|
103
105
|
for (const target of targets) {
|
|
104
106
|
const staging = join(dirname(target), `.ability-skills-next-${id}`);
|
|
105
107
|
const backup = join(dirname(target), `.ability-skills-previous-${id}`);
|
|
106
108
|
await mkdir(dirname(target), { recursive: true });
|
|
107
109
|
await mkdir(staging, { mode: 0o700 });
|
|
108
110
|
try {
|
|
109
|
-
const names = await copyExistingSkills(target, staging,
|
|
111
|
+
const names = await copyExistingSkills(target, staging, managedRoots);
|
|
110
112
|
for (const skill of nativeSkills) {
|
|
111
113
|
if (names.has(skill.name))
|
|
112
114
|
throw new Error(`ability_skill_name_conflict:${skill.name}`);
|
|
@@ -192,24 +194,56 @@ const verifyCache = async (agentsRoot, ability) => {
|
|
|
192
194
|
if (ability.memory.reduce((bytes, memory) => bytes + Buffer.byteLength(memory.content, "utf8"), 0) > 64 * 1024) {
|
|
193
195
|
throw new Error("ability_memory_snapshot_size_exceeded");
|
|
194
196
|
}
|
|
195
|
-
return root;
|
|
197
|
+
return { root, manifest };
|
|
196
198
|
};
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
+
const layerRelativePath = (path, layer) => path.startsWith(`${layer}/`) ? path.slice(layer.length + 1) : path;
|
|
200
|
+
const readManagedCatalog = async (trainingRoot) => JSON.parse(await readFile(join(trainingRoot, "ACTIVE_RELEASE.json"), "utf8"));
|
|
201
|
+
const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
202
|
+
let verified;
|
|
203
|
+
try {
|
|
204
|
+
verified = await verifyCache(agentsRoot, ability);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (!(error instanceof Error) || error.message !== "ability_artifact_cache_missing")
|
|
208
|
+
throw error;
|
|
209
|
+
const resolved = await resolveAbilityRelease(agentsRoot, {
|
|
210
|
+
type: "agent:ability:resolve",
|
|
211
|
+
reqId: `workspace-apply-${ability.releaseId}`,
|
|
212
|
+
handle,
|
|
213
|
+
repositoryUrl: ability.repositoryUrl,
|
|
214
|
+
branchGlob: ability.branch,
|
|
215
|
+
desiredCommit: ability.rootCommit,
|
|
216
|
+
});
|
|
217
|
+
if (resolved.artifactDigest !== ability.artifactDigest || resolved.rootCommit !== ability.rootCommit) {
|
|
218
|
+
throw new Error("ability_workspace_projection_source_mismatch");
|
|
219
|
+
}
|
|
220
|
+
verified = await verifyCache(agentsRoot, ability);
|
|
221
|
+
}
|
|
222
|
+
const sourceRoot = verified.root;
|
|
199
223
|
const agentRoot = join(agentsRoot, handle);
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
224
|
+
const trainingRoot = join(agentRoot, "training");
|
|
225
|
+
const projectionDigest = abilitySha256(JSON.stringify({
|
|
226
|
+
artifactDigest: ability.artifactDigest,
|
|
227
|
+
memory: ability.memory,
|
|
228
|
+
workspace: ability.workspace,
|
|
229
|
+
}));
|
|
230
|
+
let previousCatalog = null;
|
|
231
|
+
if (await exists(trainingRoot)) {
|
|
232
|
+
previousCatalog = await readManagedCatalog(trainingRoot).catch(() => null);
|
|
233
|
+
if (previousCatalog?.managedBy !== "nowcrew-agent-training") {
|
|
234
|
+
throw new Error("ability_workspace_training_conflict");
|
|
235
|
+
}
|
|
236
|
+
if (previousCatalog.releaseId === ability.releaseId
|
|
237
|
+
&& previousCatalog.artifactDigest === ability.artifactDigest
|
|
238
|
+
&& previousCatalog.projectionDigest === projectionDigest)
|
|
239
|
+
return trainingRoot;
|
|
208
240
|
}
|
|
209
|
-
|
|
241
|
+
await mkdir(agentRoot, { recursive: true });
|
|
242
|
+
const staging = join(agentRoot, `.training-next-${ability.releaseId}-${randomUUID()}`);
|
|
243
|
+
const backup = join(agentRoot, `.training-previous-${ability.releaseId}-${randomUUID()}`);
|
|
210
244
|
await mkdir(staging, { recursive: true, mode: 0o700 });
|
|
211
245
|
try {
|
|
212
|
-
const instructionsTarget = join(staging, "instructions", ability.instructions.path);
|
|
246
|
+
const instructionsTarget = join(staging, "instructions", layerRelativePath(ability.instructions.path, "instructions"));
|
|
213
247
|
await mkdir(dirname(instructionsTarget), { recursive: true });
|
|
214
248
|
await cp(join(sourceRoot, ability.instructions.path), instructionsTarget, { preserveTimestamps: true });
|
|
215
249
|
for (const skill of ability.skills) {
|
|
@@ -223,105 +257,127 @@ const materializeRelease = async (agentsRoot, handle, ability) => {
|
|
|
223
257
|
await writeFile(target, memory.content, { encoding: "utf8", mode: 0o600 });
|
|
224
258
|
}
|
|
225
259
|
for (const asset of ability.workspace) {
|
|
226
|
-
const relativeTarget = asset.target
|
|
260
|
+
const relativeTarget = abilityWorkspaceProjectionPath(asset.target);
|
|
227
261
|
const target = join(staging, "workspace", relativeTarget);
|
|
228
262
|
await mkdir(dirname(target), { recursive: true });
|
|
229
|
-
|
|
263
|
+
const previous = join(trainingRoot, "workspace", relativeTarget);
|
|
264
|
+
const source = asset.mode === "managed-copy" && previousCatalog !== null && await exists(previous)
|
|
265
|
+
? previous
|
|
266
|
+
: join(sourceRoot, asset.source);
|
|
267
|
+
await cp(source, target, { recursive: true, preserveTimestamps: true });
|
|
268
|
+
}
|
|
269
|
+
const evalPaths = [verified.manifest.spec.evals.policy, ...verified.manifest.spec.evals.cases];
|
|
270
|
+
for (const path of evalPaths) {
|
|
271
|
+
const target = join(staging, "evals", layerRelativePath(path, "evals"));
|
|
272
|
+
await mkdir(dirname(target), { recursive: true });
|
|
273
|
+
await cp(join(sourceRoot, path), target, { recursive: true, preserveTimestamps: true });
|
|
230
274
|
}
|
|
231
|
-
await writeFile(join(staging, "
|
|
275
|
+
await writeFile(join(staging, "ACTIVE_RELEASE.json"), JSON.stringify({
|
|
276
|
+
managedBy: "nowcrew-agent-training",
|
|
232
277
|
releaseId: ability.releaseId,
|
|
233
278
|
rootCommit: ability.rootCommit,
|
|
234
279
|
artifactDigest: ability.artifactDigest,
|
|
235
|
-
|
|
280
|
+
projectionDigest,
|
|
281
|
+
instructions: `training/instructions/${layerRelativePath(ability.instructions.path, "instructions")}`,
|
|
236
282
|
skills: ability.skills.map((skill) => skill.name),
|
|
237
283
|
memory: ability.memory.map((memory) => ({ id: memory.id, layer: memory.layer })),
|
|
238
|
-
workspace: ability.workspace
|
|
239
|
-
|
|
284
|
+
workspace: ability.workspace.map((asset) => ({
|
|
285
|
+
...asset,
|
|
286
|
+
projectedPath: `training/workspace/${abilityWorkspaceProjectionPath(asset.target)}`,
|
|
287
|
+
})),
|
|
288
|
+
evals: evalPaths.map((path) => `training/evals/${layerRelativePath(path, "evals")}`),
|
|
240
289
|
}, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
241
290
|
await readonlyTree(staging);
|
|
242
|
-
|
|
291
|
+
for (const asset of ability.workspace) {
|
|
292
|
+
if (asset.mode === "managed-copy") {
|
|
293
|
+
await writableTree(join(staging, "workspace", abilityWorkspaceProjectionPath(asset.target)));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
await switchDirectory(trainingRoot, staging, backup);
|
|
243
297
|
}
|
|
244
298
|
catch (error) {
|
|
245
299
|
await removeManagedTree(staging).catch(() => { });
|
|
246
300
|
throw error;
|
|
247
301
|
}
|
|
248
|
-
return
|
|
302
|
+
return trainingRoot;
|
|
249
303
|
};
|
|
250
|
-
const
|
|
304
|
+
const projectWorkspaceTraining = async (agentsRoot, handle, ability, trainingRoot) => {
|
|
251
305
|
const agentRoot = join(agentsRoot, handle);
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
await symlink(releaseRoot, next, "dir");
|
|
257
|
-
const current = join(abilityRoot, "current");
|
|
258
|
-
const hadCurrent = await exists(current);
|
|
259
|
-
if (hadCurrent)
|
|
260
|
-
await rename(current, previous);
|
|
261
|
-
try {
|
|
262
|
-
await rename(next, current);
|
|
263
|
-
if (hadCurrent)
|
|
264
|
-
await rm(previous, { recursive: true, force: true });
|
|
265
|
-
}
|
|
266
|
-
catch (error) {
|
|
267
|
-
if (hadCurrent)
|
|
268
|
-
await rename(previous, current).catch(() => { });
|
|
269
|
-
throw error;
|
|
306
|
+
await projectNativeSkills(agentRoot, trainingRoot, ability.skills);
|
|
307
|
+
const legacyAbilityRoot = join(agentRoot, ".nowcrew", "ability");
|
|
308
|
+
if (await exists(join(legacyAbilityRoot, "releases")) || await exists(join(legacyAbilityRoot, "current"))) {
|
|
309
|
+
await removeManagedTree(legacyAbilityRoot);
|
|
270
310
|
}
|
|
271
|
-
for (const asset of ability.workspace) {
|
|
272
|
-
const relativeTarget = asset.target.replace(/^\.nowcrew\/ability\//u, "");
|
|
273
|
-
const source = join(current, "workspace", relativeTarget);
|
|
274
|
-
const target = join(agentRoot, ...asset.target.split("/"));
|
|
275
|
-
await mkdir(dirname(target), { recursive: true });
|
|
276
|
-
if (asset.mode === "managed-copy") {
|
|
277
|
-
if (!await exists(target))
|
|
278
|
-
await cp(source, target, { recursive: true, preserveTimestamps: true });
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
const staging = `${target}.next-${randomUUID()}`;
|
|
282
|
-
await symlink(source, staging, "dir");
|
|
283
|
-
if (await exists(target))
|
|
284
|
-
await rm(target, { recursive: true, force: true });
|
|
285
|
-
await rename(staging, target);
|
|
286
|
-
}
|
|
287
|
-
await projectNativeSkills(agentRoot, releaseRoot, ability.skills);
|
|
288
311
|
await writeFile(join(agentRoot, ".nowwork-root"), "", { encoding: "utf8", mode: 0o600 });
|
|
289
312
|
};
|
|
290
313
|
export function createAgentAbilityMaterializer(expectedAgentsRoot) {
|
|
291
314
|
const tails = new Map();
|
|
315
|
+
const withAgentTurn = async (handle, operation) => {
|
|
316
|
+
const previous = tails.get(handle) ?? Promise.resolve();
|
|
317
|
+
let release;
|
|
318
|
+
const turn = new Promise((resolveTurn) => { release = resolveTurn; });
|
|
319
|
+
const tail = previous.then(() => turn);
|
|
320
|
+
tails.set(handle, tail);
|
|
321
|
+
await previous;
|
|
322
|
+
try {
|
|
323
|
+
return await operation();
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
release();
|
|
327
|
+
if (tails.get(handle) === tail)
|
|
328
|
+
tails.delete(handle);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
const apply = async (agentsRoot, handle, ability) => {
|
|
332
|
+
if (agentsRoot !== expectedAgentsRoot)
|
|
333
|
+
throw new Error("ability_agents_root_mismatch");
|
|
334
|
+
const trainingRoot = await materializeWorkspaceTraining(agentsRoot, handle, ability);
|
|
335
|
+
await projectWorkspaceTraining(agentsRoot, handle, ability, trainingRoot);
|
|
336
|
+
return { directory: "training", releaseId: ability.releaseId, artifactDigest: ability.artifactDigest };
|
|
337
|
+
};
|
|
292
338
|
return {
|
|
339
|
+
apply(agentsRoot, handle, ability) {
|
|
340
|
+
return withAgentTurn(handle, async () => {
|
|
341
|
+
const started = Date.now();
|
|
342
|
+
try {
|
|
343
|
+
const result = await apply(agentsRoot, handle, ability);
|
|
344
|
+
dslog("ability.workspace.projected", "Agent training projected into workspace", {
|
|
345
|
+
level: "INFO", agent_handle: handle, release_id: ability.releaseId,
|
|
346
|
+
root_commit: ability.rootCommit, artifact_digest: ability.artifactDigest,
|
|
347
|
+
workspace_directory: result.directory, duration_ms: Date.now() - started,
|
|
348
|
+
});
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
dslog("ability.workspace.projection_failed", "Agent training workspace projection failed", {
|
|
353
|
+
level: "ERROR", agent_handle: handle, release_id: ability.releaseId,
|
|
354
|
+
root_commit: ability.rootCommit,
|
|
355
|
+
error_code: error instanceof Error ? error.message.slice(0, 120) : "ability_workspace_projection_failed",
|
|
356
|
+
});
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
},
|
|
293
361
|
async prepareAndLaunch(agentsRoot, handle, ability, launch) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
catch (error) {
|
|
314
|
-
dslog("ability.execution.rejected", "Agent ability release materialization failed", {
|
|
315
|
-
level: "ERROR", agent_handle: handle, release_id: ability.releaseId,
|
|
316
|
-
root_commit: ability.rootCommit, error_code: error instanceof Error ? error.message.slice(0, 120) : "ability_materialization_failed",
|
|
317
|
-
});
|
|
318
|
-
throw error;
|
|
319
|
-
}
|
|
320
|
-
finally {
|
|
321
|
-
release();
|
|
322
|
-
if (tails.get(handle) === tail)
|
|
323
|
-
tails.delete(handle);
|
|
324
|
-
}
|
|
362
|
+
return withAgentTurn(handle, async () => {
|
|
363
|
+
const started = Date.now();
|
|
364
|
+
try {
|
|
365
|
+
await apply(agentsRoot, handle, ability);
|
|
366
|
+
dslog("ability.execution.materialized", "Agent ability release materialized", {
|
|
367
|
+
level: "INFO", agent_handle: handle, release_id: ability.releaseId,
|
|
368
|
+
root_commit: ability.rootCommit, artifact_digest: ability.artifactDigest,
|
|
369
|
+
workspace_directory: "training", duration_ms: Date.now() - started,
|
|
370
|
+
});
|
|
371
|
+
return await launch();
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
dslog("ability.execution.rejected", "Agent ability release materialization failed", {
|
|
375
|
+
level: "ERROR", agent_handle: handle, release_id: ability.releaseId,
|
|
376
|
+
root_commit: ability.rootCommit, error_code: error instanceof Error ? error.message.slice(0, 120) : "ability_materialization_failed",
|
|
377
|
+
});
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
});
|
|
325
381
|
},
|
|
326
382
|
};
|
|
327
383
|
}
|
|
@@ -5,7 +5,7 @@ import { dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { parse } from "yaml";
|
|
7
7
|
import { z } from "zod";
|
|
8
|
-
import { DaemonAbilityLockSchema, DaemonAbilityManifestSchema, matchesAbilityBranch, } from "./types.js";
|
|
8
|
+
import { abilityWorkspaceProjectionPath, DaemonAbilityLockSchema, DaemonAbilityManifestSchema, matchesAbilityBranch, } from "./types.js";
|
|
9
9
|
const runFile = promisify(execFile);
|
|
10
10
|
const MAX_FILES = 5_000;
|
|
11
11
|
const MAX_TOTAL_BYTES = 25 * 1024 * 1024;
|
|
@@ -281,13 +281,14 @@ export const evaluateAbilityCases = (files, manifest, lock, memory) => {
|
|
|
281
281
|
const workspaceReferenceResolves = (reference) => {
|
|
282
282
|
if (!reference.startsWith("workspace://"))
|
|
283
283
|
return false;
|
|
284
|
-
const target =
|
|
284
|
+
const target = reference.slice("workspace://".length);
|
|
285
285
|
return manifest.spec.workspace.assets.some((asset) => {
|
|
286
|
-
|
|
286
|
+
const projection = abilityWorkspaceProjectionPath(asset.target);
|
|
287
|
+
if (target === projection)
|
|
287
288
|
return filePaths.has(asset.source);
|
|
288
|
-
if (!target.startsWith(`${
|
|
289
|
+
if (!target.startsWith(`${projection}/`))
|
|
289
290
|
return false;
|
|
290
|
-
return filePaths.has(`${asset.source}/${target.slice(
|
|
291
|
+
return filePaths.has(`${asset.source}/${target.slice(projection.length + 1)}`);
|
|
291
292
|
});
|
|
292
293
|
};
|
|
293
294
|
const assertions = [];
|
|
@@ -1,20 +1,53 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AbilityReleaseSnapshotSchema, AgentHandleSchema } from "../execution-protocol.js";
|
|
1
3
|
import { createAgentAbilityController } from "./controller.js";
|
|
2
4
|
import { createAgentAbilityMaterializer } from "./materializer.js";
|
|
3
|
-
import { AGENT_ABILITY_CAPABILITY } from "./types.js";
|
|
5
|
+
import { AGENT_ABILITY_CAPABILITY, AGENT_ABILITY_WORKSPACE_CAPABILITY } from "./types.js";
|
|
6
|
+
const AbilityApplyCommandSchema = z.object({
|
|
7
|
+
type: z.literal("agent:ability:apply"),
|
|
8
|
+
reqId: z.string().min(1).max(128),
|
|
9
|
+
handle: AgentHandleSchema,
|
|
10
|
+
ability: AbilityReleaseSnapshotSchema,
|
|
11
|
+
}).strict();
|
|
4
12
|
export function createAgentAbilityRuntime(agentsRoot, options = {}) {
|
|
5
13
|
const controller = options.controller ?? createAgentAbilityController({ agentsRoot });
|
|
6
14
|
const materializer = options.materializer ?? createAgentAbilityMaterializer(agentsRoot);
|
|
7
15
|
return {
|
|
8
16
|
materializer,
|
|
9
17
|
async tryHandleControlMessage(input, serverCapabilities, send) {
|
|
10
|
-
|
|
18
|
+
const type = input?.type;
|
|
19
|
+
if (type !== "agent:ability:resolve" && type !== "agent:ability:apply")
|
|
11
20
|
return false;
|
|
12
21
|
const reqId = input.reqId;
|
|
13
22
|
if (typeof reqId !== "string")
|
|
14
23
|
return true;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
let result;
|
|
25
|
+
const requiredCapability = type === "agent:ability:apply"
|
|
26
|
+
? AGENT_ABILITY_WORKSPACE_CAPABILITY
|
|
27
|
+
: AGENT_ABILITY_CAPABILITY;
|
|
28
|
+
if (!serverCapabilities.has(requiredCapability)) {
|
|
29
|
+
result = { ok: false, error: "capability_unavailable" };
|
|
30
|
+
}
|
|
31
|
+
else if (type === "agent:ability:resolve") {
|
|
32
|
+
result = await controller.handle(input);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const parsed = AbilityApplyCommandSchema.safeParse(input);
|
|
36
|
+
if (!parsed.success)
|
|
37
|
+
result = { ok: false, error: "invalid_ability_apply_command" };
|
|
38
|
+
else {
|
|
39
|
+
try {
|
|
40
|
+
result = { ok: true, data: await materializer.apply(agentsRoot, parsed.data.handle, parsed.data.ability) };
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : "ability_workspace_projection_failed";
|
|
44
|
+
result = {
|
|
45
|
+
ok: false,
|
|
46
|
+
error: /^[a-z0-9_:-]+$/u.test(message) ? message.slice(0, 200) : "ability_workspace_projection_failed",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
18
51
|
try {
|
|
19
52
|
send({ type: "fs:result", reqId, ...result });
|
|
20
53
|
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export const AGENT_ABILITY_CAPABILITY = "agent_ability_release_v1";
|
|
3
|
+
export const AGENT_ABILITY_WORKSPACE_CAPABILITY = "agent_ability_workspace_v1";
|
|
3
4
|
const DigestSchema = z.string().regex(/^sha256:[a-f0-9]{64}$/u);
|
|
4
5
|
const CommitSchema = z.string().regex(/^[a-f0-9]{40}$/u);
|
|
5
6
|
export const SafeAbilityPathSchema = z.string().min(1).max(512).refine((value) => !value.startsWith("/") && !value.includes("\\") && !value.includes("\0")
|
|
6
7
|
&& value.split("/").every((part) => part.length > 0 && part !== "." && part !== ".."), "unsafe ability path");
|
|
7
|
-
export const WorkspaceAbilityTargetSchema = SafeAbilityPathSchema.refine((value) => value.startsWith(".nowcrew/ability/"), "training workspace targets must stay under
|
|
8
|
+
export const WorkspaceAbilityTargetSchema = SafeAbilityPathSchema.refine((value) => value.startsWith("training/workspace/") || value.startsWith(".nowcrew/ability/"), "training workspace targets must stay under training/workspace/");
|
|
9
|
+
export function abilityWorkspaceProjectionPath(target) {
|
|
10
|
+
WorkspaceAbilityTargetSchema.parse(target);
|
|
11
|
+
return target.startsWith("training/workspace/")
|
|
12
|
+
? target.slice("training/workspace/".length)
|
|
13
|
+
: target.slice(".nowcrew/ability/".length);
|
|
14
|
+
}
|
|
8
15
|
export const AbilityRepositoryUrlSchema = z.string().trim().min(1).max(2_048).superRefine((value, ctx) => {
|
|
9
16
|
const scpLike = /^git@[a-z0-9.-]+:[A-Za-z0-9._~/-]+(?:\.git)?$/iu.test(value);
|
|
10
17
|
let allowedUrl = false;
|
|
@@ -30,12 +37,11 @@ export function matchesAbilityBranch(glob, branch) {
|
|
|
30
37
|
let pattern = "^";
|
|
31
38
|
for (let index = 0; index < glob.length; index += 1) {
|
|
32
39
|
const character = glob[index];
|
|
33
|
-
if (character === "*"
|
|
40
|
+
if (character === "*") {
|
|
34
41
|
pattern += ".*";
|
|
35
|
-
index
|
|
42
|
+
if (glob[index + 1] === "*")
|
|
43
|
+
index += 1;
|
|
36
44
|
}
|
|
37
|
-
else if (character === "*")
|
|
38
|
-
pattern += "[^/]*";
|
|
39
45
|
else
|
|
40
46
|
pattern += character.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
41
47
|
}
|
|
@@ -23,7 +23,7 @@ const ProjectSkillRefSchema = z.object({
|
|
|
23
23
|
projectId: z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9._-]*$/u),
|
|
24
24
|
skillName: z.string().min(1).max(128).regex(/^[a-z0-9][a-z0-9._:-]*$/u),
|
|
25
25
|
}).strict();
|
|
26
|
-
const AbilityReleaseSnapshotSchema = z.object({
|
|
26
|
+
export const AbilityReleaseSnapshotSchema = z.object({
|
|
27
27
|
bindingId: z.string().uuid(),
|
|
28
28
|
releaseId: z.string().uuid(),
|
|
29
29
|
repositoryUrl: AbilityRepositoryUrlSchema,
|
package/dist/machine-info.js
CHANGED
|
@@ -34,11 +34,13 @@ export const DAEMON_CAPABILITIES = [
|
|
|
34
34
|
"project_skills_v1",
|
|
35
35
|
RUNTIME_HEALTH_PROBE_CAPABILITY,
|
|
36
36
|
"agent_ability_release_v1",
|
|
37
|
+
"agent_ability_workspace_v1",
|
|
37
38
|
];
|
|
38
39
|
export const daemonCapabilities = (runtimePlatform = process.platform) => runtimePlatform === "darwin" || runtimePlatform === "linux"
|
|
39
40
|
? DAEMON_CAPABILITIES
|
|
40
41
|
: DAEMON_CAPABILITIES.filter((capability) => capability !== "project_skills_v1"
|
|
41
|
-
&& capability !== "agent_ability_release_v1"
|
|
42
|
+
&& capability !== "agent_ability_release_v1"
|
|
43
|
+
&& capability !== "agent_ability_workspace_v1");
|
|
42
44
|
export const EXECUTION_PROTOCOL = Object.freeze({ min: 1, max: 1 });
|
|
43
45
|
/** 候选 runtime CLI:展示名 → 可执行文件名。 */
|
|
44
46
|
const RUNTIME_BINS = [
|