@nowcrew/daemon 0.5.52 → 0.6.0

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 +327 -0
  3. package/dist/agent-ability/resolver.js +408 -0
  4. package/dist/agent-ability/runtime.js +27 -0
  5. package/dist/agent-ability/types.js +111 -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 +3 -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,408 @@
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 { 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 = `.nowcrew/ability/${reference.slice("workspace://".length)}`;
285
+ return manifest.spec.workspace.assets.some((asset) => {
286
+ if (target === asset.target)
287
+ return filePaths.has(asset.source);
288
+ if (!target.startsWith(`${asset.target}/`))
289
+ return false;
290
+ return filePaths.has(`${asset.source}/${target.slice(asset.target.length + 1)}`);
291
+ });
292
+ };
293
+ const assertions = [];
294
+ for (const path of manifest.spec.evals.cases) {
295
+ const source = files.find((file) => file.path === path);
296
+ const parsed = source
297
+ ? EvalSuiteSchema.safeParse((() => {
298
+ try {
299
+ return JSON.parse(source.content.toString("utf8"));
300
+ }
301
+ catch {
302
+ return null;
303
+ }
304
+ })())
305
+ : null;
306
+ if (!parsed?.success) {
307
+ assertions.push({ name: `eval_suite_schema:${path}`.slice(0, 128), passed: false });
308
+ continue;
309
+ }
310
+ for (const testCase of parsed.data.cases) {
311
+ const expected = testCase.expected;
312
+ const dependency = expected.dependency
313
+ ? lock.dependencies.find((entry) => entry.name === expected.dependency)
314
+ : undefined;
315
+ const expectedSkills = [...(expected.skillNames ?? []), ...(expected.forbiddenSkillNames ?? [])];
316
+ const passed = (expected.instructionPath === undefined
317
+ || expected.instructionPath === manifest.spec.instructions.path)
318
+ && expectedSkills.every((name) => skillNames.has(name))
319
+ && (expected.memoryIds ?? []).every((id) => memoryIds.has(id))
320
+ && (expected.workspaceRefs ?? []).every(workspaceReferenceResolves)
321
+ && (expected.evalSuite === undefined || expected.evalSuite === parsed.data.name)
322
+ && (expected.dependency === undefined || dependency !== undefined)
323
+ && (expected.commit === undefined || dependency?.commit === expected.commit);
324
+ assertions.push({
325
+ name: `eval_case:${parsed.data.name}:${testCase.id}`.slice(0, 128),
326
+ passed,
327
+ });
328
+ }
329
+ }
330
+ return assertions;
331
+ };
332
+ export async function resolveAbilityRelease(agentsRoot, command) {
333
+ validateAbilityGitHost(command.repositoryUrl);
334
+ const cacheRoot = join(agentsRoot, ".crew", "ability-cache");
335
+ const resolved = await resolveRoot(cacheRoot, command.repositoryUrl, command.branchGlob, command.desiredCommit);
336
+ const rawManifest = await readFile(join(resolved.root, "agent.yaml"));
337
+ const manifest = DaemonAbilityManifestSchema.parse(parse(rawManifest.toString("utf8")));
338
+ const lock = DaemonAbilityLockSchema.parse(JSON.parse(await readFile(join(resolved.root, "agent.lock.json"), "utf8")));
339
+ for (const dependency of lock.dependencies)
340
+ validateAbilityGitHost(dependency.url);
341
+ if (lock.manifestDigest !== abilitySha256(rawManifest))
342
+ throw new Error("ability_manifest_lock_drift");
343
+ validateAbilityDependencyDeclarations(manifest, lock);
344
+ let files = await collectAbilityFiles(resolved.root);
345
+ for (const asset of lock.assets) {
346
+ if (abilityTreeDigest(asset.path, files) !== asset.contentDigest)
347
+ throw new Error(`ability_asset_lock_drift:${asset.path}`);
348
+ }
349
+ const externalPaths = await verifyExternalDependencies(cacheRoot, resolved.root, lock);
350
+ files = await collectAbilityFiles(resolved.root);
351
+ const security = scan(files, manifest);
352
+ const kinds = new Set(lock.assets.map((asset) => asset.kind));
353
+ const memory = [];
354
+ for (const [layer, mounts] of [["release-policy", manifest.spec.memory.policy], ["release-seed", manifest.spec.memory.seeds]]) {
355
+ for (const mount of mounts) {
356
+ for (const file of files.filter((candidate) => candidate.path.startsWith(`${mount.path}/`) && candidate.path.endsWith(".md"))) {
357
+ const content = file.content.toString("utf8");
358
+ const metadata = frontmatter(content);
359
+ if (!metadata.id || !metadata.authority)
360
+ throw new Error(`ability_memory_frontmatter_invalid:${file.path}`);
361
+ memory.push({ id: metadata.id, layer, authority: metadata.authority, path: file.path, content });
362
+ }
363
+ }
364
+ }
365
+ const lockedPaths = new Set(lock.assets.map((asset) => asset.path));
366
+ const assertions = [
367
+ { name: "manifest_lock_verified", passed: true },
368
+ { name: "manifest_asset_paths_locked", passed: lockedManifestPaths(manifest).every((path) => lockedPaths.has(path)) },
369
+ { 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)) },
370
+ { name: "dependencies_pinned", passed: lock.dependencies.every((dependency) => /^[a-f0-9]{40}$/u.test(dependency.commit)) },
371
+ { name: "security_scan_clear", passed: security.findings.length === 0 },
372
+ ...evaluateAbilityCases(files, manifest, lock, memory),
373
+ ];
374
+ const artifactDigest = abilitySha256(JSON.stringify({
375
+ repositoryUrl: command.repositoryUrl, rootCommit: resolved.commit, treeDigest: resolved.treeDigest,
376
+ lock, external: [...externalPaths.entries()].sort(([left], [right]) => left.localeCompare(right)),
377
+ }));
378
+ const assetSummary = Object.freeze(Object.fromEntries(lock.assets.reduce((counts, asset) => {
379
+ counts.set(asset.kind, (counts.get(asset.kind) ?? 0) + 1);
380
+ return counts;
381
+ }, new Map())));
382
+ const instructionsContent = (files.find((file) => file.path === manifest.spec.instructions.path)?.content
383
+ ?? (() => { throw new Error("ability_instructions_missing"); })()).toString("utf8");
384
+ if (Buffer.byteLength(instructionsContent, "utf8") > MAX_INSTRUCTIONS_BYTES) {
385
+ throw new Error("ability_instructions_size_exceeded");
386
+ }
387
+ if (memory.reduce((bytes, entry) => bytes + Buffer.byteLength(entry.content, "utf8"), 0) > MAX_MEMORY_BYTES) {
388
+ throw new Error("ability_memory_size_exceeded");
389
+ }
390
+ return {
391
+ branch: resolved.branch, rootCommit: resolved.commit, treeDigest: resolved.treeDigest,
392
+ artifactDigest, schemaVersion: manifest.apiVersion, manifest, lock, assetSummary,
393
+ scanSummary: { passed: security.findings.length === 0, blockingFindings: security.findings.length, findings: security.findings.slice(0, 100) },
394
+ evalSummary: { passed: assertions.every((assertion) => assertion.passed), assertions },
395
+ materialization: {
396
+ cacheKey: `${resolved.repositoryKey}/${resolved.commit}`, repositoryUrl: command.repositoryUrl,
397
+ branch: resolved.branch, rootCommit: resolved.commit, artifactDigest,
398
+ instructionsPath: manifest.spec.instructions.path,
399
+ instructionsContent,
400
+ skills: manifest.spec.skills.map((skill) => ({
401
+ name: skill.name,
402
+ path: skill.source.type === "local" ? skill.source.path : externalPaths.get(skill.name),
403
+ mode: skill.mode,
404
+ })),
405
+ memory, workspace: manifest.spec.workspace.assets, evals: manifest.spec.evals.cases,
406
+ },
407
+ };
408
+ }
@@ -0,0 +1,27 @@
1
+ import { createAgentAbilityController } from "./controller.js";
2
+ import { createAgentAbilityMaterializer } from "./materializer.js";
3
+ import { AGENT_ABILITY_CAPABILITY } from "./types.js";
4
+ export function createAgentAbilityRuntime(agentsRoot, options = {}) {
5
+ const controller = options.controller ?? createAgentAbilityController({ agentsRoot });
6
+ const materializer = options.materializer ?? createAgentAbilityMaterializer(agentsRoot);
7
+ return {
8
+ materializer,
9
+ async tryHandleControlMessage(input, serverCapabilities, send) {
10
+ if (input?.type !== "agent:ability:resolve")
11
+ return false;
12
+ const reqId = input.reqId;
13
+ if (typeof reqId !== "string")
14
+ return true;
15
+ const result = serverCapabilities.has(AGENT_ABILITY_CAPABILITY)
16
+ ? await controller.handle(input)
17
+ : { ok: false, error: "capability_unavailable" };
18
+ try {
19
+ send({ type: "fs:result", reqId, ...result });
20
+ }
21
+ catch {
22
+ // The control plane request timeout handles a lost response.
23
+ }
24
+ return true;
25
+ },
26
+ };
27
+ }
@@ -0,0 +1,111 @@
1
+ import { z } from "zod";
2
+ export const AGENT_ABILITY_CAPABILITY = "agent_ability_release_v1";
3
+ const DigestSchema = z.string().regex(/^sha256:[a-f0-9]{64}$/u);
4
+ const CommitSchema = z.string().regex(/^[a-f0-9]{40}$/u);
5
+ export const SafeAbilityPathSchema = z.string().min(1).max(512).refine((value) => !value.startsWith("/") && !value.includes("\\") && !value.includes("\0")
6
+ && 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 .nowcrew/ability/");
8
+ export const AbilityRepositoryUrlSchema = z.string().trim().min(1).max(2_048).superRefine((value, ctx) => {
9
+ const scpLike = /^git@[a-z0-9.-]+:[A-Za-z0-9._~/-]+(?:\.git)?$/iu.test(value);
10
+ let allowedUrl = false;
11
+ try {
12
+ const url = new URL(value);
13
+ allowedUrl = (url.protocol === "https:" || url.protocol === "ssh:")
14
+ && !url.username && !url.password && Boolean(url.hostname);
15
+ }
16
+ catch { /* handled below */ }
17
+ if (!scpLike && !allowedUrl)
18
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "unsupported repository URL" });
19
+ });
20
+ export const AbilityBranchGlobSchema = z.string().min(1).max(128).superRefine((value, ctx) => {
21
+ if (/\s|[\p{Cc}\\]/u.test(value) || value.includes("..") || value.includes("@{")
22
+ || value.startsWith("-") || !/[A-Za-z0-9._/-]/u.test(value) || /[^A-Za-z0-9._/*-]/u.test(value)) {
23
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "invalid branch glob" });
24
+ }
25
+ });
26
+ export function matchesAbilityBranch(glob, branch) {
27
+ AbilityBranchGlobSchema.parse(glob);
28
+ if (!branch || branch.startsWith("refs/") || branch.includes("..") || /\s|[\p{Cc}\\]/u.test(branch))
29
+ return false;
30
+ let pattern = "^";
31
+ for (let index = 0; index < glob.length; index += 1) {
32
+ const character = glob[index];
33
+ if (character === "*" && glob[index + 1] === "*") {
34
+ pattern += ".*";
35
+ index += 1;
36
+ }
37
+ else if (character === "*")
38
+ pattern += "[^/]*";
39
+ else
40
+ pattern += character.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
41
+ }
42
+ return new RegExp(`${pattern}$`, "u").test(branch);
43
+ }
44
+ const ModeSchema = z.enum(["managed-readonly", "managed-copy", "overlay", "generated"]);
45
+ const LocalSkillSchema = z.object({ type: z.literal("local"), path: SafeAbilityPathSchema, catalog: SafeAbilityPathSchema.optional() }).strict();
46
+ const GitSkillSchema = z.object({
47
+ type: z.literal("git"), url: AbilityRepositoryUrlSchema, ref: z.string().min(1).max(256), path: SafeAbilityPathSchema,
48
+ }).strict();
49
+ export const DaemonAbilityManifestSchema = z.object({
50
+ apiVersion: z.literal("nowcrew.ai/v1alpha1"), kind: z.literal("AgentTraining"),
51
+ 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(),
52
+ spec: z.object({
53
+ instructions: z.object({ path: SafeAbilityPathSchema }).strict(),
54
+ skills: z.array(z.object({
55
+ name: z.string().min(1).max(128), source: z.union([LocalSkillSchema, GitSkillSchema]),
56
+ mount: SafeAbilityPathSchema, mode: ModeSchema, trust: z.string().max(128).optional(),
57
+ }).strict()).max(500),
58
+ memory: z.object({
59
+ policy: z.array(z.object({ path: SafeAbilityPathSchema, mode: ModeSchema, mergeKey: z.string().optional() }).strict()).max(100),
60
+ seeds: z.array(z.object({ path: SafeAbilityPathSchema, mode: ModeSchema, mergeKey: z.string().optional() }).strict()).max(1_000),
61
+ runtime: z.object({ store: z.literal("nowcrew-managed"), promotion: z.literal("pull-request"), retentionDays: z.number().int().positive().optional() }).strict(),
62
+ }).strict(),
63
+ workspace: z.object({ assets: z.array(z.object({
64
+ source: SafeAbilityPathSchema, target: WorkspaceAbilityTargetSchema, mode: ModeSchema,
65
+ }).strict()).max(1_000) }).strict(),
66
+ evals: z.object({ policy: SafeAbilityPathSchema, cases: z.array(SafeAbilityPathSchema).min(1).max(1_000) }).strict(),
67
+ compatibility: z.object({
68
+ providers: z.array(z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness"])).min(1),
69
+ minNowCrewVersion: z.string().regex(/^\d+\.\d+\.\d+$/u),
70
+ }).strict().optional(),
71
+ permissions: z.object({
72
+ network: z.literal("declared-only"), executableAssets: z.literal("skill-declared-only"),
73
+ secrets: z.literal("runtime-only"), productionWrites: z.literal("human-confirmed").optional(),
74
+ }).strict(),
75
+ 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(),
76
+ }).strict(),
77
+ }).strict().superRefine((manifest, ctx) => {
78
+ const unique = (values, path, label) => {
79
+ if (new Set(values).size !== values.length) {
80
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: [...path], message: `duplicate ${label}` });
81
+ }
82
+ };
83
+ unique(manifest.spec.skills.map((skill) => skill.name), ["spec", "skills"], "Skill name");
84
+ unique(manifest.spec.skills.map((skill) => skill.name.replace(/[^A-Za-z0-9._-]/gu, "_")), ["spec", "skills"], "Skill projection");
85
+ unique(manifest.spec.skills.map((skill) => skill.mount), ["spec", "skills"], "Skill mount");
86
+ unique(manifest.spec.workspace.assets.map((asset) => asset.target), ["spec", "workspace", "assets"], "Workspace target");
87
+ unique(manifest.spec.evals.cases, ["spec", "evals", "cases"], "Eval case");
88
+ });
89
+ export const DaemonAbilityLockSchema = z.object({
90
+ lockVersion: z.literal(1), manifestDigest: DigestSchema,
91
+ assets: z.array(z.object({ kind: z.string().min(1).max(64), path: SafeAbilityPathSchema, contentDigest: DigestSchema }).strict()).max(5_000),
92
+ dependencies: z.array(z.object({
93
+ name: z.string().min(1).max(128), url: AbilityRepositoryUrlSchema, requestedRef: z.string().min(1).max(256),
94
+ commit: CommitSchema, path: SafeAbilityPathSchema, contentDigest: DigestSchema, trust: z.string().min(1).max(128),
95
+ }).strict()).max(500),
96
+ generatedBy: z.string().min(1).max(256),
97
+ }).strict().superRefine((lock, ctx) => {
98
+ const assetKeys = lock.assets.map((asset) => `${asset.kind}\0${asset.path}`);
99
+ if (new Set(assetKeys).size !== assetKeys.length) {
100
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["assets"], message: "duplicate locked asset" });
101
+ }
102
+ const dependencyNames = lock.dependencies.map((dependency) => dependency.name);
103
+ if (new Set(dependencyNames).size !== dependencyNames.length) {
104
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["dependencies"], message: "duplicate dependency name" });
105
+ }
106
+ });
107
+ export const AbilityResolveCommandSchema = z.object({
108
+ type: z.literal("agent:ability:resolve"), reqId: z.string().min(1).max(128),
109
+ handle: z.string().min(1).max(64), repositoryUrl: AbilityRepositoryUrlSchema,
110
+ branchGlob: AbilityBranchGlobSchema, desiredCommit: CommitSchema.optional(),
111
+ }).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
+ 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: {
@@ -501,7 +501,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
501
501
  runtime: runtime.name,
502
502
  bin: runtime.name === "deepseek-harness" ? "dsh-acp-demo" : runtime.name,
503
503
  cwd: executionWorkspace.runDir,
504
- ...(input.projectSkills === undefined ? {} : { agentRoot: workspace.dir }),
504
+ ...(input.projectSkills === undefined && input.abilityRelease === undefined ? {} : { agentRoot: workspace.dir }),
505
505
  systemPromptPath: workspace.systemPromptPath,
506
506
  systemPrompt,
507
507
  wakePrompt,
@@ -515,9 +515,12 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
515
515
  ? { imagePaths: attachmentPlan.nativeImagePaths }
516
516
  : {}),
517
517
  };
518
+ const launchWithAbility = () => input.abilityRelease !== undefined && dependencies.abilityRelease !== undefined
519
+ ? dependencies.abilityRelease.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.abilityRelease, () => launchRuntime(launchRequest))
520
+ : launchRuntime(launchRequest);
518
521
  const child = input.projectSkills !== undefined && dependencies.projectSkills !== undefined
519
- ? await dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.projectSkills, () => launchRuntime(launchRequest))
520
- : await launchRuntime(launchRequest);
522
+ ? await dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.projectSkills, launchWithAbility)
523
+ : await launchWithAbility();
521
524
  localMemoryTelemetry?.markRuntimeStarted();
522
525
  memoryPruneFailurePhase = "runtime_execution";
523
526
  if (child.cancel !== undefined) {
@@ -33,10 +33,12 @@ export const DAEMON_CAPABILITIES = [
33
33
  "execution_agent_memory_policy_v1",
34
34
  "project_skills_v1",
35
35
  RUNTIME_HEALTH_PROBE_CAPABILITY,
36
+ "agent_ability_release_v1",
36
37
  ];
37
38
  export const daemonCapabilities = (runtimePlatform = process.platform) => runtimePlatform === "darwin" || runtimePlatform === "linux"
38
39
  ? DAEMON_CAPABILITIES
39
- : DAEMON_CAPABILITIES.filter((capability) => capability !== "project_skills_v1");
40
+ : DAEMON_CAPABILITIES.filter((capability) => capability !== "project_skills_v1"
41
+ && capability !== "agent_ability_release_v1");
40
42
  export const EXECUTION_PROTOCOL = Object.freeze({ min: 1, max: 1 });
41
43
  /** 候选 runtime CLI:展示名 → 可执行文件名。 */
42
44
  const RUNTIME_BINS = [