@evo-dev/core 0.0.1-alpha
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/assets/agents/review/code-reviewer/examples.md +19 -0
- package/assets/agents/review/code-reviewer/manifest.json +10 -0
- package/assets/agents/review/code-reviewer/prompt.md +59 -0
- package/assets/agents/review/code-reviewer/verification.md +11 -0
- package/assets/skills/coding/engineering-discipline/SKILL.md +63 -0
- package/assets/skills/coding/engineering-discipline/anti-patterns.md +21 -0
- package/assets/skills/coding/engineering-discipline/examples.md +19 -0
- package/assets/skills/coding/engineering-discipline/manifest.json +10 -0
- package/assets/skills/coding/engineering-discipline/verification.md +11 -0
- package/assets/workflows/rd-bug-fix/WORKFLOW.json +45 -0
- package/assets/workflows/rd-code-review/WORKFLOW.json +45 -0
- package/assets/workflows/rd-docs-update/WORKFLOW.json +45 -0
- package/assets/workflows/rd-feature-implementation/WORKFLOW.json +45 -0
- package/assets/workflows/rd-refactor/WORKFLOW.json +45 -0
- package/assets/workflows/rd-release-readiness/WORKFLOW.json +49 -0
- package/assets/workflows/rd-security-boundary-review/WORKFLOW.json +45 -0
- package/assets/workflows/rd-test-generation/WORKFLOW.json +45 -0
- package/dist/assets/index.js +209 -0
- package/dist/config/index.js +601 -0
- package/dist/index.js +4879 -0
- package/dist/plugins/index.js +265 -0
- package/package.json +30 -0
- package/src/.gitkeep +0 -0
- package/src/agents/index.ts +561 -0
- package/src/assets/errors.ts +21 -0
- package/src/assets/index.ts +18 -0
- package/src/assets/manifest.ts +109 -0
- package/src/assets/scanner.ts +189 -0
- package/src/config/errors.ts +21 -0
- package/src/config/index.ts +26 -0
- package/src/config/paths.ts +43 -0
- package/src/config/registry.ts +84 -0
- package/src/config/settings.ts +212 -0
- package/src/config/state.ts +130 -0
- package/src/config/store.ts +166 -0
- package/src/daemon/index.ts +414 -0
- package/src/hooks/index.ts +1023 -0
- package/src/index.ts +14 -0
- package/src/learning/index.ts +714 -0
- package/src/observability/index.ts +272 -0
- package/src/pack/index.ts +779 -0
- package/src/plugins/capabilities.ts +347 -0
- package/src/plugins/index.ts +41 -0
- package/src/plugins/registry.ts +60 -0
- package/src/plugins/types.ts +123 -0
- package/src/project/index.ts +507 -0
- package/src/protected-zones/index.ts +137 -0
- package/src/sync/index.ts +7 -0
- package/src/sync/orchestrator.ts +298 -0
- package/src/task/index.ts +840 -0
- package/src/workflow/index.ts +137 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4879 @@
|
|
|
1
|
+
// packages/core/src/agents/index.ts
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
var DEFAULT_AGENT_PERMISSIONS = {
|
|
4
|
+
canReadMetadata: true,
|
|
5
|
+
canReadSourceContent: false,
|
|
6
|
+
canWriteFiles: false,
|
|
7
|
+
canRunCommands: false,
|
|
8
|
+
canUseNetwork: false,
|
|
9
|
+
canSpawnAgents: false,
|
|
10
|
+
canWriteMemory: false
|
|
11
|
+
};
|
|
12
|
+
var FORBIDDEN_INPUTS = ["rawPrompts", "secrets", "rawCommandLogs", "sourceCorpus"];
|
|
13
|
+
var SAFE_AGENT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
14
|
+
var PRIVACY_BLOCKER_PATTERN = /raw prompt|rawprompt|source corpus|sourcecorpus|secret|\.env|raw command|raw log|internal url|private url|network|write files|run commands|spawn agents|write memory/i;
|
|
15
|
+
var LENS_TO_EXPERTISE = {
|
|
16
|
+
review: ["code-review"],
|
|
17
|
+
qa: ["qa", "verification"],
|
|
18
|
+
security: ["security", "privacy"],
|
|
19
|
+
release: ["release", "packaging"],
|
|
20
|
+
architecture: ["architecture"],
|
|
21
|
+
migration: ["migration", "impact-analysis"],
|
|
22
|
+
test: ["testing"]
|
|
23
|
+
};
|
|
24
|
+
function createDefaultAgentPermissions() {
|
|
25
|
+
return { ...DEFAULT_AGENT_PERMISSIONS };
|
|
26
|
+
}
|
|
27
|
+
function applyStrictestAgentPermissions(...permissions) {
|
|
28
|
+
const merged = createDefaultAgentPermissions();
|
|
29
|
+
for (const candidate of permissions) {
|
|
30
|
+
if (candidate === undefined)
|
|
31
|
+
continue;
|
|
32
|
+
for (const key of Object.keys(merged)) {
|
|
33
|
+
merged[key] = merged[key] && candidate[key] === true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return merged;
|
|
37
|
+
}
|
|
38
|
+
function parseAgentProfile(value) {
|
|
39
|
+
if (!isRecord(value))
|
|
40
|
+
throw new Error("Agent profile must be an object.");
|
|
41
|
+
if (value.version !== 1)
|
|
42
|
+
throw new Error("Agent profile version must be 1.");
|
|
43
|
+
const profile = value;
|
|
44
|
+
assertString(profile.id, "profile.id");
|
|
45
|
+
assertString(profile.name, "profile.name");
|
|
46
|
+
if (profile.persistence !== "named" && profile.persistence !== "dynamic") {
|
|
47
|
+
throw new Error("Agent profile persistence must be named or dynamic.");
|
|
48
|
+
}
|
|
49
|
+
if (profile.privacy?.metadataOnly !== true || profile.privacy.rawPromptStored !== false || profile.privacy.sourceContentStored !== false) {
|
|
50
|
+
throw new Error("Agent profile must be metadata-only and must not store raw prompts/source.");
|
|
51
|
+
}
|
|
52
|
+
const permissions = normalizeAgentPermissions(profile.permissions);
|
|
53
|
+
if (permissions.canWriteFiles || permissions.canRunCommands || permissions.canUseNetwork || permissions.canSpawnAgents || permissions.canWriteMemory || permissions.canReadSourceContent) {
|
|
54
|
+
throw new Error("Agent profile requests permissions outside I8 dry-run boundaries.");
|
|
55
|
+
}
|
|
56
|
+
if (!SAFE_AGENT_ID_PATTERN.test(profile.id))
|
|
57
|
+
throw new Error("Agent profile id is unsafe.");
|
|
58
|
+
assertStringArray(profile.traits?.expertise, "profile.traits.expertise");
|
|
59
|
+
assertStringArray(profile.traits?.stance, "profile.traits.stance");
|
|
60
|
+
assertStringArray(profile.traits?.approach, "profile.traits.approach");
|
|
61
|
+
assertStringArray(profile.traits?.domain, "profile.traits.domain");
|
|
62
|
+
assertStringArray(profile.inputs?.required, "profile.inputs.required");
|
|
63
|
+
assertStringArray(profile.inputs?.optional, "profile.inputs.optional");
|
|
64
|
+
assertStringArray(profile.inputs?.forbidden, "profile.inputs.forbidden");
|
|
65
|
+
if (!FORBIDDEN_INPUTS.every((item) => profile.inputs.forbidden.includes(item))) {
|
|
66
|
+
throw new Error("Agent profile must forbid raw prompts, secrets, raw command logs, and source corpus.");
|
|
67
|
+
}
|
|
68
|
+
if (!isKnownOutputSchema(profile.output?.schema))
|
|
69
|
+
throw new Error("Agent profile output schema is unsupported.");
|
|
70
|
+
assertStringArray(profile.output.requiredFields, "profile.output.requiredFields");
|
|
71
|
+
return { ...profile, permissions };
|
|
72
|
+
}
|
|
73
|
+
async function readAgentProfile(path) {
|
|
74
|
+
return parseAgentProfile(JSON.parse(await readFile(path, "utf8")));
|
|
75
|
+
}
|
|
76
|
+
function composeAgentDryRun(input) {
|
|
77
|
+
const mode = input.contract.route.mode;
|
|
78
|
+
const workflowId = input.workflowId ?? input.contract.route.workflowId;
|
|
79
|
+
const warnings = [];
|
|
80
|
+
const blockers = [];
|
|
81
|
+
const permissions = createDefaultAgentPermissions();
|
|
82
|
+
const requestedLenses = dedupeLenses(input.lenses ?? []);
|
|
83
|
+
const privacyBlockers = collectPrivacyBoundaryBlockers(input.contract, requestedLenses, workflowId);
|
|
84
|
+
blockers.push(...privacyBlockers);
|
|
85
|
+
if (mode === "minimal") {
|
|
86
|
+
return {
|
|
87
|
+
ok: blockers.length === 0,
|
|
88
|
+
taskId: input.contract.taskId,
|
|
89
|
+
mode,
|
|
90
|
+
workflowId,
|
|
91
|
+
agents: [],
|
|
92
|
+
permissions,
|
|
93
|
+
mergePlan: createMergePlan([]),
|
|
94
|
+
warnings,
|
|
95
|
+
blockers,
|
|
96
|
+
rationale: "Minimal mode does not force dynamic agents by default."
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const selectedLenses = selectLenses(mode, requestedLenses, workflowId, input.contract.route.requiredReview);
|
|
100
|
+
const agents = selectedLenses.map((lens) => ({
|
|
101
|
+
profile: createDynamicProfile(lens, input.contract, permissions),
|
|
102
|
+
lens,
|
|
103
|
+
reason: createLensReason(lens, mode, workflowId),
|
|
104
|
+
plannedOnly: true
|
|
105
|
+
}));
|
|
106
|
+
if (mode === "standard" && agents.length > 1) {
|
|
107
|
+
blockers.push("Standard mode allows at most one optional reviewer/triager in I8 dry-run.");
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
ok: blockers.length === 0,
|
|
111
|
+
taskId: input.contract.taskId,
|
|
112
|
+
mode,
|
|
113
|
+
workflowId,
|
|
114
|
+
agents,
|
|
115
|
+
permissions,
|
|
116
|
+
mergePlan: createMergePlan(agents),
|
|
117
|
+
warnings,
|
|
118
|
+
blockers,
|
|
119
|
+
rationale: agents.length === 0 ? "No dynamic agents selected; deterministic verification may be sufficient." : `Selected ${agents.length} planned-only agent(s) based on mode/workflow/lenses.`
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function loadAgentContextDryRun(profile) {
|
|
123
|
+
const parsed = parseAgentProfile(profile);
|
|
124
|
+
return {
|
|
125
|
+
profile: parsed,
|
|
126
|
+
context: {
|
|
127
|
+
metadataOnly: true,
|
|
128
|
+
allowedSections: [
|
|
129
|
+
"role",
|
|
130
|
+
"inputs",
|
|
131
|
+
"permissions",
|
|
132
|
+
"output-schema",
|
|
133
|
+
"privacy",
|
|
134
|
+
"failure-behavior"
|
|
135
|
+
],
|
|
136
|
+
forbiddenSections: [...FORBIDDEN_INPUTS, "rawSource", "rawEvidence", "memoryBodies"],
|
|
137
|
+
provenance: [`agent-profile:${parsed.id}`]
|
|
138
|
+
},
|
|
139
|
+
warnings: [],
|
|
140
|
+
blockers: []
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function validateAgentOutput(schema, output) {
|
|
144
|
+
if (schema === "verification-summary-v1")
|
|
145
|
+
return validateRequiredObject(output, ["summary", "status", "evidenceRefs"]);
|
|
146
|
+
if (schema === "design-options-v1")
|
|
147
|
+
return validateRequiredObject(output, ["summary", "options", "recommendation", "evidenceRefs"]);
|
|
148
|
+
const errors = [];
|
|
149
|
+
if (!isRecord(output))
|
|
150
|
+
return { ok: false, errors: ["Output must be an object."] };
|
|
151
|
+
if (typeof output.summary !== "string")
|
|
152
|
+
errors.push("summary is required.");
|
|
153
|
+
if (!Array.isArray(output.findings))
|
|
154
|
+
errors.push("findings array is required.");
|
|
155
|
+
if (output.confidence !== "low" && output.confidence !== "medium" && output.confidence !== "high") {
|
|
156
|
+
errors.push("confidence must be low/medium/high.");
|
|
157
|
+
}
|
|
158
|
+
if (!Array.isArray(output.evidenceRefs))
|
|
159
|
+
errors.push("evidenceRefs array is required.");
|
|
160
|
+
return { ok: errors.length === 0, errors };
|
|
161
|
+
}
|
|
162
|
+
function mergeReviewFindings(outputs) {
|
|
163
|
+
const byKey = new Map;
|
|
164
|
+
const conflicts = [];
|
|
165
|
+
for (const output of outputs) {
|
|
166
|
+
for (const finding of output.findings) {
|
|
167
|
+
const key = `${finding.category}:${finding.path ?? ""}:${finding.line ?? ""}:${finding.title}`;
|
|
168
|
+
const existing = byKey.get(key);
|
|
169
|
+
if (existing === undefined) {
|
|
170
|
+
byKey.set(key, finding);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (severityRank(finding.severity) > severityRank(existing.severity))
|
|
174
|
+
byKey.set(key, finding);
|
|
175
|
+
if (finding.severity !== existing.severity || finding.recommendation !== existing.recommendation) {
|
|
176
|
+
conflicts.push(key);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { acceptedFindings: [...byKey.values()], unresolvedConflicts: conflicts };
|
|
181
|
+
}
|
|
182
|
+
function formatAgentComposeDryRun(plan) {
|
|
183
|
+
return [
|
|
184
|
+
"EvoDev agent compose dry-run",
|
|
185
|
+
"",
|
|
186
|
+
`Task: ${plan.taskId}`,
|
|
187
|
+
`Mode: ${plan.mode ?? "not routed"}`,
|
|
188
|
+
`Workflow: ${plan.workflowId ?? "none"}`,
|
|
189
|
+
`Rationale: ${plan.rationale}`,
|
|
190
|
+
"No-write/no-spawn: true",
|
|
191
|
+
"Privacy: local-private, metadata-only, raw prompts/source/secrets/logs forbidden",
|
|
192
|
+
"Agents:",
|
|
193
|
+
...plan.agents.length === 0 ? [" - none"] : plan.agents.map((agent) => ` - ${agent.profile.id} [${agent.lens}] traits=${agent.profile.traits.expertise.join(",")} schema=${agent.profile.output.schema} plannedOnly=${agent.plannedOnly}`),
|
|
194
|
+
"Inputs:",
|
|
195
|
+
...plan.agents.length === 0 ? [" - none"] : [
|
|
196
|
+
` - required: ${plan.agents[0].profile.inputs.required.join(",")}`,
|
|
197
|
+
` - optional: ${plan.agents[0].profile.inputs.optional.join(",")}`,
|
|
198
|
+
` - forbidden: ${plan.agents[0].profile.inputs.forbidden.join(",")}`
|
|
199
|
+
],
|
|
200
|
+
"Permissions:",
|
|
201
|
+
...Object.entries(plan.permissions).map(([key, value]) => ` - ${key}: ${value}`),
|
|
202
|
+
"Merge plan:",
|
|
203
|
+
` - strategy: ${plan.mergePlan.strategy}`,
|
|
204
|
+
` - outputSchema: ${plan.mergePlan.outputSchema ?? "none"}`,
|
|
205
|
+
"Warnings:",
|
|
206
|
+
...plan.warnings.length === 0 ? [" - none"] : plan.warnings.map((warning) => ` - ${warning}`),
|
|
207
|
+
"Blockers:",
|
|
208
|
+
...plan.blockers.length === 0 ? [" - none"] : plan.blockers.map((blocker) => ` - ${blocker}`)
|
|
209
|
+
].join(`
|
|
210
|
+
`);
|
|
211
|
+
}
|
|
212
|
+
function formatAgentContextDryRun(bundle) {
|
|
213
|
+
return [
|
|
214
|
+
"EvoDev agent load-context dry-run",
|
|
215
|
+
"",
|
|
216
|
+
`Agent: ${bundle.profile.id}`,
|
|
217
|
+
`Persistence: ${bundle.profile.persistence}`,
|
|
218
|
+
"Allowed sections:",
|
|
219
|
+
...bundle.context.allowedSections.map((section) => ` - ${section}`),
|
|
220
|
+
"Forbidden sections:",
|
|
221
|
+
...bundle.context.forbiddenSections.map((section) => ` - ${section}`),
|
|
222
|
+
"Permissions:",
|
|
223
|
+
...Object.entries(bundle.profile.permissions).map(([key, value]) => ` - ${key}: ${value}`),
|
|
224
|
+
"Blockers:",
|
|
225
|
+
...bundle.blockers.length === 0 ? [" - none"] : bundle.blockers.map((blocker) => ` - ${blocker}`)
|
|
226
|
+
].join(`
|
|
227
|
+
`);
|
|
228
|
+
}
|
|
229
|
+
function createDynamicProfile(lens, contract, permissions) {
|
|
230
|
+
return {
|
|
231
|
+
version: 1,
|
|
232
|
+
id: `dynamic-${lens}-${contract.taskId}`.slice(0, 100),
|
|
233
|
+
name: `Dynamic ${lens} reviewer`,
|
|
234
|
+
persistence: "dynamic",
|
|
235
|
+
description: `Task-scoped ${lens} profile for metadata-only dry-run planning.`,
|
|
236
|
+
traits: {
|
|
237
|
+
expertise: LENS_TO_EXPERTISE[lens],
|
|
238
|
+
stance: ["skeptical-reviewer"],
|
|
239
|
+
approach: ["evidence-first", "fail-closed"],
|
|
240
|
+
domain: ["software-rd"]
|
|
241
|
+
},
|
|
242
|
+
inputs: {
|
|
243
|
+
required: ["taskContract", "scope", "evidenceSummary"],
|
|
244
|
+
optional: ["workflowPlan"],
|
|
245
|
+
forbidden: FORBIDDEN_INPUTS
|
|
246
|
+
},
|
|
247
|
+
permissions,
|
|
248
|
+
output: {
|
|
249
|
+
schema: "review-findings-v1",
|
|
250
|
+
requiredFields: ["summary", "findings", "confidence", "evidenceRefs"]
|
|
251
|
+
},
|
|
252
|
+
privacy: {
|
|
253
|
+
classification: "local-private",
|
|
254
|
+
metadataOnly: true,
|
|
255
|
+
rawPromptStored: false,
|
|
256
|
+
sourceContentStored: false
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function selectLenses(mode, requested, workflowId, requiredReview) {
|
|
261
|
+
if (requested.length > 0)
|
|
262
|
+
return mode === "standard" ? requested.slice(0, 1) : requested;
|
|
263
|
+
if (mode === "standard")
|
|
264
|
+
return requiredReview.length > 0 ? ["review"] : [];
|
|
265
|
+
if (mode === "rigorous") {
|
|
266
|
+
if (workflowId?.includes("security"))
|
|
267
|
+
return ["security", "review"];
|
|
268
|
+
if (workflowId?.includes("release"))
|
|
269
|
+
return ["release", "security"];
|
|
270
|
+
if (workflowId?.includes("migration"))
|
|
271
|
+
return ["migration", "architecture"];
|
|
272
|
+
if (workflowId?.includes("architecture"))
|
|
273
|
+
return ["architecture", "review"];
|
|
274
|
+
return requiredReview.length > 0 ? ["review", "qa"] : [];
|
|
275
|
+
}
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
function createMergePlan(agents) {
|
|
279
|
+
if (agents.length === 0)
|
|
280
|
+
return {
|
|
281
|
+
strategy: "none",
|
|
282
|
+
outputSchema: null,
|
|
283
|
+
dedupeBy: [],
|
|
284
|
+
conflictPolicy: [],
|
|
285
|
+
failClosedCategories: []
|
|
286
|
+
};
|
|
287
|
+
if (agents.length === 1)
|
|
288
|
+
return {
|
|
289
|
+
strategy: "single-output",
|
|
290
|
+
outputSchema: agents[0].profile.output.schema,
|
|
291
|
+
dedupeBy: [],
|
|
292
|
+
conflictPolicy: [],
|
|
293
|
+
failClosedCategories: []
|
|
294
|
+
};
|
|
295
|
+
return {
|
|
296
|
+
strategy: "dedupe-preserve-conflicts-fail-closed",
|
|
297
|
+
outputSchema: "review-findings-v1",
|
|
298
|
+
dedupeBy: ["category", "path", "line", "title"],
|
|
299
|
+
conflictPolicy: ["preserve dissent", "keep higher severity unless evidence refutes"],
|
|
300
|
+
failClosedCategories: ["security", "privacy", "release"]
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function createLensReason(lens, mode, workflowId) {
|
|
304
|
+
return `${lens} lens selected for ${mode ?? "unrouted"} mode${workflowId ? ` and workflow ${workflowId}` : ""}.`;
|
|
305
|
+
}
|
|
306
|
+
function collectPrivacyBoundaryBlockers(contract, lenses, workflowId) {
|
|
307
|
+
const text = [
|
|
308
|
+
contract.source.summary,
|
|
309
|
+
contract.currentState.summary,
|
|
310
|
+
contract.targetState.summary,
|
|
311
|
+
contract.route.rationale,
|
|
312
|
+
workflowId ?? "",
|
|
313
|
+
...contract.scope.allowedOperations,
|
|
314
|
+
...contract.scope.requiresUserConfirmation,
|
|
315
|
+
...lenses
|
|
316
|
+
].join(" ");
|
|
317
|
+
return PRIVACY_BLOCKER_PATTERN.test(text) ? [
|
|
318
|
+
"Agent planning refuses privacy/risky context requiring raw data, writes, commands, network, spawning, or memory."
|
|
319
|
+
] : [];
|
|
320
|
+
}
|
|
321
|
+
function validateRequiredObject(output, fields) {
|
|
322
|
+
if (!isRecord(output))
|
|
323
|
+
return { ok: false, errors: ["Output must be an object."] };
|
|
324
|
+
const errors = fields.filter((field) => output[field] === undefined).map((field) => `${field} is required.`);
|
|
325
|
+
return { ok: errors.length === 0, errors };
|
|
326
|
+
}
|
|
327
|
+
function normalizeAgentPermissions(value) {
|
|
328
|
+
return { ...createDefaultAgentPermissions(), ...value };
|
|
329
|
+
}
|
|
330
|
+
function isKnownOutputSchema(value) {
|
|
331
|
+
return value === "review-findings-v1" || value === "verification-summary-v1" || value === "design-options-v1";
|
|
332
|
+
}
|
|
333
|
+
function dedupeLenses(lenses) {
|
|
334
|
+
return [...new Set(lenses)];
|
|
335
|
+
}
|
|
336
|
+
function severityRank(severity) {
|
|
337
|
+
return { info: 0, low: 1, medium: 2, high: 3, blocker: 4 }[severity];
|
|
338
|
+
}
|
|
339
|
+
function assertString(value, path) {
|
|
340
|
+
if (typeof value !== "string" || value.length === 0)
|
|
341
|
+
throw new Error(`${path} must be a non-empty string.`);
|
|
342
|
+
}
|
|
343
|
+
function assertStringArray(value, path) {
|
|
344
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) {
|
|
345
|
+
throw new Error(`${path} must be an array of non-empty strings.`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function isRecord(value) {
|
|
349
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
350
|
+
}
|
|
351
|
+
// packages/core/src/assets/errors.ts
|
|
352
|
+
class EvoDevAssetError extends Error {
|
|
353
|
+
filePath;
|
|
354
|
+
constructor(message, filePath) {
|
|
355
|
+
super(filePath ? `${message}: ${filePath}` : message);
|
|
356
|
+
this.filePath = filePath;
|
|
357
|
+
this.name = "EvoDevAssetError";
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function describeType(value) {
|
|
361
|
+
if (value === null) {
|
|
362
|
+
return "null";
|
|
363
|
+
}
|
|
364
|
+
if (Array.isArray(value)) {
|
|
365
|
+
return "array";
|
|
366
|
+
}
|
|
367
|
+
return typeof value;
|
|
368
|
+
}
|
|
369
|
+
// packages/core/src/assets/manifest.ts
|
|
370
|
+
function parseSkillManifest(value) {
|
|
371
|
+
return {
|
|
372
|
+
...parseBaseManifest(value, "skill manifest"),
|
|
373
|
+
kind: "skill"
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function parseAgentManifest(value) {
|
|
377
|
+
return {
|
|
378
|
+
...parseBaseManifest(value, "agent manifest"),
|
|
379
|
+
kind: "agent"
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function parseBaseManifest(value, label) {
|
|
383
|
+
const root = expectRecord(value, label);
|
|
384
|
+
const manifest = {
|
|
385
|
+
id: expectSlug(root.id, `${label}.id`),
|
|
386
|
+
name: expectNonEmptyString(root.name, `${label}.name`),
|
|
387
|
+
version: expectNonEmptyString(root.version, `${label}.version`),
|
|
388
|
+
category: expectSlug(root.category, `${label}.category`),
|
|
389
|
+
description: expectNonEmptyString(root.description, `${label}.description`),
|
|
390
|
+
targets: expectTargets(root.targets, `${label}.targets`),
|
|
391
|
+
entry: expectRelativePath(root.entry, `${label}.entry`)
|
|
392
|
+
};
|
|
393
|
+
if (root.license !== undefined) {
|
|
394
|
+
manifest.license = expectNonEmptyString(root.license, `${label}.license`);
|
|
395
|
+
}
|
|
396
|
+
return manifest;
|
|
397
|
+
}
|
|
398
|
+
function expectRecord(value, path) {
|
|
399
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
400
|
+
throw new EvoDevAssetError(`Invalid ${path}; expected object, got ${describeType(value)}`);
|
|
401
|
+
}
|
|
402
|
+
return value;
|
|
403
|
+
}
|
|
404
|
+
function expectNonEmptyString(value, path) {
|
|
405
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
406
|
+
throw new EvoDevAssetError(`Invalid ${path}; expected non-empty string`);
|
|
407
|
+
}
|
|
408
|
+
return value;
|
|
409
|
+
}
|
|
410
|
+
function expectSlug(value, path) {
|
|
411
|
+
const text = expectNonEmptyString(value, path);
|
|
412
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(text)) {
|
|
413
|
+
throw new EvoDevAssetError(`Invalid ${path}; expected lowercase kebab-case slug`);
|
|
414
|
+
}
|
|
415
|
+
return text;
|
|
416
|
+
}
|
|
417
|
+
function expectTargets(value, path) {
|
|
418
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
419
|
+
throw new EvoDevAssetError(`Invalid ${path}; expected non-empty target array`);
|
|
420
|
+
}
|
|
421
|
+
const targets = value.map((target, index) => {
|
|
422
|
+
if (target !== "claude" && target !== "codex") {
|
|
423
|
+
throw new EvoDevAssetError(`Invalid ${path}[${index}]; expected claude or codex`);
|
|
424
|
+
}
|
|
425
|
+
return target;
|
|
426
|
+
});
|
|
427
|
+
return [...new Set(targets)];
|
|
428
|
+
}
|
|
429
|
+
function expectRelativePath(value, path) {
|
|
430
|
+
const text = expectNonEmptyString(value, path);
|
|
431
|
+
if (text.startsWith("/") || text.includes("..")) {
|
|
432
|
+
throw new EvoDevAssetError(`Invalid ${path}; expected safe relative path`);
|
|
433
|
+
}
|
|
434
|
+
return text;
|
|
435
|
+
}
|
|
436
|
+
// packages/core/src/assets/scanner.ts
|
|
437
|
+
import { readFile as readFile2, readdir, stat } from "node:fs/promises";
|
|
438
|
+
import { join, relative } from "node:path";
|
|
439
|
+
async function scanAssets(paths) {
|
|
440
|
+
const [skills, agents] = await Promise.all([
|
|
441
|
+
scanSkillAssets(paths.skillsDir),
|
|
442
|
+
scanAgentAssets(paths.agentsDir)
|
|
443
|
+
]);
|
|
444
|
+
return { skills, agents };
|
|
445
|
+
}
|
|
446
|
+
async function scanSkillAssets(skillsDir) {
|
|
447
|
+
return scanAssetKind(skillsDir, parseSkillManifest);
|
|
448
|
+
}
|
|
449
|
+
async function scanAgentAssets(agentsDir) {
|
|
450
|
+
return scanAssetKind(agentsDir, parseAgentManifest);
|
|
451
|
+
}
|
|
452
|
+
async function scanAssetKind(rootDir, parseManifest) {
|
|
453
|
+
if (!await pathExists(rootDir)) {
|
|
454
|
+
return [];
|
|
455
|
+
}
|
|
456
|
+
const manifestPaths = await findManifestFiles(rootDir);
|
|
457
|
+
const assets = await Promise.all(manifestPaths.map((manifestPath) => readScannedAsset(rootDir, manifestPath, parseManifest)));
|
|
458
|
+
return assets.sort((left, right) => left.registryKey.localeCompare(right.registryKey));
|
|
459
|
+
}
|
|
460
|
+
async function findManifestFiles(rootDir) {
|
|
461
|
+
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
462
|
+
const nested = await Promise.all(entries.map(async (entry) => {
|
|
463
|
+
const entryPath = join(rootDir, entry.name);
|
|
464
|
+
if (entry.isDirectory()) {
|
|
465
|
+
return findManifestFiles(entryPath);
|
|
466
|
+
}
|
|
467
|
+
if (entry.isFile() && entry.name === "manifest.json") {
|
|
468
|
+
return [entryPath];
|
|
469
|
+
}
|
|
470
|
+
return [];
|
|
471
|
+
}));
|
|
472
|
+
return nested.flat();
|
|
473
|
+
}
|
|
474
|
+
async function readScannedAsset(rootDir, manifestPath, parseManifest) {
|
|
475
|
+
const json = await readManifestJson(manifestPath);
|
|
476
|
+
const manifest = parseManifestWithPath(json, manifestPath, parseManifest);
|
|
477
|
+
const assetDir = manifestPath.slice(0, -"/manifest.json".length);
|
|
478
|
+
const entryPath = join(assetDir, manifest.entry);
|
|
479
|
+
if (!await isFile(entryPath)) {
|
|
480
|
+
throw new EvoDevAssetError(`Asset entry file not found for ${manifest.id}`, entryPath);
|
|
481
|
+
}
|
|
482
|
+
const registryKey = createRegistryKey(rootDir, assetDir, manifest);
|
|
483
|
+
return {
|
|
484
|
+
manifest,
|
|
485
|
+
manifestPath,
|
|
486
|
+
entryPath,
|
|
487
|
+
assetDir,
|
|
488
|
+
registryKey
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
async function readManifestJson(manifestPath) {
|
|
492
|
+
let raw;
|
|
493
|
+
try {
|
|
494
|
+
raw = await readFile2(manifestPath, "utf8");
|
|
495
|
+
} catch (error) {
|
|
496
|
+
throw new EvoDevAssetError(`Cannot read manifest (${describeFileError(error)})`, manifestPath);
|
|
497
|
+
}
|
|
498
|
+
try {
|
|
499
|
+
return JSON.parse(raw);
|
|
500
|
+
} catch (error) {
|
|
501
|
+
throw new EvoDevAssetError(`Invalid manifest JSON (${describeFileError(error)})`, manifestPath);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function parseManifestWithPath(json, manifestPath, parseManifest) {
|
|
505
|
+
try {
|
|
506
|
+
return parseManifest(json);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
if (error instanceof EvoDevAssetError) {
|
|
509
|
+
throw new EvoDevAssetError(error.message, manifestPath);
|
|
510
|
+
}
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function createRegistryKey(rootDir, assetDir, manifest) {
|
|
515
|
+
const relativeDir = relative(rootDir, assetDir).replaceAll("\\", "/");
|
|
516
|
+
if (relativeDir === "" || relativeDir === manifest.id) {
|
|
517
|
+
return manifest.id;
|
|
518
|
+
}
|
|
519
|
+
return relativeDir;
|
|
520
|
+
}
|
|
521
|
+
async function pathExists(path) {
|
|
522
|
+
try {
|
|
523
|
+
await stat(path);
|
|
524
|
+
return true;
|
|
525
|
+
} catch (error) {
|
|
526
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
async function isFile(path) {
|
|
533
|
+
try {
|
|
534
|
+
const fileStat = await stat(path);
|
|
535
|
+
return fileStat.isFile();
|
|
536
|
+
} catch (error) {
|
|
537
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function describeFileError(error) {
|
|
544
|
+
if (error instanceof Error) {
|
|
545
|
+
return error.message;
|
|
546
|
+
}
|
|
547
|
+
return String(error);
|
|
548
|
+
}
|
|
549
|
+
function isNodeError(error) {
|
|
550
|
+
return error instanceof Error && "code" in error;
|
|
551
|
+
}
|
|
552
|
+
// packages/core/src/config/errors.ts
|
|
553
|
+
class EvoDevConfigError extends Error {
|
|
554
|
+
filePath;
|
|
555
|
+
constructor(message, filePath) {
|
|
556
|
+
super(filePath ? `${message}: ${filePath}` : message);
|
|
557
|
+
this.filePath = filePath;
|
|
558
|
+
this.name = "EvoDevConfigError";
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function describeType2(value) {
|
|
562
|
+
if (value === null) {
|
|
563
|
+
return "null";
|
|
564
|
+
}
|
|
565
|
+
if (Array.isArray(value)) {
|
|
566
|
+
return "array";
|
|
567
|
+
}
|
|
568
|
+
return typeof value;
|
|
569
|
+
}
|
|
570
|
+
// packages/core/src/config/paths.ts
|
|
571
|
+
function resolveEvoDevPaths(homeDir = getHomeDir()) {
|
|
572
|
+
const normalizedHome = stripTrailingSlash(homeDir);
|
|
573
|
+
const rootDir = `${normalizedHome}/.evodev`;
|
|
574
|
+
const stateDir = `${rootDir}/state`;
|
|
575
|
+
return {
|
|
576
|
+
homeDir: normalizedHome,
|
|
577
|
+
rootDir,
|
|
578
|
+
settingsPath: `${rootDir}/settings.json`,
|
|
579
|
+
registryPath: `${rootDir}/registry.json`,
|
|
580
|
+
stateDir,
|
|
581
|
+
installStatePath: `${stateDir}/install.json`,
|
|
582
|
+
syncStatePath: `${stateDir}/sync.json`
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function getHomeDir() {
|
|
586
|
+
const home = process.env.HOME;
|
|
587
|
+
if (home === undefined || home.trim() === "") {
|
|
588
|
+
throw new Error("Cannot resolve home directory: HOME is not set");
|
|
589
|
+
}
|
|
590
|
+
return home;
|
|
591
|
+
}
|
|
592
|
+
function stripTrailingSlash(path) {
|
|
593
|
+
if (path === "/") {
|
|
594
|
+
return path;
|
|
595
|
+
}
|
|
596
|
+
return path.replace(/\/+$/, "");
|
|
597
|
+
}
|
|
598
|
+
// packages/core/src/config/registry.ts
|
|
599
|
+
function createDefaultRegistry() {
|
|
600
|
+
return {
|
|
601
|
+
version: 1,
|
|
602
|
+
skills: {},
|
|
603
|
+
agents: {}
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function parseRegistry(value) {
|
|
607
|
+
const root = expectRecord2(value, "registry");
|
|
608
|
+
if (root.version !== 1) {
|
|
609
|
+
throw new EvoDevConfigError("Invalid registry.version; expected 1");
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
version: 1,
|
|
613
|
+
skills: parseAssetMap(root.skills, "registry.skills"),
|
|
614
|
+
agents: parseAssetMap(root.agents, "registry.agents")
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function parseAssetMap(value, path) {
|
|
618
|
+
const input = expectRecord2(value, path);
|
|
619
|
+
const output = {};
|
|
620
|
+
for (const [assetId, asset] of Object.entries(input)) {
|
|
621
|
+
output[assetId] = parseRegisteredAsset(asset, `${path}.${assetId}`);
|
|
622
|
+
}
|
|
623
|
+
return output;
|
|
624
|
+
}
|
|
625
|
+
function parseRegisteredAsset(value, path) {
|
|
626
|
+
const input = expectRecord2(value, path);
|
|
627
|
+
const source = input.source;
|
|
628
|
+
if (source !== "builtin" && source !== "user") {
|
|
629
|
+
throw new EvoDevConfigError(`Invalid ${path}.source; expected builtin or user`);
|
|
630
|
+
}
|
|
631
|
+
if (!Array.isArray(input.targets) || !input.targets.every((target) => typeof target === "string")) {
|
|
632
|
+
throw new EvoDevConfigError(`Invalid ${path}.targets; expected string array`);
|
|
633
|
+
}
|
|
634
|
+
return {
|
|
635
|
+
version: expectString(input.version, `${path}.version`),
|
|
636
|
+
source,
|
|
637
|
+
targets: [...input.targets]
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function expectRecord2(value, path) {
|
|
641
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
642
|
+
throw new EvoDevConfigError(`Invalid ${path}; expected object, got ${describeType2(value)}`);
|
|
643
|
+
}
|
|
644
|
+
return value;
|
|
645
|
+
}
|
|
646
|
+
function expectString(value, path) {
|
|
647
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
648
|
+
throw new EvoDevConfigError(`Invalid ${path}; expected non-empty string`);
|
|
649
|
+
}
|
|
650
|
+
return value;
|
|
651
|
+
}
|
|
652
|
+
// packages/core/src/hooks/index.ts
|
|
653
|
+
import { createHash } from "node:crypto";
|
|
654
|
+
import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
|
|
655
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
656
|
+
|
|
657
|
+
// packages/core/src/task/index.ts
|
|
658
|
+
import { lstat, mkdir, readFile as readFile3, realpath, stat as stat2, writeFile } from "node:fs/promises";
|
|
659
|
+
import { basename, dirname, join as join2, resolve } from "node:path";
|
|
660
|
+
var FORBIDDEN_TASK_PATHS = ["CLAUDE.md", "AGENTS.md", ".claude/**", ".codex/**"];
|
|
661
|
+
var FORBIDDEN_TASK_WRITE_SEGMENTS = new Set([".claude", ".codex"]);
|
|
662
|
+
var FORBIDDEN_PROJECT_ASSET_SEGMENTS = new Set(["packages", "src"]);
|
|
663
|
+
var FORBIDDEN_TASK_WRITE_FILES = new Set(["agents.md", "claude.md", "package.json", "readme.md"]);
|
|
664
|
+
var FORBIDDEN_RAW_KEYS = new Set([
|
|
665
|
+
"rawoutput",
|
|
666
|
+
"raw_output",
|
|
667
|
+
"stdout",
|
|
668
|
+
"stderr",
|
|
669
|
+
"source",
|
|
670
|
+
"sourcecontent",
|
|
671
|
+
"source_content",
|
|
672
|
+
"sourcetext",
|
|
673
|
+
"source_text",
|
|
674
|
+
"prompt",
|
|
675
|
+
"prompttext",
|
|
676
|
+
"prompt_text",
|
|
677
|
+
"transcript",
|
|
678
|
+
"transcripttext",
|
|
679
|
+
"transcript_text",
|
|
680
|
+
"secret",
|
|
681
|
+
"secretvalue",
|
|
682
|
+
"secret_value"
|
|
683
|
+
]);
|
|
684
|
+
var ALLOWED_VERIFICATION_KEYS = new Set([
|
|
685
|
+
"acceptanceResults",
|
|
686
|
+
"antiCriteriaResults",
|
|
687
|
+
"commands",
|
|
688
|
+
"evidence",
|
|
689
|
+
"exitCode",
|
|
690
|
+
"id",
|
|
691
|
+
"status",
|
|
692
|
+
"summary",
|
|
693
|
+
"type"
|
|
694
|
+
]);
|
|
695
|
+
var SENSITIVE_TEXT_PATTERN = /https?:\/\/\S+|\b(secret|token|password|passwd|private|internal|api[_-]?key|apikey|credential|credentials|raw log|raw source|raw prompt)\b/gi;
|
|
696
|
+
function createTaskContract(input) {
|
|
697
|
+
const taskId = createTaskId(input.title);
|
|
698
|
+
const summary = input.summary ?? input.title;
|
|
699
|
+
const acceptanceCriteria = input.acceptanceCriteria === undefined || input.acceptanceCriteria.length === 0 ? [
|
|
700
|
+
{
|
|
701
|
+
id: "AC1",
|
|
702
|
+
category: "engineering",
|
|
703
|
+
statement: "Task outcome satisfies the requested target state.",
|
|
704
|
+
requiredEvidence: ["verification-summary"],
|
|
705
|
+
status: "not-run"
|
|
706
|
+
}
|
|
707
|
+
] : input.acceptanceCriteria.map((criterion) => ({
|
|
708
|
+
id: sanitizeId(criterion.id),
|
|
709
|
+
category: criterion.category,
|
|
710
|
+
statement: sanitizeText(criterion.statement),
|
|
711
|
+
requiredEvidence: uniqueSanitizedIds(criterion.requiredEvidence ?? []),
|
|
712
|
+
status: "not-run"
|
|
713
|
+
}));
|
|
714
|
+
const defaultAntiCriteria = [
|
|
715
|
+
{
|
|
716
|
+
id: "ANTI1",
|
|
717
|
+
category: "privacy",
|
|
718
|
+
statement: "Do not collect source code, prompts, raw command output, secrets, or internal links.",
|
|
719
|
+
status: "unknown"
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
id: "ANTI2",
|
|
723
|
+
category: "scope",
|
|
724
|
+
statement: "Do not write outside approved task storage or approved implementation scope.",
|
|
725
|
+
status: "unknown"
|
|
726
|
+
}
|
|
727
|
+
];
|
|
728
|
+
return {
|
|
729
|
+
version: 1,
|
|
730
|
+
taskId,
|
|
731
|
+
status: "draft",
|
|
732
|
+
classification: "local-private",
|
|
733
|
+
source: { summary: sanitizeText(summary), rawPromptStored: false },
|
|
734
|
+
context: {
|
|
735
|
+
projectId: input.projectId === undefined || input.projectId === null ? null : sanitizeText(input.projectId),
|
|
736
|
+
relatedFiles: [],
|
|
737
|
+
assumptions: [],
|
|
738
|
+
openQuestions: []
|
|
739
|
+
},
|
|
740
|
+
currentState: { summary: "To be completed by the task owner.", evidenceRefs: [] },
|
|
741
|
+
targetState: { summary: sanitizeText(input.title), nonGoals: [], constraints: [] },
|
|
742
|
+
scope: {
|
|
743
|
+
allowedPaths: sanitizeTextList(input.allowedPaths ?? []),
|
|
744
|
+
forbiddenPaths: FORBIDDEN_TASK_PATHS,
|
|
745
|
+
allowedOperations: input.allowedOperations === undefined || input.allowedOperations.length === 0 ? ["read", "edit-approved-files", "run-local-tests"] : sanitizeTextList(input.allowedOperations),
|
|
746
|
+
requiresUserConfirmation: sanitizeTextList(input.requiresUserConfirmation ?? [])
|
|
747
|
+
},
|
|
748
|
+
acceptanceCriteria,
|
|
749
|
+
antiCriteria: [
|
|
750
|
+
...defaultAntiCriteria,
|
|
751
|
+
...(input.antiCriteria ?? []).map((criterion) => ({
|
|
752
|
+
id: sanitizeId(criterion.id),
|
|
753
|
+
category: criterion.category,
|
|
754
|
+
statement: sanitizeText(criterion.statement),
|
|
755
|
+
status: "unknown"
|
|
756
|
+
}))
|
|
757
|
+
],
|
|
758
|
+
route: {
|
|
759
|
+
mode: null,
|
|
760
|
+
workflowId: null,
|
|
761
|
+
rationale: "Not routed yet.",
|
|
762
|
+
requiredReview: [],
|
|
763
|
+
requiredVerification: uniqueSanitizedIds(input.requiredVerification ?? [])
|
|
764
|
+
},
|
|
765
|
+
verification: {
|
|
766
|
+
policy: "fail-closed",
|
|
767
|
+
commands: [],
|
|
768
|
+
acceptanceResults: [],
|
|
769
|
+
antiCriteriaResults: [],
|
|
770
|
+
status: "not-run",
|
|
771
|
+
summary: "Verification has not run."
|
|
772
|
+
},
|
|
773
|
+
evidence: { metadataOnly: true, items: [] },
|
|
774
|
+
learningCandidates: []
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function routeTaskContract(contract) {
|
|
778
|
+
const mode = selectMode(contract);
|
|
779
|
+
const requiredVerification = uniqueSanitizedIds([
|
|
780
|
+
...contract.route.requiredVerification ?? [],
|
|
781
|
+
"verification-summary"
|
|
782
|
+
]);
|
|
783
|
+
return {
|
|
784
|
+
...contract,
|
|
785
|
+
status: "routed",
|
|
786
|
+
route: {
|
|
787
|
+
mode,
|
|
788
|
+
workflowId: selectWorkflowId(contract, mode),
|
|
789
|
+
rationale: createRouteRationale(contract, mode),
|
|
790
|
+
requiredReview: mode === "rigorous" ? ["security-boundary", "verification"] : [],
|
|
791
|
+
requiredVerification
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
function verifyTaskContract(contract, input) {
|
|
796
|
+
assertMetadataOnly(input);
|
|
797
|
+
const commands = (input.commands ?? []).map((command) => ({
|
|
798
|
+
id: sanitizeId(command.id),
|
|
799
|
+
status: assertVerificationCommandStatus(command.status, `command ${command.id}`),
|
|
800
|
+
exitCode: command.exitCode,
|
|
801
|
+
summary: sanitizeText(command.summary ?? `${command.id}: ${command.status}`),
|
|
802
|
+
rawOutputStored: false
|
|
803
|
+
}));
|
|
804
|
+
const acceptanceResults = (input.acceptanceResults ?? []).map((result) => ({
|
|
805
|
+
id: sanitizeId(result.id),
|
|
806
|
+
status: assertAcceptanceResultStatus(result.status, `acceptance ${result.id}`),
|
|
807
|
+
summary: sanitizeText(result.summary ?? `${result.id}: ${result.status}`)
|
|
808
|
+
}));
|
|
809
|
+
const antiCriteriaResults = (input.antiCriteriaResults ?? []).map((result) => ({
|
|
810
|
+
id: sanitizeId(result.id),
|
|
811
|
+
status: assertAntiCriteriaResultStatus(result.status, `anti-criteria ${result.id}`),
|
|
812
|
+
summary: sanitizeText(result.summary ?? `${result.id}: ${result.status}`)
|
|
813
|
+
}));
|
|
814
|
+
const evidence = (input.evidence ?? []).map((item) => ({
|
|
815
|
+
type: assertEvidenceType(item.type, `evidence ${item.id}`),
|
|
816
|
+
id: sanitizeId(item.id),
|
|
817
|
+
status: assertVerificationStatus(item.status, `evidence ${item.id}`),
|
|
818
|
+
summary: sanitizeText(item.summary ?? `${item.id}: ${item.status}`),
|
|
819
|
+
rawOutputStored: false,
|
|
820
|
+
sourceContentStored: false
|
|
821
|
+
}));
|
|
822
|
+
const failures = collectVerificationFailures(contract, commands, acceptanceResults, antiCriteriaResults, evidence);
|
|
823
|
+
const ok = failures.length === 0;
|
|
824
|
+
const summary = ok ? "Verification passed." : `Verification failed: ${failures.join("; ")}`;
|
|
825
|
+
return {
|
|
826
|
+
ok,
|
|
827
|
+
summary,
|
|
828
|
+
contract: {
|
|
829
|
+
...contract,
|
|
830
|
+
status: ok ? "verified" : "failed",
|
|
831
|
+
acceptanceCriteria: contract.acceptanceCriteria.map((criterion) => ({
|
|
832
|
+
...criterion,
|
|
833
|
+
status: acceptanceResults.find((result) => result.id === criterion.id)?.status ?? criterion.status
|
|
834
|
+
})),
|
|
835
|
+
antiCriteria: contract.antiCriteria.map((criterion) => ({
|
|
836
|
+
...criterion,
|
|
837
|
+
status: antiCriteriaResults.find((result) => result.id === criterion.id)?.status ?? criterion.status
|
|
838
|
+
})),
|
|
839
|
+
verification: {
|
|
840
|
+
policy: "fail-closed",
|
|
841
|
+
commands,
|
|
842
|
+
acceptanceResults,
|
|
843
|
+
antiCriteriaResults,
|
|
844
|
+
status: ok ? "pass" : "fail",
|
|
845
|
+
summary
|
|
846
|
+
},
|
|
847
|
+
evidence: {
|
|
848
|
+
metadataOnly: true,
|
|
849
|
+
items: evidence
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
async function readTaskContract(path) {
|
|
855
|
+
return parseTaskContract(JSON.parse(await readFile3(path, "utf8")));
|
|
856
|
+
}
|
|
857
|
+
async function writeTaskContract(path, contract, options = {}) {
|
|
858
|
+
await assertTaskContractWritePathAllowed(path);
|
|
859
|
+
await mkdir(dirname(path), { recursive: true });
|
|
860
|
+
await writeFile(path, `${JSON.stringify(contract, null, 2)}
|
|
861
|
+
`, {
|
|
862
|
+
encoding: "utf8",
|
|
863
|
+
flag: options.overwrite === true ? "w" : "wx"
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
async function resolveTaskContractOutputPath(input) {
|
|
867
|
+
if (input.outputDir !== undefined) {
|
|
868
|
+
const outputPath = join2(input.outputDir, input.taskId, "contract.json");
|
|
869
|
+
await assertTaskContractWritePathAllowed(outputPath);
|
|
870
|
+
return outputPath;
|
|
871
|
+
}
|
|
872
|
+
if (input.projectDir !== undefined) {
|
|
873
|
+
const projectContextPath = join2(input.projectDir, ".evodev", "project.json");
|
|
874
|
+
if (!await pathExists2(projectContextPath)) {
|
|
875
|
+
throw new Error("Project mode requires existing .evodev/project.json; use --output-dir instead.");
|
|
876
|
+
}
|
|
877
|
+
const outputPath = join2(input.projectDir, ".evodev", "tasks", input.taskId, "contract.json");
|
|
878
|
+
await assertTaskContractWritePathAllowed(outputPath);
|
|
879
|
+
return outputPath;
|
|
880
|
+
}
|
|
881
|
+
throw new Error("Task writes require --output-dir or --project-dir with existing project context.");
|
|
882
|
+
}
|
|
883
|
+
function formatTaskContract(contract) {
|
|
884
|
+
return [
|
|
885
|
+
"EvoDev task contract",
|
|
886
|
+
"",
|
|
887
|
+
`Task id: ${contract.taskId}`,
|
|
888
|
+
`Status: ${contract.status}`,
|
|
889
|
+
`Summary: ${contract.source.summary}`,
|
|
890
|
+
`Mode: ${contract.route.mode ?? "not routed"}`,
|
|
891
|
+
`Workflow: ${contract.route.workflowId ?? "not routed"}`,
|
|
892
|
+
`Required verification: ${formatList(contract.route.requiredVerification)}`,
|
|
893
|
+
`Required review: ${formatList(contract.route.requiredReview)}`,
|
|
894
|
+
`Route rationale: ${contract.route.rationale}`,
|
|
895
|
+
`Verification: ${contract.verification.status}`,
|
|
896
|
+
`Verification summary: ${contract.verification.summary}`
|
|
897
|
+
].join(`
|
|
898
|
+
`);
|
|
899
|
+
}
|
|
900
|
+
function selectMode(contract) {
|
|
901
|
+
if (selectRigorousTrigger(contract) !== null) {
|
|
902
|
+
return "rigorous";
|
|
903
|
+
}
|
|
904
|
+
const allowedPaths = contract.scope.allowedPaths ?? [];
|
|
905
|
+
if (allowedPaths.length <= 1 && contract.acceptanceCriteria.length <= 1) {
|
|
906
|
+
return "minimal";
|
|
907
|
+
}
|
|
908
|
+
return "standard";
|
|
909
|
+
}
|
|
910
|
+
function selectWorkflowId(contract, mode) {
|
|
911
|
+
const text = collectRouteText(contract);
|
|
912
|
+
if (text.includes("release") || text.includes("publish"))
|
|
913
|
+
return "rd-release-readiness";
|
|
914
|
+
if (hasSecurityBoundaryTerms(text) || mode === "rigorous") {
|
|
915
|
+
return "rd-security-boundary-review";
|
|
916
|
+
}
|
|
917
|
+
if (text.includes("bug"))
|
|
918
|
+
return "rd-bug-fix";
|
|
919
|
+
if (text.includes("refactor"))
|
|
920
|
+
return "rd-refactor";
|
|
921
|
+
if (text.includes("review"))
|
|
922
|
+
return "rd-code-review";
|
|
923
|
+
if (text.includes("doc"))
|
|
924
|
+
return "rd-docs-update";
|
|
925
|
+
if (text.includes("test"))
|
|
926
|
+
return "rd-test-generation";
|
|
927
|
+
if (mode === "minimal")
|
|
928
|
+
return "rd-docs-update";
|
|
929
|
+
return "rd-feature-implementation";
|
|
930
|
+
}
|
|
931
|
+
function collectRouteText(contract) {
|
|
932
|
+
return [
|
|
933
|
+
contract.taskId,
|
|
934
|
+
contract.source.summary,
|
|
935
|
+
contract.currentState.summary,
|
|
936
|
+
...contract.currentState.evidenceRefs,
|
|
937
|
+
contract.targetState.summary,
|
|
938
|
+
...contract.targetState.nonGoals,
|
|
939
|
+
...contract.targetState.constraints,
|
|
940
|
+
...contract.context.relatedFiles.map((file) => `${file.path} ${file.reason}`),
|
|
941
|
+
...contract.context.assumptions,
|
|
942
|
+
...contract.context.openQuestions,
|
|
943
|
+
...contract.scope.allowedPaths ?? [],
|
|
944
|
+
...contract.scope.allowedOperations ?? [],
|
|
945
|
+
...contract.scope.requiresUserConfirmation ?? [],
|
|
946
|
+
...contract.acceptanceCriteria.map((criterion) => criterion.statement),
|
|
947
|
+
...contract.acceptanceCriteria.flatMap((criterion) => criterion.requiredEvidence),
|
|
948
|
+
...contract.route.requiredVerification ?? []
|
|
949
|
+
].join(" ").toLowerCase();
|
|
950
|
+
}
|
|
951
|
+
function createRouteRationale(contract, mode) {
|
|
952
|
+
const trigger = selectRigorousTrigger(contract);
|
|
953
|
+
if (mode === "rigorous")
|
|
954
|
+
return `Selected rigorous due to ${trigger ?? "high-risk"} trigger.`;
|
|
955
|
+
if (mode === "minimal")
|
|
956
|
+
return "Selected minimal for narrow scope and simple acceptance criteria.";
|
|
957
|
+
return "Selected standard for bounded engineering work requiring Task Contract verification.";
|
|
958
|
+
}
|
|
959
|
+
function selectRigorousTrigger(contract) {
|
|
960
|
+
const text = collectRouteText(contract);
|
|
961
|
+
if (text.includes("[redacted]") || hasSecurityBoundaryTerms(text))
|
|
962
|
+
return "privacy/security";
|
|
963
|
+
if (/\b(release|publish|publishing|package distribution|npm publish)\b/.test(text)) {
|
|
964
|
+
return "release/publish";
|
|
965
|
+
}
|
|
966
|
+
if (/\b(hook|hooks|learning|memory|telemetry|observability)\b/.test(text)) {
|
|
967
|
+
return "hook/learning/telemetry";
|
|
968
|
+
}
|
|
969
|
+
if (/\b(migration|migrate|hard to rollback|hard-to-rollback|irreversible|destructive)\b/.test(text)) {
|
|
970
|
+
return "hard-to-rollback";
|
|
971
|
+
}
|
|
972
|
+
if (hasHighRiskOperationTerms(text)) {
|
|
973
|
+
return "high-risk operation";
|
|
974
|
+
}
|
|
975
|
+
if ((contract.scope.requiresUserConfirmation ?? []).length > 0) {
|
|
976
|
+
return "explicit user confirmation";
|
|
977
|
+
}
|
|
978
|
+
for (const path of contract.scope.allowedPaths ?? []) {
|
|
979
|
+
const trigger = selectRigorousPathTrigger(path);
|
|
980
|
+
if (trigger !== null)
|
|
981
|
+
return trigger;
|
|
982
|
+
}
|
|
983
|
+
for (const operation of contract.scope.allowedOperations ?? []) {
|
|
984
|
+
if (hasHighRiskOperationTerms(operation))
|
|
985
|
+
return "high-risk operation";
|
|
986
|
+
}
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
function hasSecurityBoundaryTerms(text) {
|
|
990
|
+
return /\b(security|privacy|private data|auth|authentication|authorization|credential|credentials|secret|secrets|token|tokens|api key|api-key|apikey|password|passwd)\b/.test(text);
|
|
991
|
+
}
|
|
992
|
+
function selectRigorousPathTrigger(path) {
|
|
993
|
+
const normalized = path.trim().replace(/\\/g, "/").toLowerCase();
|
|
994
|
+
if (/^~\/\.(evodev|claude|codex)(\/|$)/.test(normalized)) {
|
|
995
|
+
return "user-level Code Agent/EvoDev path";
|
|
996
|
+
}
|
|
997
|
+
if (normalized === ".evodev" || normalized.startsWith(".evodev/") || normalized.includes("/.evodev/")) {
|
|
998
|
+
return "project .evodev path";
|
|
999
|
+
}
|
|
1000
|
+
if (normalized === ".claude" || normalized.startsWith(".claude/") || normalized.includes("/.claude/") || normalized === ".codex" || normalized.startsWith(".codex/") || normalized.includes("/.codex/")) {
|
|
1001
|
+
return "project Code Agent config path";
|
|
1002
|
+
}
|
|
1003
|
+
const fileName = basename(normalized);
|
|
1004
|
+
if (fileName === "claude.md" || fileName === "agents.md")
|
|
1005
|
+
return "agent instruction file";
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
function hasHighRiskOperationTerms(text) {
|
|
1009
|
+
const normalized = text.toLowerCase().replace(/[_-]+/g, " ");
|
|
1010
|
+
return /\b(delete|remove|publish|release|install|uninstall|network|external|external service|external api|write user config|user config|write project context|project context|hook|learning|memory|telemetry)\b/.test(normalized);
|
|
1011
|
+
}
|
|
1012
|
+
function collectVerificationFailures(contract, commands, acceptanceResults, antiCriteriaResults, evidence) {
|
|
1013
|
+
const failures = [];
|
|
1014
|
+
if (!isVerificationReady(contract)) {
|
|
1015
|
+
failures.push("task contract must be routed before verification");
|
|
1016
|
+
}
|
|
1017
|
+
failures.push(...collectDuplicateIdFailures("command", commands));
|
|
1018
|
+
failures.push(...collectDuplicateIdFailures("evidence", evidence));
|
|
1019
|
+
failures.push(...collectDuplicateIdFailures("acceptance result", acceptanceResults));
|
|
1020
|
+
failures.push(...collectDuplicateIdFailures("anti-criteria result", antiCriteriaResults));
|
|
1021
|
+
for (const requiredId of contract.route.requiredVerification ?? []) {
|
|
1022
|
+
const matches = [
|
|
1023
|
+
...commands.filter((command) => command.id === requiredId),
|
|
1024
|
+
...evidence.filter((item) => item.id === requiredId)
|
|
1025
|
+
];
|
|
1026
|
+
if (matches.length === 0) {
|
|
1027
|
+
failures.push(`missing required verification: ${requiredId}`);
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
if (matches.some((item) => !isSatisfyingVerificationItem(item))) {
|
|
1031
|
+
failures.push(`required verification ${requiredId} is not satisfied`);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
for (const criterion of contract.acceptanceCriteria) {
|
|
1035
|
+
const result = acceptanceResults.find((candidate) => candidate.id === criterion.id);
|
|
1036
|
+
if (result === undefined || result.status !== "pass" && result.status !== "not-applicable") {
|
|
1037
|
+
failures.push(`acceptance ${criterion.id} is not satisfied`);
|
|
1038
|
+
}
|
|
1039
|
+
for (const evidenceId of criterion.requiredEvidence) {
|
|
1040
|
+
const matches = evidence.filter((item) => item.id === evidenceId);
|
|
1041
|
+
if (matches.length === 0) {
|
|
1042
|
+
failures.push(`missing evidence for ${criterion.id}: ${evidenceId}`);
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
if (matches.some((item) => !isSatisfyingEvidenceItem(item))) {
|
|
1046
|
+
failures.push(`evidence for ${criterion.id} is not satisfied: ${evidenceId}`);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
for (const criterion of contract.antiCriteria) {
|
|
1051
|
+
const result = antiCriteriaResults.find((candidate) => candidate.id === criterion.id);
|
|
1052
|
+
if (result === undefined || result.status === "unknown" || result.status === "triggered") {
|
|
1053
|
+
failures.push(`anti-criteria ${criterion.id} is not clear`);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return failures;
|
|
1057
|
+
}
|
|
1058
|
+
function collectDuplicateIdFailures(label, items) {
|
|
1059
|
+
const seen = new Set;
|
|
1060
|
+
const duplicates = new Set;
|
|
1061
|
+
for (const item of items) {
|
|
1062
|
+
if (seen.has(item.id))
|
|
1063
|
+
duplicates.add(item.id);
|
|
1064
|
+
seen.add(item.id);
|
|
1065
|
+
}
|
|
1066
|
+
return Array.from(duplicates).map((id) => `duplicate ${label} id: ${id}`);
|
|
1067
|
+
}
|
|
1068
|
+
function isSatisfyingVerificationItem(item) {
|
|
1069
|
+
if ("rawOutputStored" in item && "type" in item)
|
|
1070
|
+
return isSatisfyingEvidenceItem(item);
|
|
1071
|
+
return item.status === "pass";
|
|
1072
|
+
}
|
|
1073
|
+
function isSatisfyingEvidenceItem(item) {
|
|
1074
|
+
if (item.type === "command-result")
|
|
1075
|
+
return item.status === "pass";
|
|
1076
|
+
return item.status === "pass" || item.status === "clear";
|
|
1077
|
+
}
|
|
1078
|
+
function isVerificationReady(contract) {
|
|
1079
|
+
return contract.status !== "draft" && contract.route.mode !== null && contract.route.workflowId !== null && (contract.route.requiredVerification ?? []).length > 0;
|
|
1080
|
+
}
|
|
1081
|
+
function parseTaskContract(value) {
|
|
1082
|
+
if (!isRecord2(value) || value.version !== 1 || typeof value.taskId !== "string") {
|
|
1083
|
+
throw new Error("Invalid Task Contract JSON.");
|
|
1084
|
+
}
|
|
1085
|
+
return value;
|
|
1086
|
+
}
|
|
1087
|
+
function assertMetadataOnly(value) {
|
|
1088
|
+
if (Array.isArray(value)) {
|
|
1089
|
+
for (const item of value)
|
|
1090
|
+
assertMetadataOnly(item);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
if (!isRecord2(value)) {
|
|
1094
|
+
if (typeof value === "string" && containsSensitiveText(value)) {
|
|
1095
|
+
throw new Error("Verification input contains sensitive content in metadata field.");
|
|
1096
|
+
}
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1100
|
+
const normalizedKey = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1101
|
+
if (FORBIDDEN_RAW_KEYS.has(key) || FORBIDDEN_RAW_KEYS.has(normalizedKey)) {
|
|
1102
|
+
throw new Error(`Verification input contains forbidden raw field: ${key}`);
|
|
1103
|
+
}
|
|
1104
|
+
if (!ALLOWED_VERIFICATION_KEYS.has(key)) {
|
|
1105
|
+
throw new Error(`Verification input contains unsupported field: ${key}`);
|
|
1106
|
+
}
|
|
1107
|
+
assertMetadataOnly(child);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function assertVerificationCommandStatus(value, label) {
|
|
1111
|
+
if (value === "pass" || value === "fail" || value === "not-run")
|
|
1112
|
+
return value;
|
|
1113
|
+
throw new Error(`Invalid verification command status for ${label}: ${String(value)}`);
|
|
1114
|
+
}
|
|
1115
|
+
function assertAcceptanceResultStatus(value, label) {
|
|
1116
|
+
if (value === "pass" || value === "fail" || value === "not-run" || value === "not-applicable") {
|
|
1117
|
+
return value;
|
|
1118
|
+
}
|
|
1119
|
+
throw new Error(`Invalid acceptance result status for ${label}: ${String(value)}`);
|
|
1120
|
+
}
|
|
1121
|
+
function assertAntiCriteriaResultStatus(value, label) {
|
|
1122
|
+
if (value === "clear" || value === "triggered" || value === "unknown")
|
|
1123
|
+
return value;
|
|
1124
|
+
throw new Error(`Invalid anti-criteria result status for ${label}: ${String(value)}`);
|
|
1125
|
+
}
|
|
1126
|
+
function assertVerificationStatus(value, label) {
|
|
1127
|
+
if (value === "pass" || value === "fail" || value === "not-run" || value === "not-applicable" || value === "unknown" || value === "triggered" || value === "clear") {
|
|
1128
|
+
return value;
|
|
1129
|
+
}
|
|
1130
|
+
throw new Error(`Invalid verification status for ${label}: ${String(value)}`);
|
|
1131
|
+
}
|
|
1132
|
+
function assertEvidenceType(value, label) {
|
|
1133
|
+
if (value === "command-result" || value === "manual-check" || value === "review")
|
|
1134
|
+
return value;
|
|
1135
|
+
throw new Error(`Invalid evidence type for ${label}: ${String(value)}`);
|
|
1136
|
+
}
|
|
1137
|
+
function sanitizeText(value) {
|
|
1138
|
+
return value.replace(SENSITIVE_TEXT_PATTERN, "[redacted]").slice(0, 500);
|
|
1139
|
+
}
|
|
1140
|
+
function containsSensitiveText(value) {
|
|
1141
|
+
SENSITIVE_TEXT_PATTERN.lastIndex = 0;
|
|
1142
|
+
return SENSITIVE_TEXT_PATTERN.test(value);
|
|
1143
|
+
}
|
|
1144
|
+
async function assertTaskContractWritePathAllowed(path) {
|
|
1145
|
+
assertTaskContractWritePathSegmentsAllowed(resolve(path));
|
|
1146
|
+
assertTaskContractWritePathSegmentsAllowed(await resolveTaskWriteRealPath(path));
|
|
1147
|
+
}
|
|
1148
|
+
function assertTaskContractWritePathSegmentsAllowed(path) {
|
|
1149
|
+
const segments = path.replace(/\\/g, "/").split("/").filter((segment) => segment.length > 0).map((segment) => segment.toLowerCase());
|
|
1150
|
+
const protectedSegment = segments.find((segment) => FORBIDDEN_TASK_WRITE_SEGMENTS.has(segment));
|
|
1151
|
+
if (protectedSegment !== undefined) {
|
|
1152
|
+
throw new Error(`Task contract write path is protected: ${protectedSegment}`);
|
|
1153
|
+
}
|
|
1154
|
+
if (!isTaskStoragePath(segments)) {
|
|
1155
|
+
const protectedProjectAssetSegment = segments.find((segment) => FORBIDDEN_PROJECT_ASSET_SEGMENTS.has(segment));
|
|
1156
|
+
if (protectedProjectAssetSegment !== undefined) {
|
|
1157
|
+
throw new Error(`Task contract write path is protected: ${protectedProjectAssetSegment}`);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
const protectedFile = segments.find((segment) => FORBIDDEN_TASK_WRITE_FILES.has(segment));
|
|
1161
|
+
if (protectedFile !== undefined) {
|
|
1162
|
+
throw new Error(`Task contract write path is protected: ${protectedFile}`);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
async function resolveTaskWriteRealPath(path) {
|
|
1166
|
+
let currentPath = resolve(path);
|
|
1167
|
+
const missingSegments = [];
|
|
1168
|
+
while (true) {
|
|
1169
|
+
try {
|
|
1170
|
+
await lstat(currentPath);
|
|
1171
|
+
return join2(await realpath(currentPath), ...missingSegments.reverse());
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
if (!isMissingPathError(error))
|
|
1174
|
+
throw error;
|
|
1175
|
+
const parentPath = dirname(currentPath);
|
|
1176
|
+
if (parentPath === currentPath)
|
|
1177
|
+
return resolve(path);
|
|
1178
|
+
missingSegments.push(basename(currentPath));
|
|
1179
|
+
currentPath = parentPath;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
function isTaskStoragePath(segments) {
|
|
1184
|
+
return segments.some((segment, index) => segment === ".evodev" && (segments[index + 1] === "tasks" || segments[index + 1] === "state" && segments[index + 2] === "tasks"));
|
|
1185
|
+
}
|
|
1186
|
+
function sanitizeTextList(values) {
|
|
1187
|
+
return values.map((value) => sanitizeText(value)).filter((value) => value.length > 0);
|
|
1188
|
+
}
|
|
1189
|
+
function sanitizeId(value) {
|
|
1190
|
+
const sanitized = sanitizeText(value).replace(/\[redacted\]/gi, "redacted").replace(/[^a-zA-Z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
1191
|
+
return sanitized || "item";
|
|
1192
|
+
}
|
|
1193
|
+
function uniqueSanitizedIds(values) {
|
|
1194
|
+
return Array.from(new Set(values.map((value) => sanitizeId(value))));
|
|
1195
|
+
}
|
|
1196
|
+
function formatList(values) {
|
|
1197
|
+
return values.length === 0 ? "none" : values.join(", ");
|
|
1198
|
+
}
|
|
1199
|
+
function createTaskId(title) {
|
|
1200
|
+
const slug = sanitizeId(title.toLowerCase()) || "task";
|
|
1201
|
+
return `task-${slug}`.slice(0, 80);
|
|
1202
|
+
}
|
|
1203
|
+
async function pathExists2(path) {
|
|
1204
|
+
try {
|
|
1205
|
+
await stat2(path);
|
|
1206
|
+
return true;
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1209
|
+
return false;
|
|
1210
|
+
}
|
|
1211
|
+
throw error;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
function isMissingPathError(error) {
|
|
1215
|
+
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
1216
|
+
}
|
|
1217
|
+
function isRecord2(value) {
|
|
1218
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// packages/core/src/hooks/index.ts
|
|
1222
|
+
var CANONICAL_HOOK_EVENT_TYPES = [
|
|
1223
|
+
"SessionStart",
|
|
1224
|
+
"UserPromptSubmit",
|
|
1225
|
+
"UserPromptExpansion",
|
|
1226
|
+
"PreToolUse",
|
|
1227
|
+
"PermissionRequest",
|
|
1228
|
+
"PostToolUse",
|
|
1229
|
+
"PostToolUseFailure",
|
|
1230
|
+
"PostToolBatch",
|
|
1231
|
+
"PermissionDenied",
|
|
1232
|
+
"SubagentStart",
|
|
1233
|
+
"Stop",
|
|
1234
|
+
"StopFailure",
|
|
1235
|
+
"TeammateIdle",
|
|
1236
|
+
"SubagentStop",
|
|
1237
|
+
"TaskCreated",
|
|
1238
|
+
"TaskCompleted",
|
|
1239
|
+
"PreCompact",
|
|
1240
|
+
"PostCompact",
|
|
1241
|
+
"SessionEnd",
|
|
1242
|
+
"ConfigChange",
|
|
1243
|
+
"CwdChanged",
|
|
1244
|
+
"FileChanged",
|
|
1245
|
+
"WorktreeCreate",
|
|
1246
|
+
"WorktreeRemove"
|
|
1247
|
+
];
|
|
1248
|
+
var DEFAULT_EVENT_SETTINGS = {
|
|
1249
|
+
SessionStart: false,
|
|
1250
|
+
UserPromptSubmit: false,
|
|
1251
|
+
UserPromptExpansion: false,
|
|
1252
|
+
PreToolUse: false,
|
|
1253
|
+
PermissionRequest: false,
|
|
1254
|
+
PostToolUse: false,
|
|
1255
|
+
PostToolUseFailure: false,
|
|
1256
|
+
PostToolBatch: false,
|
|
1257
|
+
PermissionDenied: false,
|
|
1258
|
+
SubagentStart: false,
|
|
1259
|
+
Stop: false,
|
|
1260
|
+
StopFailure: false,
|
|
1261
|
+
TeammateIdle: false,
|
|
1262
|
+
SubagentStop: false,
|
|
1263
|
+
TaskCreated: false,
|
|
1264
|
+
TaskCompleted: false,
|
|
1265
|
+
PreCompact: false,
|
|
1266
|
+
PostCompact: false,
|
|
1267
|
+
SessionEnd: false,
|
|
1268
|
+
ConfigChange: false,
|
|
1269
|
+
CwdChanged: false,
|
|
1270
|
+
FileChanged: false,
|
|
1271
|
+
WorktreeCreate: false,
|
|
1272
|
+
WorktreeRemove: false
|
|
1273
|
+
};
|
|
1274
|
+
var SENSITIVE_TEXT_PATTERN2 = /https?:\/\/\S+|\b(secret|token|password|passwd|private|internal|api[_-]?key|apikey|credential|credentials|\.env)\b/i;
|
|
1275
|
+
var SOURCE_LIKE_PATTERN = /\b(function|class|import|export|const|let|var)\b.*[{};]/s;
|
|
1276
|
+
function createDefaultHookSettings() {
|
|
1277
|
+
return {
|
|
1278
|
+
enabled: false,
|
|
1279
|
+
targets: {
|
|
1280
|
+
claude: {
|
|
1281
|
+
enabled: false,
|
|
1282
|
+
events: { ...DEFAULT_EVENT_SETTINGS }
|
|
1283
|
+
},
|
|
1284
|
+
codex: {
|
|
1285
|
+
enabled: false,
|
|
1286
|
+
events: { ...DEFAULT_EVENT_SETTINGS }
|
|
1287
|
+
}
|
|
1288
|
+
},
|
|
1289
|
+
observability: {
|
|
1290
|
+
metadataOnly: true,
|
|
1291
|
+
rawPayloadStorage: false,
|
|
1292
|
+
appendEvents: false
|
|
1293
|
+
},
|
|
1294
|
+
learning: {
|
|
1295
|
+
emitCandidates: false,
|
|
1296
|
+
writeMemory: false
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
function parseHookSettings(value) {
|
|
1301
|
+
const defaults = createDefaultHookSettings();
|
|
1302
|
+
if (value === undefined || value === null)
|
|
1303
|
+
return defaults;
|
|
1304
|
+
if (!isRecord3(value))
|
|
1305
|
+
throw new Error("Invalid hooks settings; expected object.");
|
|
1306
|
+
return {
|
|
1307
|
+
enabled: optionalBoolean(value.enabled, defaults.enabled, "hooks.enabled"),
|
|
1308
|
+
targets: {
|
|
1309
|
+
claude: parseHookTargetSettings(value.targets, defaults.targets.claude, "claude"),
|
|
1310
|
+
codex: parseHookTargetSettings(value.targets, defaults.targets.codex, "codex")
|
|
1311
|
+
},
|
|
1312
|
+
observability: {
|
|
1313
|
+
metadataOnly: true,
|
|
1314
|
+
rawPayloadStorage: false,
|
|
1315
|
+
appendEvents: false
|
|
1316
|
+
},
|
|
1317
|
+
learning: {
|
|
1318
|
+
emitCandidates: false,
|
|
1319
|
+
writeMemory: false
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
function normalizeHookEvent(input) {
|
|
1324
|
+
const warnings = [];
|
|
1325
|
+
const type = normalizeHookEventType(input.type, warnings);
|
|
1326
|
+
const redactions = [];
|
|
1327
|
+
const metadata = extractMetadata(type, input.payload, redactions);
|
|
1328
|
+
const commandClass = typeof metadata.commandClass === "string" ? metadata.commandClass : undefined;
|
|
1329
|
+
const decision = decideHookPolicy(commandClass);
|
|
1330
|
+
const sessionIdHash = hashOptionalIdentifier(input.payload.session_id ?? input.payload.sessionId);
|
|
1331
|
+
return {
|
|
1332
|
+
version: 1,
|
|
1333
|
+
eventId: stableEventId(input.pluginId, type, sessionIdHash),
|
|
1334
|
+
type,
|
|
1335
|
+
source: {
|
|
1336
|
+
pluginId: sanitizeScalar(input.pluginId, redactions),
|
|
1337
|
+
agent: sanitizeScalar(input.agent ?? "unknown", redactions),
|
|
1338
|
+
sessionIdHash,
|
|
1339
|
+
rawPayloadStored: false
|
|
1340
|
+
},
|
|
1341
|
+
time: {
|
|
1342
|
+
occurredAt: optionalSanitizedString(input.payload.timestamp ?? input.payload.occurredAt, redactions),
|
|
1343
|
+
receivedAt: input.receivedAt ?? "dry-run"
|
|
1344
|
+
},
|
|
1345
|
+
scope: {
|
|
1346
|
+
taskId: optionalSanitizedString(input.payload.taskId, redactions),
|
|
1347
|
+
projectId: optionalSanitizedString(input.payload.projectId, redactions),
|
|
1348
|
+
cwdPolicy: "metadata-only",
|
|
1349
|
+
projectContextOptedIn: false
|
|
1350
|
+
},
|
|
1351
|
+
payload: {
|
|
1352
|
+
summary: summarizeEvent(type, metadata),
|
|
1353
|
+
metadata,
|
|
1354
|
+
redactions,
|
|
1355
|
+
redactionCount: redactions.length,
|
|
1356
|
+
rawContentIncluded: false
|
|
1357
|
+
},
|
|
1358
|
+
policy: {
|
|
1359
|
+
classification: "local-private",
|
|
1360
|
+
allowedUses: ["observability", "safety-check", "workflow-suggestion"],
|
|
1361
|
+
learningAllowed: false,
|
|
1362
|
+
externalUploadAllowed: false
|
|
1363
|
+
},
|
|
1364
|
+
decision,
|
|
1365
|
+
warnings
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
function classifyCommandRisk(command) {
|
|
1369
|
+
if (command === undefined || command.trim() === "")
|
|
1370
|
+
return "unknown";
|
|
1371
|
+
const lower = command.toLowerCase();
|
|
1372
|
+
if (/(^|\s)(\.\.\/|\/\.\.)/.test(lower))
|
|
1373
|
+
return "path-escaping";
|
|
1374
|
+
if (/\b(npm|pnpm|yarn|bun)\s+publish\b|\bgh\s+release\b/.test(lower))
|
|
1375
|
+
return "publish";
|
|
1376
|
+
if (/\b(curl|wget|scp|rsync|ssh|ftp)\b|https?:\/\//.test(lower))
|
|
1377
|
+
return "network";
|
|
1378
|
+
if (/\b(rm|rmdir|unlink)\b|--delete\b/.test(lower))
|
|
1379
|
+
return "delete";
|
|
1380
|
+
if (/\b(secret|token|password|passwd|credential|api[_-]?key)\b|(^|\s)\.env(\s|$)/.test(lower)) {
|
|
1381
|
+
return "credential-sensitive";
|
|
1382
|
+
}
|
|
1383
|
+
if (/\b(vim|nano|tee|touch|mkdir|mv|cp|chmod|chown)\b|>/.test(lower))
|
|
1384
|
+
return "write";
|
|
1385
|
+
if (/\b(test|lint|typecheck|check)\b/.test(lower))
|
|
1386
|
+
return "test-command";
|
|
1387
|
+
if (/\b(ls|pwd|grep|rg|find|git status|git diff|cat)\b/.test(lower))
|
|
1388
|
+
return "read-only";
|
|
1389
|
+
return "unknown";
|
|
1390
|
+
}
|
|
1391
|
+
function formatHookInstallDryRun(plan) {
|
|
1392
|
+
return [
|
|
1393
|
+
"EvoDev hook install dry-run",
|
|
1394
|
+
"",
|
|
1395
|
+
`Target: ${plan.target}`,
|
|
1396
|
+
"Mode: dry-run (no writes)",
|
|
1397
|
+
`Hooks enabled by default: ${plan.settings.enabled}`,
|
|
1398
|
+
"Selected events:",
|
|
1399
|
+
...CANONICAL_HOOK_EVENT_TYPES.map((eventType) => ` - ${eventType}: ${plan.settings.targets[plan.target].events[eventType]}`),
|
|
1400
|
+
"Boundaries:",
|
|
1401
|
+
" - observability: metadata-only, appendEvents=false, rawPayloadStorage=false",
|
|
1402
|
+
" - learning: emitCandidates=false, writeMemory=false",
|
|
1403
|
+
" - external upload: false",
|
|
1404
|
+
" - protected project writes: .claude/.codex/CLAUDE.md/AGENTS.md not targeted",
|
|
1405
|
+
" - backup/merge: required before any future real install; dry-run writes nothing",
|
|
1406
|
+
"Planned writes:",
|
|
1407
|
+
...plan.plannedWrites.map((write) => ` - ${write.action}: ${write.targetPath} (${write.reason})`),
|
|
1408
|
+
"Warnings:",
|
|
1409
|
+
...plan.warnings.length === 0 ? [" - none"] : plan.warnings.map((warning) => ` - ${warning}`),
|
|
1410
|
+
"Blockers:",
|
|
1411
|
+
...plan.blockers.length === 0 ? [" - none"] : plan.blockers.map((blocker) => ` - ${blocker}`)
|
|
1412
|
+
].join(`
|
|
1413
|
+
`);
|
|
1414
|
+
}
|
|
1415
|
+
function formatHookEventDryRun(event) {
|
|
1416
|
+
return [
|
|
1417
|
+
"EvoDev hook event dry-run",
|
|
1418
|
+
"",
|
|
1419
|
+
`Type: ${event.type}`,
|
|
1420
|
+
`Plugin: ${event.source.pluginId}`,
|
|
1421
|
+
`Summary: ${event.payload.summary}`,
|
|
1422
|
+
`Decision: ${event.decision.action} (${event.decision.reason})`,
|
|
1423
|
+
"Metadata:",
|
|
1424
|
+
...Object.entries(event.payload.metadata).map(([key, value]) => ` - ${key}: ${formatMetadataValue(value)}`),
|
|
1425
|
+
"Redactions:",
|
|
1426
|
+
...event.payload.redactions.length === 0 ? [" - none"] : event.payload.redactions.map((redaction) => ` - ${redaction}`),
|
|
1427
|
+
"Warnings:",
|
|
1428
|
+
...event.warnings.length === 0 ? [" - none"] : event.warnings.map((warning) => ` - ${warning}`)
|
|
1429
|
+
].join(`
|
|
1430
|
+
`);
|
|
1431
|
+
}
|
|
1432
|
+
function resolveHookRuntimeSessionPaths(input) {
|
|
1433
|
+
const sessionDir = join3(input.homeDir, ".evodev", "STATE", "hooks", "sessions", input.sessionKey);
|
|
1434
|
+
return {
|
|
1435
|
+
sessionDir,
|
|
1436
|
+
bindingPath: join3(sessionDir, "binding.json"),
|
|
1437
|
+
contractPath: join3(sessionDir, "contract.json")
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
async function handleHookRuntime(input) {
|
|
1441
|
+
const enabled = isHookEventEnabled(input.settings, input.target, input.event.type);
|
|
1442
|
+
if (!enabled) {
|
|
1443
|
+
return {
|
|
1444
|
+
target: input.target,
|
|
1445
|
+
event: input.event.type,
|
|
1446
|
+
enabled,
|
|
1447
|
+
output: null,
|
|
1448
|
+
stateWrites: [],
|
|
1449
|
+
summary: `Hook ${input.event.type} ignored because EvoDev hooks are disabled.`
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
if (input.event.type === "SessionStart")
|
|
1453
|
+
return handleSessionStart(input);
|
|
1454
|
+
if (input.event.type === "UserPromptSubmit")
|
|
1455
|
+
return handleUserPromptSubmit(input);
|
|
1456
|
+
if (input.event.type === "PreToolUse")
|
|
1457
|
+
return handlePreToolUse(input);
|
|
1458
|
+
if (input.event.type === "PostToolUse" || input.event.type === "PostToolUseFailure") {
|
|
1459
|
+
return handlePostToolUse(input);
|
|
1460
|
+
}
|
|
1461
|
+
if (input.event.type === "PostToolBatch")
|
|
1462
|
+
return handleAdditionalContext(input, "PostToolBatch");
|
|
1463
|
+
if (input.event.type === "SubagentStart" || input.event.type === "TaskCreated" || input.event.type === "PermissionRequest") {
|
|
1464
|
+
return handlePreToolUse(input);
|
|
1465
|
+
}
|
|
1466
|
+
if (input.event.type === "Stop" || input.event.type === "SubagentStop" || input.event.type === "TaskCompleted" || input.event.type === "TeammateIdle") {
|
|
1467
|
+
return handleCompletionGate(input);
|
|
1468
|
+
}
|
|
1469
|
+
if (input.event.type === "PreCompact")
|
|
1470
|
+
return handlePreCompact(input);
|
|
1471
|
+
if (input.event.type === "SessionEnd")
|
|
1472
|
+
return handleSessionEnd(input);
|
|
1473
|
+
return handleAdditionalContext(input, input.event.type);
|
|
1474
|
+
}
|
|
1475
|
+
function formatHookRuntimeOutput(result) {
|
|
1476
|
+
return result.output === null ? "" : `${JSON.stringify(result.output)}
|
|
1477
|
+
`;
|
|
1478
|
+
}
|
|
1479
|
+
function isHookEventEnabled(settings, target, eventType) {
|
|
1480
|
+
return settings.enabled === true && settings.targets[target]?.enabled === true && settings.targets[target]?.events[eventType] === true;
|
|
1481
|
+
}
|
|
1482
|
+
async function handleSessionStart(input) {
|
|
1483
|
+
const binding = await readSessionBinding(input.homeDir, input.rawPayload);
|
|
1484
|
+
const context = [
|
|
1485
|
+
"EvoDev session initialized.",
|
|
1486
|
+
binding?.contractPath ? `Active Task Contract: ${binding.contractPath}` : "No active Task Contract yet.",
|
|
1487
|
+
"User prompts will be routed through EvoDev before execution."
|
|
1488
|
+
].join(" ");
|
|
1489
|
+
return createRuntimeResult(input, hookOutput(input.event.type, { additionalContext: context }), {
|
|
1490
|
+
summary: "Session context prepared."
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
async function handleUserPromptSubmit(input) {
|
|
1494
|
+
const classification = classifyUserPrompt(input.rawPayload.prompt ?? input.rawPayload.userPrompt);
|
|
1495
|
+
const sessionKey = resolveHookSessionKey(input.rawPayload);
|
|
1496
|
+
const paths = resolveHookRuntimeSessionPaths({ homeDir: input.homeDir, sessionKey });
|
|
1497
|
+
const contract = routeTaskContract(createHookTaskContract(input, classification));
|
|
1498
|
+
const binding = {
|
|
1499
|
+
version: 1,
|
|
1500
|
+
target: input.target,
|
|
1501
|
+
sessionKey,
|
|
1502
|
+
taskId: contract.taskId,
|
|
1503
|
+
contractPath: paths.contractPath,
|
|
1504
|
+
cwd: optionalPayloadString(input.rawPayload.cwd),
|
|
1505
|
+
route: contract.route,
|
|
1506
|
+
updatedAt: input.receivedAt ?? new Date().toISOString()
|
|
1507
|
+
};
|
|
1508
|
+
await writeTaskContract(paths.contractPath, contract, { overwrite: true });
|
|
1509
|
+
await writeJsonFile(paths.bindingPath, binding);
|
|
1510
|
+
const context = [
|
|
1511
|
+
`EvoDev routed this request before ${formatHookTargetName(input.target)} execution.`,
|
|
1512
|
+
`Task Contract: ${paths.contractPath}`,
|
|
1513
|
+
`Mode: ${contract.route.mode ?? "unknown"}`,
|
|
1514
|
+
`Workflow: ${contract.route.workflowId ?? "none"}`,
|
|
1515
|
+
`Reason: ${contract.route.rationale}`,
|
|
1516
|
+
"Follow the contract scope, anti-criteria, and verification plan before finishing."
|
|
1517
|
+
].join(" ");
|
|
1518
|
+
return createRuntimeResult(input, hookOutput(input.event.type, { additionalContext: context }), {
|
|
1519
|
+
summary: "User prompt routed through Task Contract.",
|
|
1520
|
+
stateWrites: [paths.contractPath, paths.bindingPath]
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
async function handlePreToolUse(input) {
|
|
1524
|
+
const contract = await readActiveContract(input.homeDir, input.rawPayload);
|
|
1525
|
+
const commandClass = typeof input.event.payload.metadata.commandClass === "string" ? input.event.payload.metadata.commandClass : undefined;
|
|
1526
|
+
const protectedPath = findProtectedPath(input.rawPayload);
|
|
1527
|
+
if (contract === null) {
|
|
1528
|
+
return createRuntimeResult(input, hookOutput(input.event.type, {
|
|
1529
|
+
permissionDecision: "ask",
|
|
1530
|
+
permissionDecisionReason: "EvoDev requires an active Task Contract before tool use.",
|
|
1531
|
+
additionalContext: "Submit the user prompt through UserPromptSubmit to create the Task Contract first."
|
|
1532
|
+
}), { summary: "Tool use requires user confirmation because no active contract exists." });
|
|
1533
|
+
}
|
|
1534
|
+
if (protectedPath !== null) {
|
|
1535
|
+
return createRuntimeResult(input, hookOutput(input.event.type, {
|
|
1536
|
+
permissionDecision: "deny",
|
|
1537
|
+
permissionDecisionReason: `EvoDev blocked access to protected project asset: ${protectedPath}`
|
|
1538
|
+
}), { summary: "Protected path access denied." });
|
|
1539
|
+
}
|
|
1540
|
+
if (commandClass === "delete" || commandClass === "publish" || commandClass === "credential-sensitive" || commandClass === "path-escaping") {
|
|
1541
|
+
return createRuntimeResult(input, hookOutput(input.event.type, {
|
|
1542
|
+
permissionDecision: "deny",
|
|
1543
|
+
permissionDecisionReason: `EvoDev blocked high-risk tool action: ${commandClass}`
|
|
1544
|
+
}), { summary: "High-risk tool action denied." });
|
|
1545
|
+
}
|
|
1546
|
+
if (commandClass === "network" || commandClass === "unknown" || commandClass === "write") {
|
|
1547
|
+
return createRuntimeResult(input, hookOutput(input.event.type, {
|
|
1548
|
+
permissionDecision: "ask",
|
|
1549
|
+
permissionDecisionReason: `EvoDev requires confirmation for ${commandClass} action under ${contract.route.mode ?? "unrouted"} mode.`,
|
|
1550
|
+
additionalContext: `Active Task Contract: ${contract.taskId}. Confirm scope before continuing.`
|
|
1551
|
+
}), { summary: "Tool use requires user confirmation." });
|
|
1552
|
+
}
|
|
1553
|
+
return createRuntimeResult(input, hookOutput(input.event.type, {
|
|
1554
|
+
permissionDecision: "allow",
|
|
1555
|
+
permissionDecisionReason: `EvoDev allowed ${commandClass ?? "metadata-only"} action under Task Contract ${contract.taskId}.`
|
|
1556
|
+
}), { summary: "Tool use allowed by EvoDev policy." });
|
|
1557
|
+
}
|
|
1558
|
+
async function handlePostToolUse(input) {
|
|
1559
|
+
const contract = await readActiveContract(input.homeDir, input.rawPayload);
|
|
1560
|
+
const binding = await readSessionBinding(input.homeDir, input.rawPayload);
|
|
1561
|
+
if (contract === null || binding?.contractPath === null || binding?.contractPath === undefined) {
|
|
1562
|
+
return handleAdditionalContext(input, input.event.type);
|
|
1563
|
+
}
|
|
1564
|
+
const status = optionalPayloadNumber(input.rawPayload.exit_code ?? input.rawPayload.exitCode);
|
|
1565
|
+
const nextContract = {
|
|
1566
|
+
...contract,
|
|
1567
|
+
evidence: {
|
|
1568
|
+
metadataOnly: true,
|
|
1569
|
+
items: [
|
|
1570
|
+
...contract.evidence.items,
|
|
1571
|
+
{
|
|
1572
|
+
type: "command-result",
|
|
1573
|
+
id: input.event.eventId,
|
|
1574
|
+
status: status === 0 ? "pass" : status === null ? "unknown" : "fail",
|
|
1575
|
+
summary: input.event.payload.summary,
|
|
1576
|
+
rawOutputStored: false,
|
|
1577
|
+
sourceContentStored: false
|
|
1578
|
+
}
|
|
1579
|
+
]
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
await writeTaskContract(binding.contractPath, nextContract, { overwrite: true });
|
|
1583
|
+
return createRuntimeResult(input, hookOutput(input.event.type, {
|
|
1584
|
+
additionalContext: `EvoDev recorded metadata-only evidence for Task Contract ${contract.taskId}. Raw output was not stored.`
|
|
1585
|
+
}), {
|
|
1586
|
+
summary: "Post-tool metadata evidence recorded.",
|
|
1587
|
+
stateWrites: [binding.contractPath]
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
async function handleCompletionGate(input) {
|
|
1591
|
+
const contract = await readActiveContract(input.homeDir, input.rawPayload);
|
|
1592
|
+
if (contract === null)
|
|
1593
|
+
return handleAdditionalContext(input, input.event.type);
|
|
1594
|
+
const stopHookActive = input.rawPayload.stop_hook_active === true;
|
|
1595
|
+
if (!stopHookActive && contract.route.mode === "rigorous" && contract.evidence.items.length === 0) {
|
|
1596
|
+
return createRuntimeResult(input, {
|
|
1597
|
+
decision: "block",
|
|
1598
|
+
reason: "EvoDev rigorous mode requires metadata-only evidence before stopping.",
|
|
1599
|
+
...hookOutput(input.event.type, {
|
|
1600
|
+
additionalContext: "Run the required verification or record metadata-only evidence before finishing."
|
|
1601
|
+
})
|
|
1602
|
+
}, { summary: "Completion blocked until evidence exists." });
|
|
1603
|
+
}
|
|
1604
|
+
return createRuntimeResult(input, hookOutput(input.event.type, {
|
|
1605
|
+
additionalContext: `EvoDev completion gate checked Task Contract ${contract.taskId}. Evidence items: ${contract.evidence.items.length}.`
|
|
1606
|
+
}), { summary: "Completion gate checked." });
|
|
1607
|
+
}
|
|
1608
|
+
async function handlePreCompact(input) {
|
|
1609
|
+
const binding = await readSessionBinding(input.homeDir, input.rawPayload);
|
|
1610
|
+
return createRuntimeResult(input, hookOutput(input.event.type, {
|
|
1611
|
+
additionalContext: binding?.contractPath ? `Preserve EvoDev Task Contract reference across compaction: ${binding.contractPath}` : "No EvoDev Task Contract is active for this session."
|
|
1612
|
+
}), { summary: "PreCompact context prepared." });
|
|
1613
|
+
}
|
|
1614
|
+
async function handleSessionEnd(input) {
|
|
1615
|
+
const binding = await readSessionBinding(input.homeDir, input.rawPayload);
|
|
1616
|
+
if (binding === null)
|
|
1617
|
+
return createRuntimeResult(input, null, { summary: "No session state to close." });
|
|
1618
|
+
const nextBinding = {
|
|
1619
|
+
...binding,
|
|
1620
|
+
updatedAt: input.receivedAt ?? new Date().toISOString()
|
|
1621
|
+
};
|
|
1622
|
+
const paths = resolveHookRuntimeSessionPaths({
|
|
1623
|
+
homeDir: input.homeDir,
|
|
1624
|
+
sessionKey: binding.sessionKey
|
|
1625
|
+
});
|
|
1626
|
+
await writeJsonFile(paths.bindingPath, nextBinding);
|
|
1627
|
+
return createRuntimeResult(input, null, {
|
|
1628
|
+
summary: "Session state updated for SessionEnd.",
|
|
1629
|
+
stateWrites: [paths.bindingPath]
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
function handleAdditionalContext(input, eventName) {
|
|
1633
|
+
return Promise.resolve(createRuntimeResult(input, hookOutput(eventName, {
|
|
1634
|
+
additionalContext: `EvoDev processed ${eventName} as metadata-only hook context.`
|
|
1635
|
+
}), { summary: `${eventName} processed as metadata-only context.` }));
|
|
1636
|
+
}
|
|
1637
|
+
function createRuntimeResult(input, output, options) {
|
|
1638
|
+
return {
|
|
1639
|
+
target: input.target,
|
|
1640
|
+
event: input.event.type,
|
|
1641
|
+
enabled: true,
|
|
1642
|
+
output,
|
|
1643
|
+
stateWrites: options.stateWrites ?? [],
|
|
1644
|
+
summary: options.summary
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
function hookOutput(eventName, output) {
|
|
1648
|
+
return {
|
|
1649
|
+
hookSpecificOutput: {
|
|
1650
|
+
hookEventName: eventName,
|
|
1651
|
+
...output
|
|
1652
|
+
}
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
function createHookTaskContract(input, classification) {
|
|
1656
|
+
const sessionKey = resolveHookSessionKey(input.rawPayload);
|
|
1657
|
+
const targetName = formatHookTargetName(input.target);
|
|
1658
|
+
const contract = createTaskContract({
|
|
1659
|
+
title: `${targetName} hook task ${sessionKey}`,
|
|
1660
|
+
summary: `${targetName} prompt classified as ${classification.kind}; raw prompt not stored.`,
|
|
1661
|
+
projectId: null
|
|
1662
|
+
});
|
|
1663
|
+
return {
|
|
1664
|
+
...contract,
|
|
1665
|
+
currentState: {
|
|
1666
|
+
summary: `UserPromptSubmit received through ${targetName} hooks; raw prompt omitted.`,
|
|
1667
|
+
evidenceRefs: []
|
|
1668
|
+
},
|
|
1669
|
+
targetState: {
|
|
1670
|
+
summary: `Complete the ${classification.kind} task through EvoDev-controlled workflow.`,
|
|
1671
|
+
nonGoals: ["Do not store raw prompts, transcripts, source content, secrets, or raw output."],
|
|
1672
|
+
constraints: [
|
|
1673
|
+
`prompt-kind:${classification.kind}`,
|
|
1674
|
+
...classification.riskTerms.map((term) => `risk:${term}`)
|
|
1675
|
+
]
|
|
1676
|
+
},
|
|
1677
|
+
scope: {
|
|
1678
|
+
...contract.scope,
|
|
1679
|
+
requiresUserConfirmation: classification.riskTerms
|
|
1680
|
+
},
|
|
1681
|
+
context: {
|
|
1682
|
+
...contract.context,
|
|
1683
|
+
assumptions: [
|
|
1684
|
+
`hook-session:${sessionKey}`,
|
|
1685
|
+
`cwd:${optionalPayloadString(input.rawPayload.cwd) ?? "unknown"}`
|
|
1686
|
+
],
|
|
1687
|
+
openQuestions: classification.needsClarification ? ["User request may need clarification before broad changes."] : []
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
function formatHookTargetName(target) {
|
|
1692
|
+
return target === "codex" ? "Codex" : "Claude";
|
|
1693
|
+
}
|
|
1694
|
+
function classifyUserPrompt(value) {
|
|
1695
|
+
const text = typeof value === "string" ? value.toLowerCase() : "";
|
|
1696
|
+
const riskTerms = [
|
|
1697
|
+
"security",
|
|
1698
|
+
"release",
|
|
1699
|
+
"publish",
|
|
1700
|
+
"hook",
|
|
1701
|
+
"memory",
|
|
1702
|
+
"learning",
|
|
1703
|
+
"secret",
|
|
1704
|
+
"privacy"
|
|
1705
|
+
].filter((term) => text.includes(term));
|
|
1706
|
+
let kind = "feature";
|
|
1707
|
+
if (/\bbug|fix|error|failed|failure\b/.test(text))
|
|
1708
|
+
kind = "bugfix";
|
|
1709
|
+
if (/\brefactor|migration|migrate\b/.test(text))
|
|
1710
|
+
kind = "refactor";
|
|
1711
|
+
if (/\breview|audit\b/.test(text))
|
|
1712
|
+
kind = "review";
|
|
1713
|
+
if (/\btest|coverage\b/.test(text))
|
|
1714
|
+
kind = "test";
|
|
1715
|
+
if (/\bdoc|readme|guide\b/.test(text))
|
|
1716
|
+
kind = "docs";
|
|
1717
|
+
if (riskTerms.includes("security") || riskTerms.includes("privacy"))
|
|
1718
|
+
kind = "security";
|
|
1719
|
+
if (riskTerms.includes("release") || riskTerms.includes("publish"))
|
|
1720
|
+
kind = "release";
|
|
1721
|
+
return {
|
|
1722
|
+
kind,
|
|
1723
|
+
riskTerms,
|
|
1724
|
+
needsClarification: text.trim().length < 12 || /\bmaybe|unclear|not sure\b/.test(text)
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
async function readActiveContract(homeDir, payload) {
|
|
1728
|
+
const binding = await readSessionBinding(homeDir, payload);
|
|
1729
|
+
if (binding?.contractPath === null || binding?.contractPath === undefined)
|
|
1730
|
+
return null;
|
|
1731
|
+
try {
|
|
1732
|
+
return JSON.parse(await readFile4(binding.contractPath, "utf8"));
|
|
1733
|
+
} catch (error) {
|
|
1734
|
+
if (isNotFoundError(error))
|
|
1735
|
+
return null;
|
|
1736
|
+
throw error;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
async function readSessionBinding(homeDir, payload) {
|
|
1740
|
+
const sessionKey = resolveHookSessionKey(payload);
|
|
1741
|
+
const paths = resolveHookRuntimeSessionPaths({ homeDir, sessionKey });
|
|
1742
|
+
try {
|
|
1743
|
+
return JSON.parse(await readFile4(paths.bindingPath, "utf8"));
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
if (isNotFoundError(error))
|
|
1746
|
+
return null;
|
|
1747
|
+
throw error;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
function resolveHookSessionKey(payload) {
|
|
1751
|
+
const sessionId = optionalPayloadString(payload.session_id ?? payload.sessionId);
|
|
1752
|
+
const cwd = optionalPayloadString(payload.cwd);
|
|
1753
|
+
const source = sessionId ?? cwd ?? "local";
|
|
1754
|
+
return `session-${createHash("sha256").update(source).digest("hex").slice(0, 16)}`;
|
|
1755
|
+
}
|
|
1756
|
+
async function writeJsonFile(path, value) {
|
|
1757
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
1758
|
+
await writeFile2(path, `${JSON.stringify(value, null, 2)}
|
|
1759
|
+
`, "utf8");
|
|
1760
|
+
}
|
|
1761
|
+
function findProtectedPath(payload) {
|
|
1762
|
+
const candidates = collectStringValues(payload).filter((value) => value.length < 500);
|
|
1763
|
+
for (const candidate of candidates) {
|
|
1764
|
+
if (candidate === "CLAUDE.md" || candidate === "AGENTS.md" || candidate.includes("/CLAUDE.md") || candidate.includes("/AGENTS.md") || candidate.includes(".claude/") || candidate.includes(".codex/")) {
|
|
1765
|
+
return candidate;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
return null;
|
|
1769
|
+
}
|
|
1770
|
+
function collectStringValues(value) {
|
|
1771
|
+
if (typeof value === "string")
|
|
1772
|
+
return [value];
|
|
1773
|
+
if (Array.isArray(value))
|
|
1774
|
+
return value.flatMap((item) => collectStringValues(item));
|
|
1775
|
+
if (isRecord3(value))
|
|
1776
|
+
return Object.values(value).flatMap((item) => collectStringValues(item));
|
|
1777
|
+
return [];
|
|
1778
|
+
}
|
|
1779
|
+
function optionalPayloadString(value) {
|
|
1780
|
+
return typeof value === "string" && value.length > 0 ? value.slice(0, 300) : null;
|
|
1781
|
+
}
|
|
1782
|
+
function optionalPayloadNumber(value) {
|
|
1783
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1784
|
+
}
|
|
1785
|
+
function isNotFoundError(error) {
|
|
1786
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
1787
|
+
}
|
|
1788
|
+
function normalizeHookEventType(type, warnings) {
|
|
1789
|
+
if (type === "AgentStop") {
|
|
1790
|
+
warnings.push("Legacy AgentStop event normalized to SubagentStop.");
|
|
1791
|
+
return "SubagentStop";
|
|
1792
|
+
}
|
|
1793
|
+
if (CANONICAL_HOOK_EVENT_TYPES.includes(type))
|
|
1794
|
+
return type;
|
|
1795
|
+
throw new Error(`Unsupported hook event type: ${type}`);
|
|
1796
|
+
}
|
|
1797
|
+
function parseHookTargetSettings(value, defaults, target) {
|
|
1798
|
+
const targets = isRecord3(value) ? value : {};
|
|
1799
|
+
const targetSettings = isRecord3(targets[target]) ? targets[target] : {};
|
|
1800
|
+
const events = isRecord3(targetSettings.events) ? targetSettings.events : {};
|
|
1801
|
+
const parsedEvents = { ...defaults.events };
|
|
1802
|
+
for (const eventType of CANONICAL_HOOK_EVENT_TYPES) {
|
|
1803
|
+
parsedEvents[eventType] = optionalBoolean(events[eventType], defaults.events[eventType], `hooks.targets.${target}.events.${eventType}`);
|
|
1804
|
+
}
|
|
1805
|
+
return {
|
|
1806
|
+
enabled: optionalBoolean(targetSettings.enabled, defaults.enabled, `hooks.targets.${target}.enabled`),
|
|
1807
|
+
events: parsedEvents
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
function extractMetadata(type, payload, redactions) {
|
|
1811
|
+
const metadata = {
|
|
1812
|
+
rawContentIncluded: false
|
|
1813
|
+
};
|
|
1814
|
+
const toolInput = isRecord3(payload.tool_input) ? payload.tool_input : isRecord3(payload.toolInput) ? payload.toolInput : {};
|
|
1815
|
+
const toolName = optionalSanitizedString(payload.tool_name ?? payload.toolName, redactions);
|
|
1816
|
+
if (toolName !== null)
|
|
1817
|
+
metadata.toolName = toolName;
|
|
1818
|
+
const rawCommand = typeof payload.command === "string" ? payload.command : typeof toolInput.command === "string" ? toolInput.command : undefined;
|
|
1819
|
+
const command = optionalSanitizedString(rawCommand, redactions, { classifyOnly: true });
|
|
1820
|
+
if (rawCommand !== undefined)
|
|
1821
|
+
redactions.push("raw-command");
|
|
1822
|
+
if (type === "PreToolUse" || type === "PostToolUse" || type === "PostToolUseFailure" || type === "PermissionRequest" || type === "PermissionDenied") {
|
|
1823
|
+
const commandClass = classifyCommandRisk(command ?? undefined);
|
|
1824
|
+
metadata.commandClass = commandClass;
|
|
1825
|
+
}
|
|
1826
|
+
const exitCode = optionalNumber(payload.exit_code ?? payload.exitCode);
|
|
1827
|
+
if (exitCode !== null)
|
|
1828
|
+
metadata.exitCode = exitCode;
|
|
1829
|
+
const status = optionalSanitizedString(payload.status, redactions);
|
|
1830
|
+
if (status !== null)
|
|
1831
|
+
metadata.status = status;
|
|
1832
|
+
const redactedPrompt = payload.prompt ?? payload.userPrompt;
|
|
1833
|
+
if (redactedPrompt !== undefined)
|
|
1834
|
+
redactions.push("raw-prompt");
|
|
1835
|
+
const rawOutput = payload.stdout ?? payload.stderr ?? payload.output;
|
|
1836
|
+
if (rawOutput !== undefined)
|
|
1837
|
+
redactions.push("raw-command-output");
|
|
1838
|
+
return metadata;
|
|
1839
|
+
}
|
|
1840
|
+
function decideHookPolicy(commandClass) {
|
|
1841
|
+
if (commandClass === "delete" || commandClass === "network" || commandClass === "publish" || commandClass === "credential-sensitive" || commandClass === "path-escaping" || commandClass === "unknown") {
|
|
1842
|
+
return {
|
|
1843
|
+
action: "ask-user",
|
|
1844
|
+
reason: `Command risk requires explicit confirmation: ${commandClass}`,
|
|
1845
|
+
requiresUserConfirmation: true
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
if (commandClass === "write") {
|
|
1849
|
+
return {
|
|
1850
|
+
action: "warn",
|
|
1851
|
+
reason: "Write-like command requires scope review.",
|
|
1852
|
+
requiresUserConfirmation: true
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
return {
|
|
1856
|
+
action: "allow",
|
|
1857
|
+
reason: "No policy violation detected.",
|
|
1858
|
+
requiresUserConfirmation: false
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
function summarizeEvent(type, metadata) {
|
|
1862
|
+
if (type === "PreToolUse")
|
|
1863
|
+
return `Tool use requested (${metadata.commandClass ?? "unknown"}).`;
|
|
1864
|
+
if (type === "PostToolUse")
|
|
1865
|
+
return "Tool use completed with metadata-only result.";
|
|
1866
|
+
if (type === "UserPromptSubmit")
|
|
1867
|
+
return "User prompt submitted; raw prompt redacted.";
|
|
1868
|
+
if (type === "SubagentStop")
|
|
1869
|
+
return "Subagent stopped; transcript omitted.";
|
|
1870
|
+
return `${type} received; metadata-only dry-run.`;
|
|
1871
|
+
}
|
|
1872
|
+
function optionalSanitizedString(value, redactions, options = {}) {
|
|
1873
|
+
if (typeof value !== "string" || value.length === 0)
|
|
1874
|
+
return null;
|
|
1875
|
+
if (SENSITIVE_TEXT_PATTERN2.test(value) || SOURCE_LIKE_PATTERN.test(value)) {
|
|
1876
|
+
redactions.push(options.classifyOnly ? "sensitive-command" : "sensitive-text");
|
|
1877
|
+
return options.classifyOnly ? value : "[redacted]";
|
|
1878
|
+
}
|
|
1879
|
+
return value.slice(0, 120);
|
|
1880
|
+
}
|
|
1881
|
+
function sanitizeScalar(value, redactions) {
|
|
1882
|
+
return optionalSanitizedString(value, redactions) ?? "unknown";
|
|
1883
|
+
}
|
|
1884
|
+
function stableEventId(pluginId, type, sessionIdHash) {
|
|
1885
|
+
const stableSessionPart = sessionIdHash ?? "local";
|
|
1886
|
+
return `hook-${pluginId}-${type}-${stableSessionPart}`.replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 100);
|
|
1887
|
+
}
|
|
1888
|
+
function hashOptionalIdentifier(value) {
|
|
1889
|
+
if (typeof value !== "string" || value.length === 0)
|
|
1890
|
+
return null;
|
|
1891
|
+
return `sha256-${createHash("sha256").update(value).digest("hex").slice(0, 16)}`;
|
|
1892
|
+
}
|
|
1893
|
+
function optionalBoolean(value, fallback, path) {
|
|
1894
|
+
if (value === undefined)
|
|
1895
|
+
return fallback;
|
|
1896
|
+
if (typeof value !== "boolean")
|
|
1897
|
+
throw new Error(`Invalid ${path}; expected boolean.`);
|
|
1898
|
+
return value;
|
|
1899
|
+
}
|
|
1900
|
+
function optionalNumber(value) {
|
|
1901
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1902
|
+
}
|
|
1903
|
+
function formatMetadataValue(value) {
|
|
1904
|
+
return Array.isArray(value) ? value.join(",") : String(value);
|
|
1905
|
+
}
|
|
1906
|
+
function isRecord3(value) {
|
|
1907
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// packages/core/src/config/settings.ts
|
|
1911
|
+
function createDefaultSettings(os = process.platform) {
|
|
1912
|
+
return {
|
|
1913
|
+
version: 1,
|
|
1914
|
+
platform: {
|
|
1915
|
+
os
|
|
1916
|
+
},
|
|
1917
|
+
plugins: {
|
|
1918
|
+
claude: {
|
|
1919
|
+
enabled: true,
|
|
1920
|
+
autoSyncSkills: true,
|
|
1921
|
+
autoSyncAgents: true
|
|
1922
|
+
},
|
|
1923
|
+
codex: {
|
|
1924
|
+
enabled: false
|
|
1925
|
+
}
|
|
1926
|
+
},
|
|
1927
|
+
assets: {
|
|
1928
|
+
skills: {
|
|
1929
|
+
enabled: true
|
|
1930
|
+
},
|
|
1931
|
+
agents: {
|
|
1932
|
+
enabled: true
|
|
1933
|
+
}
|
|
1934
|
+
},
|
|
1935
|
+
doctor: {
|
|
1936
|
+
lastRunAt: null
|
|
1937
|
+
},
|
|
1938
|
+
hooks: createDefaultHookSettings()
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
function mergeSettings(existing, defaults = createDefaultSettings()) {
|
|
1942
|
+
const merged = {
|
|
1943
|
+
...defaults,
|
|
1944
|
+
...existing,
|
|
1945
|
+
platform: {
|
|
1946
|
+
...defaults.platform,
|
|
1947
|
+
...existing.platform
|
|
1948
|
+
},
|
|
1949
|
+
plugins: {
|
|
1950
|
+
claude: {
|
|
1951
|
+
...defaults.plugins.claude,
|
|
1952
|
+
...existing.plugins?.claude
|
|
1953
|
+
},
|
|
1954
|
+
codex: {
|
|
1955
|
+
...defaults.plugins.codex,
|
|
1956
|
+
...existing.plugins?.codex
|
|
1957
|
+
}
|
|
1958
|
+
},
|
|
1959
|
+
assets: {
|
|
1960
|
+
skills: {
|
|
1961
|
+
...defaults.assets.skills,
|
|
1962
|
+
...existing.assets?.skills
|
|
1963
|
+
},
|
|
1964
|
+
agents: {
|
|
1965
|
+
...defaults.assets.agents,
|
|
1966
|
+
...existing.assets?.agents
|
|
1967
|
+
}
|
|
1968
|
+
},
|
|
1969
|
+
doctor: {
|
|
1970
|
+
...defaults.doctor,
|
|
1971
|
+
...existing.doctor
|
|
1972
|
+
},
|
|
1973
|
+
hooks: existing.hooks ?? defaults.hooks
|
|
1974
|
+
};
|
|
1975
|
+
return parseSettings(merged);
|
|
1976
|
+
}
|
|
1977
|
+
function parseSettings(value) {
|
|
1978
|
+
const root = expectRecord3(value, "settings");
|
|
1979
|
+
const version = root.version;
|
|
1980
|
+
if (version !== 1) {
|
|
1981
|
+
throw new EvoDevConfigError("Invalid settings.version; expected 1");
|
|
1982
|
+
}
|
|
1983
|
+
const platform = expectRecord3(root.platform, "settings.platform");
|
|
1984
|
+
const plugins = expectRecord3(root.plugins, "settings.plugins");
|
|
1985
|
+
const assets = expectRecord3(root.assets, "settings.assets");
|
|
1986
|
+
const doctor = expectRecord3(root.doctor, "settings.doctor");
|
|
1987
|
+
const parsed = {
|
|
1988
|
+
version: 1,
|
|
1989
|
+
platform: {
|
|
1990
|
+
os: expectString2(platform.os, "settings.platform.os")
|
|
1991
|
+
},
|
|
1992
|
+
plugins: {
|
|
1993
|
+
claude: parsePluginSettings(plugins.claude, "settings.plugins.claude"),
|
|
1994
|
+
codex: parsePluginSettings(plugins.codex, "settings.plugins.codex")
|
|
1995
|
+
},
|
|
1996
|
+
assets: {
|
|
1997
|
+
skills: {
|
|
1998
|
+
enabled: expectBoolean(expectRecord3(assets.skills, "settings.assets.skills").enabled, "settings.assets.skills.enabled")
|
|
1999
|
+
},
|
|
2000
|
+
agents: {
|
|
2001
|
+
enabled: expectBoolean(expectRecord3(assets.agents, "settings.assets.agents").enabled, "settings.assets.agents.enabled")
|
|
2002
|
+
}
|
|
2003
|
+
},
|
|
2004
|
+
doctor: {
|
|
2005
|
+
lastRunAt: expectNullableString(doctor.lastRunAt, "settings.doctor.lastRunAt")
|
|
2006
|
+
},
|
|
2007
|
+
hooks: parseHookSettings(root.hooks)
|
|
2008
|
+
};
|
|
2009
|
+
return parsed;
|
|
2010
|
+
}
|
|
2011
|
+
function parsePluginSettings(value, path) {
|
|
2012
|
+
const input = expectRecord3(value, path);
|
|
2013
|
+
const parsed = {
|
|
2014
|
+
enabled: expectBoolean(input.enabled, `${path}.enabled`)
|
|
2015
|
+
};
|
|
2016
|
+
if (input.autoSyncSkills !== undefined) {
|
|
2017
|
+
parsed.autoSyncSkills = expectBoolean(input.autoSyncSkills, `${path}.autoSyncSkills`);
|
|
2018
|
+
}
|
|
2019
|
+
if (input.autoSyncAgents !== undefined) {
|
|
2020
|
+
parsed.autoSyncAgents = expectBoolean(input.autoSyncAgents, `${path}.autoSyncAgents`);
|
|
2021
|
+
}
|
|
2022
|
+
return parsed;
|
|
2023
|
+
}
|
|
2024
|
+
function expectRecord3(value, path) {
|
|
2025
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2026
|
+
throw new EvoDevConfigError(`Invalid ${path}; expected object, got ${describeType2(value)}`);
|
|
2027
|
+
}
|
|
2028
|
+
return value;
|
|
2029
|
+
}
|
|
2030
|
+
function expectString2(value, path) {
|
|
2031
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
2032
|
+
throw new EvoDevConfigError(`Invalid ${path}; expected non-empty string`);
|
|
2033
|
+
}
|
|
2034
|
+
return value;
|
|
2035
|
+
}
|
|
2036
|
+
function expectNullableString(value, path) {
|
|
2037
|
+
if (value === null || typeof value === "string") {
|
|
2038
|
+
return value;
|
|
2039
|
+
}
|
|
2040
|
+
throw new EvoDevConfigError(`Invalid ${path}; expected string or null`);
|
|
2041
|
+
}
|
|
2042
|
+
function expectBoolean(value, path) {
|
|
2043
|
+
if (typeof value !== "boolean") {
|
|
2044
|
+
throw new EvoDevConfigError(`Invalid ${path}; expected boolean`);
|
|
2045
|
+
}
|
|
2046
|
+
return value;
|
|
2047
|
+
}
|
|
2048
|
+
// packages/core/src/config/state.ts
|
|
2049
|
+
function createDefaultInstallState() {
|
|
2050
|
+
return {
|
|
2051
|
+
version: 1,
|
|
2052
|
+
initializedAt: null,
|
|
2053
|
+
initializedBy: "evodev",
|
|
2054
|
+
selectedPlugins: []
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
function createDefaultSyncState() {
|
|
2058
|
+
return {
|
|
2059
|
+
version: 1,
|
|
2060
|
+
lastSyncAt: null,
|
|
2061
|
+
targets: {}
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
function parseInstallState(value) {
|
|
2065
|
+
const root = expectRecord4(value, "install state");
|
|
2066
|
+
if (root.version !== 1) {
|
|
2067
|
+
throw new EvoDevConfigError("Invalid install state.version; expected 1");
|
|
2068
|
+
}
|
|
2069
|
+
if (root.initializedBy !== "evodev") {
|
|
2070
|
+
throw new EvoDevConfigError("Invalid install state.initializedBy; expected evodev");
|
|
2071
|
+
}
|
|
2072
|
+
if (!Array.isArray(root.selectedPlugins) || !root.selectedPlugins.every((plugin) => typeof plugin === "string")) {
|
|
2073
|
+
throw new EvoDevConfigError("Invalid install state.selectedPlugins; expected string array");
|
|
2074
|
+
}
|
|
2075
|
+
return {
|
|
2076
|
+
version: 1,
|
|
2077
|
+
initializedAt: expectNullableString2(root.initializedAt, "install state.initializedAt"),
|
|
2078
|
+
initializedBy: "evodev",
|
|
2079
|
+
selectedPlugins: [...root.selectedPlugins]
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
function parseSyncState(value) {
|
|
2083
|
+
const root = expectRecord4(value, "sync state");
|
|
2084
|
+
if (root.version !== 1) {
|
|
2085
|
+
throw new EvoDevConfigError("Invalid sync state.version; expected 1");
|
|
2086
|
+
}
|
|
2087
|
+
const targets = expectRecord4(root.targets, "sync state.targets");
|
|
2088
|
+
const parsedTargets = {};
|
|
2089
|
+
for (const [pluginId, state] of Object.entries(targets)) {
|
|
2090
|
+
parsedTargets[pluginId] = parsePluginSyncState(state, `sync state.targets.${pluginId}`);
|
|
2091
|
+
}
|
|
2092
|
+
return {
|
|
2093
|
+
version: 1,
|
|
2094
|
+
lastSyncAt: expectNullableString2(root.lastSyncAt, "sync state.lastSyncAt"),
|
|
2095
|
+
targets: parsedTargets
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
function parsePluginSyncState(value, path) {
|
|
2099
|
+
const root = expectRecord4(value, path);
|
|
2100
|
+
const status = root.status;
|
|
2101
|
+
if (status !== "success" && status !== "partial" && status !== "failed" && status !== "never-run") {
|
|
2102
|
+
throw new EvoDevConfigError(`Invalid ${path}.status; expected success, partial, failed, or never-run`);
|
|
2103
|
+
}
|
|
2104
|
+
return {
|
|
2105
|
+
skills: expectNonNegativeInteger(root.skills, `${path}.skills`),
|
|
2106
|
+
agents: expectNonNegativeInteger(root.agents, `${path}.agents`),
|
|
2107
|
+
status
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
function expectRecord4(value, path) {
|
|
2111
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2112
|
+
throw new EvoDevConfigError(`Invalid ${path}; expected object, got ${describeType2(value)}`);
|
|
2113
|
+
}
|
|
2114
|
+
return value;
|
|
2115
|
+
}
|
|
2116
|
+
function expectNullableString2(value, path) {
|
|
2117
|
+
if (value === null || typeof value === "string") {
|
|
2118
|
+
return value;
|
|
2119
|
+
}
|
|
2120
|
+
throw new EvoDevConfigError(`Invalid ${path}; expected string or null`);
|
|
2121
|
+
}
|
|
2122
|
+
function expectNonNegativeInteger(value, path) {
|
|
2123
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
2124
|
+
throw new EvoDevConfigError(`Invalid ${path}; expected non-negative integer`);
|
|
2125
|
+
}
|
|
2126
|
+
return value;
|
|
2127
|
+
}
|
|
2128
|
+
// packages/core/src/config/store.ts
|
|
2129
|
+
import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
|
|
2130
|
+
import { dirname as dirname3 } from "node:path";
|
|
2131
|
+
function createCoreConfigStore(homeDir) {
|
|
2132
|
+
const paths = resolveEvoDevPaths(homeDir);
|
|
2133
|
+
return {
|
|
2134
|
+
paths,
|
|
2135
|
+
async ensureBaseDirs() {
|
|
2136
|
+
await mkdir3(paths.stateDir, { recursive: true });
|
|
2137
|
+
},
|
|
2138
|
+
async readSettings() {
|
|
2139
|
+
return readJsonFile(paths.settingsPath, parseSettings);
|
|
2140
|
+
},
|
|
2141
|
+
async writeSettings(settings) {
|
|
2142
|
+
await writeJsonFile2(paths.settingsPath, parseSettings(settings));
|
|
2143
|
+
},
|
|
2144
|
+
async mergeAndWriteSettings(input) {
|
|
2145
|
+
const current = await readJsonFileOrDefault(paths.settingsPath, parseSettings, createDefaultSettings());
|
|
2146
|
+
const merged = mergeSettings(input, current);
|
|
2147
|
+
await writeJsonFile2(paths.settingsPath, merged);
|
|
2148
|
+
return merged;
|
|
2149
|
+
},
|
|
2150
|
+
async readRegistry() {
|
|
2151
|
+
return readJsonFile(paths.registryPath, parseRegistry);
|
|
2152
|
+
},
|
|
2153
|
+
async writeRegistry(registry) {
|
|
2154
|
+
await writeJsonFile2(paths.registryPath, parseRegistry(registry));
|
|
2155
|
+
},
|
|
2156
|
+
async readInstallState() {
|
|
2157
|
+
return readJsonFile(paths.installStatePath, parseInstallState);
|
|
2158
|
+
},
|
|
2159
|
+
async writeInstallState(state) {
|
|
2160
|
+
await writeJsonFile2(paths.installStatePath, parseInstallState(state));
|
|
2161
|
+
},
|
|
2162
|
+
async readSyncState() {
|
|
2163
|
+
return readJsonFile(paths.syncStatePath, parseSyncState);
|
|
2164
|
+
},
|
|
2165
|
+
async writeSyncState(state) {
|
|
2166
|
+
await writeJsonFile2(paths.syncStatePath, parseSyncState(state));
|
|
2167
|
+
}
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
async function initializeCoreConfig(homeDir) {
|
|
2171
|
+
const store = createCoreConfigStore(homeDir);
|
|
2172
|
+
await store.ensureBaseDirs();
|
|
2173
|
+
await writeIfMissing(store.paths.settingsPath, createDefaultSettings());
|
|
2174
|
+
await writeIfMissing(store.paths.registryPath, createDefaultRegistry());
|
|
2175
|
+
await writeIfMissing(store.paths.installStatePath, createDefaultInstallState());
|
|
2176
|
+
await writeIfMissing(store.paths.syncStatePath, createDefaultSyncState());
|
|
2177
|
+
return store;
|
|
2178
|
+
}
|
|
2179
|
+
async function readJsonFile(filePath, parse) {
|
|
2180
|
+
let raw;
|
|
2181
|
+
try {
|
|
2182
|
+
raw = await readFile5(filePath, "utf8");
|
|
2183
|
+
} catch (error) {
|
|
2184
|
+
throw new EvoDevConfigError(`Cannot read config file (${describeFileError2(error)})`, filePath);
|
|
2185
|
+
}
|
|
2186
|
+
let json;
|
|
2187
|
+
try {
|
|
2188
|
+
json = JSON.parse(raw);
|
|
2189
|
+
} catch (error) {
|
|
2190
|
+
throw new EvoDevConfigError(`Invalid JSON (${describeFileError2(error)})`, filePath);
|
|
2191
|
+
}
|
|
2192
|
+
try {
|
|
2193
|
+
return parse(json);
|
|
2194
|
+
} catch (error) {
|
|
2195
|
+
if (error instanceof EvoDevConfigError) {
|
|
2196
|
+
throw new EvoDevConfigError(error.message, filePath);
|
|
2197
|
+
}
|
|
2198
|
+
throw error;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
async function readJsonFileOrDefault(filePath, parse, fallback) {
|
|
2202
|
+
try {
|
|
2203
|
+
return await readJsonFile(filePath, parse);
|
|
2204
|
+
} catch (error) {
|
|
2205
|
+
if (error instanceof EvoDevConfigError && error.message.includes("ENOENT")) {
|
|
2206
|
+
return fallback;
|
|
2207
|
+
}
|
|
2208
|
+
throw error;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
async function writeIfMissing(filePath, value) {
|
|
2212
|
+
try {
|
|
2213
|
+
await readFile5(filePath, "utf8");
|
|
2214
|
+
} catch (error) {
|
|
2215
|
+
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
2216
|
+
await writeJsonFile2(filePath, value);
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
throw new EvoDevConfigError(`Cannot inspect config file (${describeFileError2(error)})`, filePath);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
async function writeJsonFile2(filePath, value) {
|
|
2223
|
+
await mkdir3(dirname3(filePath), { recursive: true });
|
|
2224
|
+
await writeFile3(filePath, `${JSON.stringify(value, null, 2)}
|
|
2225
|
+
`, "utf8");
|
|
2226
|
+
}
|
|
2227
|
+
function describeFileError2(error) {
|
|
2228
|
+
if (error instanceof Error) {
|
|
2229
|
+
return error.message;
|
|
2230
|
+
}
|
|
2231
|
+
return String(error);
|
|
2232
|
+
}
|
|
2233
|
+
function isNodeError2(error) {
|
|
2234
|
+
return error instanceof Error && "code" in error;
|
|
2235
|
+
}
|
|
2236
|
+
// packages/core/src/daemon/index.ts
|
|
2237
|
+
import { randomBytes } from "node:crypto";
|
|
2238
|
+
import { mkdir as mkdir5, readFile as readFile7, readdir as readdir3, rm, stat as stat4, writeFile as writeFile5 } from "node:fs/promises";
|
|
2239
|
+
import { createServer } from "node:http";
|
|
2240
|
+
import { dirname as dirname5, join as join5 } from "node:path";
|
|
2241
|
+
|
|
2242
|
+
// packages/core/src/observability/index.ts
|
|
2243
|
+
import { mkdir as mkdir4, readFile as readFile6, readdir as readdir2, stat as stat3, writeFile as writeFile4 } from "node:fs/promises";
|
|
2244
|
+
import { dirname as dirname4, join as join4 } from "node:path";
|
|
2245
|
+
var EVENT_TYPE_TO_DIR = {
|
|
2246
|
+
"verification.completed": "verification",
|
|
2247
|
+
"workflow.step.planned": "workflows",
|
|
2248
|
+
"test-run.completed": "tests",
|
|
2249
|
+
"review.finding": "reviews"
|
|
2250
|
+
};
|
|
2251
|
+
var FORBIDDEN_RAW_KEYS2 = new Set([
|
|
2252
|
+
"commandoutput",
|
|
2253
|
+
"memorybodies",
|
|
2254
|
+
"memorybody",
|
|
2255
|
+
"promptbody",
|
|
2256
|
+
"prompttext",
|
|
2257
|
+
"rawcommandoutput",
|
|
2258
|
+
"rawoutput",
|
|
2259
|
+
"rawpayload",
|
|
2260
|
+
"secret",
|
|
2261
|
+
"secretvalue",
|
|
2262
|
+
"source",
|
|
2263
|
+
"sourcebody",
|
|
2264
|
+
"sourcecode",
|
|
2265
|
+
"sourcecontent",
|
|
2266
|
+
"sourcetext",
|
|
2267
|
+
"stderr",
|
|
2268
|
+
"stdout",
|
|
2269
|
+
"transcript",
|
|
2270
|
+
"transcriptbody",
|
|
2271
|
+
"transcripttext"
|
|
2272
|
+
]);
|
|
2273
|
+
var SENSITIVE_TEXT_PATTERN3 = /https?:\/\/\S+|\b(secret|token|password|passwd|private|internal|api[_-]?key|apikey|credential|credentials|raw log|raw source|raw prompt)\b/i;
|
|
2274
|
+
function createObservabilityEvent(input) {
|
|
2275
|
+
const event = {
|
|
2276
|
+
version: 1,
|
|
2277
|
+
eventId: sanitizeId2(input.eventId),
|
|
2278
|
+
type: input.type,
|
|
2279
|
+
timestamp: input.timestamp ?? new Date().toISOString(),
|
|
2280
|
+
scope: input.scope,
|
|
2281
|
+
privacy: {
|
|
2282
|
+
classification: "local-private",
|
|
2283
|
+
metadataOnly: true,
|
|
2284
|
+
rawPayloadStored: false,
|
|
2285
|
+
rawOutputStored: false,
|
|
2286
|
+
sourceContentStored: false,
|
|
2287
|
+
promptStored: false
|
|
2288
|
+
},
|
|
2289
|
+
summary: sanitizeText2(input.summary),
|
|
2290
|
+
data: sanitizeData(input.data ?? {})
|
|
2291
|
+
};
|
|
2292
|
+
validateObservabilityEvent(event);
|
|
2293
|
+
return event;
|
|
2294
|
+
}
|
|
2295
|
+
function validateObservabilityEvent(event) {
|
|
2296
|
+
if (event.version !== 1)
|
|
2297
|
+
throw new Error("Observability event version must be 1.");
|
|
2298
|
+
if (event.privacy.metadataOnly !== true)
|
|
2299
|
+
throw new Error("Observability event must be metadata-only.");
|
|
2300
|
+
if (event.privacy.rawPayloadStored !== false || event.privacy.rawOutputStored !== false || event.privacy.sourceContentStored !== false || event.privacy.promptStored !== false) {
|
|
2301
|
+
throw new Error("Observability event cannot store raw payload/output/source/prompt.");
|
|
2302
|
+
}
|
|
2303
|
+
assertNoForbiddenContent(event);
|
|
2304
|
+
}
|
|
2305
|
+
function resolveObservabilityStorePaths(homeDir, type) {
|
|
2306
|
+
const rootDir = join4(homeDir, ".evodev", "OBSERVABILITY", EVENT_TYPE_TO_DIR[type]);
|
|
2307
|
+
return { rootDir, eventsPath: join4(rootDir, "events.jsonl") };
|
|
2308
|
+
}
|
|
2309
|
+
async function appendObservabilityEvent(homeDir, event) {
|
|
2310
|
+
validateObservabilityEvent(event);
|
|
2311
|
+
const paths = resolveObservabilityStorePaths(homeDir, event.type);
|
|
2312
|
+
await mkdir4(dirname4(paths.eventsPath), { recursive: true });
|
|
2313
|
+
await writeFile4(paths.eventsPath, `${JSON.stringify(event)}
|
|
2314
|
+
`, { encoding: "utf8", flag: "a" });
|
|
2315
|
+
return paths.eventsPath;
|
|
2316
|
+
}
|
|
2317
|
+
async function listObservabilityEvents(homeDir, type) {
|
|
2318
|
+
const eventTypes = type === undefined ? Object.keys(EVENT_TYPE_TO_DIR) : [type];
|
|
2319
|
+
const events = [];
|
|
2320
|
+
for (const eventType of eventTypes) {
|
|
2321
|
+
const path = resolveObservabilityStorePaths(homeDir, eventType).eventsPath;
|
|
2322
|
+
if (!await pathExists3(path))
|
|
2323
|
+
continue;
|
|
2324
|
+
const lines = (await readFile6(path, "utf8")).split(`
|
|
2325
|
+
`).filter(Boolean);
|
|
2326
|
+
for (const line of lines) {
|
|
2327
|
+
const event = JSON.parse(line);
|
|
2328
|
+
validateObservabilityEvent(event);
|
|
2329
|
+
events.push(event);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
return events.sort((left, right) => left.timestamp.localeCompare(right.timestamp));
|
|
2333
|
+
}
|
|
2334
|
+
async function dryRunObservabilityRetentionCleanup(homeDir) {
|
|
2335
|
+
const root = join4(homeDir, ".evodev", "OBSERVABILITY");
|
|
2336
|
+
const candidates = [];
|
|
2337
|
+
if (!await pathExists3(root))
|
|
2338
|
+
return { candidates, totalBytes: 0 };
|
|
2339
|
+
for (const file of await collectJsonlFiles(root)) {
|
|
2340
|
+
const fileStat = await stat3(file);
|
|
2341
|
+
candidates.push({ path: file, sizeBytes: fileStat.size, reason: "observability-jsonl" });
|
|
2342
|
+
}
|
|
2343
|
+
return {
|
|
2344
|
+
candidates,
|
|
2345
|
+
totalBytes: candidates.reduce((sum, candidate) => sum + candidate.sizeBytes, 0)
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
function formatObservabilityEvents(events) {
|
|
2349
|
+
return [
|
|
2350
|
+
"EvoDev observability events",
|
|
2351
|
+
"",
|
|
2352
|
+
...events.map((event) => `- ${event.timestamp} ${event.type} ${event.eventId}: ${event.summary}`)
|
|
2353
|
+
].join(`
|
|
2354
|
+
`);
|
|
2355
|
+
}
|
|
2356
|
+
function formatRetentionDryRun(result) {
|
|
2357
|
+
return [
|
|
2358
|
+
"EvoDev observability cleanup dry-run",
|
|
2359
|
+
"",
|
|
2360
|
+
`Candidates: ${result.candidates.length}`,
|
|
2361
|
+
`Total bytes: ${result.totalBytes}`,
|
|
2362
|
+
...result.candidates.map((candidate) => `- ${candidate.path} (${candidate.sizeBytes} bytes, ${candidate.reason})`)
|
|
2363
|
+
].join(`
|
|
2364
|
+
`);
|
|
2365
|
+
}
|
|
2366
|
+
function sanitizeData(data) {
|
|
2367
|
+
const sanitized = {};
|
|
2368
|
+
for (const [key, value] of Object.entries(data)) {
|
|
2369
|
+
const normalizedKey = normalizeKey(key);
|
|
2370
|
+
if (FORBIDDEN_RAW_KEYS2.has(normalizedKey)) {
|
|
2371
|
+
throw new Error(`Observability data contains forbidden raw field: ${key}`);
|
|
2372
|
+
}
|
|
2373
|
+
if (typeof value === "string")
|
|
2374
|
+
sanitized[key] = sanitizeText2(value);
|
|
2375
|
+
else if (Array.isArray(value))
|
|
2376
|
+
sanitized[key] = value.map(sanitizeText2);
|
|
2377
|
+
else
|
|
2378
|
+
sanitized[key] = value;
|
|
2379
|
+
}
|
|
2380
|
+
return sanitized;
|
|
2381
|
+
}
|
|
2382
|
+
function assertNoForbiddenContent(value) {
|
|
2383
|
+
if (typeof value === "string") {
|
|
2384
|
+
if (value === "local-private")
|
|
2385
|
+
return;
|
|
2386
|
+
if (SENSITIVE_TEXT_PATTERN3.test(value))
|
|
2387
|
+
throw new Error("Observability event contains sensitive content.");
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
if (Array.isArray(value)) {
|
|
2391
|
+
for (const item of value)
|
|
2392
|
+
assertNoForbiddenContent(item);
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
if (typeof value !== "object" || value === null)
|
|
2396
|
+
return;
|
|
2397
|
+
for (const [key, child] of Object.entries(value)) {
|
|
2398
|
+
const normalizedKey = normalizeKey(key);
|
|
2399
|
+
if (FORBIDDEN_RAW_KEYS2.has(normalizedKey)) {
|
|
2400
|
+
throw new Error(`Observability event contains forbidden raw field: ${key}`);
|
|
2401
|
+
}
|
|
2402
|
+
assertNoForbiddenContent(child);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
async function collectJsonlFiles(root) {
|
|
2406
|
+
const entries = await readdir2(root, { withFileTypes: true });
|
|
2407
|
+
const files = [];
|
|
2408
|
+
for (const entry of entries) {
|
|
2409
|
+
const path = join4(root, entry.name);
|
|
2410
|
+
if (entry.isDirectory())
|
|
2411
|
+
files.push(...await collectJsonlFiles(path));
|
|
2412
|
+
if (entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
2413
|
+
files.push(path);
|
|
2414
|
+
}
|
|
2415
|
+
return files.sort();
|
|
2416
|
+
}
|
|
2417
|
+
async function pathExists3(path) {
|
|
2418
|
+
try {
|
|
2419
|
+
await stat3(path);
|
|
2420
|
+
return true;
|
|
2421
|
+
} catch (error) {
|
|
2422
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
2423
|
+
return false;
|
|
2424
|
+
throw error;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
function sanitizeText2(value) {
|
|
2428
|
+
return value.replace(SENSITIVE_TEXT_PATTERN3, "[redacted]").slice(0, 500);
|
|
2429
|
+
}
|
|
2430
|
+
function sanitizeId2(value) {
|
|
2431
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 80) || "event";
|
|
2432
|
+
}
|
|
2433
|
+
function normalizeKey(key) {
|
|
2434
|
+
return key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
// packages/core/src/daemon/index.ts
|
|
2438
|
+
var DEFAULT_PORT = 37645;
|
|
2439
|
+
var SENSITIVE_TEXT_PATTERN4 = /https?:\/\/\S+|\b(secret|token|password|passwd|private|internal|api[_-]?key|apikey|credential|credentials|raw log|raw output|raw source|raw prompt|transcript|stdout|stderr)\b/i;
|
|
2440
|
+
function resolveDaemonPaths(homeDir) {
|
|
2441
|
+
const rootDir = join5(homeDir, ".evodev", "STATE", "daemon");
|
|
2442
|
+
return { rootDir, lockPath: join5(rootDir, "lock.json"), tokenPath: join5(rootDir, "token") };
|
|
2443
|
+
}
|
|
2444
|
+
function validateDaemonBindHost(host) {
|
|
2445
|
+
if (host === "127.0.0.1" || host === "localhost")
|
|
2446
|
+
return host;
|
|
2447
|
+
throw new Error("Daemon bind host must be local-only: 127.0.0.1 or localhost.");
|
|
2448
|
+
}
|
|
2449
|
+
function createDaemonStartPlan(input) {
|
|
2450
|
+
const host = validateDaemonBindHost(input.host ?? "127.0.0.1");
|
|
2451
|
+
const port = input.port ?? DEFAULT_PORT;
|
|
2452
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535)
|
|
2453
|
+
throw new Error("Invalid daemon port.");
|
|
2454
|
+
const paths = resolveDaemonPaths(input.homeDir);
|
|
2455
|
+
return {
|
|
2456
|
+
host,
|
|
2457
|
+
port,
|
|
2458
|
+
paths,
|
|
2459
|
+
writes: [paths.lockPath, paths.tokenPath],
|
|
2460
|
+
warnings: ["Daemon is disabled by default and starts only via explicit daemon start."]
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
async function writeDaemonState(input) {
|
|
2464
|
+
const plan = createDaemonStartPlan(input);
|
|
2465
|
+
if (await readDaemonLock(input.homeDir) !== null) {
|
|
2466
|
+
throw new Error("Existing daemon lock found; refusing to overwrite daemon state.");
|
|
2467
|
+
}
|
|
2468
|
+
const token = createDaemonToken();
|
|
2469
|
+
const now = input.now ?? new Date().toISOString();
|
|
2470
|
+
const lock = {
|
|
2471
|
+
version: 1,
|
|
2472
|
+
component: "evodev-daemon",
|
|
2473
|
+
pid: input.pid ?? process.pid,
|
|
2474
|
+
host: plan.host,
|
|
2475
|
+
port: plan.port,
|
|
2476
|
+
startedAt: now,
|
|
2477
|
+
heartbeatAt: now,
|
|
2478
|
+
status: "running",
|
|
2479
|
+
tokenPath: plan.paths.tokenPath,
|
|
2480
|
+
versionText: input.versionText ?? "evodev 0.0.1-alpha"
|
|
2481
|
+
};
|
|
2482
|
+
await mkdir5(dirname5(plan.paths.lockPath), { recursive: true });
|
|
2483
|
+
await writeFile5(plan.paths.tokenPath, `${token}
|
|
2484
|
+
`, {
|
|
2485
|
+
encoding: "utf8",
|
|
2486
|
+
flag: "wx",
|
|
2487
|
+
mode: 384
|
|
2488
|
+
});
|
|
2489
|
+
await writeFile5(plan.paths.lockPath, `${JSON.stringify(lock, null, 2)}
|
|
2490
|
+
`, {
|
|
2491
|
+
encoding: "utf8",
|
|
2492
|
+
flag: "wx",
|
|
2493
|
+
mode: 384
|
|
2494
|
+
});
|
|
2495
|
+
return { lock, token, paths: plan.paths };
|
|
2496
|
+
}
|
|
2497
|
+
async function readDaemonLock(homeDir) {
|
|
2498
|
+
const paths = resolveDaemonPaths(homeDir);
|
|
2499
|
+
if (!await pathExists4(paths.lockPath))
|
|
2500
|
+
return null;
|
|
2501
|
+
const lock = JSON.parse(await readFile7(paths.lockPath, "utf8"));
|
|
2502
|
+
if (lock.version !== 1 || lock.component !== "evodev-daemon")
|
|
2503
|
+
throw new Error("Invalid daemon lock.");
|
|
2504
|
+
return lock;
|
|
2505
|
+
}
|
|
2506
|
+
async function readDaemonToken(homeDir) {
|
|
2507
|
+
const paths = resolveDaemonPaths(homeDir);
|
|
2508
|
+
if (!await pathExists4(paths.tokenPath))
|
|
2509
|
+
return null;
|
|
2510
|
+
return (await readFile7(paths.tokenPath, "utf8")).trim();
|
|
2511
|
+
}
|
|
2512
|
+
async function cleanupDaemonState(homeDir, token) {
|
|
2513
|
+
const paths = resolveDaemonPaths(homeDir);
|
|
2514
|
+
const currentToken = await readDaemonToken(homeDir);
|
|
2515
|
+
if (currentToken === null || currentToken !== token)
|
|
2516
|
+
throw new Error("Invalid daemon token.");
|
|
2517
|
+
const lock = await readDaemonLock(homeDir);
|
|
2518
|
+
if (lock === null || lock.component !== "evodev-daemon")
|
|
2519
|
+
throw new Error("Missing valid daemon lock.");
|
|
2520
|
+
await rm(paths.lockPath, { force: true });
|
|
2521
|
+
await rm(paths.tokenPath, { force: true });
|
|
2522
|
+
return [paths.lockPath, paths.tokenPath];
|
|
2523
|
+
}
|
|
2524
|
+
async function handleDaemonRequest(input) {
|
|
2525
|
+
const warnings = [];
|
|
2526
|
+
if (!isAllowedLocalOrigin(input.origin ?? null)) {
|
|
2527
|
+
return { status: 403, body: { ok: false, data: { error: "forbidden origin" }, warnings } };
|
|
2528
|
+
}
|
|
2529
|
+
if (input.path === "/health" && input.method === "GET") {
|
|
2530
|
+
return ok({ status: "ok", version: 1, metadataOnly: true }, warnings);
|
|
2531
|
+
}
|
|
2532
|
+
const auth = await authorize(input.homeDir, input.token ?? null);
|
|
2533
|
+
if (!auth.ok)
|
|
2534
|
+
return { status: 401, body: { ok: false, data: { error: "unauthorized" }, warnings } };
|
|
2535
|
+
if (input.path === "/shutdown" && input.method === "POST") {
|
|
2536
|
+
const removed = await cleanupDaemonState(input.homeDir, input.token ?? "");
|
|
2537
|
+
return ok({ stopped: true, removed }, warnings);
|
|
2538
|
+
}
|
|
2539
|
+
if (input.method !== "GET")
|
|
2540
|
+
return notFound(warnings);
|
|
2541
|
+
if (input.path === "/tasks")
|
|
2542
|
+
return ok(await collectTaskSummaries(input.homeDir, warnings), warnings);
|
|
2543
|
+
if (input.path === "/observability/events")
|
|
2544
|
+
return ok(await collectObservabilitySummaries(input.homeDir, warnings), warnings);
|
|
2545
|
+
if (input.path === "/memory/candidates")
|
|
2546
|
+
return ok(await collectLearningCandidateSummaries(input.homeDir, warnings), warnings);
|
|
2547
|
+
if (input.path === "/projects")
|
|
2548
|
+
return ok(await collectDirectorySummaries(join5(input.homeDir, ".evodev", "PROJECTS"), warnings), warnings);
|
|
2549
|
+
if (input.path === "/packs")
|
|
2550
|
+
return ok(await collectDirectorySummaries(join5(input.homeDir, ".evodev", "PACKS"), warnings), warnings);
|
|
2551
|
+
if (input.path === "/runs")
|
|
2552
|
+
return ok([], warnings);
|
|
2553
|
+
if (input.path === "/agents")
|
|
2554
|
+
return ok([], warnings);
|
|
2555
|
+
return notFound(warnings);
|
|
2556
|
+
}
|
|
2557
|
+
async function runDaemonForeground(input) {
|
|
2558
|
+
const state = await writeDaemonState({
|
|
2559
|
+
homeDir: input.homeDir,
|
|
2560
|
+
host: input.host,
|
|
2561
|
+
port: input.port
|
|
2562
|
+
});
|
|
2563
|
+
const server = createServer(async (request, response) => {
|
|
2564
|
+
try {
|
|
2565
|
+
const url = new URL(request.url ?? "/", `http://${state.lock.host}:${state.lock.port}`);
|
|
2566
|
+
const result = await handleDaemonRequest({
|
|
2567
|
+
method: request.method ?? "GET",
|
|
2568
|
+
path: url.pathname,
|
|
2569
|
+
token: readHeader(request, "authorization")?.replace(/^Bearer\s+/i, "") ?? readHeader(request, "x-evodev-token") ?? url.searchParams.get("token"),
|
|
2570
|
+
homeDir: input.homeDir,
|
|
2571
|
+
origin: readHeader(request, "origin")
|
|
2572
|
+
});
|
|
2573
|
+
response.writeHead(result.status, { "content-type": "application/json" });
|
|
2574
|
+
response.end(JSON.stringify(result.body));
|
|
2575
|
+
if (url.pathname === "/shutdown" && result.status === 200) {
|
|
2576
|
+
server.close();
|
|
2577
|
+
}
|
|
2578
|
+
} catch (error) {
|
|
2579
|
+
response.writeHead(500, { "content-type": "application/json" });
|
|
2580
|
+
response.end(JSON.stringify({
|
|
2581
|
+
ok: false,
|
|
2582
|
+
data: { error: error instanceof Error ? error.message : String(error) },
|
|
2583
|
+
warnings: []
|
|
2584
|
+
}));
|
|
2585
|
+
}
|
|
2586
|
+
});
|
|
2587
|
+
await new Promise((resolve2, reject) => {
|
|
2588
|
+
const onError = (error) => {
|
|
2589
|
+
server.off("listening", onListening);
|
|
2590
|
+
reject(error);
|
|
2591
|
+
};
|
|
2592
|
+
const onListening = () => {
|
|
2593
|
+
server.off("error", onError);
|
|
2594
|
+
resolve2();
|
|
2595
|
+
};
|
|
2596
|
+
server.once("error", onError);
|
|
2597
|
+
server.once("listening", onListening);
|
|
2598
|
+
server.listen(state.lock.port, state.lock.host);
|
|
2599
|
+
});
|
|
2600
|
+
input.write?.(`Daemon listening on ${state.lock.host}:${state.lock.port}; token path: ${state.paths.tokenPath}`);
|
|
2601
|
+
await new Promise((resolve2, reject) => {
|
|
2602
|
+
server.once("close", resolve2);
|
|
2603
|
+
server.once("error", reject);
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
function readHeader(request, name) {
|
|
2607
|
+
const value = request.headers[name.toLowerCase()];
|
|
2608
|
+
if (Array.isArray(value))
|
|
2609
|
+
return value[0] ?? null;
|
|
2610
|
+
return value ?? null;
|
|
2611
|
+
}
|
|
2612
|
+
function createDaemonToken() {
|
|
2613
|
+
return randomBytes(32).toString("hex");
|
|
2614
|
+
}
|
|
2615
|
+
async function authorize(homeDir, token) {
|
|
2616
|
+
const expected = await readDaemonToken(homeDir);
|
|
2617
|
+
return { ok: expected !== null && token !== null && token === expected };
|
|
2618
|
+
}
|
|
2619
|
+
function isAllowedLocalOrigin(origin) {
|
|
2620
|
+
if (origin === null || origin === "")
|
|
2621
|
+
return true;
|
|
2622
|
+
try {
|
|
2623
|
+
const parsed = new URL(origin);
|
|
2624
|
+
return (parsed.protocol === "http:" || parsed.protocol === "https:") && (parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost");
|
|
2625
|
+
} catch {
|
|
2626
|
+
return false;
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
async function collectTaskSummaries(homeDir, warnings) {
|
|
2630
|
+
const root = join5(homeDir, ".evodev", "STATE", "tasks");
|
|
2631
|
+
if (!await pathExists4(root)) {
|
|
2632
|
+
warnings.push("Task store not found; returning empty tasks.");
|
|
2633
|
+
return [];
|
|
2634
|
+
}
|
|
2635
|
+
const contracts = await collectNamedFiles(root, "contract.json");
|
|
2636
|
+
const summaries = [];
|
|
2637
|
+
for (const file of contracts) {
|
|
2638
|
+
try {
|
|
2639
|
+
const contract = JSON.parse(await readFile7(file, "utf8"));
|
|
2640
|
+
summaries.push(sanitizeMetadata({
|
|
2641
|
+
taskId: contract.taskId,
|
|
2642
|
+
status: contract.status,
|
|
2643
|
+
mode: contract.route?.mode ?? null,
|
|
2644
|
+
workflowId: contract.route?.workflowId ?? null,
|
|
2645
|
+
verificationStatus: contract.verification?.status ?? null
|
|
2646
|
+
}));
|
|
2647
|
+
} catch {
|
|
2648
|
+
warnings.push(`Skipped unreadable task contract: ${file}`);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
return summaries;
|
|
2652
|
+
}
|
|
2653
|
+
async function collectObservabilitySummaries(homeDir, warnings) {
|
|
2654
|
+
try {
|
|
2655
|
+
return (await listObservabilityEvents(homeDir)).map((event) => sanitizeMetadata({
|
|
2656
|
+
eventId: event.eventId,
|
|
2657
|
+
type: event.type,
|
|
2658
|
+
timestamp: event.timestamp,
|
|
2659
|
+
summary: event.summary,
|
|
2660
|
+
scope: event.scope ?? {}
|
|
2661
|
+
}));
|
|
2662
|
+
} catch (error) {
|
|
2663
|
+
warnings.push(`Observability unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
2664
|
+
return [];
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
async function collectLearningCandidateSummaries(homeDir, warnings) {
|
|
2668
|
+
const path = join5(homeDir, ".evodev", "STATE", "learning", "candidates.jsonl");
|
|
2669
|
+
if (!await pathExists4(path)) {
|
|
2670
|
+
warnings.push("Learning candidate store not found; returning empty candidates.");
|
|
2671
|
+
return [];
|
|
2672
|
+
}
|
|
2673
|
+
const lines = (await readFile7(path, "utf8")).split(`
|
|
2674
|
+
`).filter(Boolean);
|
|
2675
|
+
return lines.map((line) => {
|
|
2676
|
+
const candidate = JSON.parse(line);
|
|
2677
|
+
return sanitizeMetadata({
|
|
2678
|
+
id: candidate.id,
|
|
2679
|
+
kind: candidate.kind,
|
|
2680
|
+
status: candidate.status,
|
|
2681
|
+
scope: candidate.scope
|
|
2682
|
+
});
|
|
2683
|
+
});
|
|
2684
|
+
}
|
|
2685
|
+
async function collectDirectorySummaries(root, warnings) {
|
|
2686
|
+
if (!await pathExists4(root)) {
|
|
2687
|
+
warnings.push(`Store not found: ${root}`);
|
|
2688
|
+
return [];
|
|
2689
|
+
}
|
|
2690
|
+
const entries = await readdir3(root, { withFileTypes: true });
|
|
2691
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => ({ id: entry.name, metadataOnly: true }));
|
|
2692
|
+
}
|
|
2693
|
+
function sanitizeMetadata(value) {
|
|
2694
|
+
if (typeof value === "string") {
|
|
2695
|
+
if (SENSITIVE_TEXT_PATTERN4.test(value))
|
|
2696
|
+
return "[redacted]";
|
|
2697
|
+
return value.slice(0, 500);
|
|
2698
|
+
}
|
|
2699
|
+
if (Array.isArray(value))
|
|
2700
|
+
return value.map(sanitizeMetadata);
|
|
2701
|
+
if (typeof value !== "object" || value === null)
|
|
2702
|
+
return value;
|
|
2703
|
+
const output = {};
|
|
2704
|
+
for (const [key, child] of Object.entries(value)) {
|
|
2705
|
+
if (/raw|prompt|source|stdout|stderr|secret|token|password|transcript|memorybody/i.test(key))
|
|
2706
|
+
continue;
|
|
2707
|
+
output[key] = sanitizeMetadata(child);
|
|
2708
|
+
}
|
|
2709
|
+
return output;
|
|
2710
|
+
}
|
|
2711
|
+
async function collectNamedFiles(root, name) {
|
|
2712
|
+
const entries = await readdir3(root, { withFileTypes: true });
|
|
2713
|
+
const files = [];
|
|
2714
|
+
for (const entry of entries) {
|
|
2715
|
+
const path = join5(root, entry.name);
|
|
2716
|
+
if (entry.isDirectory())
|
|
2717
|
+
files.push(...await collectNamedFiles(path, name));
|
|
2718
|
+
else if (entry.isFile() && entry.name === name)
|
|
2719
|
+
files.push(path);
|
|
2720
|
+
}
|
|
2721
|
+
return files;
|
|
2722
|
+
}
|
|
2723
|
+
function ok(data, warnings) {
|
|
2724
|
+
return { status: 200, body: { ok: true, data: sanitizeMetadata(data), warnings } };
|
|
2725
|
+
}
|
|
2726
|
+
function notFound(warnings) {
|
|
2727
|
+
return { status: 404, body: { ok: false, data: { error: "not found" }, warnings } };
|
|
2728
|
+
}
|
|
2729
|
+
async function pathExists4(path) {
|
|
2730
|
+
try {
|
|
2731
|
+
await stat4(path);
|
|
2732
|
+
return true;
|
|
2733
|
+
} catch (error) {
|
|
2734
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
2735
|
+
return false;
|
|
2736
|
+
throw error;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
// packages/core/src/learning/index.ts
|
|
2740
|
+
import { mkdir as mkdir6, readFile as readFile8, stat as stat5, writeFile as writeFile6 } from "node:fs/promises";
|
|
2741
|
+
import { dirname as dirname6, join as join6 } from "node:path";
|
|
2742
|
+
var LEARNING_CANDIDATE_KINDS = [
|
|
2743
|
+
"lesson",
|
|
2744
|
+
"anti-criteria",
|
|
2745
|
+
"workflow-improvement",
|
|
2746
|
+
"skill-improvement"
|
|
2747
|
+
];
|
|
2748
|
+
var LEARNING_CANDIDATE_STATUSES = ["candidate", "rejected", "deferred"];
|
|
2749
|
+
var LEARNING_SCOPE_LEVELS = ["user", "project", "workflow", "skill", "asset"];
|
|
2750
|
+
var LEARNING_SOURCE_TYPES = [
|
|
2751
|
+
"task-close",
|
|
2752
|
+
"verification-failure",
|
|
2753
|
+
"review-finding",
|
|
2754
|
+
"user-feedback",
|
|
2755
|
+
"manual-candidate"
|
|
2756
|
+
];
|
|
2757
|
+
var LEARNING_CREATED_BY_VALUES = ["evodev", "user"];
|
|
2758
|
+
var LEARNING_REVIEW_DECISIONS = ["pending", "rejected", "deferred"];
|
|
2759
|
+
var LEARNING_CONFIDENCE_VALUES = ["low", "medium", "high"];
|
|
2760
|
+
var LEARNING_DECISION_VALUES = ["rejected", "deferred"];
|
|
2761
|
+
var LEARNING_DECIDED_BY_VALUES = ["user"];
|
|
2762
|
+
var LEARNING_CANDIDATE_DECISION_STATUSES = ["rejected", "deferred"];
|
|
2763
|
+
var FORBIDDEN_RAW_KEYS3 = new Set([
|
|
2764
|
+
"commandhistory",
|
|
2765
|
+
"commandoutput",
|
|
2766
|
+
"credential",
|
|
2767
|
+
"credentials",
|
|
2768
|
+
"env",
|
|
2769
|
+
"internalurl",
|
|
2770
|
+
"memorybody",
|
|
2771
|
+
"memorybodies",
|
|
2772
|
+
"password",
|
|
2773
|
+
"privatekey",
|
|
2774
|
+
"prompt",
|
|
2775
|
+
"promptbody",
|
|
2776
|
+
"prompttext",
|
|
2777
|
+
"rawcommand",
|
|
2778
|
+
"rawcommandoutput",
|
|
2779
|
+
"rawlog",
|
|
2780
|
+
"rawlogs",
|
|
2781
|
+
"rawoutput",
|
|
2782
|
+
"rawpayload",
|
|
2783
|
+
"rawprompt",
|
|
2784
|
+
"secret",
|
|
2785
|
+
"secretvalue",
|
|
2786
|
+
"source",
|
|
2787
|
+
"sourcebody",
|
|
2788
|
+
"sourcecode",
|
|
2789
|
+
"sourcecontent",
|
|
2790
|
+
"sourcetext",
|
|
2791
|
+
"stderr",
|
|
2792
|
+
"stdout",
|
|
2793
|
+
"token",
|
|
2794
|
+
"transcript",
|
|
2795
|
+
"transcriptbody",
|
|
2796
|
+
"transcripttext"
|
|
2797
|
+
]);
|
|
2798
|
+
var SENSITIVE_TEXT_PATTERN5 = /https?:\/\/\S+|\b(secret|token|password|passwd|private|internal|api[_-]?key|apikey|credential|credentials|raw log|raw logs|raw output|raw source|raw prompt|shell history|command history)\b/i;
|
|
2799
|
+
var PROTECTED_PATH_PATTERN = /(^|[~/\\])(?:USER|KNOWLEDGE|LEARNING|OBSERVABILITY|PACKS|RELEASES|logs?|memory|\.env[^/\\]*)(?:$|[/\\])|PROJECTS[/\\][^/\\]+[/\\]LEARNING(?:$|[/\\])|\.evodev[/\\](?:USER|KNOWLEDGE|LEARNING|OBSERVABILITY|PACKS|RELEASES)(?:$|[/\\])/i;
|
|
2800
|
+
function createLearningCandidate(input) {
|
|
2801
|
+
const candidate = {
|
|
2802
|
+
version: 1,
|
|
2803
|
+
id: sanitizeId3(input.id),
|
|
2804
|
+
kind: input.kind,
|
|
2805
|
+
status: "candidate",
|
|
2806
|
+
routingInfluence: false,
|
|
2807
|
+
scope: sanitizeScope(input.scope),
|
|
2808
|
+
content: {
|
|
2809
|
+
summary: sanitizeText3(input.content.summary),
|
|
2810
|
+
howToApply: sanitizeText3(input.content.howToApply),
|
|
2811
|
+
antiCriteriaImpact: input.content.antiCriteriaImpact.map(sanitizeText3)
|
|
2812
|
+
},
|
|
2813
|
+
provenance: {
|
|
2814
|
+
taskId: sanitizeNullableId(input.provenance.taskId),
|
|
2815
|
+
taskContractRef: sanitizeNullableText(input.provenance.taskContractRef),
|
|
2816
|
+
workflowRunId: sanitizeNullableId(input.provenance.workflowRunId),
|
|
2817
|
+
evidenceRefs: input.provenance.evidenceRefs.map(sanitizeText3),
|
|
2818
|
+
sourceType: input.provenance.sourceType,
|
|
2819
|
+
createdAt: sanitizeText3(input.provenance.createdAt),
|
|
2820
|
+
createdBy: input.provenance.createdBy,
|
|
2821
|
+
rawPromptStored: false,
|
|
2822
|
+
sourceContentStored: false,
|
|
2823
|
+
rawCommandOutputStored: false
|
|
2824
|
+
},
|
|
2825
|
+
privacy: createSafePrivacy(),
|
|
2826
|
+
review: { decision: "pending", reviewedAt: null, reviewedBy: null },
|
|
2827
|
+
retention: {
|
|
2828
|
+
staleAfter: input.retention?.staleAfter === undefined ? null : sanitizeNullableText(input.retention.staleAfter),
|
|
2829
|
+
deleteAllowed: true,
|
|
2830
|
+
exportAllowed: true
|
|
2831
|
+
},
|
|
2832
|
+
confidence: input.confidence ?? "medium"
|
|
2833
|
+
};
|
|
2834
|
+
validateLearningCandidate(candidate);
|
|
2835
|
+
return candidate;
|
|
2836
|
+
}
|
|
2837
|
+
function validateLearningCandidate(candidate) {
|
|
2838
|
+
if (!isRecord4(candidate))
|
|
2839
|
+
throw new Error("Learning candidate must be an object.");
|
|
2840
|
+
if (candidate.version !== 1)
|
|
2841
|
+
throw new Error("Learning candidate version must be 1.");
|
|
2842
|
+
assertEnumValue("kind", candidate.kind, LEARNING_CANDIDATE_KINDS);
|
|
2843
|
+
assertEnumValue("status", candidate.status, LEARNING_CANDIDATE_STATUSES);
|
|
2844
|
+
if (candidate.status !== "candidate") {
|
|
2845
|
+
throw new Error("Learning review queue accepts candidate status only.");
|
|
2846
|
+
}
|
|
2847
|
+
if (candidate.routingInfluence !== false) {
|
|
2848
|
+
throw new Error("Learning candidate routingInfluence must be false in I5.");
|
|
2849
|
+
}
|
|
2850
|
+
if (!isRecord4(candidate.scope))
|
|
2851
|
+
throw new Error("Learning candidate scope must be an object.");
|
|
2852
|
+
assertEnumValue("scope.level", candidate.scope.level, LEARNING_SCOPE_LEVELS);
|
|
2853
|
+
if (!isRecord4(candidate.content)) {
|
|
2854
|
+
throw new Error("Learning candidate content must be an object.");
|
|
2855
|
+
}
|
|
2856
|
+
assertStringField("content.summary", candidate.content.summary);
|
|
2857
|
+
assertStringField("content.howToApply", candidate.content.howToApply);
|
|
2858
|
+
assertStringArrayField("content.antiCriteriaImpact", candidate.content.antiCriteriaImpact);
|
|
2859
|
+
if (!isRecord4(candidate.provenance)) {
|
|
2860
|
+
throw new Error("Learning candidate provenance must be an object.");
|
|
2861
|
+
}
|
|
2862
|
+
assertEnumValue("provenance.sourceType", candidate.provenance.sourceType, LEARNING_SOURCE_TYPES);
|
|
2863
|
+
assertEnumValue("provenance.createdBy", candidate.provenance.createdBy, LEARNING_CREATED_BY_VALUES);
|
|
2864
|
+
assertStringField("provenance.createdAt", candidate.provenance.createdAt);
|
|
2865
|
+
assertStringArrayField("provenance.evidenceRefs", candidate.provenance.evidenceRefs);
|
|
2866
|
+
if (!isRecord4(candidate.privacy)) {
|
|
2867
|
+
throw new Error("Learning candidate privacy must be an object.");
|
|
2868
|
+
}
|
|
2869
|
+
assertEnumValue("privacy.classification", candidate.privacy.classification, ["local-private"]);
|
|
2870
|
+
if (!isRecord4(candidate.review))
|
|
2871
|
+
throw new Error("Learning candidate review must be an object.");
|
|
2872
|
+
assertEnumValue("review.decision", candidate.review.decision, LEARNING_REVIEW_DECISIONS);
|
|
2873
|
+
if (candidate.review.decision !== "pending") {
|
|
2874
|
+
throw new Error("Learning candidate review decision must be pending.");
|
|
2875
|
+
}
|
|
2876
|
+
if (!isRecord4(candidate.retention)) {
|
|
2877
|
+
throw new Error("Learning candidate retention must be an object.");
|
|
2878
|
+
}
|
|
2879
|
+
assertEnumValue("confidence", candidate.confidence, LEARNING_CONFIDENCE_VALUES);
|
|
2880
|
+
if (candidate.provenance.rawPromptStored !== false || candidate.provenance.sourceContentStored !== false || candidate.provenance.rawCommandOutputStored !== false) {
|
|
2881
|
+
throw new Error("Learning candidate cannot store raw prompt/source/command output.");
|
|
2882
|
+
}
|
|
2883
|
+
if (candidate.privacy.containsSource !== false || candidate.privacy.containsSecrets !== false || candidate.privacy.containsInternalLinks !== false || candidate.privacy.containsPersonalData !== false) {
|
|
2884
|
+
throw new Error("Learning candidate privacy fields must be local-private and raw-content-free.");
|
|
2885
|
+
}
|
|
2886
|
+
if (candidate.retention.deleteAllowed !== true || candidate.retention.exportAllowed !== true) {
|
|
2887
|
+
throw new Error("Learning candidate retention must allow delete and export.");
|
|
2888
|
+
}
|
|
2889
|
+
assertNoForbiddenContent2(candidate);
|
|
2890
|
+
}
|
|
2891
|
+
function validateLearningReviewDecisionRecord(record) {
|
|
2892
|
+
if (!isRecord4(record))
|
|
2893
|
+
throw new Error("Learning review decision must be an object.");
|
|
2894
|
+
if (record.version !== 1)
|
|
2895
|
+
throw new Error("Learning review decision version must be 1.");
|
|
2896
|
+
assertStringField("candidateId", record.candidateId);
|
|
2897
|
+
assertEnumValue("decision", record.decision, LEARNING_DECISION_VALUES);
|
|
2898
|
+
assertStringField("decidedAt", record.decidedAt);
|
|
2899
|
+
assertEnumValue("decidedBy", record.decidedBy, LEARNING_DECIDED_BY_VALUES);
|
|
2900
|
+
if (record.reason !== null)
|
|
2901
|
+
assertStringField("reason", record.reason);
|
|
2902
|
+
if (record.writesAcceptedMemory !== false) {
|
|
2903
|
+
throw new Error("Learning review decision writesAcceptedMemory must be false in I5.");
|
|
2904
|
+
}
|
|
2905
|
+
if (record.affectsRouting !== false) {
|
|
2906
|
+
throw new Error("Learning review decision affectsRouting must be false in I5.");
|
|
2907
|
+
}
|
|
2908
|
+
assertEnumValue("candidateStatus", record.candidateStatus, LEARNING_CANDIDATE_DECISION_STATUSES);
|
|
2909
|
+
if (record.candidateStatus !== record.decision) {
|
|
2910
|
+
throw new Error("Learning review decision candidateStatus must match decision.");
|
|
2911
|
+
}
|
|
2912
|
+
assertNoForbiddenContent2(record);
|
|
2913
|
+
}
|
|
2914
|
+
function parseLearningCandidate(value) {
|
|
2915
|
+
if (!isRecord4(value))
|
|
2916
|
+
throw new Error("Invalid learning candidate JSON.");
|
|
2917
|
+
validateLearningCandidate(value);
|
|
2918
|
+
return value;
|
|
2919
|
+
}
|
|
2920
|
+
function parseLearningReviewDecisionRecord(value) {
|
|
2921
|
+
if (!isRecord4(value))
|
|
2922
|
+
throw new Error("Invalid learning review decision JSON.");
|
|
2923
|
+
validateLearningReviewDecisionRecord(value);
|
|
2924
|
+
return value;
|
|
2925
|
+
}
|
|
2926
|
+
async function readLearningCandidates(path) {
|
|
2927
|
+
return parseJsonOrJsonlFile(path, parseLearningCandidate);
|
|
2928
|
+
}
|
|
2929
|
+
async function readLearningReviewDecisions(path) {
|
|
2930
|
+
return parseJsonOrJsonlFile(path, parseLearningReviewDecisionRecord);
|
|
2931
|
+
}
|
|
2932
|
+
function resolveLearningCandidateQueuePath(homeDir) {
|
|
2933
|
+
return join6(homeDir, ".evodev", "STATE", "learning", "candidates.jsonl");
|
|
2934
|
+
}
|
|
2935
|
+
function resolveLearningDecisionPath(homeDir) {
|
|
2936
|
+
return join6(homeDir, ".evodev", "STATE", "learning", "review-decisions.jsonl");
|
|
2937
|
+
}
|
|
2938
|
+
async function appendLearningCandidate(homeDir, candidate) {
|
|
2939
|
+
validateLearningCandidate(candidate);
|
|
2940
|
+
const path = resolveLearningCandidateQueuePath(homeDir);
|
|
2941
|
+
await mkdir6(dirname6(path), { recursive: true });
|
|
2942
|
+
await writeFile6(path, `${JSON.stringify(candidate)}
|
|
2943
|
+
`, { encoding: "utf8", flag: "a" });
|
|
2944
|
+
return path;
|
|
2945
|
+
}
|
|
2946
|
+
async function listLearningCandidates(homeDir) {
|
|
2947
|
+
const path = resolveLearningCandidateQueuePath(homeDir);
|
|
2948
|
+
if (!await pathExists5(path))
|
|
2949
|
+
return [];
|
|
2950
|
+
return readLearningCandidates(path);
|
|
2951
|
+
}
|
|
2952
|
+
async function listLearningReviewDecisions(homeDir) {
|
|
2953
|
+
const path = resolveLearningDecisionPath(homeDir);
|
|
2954
|
+
if (!await pathExists5(path))
|
|
2955
|
+
return [];
|
|
2956
|
+
return readLearningReviewDecisions(path);
|
|
2957
|
+
}
|
|
2958
|
+
function lintLearningCandidates(candidates, options = {}) {
|
|
2959
|
+
const findings = [];
|
|
2960
|
+
for (const candidate of candidates) {
|
|
2961
|
+
try {
|
|
2962
|
+
validateLearningCandidate(candidate);
|
|
2963
|
+
} catch (error) {
|
|
2964
|
+
findings.push({
|
|
2965
|
+
candidateId: typeof candidate.id === "string" ? candidate.id : "unknown",
|
|
2966
|
+
severity: "error",
|
|
2967
|
+
field: "candidate",
|
|
2968
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2969
|
+
});
|
|
2970
|
+
continue;
|
|
2971
|
+
}
|
|
2972
|
+
for (const [field, value] of requiredFields(candidate)) {
|
|
2973
|
+
if (value === undefined || value === null || value === "") {
|
|
2974
|
+
findings.push({
|
|
2975
|
+
candidateId: candidate.id,
|
|
2976
|
+
severity: "error",
|
|
2977
|
+
field,
|
|
2978
|
+
message: "Required learning candidate field is missing."
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
if (candidate.provenance.evidenceRefs.length === 0) {
|
|
2983
|
+
findings.push({
|
|
2984
|
+
candidateId: candidate.id,
|
|
2985
|
+
severity: "error",
|
|
2986
|
+
field: "provenance.evidenceRefs",
|
|
2987
|
+
message: "Learning candidate must include at least one evidence ref."
|
|
2988
|
+
});
|
|
2989
|
+
}
|
|
2990
|
+
if (isCandidateStale(candidate, options.now)) {
|
|
2991
|
+
findings.push({
|
|
2992
|
+
candidateId: candidate.id,
|
|
2993
|
+
severity: "error",
|
|
2994
|
+
field: "retention.staleAfter",
|
|
2995
|
+
message: "Learning candidate is stale and requires review before use."
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2998
|
+
for (const [field, path] of candidatePathFields(candidate)) {
|
|
2999
|
+
if (PROTECTED_PATH_PATTERN.test(path)) {
|
|
3000
|
+
findings.push({
|
|
3001
|
+
candidateId: candidate.id,
|
|
3002
|
+
severity: "error",
|
|
3003
|
+
field,
|
|
3004
|
+
message: "Learning candidate references a protected/private path zone."
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
for (const decision of options.decisions ?? []) {
|
|
3010
|
+
try {
|
|
3011
|
+
validateLearningReviewDecisionRecord(decision);
|
|
3012
|
+
} catch (error) {
|
|
3013
|
+
findings.push({
|
|
3014
|
+
candidateId: isRecord4(decision) && typeof decision.candidateId === "string" ? decision.candidateId : "unknown",
|
|
3015
|
+
severity: "error",
|
|
3016
|
+
field: "decision",
|
|
3017
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3018
|
+
});
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
return {
|
|
3022
|
+
ok: findings.every((finding) => finding.severity !== "error"),
|
|
3023
|
+
candidatesChecked: candidates.length,
|
|
3024
|
+
decisionsChecked: options.decisions?.length ?? 0,
|
|
3025
|
+
findings
|
|
3026
|
+
};
|
|
3027
|
+
}
|
|
3028
|
+
function buildLearningReviewEntries(candidates, decisions = [], options = {}) {
|
|
3029
|
+
const latestDecision = new Map;
|
|
3030
|
+
for (const decision of decisions) {
|
|
3031
|
+
validateLearningReviewDecisionRecord(decision);
|
|
3032
|
+
latestDecision.set(decision.candidateId, decision);
|
|
3033
|
+
}
|
|
3034
|
+
return candidates.map((candidate) => {
|
|
3035
|
+
validateLearningCandidate(candidate);
|
|
3036
|
+
const decisionRecord = latestDecision.get(candidate.id) ?? null;
|
|
3037
|
+
const decision = decisionRecord?.decision ?? "pending";
|
|
3038
|
+
return {
|
|
3039
|
+
candidate,
|
|
3040
|
+
decision,
|
|
3041
|
+
decisionRecord,
|
|
3042
|
+
stale: isCandidateStale(candidate, options.now)
|
|
3043
|
+
};
|
|
3044
|
+
}).filter((entry) => options.includeRejected === true || entry.decision !== "rejected");
|
|
3045
|
+
}
|
|
3046
|
+
function formatLearningReview(candidates, decisions = [], options = {}) {
|
|
3047
|
+
const entries = buildLearningReviewEntries(candidates, decisions, options);
|
|
3048
|
+
return [
|
|
3049
|
+
"EvoDev learning review queue",
|
|
3050
|
+
"",
|
|
3051
|
+
"Preview only: candidates are not accepted memory; accepted memory write is not implemented in I5.",
|
|
3052
|
+
"Pending/deferred candidates do not affect routing; routingInfluence must remain false.",
|
|
3053
|
+
`Candidates shown: ${entries.length}`,
|
|
3054
|
+
...entries.flatMap((entry) => [
|
|
3055
|
+
"",
|
|
3056
|
+
`- ${entry.candidate.id} (${entry.candidate.kind}, ${entry.candidate.scope.level}, ${entry.candidate.confidence})`,
|
|
3057
|
+
` Candidate status: ${entry.candidate.status}; decision state: ${entry.decision}`,
|
|
3058
|
+
` routingInfluence: ${entry.candidate.routingInfluence}; future influence: none in I5`,
|
|
3059
|
+
` Stale: ${entry.stale ? "yes" : "no"}; staleAfter: ${entry.candidate.retention.staleAfter ?? "none"}`,
|
|
3060
|
+
` Retention: deleteAllowed=${entry.candidate.retention.deleteAllowed}; exportAllowed=${entry.candidate.retention.exportAllowed}`,
|
|
3061
|
+
` Summary: ${entry.candidate.content.summary}`,
|
|
3062
|
+
` How to apply: ${entry.candidate.content.howToApply}`,
|
|
3063
|
+
` Provenance: task=${entry.candidate.provenance.taskId ?? "none"}, workflow=${entry.candidate.provenance.workflowRunId ?? "none"}, evidence=${entry.candidate.provenance.evidenceRefs.length}`,
|
|
3064
|
+
" Privacy: local-private; rawPrompt=false; sourceContent=false; rawCommandOutput=false; secrets=false; internalLinks=false"
|
|
3065
|
+
])
|
|
3066
|
+
].join(`
|
|
3067
|
+
`);
|
|
3068
|
+
}
|
|
3069
|
+
function formatLearningLint(result) {
|
|
3070
|
+
return [
|
|
3071
|
+
"EvoDev learning lint",
|
|
3072
|
+
"",
|
|
3073
|
+
`Status: ${result.ok ? "PASS" : "FAIL"}`,
|
|
3074
|
+
`Candidates checked: ${result.candidatesChecked}`,
|
|
3075
|
+
`Decisions checked: ${result.decisionsChecked}`,
|
|
3076
|
+
`Findings: ${result.findings.length}`,
|
|
3077
|
+
...result.findings.map((finding) => `- ${finding.severity.toUpperCase()} ${finding.candidateId} ${finding.field}: ${finding.message}`)
|
|
3078
|
+
].join(`
|
|
3079
|
+
`);
|
|
3080
|
+
}
|
|
3081
|
+
function createLearningReviewDecisionRecord(input) {
|
|
3082
|
+
assertEnumValue("decision", input.decision, LEARNING_DECISION_VALUES);
|
|
3083
|
+
const candidateStatus = input.decision === "rejected" ? "rejected" : "deferred";
|
|
3084
|
+
const record = {
|
|
3085
|
+
version: 1,
|
|
3086
|
+
candidateId: sanitizeId3(input.candidateId),
|
|
3087
|
+
decision: input.decision,
|
|
3088
|
+
decidedAt: sanitizeText3(input.decidedAt ?? new Date().toISOString()),
|
|
3089
|
+
decidedBy: "user",
|
|
3090
|
+
reason: input.reason === undefined ? null : sanitizeNullableText(input.reason),
|
|
3091
|
+
writesAcceptedMemory: false,
|
|
3092
|
+
affectsRouting: false,
|
|
3093
|
+
candidateStatus
|
|
3094
|
+
};
|
|
3095
|
+
validateLearningReviewDecisionRecord(record);
|
|
3096
|
+
return record;
|
|
3097
|
+
}
|
|
3098
|
+
async function appendLearningReviewDecision(homeDir, record) {
|
|
3099
|
+
validateLearningReviewDecisionRecord(record);
|
|
3100
|
+
const path = resolveLearningDecisionPath(homeDir);
|
|
3101
|
+
await mkdir6(dirname6(path), { recursive: true });
|
|
3102
|
+
await writeFile6(path, `${JSON.stringify(record)}
|
|
3103
|
+
`, { encoding: "utf8", flag: "a" });
|
|
3104
|
+
return path;
|
|
3105
|
+
}
|
|
3106
|
+
async function pathExists5(path) {
|
|
3107
|
+
try {
|
|
3108
|
+
await stat5(path);
|
|
3109
|
+
return true;
|
|
3110
|
+
} catch (error) {
|
|
3111
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
3112
|
+
return false;
|
|
3113
|
+
}
|
|
3114
|
+
throw error;
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
function createSafePrivacy() {
|
|
3118
|
+
return {
|
|
3119
|
+
classification: "local-private",
|
|
3120
|
+
containsSource: false,
|
|
3121
|
+
containsSecrets: false,
|
|
3122
|
+
containsInternalLinks: false,
|
|
3123
|
+
containsPersonalData: false
|
|
3124
|
+
};
|
|
3125
|
+
}
|
|
3126
|
+
function sanitizeScope(scope) {
|
|
3127
|
+
return {
|
|
3128
|
+
level: scope.level,
|
|
3129
|
+
projectId: sanitizeNullableId(scope.projectId),
|
|
3130
|
+
workflowId: sanitizeNullableId(scope.workflowId),
|
|
3131
|
+
skillId: sanitizeNullableId(scope.skillId),
|
|
3132
|
+
assetId: scope.assetId === undefined ? undefined : sanitizeNullableId(scope.assetId)
|
|
3133
|
+
};
|
|
3134
|
+
}
|
|
3135
|
+
function requiredFields(candidate) {
|
|
3136
|
+
return [
|
|
3137
|
+
["id", candidate.id],
|
|
3138
|
+
["kind", candidate.kind],
|
|
3139
|
+
["status", candidate.status],
|
|
3140
|
+
["routingInfluence", candidate.routingInfluence],
|
|
3141
|
+
["scope.level", candidate.scope.level],
|
|
3142
|
+
["content.summary", candidate.content.summary],
|
|
3143
|
+
["content.howToApply", candidate.content.howToApply],
|
|
3144
|
+
["provenance.sourceType", candidate.provenance.sourceType],
|
|
3145
|
+
["provenance.createdAt", candidate.provenance.createdAt],
|
|
3146
|
+
["provenance.createdBy", candidate.provenance.createdBy],
|
|
3147
|
+
["privacy.classification", candidate.privacy.classification],
|
|
3148
|
+
["review.decision", candidate.review.decision],
|
|
3149
|
+
["retention.deleteAllowed", candidate.retention.deleteAllowed],
|
|
3150
|
+
["retention.exportAllowed", candidate.retention.exportAllowed],
|
|
3151
|
+
["confidence", candidate.confidence]
|
|
3152
|
+
];
|
|
3153
|
+
}
|
|
3154
|
+
async function parseJsonOrJsonlFile(path, parseItem) {
|
|
3155
|
+
const raw = await readFile8(path, "utf8");
|
|
3156
|
+
if (raw.trim() === "")
|
|
3157
|
+
return [];
|
|
3158
|
+
if (path.endsWith(".jsonl")) {
|
|
3159
|
+
const lines = raw.split(`
|
|
3160
|
+
`).filter((line) => line.trim() !== "");
|
|
3161
|
+
return lines.map((line) => parseItem(JSON.parse(line)));
|
|
3162
|
+
}
|
|
3163
|
+
const parsed = JSON.parse(raw);
|
|
3164
|
+
const values = Array.isArray(parsed) ? parsed : [parsed];
|
|
3165
|
+
return values.map(parseItem);
|
|
3166
|
+
}
|
|
3167
|
+
function candidatePathFields(candidate) {
|
|
3168
|
+
return [
|
|
3169
|
+
["provenance.taskContractRef", candidate.provenance.taskContractRef],
|
|
3170
|
+
...candidate.provenance.evidenceRefs.map((ref, index) => [
|
|
3171
|
+
`provenance.evidenceRefs[${index}]`,
|
|
3172
|
+
ref
|
|
3173
|
+
])
|
|
3174
|
+
].filter((entry) => typeof entry[1] === "string");
|
|
3175
|
+
}
|
|
3176
|
+
function isCandidateStale(candidate, now) {
|
|
3177
|
+
if (candidate.retention.staleAfter === null)
|
|
3178
|
+
return false;
|
|
3179
|
+
const staleAfter = Date.parse(candidate.retention.staleAfter);
|
|
3180
|
+
const current = Date.parse(now ?? new Date().toISOString());
|
|
3181
|
+
return Number.isFinite(staleAfter) && Number.isFinite(current) && staleAfter < current;
|
|
3182
|
+
}
|
|
3183
|
+
function assertEnumValue(field, value, allowed) {
|
|
3184
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
3185
|
+
throw new Error(`${field} must be one of: ${allowed.join(", ")}.`);
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
function assertStringField(field, value) {
|
|
3189
|
+
if (typeof value !== "string") {
|
|
3190
|
+
throw new Error(`${field} must be a string.`);
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
function assertStringArrayField(field, value) {
|
|
3194
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
3195
|
+
throw new Error(`${field} must be an array of strings.`);
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
function assertNoForbiddenContent2(value) {
|
|
3199
|
+
if (typeof value === "string") {
|
|
3200
|
+
if (value === "local-private")
|
|
3201
|
+
return;
|
|
3202
|
+
if (SENSITIVE_TEXT_PATTERN5.test(value)) {
|
|
3203
|
+
throw new Error("Learning candidate contains sensitive content.");
|
|
3204
|
+
}
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
if (Array.isArray(value)) {
|
|
3208
|
+
for (const item of value)
|
|
3209
|
+
assertNoForbiddenContent2(item);
|
|
3210
|
+
return;
|
|
3211
|
+
}
|
|
3212
|
+
if (!isRecord4(value))
|
|
3213
|
+
return;
|
|
3214
|
+
for (const [key, child] of Object.entries(value)) {
|
|
3215
|
+
const normalizedKey = normalizeKey2(key);
|
|
3216
|
+
if (FORBIDDEN_RAW_KEYS3.has(normalizedKey)) {
|
|
3217
|
+
throw new Error(`Learning candidate contains forbidden raw field: ${key}`);
|
|
3218
|
+
}
|
|
3219
|
+
assertNoForbiddenContent2(child);
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
function sanitizeText3(value) {
|
|
3223
|
+
return value.replace(SENSITIVE_TEXT_PATTERN5, "[redacted]").slice(0, 500);
|
|
3224
|
+
}
|
|
3225
|
+
function sanitizeNullableText(value) {
|
|
3226
|
+
return value === null ? null : sanitizeText3(value);
|
|
3227
|
+
}
|
|
3228
|
+
function sanitizeId3(value) {
|
|
3229
|
+
const sanitized = sanitizeText3(value).replace(/\[redacted\]/gi, "redacted").replace(/[^a-zA-Z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 100);
|
|
3230
|
+
return sanitized || "learning-candidate";
|
|
3231
|
+
}
|
|
3232
|
+
function sanitizeNullableId(value) {
|
|
3233
|
+
return value === null ? null : sanitizeId3(value);
|
|
3234
|
+
}
|
|
3235
|
+
function normalizeKey2(key) {
|
|
3236
|
+
return key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3237
|
+
}
|
|
3238
|
+
function isRecord4(value) {
|
|
3239
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3240
|
+
}
|
|
3241
|
+
// packages/core/src/pack/index.ts
|
|
3242
|
+
import { readFile as readFile9, readdir as readdir4, stat as stat6 } from "node:fs/promises";
|
|
3243
|
+
import { isAbsolute, join as join7, relative as relative2, sep } from "node:path";
|
|
3244
|
+
|
|
3245
|
+
// packages/core/src/protected-zones/index.ts
|
|
3246
|
+
var SENSITIVE_DIRECTORY_SEGMENTS = new Set([
|
|
3247
|
+
".claude",
|
|
3248
|
+
".codex",
|
|
3249
|
+
".evodev",
|
|
3250
|
+
"KNOWLEDGE",
|
|
3251
|
+
"LEARNING",
|
|
3252
|
+
"OBSERVABILITY",
|
|
3253
|
+
"PROJECTS",
|
|
3254
|
+
"STATE",
|
|
3255
|
+
"USER",
|
|
3256
|
+
"WORK",
|
|
3257
|
+
"accepted-memory",
|
|
3258
|
+
"evidence",
|
|
3259
|
+
"hook-logs",
|
|
3260
|
+
"knowledge",
|
|
3261
|
+
"learning-candidates",
|
|
3262
|
+
"logs",
|
|
3263
|
+
"memory",
|
|
3264
|
+
"pack-logs",
|
|
3265
|
+
"private-project-context",
|
|
3266
|
+
"raw-evidence",
|
|
3267
|
+
"raw-output",
|
|
3268
|
+
"raw-payload",
|
|
3269
|
+
"raw-payloads",
|
|
3270
|
+
"secrets",
|
|
3271
|
+
"state",
|
|
3272
|
+
"task-contract",
|
|
3273
|
+
"task-contracts",
|
|
3274
|
+
"workflow-logs"
|
|
3275
|
+
]);
|
|
3276
|
+
var SENSITIVE_FILE_NAMES = new Set(["AGENTS.md", "CLAUDE.md"]);
|
|
3277
|
+
var SENSITIVE_EXTENSIONS = [".key", ".pem", ".p12", ".pfx"];
|
|
3278
|
+
var SENSITIVE_NAME_PARTS = ["raw-command-output", "raw-hook-payload", "secret", "token"];
|
|
3279
|
+
var DEFAULT_PROTECTED_ZONE_RULES = [
|
|
3280
|
+
{
|
|
3281
|
+
id: "dotenv-file",
|
|
3282
|
+
reason: "Environment files must not be included in packages or release artifacts.",
|
|
3283
|
+
matches(path) {
|
|
3284
|
+
const name = basename2(path);
|
|
3285
|
+
return name === ".env" || name.startsWith(".env.");
|
|
3286
|
+
}
|
|
3287
|
+
},
|
|
3288
|
+
{
|
|
3289
|
+
id: "runtime-private-directory",
|
|
3290
|
+
reason: "Runtime/private EvoDev directories must not be included in packages or release artifacts.",
|
|
3291
|
+
matches(_path, segments) {
|
|
3292
|
+
return segments.some((segment) => SENSITIVE_DIRECTORY_SEGMENTS.has(segment));
|
|
3293
|
+
}
|
|
3294
|
+
},
|
|
3295
|
+
{
|
|
3296
|
+
id: "project-agent-config",
|
|
3297
|
+
reason: "Project-level Code Agent configuration files are outside the release/package boundary.",
|
|
3298
|
+
matches(path) {
|
|
3299
|
+
return SENSITIVE_FILE_NAMES.has(basename2(path));
|
|
3300
|
+
}
|
|
3301
|
+
},
|
|
3302
|
+
{
|
|
3303
|
+
id: "secret-key-material",
|
|
3304
|
+
reason: "Key/certificate material must not be included in packages or release artifacts.",
|
|
3305
|
+
matches(path) {
|
|
3306
|
+
const lowerPath = path.toLowerCase();
|
|
3307
|
+
return SENSITIVE_EXTENSIONS.some((extension) => lowerPath.endsWith(extension));
|
|
3308
|
+
}
|
|
3309
|
+
},
|
|
3310
|
+
{
|
|
3311
|
+
id: "sensitive-name",
|
|
3312
|
+
reason: "Files or directories whose names suggest secrets, tokens, or raw payloads must not be included without explicit review.",
|
|
3313
|
+
matches(path, segments) {
|
|
3314
|
+
const lowerName = basename2(path).toLowerCase();
|
|
3315
|
+
const lowerSegments = segments.map((segment) => segment.toLowerCase());
|
|
3316
|
+
return SENSITIVE_NAME_PARTS.some((part) => lowerName.includes(part) || lowerSegments.some((segment) => segment.includes(part)));
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
];
|
|
3320
|
+
function checkProtectedZonePaths(paths, rules = DEFAULT_PROTECTED_ZONE_RULES) {
|
|
3321
|
+
const findings = [];
|
|
3322
|
+
for (const path of paths) {
|
|
3323
|
+
const normalizedPath = normalizePackagePath(path);
|
|
3324
|
+
const segments = normalizedPath.split("/").filter((segment) => segment.length > 0);
|
|
3325
|
+
for (const rule of rules) {
|
|
3326
|
+
if (rule.matches(normalizedPath, segments)) {
|
|
3327
|
+
findings.push({
|
|
3328
|
+
path,
|
|
3329
|
+
ruleId: rule.id,
|
|
3330
|
+
severity: "blocker",
|
|
3331
|
+
reason: rule.reason
|
|
3332
|
+
});
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
return { ok: findings.length === 0, findings };
|
|
3337
|
+
}
|
|
3338
|
+
function normalizePackagePath(path) {
|
|
3339
|
+
const normalized = path.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
3340
|
+
return normalized.startsWith("package/") ? normalized.slice("package/".length) : normalized;
|
|
3341
|
+
}
|
|
3342
|
+
function basename2(path) {
|
|
3343
|
+
const normalized = normalizePackagePath(path);
|
|
3344
|
+
return normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
// packages/core/src/pack/index.ts
|
|
3348
|
+
var ASSET_KINDS = ["skills", "agents", "workflows", "hooks", "docs"];
|
|
3349
|
+
var MANIFEST_FILE_NAME = "PACK.json";
|
|
3350
|
+
var RISKY_PERMISSION_LABELS = {
|
|
3351
|
+
writesUserConfig: "writes user-level EvoDev or Code Agent configuration",
|
|
3352
|
+
writesProjectFiles: "writes project files",
|
|
3353
|
+
usesHooks: "installs or enables hooks",
|
|
3354
|
+
usesNetwork: "uses network or remote services",
|
|
3355
|
+
publishes: "publishes packages or artifacts",
|
|
3356
|
+
deletesFiles: "deletes files",
|
|
3357
|
+
spawnsAgents: "spawns agents",
|
|
3358
|
+
writesMemory: "writes memory or knowledge",
|
|
3359
|
+
readsSourceContent: "reads source content",
|
|
3360
|
+
readsProjectMetadata: "reads project metadata"
|
|
3361
|
+
};
|
|
3362
|
+
var FIRST_SLICE_DENIED_PERMISSIONS = [
|
|
3363
|
+
"writesProjectFiles",
|
|
3364
|
+
"usesHooks",
|
|
3365
|
+
"usesNetwork",
|
|
3366
|
+
"publishes",
|
|
3367
|
+
"deletesFiles",
|
|
3368
|
+
"writesMemory",
|
|
3369
|
+
"readsSourceContent"
|
|
3370
|
+
];
|
|
3371
|
+
async function validatePack(packPath) {
|
|
3372
|
+
if (isRemotePackInput(packPath)) {
|
|
3373
|
+
throw new Error("Only local pack directories are supported in I6.");
|
|
3374
|
+
}
|
|
3375
|
+
const packRoot = await resolvePackRoot(packPath);
|
|
3376
|
+
const manifestPath = join7(packRoot, MANIFEST_FILE_NAME);
|
|
3377
|
+
const findings = [];
|
|
3378
|
+
let manifest;
|
|
3379
|
+
let assets = [];
|
|
3380
|
+
let scannedPaths = [];
|
|
3381
|
+
try {
|
|
3382
|
+
manifest = parsePackManifest(JSON.parse(await readFile9(manifestPath, "utf8")));
|
|
3383
|
+
} catch (error) {
|
|
3384
|
+
findings.push({
|
|
3385
|
+
severity: "error",
|
|
3386
|
+
code: "manifest-invalid",
|
|
3387
|
+
message: formatError(error),
|
|
3388
|
+
path: MANIFEST_FILE_NAME
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
if (manifest !== undefined) {
|
|
3392
|
+
assets = manifestAssetRefs(manifest, packRoot, findings);
|
|
3393
|
+
await validateReferencedAssets(assets, findings);
|
|
3394
|
+
validatePermissionBoundaries(manifest, findings);
|
|
3395
|
+
validateGuideReferences(manifest, assets, findings);
|
|
3396
|
+
}
|
|
3397
|
+
try {
|
|
3398
|
+
scannedPaths = await collectPackRelativePaths(packRoot);
|
|
3399
|
+
} catch (error) {
|
|
3400
|
+
findings.push({ severity: "error", code: "scan-failed", message: formatError(error) });
|
|
3401
|
+
}
|
|
3402
|
+
const protectedZoneFindings = checkProtectedZonePaths(scannedPaths).findings;
|
|
3403
|
+
for (const finding of protectedZoneFindings) {
|
|
3404
|
+
findings.push({
|
|
3405
|
+
severity: "error",
|
|
3406
|
+
code: `protected-zone:${finding.ruleId}`,
|
|
3407
|
+
message: finding.reason,
|
|
3408
|
+
path: finding.path
|
|
3409
|
+
});
|
|
3410
|
+
}
|
|
3411
|
+
return {
|
|
3412
|
+
ok: findings.every((finding) => finding.severity !== "error"),
|
|
3413
|
+
packRoot,
|
|
3414
|
+
manifestPath,
|
|
3415
|
+
source: packRoot,
|
|
3416
|
+
manifest,
|
|
3417
|
+
assets,
|
|
3418
|
+
scannedPaths,
|
|
3419
|
+
protectedZoneFindings,
|
|
3420
|
+
findings
|
|
3421
|
+
};
|
|
3422
|
+
}
|
|
3423
|
+
async function planPackInstallDryRun(input) {
|
|
3424
|
+
const packPath = typeof input === "string" ? input : input.packDir;
|
|
3425
|
+
const homePrefix = typeof input === "string" ? "~" : input.homeDir ?? "~";
|
|
3426
|
+
const displayEvodevRoot = `${homePrefix.replace(/\/$/, "")}/.evodev`;
|
|
3427
|
+
const validation = await validatePack(packPath);
|
|
3428
|
+
const manifest = validation.manifest;
|
|
3429
|
+
const plannedWrites = [];
|
|
3430
|
+
const warnings = [];
|
|
3431
|
+
const blockers = validation.findings.filter((finding) => finding.severity === "error").map((finding) => `${finding.code}${finding.path ? ` ${finding.path}` : ""}: ${finding.message}`);
|
|
3432
|
+
if (manifest === undefined) {
|
|
3433
|
+
blockers.push("Cannot plan install because PACK.json is invalid or missing.");
|
|
3434
|
+
} else {
|
|
3435
|
+
const permissions2 = normalizePackPermissions(manifest.permissions);
|
|
3436
|
+
if (!permissions2.writesUserConfig) {
|
|
3437
|
+
warnings.push("Pack does not declare writesUserConfig; dry-run only reports source assets.");
|
|
3438
|
+
}
|
|
3439
|
+
for (const asset of validation.assets) {
|
|
3440
|
+
plannedWrites.push({
|
|
3441
|
+
action: "create",
|
|
3442
|
+
sourcePath: asset.path,
|
|
3443
|
+
targetPath: `${displayEvodevRoot}/PACKS/${manifest.id}/${asset.path}`,
|
|
3444
|
+
reason: "dry-run planned pack asset write; no files are written"
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
const permissions = normalizePackPermissions(manifest?.permissions ?? {});
|
|
3449
|
+
return {
|
|
3450
|
+
validation,
|
|
3451
|
+
plannedWrites,
|
|
3452
|
+
plannedRegistryUpdates: manifest === undefined ? [] : [`dry-run metadata merge for registry.packs.${manifest.id}; no registry file is written`],
|
|
3453
|
+
plannedSettingsUpdates: manifest === undefined ? [] : [`dry-run settings compatibility review for ${manifest.id}; no settings file is written`],
|
|
3454
|
+
permissions,
|
|
3455
|
+
riskyCapabilities: riskyCapabilities(permissions),
|
|
3456
|
+
verificationPlan: manifest?.verify.requiredChecks ?? [],
|
|
3457
|
+
uninstallPlan: manifest === undefined ? [] : [
|
|
3458
|
+
"dry-run-first uninstall required",
|
|
3459
|
+
"confirmation required before future uninstall",
|
|
3460
|
+
"deleteUserContent=false; customizations and user content must be preserved"
|
|
3461
|
+
],
|
|
3462
|
+
blockers,
|
|
3463
|
+
warnings
|
|
3464
|
+
};
|
|
3465
|
+
}
|
|
3466
|
+
function parsePackManifest(value) {
|
|
3467
|
+
if (!isRecord5(value))
|
|
3468
|
+
throw new Error("Pack manifest must be an object.");
|
|
3469
|
+
const manifest = value;
|
|
3470
|
+
validateManifestTopLevelFields(manifest);
|
|
3471
|
+
const id = requireString(manifest, "id");
|
|
3472
|
+
const version = requireString(manifest, "version");
|
|
3473
|
+
const name = requireString(manifest, "name");
|
|
3474
|
+
const description = requireString(manifest, "description");
|
|
3475
|
+
const publisher = optionalString(manifest, "publisher");
|
|
3476
|
+
const license = optionalString(manifest, "license");
|
|
3477
|
+
const compatibility = requireRecord(manifest, "compatibility");
|
|
3478
|
+
const assets = parseAssets(requireRecord(manifest, "assets"));
|
|
3479
|
+
const permissions = parsePermissions(manifest.permissions);
|
|
3480
|
+
const install = requireRecord(manifest, "install");
|
|
3481
|
+
const verify = requireRecord(manifest, "verify");
|
|
3482
|
+
const customizations = requireRecord(manifest, "customizations");
|
|
3483
|
+
const observability = requireRecord(manifest, "observability");
|
|
3484
|
+
const uninstall = requireRecord(manifest, "uninstall");
|
|
3485
|
+
validateCompatibilityTargets(id, requireStringArray(compatibility, "targets"));
|
|
3486
|
+
validateProtectedZoneDeclarations(manifest.protectedZones);
|
|
3487
|
+
if (observability.metadataOnly !== true) {
|
|
3488
|
+
throw new Error(`Pack manifest ${id} must declare observability.metadataOnly=true.`);
|
|
3489
|
+
}
|
|
3490
|
+
if (uninstall.deleteUserContent !== false) {
|
|
3491
|
+
throw new Error(`Pack manifest ${id} must declare uninstall.deleteUserContent=false.`);
|
|
3492
|
+
}
|
|
3493
|
+
return {
|
|
3494
|
+
id,
|
|
3495
|
+
version,
|
|
3496
|
+
name,
|
|
3497
|
+
description,
|
|
3498
|
+
publisher,
|
|
3499
|
+
license,
|
|
3500
|
+
compatibility: {
|
|
3501
|
+
evodev: requireString(compatibility, "evodev"),
|
|
3502
|
+
targets: requireStringArray(compatibility, "targets")
|
|
3503
|
+
},
|
|
3504
|
+
assets,
|
|
3505
|
+
permissions,
|
|
3506
|
+
install: {
|
|
3507
|
+
guide: requireString(install, "guide"),
|
|
3508
|
+
requiresConfirmation: requireBoolean(install, "requiresConfirmation"),
|
|
3509
|
+
supportsDryRun: requireBoolean(install, "supportsDryRun"),
|
|
3510
|
+
backupPolicy: requireString(install, "backupPolicy"),
|
|
3511
|
+
mergePolicy: requireString(install, "mergePolicy")
|
|
3512
|
+
},
|
|
3513
|
+
verify: {
|
|
3514
|
+
guide: requireString(verify, "guide"),
|
|
3515
|
+
requiredChecks: requireStringArray(verify, "requiredChecks")
|
|
3516
|
+
},
|
|
3517
|
+
customizations: {
|
|
3518
|
+
guide: requireString(customizations, "guide"),
|
|
3519
|
+
userPath: requireString(customizations, "userPath"),
|
|
3520
|
+
projectPath: requireString(customizations, "projectPath")
|
|
3521
|
+
},
|
|
3522
|
+
protectedZones: isRecord5(manifest.protectedZones) ? {
|
|
3523
|
+
neverInclude: optionalStringArray(manifest.protectedZones, "neverInclude"),
|
|
3524
|
+
neverWrite: optionalStringArray(manifest.protectedZones, "neverWrite")
|
|
3525
|
+
} : undefined,
|
|
3526
|
+
observability: {
|
|
3527
|
+
metadataOnly: true,
|
|
3528
|
+
logEvents: requireStringArray(observability, "logEvents")
|
|
3529
|
+
},
|
|
3530
|
+
uninstall: {
|
|
3531
|
+
supported: requireBoolean(uninstall, "supported"),
|
|
3532
|
+
deleteUserContent: false,
|
|
3533
|
+
requiresDryRun: requireBoolean(uninstall, "requiresDryRun"),
|
|
3534
|
+
requiresConfirmation: requireBoolean(uninstall, "requiresConfirmation")
|
|
3535
|
+
}
|
|
3536
|
+
};
|
|
3537
|
+
}
|
|
3538
|
+
function normalizePackPermissions(permissions) {
|
|
3539
|
+
return {
|
|
3540
|
+
writesUserConfig: permissions.writesUserConfig === true,
|
|
3541
|
+
writesProjectFiles: permissions.writesProjectFiles === true,
|
|
3542
|
+
usesHooks: permissions.usesHooks === true,
|
|
3543
|
+
usesNetwork: permissions.usesNetwork === true,
|
|
3544
|
+
publishes: permissions.publishes === true,
|
|
3545
|
+
deletesFiles: permissions.deletesFiles === true,
|
|
3546
|
+
spawnsAgents: permissions.spawnsAgents === true,
|
|
3547
|
+
writesMemory: permissions.writesMemory === true,
|
|
3548
|
+
readsSourceContent: permissions.readsSourceContent === true,
|
|
3549
|
+
readsProjectMetadata: permissions.readsProjectMetadata === true
|
|
3550
|
+
};
|
|
3551
|
+
}
|
|
3552
|
+
function formatPackValidation(result) {
|
|
3553
|
+
const manifest = result.manifest;
|
|
3554
|
+
return [
|
|
3555
|
+
"EvoDev pack validate",
|
|
3556
|
+
"",
|
|
3557
|
+
`Pack root: ${result.packRoot}`,
|
|
3558
|
+
`Status: ${result.ok ? "PASS" : "FAIL"}`,
|
|
3559
|
+
manifest === undefined ? "Pack: unknown" : `Pack: ${manifest.id}@${manifest.version} (${manifest.name})`,
|
|
3560
|
+
`Assets: ${result.assets.length}`,
|
|
3561
|
+
`Scanned paths: ${result.scannedPaths.length}`,
|
|
3562
|
+
`Protected-zone blockers: ${result.protectedZoneFindings.length}`,
|
|
3563
|
+
"",
|
|
3564
|
+
"Findings:",
|
|
3565
|
+
...result.findings.length === 0 ? [" - none"] : result.findings.map((finding) => ` - ${finding.severity.toUpperCase()} ${finding.code}${finding.path ? ` ${finding.path}` : ""}: ${finding.message}`)
|
|
3566
|
+
].join(`
|
|
3567
|
+
`);
|
|
3568
|
+
}
|
|
3569
|
+
function formatPackInstallDryRun(plan) {
|
|
3570
|
+
const manifest = plan.validation.manifest;
|
|
3571
|
+
return [
|
|
3572
|
+
"EvoDev pack install dry-run",
|
|
3573
|
+
"",
|
|
3574
|
+
manifest === undefined ? "Pack: unknown" : `Pack: ${manifest.id}@${manifest.version} (${manifest.name})`,
|
|
3575
|
+
"Mode: dry-run (no writes)",
|
|
3576
|
+
`Status: ${plan.blockers.length === 0 ? "PASS" : "FAIL"}`,
|
|
3577
|
+
"",
|
|
3578
|
+
`Source: ${plan.validation.source}`,
|
|
3579
|
+
"",
|
|
3580
|
+
"Asset inventory:",
|
|
3581
|
+
...formatAssetInventory(plan.validation.assets),
|
|
3582
|
+
"Asset files:",
|
|
3583
|
+
...formatAssetFiles(plan.validation.assets),
|
|
3584
|
+
"",
|
|
3585
|
+
"Planned writes:",
|
|
3586
|
+
...plan.plannedWrites.length === 0 ? [" - none"] : plan.plannedWrites.map((write) => ` - ${write.action}: ${write.sourcePath} -> ${write.targetPath} (${write.reason})`),
|
|
3587
|
+
"",
|
|
3588
|
+
"Planned registry/settings metadata:",
|
|
3589
|
+
...plan.plannedRegistryUpdates.length === 0 ? [" - registry: none"] : plan.plannedRegistryUpdates.map((update) => ` - registry: ${update}`),
|
|
3590
|
+
...plan.plannedSettingsUpdates.length === 0 ? [" - settings: none"] : plan.plannedSettingsUpdates.map((update) => ` - settings: ${update}`),
|
|
3591
|
+
"",
|
|
3592
|
+
"Permissions:",
|
|
3593
|
+
...Object.entries(plan.permissions).map(([key, value]) => ` - ${key}: ${value}`),
|
|
3594
|
+
"",
|
|
3595
|
+
"Risky capabilities:",
|
|
3596
|
+
...plan.riskyCapabilities.length === 0 ? [" - none"] : plan.riskyCapabilities.map((capability) => ` - ${capability}`),
|
|
3597
|
+
"",
|
|
3598
|
+
"Protected-zone results:",
|
|
3599
|
+
...plan.validation.protectedZoneFindings.length === 0 ? [" - PASS"] : plan.validation.protectedZoneFindings.map((finding) => ` - BLOCKER ${finding.ruleId} ${finding.path}: ${finding.reason}`),
|
|
3600
|
+
"",
|
|
3601
|
+
"Verification plan:",
|
|
3602
|
+
...plan.verificationPlan.length === 0 ? [" - none"] : plan.verificationPlan.map((check) => ` - ${check}`),
|
|
3603
|
+
"",
|
|
3604
|
+
"Uninstall plan:",
|
|
3605
|
+
...plan.uninstallPlan.length === 0 ? [" - none"] : plan.uninstallPlan.map((step) => ` - ${step}`),
|
|
3606
|
+
"",
|
|
3607
|
+
"Warnings:",
|
|
3608
|
+
...plan.warnings.length === 0 ? [" - none"] : plan.warnings.map((warning) => ` - ${warning}`),
|
|
3609
|
+
"",
|
|
3610
|
+
"Blockers:",
|
|
3611
|
+
...plan.blockers.length === 0 ? [" - none"] : plan.blockers.map((blocker) => ` - ${blocker}`)
|
|
3612
|
+
].join(`
|
|
3613
|
+
`);
|
|
3614
|
+
}
|
|
3615
|
+
async function resolvePackRoot(packPath) {
|
|
3616
|
+
const pathStat = await stat6(packPath);
|
|
3617
|
+
if (pathStat.isDirectory())
|
|
3618
|
+
return packPath;
|
|
3619
|
+
throw new Error(`Pack path must be a local directory: ${packPath}`);
|
|
3620
|
+
}
|
|
3621
|
+
function manifestAssetRefs(manifest, packRoot, findings) {
|
|
3622
|
+
const refs = [];
|
|
3623
|
+
for (const kind of ASSET_KINDS) {
|
|
3624
|
+
for (const assetPath of manifest.assets[kind]) {
|
|
3625
|
+
const pathFinding = validatePackRelativePath(assetPath, packRoot);
|
|
3626
|
+
if (pathFinding !== undefined) {
|
|
3627
|
+
findings.push({ ...pathFinding, path: assetPath });
|
|
3628
|
+
continue;
|
|
3629
|
+
}
|
|
3630
|
+
refs.push({ kind, path: assetPath, absolutePath: join7(packRoot, assetPath) });
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
return refs;
|
|
3634
|
+
}
|
|
3635
|
+
async function validateReferencedAssets(assets, findings) {
|
|
3636
|
+
for (const asset of assets) {
|
|
3637
|
+
try {
|
|
3638
|
+
const assetStat = await stat6(asset.absolutePath);
|
|
3639
|
+
if (!assetStat.isFile()) {
|
|
3640
|
+
findings.push({
|
|
3641
|
+
severity: "error",
|
|
3642
|
+
code: "asset-not-file",
|
|
3643
|
+
message: "Referenced asset must be a file.",
|
|
3644
|
+
path: asset.path
|
|
3645
|
+
});
|
|
3646
|
+
}
|
|
3647
|
+
} catch (error) {
|
|
3648
|
+
if (isEnoent(error)) {
|
|
3649
|
+
findings.push({
|
|
3650
|
+
severity: "error",
|
|
3651
|
+
code: "asset-missing",
|
|
3652
|
+
message: "Referenced asset file does not exist.",
|
|
3653
|
+
path: asset.path
|
|
3654
|
+
});
|
|
3655
|
+
continue;
|
|
3656
|
+
}
|
|
3657
|
+
throw error;
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
function validatePermissionBoundaries(manifest, findings) {
|
|
3662
|
+
const permissions = normalizePackPermissions(manifest.permissions);
|
|
3663
|
+
for (const permission of FIRST_SLICE_DENIED_PERMISSIONS) {
|
|
3664
|
+
if (permissions[permission]) {
|
|
3665
|
+
findings.push({
|
|
3666
|
+
severity: "error",
|
|
3667
|
+
code: "permission-denied-first-slice",
|
|
3668
|
+
message: `${permission} is outside I6 local validate/install dry-run scope.`
|
|
3669
|
+
});
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
if (manifest.assets.hooks.length > 0 && !permissions.usesHooks) {
|
|
3673
|
+
findings.push({
|
|
3674
|
+
severity: "error",
|
|
3675
|
+
code: "permission-asset-mismatch",
|
|
3676
|
+
message: "Pack declares hook assets but usesHooks permission is false."
|
|
3677
|
+
});
|
|
3678
|
+
}
|
|
3679
|
+
if (!manifest.install.requiresConfirmation || !manifest.install.supportsDryRun) {
|
|
3680
|
+
findings.push({
|
|
3681
|
+
severity: "error",
|
|
3682
|
+
code: "install-policy-invalid",
|
|
3683
|
+
message: "Pack install must require confirmation and support dry-run."
|
|
3684
|
+
});
|
|
3685
|
+
}
|
|
3686
|
+
if (!manifest.uninstall.requiresDryRun || !manifest.uninstall.requiresConfirmation) {
|
|
3687
|
+
findings.push({
|
|
3688
|
+
severity: "error",
|
|
3689
|
+
code: "uninstall-policy-invalid",
|
|
3690
|
+
message: "Pack uninstall must require dry-run and confirmation."
|
|
3691
|
+
});
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
function validateGuideReferences(manifest, assets, findings) {
|
|
3695
|
+
const docsAssetPaths = new Set(assets.filter((asset) => asset.kind === "docs").map((asset) => asset.path));
|
|
3696
|
+
for (const guide of [
|
|
3697
|
+
manifest.install.guide,
|
|
3698
|
+
manifest.verify.guide,
|
|
3699
|
+
manifest.customizations.guide
|
|
3700
|
+
]) {
|
|
3701
|
+
if (!docsAssetPaths.has(guide)) {
|
|
3702
|
+
findings.push({
|
|
3703
|
+
severity: "error",
|
|
3704
|
+
code: "guide-not-declared",
|
|
3705
|
+
message: "Guide file must be listed in assets.docs.",
|
|
3706
|
+
path: guide
|
|
3707
|
+
});
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
async function collectPackRelativePaths(packRoot, dir = packRoot) {
|
|
3712
|
+
const entries = await readdir4(dir, { withFileTypes: true });
|
|
3713
|
+
const paths = [];
|
|
3714
|
+
for (const entry of entries) {
|
|
3715
|
+
const absolutePath = join7(dir, entry.name);
|
|
3716
|
+
const relativePath = normalizeRelativePath(relative2(packRoot, absolutePath));
|
|
3717
|
+
paths.push(relativePath);
|
|
3718
|
+
if (entry.isDirectory()) {
|
|
3719
|
+
paths.push(...await collectPackRelativePaths(packRoot, absolutePath));
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
return paths.sort();
|
|
3723
|
+
}
|
|
3724
|
+
function validatePackRelativePath(path, packRoot) {
|
|
3725
|
+
if (path.length === 0) {
|
|
3726
|
+
return { severity: "error", code: "path-empty", message: "Path must not be empty." };
|
|
3727
|
+
}
|
|
3728
|
+
if (path.includes("\x00")) {
|
|
3729
|
+
return { severity: "error", code: "path-invalid", message: "Path must not contain NUL bytes." };
|
|
3730
|
+
}
|
|
3731
|
+
if (isAbsolute(path) || path.startsWith("~")) {
|
|
3732
|
+
return {
|
|
3733
|
+
severity: "error",
|
|
3734
|
+
code: "path-absolute",
|
|
3735
|
+
message: "Path must be relative to pack root."
|
|
3736
|
+
};
|
|
3737
|
+
}
|
|
3738
|
+
const normalized = normalizeRelativePath(path);
|
|
3739
|
+
if (normalized.split("/").includes("..")) {
|
|
3740
|
+
return {
|
|
3741
|
+
severity: "error",
|
|
3742
|
+
code: "path-traversal",
|
|
3743
|
+
message: "Path must not escape pack root."
|
|
3744
|
+
};
|
|
3745
|
+
}
|
|
3746
|
+
const absolute = join7(packRoot, normalized);
|
|
3747
|
+
const rel = relative2(packRoot, absolute);
|
|
3748
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
|
|
3749
|
+
return {
|
|
3750
|
+
severity: "error",
|
|
3751
|
+
code: "path-traversal",
|
|
3752
|
+
message: "Path must stay inside pack root."
|
|
3753
|
+
};
|
|
3754
|
+
}
|
|
3755
|
+
return;
|
|
3756
|
+
}
|
|
3757
|
+
function riskyCapabilities(permissions) {
|
|
3758
|
+
return Object.entries(permissions).filter(([, value]) => value).map(([permission]) => RISKY_PERMISSION_LABELS[permission]);
|
|
3759
|
+
}
|
|
3760
|
+
function formatAssetInventory(assets) {
|
|
3761
|
+
if (assets.length === 0)
|
|
3762
|
+
return [" - none"];
|
|
3763
|
+
const counts = new Map;
|
|
3764
|
+
for (const kind of ASSET_KINDS)
|
|
3765
|
+
counts.set(kind, 0);
|
|
3766
|
+
for (const asset of assets)
|
|
3767
|
+
counts.set(asset.kind, (counts.get(asset.kind) ?? 0) + 1);
|
|
3768
|
+
return ASSET_KINDS.map((kind) => ` - ${kind}: ${counts.get(kind) ?? 0}`);
|
|
3769
|
+
}
|
|
3770
|
+
function formatAssetFiles(assets) {
|
|
3771
|
+
if (assets.length === 0)
|
|
3772
|
+
return [" - none"];
|
|
3773
|
+
return assets.map((asset) => ` - ${asset.kind}: ${asset.path}`);
|
|
3774
|
+
}
|
|
3775
|
+
function parseAssets(value) {
|
|
3776
|
+
const knownKinds = new Set(ASSET_KINDS);
|
|
3777
|
+
for (const key of Object.keys(value)) {
|
|
3778
|
+
if (!knownKinds.has(key)) {
|
|
3779
|
+
throw new Error(`Pack manifest contains unknown asset kind: ${key}`);
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
return {
|
|
3783
|
+
skills: optionalStringArray(value, "skills") ?? [],
|
|
3784
|
+
agents: optionalStringArray(value, "agents") ?? [],
|
|
3785
|
+
workflows: optionalStringArray(value, "workflows") ?? [],
|
|
3786
|
+
hooks: optionalStringArray(value, "hooks") ?? [],
|
|
3787
|
+
docs: optionalStringArray(value, "docs") ?? []
|
|
3788
|
+
};
|
|
3789
|
+
}
|
|
3790
|
+
function parsePermissions(value) {
|
|
3791
|
+
if (value === undefined)
|
|
3792
|
+
return {};
|
|
3793
|
+
if (!isRecord5(value))
|
|
3794
|
+
throw new Error("Pack manifest permissions must be an object.");
|
|
3795
|
+
const permissions = {};
|
|
3796
|
+
const knownKeys = new Set(Object.keys(RISKY_PERMISSION_LABELS));
|
|
3797
|
+
for (const key of Object.keys(value)) {
|
|
3798
|
+
if (!knownKeys.has(key))
|
|
3799
|
+
throw new Error(`Pack manifest contains unknown permission: ${key}`);
|
|
3800
|
+
}
|
|
3801
|
+
for (const key of Object.keys(RISKY_PERMISSION_LABELS)) {
|
|
3802
|
+
if (value[key] !== undefined)
|
|
3803
|
+
permissions[key] = requireBoolean(value, key);
|
|
3804
|
+
}
|
|
3805
|
+
return permissions;
|
|
3806
|
+
}
|
|
3807
|
+
function optionalString(record, key) {
|
|
3808
|
+
const value = record[key];
|
|
3809
|
+
if (value === undefined)
|
|
3810
|
+
return;
|
|
3811
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
3812
|
+
throw new Error(`Pack manifest field must be a non-empty string: ${key}`);
|
|
3813
|
+
}
|
|
3814
|
+
return value;
|
|
3815
|
+
}
|
|
3816
|
+
function requireString(record, key) {
|
|
3817
|
+
const value = record[key];
|
|
3818
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
3819
|
+
throw new Error(`Pack manifest missing string field: ${key}`);
|
|
3820
|
+
}
|
|
3821
|
+
return value;
|
|
3822
|
+
}
|
|
3823
|
+
function requireBoolean(record, key) {
|
|
3824
|
+
const value = record[key];
|
|
3825
|
+
if (typeof value !== "boolean")
|
|
3826
|
+
throw new Error(`Pack manifest missing boolean field: ${key}`);
|
|
3827
|
+
return value;
|
|
3828
|
+
}
|
|
3829
|
+
function requireRecord(record, key) {
|
|
3830
|
+
const value = record[key];
|
|
3831
|
+
if (!isRecord5(value))
|
|
3832
|
+
throw new Error(`Pack manifest missing object field: ${key}`);
|
|
3833
|
+
return value;
|
|
3834
|
+
}
|
|
3835
|
+
function requireStringArray(record, key) {
|
|
3836
|
+
const value = record[key];
|
|
3837
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) {
|
|
3838
|
+
throw new Error(`Pack manifest missing string array field: ${key}`);
|
|
3839
|
+
}
|
|
3840
|
+
return value;
|
|
3841
|
+
}
|
|
3842
|
+
function optionalStringArray(record, key) {
|
|
3843
|
+
const value = record[key];
|
|
3844
|
+
if (value === undefined)
|
|
3845
|
+
return;
|
|
3846
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) {
|
|
3847
|
+
throw new Error(`Pack manifest field must be a string array: ${key}`);
|
|
3848
|
+
}
|
|
3849
|
+
return value;
|
|
3850
|
+
}
|
|
3851
|
+
function validateManifestTopLevelFields(manifest) {
|
|
3852
|
+
const knownFields = new Set([
|
|
3853
|
+
"id",
|
|
3854
|
+
"version",
|
|
3855
|
+
"name",
|
|
3856
|
+
"description",
|
|
3857
|
+
"publisher",
|
|
3858
|
+
"license",
|
|
3859
|
+
"compatibility",
|
|
3860
|
+
"assets",
|
|
3861
|
+
"permissions",
|
|
3862
|
+
"install",
|
|
3863
|
+
"verify",
|
|
3864
|
+
"customizations",
|
|
3865
|
+
"protectedZones",
|
|
3866
|
+
"observability",
|
|
3867
|
+
"uninstall"
|
|
3868
|
+
]);
|
|
3869
|
+
for (const key of Object.keys(manifest)) {
|
|
3870
|
+
if (!knownFields.has(key))
|
|
3871
|
+
throw new Error(`Pack manifest contains unknown field: ${key}`);
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
function validateCompatibilityTargets(packId, targets) {
|
|
3875
|
+
if (targets.length === 0)
|
|
3876
|
+
throw new Error(`Pack manifest ${packId} must declare at least one target.`);
|
|
3877
|
+
const supportedTargets = new Set(["claude"]);
|
|
3878
|
+
for (const target of targets) {
|
|
3879
|
+
if (!supportedTargets.has(target)) {
|
|
3880
|
+
throw new Error(`Pack manifest ${packId} declares unsupported target: ${target}`);
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
function validateProtectedZoneDeclarations(value) {
|
|
3885
|
+
if (value === undefined)
|
|
3886
|
+
return;
|
|
3887
|
+
if (!isRecord5(value))
|
|
3888
|
+
throw new Error("Pack manifest protectedZones must be an object.");
|
|
3889
|
+
const knownFields = new Set(["neverInclude", "neverWrite"]);
|
|
3890
|
+
for (const key of Object.keys(value)) {
|
|
3891
|
+
if (!knownFields.has(key))
|
|
3892
|
+
throw new Error(`Pack manifest protectedZones contains unknown field: ${key}`);
|
|
3893
|
+
}
|
|
3894
|
+
optionalStringArray(value, "neverInclude");
|
|
3895
|
+
optionalStringArray(value, "neverWrite");
|
|
3896
|
+
}
|
|
3897
|
+
function isRemotePackInput(packPath) {
|
|
3898
|
+
return /^[a-z][a-z0-9+.-]*:\/\//i.test(packPath);
|
|
3899
|
+
}
|
|
3900
|
+
function isRecord5(value) {
|
|
3901
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3902
|
+
}
|
|
3903
|
+
function normalizeRelativePath(path) {
|
|
3904
|
+
return path.split(sep).join("/").replace(/^\.\//, "");
|
|
3905
|
+
}
|
|
3906
|
+
function isEnoent(error) {
|
|
3907
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
3908
|
+
}
|
|
3909
|
+
function formatError(error) {
|
|
3910
|
+
return error instanceof Error ? error.message : String(error);
|
|
3911
|
+
}
|
|
3912
|
+
// packages/core/src/plugins/capabilities.ts
|
|
3913
|
+
import { mkdir as mkdir7, readFile as readFile10, writeFile as writeFile7 } from "node:fs/promises";
|
|
3914
|
+
import { dirname as dirname7, join as join8 } from "node:path";
|
|
3915
|
+
function createUnknownNegotiatedCapabilities(pluginId) {
|
|
3916
|
+
return {
|
|
3917
|
+
pluginId,
|
|
3918
|
+
skills: "unsupported",
|
|
3919
|
+
agents: "unsupported",
|
|
3920
|
+
hooks: "unsupported",
|
|
3921
|
+
canPlanWrites: false,
|
|
3922
|
+
warnings: [],
|
|
3923
|
+
blockers: [`Plugin ${pluginId} capabilities are unknown and unsupported.`]
|
|
3924
|
+
};
|
|
3925
|
+
}
|
|
3926
|
+
function isCapabilityUsable(state) {
|
|
3927
|
+
return state === "enabled";
|
|
3928
|
+
}
|
|
3929
|
+
function assertNoUnverifiedWrites(negotiation) {
|
|
3930
|
+
if (!negotiation.canPlanWrites) {
|
|
3931
|
+
throw new Error(`Plugin ${negotiation.pluginId} has no verified enabled write capabilities.`);
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
function negotiatePluginCapabilities(input) {
|
|
3935
|
+
if (input.pluginId === "codex") {
|
|
3936
|
+
const declaredSupport = [
|
|
3937
|
+
input.declared.skills.supported ? "skills" : null,
|
|
3938
|
+
input.declared.agents.supported ? "agents" : null,
|
|
3939
|
+
input.declared.hooks.supported ? "hooks" : null
|
|
3940
|
+
].filter(Boolean);
|
|
3941
|
+
const blockers = ["Codex capabilities are unverified; writes are unsupported in I10."];
|
|
3942
|
+
const warnings = [
|
|
3943
|
+
input.verified ? "Codex readonly verification artifact exists; sync remains no-write until formats are verified." : null,
|
|
3944
|
+
declaredSupport.length > 0 ? `Codex declares ${declaredSupport.join(", ")} support, but declared support alone cannot authorize writes.` : null
|
|
3945
|
+
].filter((warning) => warning !== null);
|
|
3946
|
+
return {
|
|
3947
|
+
pluginId: input.pluginId,
|
|
3948
|
+
skills: input.declared.skills.supported ? "unverified" : "unsupported",
|
|
3949
|
+
agents: input.declared.agents.supported ? "unverified" : "unsupported",
|
|
3950
|
+
hooks: input.declared.hooks.supported ? "unverified" : "unsupported",
|
|
3951
|
+
canPlanWrites: false,
|
|
3952
|
+
warnings,
|
|
3953
|
+
blockers
|
|
3954
|
+
};
|
|
3955
|
+
}
|
|
3956
|
+
return {
|
|
3957
|
+
pluginId: input.pluginId,
|
|
3958
|
+
skills: resolveNonCodexCapabilityState(input.declared.skills.supported, input.enabled),
|
|
3959
|
+
agents: resolveNonCodexCapabilityState(input.declared.agents.supported, input.enabled),
|
|
3960
|
+
hooks: resolveNonCodexCapabilityState(input.declared.hooks.supported, input.enabled),
|
|
3961
|
+
canPlanWrites: input.enabled && input.declared.skills.supported && input.declared.agents.supported,
|
|
3962
|
+
warnings: input.enabled ? [] : [
|
|
3963
|
+
`Plugin ${input.pluginId} has declared capabilities but is not enabled; writes are not authorized.`
|
|
3964
|
+
],
|
|
3965
|
+
blockers: []
|
|
3966
|
+
};
|
|
3967
|
+
}
|
|
3968
|
+
function resolveNonCodexCapabilityState(supported, enabled) {
|
|
3969
|
+
if (!supported)
|
|
3970
|
+
return "unsupported";
|
|
3971
|
+
return enabled ? "enabled" : "declared";
|
|
3972
|
+
}
|
|
3973
|
+
async function createCodexCapabilityVerificationArtifact(input) {
|
|
3974
|
+
const timestamp = input.createdAt ?? new Date().toISOString();
|
|
3975
|
+
const detectionMessage = sanitizeText4(input.detection.message);
|
|
3976
|
+
const diagnostics = [
|
|
3977
|
+
"Codex remains no-write until concrete user-level paths and formats are verified."
|
|
3978
|
+
];
|
|
3979
|
+
return {
|
|
3980
|
+
version: 1,
|
|
3981
|
+
pluginId: "codex",
|
|
3982
|
+
status: "verified-readonly",
|
|
3983
|
+
verifiedAt: timestamp,
|
|
3984
|
+
createdAt: timestamp,
|
|
3985
|
+
metadataOnly: true,
|
|
3986
|
+
rawOutputStored: false,
|
|
3987
|
+
sourceContentStored: false,
|
|
3988
|
+
promptHistoryStored: false,
|
|
3989
|
+
promptStored: false,
|
|
3990
|
+
secretsStored: false,
|
|
3991
|
+
externalUpload: false,
|
|
3992
|
+
networkUsed: false,
|
|
3993
|
+
toolSummary: { tool: "codex", detectionStatus: input.detection.status, version: null },
|
|
3994
|
+
capabilities: { skills: "unverified", agents: "unverified", hooks: "unverified" },
|
|
3995
|
+
writeBoundary: {
|
|
3996
|
+
writesAllowed: false,
|
|
3997
|
+
allowedPaths: [],
|
|
3998
|
+
userLevelOnly: true,
|
|
3999
|
+
noOverwrite: true,
|
|
4000
|
+
projectWritesAllowed: false,
|
|
4001
|
+
codeAgentWritesAllowed: false
|
|
4002
|
+
},
|
|
4003
|
+
privacy: {
|
|
4004
|
+
classification: "local-private",
|
|
4005
|
+
metadataOnly: true,
|
|
4006
|
+
rawOutputStored: false,
|
|
4007
|
+
sourceContentStored: false,
|
|
4008
|
+
promptHistoryStored: false,
|
|
4009
|
+
secretsStored: false,
|
|
4010
|
+
externalUpload: false
|
|
4011
|
+
},
|
|
4012
|
+
blockers: ["Codex asset capabilities are unverified and unsupported for writes in I10."],
|
|
4013
|
+
diagnostics,
|
|
4014
|
+
evidenceRefs: [],
|
|
4015
|
+
evidence: {
|
|
4016
|
+
detectionStatus: input.detection.status,
|
|
4017
|
+
detectionMessage,
|
|
4018
|
+
writesAllowed: false,
|
|
4019
|
+
notes: [
|
|
4020
|
+
"Metadata-only readonly artifact; no Codex paths, formats, hooks, or writes verified."
|
|
4021
|
+
]
|
|
4022
|
+
}
|
|
4023
|
+
};
|
|
4024
|
+
}
|
|
4025
|
+
function resolveCodexCapabilityArtifactPath(homeDir) {
|
|
4026
|
+
return join8(homeDir, ".evodev", "STATE", "plugins", "codex", "capability-verification.json");
|
|
4027
|
+
}
|
|
4028
|
+
async function writeCodexCapabilityVerificationArtifact(homeDir, artifact) {
|
|
4029
|
+
validateCodexCapabilityVerificationArtifact(artifact);
|
|
4030
|
+
const path = resolveCodexCapabilityArtifactPath(homeDir);
|
|
4031
|
+
await mkdir7(dirname7(path), { recursive: true });
|
|
4032
|
+
await writeFile7(path, `${JSON.stringify(artifact, null, 2)}
|
|
4033
|
+
`, { encoding: "utf8", flag: "wx" });
|
|
4034
|
+
return path;
|
|
4035
|
+
}
|
|
4036
|
+
async function readCodexCapabilityVerificationArtifact(homeDir) {
|
|
4037
|
+
try {
|
|
4038
|
+
const artifact = JSON.parse(await readFile10(resolveCodexCapabilityArtifactPath(homeDir), "utf8"));
|
|
4039
|
+
validateCodexCapabilityVerificationArtifact(artifact);
|
|
4040
|
+
return artifact;
|
|
4041
|
+
} catch (error) {
|
|
4042
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
4043
|
+
return null;
|
|
4044
|
+
}
|
|
4045
|
+
throw error;
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
function validateCodexCapabilityVerificationArtifact(artifact) {
|
|
4049
|
+
if (artifact.version !== 1 || artifact.pluginId !== "codex")
|
|
4050
|
+
throw new Error("Invalid Codex artifact identity.");
|
|
4051
|
+
if (artifact.metadataOnly !== true || artifact.rawOutputStored !== false || artifact.sourceContentStored !== false || artifact.promptStored !== false || artifact.promptHistoryStored !== false || artifact.secretsStored !== false || artifact.externalUpload !== false || artifact.networkUsed !== false || artifact.privacy?.metadataOnly !== true || artifact.privacy.rawOutputStored !== false || artifact.privacy.sourceContentStored !== false || artifact.privacy.promptHistoryStored !== false || artifact.privacy.secretsStored !== false || artifact.privacy.externalUpload !== false || artifact.writeBoundary?.writesAllowed !== false || artifact.writeBoundary.allowedPaths.length !== 0 || artifact.writeBoundary.userLevelOnly !== true || artifact.writeBoundary.noOverwrite !== true || artifact.writeBoundary.projectWritesAllowed !== false || artifact.writeBoundary.codeAgentWritesAllowed !== false) {
|
|
4052
|
+
throw new Error("Codex artifact must be metadata-only and local-only.");
|
|
4053
|
+
}
|
|
4054
|
+
assertNoSensitiveContent(artifact);
|
|
4055
|
+
}
|
|
4056
|
+
function formatCodexCapabilityVerificationArtifact(artifact, path) {
|
|
4057
|
+
return [
|
|
4058
|
+
"EvoDev Codex capability verification",
|
|
4059
|
+
"",
|
|
4060
|
+
`Status: ${artifact.status}`,
|
|
4061
|
+
`Artifact: ${path ?? "dry-run only"}`,
|
|
4062
|
+
`Verified at: ${artifact.verifiedAt}`,
|
|
4063
|
+
"Capabilities: skills=unverified agents=unverified hooks=unverified",
|
|
4064
|
+
"Writes allowed: false",
|
|
4065
|
+
"Allowed paths: none",
|
|
4066
|
+
"User-level write paths verified: false",
|
|
4067
|
+
"Project writes allowed: false",
|
|
4068
|
+
"No-overwrite verified: false",
|
|
4069
|
+
`External upload: ${artifact.externalUpload}`,
|
|
4070
|
+
`Detection: ${artifact.evidence.detectionStatus}`,
|
|
4071
|
+
...artifact.evidence.notes.map((note) => `Note: ${note}`)
|
|
4072
|
+
].join(`
|
|
4073
|
+
`);
|
|
4074
|
+
}
|
|
4075
|
+
async function runPluginConformance(plugin) {
|
|
4076
|
+
const findings = [];
|
|
4077
|
+
try {
|
|
4078
|
+
await plugin.detect();
|
|
4079
|
+
await plugin.getCapabilities();
|
|
4080
|
+
} catch (error) {
|
|
4081
|
+
findings.push(`Plugin ${plugin.id} threw during detect/capabilities: ${error instanceof Error ? error.message : String(error)}`);
|
|
4082
|
+
}
|
|
4083
|
+
if (plugin.id === "codex") {
|
|
4084
|
+
const capabilities = await plugin.getCapabilities();
|
|
4085
|
+
if (capabilities.skills.supported || capabilities.agents.supported || capabilities.hooks.supported) {
|
|
4086
|
+
findings.push("Codex must not declare verified runtime support in I10.");
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
return { ok: findings.length === 0, findings };
|
|
4090
|
+
}
|
|
4091
|
+
function sanitizeText4(value) {
|
|
4092
|
+
return value.replace(/https?:\/\/\S+|\b(secret|token|password|private|internal|api[_-]?key)\b/gi, "[redacted]").slice(0, 300);
|
|
4093
|
+
}
|
|
4094
|
+
function assertNoSensitiveContent(value) {
|
|
4095
|
+
if (typeof value === "string") {
|
|
4096
|
+
if (value === "local-private")
|
|
4097
|
+
return;
|
|
4098
|
+
if (/https?:\/\/\S+|\b(secret|token|password|private|internal|api[_-]?key|raw output|raw source|raw prompt)\b/i.test(value)) {
|
|
4099
|
+
throw new Error("Codex artifact contains sensitive content.");
|
|
4100
|
+
}
|
|
4101
|
+
return;
|
|
4102
|
+
}
|
|
4103
|
+
if (Array.isArray(value)) {
|
|
4104
|
+
for (const item of value)
|
|
4105
|
+
assertNoSensitiveContent(item);
|
|
4106
|
+
return;
|
|
4107
|
+
}
|
|
4108
|
+
if (typeof value !== "object" || value === null)
|
|
4109
|
+
return;
|
|
4110
|
+
for (const [key, child] of Object.entries(value)) {
|
|
4111
|
+
if (/raw|prompt|source|secret|token|password|memorybody/i.test(key) && child !== false) {
|
|
4112
|
+
throw new Error(`Codex artifact contains forbidden field: ${key}`);
|
|
4113
|
+
}
|
|
4114
|
+
assertNoSensitiveContent(child);
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
// packages/core/src/plugins/registry.ts
|
|
4118
|
+
class PluginRegistryError extends Error {
|
|
4119
|
+
constructor(message) {
|
|
4120
|
+
super(message);
|
|
4121
|
+
this.name = "PluginRegistryError";
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4125
|
+
class PluginRegistry {
|
|
4126
|
+
#plugins = new Map;
|
|
4127
|
+
register(plugin) {
|
|
4128
|
+
if (this.#plugins.has(plugin.id)) {
|
|
4129
|
+
throw new PluginRegistryError(`Plugin already registered: ${plugin.id}`);
|
|
4130
|
+
}
|
|
4131
|
+
this.#plugins.set(plugin.id, plugin);
|
|
4132
|
+
}
|
|
4133
|
+
get(pluginId) {
|
|
4134
|
+
return this.#plugins.get(pluginId);
|
|
4135
|
+
}
|
|
4136
|
+
require(pluginId) {
|
|
4137
|
+
const plugin = this.get(pluginId);
|
|
4138
|
+
if (plugin === undefined) {
|
|
4139
|
+
throw new PluginRegistryError(`Plugin not registered: ${pluginId}`);
|
|
4140
|
+
}
|
|
4141
|
+
return plugin;
|
|
4142
|
+
}
|
|
4143
|
+
list() {
|
|
4144
|
+
return [...this.#plugins.values()].sort((left, right) => left.id.localeCompare(right.id));
|
|
4145
|
+
}
|
|
4146
|
+
getEnabled(settings) {
|
|
4147
|
+
return getEnabledPluginIds(settings).map((pluginId) => this.require(pluginId));
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
function createPluginRegistry(plugins = []) {
|
|
4151
|
+
const registry = new PluginRegistry;
|
|
4152
|
+
for (const plugin of plugins) {
|
|
4153
|
+
registry.register(plugin);
|
|
4154
|
+
}
|
|
4155
|
+
return registry;
|
|
4156
|
+
}
|
|
4157
|
+
function getEnabledPluginIds(settings) {
|
|
4158
|
+
return Object.entries(settings.plugins).filter(([, pluginSettings]) => pluginSettings.enabled).map(([pluginId]) => pluginId).sort((left, right) => left.localeCompare(right));
|
|
4159
|
+
}
|
|
4160
|
+
// packages/core/src/project/index.ts
|
|
4161
|
+
import { mkdir as mkdir8, readFile as readFile11, readdir as readdir5, stat as stat7, writeFile as writeFile8 } from "node:fs/promises";
|
|
4162
|
+
import { basename as basename3, join as join9, relative as relative3 } from "node:path";
|
|
4163
|
+
var PROJECT_CONTEXT_ALLOWED_RELATIVE_PATHS = [
|
|
4164
|
+
".evodev/project.json",
|
|
4165
|
+
".evodev/profile.md",
|
|
4166
|
+
".evodev/index.json",
|
|
4167
|
+
".evodev/commands.json",
|
|
4168
|
+
".evodev/privacy.json",
|
|
4169
|
+
".evodev/decisions/README.md"
|
|
4170
|
+
];
|
|
4171
|
+
var EXCLUDED_DIRECTORY_NAMES = new Set([
|
|
4172
|
+
".git",
|
|
4173
|
+
".claude",
|
|
4174
|
+
".codex",
|
|
4175
|
+
"backups",
|
|
4176
|
+
"build",
|
|
4177
|
+
"coverage",
|
|
4178
|
+
"dist",
|
|
4179
|
+
"node_modules"
|
|
4180
|
+
]);
|
|
4181
|
+
var EXCLUDED_FILE_PREFIXES = [".env"];
|
|
4182
|
+
var EXCLUDED_FILE_PARTS = [
|
|
4183
|
+
"api-key",
|
|
4184
|
+
"api_key",
|
|
4185
|
+
"apikey",
|
|
4186
|
+
"credential",
|
|
4187
|
+
"credentials",
|
|
4188
|
+
"internal",
|
|
4189
|
+
"internal-link",
|
|
4190
|
+
"password",
|
|
4191
|
+
"passwords",
|
|
4192
|
+
"passwd",
|
|
4193
|
+
"private",
|
|
4194
|
+
"private-url",
|
|
4195
|
+
"secret",
|
|
4196
|
+
"token"
|
|
4197
|
+
];
|
|
4198
|
+
var EXCLUDED_FILE_EXTENSIONS = [".key", ".pem", ".p12", ".pfx"];
|
|
4199
|
+
var DOC_ENTRYPOINTS = new Set(["README.md"]);
|
|
4200
|
+
var DEFAULT_EXCLUDED_GLOBS = [
|
|
4201
|
+
".git/**",
|
|
4202
|
+
"node_modules/**",
|
|
4203
|
+
"dist/**",
|
|
4204
|
+
"build/**",
|
|
4205
|
+
"coverage/**",
|
|
4206
|
+
".env*",
|
|
4207
|
+
"**/*secret*",
|
|
4208
|
+
"**/*token*",
|
|
4209
|
+
"**/*password*",
|
|
4210
|
+
"**/*passwd*",
|
|
4211
|
+
"**/*api-key*",
|
|
4212
|
+
"**/*api_key*",
|
|
4213
|
+
"**/*apikey*",
|
|
4214
|
+
"**/*credential*",
|
|
4215
|
+
"**/*private*",
|
|
4216
|
+
"**/*internal*",
|
|
4217
|
+
"**/*.key",
|
|
4218
|
+
"**/*.pem",
|
|
4219
|
+
".claude/**",
|
|
4220
|
+
".codex/**",
|
|
4221
|
+
".evodev/backups/**"
|
|
4222
|
+
];
|
|
4223
|
+
var PROTECTED_PATTERNS = [
|
|
4224
|
+
"secret",
|
|
4225
|
+
"token",
|
|
4226
|
+
"password",
|
|
4227
|
+
"passwd",
|
|
4228
|
+
"api-key",
|
|
4229
|
+
"credential",
|
|
4230
|
+
"private",
|
|
4231
|
+
"private-url",
|
|
4232
|
+
"internal",
|
|
4233
|
+
"internal-link"
|
|
4234
|
+
];
|
|
4235
|
+
async function createProjectContextPlan(projectDir) {
|
|
4236
|
+
const projectDirStat = await stat7(projectDir);
|
|
4237
|
+
if (!projectDirStat.isDirectory()) {
|
|
4238
|
+
throw new Error(`Project dir is not a directory: ${projectDir}`);
|
|
4239
|
+
}
|
|
4240
|
+
const projectId = createProjectId(projectDir);
|
|
4241
|
+
const privacy = createProjectPrivacy();
|
|
4242
|
+
const files = await collectProjectFileMetadata(projectDir);
|
|
4243
|
+
const commands = await collectProjectCommands(projectDir);
|
|
4244
|
+
const docs = files.filter((file) => file.kind === "file" && DOC_ENTRYPOINTS.has(basename3(file.path))).map((file) => file.path).sort();
|
|
4245
|
+
const index = {
|
|
4246
|
+
version: 1,
|
|
4247
|
+
mode: "metadata-only",
|
|
4248
|
+
projectId,
|
|
4249
|
+
generatedAt: null,
|
|
4250
|
+
rootName: basename3(projectDir),
|
|
4251
|
+
files,
|
|
4252
|
+
docs: { entrypoints: docs },
|
|
4253
|
+
commands: {
|
|
4254
|
+
scripts: commands.scripts,
|
|
4255
|
+
testCommandCandidates: commands.scripts.filter((script) => script.commandClass === "test").map((script) => script.name)
|
|
4256
|
+
},
|
|
4257
|
+
exclusions: privacy.excludedGlobs,
|
|
4258
|
+
sourceContentIncluded: false
|
|
4259
|
+
};
|
|
4260
|
+
const planFiles = await Promise.all(PROJECT_CONTEXT_ALLOWED_RELATIVE_PATHS.map(async (relativePath) => {
|
|
4261
|
+
const absolutePath = join9(projectDir, relativePath);
|
|
4262
|
+
const exists = await pathExists6(absolutePath);
|
|
4263
|
+
return {
|
|
4264
|
+
relativePath,
|
|
4265
|
+
absolutePath,
|
|
4266
|
+
action: exists ? "error" : "create",
|
|
4267
|
+
reason: exists ? "Target already exists; I1 does not overwrite." : "Allowed project context file."
|
|
4268
|
+
};
|
|
4269
|
+
}));
|
|
4270
|
+
const errors = planFiles.filter((file) => file.action === "error").map((file) => `${file.relativePath}: ${file.reason}`);
|
|
4271
|
+
return {
|
|
4272
|
+
projectDir,
|
|
4273
|
+
projectId,
|
|
4274
|
+
files: planFiles,
|
|
4275
|
+
index,
|
|
4276
|
+
commands,
|
|
4277
|
+
privacy,
|
|
4278
|
+
warnings: commands.warnings,
|
|
4279
|
+
errors
|
|
4280
|
+
};
|
|
4281
|
+
}
|
|
4282
|
+
async function writeProjectContext(plan) {
|
|
4283
|
+
if (plan.errors.length > 0) {
|
|
4284
|
+
throw new Error(`Cannot write project context:
|
|
4285
|
+
${plan.errors.join(`
|
|
4286
|
+
`)}`);
|
|
4287
|
+
}
|
|
4288
|
+
const payloads = createProjectContextPayloads(plan);
|
|
4289
|
+
const writtenFiles = [];
|
|
4290
|
+
for (const relativePath of PROJECT_CONTEXT_ALLOWED_RELATIVE_PATHS) {
|
|
4291
|
+
const content = payloads[relativePath];
|
|
4292
|
+
const absolutePath = join9(plan.projectDir, relativePath);
|
|
4293
|
+
await mkdir8(join9(absolutePath, ".."), { recursive: true });
|
|
4294
|
+
await writeFile8(absolutePath, content, { encoding: "utf8", flag: "wx" });
|
|
4295
|
+
writtenFiles.push(relativePath);
|
|
4296
|
+
}
|
|
4297
|
+
return { writtenFiles };
|
|
4298
|
+
}
|
|
4299
|
+
function formatProjectContextPlan(plan, mode) {
|
|
4300
|
+
return [
|
|
4301
|
+
"EvoDev project init",
|
|
4302
|
+
"",
|
|
4303
|
+
`Mode: ${mode}`,
|
|
4304
|
+
`Project: ${plan.projectDir}`,
|
|
4305
|
+
`Project id: ${plan.projectId}`,
|
|
4306
|
+
"",
|
|
4307
|
+
"Plan:",
|
|
4308
|
+
...plan.files.map((file) => ` - ${file.action}: ${file.relativePath} (${file.reason})`),
|
|
4309
|
+
"",
|
|
4310
|
+
"Metadata-only index summary:",
|
|
4311
|
+
` - files: ${plan.index.files.filter((file) => file.kind === "file").length}`,
|
|
4312
|
+
` - directories: ${plan.index.files.filter((file) => file.kind === "directory").length}`,
|
|
4313
|
+
` - docs: ${plan.index.docs.entrypoints.length}`,
|
|
4314
|
+
` - package scripts: ${plan.commands.scripts.length}`,
|
|
4315
|
+
...plan.warnings.map((warning) => `Warning: ${warning}`),
|
|
4316
|
+
...plan.errors.map((error) => `Error: ${error}`)
|
|
4317
|
+
].join(`
|
|
4318
|
+
`);
|
|
4319
|
+
}
|
|
4320
|
+
function createProjectContextPayloads(plan) {
|
|
4321
|
+
const profile = {
|
|
4322
|
+
version: 1,
|
|
4323
|
+
projectId: plan.projectId,
|
|
4324
|
+
displayName: basename3(plan.projectDir),
|
|
4325
|
+
root: { pathPolicy: "local-only" },
|
|
4326
|
+
privacy: {
|
|
4327
|
+
classification: "local-private",
|
|
4328
|
+
metadataOnly: true,
|
|
4329
|
+
sourceContentIncluded: false,
|
|
4330
|
+
rawCommandsIncluded: false
|
|
4331
|
+
}
|
|
4332
|
+
};
|
|
4333
|
+
return {
|
|
4334
|
+
".evodev/project.json": `${JSON.stringify(profile, null, 2)}
|
|
4335
|
+
`,
|
|
4336
|
+
".evodev/profile.md": createProfileMarkdown(plan),
|
|
4337
|
+
".evodev/index.json": `${JSON.stringify(plan.index, null, 2)}
|
|
4338
|
+
`,
|
|
4339
|
+
".evodev/commands.json": `${JSON.stringify(plan.commands, null, 2)}
|
|
4340
|
+
`,
|
|
4341
|
+
".evodev/privacy.json": `${JSON.stringify(plan.privacy, null, 2)}
|
|
4342
|
+
`,
|
|
4343
|
+
".evodev/decisions/README.md": `# Project Decisions
|
|
4344
|
+
|
|
4345
|
+
Record project decisions here.
|
|
4346
|
+
`
|
|
4347
|
+
};
|
|
4348
|
+
}
|
|
4349
|
+
function createProfileMarkdown(plan) {
|
|
4350
|
+
return [
|
|
4351
|
+
`# ${basename3(plan.projectDir)} Project Context`,
|
|
4352
|
+
"",
|
|
4353
|
+
"This project context was generated as metadata-only local state.",
|
|
4354
|
+
"",
|
|
4355
|
+
`- Project id: ${plan.projectId}`,
|
|
4356
|
+
"- Source content included: false",
|
|
4357
|
+
"- Raw package script commands included: false",
|
|
4358
|
+
"- External upload: false",
|
|
4359
|
+
""
|
|
4360
|
+
].join(`
|
|
4361
|
+
`);
|
|
4362
|
+
}
|
|
4363
|
+
async function collectProjectFileMetadata(projectDir) {
|
|
4364
|
+
const files = [];
|
|
4365
|
+
async function visit(dir) {
|
|
4366
|
+
const entries = await readdir5(dir, { withFileTypes: true });
|
|
4367
|
+
for (const entry of entries) {
|
|
4368
|
+
const absolutePath = join9(dir, entry.name);
|
|
4369
|
+
const relativePath = relative3(projectDir, absolutePath).replaceAll("\\", "/");
|
|
4370
|
+
if (shouldExcludePath(relativePath, entry.isDirectory())) {
|
|
4371
|
+
continue;
|
|
4372
|
+
}
|
|
4373
|
+
if (entry.isDirectory()) {
|
|
4374
|
+
files.push({ path: relativePath, kind: "directory" });
|
|
4375
|
+
await visit(absolutePath);
|
|
4376
|
+
continue;
|
|
4377
|
+
}
|
|
4378
|
+
if (entry.isFile()) {
|
|
4379
|
+
const fileStat = await stat7(absolutePath);
|
|
4380
|
+
files.push({ path: relativePath, kind: "file", sizeBytes: fileStat.size });
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
await visit(projectDir);
|
|
4385
|
+
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
4386
|
+
}
|
|
4387
|
+
async function collectProjectCommands(projectDir) {
|
|
4388
|
+
const packageJsonPath = join9(projectDir, "package.json");
|
|
4389
|
+
const warnings = [];
|
|
4390
|
+
if (!await pathExists6(packageJsonPath)) {
|
|
4391
|
+
return { version: 1, metadataOnly: true, rawCommandsIncluded: false, scripts: [], warnings };
|
|
4392
|
+
}
|
|
4393
|
+
let parsed;
|
|
4394
|
+
try {
|
|
4395
|
+
parsed = JSON.parse(await readFile11(packageJsonPath, "utf8"));
|
|
4396
|
+
} catch (error) {
|
|
4397
|
+
warnings.push(`package.json scripts skipped: ${describeError(error)}`);
|
|
4398
|
+
return { version: 1, metadataOnly: true, rawCommandsIncluded: false, scripts: [], warnings };
|
|
4399
|
+
}
|
|
4400
|
+
const scripts = isRecord6(parsed) && isRecord6(parsed.scripts) ? parsed.scripts : {};
|
|
4401
|
+
const summaries = [];
|
|
4402
|
+
for (const [name, value] of Object.entries(scripts).sort(([left], [right]) => left.localeCompare(right))) {
|
|
4403
|
+
if (typeof value !== "string") {
|
|
4404
|
+
continue;
|
|
4405
|
+
}
|
|
4406
|
+
const commandClass = classifyScript(name, value);
|
|
4407
|
+
const sensitiveReason = detectSensitiveScript(`${name} ${value}`);
|
|
4408
|
+
if (sensitiveReason !== null) {
|
|
4409
|
+
const safeName = `redacted-${commandClass}-script`;
|
|
4410
|
+
const warning = `Protected ${commandClass} script name/command omitted because it matched protected pattern: ${sensitiveReason}.`;
|
|
4411
|
+
warnings.push(warning);
|
|
4412
|
+
summaries.push({
|
|
4413
|
+
name: safeName,
|
|
4414
|
+
commandClass,
|
|
4415
|
+
summary: `Protected ${commandClass} script name/command omitted.`,
|
|
4416
|
+
rawCommandStored: false,
|
|
4417
|
+
redacted: true,
|
|
4418
|
+
warning
|
|
4419
|
+
});
|
|
4420
|
+
continue;
|
|
4421
|
+
}
|
|
4422
|
+
summaries.push({
|
|
4423
|
+
name,
|
|
4424
|
+
commandClass,
|
|
4425
|
+
summary: `Safe ${commandClass} script command summary only.`,
|
|
4426
|
+
rawCommandStored: false,
|
|
4427
|
+
redacted: false
|
|
4428
|
+
});
|
|
4429
|
+
}
|
|
4430
|
+
return {
|
|
4431
|
+
version: 1,
|
|
4432
|
+
metadataOnly: true,
|
|
4433
|
+
rawCommandsIncluded: false,
|
|
4434
|
+
scripts: summaries,
|
|
4435
|
+
warnings
|
|
4436
|
+
};
|
|
4437
|
+
}
|
|
4438
|
+
function classifyScript(name, command) {
|
|
4439
|
+
const text = `${name} ${command}`.toLowerCase();
|
|
4440
|
+
if (text.includes("typecheck") || text.includes("tsc"))
|
|
4441
|
+
return "typecheck";
|
|
4442
|
+
if (text.includes("lint") || text.includes("biome") || text.includes("eslint"))
|
|
4443
|
+
return "lint";
|
|
4444
|
+
if (text.includes("test"))
|
|
4445
|
+
return "test";
|
|
4446
|
+
if (text.includes("build"))
|
|
4447
|
+
return "build";
|
|
4448
|
+
if (text.includes("format") || text.includes("prettier"))
|
|
4449
|
+
return "format";
|
|
4450
|
+
if (text.includes("install"))
|
|
4451
|
+
return "install";
|
|
4452
|
+
if (text.includes("release") || text.includes("publish") || text.includes("pack"))
|
|
4453
|
+
return "release";
|
|
4454
|
+
return "other";
|
|
4455
|
+
}
|
|
4456
|
+
function detectSensitiveScript(command) {
|
|
4457
|
+
const lower = command.toLowerCase();
|
|
4458
|
+
if (/(^|[^a-z0-9])(secret|token|password|passwd|api[_-]?key|apikey|credential|credentials|private|internal|private-url|internal-link)([^a-z0-9]|$)/.test(lower))
|
|
4459
|
+
return "protected-name-or-command";
|
|
4460
|
+
if (/https?:\/\//i.test(command))
|
|
4461
|
+
return "url";
|
|
4462
|
+
if (/\b[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}\b/i.test(command))
|
|
4463
|
+
return "email-or-internal-id";
|
|
4464
|
+
return null;
|
|
4465
|
+
}
|
|
4466
|
+
function createProjectPrivacy() {
|
|
4467
|
+
return {
|
|
4468
|
+
version: 1,
|
|
4469
|
+
classification: "local-private",
|
|
4470
|
+
metadataOnly: true,
|
|
4471
|
+
sourceContentIndex: false,
|
|
4472
|
+
promptHistoryIndex: false,
|
|
4473
|
+
shellHistoryIndex: false,
|
|
4474
|
+
rawCommandOutputIndex: false,
|
|
4475
|
+
externalUpload: false,
|
|
4476
|
+
excludedGlobs: DEFAULT_EXCLUDED_GLOBS,
|
|
4477
|
+
protectedPatterns: PROTECTED_PATTERNS
|
|
4478
|
+
};
|
|
4479
|
+
}
|
|
4480
|
+
function createProjectId(projectDir) {
|
|
4481
|
+
return basename3(projectDir).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
4482
|
+
}
|
|
4483
|
+
function shouldExcludePath(relativePath, isDirectory) {
|
|
4484
|
+
const segments = relativePath.split("/");
|
|
4485
|
+
const name = segments.at(-1) ?? relativePath;
|
|
4486
|
+
const lowerName = name.toLowerCase();
|
|
4487
|
+
if (isDirectory && EXCLUDED_DIRECTORY_NAMES.has(name))
|
|
4488
|
+
return true;
|
|
4489
|
+
if (segments.includes(".evodev") && !relativePath.startsWith(".evodev/decisions"))
|
|
4490
|
+
return true;
|
|
4491
|
+
if (EXCLUDED_FILE_PREFIXES.some((prefix) => name.startsWith(prefix)))
|
|
4492
|
+
return true;
|
|
4493
|
+
if (EXCLUDED_FILE_PARTS.some((part) => lowerName.includes(part)))
|
|
4494
|
+
return true;
|
|
4495
|
+
if (EXCLUDED_FILE_EXTENSIONS.some((extension) => lowerName.endsWith(extension)))
|
|
4496
|
+
return true;
|
|
4497
|
+
return false;
|
|
4498
|
+
}
|
|
4499
|
+
async function pathExists6(path) {
|
|
4500
|
+
try {
|
|
4501
|
+
await stat7(path);
|
|
4502
|
+
return true;
|
|
4503
|
+
} catch (error) {
|
|
4504
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
4505
|
+
return false;
|
|
4506
|
+
}
|
|
4507
|
+
throw error;
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
function isRecord6(value) {
|
|
4511
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4512
|
+
}
|
|
4513
|
+
function describeError(error) {
|
|
4514
|
+
return error instanceof Error ? error.message : String(error);
|
|
4515
|
+
}
|
|
4516
|
+
// packages/core/src/sync/orchestrator.ts
|
|
4517
|
+
import { join as join10 } from "node:path";
|
|
4518
|
+
async function runSync(options) {
|
|
4519
|
+
const store = createCoreConfigStore(options.homeDir);
|
|
4520
|
+
const settings = await store.readSettings();
|
|
4521
|
+
const assets = await scanAssets(resolveAssetScannerPaths(options.assetsRootDir));
|
|
4522
|
+
const plugins = getSyncTargetPlugins(settings, options.pluginRegistry, options.targetPlugins);
|
|
4523
|
+
const plans = buildSyncPlans({
|
|
4524
|
+
settings,
|
|
4525
|
+
assets,
|
|
4526
|
+
plugins,
|
|
4527
|
+
dryRun: options.dryRun ?? false,
|
|
4528
|
+
includeSkills: options.includeSkills,
|
|
4529
|
+
includeAgents: options.includeAgents
|
|
4530
|
+
});
|
|
4531
|
+
const results = await executeSyncPlans(plans, plugins);
|
|
4532
|
+
const registry = updateRegistryWithSyncResults(await readRegistryOrDefault(store), assets, results);
|
|
4533
|
+
const syncState = updateSyncStateWithResults(await readSyncStateOrDefault(store), results, options.now?.() ?? new Date().toISOString());
|
|
4534
|
+
if (!(options.dryRun ?? false) && shouldPersistSyncResults(results)) {
|
|
4535
|
+
await store.writeRegistry(registry);
|
|
4536
|
+
await store.writeSyncState(syncState);
|
|
4537
|
+
}
|
|
4538
|
+
return { plans, results, registry, syncState };
|
|
4539
|
+
}
|
|
4540
|
+
function buildSyncPlans(input) {
|
|
4541
|
+
const shouldIncludeSkills = input.includeSkills ?? true;
|
|
4542
|
+
const shouldIncludeAgents = input.includeAgents ?? true;
|
|
4543
|
+
const skills = input.settings.assets.skills.enabled && shouldIncludeSkills ? input.assets.skills : [];
|
|
4544
|
+
const agents = input.settings.assets.agents.enabled && shouldIncludeAgents ? input.assets.agents : [];
|
|
4545
|
+
return input.plugins.map((plugin) => ({
|
|
4546
|
+
targetPlugin: plugin.id,
|
|
4547
|
+
skills: filterAssetsForTarget(skills, plugin.id),
|
|
4548
|
+
agents: filterAssetsForTarget(agents, plugin.id),
|
|
4549
|
+
dryRun: input.dryRun
|
|
4550
|
+
}));
|
|
4551
|
+
}
|
|
4552
|
+
async function executeSyncPlans(plans, plugins) {
|
|
4553
|
+
const pluginById = new Map(plugins.map((plugin) => [plugin.id, plugin]));
|
|
4554
|
+
const results = [];
|
|
4555
|
+
for (const plan of plans) {
|
|
4556
|
+
const plugin = pluginById.get(plan.targetPlugin);
|
|
4557
|
+
if (plugin === undefined) {
|
|
4558
|
+
throw new Error(`Cannot execute sync plan for unregistered plugin: ${plan.targetPlugin}`);
|
|
4559
|
+
}
|
|
4560
|
+
const [skillsResult, agentsResult] = await Promise.all([
|
|
4561
|
+
plugin.syncSkills({
|
|
4562
|
+
targetPlugin: plan.targetPlugin,
|
|
4563
|
+
skills: plan.skills,
|
|
4564
|
+
dryRun: plan.dryRun
|
|
4565
|
+
}),
|
|
4566
|
+
plugin.syncAgents({
|
|
4567
|
+
targetPlugin: plan.targetPlugin,
|
|
4568
|
+
agents: plan.agents,
|
|
4569
|
+
dryRun: plan.dryRun
|
|
4570
|
+
})
|
|
4571
|
+
]);
|
|
4572
|
+
results.push(mergeSyncResults(plan.targetPlugin, skillsResult, agentsResult));
|
|
4573
|
+
}
|
|
4574
|
+
return results;
|
|
4575
|
+
}
|
|
4576
|
+
function mergeSyncResults(targetPlugin, skillsResult, agentsResult) {
|
|
4577
|
+
return {
|
|
4578
|
+
targetPlugin,
|
|
4579
|
+
syncedSkills: unique([...skillsResult.syncedSkills, ...agentsResult.syncedSkills]),
|
|
4580
|
+
syncedAgents: unique([...skillsResult.syncedAgents, ...agentsResult.syncedAgents]),
|
|
4581
|
+
skipped: unique([...skillsResult.skipped, ...agentsResult.skipped]),
|
|
4582
|
+
warnings: [...skillsResult.warnings, ...agentsResult.warnings],
|
|
4583
|
+
errors: [...skillsResult.errors, ...agentsResult.errors]
|
|
4584
|
+
};
|
|
4585
|
+
}
|
|
4586
|
+
function updateRegistryWithSyncResults(registry, assets, results) {
|
|
4587
|
+
const next = {
|
|
4588
|
+
version: 1,
|
|
4589
|
+
skills: { ...registry.skills },
|
|
4590
|
+
agents: { ...registry.agents }
|
|
4591
|
+
};
|
|
4592
|
+
for (const result of results) {
|
|
4593
|
+
for (const key of result.syncedSkills) {
|
|
4594
|
+
const asset = assets.skills.find((candidate) => candidate.registryKey === key);
|
|
4595
|
+
if (asset !== undefined) {
|
|
4596
|
+
next.skills[key] = mergeRegisteredAsset(next.skills[key], asset.manifest.version, result.targetPlugin);
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
for (const key of result.syncedAgents) {
|
|
4600
|
+
const asset = assets.agents.find((candidate) => candidate.registryKey === key);
|
|
4601
|
+
if (asset !== undefined) {
|
|
4602
|
+
next.agents[key] = mergeRegisteredAsset(next.agents[key], asset.manifest.version, result.targetPlugin);
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
return next;
|
|
4607
|
+
}
|
|
4608
|
+
function updateSyncStateWithResults(syncState, results, timestamp) {
|
|
4609
|
+
const next = {
|
|
4610
|
+
version: 1,
|
|
4611
|
+
lastSyncAt: timestamp,
|
|
4612
|
+
targets: { ...syncState.targets }
|
|
4613
|
+
};
|
|
4614
|
+
for (const result of results) {
|
|
4615
|
+
next.targets[result.targetPlugin] = {
|
|
4616
|
+
skills: result.syncedSkills.length,
|
|
4617
|
+
agents: result.syncedAgents.length,
|
|
4618
|
+
status: getSyncStatus(result)
|
|
4619
|
+
};
|
|
4620
|
+
}
|
|
4621
|
+
return next;
|
|
4622
|
+
}
|
|
4623
|
+
function getSyncTargetPlugins(settings, pluginRegistry, targetPlugins) {
|
|
4624
|
+
const enabledPluginIds = getEnabledPluginIds(settings);
|
|
4625
|
+
const selectedPluginIds = targetPlugins === undefined ? enabledPluginIds : unique(targetPlugins).filter((pluginId) => enabledPluginIds.includes(pluginId));
|
|
4626
|
+
return selectedPluginIds.map((pluginId) => pluginRegistry.require(pluginId));
|
|
4627
|
+
}
|
|
4628
|
+
function shouldPersistSyncResults(results) {
|
|
4629
|
+
if (results.length === 0)
|
|
4630
|
+
return false;
|
|
4631
|
+
if (results.every((result) => result.errors.length === 0))
|
|
4632
|
+
return true;
|
|
4633
|
+
return results.some((result) => result.syncedSkills.length > 0 || result.syncedAgents.length > 0);
|
|
4634
|
+
}
|
|
4635
|
+
function getSyncStatus(result) {
|
|
4636
|
+
if (result.errors.length === 0) {
|
|
4637
|
+
return "success";
|
|
4638
|
+
}
|
|
4639
|
+
if (result.syncedSkills.length > 0 || result.syncedAgents.length > 0) {
|
|
4640
|
+
return "partial";
|
|
4641
|
+
}
|
|
4642
|
+
return "failed";
|
|
4643
|
+
}
|
|
4644
|
+
function mergeRegisteredAsset(existing, version, targetPlugin) {
|
|
4645
|
+
return {
|
|
4646
|
+
version,
|
|
4647
|
+
source: existing?.source ?? "builtin",
|
|
4648
|
+
targets: unique([...existing?.targets ?? [], targetPlugin])
|
|
4649
|
+
};
|
|
4650
|
+
}
|
|
4651
|
+
function filterAssetsForTarget(assets, targetPlugin) {
|
|
4652
|
+
return assets.filter((asset) => asset.manifest.targets.includes(targetPlugin));
|
|
4653
|
+
}
|
|
4654
|
+
function resolveAssetScannerPaths(assetsRootDir) {
|
|
4655
|
+
return {
|
|
4656
|
+
skillsDir: join10(assetsRootDir, "skills"),
|
|
4657
|
+
agentsDir: join10(assetsRootDir, "agents")
|
|
4658
|
+
};
|
|
4659
|
+
}
|
|
4660
|
+
async function readRegistryOrDefault(store) {
|
|
4661
|
+
try {
|
|
4662
|
+
return await store.readRegistry();
|
|
4663
|
+
} catch (error) {
|
|
4664
|
+
if (isNotFoundError2(error)) {
|
|
4665
|
+
return createDefaultRegistry();
|
|
4666
|
+
}
|
|
4667
|
+
throw error;
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
async function readSyncStateOrDefault(store) {
|
|
4671
|
+
try {
|
|
4672
|
+
return await store.readSyncState();
|
|
4673
|
+
} catch (error) {
|
|
4674
|
+
if (isNotFoundError2(error)) {
|
|
4675
|
+
return createDefaultSyncState();
|
|
4676
|
+
}
|
|
4677
|
+
throw error;
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4680
|
+
function isNotFoundError2(error) {
|
|
4681
|
+
return error instanceof Error && error.message.includes("ENOENT");
|
|
4682
|
+
}
|
|
4683
|
+
function unique(values) {
|
|
4684
|
+
return [...new Set(values)];
|
|
4685
|
+
}
|
|
4686
|
+
// packages/core/src/workflow/index.ts
|
|
4687
|
+
import { readFile as readFile12, readdir as readdir6 } from "node:fs/promises";
|
|
4688
|
+
import { join as join11 } from "node:path";
|
|
4689
|
+
async function scanWorkflowManifests(workflowsDir) {
|
|
4690
|
+
const manifests = [];
|
|
4691
|
+
const entries = await readdir6(workflowsDir, { withFileTypes: true });
|
|
4692
|
+
for (const entry of entries) {
|
|
4693
|
+
if (!entry.isDirectory())
|
|
4694
|
+
continue;
|
|
4695
|
+
const manifestPath = join11(workflowsDir, entry.name, "WORKFLOW.json");
|
|
4696
|
+
manifests.push(parseWorkflowManifest(JSON.parse(await readFile12(manifestPath, "utf8"))));
|
|
4697
|
+
}
|
|
4698
|
+
return manifests.sort((left, right) => left.id.localeCompare(right.id));
|
|
4699
|
+
}
|
|
4700
|
+
function parseWorkflowManifest(value) {
|
|
4701
|
+
if (!isRecord7(value))
|
|
4702
|
+
throw new Error("Workflow manifest must be an object.");
|
|
4703
|
+
const manifest = value;
|
|
4704
|
+
if (typeof manifest.id !== "string" || typeof manifest.version !== "string") {
|
|
4705
|
+
throw new Error("Workflow manifest missing id/version.");
|
|
4706
|
+
}
|
|
4707
|
+
if (!Array.isArray(manifest.modes) || !Array.isArray(manifest.steps)) {
|
|
4708
|
+
throw new Error(`Workflow manifest ${manifest.id} missing modes/steps.`);
|
|
4709
|
+
}
|
|
4710
|
+
if (manifest.privacy?.metadataOnly !== true || manifest.privacy.usesNetwork !== false || manifest.privacy.storesRawPrompts !== false || manifest.privacy.storesSourceContent !== false || manifest.privacy.storesRawCommandOutput !== false) {
|
|
4711
|
+
throw new Error(`Workflow manifest ${manifest.id} violates privacy requirements.`);
|
|
4712
|
+
}
|
|
4713
|
+
return manifest;
|
|
4714
|
+
}
|
|
4715
|
+
function planWorkflow(input) {
|
|
4716
|
+
const mode = input.contract?.route.mode ?? null;
|
|
4717
|
+
const warnings = [];
|
|
4718
|
+
const blockers = [];
|
|
4719
|
+
if (mode !== null && !input.workflow.modes.includes(mode)) {
|
|
4720
|
+
blockers.push(`Workflow ${input.workflow.id} does not support task mode ${mode}.`);
|
|
4721
|
+
}
|
|
4722
|
+
return {
|
|
4723
|
+
workflow: input.workflow,
|
|
4724
|
+
taskId: input.contract?.taskId,
|
|
4725
|
+
mode,
|
|
4726
|
+
steps: input.workflow.steps.map((step) => ({ ...step, plannedOnly: true })),
|
|
4727
|
+
requiredEvidence: input.workflow.requiredEvidence,
|
|
4728
|
+
warnings,
|
|
4729
|
+
blockers
|
|
4730
|
+
};
|
|
4731
|
+
}
|
|
4732
|
+
function formatWorkflowList(manifests) {
|
|
4733
|
+
return [
|
|
4734
|
+
"EvoDev workflows",
|
|
4735
|
+
"",
|
|
4736
|
+
...manifests.map((workflow) => `- ${workflow.id} (${workflow.modes.join(", ")}): ${workflow.description}`)
|
|
4737
|
+
].join(`
|
|
4738
|
+
`);
|
|
4739
|
+
}
|
|
4740
|
+
function formatWorkflowPlan(plan) {
|
|
4741
|
+
return [
|
|
4742
|
+
"EvoDev workflow dry-run",
|
|
4743
|
+
"",
|
|
4744
|
+
`Workflow: ${plan.workflow.id}`,
|
|
4745
|
+
`Task: ${plan.taskId ?? "none"}`,
|
|
4746
|
+
`Mode: ${plan.mode ?? "not routed"}`,
|
|
4747
|
+
"",
|
|
4748
|
+
"Steps:",
|
|
4749
|
+
...plan.steps.map((step) => ` - ${step.id}: ${step.name} [${step.actor}] evidence=${step.evidence.join(",") || "none"}`),
|
|
4750
|
+
"",
|
|
4751
|
+
"Required evidence:",
|
|
4752
|
+
...plan.requiredEvidence.map((item) => ` - ${item}`),
|
|
4753
|
+
...plan.warnings.map((warning) => `Warning: ${warning}`),
|
|
4754
|
+
...plan.blockers.map((blocker) => `Blocker: ${blocker}`)
|
|
4755
|
+
].join(`
|
|
4756
|
+
`);
|
|
4757
|
+
}
|
|
4758
|
+
function isRecord7(value) {
|
|
4759
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4760
|
+
}
|
|
4761
|
+
export {
|
|
4762
|
+
writeTaskContract,
|
|
4763
|
+
writeProjectContext,
|
|
4764
|
+
writeDaemonState,
|
|
4765
|
+
writeCodexCapabilityVerificationArtifact,
|
|
4766
|
+
verifyTaskContract,
|
|
4767
|
+
validatePack,
|
|
4768
|
+
validateObservabilityEvent,
|
|
4769
|
+
validateLearningReviewDecisionRecord,
|
|
4770
|
+
validateLearningCandidate,
|
|
4771
|
+
validateDaemonBindHost,
|
|
4772
|
+
validateCodexCapabilityVerificationArtifact,
|
|
4773
|
+
validateAgentOutput,
|
|
4774
|
+
scanWorkflowManifests,
|
|
4775
|
+
scanSkillAssets,
|
|
4776
|
+
scanAssets,
|
|
4777
|
+
scanAgentAssets,
|
|
4778
|
+
runSync,
|
|
4779
|
+
runPluginConformance,
|
|
4780
|
+
runDaemonForeground,
|
|
4781
|
+
routeTaskContract,
|
|
4782
|
+
resolveTaskContractOutputPath,
|
|
4783
|
+
resolveObservabilityStorePaths,
|
|
4784
|
+
resolveLearningDecisionPath,
|
|
4785
|
+
resolveLearningCandidateQueuePath,
|
|
4786
|
+
resolveHookRuntimeSessionPaths,
|
|
4787
|
+
resolveEvoDevPaths,
|
|
4788
|
+
resolveDaemonPaths,
|
|
4789
|
+
resolveCodexCapabilityArtifactPath,
|
|
4790
|
+
readTaskContract,
|
|
4791
|
+
readLearningReviewDecisions,
|
|
4792
|
+
readLearningCandidates,
|
|
4793
|
+
readDaemonToken,
|
|
4794
|
+
readDaemonLock,
|
|
4795
|
+
readCodexCapabilityVerificationArtifact,
|
|
4796
|
+
readAgentProfile,
|
|
4797
|
+
planWorkflow,
|
|
4798
|
+
planPackInstallDryRun,
|
|
4799
|
+
pathExists5 as pathExists,
|
|
4800
|
+
parseWorkflowManifest,
|
|
4801
|
+
parseSyncState,
|
|
4802
|
+
parseSkillManifest,
|
|
4803
|
+
parseSettings,
|
|
4804
|
+
parseRegistry,
|
|
4805
|
+
parsePackManifest,
|
|
4806
|
+
parseLearningReviewDecisionRecord,
|
|
4807
|
+
parseLearningCandidate,
|
|
4808
|
+
parseInstallState,
|
|
4809
|
+
parseHookSettings,
|
|
4810
|
+
parseAgentProfile,
|
|
4811
|
+
parseAgentManifest,
|
|
4812
|
+
normalizePackagePath,
|
|
4813
|
+
normalizePackPermissions,
|
|
4814
|
+
normalizeHookEvent,
|
|
4815
|
+
negotiatePluginCapabilities,
|
|
4816
|
+
mergeSettings,
|
|
4817
|
+
mergeReviewFindings,
|
|
4818
|
+
loadAgentContextDryRun,
|
|
4819
|
+
listObservabilityEvents,
|
|
4820
|
+
listLearningReviewDecisions,
|
|
4821
|
+
listLearningCandidates,
|
|
4822
|
+
lintLearningCandidates,
|
|
4823
|
+
isCapabilityUsable,
|
|
4824
|
+
initializeCoreConfig,
|
|
4825
|
+
handleHookRuntime,
|
|
4826
|
+
handleDaemonRequest,
|
|
4827
|
+
getEnabledPluginIds,
|
|
4828
|
+
formatWorkflowPlan,
|
|
4829
|
+
formatWorkflowList,
|
|
4830
|
+
formatTaskContract,
|
|
4831
|
+
formatRetentionDryRun,
|
|
4832
|
+
formatProjectContextPlan,
|
|
4833
|
+
formatPackValidation,
|
|
4834
|
+
formatPackInstallDryRun,
|
|
4835
|
+
formatObservabilityEvents,
|
|
4836
|
+
formatLearningReview,
|
|
4837
|
+
formatLearningLint,
|
|
4838
|
+
formatHookRuntimeOutput,
|
|
4839
|
+
formatHookInstallDryRun,
|
|
4840
|
+
formatHookEventDryRun,
|
|
4841
|
+
formatCodexCapabilityVerificationArtifact,
|
|
4842
|
+
formatAgentContextDryRun,
|
|
4843
|
+
formatAgentComposeDryRun,
|
|
4844
|
+
dryRunObservabilityRetentionCleanup,
|
|
4845
|
+
createUnknownNegotiatedCapabilities,
|
|
4846
|
+
createTaskContract,
|
|
4847
|
+
createProjectContextPlan,
|
|
4848
|
+
createPluginRegistry,
|
|
4849
|
+
createObservabilityEvent,
|
|
4850
|
+
createLearningReviewDecisionRecord,
|
|
4851
|
+
createLearningCandidate,
|
|
4852
|
+
createDefaultSyncState,
|
|
4853
|
+
createDefaultSettings,
|
|
4854
|
+
createDefaultRegistry,
|
|
4855
|
+
createDefaultInstallState,
|
|
4856
|
+
createDefaultHookSettings,
|
|
4857
|
+
createDefaultAgentPermissions,
|
|
4858
|
+
createDaemonStartPlan,
|
|
4859
|
+
createCoreConfigStore,
|
|
4860
|
+
createCodexCapabilityVerificationArtifact,
|
|
4861
|
+
composeAgentDryRun,
|
|
4862
|
+
cleanupDaemonState,
|
|
4863
|
+
classifyCommandRisk,
|
|
4864
|
+
checkProtectedZonePaths,
|
|
4865
|
+
buildSyncPlans,
|
|
4866
|
+
buildLearningReviewEntries,
|
|
4867
|
+
assertNoUnverifiedWrites,
|
|
4868
|
+
applyStrictestAgentPermissions,
|
|
4869
|
+
appendObservabilityEvent,
|
|
4870
|
+
appendLearningReviewDecision,
|
|
4871
|
+
appendLearningCandidate,
|
|
4872
|
+
PluginRegistryError,
|
|
4873
|
+
PluginRegistry,
|
|
4874
|
+
PROJECT_CONTEXT_ALLOWED_RELATIVE_PATHS,
|
|
4875
|
+
EvoDevConfigError,
|
|
4876
|
+
EvoDevAssetError,
|
|
4877
|
+
DEFAULT_PROTECTED_ZONE_RULES,
|
|
4878
|
+
CANONICAL_HOOK_EVENT_TYPES
|
|
4879
|
+
};
|