@nowcrew/daemon 0.6.77-beta.9 → 0.6.78

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.
@@ -1,18 +1,12 @@
1
1
  import { executionBackendCapability } from "./execution-backend.js";
2
2
  import { daemonCapabilities, EXECUTION_PROTOCOL } from "./machine-info.js";
3
- import { PROJECT_SKILLS_CAPABILITY, PROJECT_SKILL_PROJECTION_V2_CAPABILITY, } from "./project-skills/types.js";
4
- export function buildControlPlaneUrl(serverUrl, machineToken, runtimePlatform = process.platform, jobObjectProbe, projectSkillsAvailable = true, capabilities = daemonCapabilities(runtimePlatform)) {
3
+ export function buildControlPlaneUrl(serverUrl, machineToken, runtimePlatform = process.platform, jobObjectProbe, capabilities = daemonCapabilities(runtimePlatform)) {
5
4
  const query = new URLSearchParams({ key: machineToken });
6
5
  if (executionBackendCapability(runtimePlatform, jobObjectProbe).supported) {
7
6
  query.set("execution_min", String(EXECUTION_PROTOCOL.min));
8
7
  query.set("execution_max", String(EXECUTION_PROTOCOL.max));
9
8
  }
10
- for (const capability of capabilities) {
11
- if (projectSkillsAvailable
12
- || (capability !== PROJECT_SKILLS_CAPABILITY
13
- && capability !== PROJECT_SKILL_PROJECTION_V2_CAPABILITY)) {
14
- query.append("capability", capability);
15
- }
16
- }
9
+ for (const capability of capabilities)
10
+ query.append("capability", capability);
17
11
  return `${serverUrl.replace(/^http/, "ws").replace(/\/+$/, "")}/daemon/connect?${query.toString()}`;
18
12
  }
@@ -17,8 +17,7 @@ import { HostReservationCancelledError } from "./host-execution-coordinator.js";
17
17
  import { supervisorLaunch } from "./supervised-runtime.js";
18
18
  import { appendAgentMemoryContext } from "./agent-memory/policy.js";
19
19
  import { ProjectSkillRuntimeOwnershipUnverifiedError, } from "./project-skills/reconciler.js";
20
- import { projectSkillExecutionProjection, projectSkillProjectionErrorCode } from "./project-skills/execution-adapter.js";
21
- import { redactProjectSkillRuntimeRootError, } from "./project-skills/runtime-launch.js";
20
+ import { projectSkillExecutionProjection, projectSkillProjectionApplies, projectSkillProjectionErrorCode, } from "./project-skills/execution-adapter.js";
22
21
  import { createProjectRegistry } from "./project-skills/registry.js";
23
22
  import { ProjectDependencyError, } from "./project-dependencies.js";
24
23
  import { ProjectContextUnavailableError, resolveProjectContext } from "./project-workspaces/resolver.js";
@@ -737,7 +736,7 @@ export async function runExecution(config, input, dependencies) {
737
736
  }
738
737
  catch (error) {
739
738
  await proveActiveSupervisorStopped(error, true);
740
- throw redactProjectSkillRuntimeRootError(error, root);
739
+ throw error;
741
740
  }
742
741
  };
743
742
  return safeLaunch;
@@ -974,14 +973,14 @@ export async function runExecution(config, input, dependencies) {
974
973
  await dependencies.cancellation?.waitForStop();
975
974
  }
976
975
  catch (stopError) {
977
- if (spec.agent.projectSkillBindingGeneration !== undefined) {
976
+ if (projectSkillProjectionApplies(spec)) {
978
977
  throw new ProjectSkillRuntimeOwnershipUnverifiedError();
979
978
  }
980
979
  throw stopError;
981
980
  }
982
981
  }
983
982
  else {
984
- await proveActiveSupervisorStopped(error, spec.agent.projectSkillBindingGeneration !== undefined);
983
+ await proveActiveSupervisorStopped(error, projectSkillProjectionApplies(spec));
985
984
  }
986
985
  const failureCompletion = cancelled
