@nowcrew/daemon 0.6.37 → 0.6.39

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.
@@ -15,7 +15,7 @@ const isRealDirectory = (path) => lstat(path)
15
15
  const repositoryKey = (url) => createHash("sha256").update(url).digest("hex");
16
16
  const posixPath = (value) => value.split(sep).join("/");
17
17
  const sameSnapshot = (left, right) => JSON.stringify(left) === JSON.stringify(right);
18
- const readonlyTree = async (root) => {
18
+ const readonlyTree = async (root, options = {}) => {
19
19
  const entries = await readdir(root, { withFileTypes: true });
20
20
  for (const entry of entries) {
21
21
  const path = join(root, entry.name);
@@ -28,7 +28,8 @@ const readonlyTree = async (root) => {
28
28
  await chmod(path, current.mode & 0o111 ? 0o555 : 0o444);
29
29
  }
30
30
  }
31
- await chmod(root, 0o555);
31
+ // Keep the training root renameable on macOS; its children still carry the package ownership mode.
32
+ await chmod(root, options.rootWritable === true ? 0o700 : 0o555);
32
33
  };
33
34
  const writableTree = async (root) => {
34
35
  const info = await lstat(root);
@@ -42,6 +43,44 @@ const writableTree = async (root) => {
42
43
  for (const entry of await readdir(root))
43
44
  await writableTree(join(root, entry));
44
45
  };
46
+ const writableDirectories = async (root) => {
47
+ const info = await lstat(root);
48
+ if (info.isSymbolicLink() || !info.isDirectory())
49
+ return;
50
+ await chmod(root, 0o700);
51
+ for (const entry of await readdir(root))
52
+ await writableDirectories(join(root, entry));
53
+ };
54
+ const copyMissingEntries = async (source, target) => {
55
+ const sourceInfo = await lstat(source).catch(() => null);
56
+ if (sourceInfo === null || sourceInfo.isSymbolicLink() || !sourceInfo.isDirectory())
57
+ return;
58
+ await mkdir(target, { recursive: true });
59
+ for (const entry of await readdir(source, { withFileTypes: true })) {
60
+ const sourceEntry = join(source, entry.name);
61
+ const targetEntry = join(target, entry.name);
62
+ if (await exists(targetEntry)) {
63
+ if (entry.isDirectory() && await isRealDirectory(targetEntry)) {
64
+ await copyMissingEntries(sourceEntry, targetEntry);
65
+ }
66
+ continue;
67
+ }
68
+ if (entry.isSymbolicLink())
69
+ continue;
70
+ if (entry.isDirectory()) {
71
+ await mkdir(targetEntry, { recursive: true });
72
+ await copyMissingEntries(sourceEntry, targetEntry);
73
+ continue;
74
+ }
75
+ const sourceEntryInfo = await lstat(sourceEntry);
76
+ // Read-only files belong to the package, so a removed package file must not
77
+ // be mistaken for runtime state during a later Release refresh.
78
+ if ((sourceEntryInfo.mode & 0o222) === 0)
79
+ continue;
80
+ await cp(sourceEntry, targetEntry, { recursive: true, preserveTimestamps: true });
81
+ await writableTree(targetEntry);
82
+ }
83
+ };
45
84
  const removeManagedTree = async (root) => {
46
85
  if (!await exists(root))
47
86
  return;
@@ -271,14 +310,17 @@ const finishTrainingRestore = async (agentRoot) => {
271
310
  const hasRollback = await assertManagedTrainingTreeIfPresent(rollbackRoot, "ability_training_rollback_invalid");
272
311
  const hasDiscard = await assertManagedTrainingTreeIfPresent(discardRoot, "ability_training_discard_invalid");
273
312
  if (hasTraining && hasRollback && !hasDiscard) {
313
+ await chmod(trainingRoot, 0o700);
274
314
  await rename(trainingRoot, discardRoot);
275
315
  await writeTrainingSwitchJournal(agentRoot, { operation: "restore", phase: "current-moved" });
276
316
  }
277
317
  if (!await exists(trainingRoot) && await exists(rollbackRoot)) {
318
+ await chmod(rollbackRoot, 0o700);
278
319
  await rename(rollbackRoot, trainingRoot);
279
320
  await writeTrainingSwitchJournal(agentRoot, { operation: "restore", phase: "restored" });
280
321
  }
281
322
  if (!await exists(trainingRoot) && !await exists(rollbackRoot) && await exists(discardRoot)) {
323
+ await chmod(discardRoot, 0o700);
282
324
  await rename(discardRoot, trainingRoot);
283
325
  }
284
326
  if (!await exists(trainingRoot))
@@ -304,9 +346,11 @@ const recoverTrainingSwitch = async (agentRoot) => {
304
346
  await removeManagedTree(staging);
305
347
  }
306
348
  else if (await exists(staging)) {
349
+ await chmod(staging, 0o700);
307
350
  await rename(staging, trainingRoot);
308
351
  }
309
352
  else if (await exists(rollbackRoot)) {
353
+ await chmod(rollbackRoot, 0o700);
310
354
  await rename(rollbackRoot, trainingRoot);
311
355
  }
312
356
  else {
@@ -327,6 +371,9 @@ const switchTrainingDirectory = async (agentRoot, staging) => {
327
371
  });
328
372
  try {
329
373
  if (await exists(trainingRoot)) {
374
+ // Older materializer versions made the root itself read-only. macOS requires
375
+ // a writable source directory for rename, while its package children stay protected.
376
+ await chmod(trainingRoot, 0o700);
330
377
  await rename(trainingRoot, rollbackRoot);
331
378
  await writeTrainingSwitchJournal(agentRoot, {
332
379
  operation: "install", phase: "previous-renamed", stagingName: basename(staging),
@@ -489,11 +536,18 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
489
536
  })),
490
537
  evals: evalPaths.map((path) => `training/evals/${layerRelativePath(path, "evals")}`),
491
538
  }, null, 2), { encoding: "utf8", mode: 0o600 });
492
- await readonlyTree(staging);
539
+ await readonlyTree(staging, { rootWritable: true });
493
540
  for (const asset of ability.workspace) {
494
541
  if (asset.mode === "managed-copy") {
495
542
  await writableTree(join(staging, "workspace", abilityWorkspaceProjectionPath(asset.target)));
496
543
  }
544
+ else if (asset.mode === "managed-runtime") {
545
+ const target = join(staging, "workspace", abilityWorkspaceProjectionPath(asset.target));
546
+ await writableDirectories(target);
547
+ const previous = join(trainingRoot, "workspace", abilityWorkspaceProjectionPath(asset.target));
548
+ if (previousCatalog !== null && await exists(previous))
549
+ await copyMissingEntries(previous, target);
550
+ }
497
551
  }
498
552
  await switchTrainingDirectory(agentRoot, staging);
499
553
  }
@@ -6,6 +6,7 @@ import { promisify } from "node:util";
6
6
  import { parse } from "yaml";
7
7
  import { z } from "zod";
8
8
  import { abilityWorkspaceProjectionPath, DaemonAbilityLockSchema, DaemonAbilityManifestSchema, matchesAbilityBranch, } from "./types.js";
9
+ import { PROJECT_MANIFEST_SOURCE_PATH, ProjectDependencyManifestSchema, } from "../project-dependency-manifest.js";
9
10
  const runFile = promisify(execFile);
10
11
  const MAX_FILES = 5_000;
11
12
  const MAX_TOTAL_BYTES = 25 * 1024 * 1024;
@@ -76,6 +77,22 @@ const git = async (args, cwd) => {
76
77
  });
