@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.
- package/dist/control-plane-url.js +3 -9
- package/dist/execution-runner.js +4 -5
- package/dist/local-executor.js +18 -17
- package/dist/local-memory/runtime.js +5 -3
- package/dist/machine-info.js +3 -0
- package/dist/project-skills/controller.js +6 -71
- package/dist/project-skills/execution-adapter.js +7 -0
- package/dist/project-skills/execution-projection.js +163 -0
- package/dist/project-skills/initialized-reconciler.js +10 -8
- package/dist/project-skills/legacy-cleanup.js +224 -0
- package/dist/project-skills/machine-hello-publisher.js +20 -0
- package/dist/project-skills/reconciler.js +114 -500
- package/dist/project-skills/resolution-records.js +63 -0
- package/dist/project-skills/runtime-launch.js +0 -42
- package/dist/project-skills/runtime-root-bootstrap.js +0 -6
- package/dist/project-skills/runtime-root-startup.js +1 -2
- package/dist/project-skills/runtime-warning.js +1 -3
- package/dist/project-skills/scanner.js +1 -17
- package/dist/project-skills/serve-startup.js +0 -23
- package/dist/project-skills/startup-cleanup.js +24 -0
- package/dist/serve.js +44 -45
- package/package.json +1 -1
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
import { executionBackendCapability } from "./execution-backend.js";
|
|
2
2
|
import { daemonCapabilities, EXECUTION_PROTOCOL } from "./machine-info.js";
|
|
3
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/execution-runner.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
983
|
+
await proveActiveSupervisorStopped(error, projectSkillProjectionApplies(spec));
|
|
985
984
|
}
|
|
986
985
|
const failureCompletion = cancelled
|
|
987
986
|
? ExecutionCompletedSchema.parse({
|
package/dist/local-executor.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 ?? ""}${
|
|
839
|
+
finalText = incremental ? `${finalText ?? ""}${extracted}` : extracted;
|
|
837
840
|
}
|
|
838
841
|
for (const text of decodeExternalOutputEvent(runtime.name, event, externalOutput)) {
|
|
839
|
-
callbacks.onExternalOutput?.(
|
|
842
|
+
callbacks.onExternalOutput?.(text);
|
|
840
843
|
}
|
|
841
844
|
for (const chunk of consoleFormatter.format(event)) {
|
|
842
|
-
callbacks.onConsole?.(
|
|
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 =
|
|
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
|
-
// 截断"
|
|
998
|
+
// 截断"这一老兜底。注意这放宽了"externalAnswer 非空
|
|
998
999
|
// ⇒ finalText 非空"的旧不变式;是否上报仍由 execution-runner 按 exitCode===0 把关。
|
|
999
1000
|
externalAnswer: input.captureFinal
|
|
1000
1001
|
? (externalOutput.lastClosedBlock !== null
|
|
1001
|
-
?
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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) {
|
package/dist/machine-info.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 === "
|
|
278
|
-
? "
|
|
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
|
|
2
|
-
/**
|
|
3
|
-
export function initializedProjectSkillsReconciler(
|
|
1
|
+
import { ProjectProjectionError } from "./reconciler.js";
|
|
2
|
+
/** 执行前等待注册表首次扫描;超时/失败 → 本次执行 project_skills_unavailable,不影响能力宣告。 */
|
|
3
|
+
export function initializedProjectSkillsReconciler(ensureScanned, reconciler) {
|
|
4
4
|
const ready = async () => {
|
|
5
|
-
|
|
5
|
+
try {
|
|
6
|
+
await ensureScanned();
|
|
6
7
|
return reconciler;
|
|
7
|
-
|
|
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
|
},
|