987
986
  ? ExecutionCompletedSchema.parse({
@@ -24,8 +24,9 @@ import { isRuntimeReadyEvent, isRuntimeRunningEvent, } from "./runtime-startup-g
24
24
  import { dslog } from "./slog.js";
25
25
  import { boundedDiagnosticJsonArray } from "./diagnostic-json.js";
26
26
  import { ProjectProjectionError, } from "./project-skills/reconciler.js";
27
+ import { projectSkillProjectionApplies } from "./project-skills/execution-adapter.js";
27
28
  import { formatProjectSkillRuntimeWarning } from "./project-skills/runtime-warning.js";
28
- import { projectSkillRuntimeDirectories, redactProjectSkillRuntimeRootText, redactProjectSkillRuntimeRootValue, } from "./project-skills/runtime-launch.js";
29
+ import { projectSkillRuntimeDirectories, } from "./project-skills/runtime-launch.js";
29
30
  import { logIdentityRosterRuntimePrepared } from "./identity-roster-telemetry.js";
30
31
  import { diffMemoryPruneNotes, evaluateMemoryPrunePostcondition, inspectMemoryPruneFilesWithinDeadline, logMemoryPruneDiagnosticsFailure, memoryPruneSnapshotFields, parseMemoryPruneTraceId, } from "./memory-prune-diagnostics.js";
31
32
  import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
@@ -525,12 +526,10 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
525
526
  });
526
527
  if (dependencies.cancellation?.isRequested())
527
528
  throw new RuntimeCancelledError();
528
- let activeProjectSkillRuntimeRoot;
529
529
  const abilityContext = input.abilityRelease !== undefined && dependencies.abilityRelease !== undefined
530
530
  ? await dependencies.abilityRelease.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.abilityRelease, async (context) => context)
531
531
  : undefined;
532
532
  const prepareRuntimeLaunch = async (runtimeRoot) => {
533
- activeProjectSkillRuntimeRoot = runtimeRoot;
534
533
  const effectiveSystemPrompt = abilityContext === undefined
535
534
  ? systemPrompt
536
535
  : `${systemPrompt}\n\n${abilityContext.prompt}`;
@@ -681,11 +680,17 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
681
680
  return launchRuntime(launchRequest);
682
681
  };
683
682
  let child;
684
- if (input.projectSkills !== undefined && dependencies.projectSkills !== undefined) {
683
+ const projectionBindings = projectSkillProjectionApplies({
684
+ agent: { projectSkills: input.projectSkills },
685
+ runtime: input.runtime,
686
+ })
687
+ ? input.projectSkills
688
+ : undefined;
689
+ if (projectionBindings !== undefined && dependencies.projectSkills !== undefined) {
685
690
  const launchOwner = dependencies.projectSkillLeaseSafeLaunchOwner;
686
691
  if (launchOwner === undefined)
687
692
  throw new ProjectProjectionError("skill_projection_failed");
688
- child = await dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.executionId, input.handle, input.projectSkills, launchOwner.claim(launchPreparedRuntime), (warning) => callbacks.onConsole?.({
693
+ child = await dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.executionId, input.handle, projectionBindings, launchOwner.claim(launchPreparedRuntime), (warning) => callbacks.onConsole?.({
689
694
  stream: "system",
690
695
  text: formatProjectSkillRuntimeWarning(warning),
691
696
  }));
@@ -818,8 +823,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
818
823
  usage = meta.usage;
819
824
  if (meta.model)
820
825
  observedModel = meta.model;
821
- for (const rawActivity of normalizeEvent(event)) {
822
- const activity = redactProjectSkillRuntimeRootValue(rawActivity, activeProjectSkillRuntimeRoot);
826
+ for (const activity of normalizeEvent(event)) {
823
827
  if (activity.kind === "sending")
824
828
  sentViaCrew = true;
825
829
  activities.push(activity);
@@ -827,19 +831,18 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
827
831
  }
828
832
  const extracted = extractFinalText(event);
829
833
  if (extracted) {
830
- const safeExtracted = redactProjectSkillRuntimeRootText(extracted, activeProjectSkillRuntimeRoot);
831
834
  const incremental = typeof event === "object" && event !== null
832
835
  && "type" in event && (event.type === "kimi.acp.text_delta"
833
836
  || event.type === "hermes.acp.text_delta"
834
837
  || event.type === "deepseek-harness.acp.text_delta"
835
838
  || event.type === "opencode.text_delta");
836
- finalText = incremental ? `${finalText ?? ""}${safeExtracted}` : safeExtracted;
839
+ finalText = incremental ? `${finalText ?? ""}${extracted}` : extracted;
837
840
  }
838
841
  for (const text of decodeExternalOutputEvent(runtime.name, event, externalOutput)) {
839
- callbacks.onExternalOutput?.(redactProjectSkillRuntimeRootText(text, activeProjectSkillRuntimeRoot));
842
+ callbacks.onExternalOutput?.(text);
840
843
  }
841
844
  for (const chunk of consoleFormatter.format(event)) {
842
- callbacks.onConsole?.(redactProjectSkillRuntimeRootValue(chunk, activeProjectSkillRuntimeRoot));
845
+ callbacks.onConsole?.(chunk);
843
846
  }
844
847
  });
845
848
  let stderrTail = "";
@@ -861,7 +864,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
861
864
  })