77
78
  return result.stdout.trim();
78
79
  };
80
+ export const projectDependencyIds = (files, manifest) => {
81
+ const assets = manifest.spec.workspace.assets.filter((asset) => asset.source === PROJECT_MANIFEST_SOURCE_PATH);
82
+ if (assets.length === 0)
83
+ return Object.freeze([]);
84
+ if (assets.length > 1)
85
+ throw new Error("ability_project_dependency_manifest_duplicate");
86
+ const file = files.find((candidate) => candidate.path === PROJECT_MANIFEST_SOURCE_PATH);
87
+ if (file === undefined)
88
+ throw new Error("ability_project_dependency_manifest_missing");
89
+ try {
90
+ return Object.freeze(ProjectDependencyManifestSchema.parse(parse(file.content.toString("utf8"))).projects.map((project) => project.id));
91
+ }
92
+ catch {
93
+ throw new Error("ability_project_dependency_manifest_invalid");
94
+ }
95
+ };
79
96
  export const collectAbilityFiles = async (root) => {
80
97
  const files = [];
81
98
  let totalBytes = 0;
@@ -404,6 +421,7 @@ export async function resolveAbilityRelease(agentsRoot, command) {
404
421
  if (memory.reduce((bytes, entry) => bytes + Buffer.byteLength(entry.content, "utf8"), 0) > MAX_MEMORY_BYTES) {
405
422
  throw new Error("ability_memory_size_exceeded");
406
423
  }
424
+ const projectDependencies = projectDependencyIds(files, manifest);
407
425
  return {
408
426
  branch: resolved.branch, rootCommit: resolved.commit, treeDigest: resolved.treeDigest,
409
427
  artifactDigest, schemaVersion: manifest.apiVersion, manifest, lock, assetSummary,
@@ -419,6 +437,7 @@ export async function resolveAbilityRelease(agentsRoot, command) {
419
437
  mode: skill.mode,
420
438
  })),
421
439
  workspace: manifest.spec.workspace.assets, evals: manifest.spec.evals.cases,
440
+ projectDependencies,
422
441
  },
