@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,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),
|
package/dist/execution-runner.js
CHANGED
|
@@ -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: {
|