@nowcrew/daemon 0.5.52 → 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.
- package/dist/agent-ability/controller.js +19 -0
- package/dist/agent-ability/materializer.js +383 -0
- package/dist/agent-ability/resolver.js +409 -0
- package/dist/agent-ability/runtime.js +60 -0
- package/dist/agent-ability/types.js +117 -0
- package/dist/control-plane-url.js +16 -0
- package/dist/execution-protocol.js +20 -0
- package/dist/execution-runner.js +2 -0
- package/dist/local-executor.js +6 -3
- package/dist/machine-info.js +5 -1
- package/dist/remote/claude-bridge.js +558 -0
- package/dist/remote/claude-channel.js +164 -0
- package/dist/remote/codex-client.js +451 -0
- package/dist/remote/codex-runtime.js +77 -0
- package/dist/remote/config.js +135 -0
- package/dist/remote/gateway.js +879 -0
- package/dist/remote/identity.js +39 -0
- package/dist/remote/owner.js +77 -0
- package/dist/remote/protocol.js +211 -0
- package/dist/remote/remote-cli.js +254 -0
- package/dist/remote/runtime-probe.js +182 -0
- package/dist/remote/session-discovery.js +249 -0
- package/dist/remote/wrapper.js +40 -0
- package/dist/remote-web/assets/index-B_6VM_tw.js +94 -0
- package/dist/remote-web/assets/index-L6EiQbJn.css +1 -0
- package/dist/remote-web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/remote-web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/remote-web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/remote-web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/remote-web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/remote-web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/remote-web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/remote-web/icons/nowwork-192.png +0 -0
- package/dist/remote-web/icons/nowwork-512.png +0 -0
- package/dist/remote-web/icons/nowwork.svg +7 -0
- package/dist/remote-web/index.html +20 -0
- package/dist/remote-web/manifest.webmanifest +13 -0
- package/dist/remote-web/sw.js +12 -0
- package/dist/serve.js +8 -15
- package/package.json +2 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AbilityResolveCommandSchema } from "./types.js";
|
|
2
|
+
import { resolveAbilityRelease } from "./resolver.js";
|
|
3
|
+
export function createAgentAbilityController(deps) {
|
|
4
|
+
const resolver = deps.resolve ?? resolveAbilityRelease;
|
|
5
|
+
return {
|
|
6
|
+
async handle(input) {
|
|
7
|
+
const parsed = AbilityResolveCommandSchema.safeParse(input);
|
|
8
|
+
if (!parsed.success)
|
|
9
|
+
return { ok: false, error: "invalid_ability_command" };
|
|
10
|
+
try {
|
|
11
|
+
return { ok: true, data: await resolver(deps.agentsRoot, parsed.data) };
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
const message = error instanceof Error ? error.message : "ability_resolve_failed";
|
|
15
|
+
return { ok: false, error: /^[a-z0-9_:-]+$/u.test(message) ? message.slice(0, 200) : "ability_resolve_failed" };
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, cp, lstat, mkdir, readFile, readdir, readlink, rename, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { parse } from "yaml";
|
|
5
|
+
import { dslog } from "../slog.js";
|
|
6
|
+
import { abilitySha256, abilityTreeDigest, collectAbilityFiles, resolveAbilityRelease, safeAbilityName, } from "./resolver.js";
|
|
7
|
+
import { abilityWorkspaceProjectionPath, DaemonAbilityLockSchema, DaemonAbilityManifestSchema } from "./types.js";
|
|
8
|
+
const exists = (path) => lstat(path).then(() => true, () => false);
|
|
9
|
+
const repositoryKey = (url) => createHash("sha256").update(url).digest("hex");
|
|
10
|
+
const posixPath = (value) => value.split(sep).join("/");
|
|
11
|
+
const sameSnapshot = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
12
|
+
const readonlyTree = async (root) => {
|
|
13
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
const path = join(root, entry.name);
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
await readonlyTree(path);
|
|
18
|
+
await chmod(path, 0o555);
|
|
19
|
+
}
|
|
20
|
+
else if (entry.isFile()) {
|
|
21
|
+
const current = await lstat(path);
|
|
22
|
+
await chmod(path, current.mode & 0o111 ? 0o555 : 0o444);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
await chmod(root, 0o555);
|
|
26
|
+
};
|
|
27
|
+
const writableTree = async (root) => {
|
|
28
|
+
const info = await lstat(root);
|
|
29
|
+
if (info.isSymbolicLink())
|
|
30
|
+
return;
|
|
31
|
+
if (!info.isDirectory()) {
|
|
32
|
+
await chmod(root, 0o600);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
await chmod(root, 0o700);
|
|
36
|
+
for (const entry of await readdir(root))
|
|
37
|
+
await writableTree(join(root, entry));
|
|
38
|
+
};
|
|
39
|
+
const removeManagedTree = async (root) => {
|
|
40
|
+
if (!await exists(root))
|
|
41
|
+
return;
|
|
42
|
+
await writableTree(root);
|
|
43
|
+
await rm(root, { recursive: true, force: true });
|
|
44
|
+
};
|
|
45
|
+
const switchDirectory = async (target, staging, backup) => {
|
|
46
|
+
const hadPrevious = await exists(target);
|
|
47
|
+
try {
|
|
48
|
+
if (hadPrevious)
|
|
49
|
+
await rename(target, backup);
|
|
50
|
+
await rename(staging, target);
|
|
51
|
+
if (hadPrevious) {
|
|
52
|
+
await writableTree(backup);
|
|
53
|
+
await rm(backup, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (await exists(target))
|
|
58
|
+
await removeManagedTree(target).catch(() => { });
|
|
59
|
+
if (hadPrevious && await exists(backup))
|
|
60
|
+
await rename(backup, target).catch(() => { });
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const copyExistingSkills = async (target, staging, managedRoots) => {
|
|
65
|
+
const names = new Set();
|
|
66
|
+
if (!await exists(target))
|
|
67
|
+
return names;
|
|
68
|
+
for (const entry of await readdir(target, { withFileTypes: true })) {
|
|
69
|
+
const source = join(target, entry.name);
|
|
70
|
+
const destination = join(staging, entry.name);
|
|
71
|
+
const info = await lstat(source);
|
|
72
|
+
if (info.isSymbolicLink()) {
|
|
73
|
+
const link = await readlink(source);
|
|
74
|
+
const resolvedLink = resolve(dirname(source), link);
|
|
75
|
+
if (managedRoots.some((root) => resolvedLink === root || resolvedLink.startsWith(`${root}${sep}`)))
|
|
76
|
+
continue;
|
|
77
|
+
await symlink(link, destination, "dir");
|
|
78
|
+
}
|
|
79
|
+
else
|
|
80
|
+
await cp(source, destination, { recursive: true, preserveTimestamps: true });
|
|
81
|
+
names.add(entry.name);
|
|
82
|
+
}
|
|
83
|
+
return names;
|
|
84
|
+
};
|
|
85
|
+
const projectNativeSkills = async (agentRoot, trainingRoot, skills) => {
|
|
86
|
+
const nativeSkills = [];
|
|
87
|
+
for (const skill of skills) {
|
|
88
|
+
const source = join(trainingRoot, "skills", safeAbilityName(skill.name));
|
|
89
|
+
if (await exists(join(source, "SKILL.md"))) {
|
|
90
|
+
nativeSkills.push({ name: skill.name, source });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
for (const entry of await readdir(source, { withFileTypes: true })) {
|
|
94
|
+
if (entry.isDirectory() && await exists(join(source, entry.name, "SKILL.md"))) {
|
|
95
|
+
nativeSkills.push({ name: entry.name, source: join(source, entry.name) });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const id = randomUUID();
|
|
100
|
+
const targets = [
|
|
101
|
+
join(agentRoot, ".agents", "skills"),
|
|
102
|
+
join(agentRoot, ".crew", "claude-skills", ".claude", "skills"),
|
|
103
|
+
];
|
|
104
|
+
const managedRoots = [trainingRoot, join(agentRoot, ".nowcrew", "ability", "releases")];
|
|
105
|
+
for (const target of targets) {
|
|
106
|
+
const staging = join(dirname(target), `.ability-skills-next-${id}`);
|
|
107
|
+
const backup = join(dirname(target), `.ability-skills-previous-${id}`);
|
|
108
|
+
await mkdir(dirname(target), { recursive: true });
|
|
109
|
+
await mkdir(staging, { mode: 0o700 });
|
|
110
|
+
try {
|
|
111
|
+
const names = await copyExistingSkills(target, staging, managedRoots);
|
|
112
|
+
for (const skill of nativeSkills) {
|
|
113
|
+
if (names.has(skill.name))
|
|
114
|
+
throw new Error(`ability_skill_name_conflict:${skill.name}`);
|
|
115
|
+
await symlink(skill.source, join(staging, skill.name), "dir");
|
|
116
|
+
}
|
|
117
|
+
await switchDirectory(target, staging, backup);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
await rm(staging, { recursive: true, force: true });
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const verifyCache = async (agentsRoot, ability) => {
|
|
126
|
+
const root = join(agentsRoot, ".crew", "ability-cache", "artifacts", repositoryKey(ability.repositoryUrl), ability.rootCommit);
|
|
127
|
+
if (!await exists(root))
|
|
128
|
+
throw new Error("ability_artifact_cache_missing");
|
|
129
|
+
const rawManifest = await readFile(join(root, "agent.yaml"));
|
|
130
|
+
const manifest = DaemonAbilityManifestSchema.parse(parse(rawManifest.toString("utf8")));
|
|
131
|
+
const lock = DaemonAbilityLockSchema.parse(JSON.parse(await readFile(join(root, "agent.lock.json"), "utf8")));
|
|
132
|
+
if (lock.manifestDigest !== abilitySha256(rawManifest))
|
|
133
|
+
throw new Error("ability_manifest_lock_drift");
|
|
134
|
+
const files = await collectAbilityFiles(root);
|
|
135
|
+
for (const asset of lock.assets) {
|
|
136
|
+
if (abilityTreeDigest(asset.path, files) !== asset.contentDigest) {
|
|
137
|
+
throw new Error(`ability_asset_lock_drift:${asset.path}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const externalPaths = new Map();
|
|
141
|
+
const external = lock.dependencies.map((dependency) => {
|
|
142
|
+
const dependencyRoot = join(root, ".nowcrew-external", safeAbilityName(dependency.name));
|
|
143
|
+
const dependencyFiles = files.filter((file) => file.path.startsWith(`${posixPath(relative(root, dependencyRoot))}/`))
|
|
144
|
+
.map((file) => ({
|
|
145
|
+
path: file.path.slice(posixPath(relative(root, dependencyRoot)).length + 1),
|
|
146
|
+
content: file.content,
|
|
147
|
+
}));
|
|
148
|
+
if (abilityTreeDigest(dependency.path, dependencyFiles) !== dependency.contentDigest) {
|
|
149
|
+
throw new Error(`ability_dependency_lock_drift:${dependency.name}`);
|
|
150
|
+
}
|
|
151
|
+
const path = posixPath(join(".nowcrew-external", safeAbilityName(dependency.name), dependency.path));
|
|
152
|
+
externalPaths.set(dependency.name, path);
|
|
153
|
+
return [dependency.name, path];
|
|
154
|
+
}).sort(([left], [right]) => left.localeCompare(right));
|
|
155
|
+
const artifactDigest = abilitySha256(JSON.stringify({
|
|
156
|
+
repositoryUrl: ability.repositoryUrl,
|
|
157
|
+
rootCommit: ability.rootCommit,
|
|
158
|
+
treeDigest: ability.treeDigest,
|
|
159
|
+
lock,
|
|
160
|
+
external,
|
|
161
|
+
}));
|
|
162
|
+
if (artifactDigest !== ability.artifactDigest)
|
|
163
|
+
throw new Error("ability_artifact_digest_mismatch");
|
|
164
|
+
if (manifest.spec.instructions.path !== ability.instructions.path
|
|
165
|
+
|| (await readFile(join(root, ability.instructions.path), "utf8")) !== ability.instructions.content) {
|
|
166
|
+
throw new Error("ability_instruction_snapshot_mismatch");
|
|
167
|
+
}
|
|
168
|
+
const expectedSkills = manifest.spec.skills.map((skill) => ({
|
|
169
|
+
name: skill.name,
|
|
170
|
+
path: skill.source.type === "local" ? skill.source.path : externalPaths.get(skill.name),
|
|
171
|
+
mode: skill.mode,
|
|
172
|
+
}));
|
|
173
|
+
if (expectedSkills.some((skill) => skill.path === undefined)
|
|
174
|
+
|| !sameSnapshot(ability.skills, expectedSkills)) {
|
|
175
|
+
throw new Error("ability_skill_snapshot_mismatch");
|
|
176
|
+
}
|
|
177
|
+
if (new Set(ability.skills.map((skill) => safeAbilityName(skill.name))).size !== ability.skills.length) {
|
|
178
|
+
throw new Error("ability_skill_projection_collision");
|
|
179
|
+
}
|
|
180
|
+
if (!sameSnapshot(ability.workspace, manifest.spec.workspace.assets)) {
|
|
181
|
+
throw new Error("ability_workspace_snapshot_mismatch");
|
|
182
|
+
}
|
|
183
|
+
if (!sameSnapshot(ability.evalProvenance.cases, manifest.spec.evals.cases)) {
|
|
184
|
+
throw new Error("ability_eval_snapshot_mismatch");
|
|
185
|
+
}
|
|
186
|
+
const releaseMemory = ability.memory.filter((memory) => memory.layer === "release-policy" || memory.layer === "release-seed");
|
|
187
|
+
for (const memory of releaseMemory) {
|
|
188
|
+
const mounts = memory.layer === "release-policy" ? manifest.spec.memory.policy : manifest.spec.memory.seeds;
|
|
189
|
+
if (!mounts.some((mount) => memory.path === mount.path || memory.path.startsWith(`${mount.path}/`))
|
|
190
|
+
|| await readFile(join(root, memory.path), "utf8") !== memory.content) {
|
|
191
|
+
throw new Error("ability_memory_snapshot_mismatch");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (ability.memory.reduce((bytes, memory) => bytes + Buffer.byteLength(memory.content, "utf8"), 0) > 64 * 1024) {
|
|
195
|
+
throw new Error("ability_memory_snapshot_size_exceeded");
|
|
196
|
+
}
|
|
197
|
+
return { root, manifest };
|
|
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;
|
|
223
|
+
const agentRoot = join(agentsRoot, handle);
|
|
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;
|
|
240
|
+
}
|
|
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()}`);
|
|
244
|
+
await mkdir(staging, { recursive: true, mode: 0o700 });
|
|
245
|
+
try {
|
|
246
|
+
const instructionsTarget = join(staging, "instructions", layerRelativePath(ability.instructions.path, "instructions"));
|
|
247
|
+
await mkdir(dirname(instructionsTarget), { recursive: true });
|
|
248
|
+
await cp(join(sourceRoot, ability.instructions.path), instructionsTarget, { preserveTimestamps: true });
|
|
249
|
+
for (const skill of ability.skills) {
|
|
250
|
+
const target = join(staging, "skills", safeAbilityName(skill.name));
|
|
251
|
+
await mkdir(dirname(target), { recursive: true });
|
|
252
|
+
await cp(join(sourceRoot, skill.path), target, { recursive: true, preserveTimestamps: true });
|
|
253
|
+
}
|
|
254
|
+
for (const memory of ability.memory) {
|
|
255
|
+
const target = join(staging, "memory", safeAbilityName(memory.layer), `${safeAbilityName(memory.id)}.md`);
|
|
256
|
+
await mkdir(dirname(target), { recursive: true });
|
|
257
|
+
await writeFile(target, memory.content, { encoding: "utf8", mode: 0o600 });
|
|
258
|
+
}
|
|
259
|
+
for (const asset of ability.workspace) {
|
|
260
|
+
const relativeTarget = abilityWorkspaceProjectionPath(asset.target);
|
|
261
|
+
const target = join(staging, "workspace", relativeTarget);
|
|
262
|
+
await mkdir(dirname(target), { recursive: true });
|
|
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 });
|
|
274
|
+
}
|
|
275
|
+
await writeFile(join(staging, "ACTIVE_RELEASE.json"), JSON.stringify({
|
|
276
|
+
managedBy: "nowcrew-agent-training",
|
|
277
|
+
releaseId: ability.releaseId,
|
|
278
|
+
rootCommit: ability.rootCommit,
|
|
279
|
+
artifactDigest: ability.artifactDigest,
|
|
280
|
+
projectionDigest,
|
|
281
|
+
instructions: `training/instructions/${layerRelativePath(ability.instructions.path, "instructions")}`,
|
|
282
|
+
skills: ability.skills.map((skill) => skill.name),
|
|
283
|
+
memory: ability.memory.map((memory) => ({ id: memory.id, layer: memory.layer })),
|
|
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")}`),
|
|
289
|
+
}, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
290
|
+
await readonlyTree(staging);
|
|
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);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
await removeManagedTree(staging).catch(() => { });
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
return trainingRoot;
|
|
303
|
+
};
|
|
304
|
+
const projectWorkspaceTraining = async (agentsRoot, handle, ability, trainingRoot) => {
|
|
305
|
+
const agentRoot = join(agentsRoot, handle);
|
|
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);
|
|
310
|
+
}
|
|
311
|
+
await writeFile(join(agentRoot, ".nowwork-root"), "", { encoding: "utf8", mode: 0o600 });
|
|
312
|
+
};
|
|
313
|
+
export function createAgentAbilityMaterializer(expectedAgentsRoot) {
|
|
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
|
+
};
|
|
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
|
+
},
|
|
361
|
+
async prepareAndLaunch(agentsRoot, handle, ability, launch) {
|
|
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
|
+
});
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|