@vercel/factory 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{engine-BXVTLiXU.d.ts → approval-Z7iaWibT.d.ts} +195 -4
- package/dist/blob/index.d.ts +26 -0
- package/dist/blob/index.js +127 -0
- package/dist/blob/index.js.map +1 -0
- package/dist/chunk-LBEYCOSR.js +351 -0
- package/dist/chunk-LBEYCOSR.js.map +1 -0
- package/dist/connectors/github.d.ts +26 -1
- package/dist/connectors/github.js +62 -0
- package/dist/connectors/github.js.map +1 -1
- package/dist/eve/index.d.ts +459 -4
- package/dist/eve/index.js +506 -33
- package/dist/eve/index.js.map +1 -1
- package/dist/index.d.ts +92 -3
- package/dist/index.js +257 -62
- package/dist/index.js.map +1 -1
- package/dist/next-page.d.ts +5 -0
- package/dist/next-page.js +75 -0
- package/dist/next-page.js.map +1 -0
- package/dist/next.d.ts +12 -0
- package/dist/next.js +45 -0
- package/dist/next.js.map +1 -0
- package/package.json +8 -8
- package/dist/chunk-BSDWPFKE.js +0 -197
- package/dist/chunk-BSDWPFKE.js.map +0 -1
package/dist/eve/index.js
CHANGED
|
@@ -1,56 +1,476 @@
|
|
|
1
1
|
import {
|
|
2
|
+
changeEvidenceSchema,
|
|
2
3
|
changeIdSchema,
|
|
4
|
+
defaultApprovalPolicy,
|
|
5
|
+
dispatchTask,
|
|
6
|
+
findingFingerprint,
|
|
7
|
+
findingSchema,
|
|
3
8
|
levelSchema,
|
|
4
9
|
repositoryIdSchema,
|
|
10
|
+
requiresApproval,
|
|
5
11
|
signalIdSchema,
|
|
6
12
|
taskIdSchema,
|
|
7
13
|
taskQuestionSchema,
|
|
8
14
|
vocabularySchema
|
|
9
|
-
} from "../chunk-
|
|
15
|
+
} from "../chunk-LBEYCOSR.js";
|
|
16
|
+
import "../chunk-CEF6VJZC.js";
|
|
10
17
|
|
|
11
18
|
// src/eve/index.ts
|
|
19
|
+
import { defineHook } from "eve/hooks";
|
|
20
|
+
import { defineTool as defineTool6 } from "eve/tools";
|
|
21
|
+
import { z as z6 } from "zod";
|
|
22
|
+
|
|
23
|
+
// src/eve/deepsec.ts
|
|
24
|
+
import { defineTool as defineTool2 } from "eve/tools";
|
|
25
|
+
import { z as z2 } from "zod";
|
|
26
|
+
|
|
27
|
+
// src/eve/finding.ts
|
|
12
28
|
import { defineTool } from "eve/tools";
|
|
13
29
|
import { z } from "zod";
|
|
14
|
-
|
|
30
|
+
async function recordFinding(stores, repositoryId, finding, options = {}) {
|
|
31
|
+
const source = options.source ?? "security_scan";
|
|
32
|
+
const policy = options.approval ?? defaultApprovalPolicy;
|
|
33
|
+
const repository = await stores.repositories.get(repositoryId);
|
|
34
|
+
if (repository === null) {
|
|
35
|
+
return { recorded: false, reason: `repository ${repositoryId} does not exist` };
|
|
36
|
+
}
|
|
37
|
+
const fingerprint = findingFingerprint(repositoryId, finding);
|
|
38
|
+
const signal = await stores.signals.create({
|
|
39
|
+
repositoryId,
|
|
40
|
+
source,
|
|
41
|
+
deliveryId: fingerprint,
|
|
42
|
+
replyTo: { channel: "finding", address: fingerprint },
|
|
43
|
+
payload: finding
|
|
44
|
+
});
|
|
45
|
+
if (signal.outcome !== "pending") {
|
|
46
|
+
return { recorded: false, duplicate: true, signalId: signal.id, reason: `already recorded as ${signal.id}` };
|
|
47
|
+
}
|
|
48
|
+
await stores.signals.decide(signal.id, "accepted", {
|
|
49
|
+
detail: finding.priority,
|
|
50
|
+
reason: `${finding.tool ?? "scanner"} reported it, ${finding.verification.level} by ${finding.verification.method}`
|
|
51
|
+
});
|
|
52
|
+
const approval = requiresApproval(policy, { source, kind: "code_change", priority: finding.priority });
|
|
53
|
+
const task = await stores.tasks.create({
|
|
54
|
+
repositoryIds: [repositoryId],
|
|
55
|
+
kind: "code_change",
|
|
56
|
+
origin: { signalId: signal.id },
|
|
57
|
+
replyTo: signal.replyTo,
|
|
58
|
+
...approval ? { approval: "required" } : {}
|
|
59
|
+
});
|
|
60
|
+
return { recorded: true, signalId: signal.id, taskId: task.id, approval: approval ? "required" : "not_required" };
|
|
61
|
+
}
|
|
62
|
+
function recordFindingTool(stores, options = {}) {
|
|
63
|
+
return defineTool({
|
|
64
|
+
description: "Record one problem you found in the repository. Creates a fix task (which may wait for human approval) unless the same finding was already recorded. Report every finding separately; never batch.",
|
|
65
|
+
inputSchema: z.object({
|
|
66
|
+
repositoryId: repositoryIdSchema,
|
|
67
|
+
finding: findingSchema
|
|
68
|
+
}),
|
|
69
|
+
execute({ repositoryId, finding }) {
|
|
70
|
+
return recordFinding(stores, repositoryId, finding, options);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/eve/deepsec.ts
|
|
76
|
+
var deepsecFindingSchema = z2.looseObject({
|
|
77
|
+
title: z2.string().min(1),
|
|
78
|
+
description: z2.string().min(1),
|
|
79
|
+
severity: z2.string().min(1),
|
|
80
|
+
metadata: z2.looseObject({
|
|
81
|
+
filePath: z2.string().min(1).optional(),
|
|
82
|
+
lineNumbers: z2.array(z2.number().int().min(1)).optional(),
|
|
83
|
+
vulnSlug: z2.string().min(1).optional(),
|
|
84
|
+
confidence: z2.string().optional(),
|
|
85
|
+
runId: z2.string().optional()
|
|
86
|
+
}).optional()
|
|
87
|
+
});
|
|
88
|
+
var deepsecExportSchema = z2.union([
|
|
89
|
+
z2.array(deepsecFindingSchema),
|
|
90
|
+
z2.looseObject({ findings: z2.array(deepsecFindingSchema) }).transform((value) => value.findings)
|
|
91
|
+
]);
|
|
92
|
+
var priorityBySeverity = {
|
|
93
|
+
CRITICAL: "P0",
|
|
94
|
+
HIGH: "P1",
|
|
95
|
+
MEDIUM: "P2",
|
|
96
|
+
HIGH_BUG: "P2",
|
|
97
|
+
BUG: "P3",
|
|
98
|
+
LOW: "P3"
|
|
99
|
+
};
|
|
100
|
+
function findingFromDeepsec(raw) {
|
|
101
|
+
const meta = raw.metadata ?? {};
|
|
102
|
+
const confidence = (meta.confidence ?? "").toLowerCase();
|
|
103
|
+
const line = meta.lineNumbers?.[0];
|
|
104
|
+
const endLine = meta.lineNumbers === void 0 ? void 0 : Math.max(...meta.lineNumbers);
|
|
105
|
+
return findingSchema.parse({
|
|
106
|
+
title: raw.title.replace(/^\[[A-Z_]+\]\s*/u, "").slice(0, 200),
|
|
107
|
+
body: raw.description.slice(0, 8e3),
|
|
108
|
+
...meta.filePath === void 0 ? {} : { path: meta.filePath.slice(0, 500) },
|
|
109
|
+
...line === void 0 ? {} : { line },
|
|
110
|
+
...endLine === void 0 || endLine === line ? {} : { endLine },
|
|
111
|
+
priority: priorityBySeverity[raw.severity.toUpperCase()] ?? "P3",
|
|
112
|
+
category: "security",
|
|
113
|
+
verification: {
|
|
114
|
+
// deepsec's agent reads the code; it does not run it. Say so.
|
|
115
|
+
level: confidence === "high" ? "supported" : "unverified",
|
|
116
|
+
method: "static_analysis",
|
|
117
|
+
summary: `deepsec ${meta.vulnSlug ?? "finding"} with ${confidence || "unknown"} confidence${meta.runId === void 0 ? "" : ` (run ${meta.runId})`}.`,
|
|
118
|
+
limitations: "AI-assisted static analysis of the checkout; not reproduced at runtime."
|
|
119
|
+
},
|
|
120
|
+
tool: "deepsec",
|
|
121
|
+
...meta.vulnSlug === void 0 ? {} : { ruleId: meta.vulnSlug }
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
var findingsPath = "/tmp/deepsec-findings.json";
|
|
125
|
+
function candidateCount(output) {
|
|
126
|
+
const match = /(\d+)\s+file\(s\) with candidates/u.exec(output);
|
|
127
|
+
return match?.[1] === void 0 ? null : Number(match[1]);
|
|
128
|
+
}
|
|
129
|
+
function tail(output, lines = 4) {
|
|
130
|
+
const ansi = /\u001b\[[0-9;]*m/gu;
|
|
131
|
+
return output.replaceAll(ansi, "").split("\n").map((line) => line.trim()).filter((line) => line !== "").slice(-lines).join(" | ").slice(0, 600);
|
|
132
|
+
}
|
|
133
|
+
function deepsecScanTool(stores, options) {
|
|
134
|
+
const repoPath = options.repoPath ?? "repo";
|
|
135
|
+
const workspace = options.workspacePath ?? "deepsec";
|
|
136
|
+
const version = options.version ?? "2.3.8";
|
|
137
|
+
const agent = options.agent ?? "claude";
|
|
138
|
+
const model = options.model ?? "claude-sonnet-4-6";
|
|
139
|
+
const thinkingLevel = options.thinkingLevel ?? "medium";
|
|
140
|
+
const concurrency = options.concurrency ?? 4;
|
|
141
|
+
const prefix = options.commandPrefix === void 0 ? [] : [options.commandPrefix];
|
|
142
|
+
const filesPerRun = options.filesPerRun ?? 40;
|
|
143
|
+
return defineTool2({
|
|
144
|
+
description: "Run deepsec, the AI-powered vulnerability scanner, over the repository checkout and record every finding it reports as a fix task. Takes minutes. Call once per scan; then summarize the result.",
|
|
145
|
+
inputSchema: z2.object({
|
|
146
|
+
repositoryId: repositoryIdSchema,
|
|
147
|
+
limit: z2.number().int().min(1).max(500).optional().describe("Candidate files to investigate this run (default 40; deepsec resumes where it left off next time).")
|
|
148
|
+
}),
|
|
149
|
+
async execute({ repositoryId, limit }, ctx) {
|
|
150
|
+
const repository = await stores.repositories.get(repositoryId);
|
|
151
|
+
if (repository === null) {
|
|
152
|
+
return { scanned: false, reason: `repository ${repositoryId} does not exist` };
|
|
153
|
+
}
|
|
154
|
+
const sandbox = await ctx.getSandbox();
|
|
155
|
+
const projectId = repository.slug.replace(/[^a-z0-9]+/gu, "-");
|
|
156
|
+
const startedAt = Date.now();
|
|
157
|
+
const phase = async (state, reason) => {
|
|
158
|
+
await stores.receipts.append({
|
|
159
|
+
repositoryIds: [repositoryId],
|
|
160
|
+
kind: "deepsec",
|
|
161
|
+
state,
|
|
162
|
+
phase: "security_scan",
|
|
163
|
+
reason: `${reason} (${Math.round((Date.now() - startedAt) / 1e3)}s in)`
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
await phase("started", `deepsec ${version} on ${repository.slug}, ${agent}/${model}`);
|
|
167
|
+
const prepare = await sandbox.run({
|
|
168
|
+
command: [
|
|
169
|
+
...prefix,
|
|
170
|
+
"set -e",
|
|
171
|
+
`if [ ! -f ${workspace}/deepsec.config.ts ]; then`,
|
|
172
|
+
` npx -y deepsec@${version} init ./${workspace} ./${repoPath} --id ${projectId} --scaffold-only --headless --yes --skip-install`,
|
|
173
|
+
` (cd ${workspace} && npm install --no-audit --no-fund --loglevel=error)`,
|
|
174
|
+
"fi",
|
|
175
|
+
// deepsec resolves deepsec.config.ts from the current directory, so
|
|
176
|
+
// every deepsec command runs inside the workspace. No --root: it is a
|
|
177
|
+
// one-off override that skips the project's file records, leaving
|
|
178
|
+
// `process` with nothing to investigate.
|
|
179
|
+
`cd ${workspace}`,
|
|
180
|
+
`./node_modules/.bin/deepsec scan --project-id ${projectId}`
|
|
181
|
+
].join("\n")
|
|
182
|
+
});
|
|
183
|
+
if (typeof prepare.exitCode === "number" && prepare.exitCode !== 0) {
|
|
184
|
+
const detail = (prepare.stderr ?? prepare.stdout).trim().slice(-1500);
|
|
185
|
+
await phase("failed", `scan phase failed: ${detail.slice(-300)}`);
|
|
186
|
+
return { scanned: false, reason: `deepsec scan failed: ${detail}` };
|
|
187
|
+
}
|
|
188
|
+
const candidates = candidateCount(prepare.stdout);
|
|
189
|
+
await phase(
|
|
190
|
+
"scanned",
|
|
191
|
+
`${candidates === null ? "scan output" : `${candidates} files with candidates`}: ${tail(`${prepare.stdout}
|
|
192
|
+
${prepare.stderr ?? ""}`)}`
|
|
193
|
+
);
|
|
194
|
+
const investigate = await options.withModelCredential(
|
|
195
|
+
sandbox,
|
|
196
|
+
[
|
|
197
|
+
...prefix,
|
|
198
|
+
"set -e",
|
|
199
|
+
`cd ${workspace}`,
|
|
200
|
+
`./node_modules/.bin/deepsec process --project-id ${projectId} --agent ${agent} --model ${model} --thinking-level ${thinkingLevel} --concurrency ${concurrency} --limit ${limit ?? filesPerRun}`,
|
|
201
|
+
`./node_modules/.bin/deepsec export --project-id ${projectId} --format json --out ${findingsPath}`
|
|
202
|
+
].join("\n")
|
|
203
|
+
);
|
|
204
|
+
if (typeof investigate.exitCode === "number" && investigate.exitCode !== 0) {
|
|
205
|
+
const detail = (investigate.stderr ?? investigate.stdout).trim().slice(-1500);
|
|
206
|
+
await phase("failed", `process phase failed: ${detail.slice(-300)}`);
|
|
207
|
+
return { scanned: false, reason: `deepsec process failed: ${detail}` };
|
|
208
|
+
}
|
|
209
|
+
const exported = await sandbox.run({ command: `cat ${findingsPath}` });
|
|
210
|
+
let parsed;
|
|
211
|
+
try {
|
|
212
|
+
parsed = JSON.parse(exported.stdout);
|
|
213
|
+
} catch {
|
|
214
|
+
return { scanned: false, reason: "deepsec export was not valid JSON" };
|
|
215
|
+
}
|
|
216
|
+
const raw = deepsecExportSchema.safeParse(parsed);
|
|
217
|
+
if (!raw.success) {
|
|
218
|
+
return { scanned: false, reason: `deepsec export had an unexpected shape: ${raw.error.issues[0]?.message ?? "unknown"}` };
|
|
219
|
+
}
|
|
220
|
+
const results = [];
|
|
221
|
+
for (const entry of raw.data) {
|
|
222
|
+
const finding = findingFromDeepsec(entry);
|
|
223
|
+
results.push({ finding, result: await recordFinding(stores, repositoryId, finding, options) });
|
|
224
|
+
}
|
|
225
|
+
const recorded = results.filter(({ result }) => result.recorded);
|
|
226
|
+
await phase(
|
|
227
|
+
"completed",
|
|
228
|
+
`${results.length} findings, ${recorded.length} new. process output: ${tail(`${investigate.stdout}
|
|
229
|
+
${investigate.stderr ?? ""}`, 6)}`
|
|
230
|
+
);
|
|
231
|
+
return {
|
|
232
|
+
scanned: true,
|
|
233
|
+
tool: "deepsec",
|
|
234
|
+
found: results.length,
|
|
235
|
+
recorded: recorded.length,
|
|
236
|
+
duplicates: results.length - recorded.length,
|
|
237
|
+
awaitingApproval: recorded.filter(({ result }) => result.recorded && result.approval === "required").length,
|
|
238
|
+
findings: results.map(({ finding, result }) => ({
|
|
239
|
+
title: finding.title,
|
|
240
|
+
priority: finding.priority,
|
|
241
|
+
path: finding.path,
|
|
242
|
+
line: finding.line,
|
|
243
|
+
...result.recorded ? { taskId: result.taskId, approval: result.approval } : { duplicate: true }
|
|
244
|
+
}))
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/eve/fetch.ts
|
|
251
|
+
import { defineTool as defineTool3 } from "eve/tools";
|
|
252
|
+
import { z as z3 } from "zod";
|
|
253
|
+
function fetchPullRequestTool(stores, options) {
|
|
254
|
+
const repoPath = options.repoPath ?? "repo";
|
|
255
|
+
const depth = options.depth ?? 50;
|
|
256
|
+
return defineTool3({
|
|
257
|
+
description: "Fetch the pull request under review into the checkout and check it out as the local branch `pr`. Call this after get_task, before inspecting or testing the change. Returns the PR number, URL, and the commit you are now on.",
|
|
258
|
+
inputSchema: z3.object({
|
|
259
|
+
changeId: changeIdSchema.describe("The change under review, from your starting message.")
|
|
260
|
+
}),
|
|
261
|
+
async execute({ changeId }, ctx) {
|
|
262
|
+
const change = await stores.changes.get(changeId);
|
|
263
|
+
if (change === null) {
|
|
264
|
+
return { fetched: false, reason: `change ${changeId} does not exist` };
|
|
265
|
+
}
|
|
266
|
+
if (change.state !== "verifying") {
|
|
267
|
+
return { fetched: false, reason: `change is ${change.state}, not verifying; nothing to fetch` };
|
|
268
|
+
}
|
|
269
|
+
const repository = await stores.repositories.get(change.repositoryId);
|
|
270
|
+
if (repository === null) {
|
|
271
|
+
return { fetched: false, reason: `repository ${change.repositoryId} does not exist` };
|
|
272
|
+
}
|
|
273
|
+
const sandbox = await ctx.getSandbox();
|
|
274
|
+
const tracking = `refs/remotes/origin/pr-${change.prNumber}`;
|
|
275
|
+
const git = `git -C ${repoPath}`;
|
|
276
|
+
const result = await options.withCredential(
|
|
277
|
+
sandbox,
|
|
278
|
+
repository.slug,
|
|
279
|
+
`${git} fetch --depth ${depth} origin +refs/pull/${change.prNumber}/head:${tracking} && ${git} checkout -B pr ${tracking}`
|
|
280
|
+
);
|
|
281
|
+
if (typeof result.exitCode === "number" && result.exitCode !== 0) {
|
|
282
|
+
return {
|
|
283
|
+
fetched: false,
|
|
284
|
+
reason: `git fetch of pull request ${change.prNumber} failed: ${(result.stderr ?? "").trim()}`
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const head = await sandbox.run({ command: `${git} rev-parse HEAD` });
|
|
288
|
+
return {
|
|
289
|
+
fetched: true,
|
|
290
|
+
prNumber: change.prNumber,
|
|
291
|
+
prUrl: change.prUrl,
|
|
292
|
+
branch: "pr",
|
|
293
|
+
headSha: head.stdout.trim()
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/eve/invoke.ts
|
|
300
|
+
import { defineTool as defineTool4 } from "eve/tools";
|
|
301
|
+
import { z as z4 } from "zod";
|
|
302
|
+
function invokeAgentTool(stores, options) {
|
|
303
|
+
const doFetch = options.fetch ?? fetch;
|
|
304
|
+
return defineTool4({
|
|
305
|
+
description: `${options.description} Hands the work to a separate agent in a fresh session; you will not see its reasoning. Returns the handoff task id.`,
|
|
306
|
+
inputSchema: z4.object({
|
|
307
|
+
repositoryIds: z4.array(repositoryIdSchema).min(1),
|
|
308
|
+
parentTaskId: taskIdSchema.optional().describe("Your own task, when this work continues it."),
|
|
309
|
+
changeId: changeIdSchema.optional().describe("The change the target agent should work on."),
|
|
310
|
+
channel: z4.string().min(1).describe("Reply connector, e.g. github."),
|
|
311
|
+
address: z4.string().min(1).describe("Reply address, e.g. acme/web#41."),
|
|
312
|
+
brief: z4.string().min(1).max(4e3).describe("What the target agent needs to know. Facts and references only, not your reasoning.")
|
|
313
|
+
}),
|
|
314
|
+
async execute(input) {
|
|
315
|
+
const task = await stores.tasks.create({
|
|
316
|
+
repositoryIds: input.repositoryIds,
|
|
317
|
+
kind: options.kind,
|
|
318
|
+
origin: input.parentTaskId === void 0 ? { operator: "factory" } : { parentTaskId: input.parentTaskId },
|
|
319
|
+
replyTo: { channel: input.channel, address: input.address }
|
|
320
|
+
});
|
|
321
|
+
const running = await dispatchTask(stores, task.id, {
|
|
322
|
+
launch: async (dispatched) => {
|
|
323
|
+
const headers = {
|
|
324
|
+
"content-type": "application/json",
|
|
325
|
+
...await options.headers?.()
|
|
326
|
+
};
|
|
327
|
+
const token = await options.token?.();
|
|
328
|
+
if (token !== void 0) {
|
|
329
|
+
headers.authorization = `Bearer ${token}`;
|
|
330
|
+
}
|
|
331
|
+
const message = [
|
|
332
|
+
`You are assigned factory task ${dispatched.id}.`,
|
|
333
|
+
input.changeId === void 0 ? null : `Change under review: ${input.changeId}.`,
|
|
334
|
+
`Reply address: ${input.channel} ${input.address}.`,
|
|
335
|
+
"",
|
|
336
|
+
input.brief,
|
|
337
|
+
"",
|
|
338
|
+
"Read the task with get_task first, do the work, and record the outcome with your factory tools."
|
|
339
|
+
].filter((line) => line !== null).join("\n");
|
|
340
|
+
const response = await doFetch(`${options.targetUrl()}/eve/v1/session`, {
|
|
341
|
+
method: "POST",
|
|
342
|
+
headers,
|
|
343
|
+
body: JSON.stringify({ message, operationId: `factory-task:${dispatched.id}` })
|
|
344
|
+
});
|
|
345
|
+
if (!response.ok) {
|
|
346
|
+
throw new Error(`target agent refused the session: ${response.status}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
return { taskId: running.id, kind: running.kind, state: running.state };
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/eve/verification.ts
|
|
356
|
+
import { defineTool as defineTool5 } from "eve/tools";
|
|
357
|
+
import { z as z5 } from "zod";
|
|
358
|
+
function recordVerificationTool(stores) {
|
|
359
|
+
return defineTool5({
|
|
360
|
+
description: "Record the outcome of a verification task: the evidence you gathered and whether the change passed. Pass moves the change to ready for human review; fail keeps it out of review.",
|
|
361
|
+
inputSchema: z5.object({
|
|
362
|
+
taskId: taskIdSchema.describe("Your verification task."),
|
|
363
|
+
changeId: changeIdSchema.describe("The change you verified."),
|
|
364
|
+
verdict: z5.enum(["pass", "fail"]),
|
|
365
|
+
evidence: changeEvidenceSchema,
|
|
366
|
+
confidence: levelSchema.describe("How certain you are in the verdict."),
|
|
367
|
+
summary: z5.string().min(1).max(2e3).describe("What you checked and what you found.")
|
|
368
|
+
}),
|
|
369
|
+
async execute({ taskId, changeId, verdict, evidence, confidence, summary }) {
|
|
370
|
+
const change = await stores.changes.get(changeId);
|
|
371
|
+
if (change === null) {
|
|
372
|
+
return { recorded: false, reason: `change ${changeId} does not exist` };
|
|
373
|
+
}
|
|
374
|
+
if (change.state !== "verifying") {
|
|
375
|
+
return {
|
|
376
|
+
recorded: false,
|
|
377
|
+
reason: `change is ${change.state}, not verifying; nothing to record`
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const task = await stores.tasks.get(taskId);
|
|
381
|
+
if (task === null || task.kind !== "verification") {
|
|
382
|
+
return { recorded: false, reason: `${taskId} is not a verification task` };
|
|
383
|
+
}
|
|
384
|
+
let updatedChange = change;
|
|
385
|
+
if (verdict === "pass") {
|
|
386
|
+
updatedChange = await stores.changes.transition(changeId, "ready", {
|
|
387
|
+
patch: { evidence, confidence },
|
|
388
|
+
reason: summary
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
let finished;
|
|
392
|
+
if (verdict === "pass") {
|
|
393
|
+
await stores.tasks.transition(taskId, "verifying", { reason: summary });
|
|
394
|
+
finished = await stores.tasks.transition(taskId, "succeeded");
|
|
395
|
+
} else {
|
|
396
|
+
finished = await stores.tasks.transition(taskId, "failed", { reason: summary });
|
|
397
|
+
}
|
|
398
|
+
if (change.taskId !== void 0) {
|
|
399
|
+
const parent = await stores.tasks.get(change.taskId);
|
|
400
|
+
if (parent?.state === "verifying") {
|
|
401
|
+
await stores.tasks.transition(
|
|
402
|
+
change.taskId,
|
|
403
|
+
verdict === "pass" ? "succeeded" : "failed",
|
|
404
|
+
verdict === "pass" ? {} : { reason: `verification failed: ${summary}` }
|
|
405
|
+
).catch(() => void 0);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
recorded: true,
|
|
410
|
+
verdict,
|
|
411
|
+
changeState: updatedChange.state,
|
|
412
|
+
taskState: finished.state
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/eve/index.ts
|
|
419
|
+
var decidedOutcomeSchema = z6.enum([
|
|
15
420
|
"accepted",
|
|
16
421
|
"duplicate",
|
|
17
422
|
"unactionable",
|
|
18
423
|
"low_confidence",
|
|
19
424
|
"ignored"
|
|
20
425
|
]);
|
|
21
|
-
|
|
426
|
+
var recordChangeInputSchema = z6.object({
|
|
427
|
+
taskId: taskIdSchema,
|
|
428
|
+
repositoryId: repositoryIdSchema,
|
|
429
|
+
prNumber: z6.number().int().min(1),
|
|
430
|
+
prUrl: z6.url(),
|
|
431
|
+
risk: levelSchema.describe("Blast radius of the change if it is wrong."),
|
|
432
|
+
confidence: levelSchema.describe("How certain you are the change is correct."),
|
|
433
|
+
summary: z6.string().min(1).describe("One-sentence description of the change.")
|
|
434
|
+
});
|
|
435
|
+
function factoryTools(stores, options = {}) {
|
|
436
|
+
const recordChangeGate = options.recordChangeGate;
|
|
22
437
|
return {
|
|
23
|
-
get_task:
|
|
438
|
+
get_task: defineTool6({
|
|
24
439
|
description: "Read one factory task by id: its kind, state, repositories, origin, and results.",
|
|
25
|
-
inputSchema:
|
|
440
|
+
inputSchema: z6.object({ taskId: taskIdSchema }),
|
|
26
441
|
async execute({ taskId }) {
|
|
27
442
|
const task = await stores.tasks.get(taskId);
|
|
28
443
|
return task ?? { found: false, taskId };
|
|
29
444
|
}
|
|
30
445
|
}),
|
|
31
|
-
decide_signal:
|
|
446
|
+
decide_signal: defineTool6({
|
|
32
447
|
description: "Record the triage decision for a pending signal. A signal is decided exactly once.",
|
|
33
|
-
inputSchema:
|
|
448
|
+
inputSchema: z6.object({
|
|
34
449
|
signalId: signalIdSchema,
|
|
35
450
|
outcome: decidedOutcomeSchema,
|
|
36
|
-
reason:
|
|
37
|
-
detail:
|
|
451
|
+
reason: z6.string().min(1).describe("Why this outcome was chosen."),
|
|
452
|
+
detail: z6.string().min(1).optional().describe("Severity if accepted, or the duplicate-of signal id.")
|
|
38
453
|
}),
|
|
39
454
|
execute({ signalId, outcome, reason, detail }) {
|
|
40
455
|
return stores.signals.decide(signalId, outcome, { reason, detail });
|
|
41
456
|
}
|
|
42
457
|
}),
|
|
43
|
-
record_change:
|
|
458
|
+
record_change: defineTool6({
|
|
44
459
|
description: "Record the pull request produced for a code_change task. Creates the change, links it to the task, and moves the task to verifying.",
|
|
45
|
-
inputSchema:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
460
|
+
inputSchema: recordChangeInputSchema,
|
|
461
|
+
...recordChangeGate === void 0 ? {} : {
|
|
462
|
+
approval: async ({ toolInput }) => {
|
|
463
|
+
const parsed = recordChangeInputSchema.safeParse(toolInput);
|
|
464
|
+
if (!parsed.success) {
|
|
465
|
+
return {
|
|
466
|
+
type: "denied",
|
|
467
|
+
reason: "record_change input is malformed; fix the arguments and retry"
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
const verdict = await recordChangeGate(parsed.data);
|
|
471
|
+
return verdict.ok ? "not-applicable" : { type: "denied", reason: verdict.reason };
|
|
472
|
+
}
|
|
473
|
+
},
|
|
54
474
|
async execute({ taskId, repositoryId, prNumber, prUrl, risk, confidence, summary }) {
|
|
55
475
|
const change = await stores.changes.create({
|
|
56
476
|
repositoryId,
|
|
@@ -75,23 +495,45 @@ function factoryTools(stores) {
|
|
|
75
495
|
return { change, task };
|
|
76
496
|
}
|
|
77
497
|
}),
|
|
78
|
-
|
|
498
|
+
finish_task: defineTool6({
|
|
499
|
+
description: "Mark your task finished with a summary, for task kinds that have no other outcome tool (for example a security_scan). Not for code_change or verification tasks: those end with record_change and record_verification.",
|
|
500
|
+
inputSchema: z6.object({
|
|
501
|
+
taskId: taskIdSchema,
|
|
502
|
+
summary: z6.string().min(1).max(4e3).describe("What you did and found, for the ledger.")
|
|
503
|
+
}),
|
|
504
|
+
async execute({ taskId, summary }) {
|
|
505
|
+
const task = await stores.tasks.get(taskId);
|
|
506
|
+
if (task === null) {
|
|
507
|
+
return { finished: false, reason: `${taskId} does not exist` };
|
|
508
|
+
}
|
|
509
|
+
if (task.kind === "code_change" || task.kind === "verification") {
|
|
510
|
+
return { finished: false, reason: `${task.kind} tasks end with their own outcome tool` };
|
|
511
|
+
}
|
|
512
|
+
if (task.state !== "running") {
|
|
513
|
+
return { finished: false, reason: `task is ${task.state}, not running` };
|
|
514
|
+
}
|
|
515
|
+
await stores.tasks.transition(taskId, "verifying", { reason: summary });
|
|
516
|
+
const finished = await stores.tasks.transition(taskId, "succeeded", { reason: summary });
|
|
517
|
+
return { finished: true, taskState: finished.state };
|
|
518
|
+
}
|
|
519
|
+
}),
|
|
520
|
+
report_blocked: defineTool6({
|
|
79
521
|
description: "Mark a running task as needing a human because the work cannot continue without help.",
|
|
80
|
-
inputSchema:
|
|
522
|
+
inputSchema: z6.object({
|
|
81
523
|
taskId: taskIdSchema,
|
|
82
|
-
reason:
|
|
524
|
+
reason: z6.string().min(1).describe("What is blocking, written for the human.")
|
|
83
525
|
}),
|
|
84
526
|
execute({ taskId, reason }) {
|
|
85
527
|
return stores.tasks.transition(taskId, "needs_human", { reason });
|
|
86
528
|
}
|
|
87
529
|
}),
|
|
88
|
-
ask_question:
|
|
530
|
+
ask_question: defineTool6({
|
|
89
531
|
description: "Raise a question a human operator must answer. Creates a question task that waits for the answer.",
|
|
90
|
-
inputSchema:
|
|
91
|
-
repositoryIds:
|
|
532
|
+
inputSchema: z6.object({
|
|
533
|
+
repositoryIds: z6.array(repositoryIdSchema).min(1),
|
|
92
534
|
parentTaskId: taskIdSchema.optional().describe("Task the answer unblocks, when there is one."),
|
|
93
|
-
channel:
|
|
94
|
-
address:
|
|
535
|
+
channel: z6.string().min(1).describe("Reply connector, e.g. github."),
|
|
536
|
+
address: z6.string().min(1).describe("Reply address, e.g. acme/web#41."),
|
|
95
537
|
question: taskQuestionSchema.omit({ answer: true, answeredBy: true })
|
|
96
538
|
}),
|
|
97
539
|
async execute({ repositoryIds, parentTaskId, channel, address, question }) {
|
|
@@ -105,16 +547,16 @@ function factoryTools(stores) {
|
|
|
105
547
|
return stores.tasks.transition(task.id, "needs_human");
|
|
106
548
|
}
|
|
107
549
|
}),
|
|
108
|
-
append_receipt:
|
|
550
|
+
append_receipt: defineTool6({
|
|
109
551
|
description: "Append one immutable audit receipt for an event worth remembering. Receipts can never be edited or deleted.",
|
|
110
|
-
inputSchema:
|
|
111
|
-
repositoryIds:
|
|
552
|
+
inputSchema: z6.object({
|
|
553
|
+
repositoryIds: z6.array(repositoryIdSchema).min(1),
|
|
112
554
|
taskId: taskIdSchema.optional(),
|
|
113
555
|
changeId: changeIdSchema.optional(),
|
|
114
556
|
kind: vocabularySchema.describe("What the receipt is about, e.g. investigation."),
|
|
115
557
|
state: vocabularySchema.describe("What happened, e.g. flag_ramped."),
|
|
116
|
-
reason:
|
|
117
|
-
dedupeKey:
|
|
558
|
+
reason: z6.string().min(1).max(2e3).optional(),
|
|
559
|
+
dedupeKey: z6.string().min(1).optional().describe("Same key twice records one receipt instead of two.")
|
|
118
560
|
}),
|
|
119
561
|
execute(input) {
|
|
120
562
|
return stores.receipts.append(input);
|
|
@@ -122,7 +564,38 @@ function factoryTools(stores) {
|
|
|
122
564
|
})
|
|
123
565
|
};
|
|
124
566
|
}
|
|
567
|
+
function factoryHooks(stores) {
|
|
568
|
+
return {
|
|
569
|
+
factory: defineHook({
|
|
570
|
+
events: {
|
|
571
|
+
async "turn.completed"(_event, ctx) {
|
|
572
|
+
const address = ctx.channel.continuationToken;
|
|
573
|
+
if (address === void 0) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const tasks = await stores.tasks.list();
|
|
577
|
+
const abandoned = tasks.filter(
|
|
578
|
+
(task) => task.state === "running" && task.replyTo.address === address
|
|
579
|
+
);
|
|
580
|
+
for (const task of abandoned) {
|
|
581
|
+
await stores.tasks.transition(task.id, "needs_human", {
|
|
582
|
+
reason: "agent turn ended without recording an outcome"
|
|
583
|
+
}).catch(() => void 0);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
})
|
|
588
|
+
};
|
|
589
|
+
}
|
|
125
590
|
export {
|
|
126
|
-
|
|
591
|
+
deepsecScanTool,
|
|
592
|
+
factoryHooks,
|
|
593
|
+
factoryTools,
|
|
594
|
+
fetchPullRequestTool,
|
|
595
|
+
findingFromDeepsec,
|
|
596
|
+
invokeAgentTool,
|
|
597
|
+
recordFinding,
|
|
598
|
+
recordFindingTool,
|
|
599
|
+
recordVerificationTool
|
|
127
600
|
};
|
|
128
601
|
//# sourceMappingURL=index.js.map
|