@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.
Files changed (40) hide show
  1. package/dist/agent-ability/controller.js +19 -0
  2. package/dist/agent-ability/materializer.js +383 -0
  3. package/dist/agent-ability/resolver.js +409 -0
  4. package/dist/agent-ability/runtime.js +60 -0
  5. package/dist/agent-ability/types.js +117 -0
  6. package/dist/control-plane-url.js +16 -0
  7. package/dist/execution-protocol.js +20 -0
  8. package/dist/execution-runner.js +2 -0
  9. package/dist/local-executor.js +6 -3
  10. package/dist/machine-info.js +5 -1
  11. package/dist/remote/claude-bridge.js +558 -0
  12. package/dist/remote/claude-channel.js +164 -0
  13. package/dist/remote/codex-client.js +451 -0
  14. package/dist/remote/codex-runtime.js +77 -0
  15. package/dist/remote/config.js +135 -0
  16. package/dist/remote/gateway.js +879 -0
  17. package/dist/remote/identity.js +39 -0
  18. package/dist/remote/owner.js +77 -0
  19. package/dist/remote/protocol.js +211 -0
  20. package/dist/remote/remote-cli.js +254 -0
  21. package/dist/remote/runtime-probe.js +182 -0
  22. package/dist/remote/session-discovery.js +249 -0
  23. package/dist/remote/wrapper.js +40 -0
  24. package/dist/remote-web/assets/index-B_6VM_tw.js +94 -0
  25. package/dist/remote-web/assets/index-L6EiQbJn.css +1 -0
  26. package/dist/remote-web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  27. package/dist/remote-web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  28. package/dist/remote-web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  29. package/dist/remote-web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  30. package/dist/remote-web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  31. package/dist/remote-web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  32. package/dist/remote-web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  33. package/dist/remote-web/icons/nowwork-192.png +0 -0
  34. package/dist/remote-web/icons/nowwork-512.png +0 -0
  35. package/dist/remote-web/icons/nowwork.svg +7 -0
  36. package/dist/remote-web/index.html +20 -0
  37. package/dist/remote-web/manifest.webmanifest +13 -0
  38. package/dist/remote-web/sw.js +12 -0
  39. package/dist/serve.js +8 -15
  40. package/package.json +2 -1