423
442
  };
424
443
  }
@@ -48,7 +48,7 @@ export function matchesAbilityBranch(glob, branch) {
48
48
  }
49
49
  return new RegExp(`${pattern}$`, "u").test(branch);
50
50
  }
51
- const ModeSchema = z.enum(["managed-readonly", "managed-copy", "overlay", "generated"]);
51
+ const ModeSchema = z.enum(["managed-readonly", "managed-copy", "managed-runtime", "overlay", "generated"]);
52
52
  const LocalSkillSchema = z.object({ type: z.literal("local"), path: SafeAbilityPathSchema, catalog: SafeAbilityPathSchema.optional() }).strict();
53
53
  const GitSkillSchema = z.object({
54
54
  type: z.literal("git"), url: AbilityRepositoryUrlSchema, ref: z.string().min(1).max(256), path: SafeAbilityPathSchema,
@@ -80,8 +80,8 @@ export const DaemonAbilityManifestSchema = z.object({
80
80
  minNowCrewVersion: z.string().regex(/^\d+\.\d+\.\d+$/u),
81
81
  }).strict().optional(),
82
82
  permissions: z.object({
83
- network: z.literal("declared-only"), executableAssets: z.literal("skill-declared-only"),
84
- secrets: z.literal("runtime-only"), productionWrites: z.literal("human-confirmed").optional(),
83
+ network: z.literal("declared-only"), executableAssets: z.enum(["skill-declared-only", "declared-only"]),
84
+ secrets: z.enum(["runtime-only", "controlled-memory"]), productionWrites: z.literal("human-confirmed").optional(),
85
85
  }).strict(),
86
86
  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(),
87
87
  }).strict(),
@@ -95,6 +95,16 @@ export const DaemonAbilityManifestSchema = z.object({
95
95
  unique(manifest.spec.skills.map((skill) => skill.name.replace(/[^A-Za-z0-9._-]/gu, "_")), ["spec", "skills"], "Skill projection");
96
96
  unique(manifest.spec.skills.map((skill) => skill.mount), ["spec", "skills"], "Skill mount");
97
97
  unique(manifest.spec.workspace.assets.map((asset) => asset.target), ["spec", "workspace", "assets"], "Workspace target");
98
+ for (const asset of manifest.spec.workspace.assets) {
99
+ if (asset.source === "workspace/project-dependencies.yaml"
100
+ && asset.target !== "training/workspace/project-dependencies.yaml") {
101
+ ctx.addIssue({
102
+ code: z.ZodIssueCode.custom,
103
+ path: ["spec", "workspace", "assets"],
104
+ message: "project dependency manifest target must be training/workspace/project-dependencies.yaml",
105
+ });
106
+ }
107
+ }
98
108
  unique(manifest.spec.evals.cases, ["spec", "evals", "cases"], "Eval case");
99
109
  });