862
865
  : null;
863
866
  child.stderr.on("data", (data) => {
864
- const text = redactProjectSkillRuntimeRootText(String(data), activeProjectSkillRuntimeRoot);
867
+ const text = String(data);
865
868
  process.stderr.write(text);
866
869
  stderrTail = (stderrTail + text).slice(-STDERR_TAIL_CAP);
867
870
  codexStartupStageParser?.push(text);
@@ -940,9 +943,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
940
943
  }
941
944
  const errorTail = [
942
945
  stderrTail.trim(),
943
- spawnError === undefined
944
- ? undefined
945
- : redactProjectSkillRuntimeRootText(spawnError, activeProjectSkillRuntimeRoot),
946
+ spawnError === undefined ? undefined : spawnError,
946
947
  terminationSignal ? `terminated by ${terminationSignal}` : undefined,
947
948
  ].filter(Boolean).join(" ").trim();
948
949
  const providerError = providerErrorFromExit(exitCode, errorTail);
@@ -994,11 +995,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
994
995
  : null,
995
996
  // 契约答案 = 整轮最后一个闭合的标签块(六个 runtime 语义一致的"后到者胜",半截块与
996
997
  // 叙述里的开标签不算数);从未闭合过时才回落到对 finalText 的宽容提取,保住"收尾被
997
- // 截断"这一老兜底。解码器存的是原文,这里补脱敏。注意这放宽了"externalAnswer 非空
998
+ // 截断"这一老兜底。注意这放宽了"externalAnswer 非空
998
999
  // ⇒ finalText 非空"的旧不变式;是否上报仍由 execution-runner 按 exitCode===0 把关。
999
1000
  externalAnswer: input.captureFinal
1000
1001
  ? (externalOutput.lastClosedBlock !== null
1001
- ? redactProjectSkillRuntimeRootText(externalOutput.lastClosedBlock, activeProjectSkillRuntimeRoot)
1002
+ ? externalOutput.lastClosedBlock
1002
1003
  : finalText !== null ? extractExternalAnswer(finalText) : null)
1003
1004
  : null,
1004
1005
  sentViaCrew,
@@ -3,9 +3,11 @@ import { lstat, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { applyLocalMemoryResult, parseLocalMemoryResult } from "./write.js";
5
5
  export function isLocalMemoryV2Eligible(input) {
6
- return input.enabledForHandle
7
- && (input.localMemoryV2Initialized || (!input.agentHomePreexisted && input.memorySeedCreated))
8
- && input.memoryPrune;
6
+ // The allowlist is the rollout boundary. The marker and seed/pre-existing
7
+ // state are retained as diagnostics and for backward-compatible marker
8
+ // creation, but must not prevent an explicitly allowlisted Agent from using
9
+ // the structured memory-prune protocol.
10
+ return input.enabledForHandle && input.memoryPrune;
9
11
  }
10
12
  const LOCAL_MEMORY_V2_MARKER = "v2-enabled";
11
13
  export async function initializeLocalMemoryV2(agentDir) {
@@ -43,6 +43,7 @@ export const DAEMON_CAPABILITIES = Object.freeze([
43
43
  "execution_agent_memory_policy_v1",
44
44
  "execution_agent_memory_mapping_v1",
45
45
  "project_skills_v1",
46
+ "project_skill_projection_v2",
46
47
  PROJECT_WORKSPACES_CAPABILITY,
47
48
  TRAINING_PROJECT_DEPENDENCIES_CAPABILITY,
48
49
  RUNTIME_HEALTH_PROBE_CAPABILITY,
@@ -50,6 +51,7 @@ export const DAEMON_CAPABILITIES = Object.freeze([
50
51
  "agent_ability_workspace_v1",
51
52
  "agent_ability_runtime_assets_v1",
52
53
  ]);
54
+ /** 能力只按平台过滤,进程存活期间静态(CLAUDE.md 红线)。Windows 在 junction/copy 真机 smoke 通过前不宣告 Project Skills。 */
53
55
  export const daemonCapabilities = (runtimePlatform = process.platform, readiness = NATIVE_PROJECT_WORKSPACE_READINESS) => {
54
56
  // Windows stays fail-closed until the native workspace adapter gate is verified on a real host.
55
57
  const nativeWorkspacePlatform = runtimePlatform === "darwin" || runtimePlatform === "linux";
@@ -59,6 +61,7 @@ export const daemonCapabilities = (runtimePlatform = process.platform, readiness
59
61
  return Object.freeze(DAEMON_CAPABILITIES.filter((capability) => ((capability !== PROJECT_WORKSPACES_CAPABILITY || nativeWorkspaceReady)
60
62
  && (nativeWorkspacePlatform
61
63
  || (capability !== "project_skills_v1"
64
+ && capability !== "project_skill_projection_v2"
62
65
  && capability !== "agent_ability_release_v1"
63
66
  && capability !== "agent_ability_workspace_v1"
64
67
  && capability !== "agent_ability_runtime_assets_v1")))));
@@ -1,11 +1,9 @@
1
1
  import { z } from "zod";
2
- import { projectSkillResolutionRecords, scanProjects } from "./scanner.js";
2
+ import { scanProjects } from "./scanner.js";
3
3
  import { ProjectRegistryError } from "./registry.js";
4
4
  import { isProjectId, isProjectSkillName, compareProjectSkillRefs, MAX_AGENT_PROJECT_SKILL_BINDINGS, } from "./types.js";
5
- import { ProjectProjectionError, } from "./reconciler.js";
6
5
  import { AgentHandleSchema } from "../execution-protocol.js";
7
6
  import { createPromiseTail } from "../promise-tail.js";
8
- import { createProjectSkillSnapshotEpoch, MAX_REMEMBERED_PROJECT_SKILL_AGENTS, toPathPrivateConvergenceResults, } from "./controller-convergence.js";
9
7
  const ProjectIdSchema = z.string().refine(isProjectId);
10
8
  const AgentSkillsSyncSchema = z.object({
11
9
  type: z.literal("agent:skills:sync"),
@@ -50,7 +48,7 @@ const ProjectCommandSchema = z.discriminatedUnion("type", [
50
48
  }
51
49
  });
52
50
  const DEFAULT_PROJECT_SCAN_TIMEOUT_MS = 10_000;
53
- class ProjectScanTimeoutError extends Error {
51
+ export class ProjectScanTimeoutError extends Error {
54
52
  code = "project_scan_timeout";
55
53
  constructor() {
56
54
  super("project_scan_timeout");
@@ -87,7 +85,6 @@ export function createProjectSkillsController(deps) {
87
85
  let initializationState = "idle";
88
86
  const scan = deps.scan ?? scanProjects;
89
87
  const scanTimeoutMs = deps.scanTimeoutMs ?? DEFAULT_PROJECT_SCAN_TIMEOUT_MS;
90
- const snapshotEpoch = createProjectSkillSnapshotEpoch();
91
88
  let connectionEpochIdentity = Object.freeze({});
92
89
  const assertCurrentEpoch = (token) => {
93
90
  if (token !== undefined && token !== connectionEpochIdentity)
@@ -110,51 +107,6 @@ export function createProjectSkillsController(deps) {
110
107
  }));
111
108
  };
112
109
  const enqueue = (operation) => operationTail.enqueue(operation);
113
- const applyV2Snapshot = async (handle, snapshot, epochToken) => {
114
- if (deps.ensureSnapshot === undefined)
115
- throw new ProjectProjectionError("skill_projection_failed");
116
- assertCurrentEpoch(epochToken);
117
- const acceptedEpochIdentity = connectionEpochIdentity;
118
- const rememberedEntries = snapshotEpoch.entries();
119
- const firstAcceptedInEpoch = !rememberedEntries.some(([candidate]) => candidate === handle);
120
- if (firstAcceptedInEpoch && rememberedEntries.length >= MAX_REMEMBERED_PROJECT_SKILL_AGENTS) {
121
- throw new ProjectProjectionError("skill_projection_failed");
122
- }
123
- if (firstAcceptedInEpoch) {
124
- await deps.ensureSnapshot(handle, snapshot, Object.freeze({ refreshCopyProjection: true }));
125
- }
126
- else {
127
- await deps.ensureSnapshot(handle, snapshot);
128
- }
129
- assertCurrentEpoch(epochToken);
130
- if (acceptedEpochIdentity === connectionEpochIdentity)
131
- snapshotEpoch.remember(handle, snapshot);
132
- return Object.freeze(projectSkillResolutionRecords(snapshot.bindings, scanned)
133
- .map((record) => Object.freeze({
134
- projectId: record.projectId,
135
- skillName: record.skillName,
136
- status: record.mode === "resolved" ? "linked" : "unavailable",
137
- })));
138
- };
139
- const convergeV2Snapshots = async (refreshCopyProjection, epochToken) => {
140
- assertCurrentEpoch(epochToken);
141
- const acceptedEpochIdentity = connectionEpochIdentity;
142
- const entries = snapshotEpoch.entries();
143
- if (deps.ensureSnapshot === undefined || entries.length === 0)
144
- return;
145
- const results = await Promise.allSettled(entries.map(([handle, snapshot]) => refreshCopyProjection
146
- ? deps.ensureSnapshot(handle, snapshot, Object.freeze({ refreshCopyProjection: true }))
147
- : deps.ensureSnapshot(handle, snapshot)));
148
- if (acceptedEpochIdentity !== connectionEpochIdentity) {
149
- if (epochToken !== undefined)
150
- throw new ProjectSkillStaleEpochError();
151
- return;
152
- }
153
- try {
154
- deps.onConvergence?.(toPathPrivateConvergenceResults(entries, results));
155
- }
156
- catch { /* convergence diagnostics must not change a successful inventory mutation */ }
157
- };
158
110
  const initialize = () => {
159
111
  if (initializationState === "ready" && initialization !== null)
160
112
  return initialization;
@@ -173,10 +125,10 @@ export function createProjectSkillsController(deps) {
173
125
  return {
174
126
  beginConnectionEpoch: () => {
175
127
  connectionEpochIdentity = Object.freeze({});
176
- snapshotEpoch.begin();
177
128
  return connectionEpochIdentity;
178
129
  },
179
130
  initialize,
131
+ ensureScanned: () => initialize(),
180
132
  publishCurrent: () => enqueue(publishCurrent),
181
133
  scannedProjects: () => scanned,
182
134
  async handle(input, epochToken) {
@@ -208,25 +160,9 @@ export function createProjectSkillsController(deps) {
208
160
  try {
209
161
  assertCurrentEpoch(epochToken);
210
162
  if (command.type === "agent:skills:sync") {
211
- if (command.generation !== undefined) {
212
- const snapshot = Object.freeze({
213
- bindings: Object.freeze(command.bindings.map((binding) => Object.freeze({ ...binding }))),
214
- generation: command.generation,
215
- });
216
- return {
217
- ok: true,
218
- data: { bindings: await applyV2Snapshot(command.handle, snapshot, epochToken) },
219
- };
220
- }
221
- if (deps.reconcile === undefined)
222
- return { ok: false, error: "skill_projection_failed" };
223
163
  const bindings = await deps.reconcile(command.handle, command.bindings);
224
164
  assertCurrentEpoch(epochToken);
225
- snapshotEpoch.forget(command.handle);
226
- return {
227
- ok: true,
228
- data: { bindings },
229
- };
165
+ return { ok: true, data: { bindings } };
230
166
  }
231
167
  if (command.type === "project:add") {
232
168
  const existed = (await deps.registry.list())
@@ -260,7 +196,6 @@ export function createProjectSkillsController(deps) {
260
196
  assertCurrentEpoch(epochToken);
261
197
  await publishCurrent();
262
198
  assertCurrentEpoch(epochToken);
263
- await convergeV2Snapshots(command.type === "project:rescan", epochToken);
264
199
  return { ok: true, data: { projectId: command.projectId } };
265
200
  }
266
201
  catch (error) {
@@ -274,8 +209,8 @@ export function createProjectSkillsController(deps) {
274
209
  ? "skill_projection_stale"
275
210
  : error.code === "skill_projection_failed"
276
211
  ? "skill_projection_failed"
277
- : error.code === "skill_projection_snapshot_corrupt"
278
- ? "skill_projection_snapshot_corrupt"
212
+ : error.code === "project_skills_unavailable"
213
+ ? "project_skills_unavailable"
279
214
  : "project_operation_failed",
280
215
  };
281
216
  }
@@ -8,4 +8,11 @@ export function projectSkillExecutionProjection(bindings, generation) {
8
8
  generation,
9
9
  });
10
10
  }
11
+ /**
12
+ * 投影门控与 ownership 判定的唯一谓词:只有绑定了 Project Skills 且运行时是 codex/claude 的执行
13
+ * 才会有投影目录,也才需要在停止无法证明时保留它。两处判定分开写过一次,结果 kimi + projectSkills
14
+ * 会在取消路径上错抛 ownership 未确认。
15
+ */
16
+ export const projectSkillProjectionApplies = (spec) => spec.agent.projectSkills !== undefined
17
+ && (spec.runtime.name === "codex" || spec.runtime.name === "claude");
11
18
  export const projectSkillProjectionErrorCode = (error) => error instanceof ProjectProjectionError ? error.code : null;
@@ -0,0 +1,163 @@
1
+ /**
2
+ * 按执行投影:每次 execution 在 Agent 目录下独占一个短命目录,投影绑定的 Project Skills;
3
+ * 用完即删,启动时按 journal 清扫。没有共享状态,所以没有 manifest / 目录身份 / 租约 / GC。
4
+ * (取代 2026-08-21 版本化 runtime root;incident: 2026-09-04 st_dev 漂移摘掉整机能力)
5
+ */
6
+ import { lstat, mkdir, readdir, rm } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+ import { DirectoryProjectionError, projectDirectory } from "../directory-projection.js";
9
+ /** 稳定错误:code 与既有 skill_projection_failed 对齐,reason 是 path-free 的细分原因。 */
10
+ export class ExecutionProjectionError extends Error {
11
+ reason;
12
+ code = "skill_projection_failed";
13
+ constructor(reason) {
14
+ super(`execution projection failed: ${reason}`);
15
+ this.reason = reason;
16
+ this.name = "ExecutionProjectionError";
17
+ }
18
+ }
19
+ export const EXECUTION_PROJECTION_PARENT = Object.freeze([".crew", "project-skills"]);
20
+ const DIRECTORY_PREFIX = "exec-";
21
+ const SAFE_EXECUTION_ID = /^[A-Za-z0-9._-]{1,128}$/u;
22
+ const projectionParent = (agentRoot) => join(agentRoot, ...EXECUTION_PROJECTION_PARENT);
23
+ export function executionProjectionDescriptor(agentRoot, executionId) {
24
+ if (!SAFE_EXECUTION_ID.test(executionId) || executionId === "." || executionId === "..") {
25
+ throw new ExecutionProjectionError("unmanaged_path");
26
+ }
27
+ const rootDirectory = join(projectionParent(agentRoot), `${DIRECTORY_PREFIX}${executionId}`);
28
+ const runtime = join(rootDirectory, "runtime");
29
+ return Object.freeze({
30
+ executionId,
31
+ rootDirectory,
32
+ codexSkillRoot: join(runtime, ".claude", "skills"),
33
+ claudeAdditionalDirectory: runtime,
34
+ });
35
+ }
36
+ const ownedByCurrentUser = (uid) => process.getuid === undefined || uid === process.getuid();
37
+ /** 每一层都必须是本用户的真实目录;symlink / 他人目录 → unmanaged_path。 */
38
+ const assertManagedDirectory = async (path) => {
39
+ let info;
40
+ try {
41
+ info = await lstat(path);
42
+ }
43
+ catch {
44
+ throw new ExecutionProjectionError("unmanaged_path");
45
+ }
46
+ if (!info.isDirectory() || info.isSymbolicLink() || !ownedByCurrentUser(info.uid)) {
47
+ throw new ExecutionProjectionError("unmanaged_path");
48
+ }
49
+ };
50
+ const isErrno = (error, code) => typeof error === "object" && error !== null && error.code === code;
51
+ export async function materializeExecutionProjection(input) {
52
+ const platform = input.platform ?? process.platform;
53
+ const project = input.projectDirectory ?? projectDirectory;
54
+ const root = executionProjectionDescriptor(input.agentRoot, input.executionId);
55
+ const parent = projectionParent(input.agentRoot);
56
+ // 逐层 assert-then-create:先证明这一层是本用户的真实目录,再在其下创建下一层。
57
+ // 反过来(先 recursive mkdir 再 assert)会让 `.crew` 是 symlink 的情况先在别人的树里建好目录。
58
+ const crewRoot = join(input.agentRoot, EXECUTION_PROJECTION_PARENT[0]);
59
+ await assertManagedDirectory(input.agentRoot);
60
+ try {
61
+ await mkdir(crewRoot, { recursive: true, mode: 0o700 });
62
+ }
63
+ catch {
64
+ throw new ExecutionProjectionError("mkdir_failed");
65
+ }
66
+ await assertManagedDirectory(crewRoot);
67
+ try {
68
+ await mkdir(parent, { recursive: true, mode: 0o700 });
69
+ }
70
+ catch {
71
+ throw new ExecutionProjectionError("mkdir_failed");
72
+ }
73
+ await assertManagedDirectory(parent);
74
+ try {
75
+ await mkdir(root.rootDirectory, { mode: 0o700 });
76
+ }
77
+ catch (error) {
78
+ throw new ExecutionProjectionError(isErrno(error, "EEXIST") ? "execution_projection_exists" : "mkdir_failed");
79
+ }
80
+ const modes = [];
81
+ try {
82
+ await mkdir(root.codexSkillRoot, { recursive: true, mode: 0o700 });
83
+ for (const record of input.resolutions) {
84
+ if (record.mode !== "resolved" || record.sourcePath === null)
85
+ continue;
86
+ let projected;
87
+ try {
88
+ // `projectDirectory`'s symlink path never lstats the source (POSIX symlink() happily
89
+ // creates a dangling link), so a source that vanished between scan and materialize would
90
+ // otherwise be silently projected as a broken link instead of failing loudly.
91
+ const sourceExists = await lstat(record.sourcePath).then(() => true, () => false);
92
+ if (!sourceExists)
93
+ throw new ExecutionProjectionError("link_failed");
94
+ projected = await project(record.sourcePath, join(root.codexSkillRoot, record.skillName), platform);
95
+ }
96
+ catch (error) {
97
+ if (error instanceof ExecutionProjectionError)
98
+ throw error;
99
+ const copying = platform === "win32" && error instanceof DirectoryProjectionError
100
+ && error.code === "directory_projection_limit_exceeded";
101
+ throw new ExecutionProjectionError(copying ? "copy_failed" : "link_failed");
102
+ }
103
+ modes.push({ skillName: record.skillName, mode: projected.mode });
104
+ }
105
+ }
106
+ catch (error) {
107
+ await rm(root.rootDirectory, { recursive: true, force: true }).catch(() => undefined);
108
+ if (error instanceof ExecutionProjectionError)
109
+ throw error;
110
+ throw new ExecutionProjectionError("mkdir_failed");
111
+ }
112
+ return Object.freeze({ root, modes: Object.freeze(modes) });
113
+ }
114
+ export async function removeExecutionProjection(agentRoot, executionId) {
115
+ const root = executionProjectionDescriptor(agentRoot, executionId);
116
+ await rm(root.rootDirectory, { recursive: true, force: true });
117
+ }
118
+ export async function sweepExecutionProjections(agentRoot, isLive) {
119
+ const parent = projectionParent(agentRoot);
120
+ const parentExists = await lstat(parent).then(() => true, (error) => {
121
+ if (isErrno(error, "ENOENT"))
122
+ return false;
123
+ throw error;
124
+ });
125
+ if (!parentExists)
126
+ return Object.freeze({ removed: Object.freeze([]), kept: Object.freeze([]) });
127
+ // Every layer must still be a real, owned directory — a symlink swapped in for `.crew` or
128
+ // `.crew/project-skills` (or the agent root itself) must never let readdir/rm follow it out
129
+ // of the managed tree and delete something outside it.
130
+ await assertManagedDirectory(agentRoot);
131
+ await assertManagedDirectory(join(agentRoot, EXECUTION_PROJECTION_PARENT[0]));
132
+ await assertManagedDirectory(parent);
133
+ const entries = await readdir(parent, { withFileTypes: true });
134
+ const removed = [];
135
+ const kept = [];
136
+ for (const entry of entries) {
137
+ if (!entry.isDirectory() || !entry.name.startsWith(DIRECTORY_PREFIX))
138
+ continue;
139
+ const candidate = join(parent, entry.name);
140
+ const candidateInfo = await lstat(candidate).catch(() => null);
141
+ if (candidateInfo === null || candidateInfo.isSymbolicLink())
142
+ continue;
143
+ const executionId = entry.name.slice(DIRECTORY_PREFIX.length);
144
+ if (!SAFE_EXECUTION_ID.test(executionId))
145
+ continue;
146
+ let live;
147
+ try {
148
+ live = await isLive(executionId);
149
+ }
150
+ catch {
151
+ // An `isLive` failure is ambiguous, not evidence of death — err toward keeping the
152
+ // directory rather than deleting a possibly-live execution's projection.
153
+ live = true;
154
+ }
155
+ if (live) {
156
+ kept.push(executionId);
157
+ continue;
158
+ }
159
+ await rm(candidate, { recursive: true, force: true });
160
+ removed.push(executionId);
161
+ }
162
+ return Object.freeze({ removed: Object.freeze(removed.sort()), kept: Object.freeze(kept.sort()) });
163
+ }
@@ -1,18 +1,20 @@
1
- import { ProjectProjectionError, } from "./reconciler.js";
2
- /** Gates every projection entrypoint on the daemon's bounded registry/scanner initialization. */
3
- export function initializedProjectSkillsReconciler(ensureInitialized, reconciler) {
1
+ import { ProjectProjectionError } from "./reconciler.js";
2
+ /** 执行前等待注册表首次扫描;超时/失败 → 本次执行 project_skills_unavailable,不影响能力宣告。 */
3
+ export function initializedProjectSkillsReconciler(ensureScanned, reconciler) {
4
4
  const ready = async () => {
5
- if (await ensureInitialized())
5
+ try {
6
+ await ensureScanned();
6
7
  return reconciler;
7
- throw new ProjectProjectionError("skill_projection_failed");
8
+ }
9
+ catch (error) {
10
+ const code = error.code;
11
+ throw new ProjectProjectionError("project_skills_unavailable", code === "project_scan_timeout" ? "registry_scan_timeout" : "registry_unavailable");
12
+ }
8
13
  };
9
14
  return {
10
15
  async reconcile(handle, bindings) {
11
16
  return (await ready()).reconcile(handle, bindings);
12
17
  },
13
- async ensureSnapshot(handle, snapshot, options) {
14
- return (await ready()).ensureSnapshot(handle, snapshot, options);
15
- },
16
18
  async prepareAndLaunch(agentsRoot, executionId, handle, projection, launch, onWarning) {
17
19
  return (await ready()).prepareAndLaunch(agentsRoot, executionId, handle, projection, launch, onWarning);
18
20
  },