@@ -0,0 +1,409 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { cp, lstat, mkdir, readdir, readFile, rename, rm } from "node:fs/promises";
4
+ import { dirname, extname, join, relative, resolve, sep } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { parse } from "yaml";
7
+ import { z } from "zod";
8
+ import { abilityWorkspaceProjectionPath, DaemonAbilityLockSchema, DaemonAbilityManifestSchema, matchesAbilityBranch, } from "./types.js";
9
+ const runFile = promisify(execFile);
10
+ const MAX_FILES = 5_000;
11
+ const MAX_TOTAL_BYTES = 25 * 1024 * 1024;
12
+ const MAX_FILE_BYTES = 2 * 1024 * 1024;
13
+ const MAX_INSTRUCTIONS_BYTES = 96 * 1024;
14
+ const MAX_MEMORY_BYTES = 64 * 1024;
15
+ const DEFAULT_GIT_HOSTS = ["codeup.aliyun.com", "github.com", "gitlab.com", "bitbucket.org"];
16
+ const EvalSuiteSchema = z.object({
17
+ schemaVersion: z.literal("nowcrew-eval-suite/v1"),
18
+ name: z.string().min(1).max(128),
19
+ cases: z.array(z.object({
20
+ id: z.string().min(1).max(128),
21
+ input: z.string().max(64 * 1024),
22
+ expected: z.object({
23
+ instructionPath: z.string().optional(),
24
+ skillNames: z.array(z.string()).max(500).optional(),
25
+ forbiddenSkillNames: z.array(z.string()).max(500).optional(),
26
+ memoryIds: z.array(z.string()).max(1_000).optional(),
27
+ workspaceRefs: z.array(z.string()).max(1_000).optional(),
28
+ evalSuite: z.string().optional(),
29
+ dependency: z.string().optional(),
30
+ commit: z.string().optional(),
31
+ refusalReason: z.string().optional(),
32
+ }).passthrough(),
33
+ assertions: z.array(z.string().min(1).max(128)).max(1_000),
34
+ }).strict()).max(1_000),
35
+ }).strict();
36
+ const GIT_ARGS = ["-c", "core.hooksPath=/dev/null", "-c", "filter.lfs.smudge=", "-c", "filter.lfs.required=false"];
37
+ const gitEnv = () => ({
38
+ ...process.env,
39
+ GIT_CONFIG_NOSYSTEM: "1",
40
+ GIT_CONFIG_GLOBAL: process.platform === "win32" ? "NUL" : "/dev/null",
41
+ GIT_TERMINAL_PROMPT: "0",
42
+ });
43
+ export const abilitySha256 = (content) => `sha256:${createHash("sha256").update(content).digest("hex")}`;
44
+ export const safeAbilityName = (value) => value.replace(/[^A-Za-z0-9._-]/gu, "_").slice(0, 128);
45
+ export const validateAbilityGitHost = (repositoryUrl, configuredHosts = process.env.CREW_AGENT_ABILITY_GIT_HOSTS ?? "") => {
46
+ const scpHost = repositoryUrl.match(/^git@([a-z0-9.-]+):/iu)?.[1];
47
+ let hostname = scpHost;
48
+ if (!hostname) {
49
+ try {
50
+ hostname = new URL(repositoryUrl).hostname;
51
+ }
52
+ catch { /* URL schema reports the syntax error. */ }
53
+ }
54
+ const allowed = new Set([
55
+ ...DEFAULT_GIT_HOSTS,
56
+ ...configuredHosts.split(",").map((entry) => entry.trim().toLowerCase()).filter(Boolean),
57
+ ]);
58
+ if (!hostname || !allowed.has(hostname.toLowerCase()))
59
+ throw new Error("ability_repository_host_not_allowed");
60
+ return hostname.toLowerCase();
61
+ };
62
+ const comparePosixPaths = (left, right) => {
63
+ const leftParts = left.split("/");
64
+ const rightParts = right.split("/");
65
+ for (let index = 0; index < Math.min(leftParts.length, rightParts.length); index += 1) {
66
+ if (leftParts[index] === rightParts[index])
67
+ continue;
68
+ return leftParts[index] < rightParts[index] ? -1 : 1;
69
+ }
70
+ return leftParts.length - rightParts.length;
71
+ };
72
+ const git = async (args, cwd) => {
73
+ const result = await runFile("git", [...GIT_ARGS, ...args], {
74
+ ...(cwd ? { cwd } : {}), env: gitEnv(), timeout: 60_000, maxBuffer: 16 * 1024 * 1024,
75
+ });
76
+ return result.stdout.trim();
77
+ };
78
+ export const collectAbilityFiles = async (root) => {
79
+ const files = [];
80
+ let totalBytes = 0;
81
+ const visit = async (directory) => {
82
+ const entries = await readdir(directory, { withFileTypes: true });
83
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
84
+ if (entry.name === ".git" || entry.name === ".DS_Store" || entry.name === "__pycache__"
85
+ || entry.name.endsWith(".pyc") || entry.name.endsWith(".pyo"))
86
+ continue;
87
+ const absolute = join(directory, entry.name);
88
+ const info = await lstat(absolute);
89
+ if (info.isSymbolicLink())
90
+ throw new Error(`ability_symlink_forbidden:${relative(root, absolute)}`);
91
+ if (info.isDirectory())
92
+ await visit(absolute);
93
+ else if (info.isFile()) {
94
+ if (info.size > MAX_FILE_BYTES)
95
+ throw new Error(`ability_file_size_exceeded:${relative(root, absolute)}`);
96
+ totalBytes += info.size;
97
+ if (totalBytes > MAX_TOTAL_BYTES)
98
+ throw new Error("ability_total_size_exceeded");
99
+ if (files.length >= MAX_FILES)
100
+ throw new Error("ability_file_count_exceeded");
101
+ files.push({ path: relative(root, absolute).split(sep).join("/"), content: await readFile(absolute) });
102
+ }
103
+ }
104
+ };
105
+ await visit(root);
106
+ return files.sort((left, right) => comparePosixPaths(left.path, right.path));
107
+ };
108
+ export const abilityTreeDigest = (rootPath, files) => {
109
+ const exact = files.find((file) => file.path === rootPath);
110
+ const nested = exact ? [exact] : files.filter((file) => file.path.startsWith(`${rootPath}/`));
111
+ if (nested.length === 0)
112
+ throw new Error(`ability_locked_asset_missing:${rootPath}`);
113
+ const digest = createHash("sha256");
114
+ for (const file of nested) {
115
+ digest.update(file.path);
116
+ digest.update("\0");
117
+ digest.update(file.content);
118
+ digest.update("\0");
119
+ }
120
+ return `sha256:${digest.digest("hex")}`;
121
+ };
122
+ const frontmatter = (content) => {
123
+ const lines = content.split(/\r?\n/u);
124
+ if (lines[0] !== "---")
125
+ throw new Error("ability_memory_frontmatter_missing");
126
+ const result = {};
127
+ for (const line of lines.slice(1)) {
128
+ if (line === "---")
129
+ break;
130
+ const separator = line.indexOf(":");
131
+ if (separator > 0 && !line.startsWith(" "))
132
+ result[line.slice(0, separator).trim()] = line.slice(separator + 1).trim();
133
+ }
134
+ return result;
135
+ };
136
+ const materializeGit = async (cacheRoot, repositoryUrl, commit, target) => {
137
+ const repositoryKey = createHash("sha256").update(repositoryUrl).digest("hex");
138
+ const bare = join(cacheRoot, "git", `${repositoryKey}.git`);
139
+ await mkdir(dirname(bare), { recursive: true });
140
+ try {
141
+ await lstat(bare);
142
+ }
143
+ catch {
144
+ await git(["init", "--bare", bare]);
145
+ }
146
+ const remotes = await git(["--git-dir", bare, "remote"]);
147
+ if (!remotes.split("\n").includes("origin"))
148
+ await git(["--git-dir", bare, "remote", "add", "origin", repositoryUrl]);
149
+ else
150
+ await git(["--git-dir", bare, "remote", "set-url", "origin", repositoryUrl]);
151
+ await git(["--git-dir", bare, "fetch", "--no-tags", "--prune", "origin", commit]);
152
+ const temp = `${target}.tmp-${randomUUID()}`;
153
+ await rm(temp, { recursive: true, force: true });
154
+ await git(["clone", "--no-checkout", "--shared", bare, temp]);
155
+ await git(["checkout", "--detach", commit], temp);
156
+ await rm(join(temp, ".git"), { recursive: true, force: true });
157
+ await mkdir(dirname(target), { recursive: true });
158
+ await rm(target, { recursive: true, force: true });
159
+ await rename(temp, target);
160
+ };
161
+ const resolveRoot = async (cacheRoot, repositoryUrl, branchGlob, desiredCommit) => {
162
+ const repositoryKey = createHash("sha256").update(repositoryUrl).digest("hex");
163
+ const bare = join(cacheRoot, "git", `${repositoryKey}.git`);
164
+ await mkdir(dirname(bare), { recursive: true });
165
+ try {
166
+ await lstat(bare);
167
+ }
168
+ catch {
169
+ await git(["init", "--bare", bare]);
170
+ }
171
+ const remotes = await git(["--git-dir", bare, "remote"]);
172
+ if (!remotes.split("\n").includes("origin"))
173
+ await git(["--git-dir", bare, "remote", "add", "origin", repositoryUrl]);
174
+ else
175
+ await git(["--git-dir", bare, "remote", "set-url", "origin", repositoryUrl]);
176
+ await git(["--git-dir", bare, "fetch", "--no-tags", "--prune", "origin", "+refs/heads/*:refs/remotes/origin/*"]);
177
+ const refs = (await git(["--git-dir", bare, "for-each-ref", "--format=%(refname:strip=3)\t%(objectname)\t%(committerdate:unix)", "refs/remotes/origin"])).split("\n")
178
+ .filter(Boolean).map((line) => {
179
+ const [branch, commit, time] = line.split("\t");
180
+ return { branch: branch, commit: commit, time: Number(time) };
181
+ }).filter((candidate) => matchesAbilityBranch(branchGlob, candidate.branch));
182
+ if (refs.length === 0)
183
+ throw new Error("ability_release_branch_not_found");
184
+ const selected = refs.filter((candidate) => desiredCommit === undefined || candidate.commit === desiredCommit)
185
+ .sort((left, right) => right.time - left.time || right.branch.localeCompare(left.branch))[0];
186
+ if (!selected)
187
+ throw new Error("ability_desired_commit_not_found");
188
+ const root = join(cacheRoot, "artifacts", repositoryKey, selected.commit);
189
+ try {
190
+ await lstat(root);
191
+ }
192
+ catch {
193
+ await materializeGit(cacheRoot, repositoryUrl, selected.commit, root);
194
+ }
195
+ const tree = await git(["--git-dir", bare, "rev-parse", `${selected.commit}^{tree}`]);
196
+ return { ...selected, root, repositoryKey, treeDigest: `git:${tree}` };
197
+ };
198
+ const verifyExternalDependencies = async (cacheRoot, root, lock) => {
199
+ const paths = new Map();
200
+ for (const dependency of lock.dependencies) {
201
+ const target = join(root, ".nowcrew-external", safeAbilityName(dependency.name));
202
+ await materializeGit(cacheRoot, dependency.url, dependency.commit, target);
203
+ const files = await collectAbilityFiles(target);
204
+ const actual = abilityTreeDigest(dependency.path, files);
205
+ if (actual !== dependency.contentDigest)
206
+ throw new Error(`ability_dependency_lock_drift:${dependency.name}`);
207
+ paths.set(dependency.name, join(".nowcrew-external", safeAbilityName(dependency.name), dependency.path).split(sep).join("/"));
208
+ }
209
+ return paths;
210
+ };
211
+ export const validateAbilityDependencyDeclarations = (manifest, lock) => {
212
+ const dependencies = new Map(lock.dependencies.map((dependency) => [dependency.name, dependency]));
213
+ const declared = new Set();
214
+ for (const skill of manifest.spec.skills) {
215
+ if (skill.source.type !== "git")
216
+ continue;
217
+ declared.add(skill.name);
218
+ const dependency = dependencies.get(skill.name);
219
+ if (!dependency
220
+ || dependency.url !== skill.source.url
221
+ || dependency.requestedRef !== skill.source.ref
222
+ || dependency.path !== skill.source.path
223
+ || (skill.trust !== undefined && dependency.trust !== skill.trust)) {
224
+ throw new Error(`ability_dependency_manifest_mismatch:${skill.name}`);
225
+ }
226
+ }
227
+ if (lock.dependencies.some((dependency) => !declared.has(dependency.name))) {
228
+ throw new Error("ability_lock_has_undeclared_dependency");
229
+ }
230
+ };
231
+ const scan = (files, manifest) => {
232
+ const findings = [];
233
+ const secrets = [
234
+ /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/u,
235
+ /\b(?:sk|sk-ant|ghp|glpat|xox[baprs])[-_][-A-Za-z0-9_]{16,}\b/u,
236
+ /\bAK(?:ID|IA)[A-Z0-9]{12,}\b/u,
237
+ ];
238
+ const executable = new Set([".exe", ".dll", ".dylib", ".so", ".bin", ".class", ".jar"]);
239
+ const allowedBinaryDocuments = new Set([".pdf", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".xlsx", ".xls", ".docx", ".pptx"]);
240
+ for (const file of files) {
241
+ let text;
242
+ try {
243
+ text = new TextDecoder("utf-8", { fatal: true }).decode(file.content);
244
+ }
245
+ catch {
246
+ if (!allowedBinaryDocuments.has(extname(file.path).toLowerCase())) {
247
+ findings.push({ code: "binary_asset", path: file.path });
248
+ }
249
+ continue;
250
+ }
251
+ if (secrets.some((pattern) => pattern.test(text)))
252
+ findings.push({ code: "secret_detected", path: file.path });
253
+ if (executable.has(extname(file.path).toLowerCase()))
254
+ findings.push({ code: "executable_asset", path: file.path });
255
+ }
256
+ const required = new Set(["instructions", "memory-policy", "memory-seed", "workspace", "eval-policy", "eval-cases"]);
257
+ if (manifest.spec.skills.length > 0)
258
+ required.add("skills");
259
+ return { findings, required };
260
+ };
261
+ const lockedManifestPaths = (manifest) => [
262
+ manifest.spec.instructions.path,
263
+ ...manifest.spec.skills.flatMap((skill) => skill.source.type === "local" ? [skill.source.path] : []),
264
+ ...manifest.spec.memory.policy.map((entry) => entry.path),
265
+ ...manifest.spec.memory.seeds.map((entry) => entry.path),
266
+ ...manifest.spec.workspace.assets.map((entry) => entry.source),
267
+ manifest.spec.evals.policy,
268
+ ...manifest.spec.evals.cases,
269
+ ];
270
+ export const evaluateAbilityCases = (files, manifest, lock, memory) => {
271
+ const skillNames = new Set(manifest.spec.skills.map((skill) => skill.name));
272
+ for (const file of files) {
273
+ if (!file.path.endsWith("/SKILL.md"))
274
+ continue;
275
+ const parts = file.path.split("/");
276
+ if (parts.length > 1)
277
+ skillNames.add(parts.at(-2));
278
+ }
279
+ const memoryIds = new Set(memory.map((entry) => entry.id));
280
+ const filePaths = new Set(files.map((file) => file.path));
281
+ const workspaceReferenceResolves = (reference) => {
282
+ if (!reference.startsWith("workspace://"))
283
+ return false;
284
+ const target = reference.slice("workspace://".length);
285
+ return manifest.spec.workspace.assets.some((asset) => {
286
+ const projection = abilityWorkspaceProjectionPath(asset.target);
287
+ if (target === projection)
288
+ return filePaths.has(asset.source);
289
+ if (!target.startsWith(`${projection}/`))
290
+ return false;
291
+ return filePaths.has(`${asset.source}/${target.slice(projection.length + 1)}`);
292
+ });
293
+ };
294
+ const assertions = [];
295
+ for (const path of manifest.spec.evals.cases) {
296
+ const source = files.find((file) => file.path === path);
297
+ const parsed = source
298
+ ? EvalSuiteSchema.safeParse((() => {
299
+ try {
300
+ return JSON.parse(source.content.toString("utf8"));
301
+ }
302
+ catch {
303
+ return null;
304
+ }
305
+ })())
306
+ : null;
307
+ if (!parsed?.success) {
308
+ assertions.push({ name: `eval_suite_schema:${path}`.slice(0, 128), passed: false });
309
+ continue;
310
+ }
311
+ for (const testCase of parsed.data.cases) {
312
+ const expected = testCase.expected;
313
+ const dependency = expected.dependency
314
+ ? lock.dependencies.find((entry) => entry.name === expected.dependency)
315
+ : undefined;
316
+ const expectedSkills = [...(expected.skillNames ?? []), ...(expected.forbiddenSkillNames ?? [])];
317
+ const passed = (expected.instructionPath === undefined
318
+ || expected.instructionPath === manifest.spec.instructions.path)
319
+ && expectedSkills.every((name) => skillNames.has(name))
320
+ && (expected.memoryIds ?? []).every((id) => memoryIds.has(id))
321
+ && (expected.workspaceRefs ?? []).every(workspaceReferenceResolves)
322
+ && (expected.evalSuite === undefined || expected.evalSuite === parsed.data.name)
323
+ && (expected.dependency === undefined || dependency !== undefined)
324
+ && (expected.commit === undefined || dependency?.commit === expected.commit);
325
+ assertions.push({
326
+ name: `eval_case:${parsed.data.name}:${testCase.id}`.slice(0, 128),
327
+ passed,
328
+ });
329
+ }
330
+ }
331
+ return assertions;
332
+ };
333
+ export async function resolveAbilityRelease(agentsRoot, command) {
334
+ validateAbilityGitHost(command.repositoryUrl);
335
+ const cacheRoot = join(agentsRoot, ".crew", "ability-cache");
336
+ const resolved = await resolveRoot(cacheRoot, command.repositoryUrl, command.branchGlob, command.desiredCommit);
337
+ const rawManifest = await readFile(join(resolved.root, "agent.yaml"));
338
+ const manifest = DaemonAbilityManifestSchema.parse(parse(rawManifest.toString("utf8")));
339
+ const lock = DaemonAbilityLockSchema.parse(JSON.parse(await readFile(join(resolved.root, "agent.lock.json"), "utf8")));
340
+ for (const dependency of lock.dependencies)
341
+ validateAbilityGitHost(dependency.url);
342
+ if (lock.manifestDigest !== abilitySha256(rawManifest))
343
+ throw new Error("ability_manifest_lock_drift");
344
+ validateAbilityDependencyDeclarations(manifest, lock);
345
+ let files = await collectAbilityFiles(resolved.root);
346
+ for (const asset of lock.assets) {
347
+ if (abilityTreeDigest(asset.path, files) !== asset.contentDigest)
348
+ throw new Error(`ability_asset_lock_drift:${asset.path}`);
349
+ }
350
+ const externalPaths = await verifyExternalDependencies(cacheRoot, resolved.root, lock);
351
+ files = await collectAbilityFiles(resolved.root);
352
+ const security = scan(files, manifest);
353
+ const kinds = new Set(lock.assets.map((asset) => asset.kind));
354
+ const memory = [];
355
+ for (const [layer, mounts] of [["release-policy", manifest.spec.memory.policy], ["release-seed", manifest.spec.memory.seeds]]) {
356
+ for (const mount of mounts) {
357
+ for (const file of files.filter((candidate) => candidate.path.startsWith(`${mount.path}/`) && candidate.path.endsWith(".md"))) {
358
+ const content = file.content.toString("utf8");
359
+ const metadata = frontmatter(content);
360
+ if (!metadata.id || !metadata.authority)
361
+ throw new Error(`ability_memory_frontmatter_invalid:${file.path}`);
362
+ memory.push({ id: metadata.id, layer, authority: metadata.authority, path: file.path, content });
363
+ }
364
+ }
365
+ }
366
+ const lockedPaths = new Set(lock.assets.map((asset) => asset.path));
367
+ const assertions = [
368
+ { name: "manifest_lock_verified", passed: true },
369
+ { name: "manifest_asset_paths_locked", passed: lockedManifestPaths(manifest).every((path) => lockedPaths.has(path)) },
370
+ { name: "all_five_layers_resolvable", passed: [...security.required].every((kind) => kind === "skills" ? kinds.has(kind) || (manifest.spec.skills.length > 0 && lock.dependencies.length > 0) : kinds.has(kind)) },
371
+ { name: "dependencies_pinned", passed: lock.dependencies.every((dependency) => /^[a-f0-9]{40}$/u.test(dependency.commit)) },
372
+ { name: "security_scan_clear", passed: security.findings.length === 0 },
373
+ ...evaluateAbilityCases(files, manifest, lock, memory),
374
+ ];
375
+ const artifactDigest = abilitySha256(JSON.stringify({
376
+ repositoryUrl: command.repositoryUrl, rootCommit: resolved.commit, treeDigest: resolved.treeDigest,
377
+ lock, external: [...externalPaths.entries()].sort(([left], [right]) => left.localeCompare(right)),
378
+ }));
379
+ const assetSummary = Object.freeze(Object.fromEntries(lock.assets.reduce((counts, asset) => {
380
+ counts.set(asset.kind, (counts.get(asset.kind) ?? 0) + 1);
381
+ return counts;
382
+ }, new Map())));
383
+ const instructionsContent = (files.find((file) => file.path === manifest.spec.instructions.path)?.content
384
+ ?? (() => { throw new Error("ability_instructions_missing"); })()).toString("utf8");
385
+ if (Buffer.byteLength(instructionsContent, "utf8") > MAX_INSTRUCTIONS_BYTES) {
386
+ throw new Error("ability_instructions_size_exceeded");
387
+ }
388
+ if (memory.reduce((bytes, entry) => bytes + Buffer.byteLength(entry.content, "utf8"), 0) > MAX_MEMORY_BYTES) {
389
+ throw new Error("ability_memory_size_exceeded");
390
+ }
391
+ return {
392
+ branch: resolved.branch, rootCommit: resolved.commit, treeDigest: resolved.treeDigest,
393
+ artifactDigest, schemaVersion: manifest.apiVersion, manifest, lock, assetSummary,
394
+ scanSummary: { passed: security.findings.length === 0, blockingFindings: security.findings.length, findings: security.findings.slice(0, 100) },
395
+ evalSummary: { passed: assertions.every((assertion) => assertion.passed), assertions },
396
+ materialization: {
397
+ cacheKey: `${resolved.repositoryKey}/${resolved.commit}`, repositoryUrl: command.repositoryUrl,
398
+ branch: resolved.branch, rootCommit: resolved.commit, artifactDigest,
399
+ instructionsPath: manifest.spec.instructions.path,
400
+ instructionsContent,
401
+ skills: manifest.spec.skills.map((skill) => ({
402
+ name: skill.name,
403
+ path: skill.source.type === "local" ? skill.source.path : externalPaths.get(skill.name),
404
+ mode: skill.mode,
405
+ })),
406
+ memory, workspace: manifest.spec.workspace.assets, evals: manifest.spec.evals.cases,
407
+ },
408
+ };
409
+ }
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+ import { AbilityReleaseSnapshotSchema, AgentHandleSchema } from "../execution-protocol.js";
3
+ import { createAgentAbilityController } from "./controller.js";
4
+ import { createAgentAbilityMaterializer } from "./materializer.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();
12
+ export function createAgentAbilityRuntime(agentsRoot, options = {}) {
13
+ const controller = options.controller ?? createAgentAbilityController({ agentsRoot });
14
+ const materializer = options.materializer ?? createAgentAbilityMaterializer(agentsRoot);
15
+ return {
16
+ materializer,
17
+ async tryHandleControlMessage(input, serverCapabilities, send) {
18
+ const type = input?.type;
19
+ if (type !== "agent:ability:resolve" && type !== "agent:ability:apply")
20
+ return false;
21
+ const reqId = input.reqId;
22
+ if (typeof reqId !== "string")
23
+ return true;
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
+ }
51
+ try {
52
+ send({ type: "fs:result", reqId, ...result });
53
+ }
54
+ catch {
55
+ // The control plane request timeout handles a lost response.
56
+ }
57
+ return true;
58
+ },
59
+ };
60
+ }
@@ -0,0 +1,117 @@
1
+ import { z } from "zod";
2
+ export const AGENT_ABILITY_CAPABILITY = "agent_ability_release_v1";
3
+ export const AGENT_ABILITY_WORKSPACE_CAPABILITY = "agent_ability_workspace_v1";
4
+ const DigestSchema = z.string().regex(/^sha256:[a-f0-9]{64}$/u);
5
+ const CommitSchema = z.string().regex(/^[a-f0-9]{40}$/u);
6
+ export const SafeAbilityPathSchema = z.string().min(1).max(512).refine((value) => !value.startsWith("/") && !value.includes("\\") && !value.includes("\0")
7
+ && value.split("/").every((part) => part.length > 0 && part !== "." && part !== ".."), "unsafe ability path");
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
+ }
15
+ export const AbilityRepositoryUrlSchema = z.string().trim().min(1).max(2_048).superRefine((value, ctx) => {
16
+ const scpLike = /^git@[a-z0-9.-]+:[A-Za-z0-9._~/-]+(?:\.git)?$/iu.test(value);
17
+ let allowedUrl = false;
18
+ try {
19
+ const url = new URL(value);
20
+ allowedUrl = (url.protocol === "https:" || url.protocol === "ssh:")
21
+ && !url.username && !url.password && Boolean(url.hostname);
22
+ }
23
+ catch { /* handled below */ }
24
+ if (!scpLike && !allowedUrl)
25
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "unsupported repository URL" });
26
+ });
27
+ export const AbilityBranchGlobSchema = z.string().min(1).max(128).superRefine((value, ctx) => {
28
+ if (/\s|[\p{Cc}\\]/u.test(value) || value.includes("..") || value.includes("@{")
29
+ || value.startsWith("-") || !/[A-Za-z0-9._/-]/u.test(value) || /[^A-Za-z0-9._/*-]/u.test(value)) {
30
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "invalid branch glob" });
31
+ }
32
+ });
33
+ export function matchesAbilityBranch(glob, branch) {
34
+ AbilityBranchGlobSchema.parse(glob);
35
+ if (!branch || branch.startsWith("refs/") || branch.includes("..") || /\s|[\p{Cc}\\]/u.test(branch))
36
+ return false;
37
+ let pattern = "^";
38
+ for (let index = 0; index < glob.length; index += 1) {
39
+ const character = glob[index];
40
+ if (character === "*") {
41
+ pattern += ".*";
42
+ if (glob[index + 1] === "*")
43
+ index += 1;
44
+ }
45
+ else
46
+ pattern += character.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
47
+ }
48
+ return new RegExp(`${pattern}$`, "u").test(branch);
49
+ }
50
+ const ModeSchema = z.enum(["managed-readonly", "managed-copy", "overlay", "generated"]);
51
+ const LocalSkillSchema = z.object({ type: z.literal("local"), path: SafeAbilityPathSchema, catalog: SafeAbilityPathSchema.optional() }).strict();
52
+ const GitSkillSchema = z.object({
53
+ type: z.literal("git"), url: AbilityRepositoryUrlSchema, ref: z.string().min(1).max(256), path: SafeAbilityPathSchema,
54
+ }).strict();
55
+ export const DaemonAbilityManifestSchema = z.object({
56
+ apiVersion: z.literal("nowcrew.ai/v1alpha1"), kind: z.literal("AgentTraining"),
57
+ metadata: z.object({ name: z.string().min(1).max(128), version: z.string().min(1).max(64), description: z.string().max(1_000).optional() }).strict(),
58
+ spec: z.object({
59
+ instructions: z.object({ path: SafeAbilityPathSchema }).strict(),
60
+ skills: z.array(z.object({
61
+ name: z.string().min(1).max(128), source: z.union([LocalSkillSchema, GitSkillSchema]),
62
+ mount: SafeAbilityPathSchema, mode: ModeSchema, trust: z.string().max(128).optional(),
63
+ }).strict()).max(500),
64
+ memory: z.object({
65
+ policy: z.array(z.object({ path: SafeAbilityPathSchema, mode: ModeSchema, mergeKey: z.string().optional() }).strict()).max(100),
66
+ seeds: z.array(z.object({ path: SafeAbilityPathSchema, mode: ModeSchema, mergeKey: z.string().optional() }).strict()).max(1_000),
67
+ runtime: z.object({ store: z.literal("nowcrew-managed"), promotion: z.literal("pull-request"), retentionDays: z.number().int().positive().optional() }).strict(),
68
+ }).strict(),
69
+ workspace: z.object({ assets: z.array(z.object({
70
+ source: SafeAbilityPathSchema, target: WorkspaceAbilityTargetSchema, mode: ModeSchema,
71
+ }).strict()).max(1_000) }).strict(),
72
+ evals: z.object({ policy: SafeAbilityPathSchema, cases: z.array(SafeAbilityPathSchema).min(1).max(1_000) }).strict(),
73
+ compatibility: z.object({
74
+ providers: z.array(z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness"])).min(1),
75
+ minNowCrewVersion: z.string().regex(/^\d+\.\d+\.\d+$/u),
76
+ }).strict().optional(),
77
+ permissions: z.object({
78
+ network: z.literal("declared-only"), executableAssets: z.literal("skill-declared-only"),
79
+ secrets: z.literal("runtime-only"), productionWrites: z.literal("human-confirmed").optional(),
80
+ }).strict(),
81
+ rollout: z.object({ mode: z.enum(["manual", "auto_after_gates", "scheduled", "pinned"]), canaryExecutions: z.number().int().min(0).max(100), rollbackOnGateFailure: z.boolean().optional() }).strict(),
82
+ }).strict(),
83
+ }).strict().superRefine((manifest, ctx) => {
84
+ const unique = (values, path, label) => {
85
+ if (new Set(values).size !== values.length) {
86
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: [...path], message: `duplicate ${label}` });
87
+ }
88
+ };
89
+ unique(manifest.spec.skills.map((skill) => skill.name), ["spec", "skills"], "Skill name");
90
+ unique(manifest.spec.skills.map((skill) => skill.name.replace(/[^A-Za-z0-9._-]/gu, "_")), ["spec", "skills"], "Skill projection");
91
+ unique(manifest.spec.skills.map((skill) => skill.mount), ["spec", "skills"], "Skill mount");
92
+ unique(manifest.spec.workspace.assets.map((asset) => asset.target), ["spec", "workspace", "assets"], "Workspace target");
93
+ unique(manifest.spec.evals.cases, ["spec", "evals", "cases"], "Eval case");
94
+ });
95
+ export const DaemonAbilityLockSchema = z.object({
96
+ lockVersion: z.literal(1), manifestDigest: DigestSchema,
97
+ assets: z.array(z.object({ kind: z.string().min(1).max(64), path: SafeAbilityPathSchema, contentDigest: DigestSchema }).strict()).max(5_000),
98
+ dependencies: z.array(z.object({
99
+ name: z.string().min(1).max(128), url: AbilityRepositoryUrlSchema, requestedRef: z.string().min(1).max(256),
100
+ commit: CommitSchema, path: SafeAbilityPathSchema, contentDigest: DigestSchema, trust: z.string().min(1).max(128),
101
+ }).strict()).max(500),
102
+ generatedBy: z.string().min(1).max(256),
103
+ }).strict().superRefine((lock, ctx) => {
104
+ const assetKeys = lock.assets.map((asset) => `${asset.kind}\0${asset.path}`);
105
+ if (new Set(assetKeys).size !== assetKeys.length) {
106
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["assets"], message: "duplicate locked asset" });
107
+ }
108
+ const dependencyNames = lock.dependencies.map((dependency) => dependency.name);
109
+ if (new Set(dependencyNames).size !== dependencyNames.length) {
110
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["dependencies"], message: "duplicate dependency name" });
111
+ }
112
+ });
113
+ export const AbilityResolveCommandSchema = z.object({
114
+ type: z.literal("agent:ability:resolve"), reqId: z.string().min(1).max(128),
115
+ handle: z.string().min(1).max(64), repositoryUrl: AbilityRepositoryUrlSchema,
116
+ branchGlob: AbilityBranchGlobSchema, desiredCommit: CommitSchema.optional(),
117
+ }).strict();
@@ -0,0 +1,16 @@
1
+ import { executionBackendCapability } from "./execution-backend.js";
2
+ import { daemonCapabilities, EXECUTION_PROTOCOL } from "./machine-info.js";
3
+ import { PROJECT_SKILLS_CAPABILITY } from "./project-skills/types.js";
4
+ export function buildControlPlaneUrl(serverUrl, machineToken, runtimePlatform = process.platform, jobObjectProbe, projectSkillsAvailable = true) {
5
+ const query = new URLSearchParams({ key: machineToken });
6
+ if (executionBackendCapability(runtimePlatform, jobObjectProbe).supported) {
7
+ query.set("execution_min", String(EXECUTION_PROTOCOL.min));
8
+ query.set("execution_max", String(EXECUTION_PROTOCOL.max));
9
+ }
10
+ for (const capability of daemonCapabilities(runtimePlatform)) {
11
+ if (capability !== PROJECT_SKILLS_CAPABILITY || projectSkillsAvailable) {
12
+ query.append("capability", capability);
13
+ }
14
+ }
15
+ return `${serverUrl.replace(/^http/, "ws").replace(/\/+$/, "")}/daemon/connect?${query.toString()}`;
16
+ }
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { AbilityRepositoryUrlSchema, SafeAbilityPathSchema, WorkspaceAbilityTargetSchema, } from "./agent-ability/types.js";
2
3
  import { MAX_AGENT_PROJECT_SKILL_BINDINGS } from "./project-skills/types.js";
3
4
  const ExecutionIdSchema = z.string().uuid();
4
5
  const ProtocolVersionSchema = z.literal(1);
@@ -22,6 +23,24 @@ const ProjectSkillRefSchema = z.object({
22
23
  projectId: z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9._-]*$/u),
23
24
  skillName: z.string().min(1).max(128).regex(/^[a-z0-9][a-z0-9._:-]*$/u),
24
25
  }).strict();
26
+ export const AbilityReleaseSnapshotSchema = z.object({
27
+ bindingId: z.string().uuid(),
28
+ releaseId: z.string().uuid(),
29
+ repositoryUrl: AbilityRepositoryUrlSchema,
30
+ branch: z.string().min(1).max(256),
31
+ rootCommit: z.string().regex(/^[a-f0-9]{40}$/u),
32
+ treeDigest: z.string().regex(/^git:[a-f0-9]{40,64}$/u),
33
+ artifactDigest: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
34
+ instructions: z.object({ path: SafeAbilityPathSchema, content: z.string().max(96 * 1024) }).strict(),
35
+ skills: z.array(z.object({ name: z.string().min(1).max(128), path: SafeAbilityPathSchema, mode: z.string().max(64) }).strict()).max(500),
36
+ memory: z.array(z.object({
37
+ id: z.string().min(1).max(256),
38
+ layer: z.enum(["release-policy", "release-seed", "runtime-semantic", "runtime-episodic", "user"]),
39
+ authority: z.string().min(1).max(128), path: SafeAbilityPathSchema, content: z.string().max(64 * 1024),
40
+ }).strict()).max(100),
41
+ workspace: z.array(z.object({ source: SafeAbilityPathSchema, target: WorkspaceAbilityTargetSchema, mode: z.string().max(64) }).strict()).max(1_000),
42
+ evalProvenance: z.object({ cases: z.array(SafeAbilityPathSchema).max(1_000) }).strict(),
43
+ }).strict();
25
44
  export const ReasoningSchema = z.string().min(1).max(64)
26
45
  .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u, "invalid runtime reasoning token");
27
46
  export const PermissionSchema = z.enum([
@@ -97,6 +116,7 @@ export const ExecutionStartSchema = z.object({
97
116
  handle: AgentHandleSchema,
98
117
  memoryEnabled: z.boolean().optional(),
99
118
  projectSkills: z.array(ProjectSkillRefSchema).max(MAX_AGENT_PROJECT_SKILL_BINDINGS).optional(),
119
+ abilityRelease: AbilityReleaseSnapshotSchema.optional(),
100
120
  }).strict(),
101
121
  workspace: z.object({
102
122
  taskKey: z.string().min(1).max(200),
@@ -517,6 +517,7 @@ export async function runExecution(config, input, dependencies) {
517
517
  ? {}
518
518
  : { startupTimeoutMs: dependencies.startupTimeoutMs }),
519
519
  ...(dependencies.projectSkills === undefined ? {} : { projectSkills: dependencies.projectSkills }),
520
+ ...(dependencies.abilityRelease === undefined ? {} : { abilityRelease: dependencies.abilityRelease }),
520
521
  launchRuntime: async (request) => {
521
522
  if (launchClosed || dependencies.cancellation?.isRequested())
522
523
  throw new ExecutionCancelledError();
@@ -585,6 +586,7 @@ export async function runExecution(config, input, dependencies) {
585
586
  ...(spec.context.wakeMessageId === undefined ? {} : { wakeMessageId: spec.context.wakeMessageId }),
586
587
  ...(spec.context.attachments === undefined ? {} : { attachments: spec.context.attachments }),
587
588
  ...(spec.agent.projectSkills === undefined ? {} : { projectSkills: spec.agent.projectSkills }),
589
+ ...(spec.agent.abilityRelease === undefined ? {} : { abilityRelease: spec.agent.abilityRelease }),
588
590
  systemPrompt: boundedSystemPrompt,
589
591
  wakePrompt: spec.instructions.wakePrompt,
590
592
  runtime: {