100
110
  export const DaemonAbilityLockSchema = z.object({
@@ -18,6 +18,7 @@ import { ProjectSkillRuntimeOwnershipUnverifiedError, } from "./project-skills/r
18
18
  import { projectSkillExecutionProjection, projectSkillProjectionErrorCode } from "./project-skills/execution-adapter.js";
19
19
  import { redactProjectSkillRuntimeRootError, } from "./project-skills/runtime-launch.js";
20
20
  import { createProjectRegistry } from "./project-skills/registry.js";
21
+ import { ProjectDependencyError, } from "./project-dependencies.js";
21
22
  import { ProjectContextUnavailableError, resolveProjectContext } from "./project-workspaces/resolver.js";
22
23
  import { PROJECT_WORKSPACES_CAPABILITY } from "./machine-info.js";
23
24
  import { PROJECT_SKILL_PROJECTION_V2_CAPABILITY } from "./project-skills/types.js";
@@ -333,7 +334,9 @@ function failedCompletion(spec, error, startedAt, finishedAt) {
333
334
  ? "queue_timeout"
334
335
  : error instanceof ProjectContextUnavailableError
335
336
  ? error.code
336
- : projectSkillProjectionErrorCode(error) ?? "local_execution_failed";
337
+ : error instanceof ProjectDependencyError
338
+ ? error.code
339
+ : projectSkillProjectionErrorCode(error) ?? "local_execution_failed";
337
340
  return ExecutionCompletedSchema.parse({
338
341
  type: "execution:completed",
339
342
  protocolVersion: 1,
@@ -505,7 +508,31 @@ export async function runExecution(config, input, dependencies) {
505
508
  ? {}
506
509
  : { primaryProjectId: logicalProjectContext.primaryProjectId }),
507
510
  };
508
- projectContext = await cancellable(resolveProjectContext(projectSnapshot, dependencies.projectWorkspaceRegistry ?? createProjectRegistry(config.agentsRoot)), dependencies.cancellation);
511
+ const projectWorkspaceRegistry = dependencies.projectWorkspaceRegistry
512
+ ?? dependencies.projectRegistry
513
+ ?? createProjectRegistry(config.agentsRoot);
514
+ if (spec.agent.abilityRelease !== undefined && dependencies.projectDependencies !== undefined) {
515
+ if (dependencies.abilityRelease !== undefined) {
516
+ await cancellable(dependencies.abilityRelease.apply(config.agentsRoot, spec.agent.handle, spec.agent.abilityRelease), dependencies.cancellation);
517
+ }
518
+ const preparedProjectDependencies = await cancellable(dependencies.projectDependencies.prepare({
519
+ trainingRoot: join(config.agentsRoot, spec.agent.handle, "training"),
520
+ projectsRoot: join(config.agentsRoot, "Projects"),
521
+ projectIds: projectSnapshot.projectIds,
522
+ }), dependencies.cancellation);
523
+ for (const project of preparedProjectDependencies) {
524
+ dslog("project_dependencies.prepared", "Training project dependency prepared", {
525
+ execution_id: spec.executionId,
526
+ agent_handle: spec.agent.handle,
527
+ project_id: project.projectId,
528
+ project_branch: project.branch,
529
+ project_branch_source: project.branchSource,
530
+ project_commit: project.resolvedCommit,
531
+ project_checkout_mode: project.checkoutMode,
532
+ });
533
+ }
534
+ }
535
+ projectContext = await cancellable(resolveProjectContext(projectSnapshot, projectWorkspaceRegistry), dependencies.cancellation);
509
536
  sessionContextFingerprint = projectSessionContextFingerprint(spec.runtime.name, projectSnapshot);
510
537
  }
511
538
  let recalledMemory = "";
@@ -469,6 +469,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
469
469
  CREW_TOKEN: input.launch.token,
470
470
  CREW_CHANNEL: input.channelId,
471
471
  CREW_HOME: workspace.dir,
472
+ // Expose only the training root; ability-specific workspace layout stays
473
+ // owned by the training package rather than the NowWork daemon.
474
+ QA_RUNTIME_ROOT: join(workspace.dir, "training"),
472
475
  CREW_TASK_DIR: executionWorkspace.runDir,
473
476
  CREW_TASK_LOG: executionWorkspace.workLogPath,
474
477
  ...(input.wakeMessageId ? { CREW_WAKE_MESSAGE_ID: input.wakeMessageId } : {}),
@@ -23,6 +23,7 @@ import { daemonHome } from "./computer-profile.js";
23
23
  export { executableRuntimes } from "./runtime-capabilities.js";
24
24
  const execFileP = promisify(execFile);
25
25
  export const PROJECT_WORKSPACES_CAPABILITY = "project_workspaces_v1";
26
+ export const TRAINING_PROJECT_DEPENDENCIES_CAPABILITY = "training_project_dependencies_v1";
26
27
  export const NATIVE_PROJECT_WORKSPACE_READINESS = Object.freeze({
27
28
  resolverReady: true,
28
29
  nativeRuntimeAdaptersReady: true,
@@ -40,6 +41,7 @@ export const DAEMON_CAPABILITIES = Object.freeze([
40
41
  "execution_agent_memory_mapping_v1",
41
42
  "project_skills_v1",
42
43
  PROJECT_WORKSPACES_CAPABILITY,
44
+ TRAINING_PROJECT_DEPENDENCIES_CAPABILITY,
43
45
  RUNTIME_HEALTH_PROBE_CAPABILITY,
44
46
  "agent_ability_release_v1",
45
47
  "agent_ability_workspace_v1",
package/dist/main.js CHANGED
File without changes
@@ -0,0 +1,254 @@
1
+ import { execFile } from "node:child_process";
2
+ import { lstat, mkdir, readFile, rm } from "node:fs/promises";
3
+ import { dirname, join, resolve, sep } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { parse } from "yaml";
6
+ import { validateAbilityGitHost } from "./agent-ability/resolver.js";
7
+ import { PROJECT_BRANCH, PROJECT_BRANCH_FORBIDDEN, PROJECT_MANIFEST_SOURCE_PATH, ProjectDependencyManifestSchema, } from "./project-dependency-manifest.js";
8
+ const runFile = promisify(execFile);
9
+ const PROJECT_MANIFEST_RELATIVE_PATH = PROJECT_MANIFEST_SOURCE_PATH;
10
+ const PROJECT_MANIFEST_MAX_BYTES = 128 * 1024;
11
+ const GIT_ARGS = ["-c", "core.hooksPath=/dev/null"];
12
+ export { ProjectDependencyManifestSchema } from "./project-dependency-manifest.js";
13
+ export class ProjectDependencyError extends Error {
14
+ code;
15
+ detail;
16
+ projectId;
17
+ constructor(code, detail = undefined, projectId) {
18
+ super(code);
19
+ this.code = code;
20
+ this.detail = detail;
21
+ this.projectId = projectId;
22
+ this.name = "ProjectDependencyError";
23
+ }
24
+ }
25
+ const gitEnv = () => ({
26
+ ...process.env,
27
+ GIT_CONFIG_NOSYSTEM: "1",
28
+ GIT_CONFIG_GLOBAL: process.platform === "win32" ? "NUL" : "/dev/null",
29
+ GIT_TERMINAL_PROMPT: "0",
30
+ });
31
+ const defaultGit = {
32
+ async run(args, cwd) {
33
+ try {
34
+ const result = await runFile("git", [...GIT_ARGS, ...args], {
35
+ ...(cwd === undefined ? {} : { cwd }),
36
+ env: gitEnv(),
37
+ timeout: 120_000,
38
+ maxBuffer: 16 * 1024 * 1024,
39
+ });
40
+ return result.stdout.trim();
41
+ }
42
+ catch (error) {
43
+ const detail = error instanceof Error ? error.message : String(error);
44
+ throw new ProjectDependencyError("project_dependency_checkout_failed", detail);
45
+ }
46
+ },
47
+ };
48
+ const safeBranchDirectory = (branch) => branch
49
+ .replaceAll("/", "__")
50
+ .replace(/[^A-Za-z0-9._-]/gu, "_")
51
+ .replace(/^[-.]+/u, "_")
52
+ .slice(0, 128) || "default";
53
+ const projectManifestPath = (trainingRoot) => resolve(trainingRoot, PROJECT_MANIFEST_RELATIVE_PATH);
54
+ const isWithin = (root, target) => {
55
+ const resolvedRoot = resolve(root);
56
+ const resolvedTarget = resolve(target);
57
+ return resolvedTarget === resolvedRoot || resolvedTarget.startsWith(`${resolvedRoot}${sep}`);
58
+ };
59
+ async function readProjectManifest(trainingRoot) {
60
+ const path = projectManifestPath(trainingRoot);
61
+ let raw;
62
+ try {
63
+ raw = await readFile(path, "utf8");
64
+ }
65
+ catch (error) {
66
+ if (error.code === "ENOENT")
67
+ return null;
68
+ throw new ProjectDependencyError("project_dependency_manifest_invalid", "cannot read project dependency manifest");
69
+ }
70
+ if (Buffer.byteLength(raw, "utf8") > PROJECT_MANIFEST_MAX_BYTES) {
71
+ throw new ProjectDependencyError("project_dependency_manifest_invalid", "project dependency manifest is too large");
72
+ }
73
+ try {
74
+ return ProjectDependencyManifestSchema.parse(parse(raw));
75
+ }
76
+ catch {
77
+ throw new ProjectDependencyError("project_dependency_manifest_invalid", "project dependency manifest schema is invalid");
78
+ }
79
+ }
80
+ async function resolveDefaultBranch(git, repo, projectId) {
81
+ let output;
82
+ try {
83
+ output = await git.run(["ls-remote", "--symref", repo, "HEAD"]);
84
+ }
85
+ catch (error) {
86
+ throw new ProjectDependencyError("project_dependency_default_branch_unavailable", `cannot resolve remote default branch for ${projectId}`, projectId);
87
+ }
88
+ const branch = output.split(/\r?\n/u)
89
+ .map((line) => line.match(/^ref:\s+refs\/heads\/([^\s]+)\s+HEAD$/u)?.[1])
90
+ .find((value) => value !== undefined);
91
+ if (branch === undefined || !PROJECT_BRANCH.test(branch) || PROJECT_BRANCH_FORBIDDEN.test(branch)) {
92
+ throw new ProjectDependencyError("project_dependency_default_branch_unavailable", `remote default branch is unavailable for ${projectId}`, projectId);
93
+ }
94
+ return branch;
95
+ }
96
+ async function checkoutProject(git, project, branch, target) {
97
+ const targetInfo = await lstat(target).catch(() => null);
98
+ if (targetInfo?.isSymbolicLink() || (targetInfo !== null && !targetInfo.isDirectory())) {
99
+ throw new ProjectDependencyError("project_dependency_checkout_conflict", "checkout target is not a directory", project.id);
100
+ }
101
+ const created = targetInfo === null;
102
+ if (created) {
103
+ await mkdir(dirname(target), { recursive: true });
104
+ try {
105
+ await git.run(["clone", "--single-branch", "--branch", branch, project.repo, target]);
106
+ }
107
+ catch (error) {
108
+ await rm(target, { recursive: true, force: true }).catch(() => { });
109
+ throw new ProjectDependencyError("project_dependency_checkout_failed", error instanceof Error ? error.message : String(error), project.id);
110
+ }
111
+ }
112
+ else {
113
+ let origin;
114
+ try {
115
+ origin = await git.run(["remote", "get-url", "origin"], target);
116
+ if (origin !== project.repo) {
117
+ throw new ProjectDependencyError("project_dependency_checkout_conflict", "checkout origin does not match manifest", project.id);
118
+ }
119
+ const dirty = await git.run(["status", "--porcelain"], target);
120
+ if (dirty.length > 0) {
121
+ throw new ProjectDependencyError("project_dependency_checkout_dirty", "checkout has local changes", project.id);
122
+ }
123
+ await git.run(["fetch", "--no-tags", "origin", branch], target);
124
+ await git.run(["checkout", "--force", "-B", branch, `origin/${branch}`], target);
125
+ }
126
+ catch (error) {
127
+ if (error instanceof ProjectDependencyError)
128
+ throw error;
129
+ throw new ProjectDependencyError("project_dependency_checkout_conflict", `cannot reuse checkout for ${project.id}`, project.id);
130
+ }
131
+ }
132
+ try {
133
+ return {
134
+ resolvedCommit: await git.run(["rev-parse", "HEAD"], target),
135
+ created,
136
+ };
137
+ }
138
+ catch {
139
+ throw new ProjectDependencyError("project_dependency_checkout_failed", "checkout commit cannot be resolved", project.id);
140
+ }
141
+ }
142
+ const existingAncestorIsSymlink = async (root, target) => {
143
+ const resolvedRoot = resolve(root);
144
+ let current = resolve(target);
145
+ while (current !== resolvedRoot && isWithin(resolvedRoot, current)) {
146
+ const info = await lstat(current).catch(() => null);
147
+ if (info?.isSymbolicLink())
148
+ return true;
149
+ current = dirname(current);
150
+ }
151
+ const rootInfo = await lstat(resolvedRoot).catch(() => null);
152
+ return rootInfo?.isSymbolicLink() === true;
153
+ };
154
+ async function ensureRegistered(registry, project, root) {
155
+ let registrations;
156
+ try {
157
+ registrations = await registry.list();
158
+ }
159
+ catch {
160
+ throw new ProjectDependencyError("project_dependency_registration_conflict", `cannot read project registry for ${project.id}`, project.id);
161
+ }
162
+ const existing = registrations.find((candidate) => candidate.projectId === project.id);
163
+ if (existing === undefined) {
164
+ try {
165
+ await registry.add(project.id, root);
166
+ }
167
+ catch (error) {
168
+ throw new ProjectDependencyError("project_dependency_registration_conflict", `cannot register project ${project.id}`, project.id);
169
+ }
170
+ return;
171
+ }
172
+ if (resolve(existing.root) === resolve(root))
173
+ return;
174
+ // A manifest branch change intentionally moves the logical project ID to the
175
+ // new checkout. Keep the old registration if the replacement cannot finish.
176
+ try {
177
+ const removed = await registry.remove(project.id);
178
+ if (!removed) {
179
+ throw new Error("project registration disappeared before replacement");
180
+ }
181
+ try {
182
+ await registry.add(project.id, root);
183
+ }
184
+ catch (error) {
185
+ await registry.add(project.id, existing.root).catch(() => { });
186
+ throw error;
187
+ }
188
+ }
189
+ catch {
190
+ throw new ProjectDependencyError("project_dependency_registration_conflict", `cannot move project ${project.id} registration to the prepared checkout`, project.id);
191
+ }
192
+ }
193
+ export function createProjectDependencyProvisioner(options) {
194
+ const git = options.git ?? defaultGit;
195
+ return {
196
+ async prepare(input) {
197
+ const manifest = await readProjectManifest(input.trainingRoot);
198
+ if (manifest === null || input.projectIds.length === 0)
199
+ return Object.freeze([]);
200
+ if (!isWithin(input.trainingRoot, projectManifestPath(input.trainingRoot))) {
201
+ throw new ProjectDependencyError("project_dependency_manifest_invalid", "project manifest path escapes training root");
202
+ }
203
+ const byId = new Map(manifest.projects.map((project) => [project.id, project]));
204
+ const requested = [...new Set(input.projectIds)];
205
+ const results = [];
206
+ for (const projectId of requested) {
207
+ const project = byId.get(projectId);
208
+ if (project === undefined) {
209
+ throw new ProjectDependencyError("project_dependency_not_declared", `project ${projectId} is not declared by the active training package`, projectId);
210
+ }
211
+ try {
212
+ validateAbilityGitHost(project.repo);
213
+ }
214
+ catch {
215
+ throw new ProjectDependencyError("project_dependency_host_not_allowed", "repository host is not allowed", project.id);
216
+ }
217
+ const branchSource = project.branch === undefined ? "remote-default" : "manifest";
218
+ const branch = project.branch ?? await resolveDefaultBranch(git, project.repo, project.id);
219
+ const root = resolve(input.projectsRoot, project.id, safeBranchDirectory(branch));
220
+ if (!isWithin(input.projectsRoot, root)) {
221
+ throw new ProjectDependencyError("project_dependency_checkout_conflict", "checkout path escapes Projects root", project.id);
222
+ }
223
+ if (await existingAncestorIsSymlink(input.projectsRoot, root)) {
224
+ throw new ProjectDependencyError("project_dependency_checkout_conflict", "checkout path contains a symbolic link", project.id);
225
+ }
226
+ const checkout = await checkoutProject(git, project, branch, root);
227
+ try {
228
+ await ensureRegistered(options.registry, project, root);
229
+ }
230
+ catch (error) {
231
+ if (checkout.created)
232
+ await rm(root, { recursive: true, force: true }).catch(() => { });
233
+ throw error;
234
+ }
235
+ results.push(Object.freeze({
236
+ projectId: project.id,
237
+ repo: project.repo,
238
+ branch,
239
+ branchSource,
240
+ resolvedCommit: checkout.resolvedCommit,
241
+ checkoutMode: "shared",
242
+ localRoot: root,
243
+ summary: project.summary,
244
+ }));
245
+ }
246
+ return Object.freeze(results);
247
+ },
248
+ };
249
+ }
250
+ export async function listProjectDependencies(trainingRoot) {
251
+ const manifest = await readProjectManifest(trainingRoot);
252
+ return Object.freeze(manifest?.projects ?? []);
253
+ }
254
+ export { PROJECT_MANIFEST_RELATIVE_PATH };
@@ -0,0 +1,30 @@
1
+ import { z } from "zod";
2
+ import { AbilityRepositoryUrlSchema } from "./agent-ability/types.js";
3
+ import { isProjectId } from "./project-skills/types.js";
4
+ export const PROJECT_MANIFEST_SOURCE_PATH = "workspace/project-dependencies.yaml";
5
+ export const PROJECT_BRANCH = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,255}$/u;
6
+ export const PROJECT_BRANCH_FORBIDDEN = /(?:^|\/)\.\.?(?:\/|$)|[\s\p{Cc}\\]/u;
7
+ export const PROJECT_SUMMARY_MAX_LENGTH = 1_000;
8
+ const ProjectDependencySchema = z.object({
9
+ id: z.string().min(1).max(64),
10
+ repo: AbilityRepositoryUrlSchema,
11
+ branch: z.string().min(1).max(256).optional(),
12
+ summary: z.string().min(1).max(PROJECT_SUMMARY_MAX_LENGTH),
13
+ }).strict().superRefine((project, ctx) => {
14
+ if (!isProjectId(project.id)) {
15
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["id"], message: "invalid project id" });
16
+ }
17
+ if (project.branch !== undefined
18
+ && (!PROJECT_BRANCH.test(project.branch) || PROJECT_BRANCH_FORBIDDEN.test(project.branch))) {
19
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["branch"], message: "invalid project branch" });
20
+ }
21
+ });
22
+ export const ProjectDependencyManifestSchema = z.object({
23
+ version: z.literal(1),
24
+ projects: z.array(ProjectDependencySchema).min(1).max(32),
25
+ }).strict().superRefine((manifest, ctx) => {
26
+ const ids = manifest.projects.map((project) => project.id);
27
+ if (new Set(ids).size !== ids.length) {
28
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["projects"], message: "duplicate project id" });
29
+ }
30
+ });
@@ -0,0 +1,29 @@
1
+ import { createProjectDependencyProvisioner } from "./project-dependencies.js";
2
+ import { createProjectRegistry } from "./project-skills/registry.js";
3
+ const writableRegistry = (registry) => {
4
+ if (registry === undefined)
5
+ return undefined;
6
+ const candidate = registry;
7
+ return typeof candidate.add === "function" && typeof candidate.remove === "function"
8
+ ? registry
9
+ : undefined;
10
+ };
11
+ export function createProjectRegistryWiring(agentsRoot, input = {}) {
12
+ const writableInjectedRegistry = writableRegistry(input.projectWorkspaceRegistry);
13
+ const projectRegistry = input.projectRegistry
14
+ ?? writableInjectedRegistry
15
+ ?? createProjectRegistry(agentsRoot);
16
+ const projectWorkspaceRegistry = input.projectWorkspaceRegistry ?? projectRegistry;
17
+ if (input.projectDependencies !== undefined && input.projectRegistry === undefined) {
18
+ throw new Error("project_dependency_injection_requires_project_registry");
19
+ }
20
+ if (input.projectWorkspaceRegistry !== undefined && projectWorkspaceRegistry !== projectRegistry) {
21
+ throw new Error("project_registry_injection_requires_writable_registry");
22
+ }
23
+ return Object.freeze({
24
+ projectRegistry,
25
+ projectWorkspaceRegistry,
26
+ projectDependencyProvisioner: input.projectDependencies
27
+ ?? createProjectDependencyProvisioner({ registry: projectRegistry }),
28
+ });
29
+ }
package/dist/serve.js CHANGED
@@ -35,7 +35,7 @@ import { detectDaemonUpdateEligibility, managedDaemonCapabilities, } from "./dae
35
35
  import { createDaemonUpdateController } from "./daemon-update-controller.js";
36
36
  import { installExactDaemonUpdate, prepareDaemonRestart } from "./daemon-updater.js";
37
37
  import { scheduleServiceRestart } from "./computer-service.js";
38
- import { createProjectRegistry } from "./project-skills/registry.js";
38
+ import { createProjectRegistryWiring } from "./project-registry-wiring.js";
39
39
  import { createProjectSkillsController, } from "./project-skills/controller.js";
40
40
  import { PROJECT_SKILLS_CAPABILITY } from "./project-skills/types.js";
41
41
  import { createAgentProjectionCoordinator } from "./project-skills/agent-projection-coordinator.js";
@@ -138,6 +138,7 @@ export function serve(config, opts = {}) {
138
138
  }
139
139
  catch { /* reconnect */ } } });
140
140
  const projectionCoordinator = createAgentProjectionCoordinator();
141
+ const { projectRegistry, projectWorkspaceRegistry, projectDependencyProvisioner } = createProjectRegistryWiring(config.agentsRoot, opts.execution?.dependencies);
141
142
  const agentAbilityRuntime = createAgentAbilityRuntime(config.agentsRoot, {
142
143
  ...opts.agentAbility,
143
144
  coordinator: projectionCoordinator,
@@ -150,7 +151,7 @@ export function serve(config, opts = {}) {
150
151
  protectedExecutionIds,
151
152
  });
152
153
  projectSkillsController = opts.projectSkills?.controller ?? createProjectSkillsController({
153
- registry: createProjectRegistry(config.agentsRoot),
154
+ registry: projectRegistry,
154
155
  publish: (frame) => {
155
156
  if (!serverCapabilities.has(PROJECT_SKILLS_CAPABILITY))
156
157
  return;
@@ -618,6 +619,8 @@ export function serve(config, opts = {}) {
618
619
  capabilities: capabilities.executionRunner,
619
620
  projectSkills: opts.execution?.dependencies?.projectSkills ?? initializedProjectSkills,
620
621
  abilityRelease: opts.execution?.dependencies?.abilityRelease ?? agentAbilityRuntime.materializer,
622
+ projectWorkspaceRegistry,
623
+ projectDependencies: projectDependencyProvisioner,
621
624
  ...(agentMemory === undefined ? {} : { agentMemory }),
622
625
  journal: executionJournal,
623
626
  facts: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.37",
3
+ "version": "0.6.39",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",
@@ -16,6 +16,14 @@
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },
19
+ "scripts": {
20
+ "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
21
+ "build": "tsc -p tsconfig.json",
22
+ "codex-home:migrate": "tsx src/runtimes/codex-home-migration-cli.ts",
23
+ "prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
24
+ "test": "vitest run",
25
+ "typecheck": "tsc --noEmit"
26
+ },
19
27
  "dependencies": {
20
28
  "@agentclientprotocol/sdk": "1.2.1",
21
29
  "@nowcrew/cli": "^0.4.13",
@@ -34,12 +42,5 @@
34
42
  "tsx": "^4.19.0",
35
43
  "typescript": "^5.6.0",
36
44
  "vitest": "^2.1.0"
37
- },
38
- "scripts": {
39
- "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
40
- "build": "tsc -p tsconfig.json",
41
- "codex-home:migrate": "tsx src/runtimes/codex-home-migration-cli.ts",
42
- "test": "vitest run",
43
- "typecheck": "tsc --noEmit"
44
45
  }
45
- }
46
+ }