@synovitec/trellis 0.0.0-stage → 0.6.0-rc.3
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/CHANGELOG.md +406 -0
- package/LICENSE +190 -0
- package/README.md +206 -2
- package/SECURITY.md +105 -0
- package/bin/trellis.mjs +5 -0
- package/docs/attestation/v0.1.md +96 -0
- package/package.json +78 -4
- package/schema/trellis.config.schema.json +349 -0
- package/src/agents/adapters.mjs +155 -0
- package/src/agents/git.mjs +241 -0
- package/src/agents/glob.mjs +41 -0
- package/src/agents/hooks.mjs +780 -0
- package/src/agents/install-adapters.mjs +172 -0
- package/src/agents/install.mjs +302 -0
- package/src/agents/manifests.mjs +127 -0
- package/src/agents/rules.mjs +330 -0
- package/src/agents/shell.mjs +253 -0
- package/src/agents/state.mjs +233 -0
- package/src/agents/transcript.mjs +120 -0
- package/src/cli/main.mjs +191 -0
- package/src/commands/attest.mjs +208 -0
- package/src/commands/checkpoint-state.mjs +62 -0
- package/src/commands/doctor.mjs +98 -0
- package/src/commands/guide.mjs +62 -0
- package/src/commands/init.mjs +131 -0
- package/src/commands/key.mjs +31 -0
- package/src/commands/scan.mjs +51 -0
- package/src/commands/uninstall.mjs +36 -0
- package/src/commands/verify.mjs +510 -0
- package/src/core/abatty.mjs +25 -0
- package/src/core/agents-block.mjs +65 -0
- package/src/core/attestation.mjs +213 -0
- package/src/core/config.mjs +135 -0
- package/src/core/context.mjs +280 -0
- package/src/core/diff.mjs +189 -0
- package/src/core/evidence.mjs +167 -0
- package/src/core/mcp.mjs +51 -0
- package/src/index.mjs +11 -0
- package/src/mcp/client.mjs +164 -0
- package/src/mcp/launch.mjs +57 -0
- package/src/mcp/relay.mjs +111 -0
- package/src/mcp/server.mjs +210 -0
- package/src/mcp/trust.mjs +142 -0
- package/src/tools/registry.mjs +370 -0
- package/src/tools/roles.mjs +17 -0
- package/src/verify/deps.mjs +382 -0
- package/src/verify/gitleaks-default.toml +3211 -0
- package/src/verify/rules.mjs +177 -0
- package/src/verify/secrets.mjs +169 -0
- package/src/verify/test-commands.mjs +116 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://unpkg.com/@synovitec/trellis/schema/trellis.config.schema.json",
|
|
4
|
+
"title": "trellis.config.json",
|
|
5
|
+
"description": "Which tools a repository uses. false is a decision on record (the tool does not apply here), not a gap.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"tools"
|
|
9
|
+
],
|
|
10
|
+
"properties": {
|
|
11
|
+
"$schema": {
|
|
12
|
+
"type": "string"
|
|
13
|
+
},
|
|
14
|
+
"tools": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": false,
|
|
17
|
+
"properties": {
|
|
18
|
+
"abatty": {
|
|
19
|
+
"type": "boolean"
|
|
20
|
+
},
|
|
21
|
+
"agentshield": {
|
|
22
|
+
"type": "boolean"
|
|
23
|
+
},
|
|
24
|
+
"graphify": {
|
|
25
|
+
"type": "boolean"
|
|
26
|
+
},
|
|
27
|
+
"context7": {
|
|
28
|
+
"type": "boolean"
|
|
29
|
+
},
|
|
30
|
+
"postgres-mcp": {
|
|
31
|
+
"oneOf": [
|
|
32
|
+
{
|
|
33
|
+
"type": "boolean"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"uriEnv": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"description": "The environment variable holding the connection string; never the string itself."
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"additionalProperties": false
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"find-skills": {
|
|
48
|
+
"type": "boolean"
|
|
49
|
+
},
|
|
50
|
+
"ecc": {
|
|
51
|
+
"type": "boolean"
|
|
52
|
+
},
|
|
53
|
+
"playwright-mcp": {
|
|
54
|
+
"oneOf": [
|
|
55
|
+
{
|
|
56
|
+
"type": "boolean"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"type": "object",
|
|
60
|
+
"properties": {
|
|
61
|
+
"origin": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"description": "The one origin the agent's browser may open, e.g. http://localhost:3000."
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"additionalProperties": false
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
"playwright": {
|
|
71
|
+
"type": "boolean"
|
|
72
|
+
},
|
|
73
|
+
"gitleaks": {
|
|
74
|
+
"type": "boolean"
|
|
75
|
+
},
|
|
76
|
+
"osv-scanner": {
|
|
77
|
+
"type": "boolean"
|
|
78
|
+
},
|
|
79
|
+
"lighthouse": {
|
|
80
|
+
"type": "boolean"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"agents": {
|
|
85
|
+
"type": "object",
|
|
86
|
+
"description": "The session rules the agent hooks enforce. Each is active only when the tool it asks for is enabled.",
|
|
87
|
+
"additionalProperties": false,
|
|
88
|
+
"properties": {
|
|
89
|
+
"mode": {
|
|
90
|
+
"enum": [
|
|
91
|
+
"block",
|
|
92
|
+
"warn",
|
|
93
|
+
"off"
|
|
94
|
+
],
|
|
95
|
+
"description": "The default for every rule: block sends the agent back, warn tells the person, off skips it."
|
|
96
|
+
},
|
|
97
|
+
"rules": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"additionalProperties": false,
|
|
100
|
+
"properties": {
|
|
101
|
+
"evidence-intact": {
|
|
102
|
+
"enum": [
|
|
103
|
+
"block",
|
|
104
|
+
"warn",
|
|
105
|
+
"off"
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
"config-unchanged": {
|
|
109
|
+
"enum": [
|
|
110
|
+
"block",
|
|
111
|
+
"warn",
|
|
112
|
+
"off"
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
"ui-verified": {
|
|
116
|
+
"enum": [
|
|
117
|
+
"block",
|
|
118
|
+
"warn",
|
|
119
|
+
"off"
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
"agent-setup-scanned": {
|
|
123
|
+
"enum": [
|
|
124
|
+
"block",
|
|
125
|
+
"warn",
|
|
126
|
+
"off"
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
"schema-before-change": {
|
|
130
|
+
"enum": [
|
|
131
|
+
"block",
|
|
132
|
+
"warn",
|
|
133
|
+
"off"
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
"graph-before-edit": {
|
|
137
|
+
"enum": [
|
|
138
|
+
"block",
|
|
139
|
+
"warn",
|
|
140
|
+
"off"
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
"docs-looked-up": {
|
|
144
|
+
"enum": [
|
|
145
|
+
"block",
|
|
146
|
+
"warn",
|
|
147
|
+
"off"
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
"graph-first": {
|
|
151
|
+
"enum": [
|
|
152
|
+
"block",
|
|
153
|
+
"warn",
|
|
154
|
+
"off"
|
|
155
|
+
]
|
|
156
|
+
},
|
|
157
|
+
"secrets-scanned": {
|
|
158
|
+
"enum": [
|
|
159
|
+
"block",
|
|
160
|
+
"warn",
|
|
161
|
+
"off"
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
"uiPaths": {
|
|
167
|
+
"type": "array",
|
|
168
|
+
"items": {
|
|
169
|
+
"type": "string"
|
|
170
|
+
},
|
|
171
|
+
"description": "Globs of the files a browser check must follow."
|
|
172
|
+
},
|
|
173
|
+
"agentSetupPaths": {
|
|
174
|
+
"type": "array",
|
|
175
|
+
"items": {
|
|
176
|
+
"type": "string"
|
|
177
|
+
},
|
|
178
|
+
"description": "Globs of the agent setup an AgentShield scan must follow."
|
|
179
|
+
},
|
|
180
|
+
"graphAfterSearches": {
|
|
181
|
+
"type": "integer",
|
|
182
|
+
"minimum": 1
|
|
183
|
+
},
|
|
184
|
+
"uiIgnore": {
|
|
185
|
+
"type": "array",
|
|
186
|
+
"items": {
|
|
187
|
+
"type": "string"
|
|
188
|
+
},
|
|
189
|
+
"description": "Globs under uiPaths that are not UI (tests, stories, build output)."
|
|
190
|
+
},
|
|
191
|
+
"maxBlocks": {
|
|
192
|
+
"type": "integer",
|
|
193
|
+
"minimum": 1,
|
|
194
|
+
"description": "Times the agent is sent back before the person decides. Default 2."
|
|
195
|
+
},
|
|
196
|
+
"codePaths": {
|
|
197
|
+
"type": "array",
|
|
198
|
+
"items": {
|
|
199
|
+
"type": "string"
|
|
200
|
+
},
|
|
201
|
+
"description": "Globs of the source files the code graph must be asked about before they change."
|
|
202
|
+
},
|
|
203
|
+
"codeIgnore": {
|
|
204
|
+
"type": "array",
|
|
205
|
+
"items": {
|
|
206
|
+
"type": "string"
|
|
207
|
+
},
|
|
208
|
+
"description": "Globs under codePaths and schemaPaths that are generated, not written."
|
|
209
|
+
},
|
|
210
|
+
"schemaPaths": {
|
|
211
|
+
"type": "array",
|
|
212
|
+
"items": {
|
|
213
|
+
"type": "string"
|
|
214
|
+
},
|
|
215
|
+
"description": "Globs of migrations, models and schema files the database schema must be looked at before they change."
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
"verify": {
|
|
220
|
+
"type": "object",
|
|
221
|
+
"description": "The commit checkpoint (`trellis verify`, pre-commit and CI): a preset and per-rule modes. Blocking rules are the deterministic ones; the rest warn.",
|
|
222
|
+
"additionalProperties": false,
|
|
223
|
+
"properties": {
|
|
224
|
+
"preset": {
|
|
225
|
+
"enum": [
|
|
226
|
+
"recommended",
|
|
227
|
+
"strict"
|
|
228
|
+
],
|
|
229
|
+
"default": "recommended"
|
|
230
|
+
},
|
|
231
|
+
"unattested": {
|
|
232
|
+
"enum": [
|
|
233
|
+
"refuse",
|
|
234
|
+
"judge"
|
|
235
|
+
],
|
|
236
|
+
"default": "refuse",
|
|
237
|
+
"description": "In CI, a commit with no Trellis attestation (a fork's contributor without Trellis, Dependabot, Renovate, the web editor): refused, or judged on its own as an agent's (secrets, dependencies, protected tests, the net change), read at the base."
|
|
238
|
+
},
|
|
239
|
+
"tests": {
|
|
240
|
+
"type": "array",
|
|
241
|
+
"description": "The project's test commands: only a run of exactly one of them counts for tests-run (no extra arguments: a filter can match nothing). Default: read from the project (package.json's test script, pytest, go test ./..., cargo test).",
|
|
242
|
+
"items": {
|
|
243
|
+
"type": "string",
|
|
244
|
+
"minLength": 1
|
|
245
|
+
},
|
|
246
|
+
"minItems": 1
|
|
247
|
+
},
|
|
248
|
+
"rules": {
|
|
249
|
+
"type": "object",
|
|
250
|
+
"additionalProperties": false,
|
|
251
|
+
"properties": {
|
|
252
|
+
"secrets-scanned": {
|
|
253
|
+
"enum": [
|
|
254
|
+
"block",
|
|
255
|
+
"warn",
|
|
256
|
+
"off"
|
|
257
|
+
]
|
|
258
|
+
},
|
|
259
|
+
"deps-verified": {
|
|
260
|
+
"enum": [
|
|
261
|
+
"block",
|
|
262
|
+
"warn",
|
|
263
|
+
"off"
|
|
264
|
+
]
|
|
265
|
+
},
|
|
266
|
+
"deps-pinned": {
|
|
267
|
+
"enum": [
|
|
268
|
+
"block",
|
|
269
|
+
"warn",
|
|
270
|
+
"off"
|
|
271
|
+
]
|
|
272
|
+
},
|
|
273
|
+
"deps-registry": {
|
|
274
|
+
"enum": [
|
|
275
|
+
"block",
|
|
276
|
+
"warn",
|
|
277
|
+
"off"
|
|
278
|
+
]
|
|
279
|
+
},
|
|
280
|
+
"tests-protected": {
|
|
281
|
+
"enum": [
|
|
282
|
+
"block",
|
|
283
|
+
"warn",
|
|
284
|
+
"off"
|
|
285
|
+
]
|
|
286
|
+
},
|
|
287
|
+
"escalation": {
|
|
288
|
+
"enum": [
|
|
289
|
+
"block",
|
|
290
|
+
"warn",
|
|
291
|
+
"off"
|
|
292
|
+
]
|
|
293
|
+
},
|
|
294
|
+
"tests-run": {
|
|
295
|
+
"enum": [
|
|
296
|
+
"block",
|
|
297
|
+
"warn",
|
|
298
|
+
"off"
|
|
299
|
+
]
|
|
300
|
+
},
|
|
301
|
+
"graph-before-edit": {
|
|
302
|
+
"enum": [
|
|
303
|
+
"block",
|
|
304
|
+
"warn",
|
|
305
|
+
"off"
|
|
306
|
+
]
|
|
307
|
+
},
|
|
308
|
+
"docs-looked-up": {
|
|
309
|
+
"enum": [
|
|
310
|
+
"block",
|
|
311
|
+
"warn",
|
|
312
|
+
"off"
|
|
313
|
+
]
|
|
314
|
+
},
|
|
315
|
+
"ui-verified": {
|
|
316
|
+
"enum": [
|
|
317
|
+
"block",
|
|
318
|
+
"warn",
|
|
319
|
+
"off"
|
|
320
|
+
]
|
|
321
|
+
},
|
|
322
|
+
"schema-before-change": {
|
|
323
|
+
"enum": [
|
|
324
|
+
"block",
|
|
325
|
+
"warn",
|
|
326
|
+
"off"
|
|
327
|
+
]
|
|
328
|
+
},
|
|
329
|
+
"review": {
|
|
330
|
+
"enum": [
|
|
331
|
+
"block",
|
|
332
|
+
"warn",
|
|
333
|
+
"off"
|
|
334
|
+
]
|
|
335
|
+
},
|
|
336
|
+
"plan-before-change": {
|
|
337
|
+
"enum": [
|
|
338
|
+
"block",
|
|
339
|
+
"warn",
|
|
340
|
+
"off"
|
|
341
|
+
]
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
"additionalProperties": false
|
|
349
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// The hooks of agents other than Claude Code: Cursor, Codex and Copilot. Each sends its own
|
|
2
|
+
// payload and expects its own answer; an adapter turns the payload into one shape, applies the
|
|
3
|
+
// same guard and gates as for Claude Code, feeds the shared evidence log, and answers in the
|
|
4
|
+
// agent's format (sources: each agent's hook reference, read 2026-10-30).
|
|
5
|
+
//
|
|
6
|
+
// Weaker than the Claude Code path: these agents' transcripts are not documented, so a call is not
|
|
7
|
+
// cross-checked against one. A hook run by hand is still refused by the shell guard, and CI, which
|
|
8
|
+
// re-runs the deterministic checks, stays the authority.
|
|
9
|
+
import { appendEvent, readWindow } from "../core/evidence.mjs";
|
|
10
|
+
import { CONFIG_FILE, enabledTools } from "../core/config.mjs";
|
|
11
|
+
import { DEFAULTS, gate, isBrowser, isContext7, isSchemaLookup } from "./rules.mjs";
|
|
12
|
+
import { emptyLedger } from "./state.mjs";
|
|
13
|
+
import { graphAnswered, succeeded, tampering } from "./hooks.mjs";
|
|
14
|
+
import { relativeTo } from "./transcript.mjs";
|
|
15
|
+
import { treeOf, worktreeDiff } from "../core/diff.mjs";
|
|
16
|
+
import { matchesAny } from "./glob.mjs";
|
|
17
|
+
import { parseJson } from "../core/context.mjs";
|
|
18
|
+
|
|
19
|
+
/** The agents an adapter speaks for. */
|
|
20
|
+
export const ADAPTED = Object.freeze(["cursor", "codex", "copilot"]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} Call
|
|
24
|
+
* @property {string} name Claude-style: Bash, Edit, Write, mcp__<server>__<tool>...
|
|
25
|
+
* @property {Record<string, any>} input
|
|
26
|
+
* @property {any} output
|
|
27
|
+
* @property {boolean} ok
|
|
28
|
+
* @property {string[]} edits files the call writes, relative as given
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** Cursor names an MCP tool `MCP:<tool>`, without its server: the server, from the tool's name. @param {string} tool */
|
|
32
|
+
function cursorServer(tool) {
|
|
33
|
+
if (/^browser_/.test(tool)) return "playwright";
|
|
34
|
+
if (/^(resolve-library-id|query-docs|get-library-docs)$/.test(tool)) return "context7";
|
|
35
|
+
if (/^(list_schemas|list_objects|get_object_details|execute_sql|explain_query|analyze_\w+|get_top_queries)$/.test(tool)) return "postgres";
|
|
36
|
+
return "unknown";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The files an apply_patch writes, from the patch's own headers. @param {string} patch */
|
|
40
|
+
const patchFiles = (patch) => [...patch.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$|^\*\*\* Move to: (.+)$/gm)].map((m) => (m[1] ?? m[2]).trim());
|
|
41
|
+
|
|
42
|
+
/** @param {Record<string, any>} input */
|
|
43
|
+
const pathOf = (input) => input.file_path ?? input.filePath ?? input.path ?? input.target_file ?? null;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* One shape for every agent's tool-call payload.
|
|
47
|
+
* @param {string} agent
|
|
48
|
+
* @param {Record<string, any>} p
|
|
49
|
+
* @returns {Call}
|
|
50
|
+
*/
|
|
51
|
+
export function normalize(agent, p) {
|
|
52
|
+
const input = p.tool_input && typeof p.tool_input === "object" ? p.tool_input : {};
|
|
53
|
+
let name = String(p.tool_name ?? "");
|
|
54
|
+
/** @type {any} */
|
|
55
|
+
let output = p.tool_response;
|
|
56
|
+
let ok = true;
|
|
57
|
+
if (agent === "cursor") {
|
|
58
|
+
if (name === "Shell") name = "Bash";
|
|
59
|
+
const mcp = name.match(/^MCP:(.+)$/);
|
|
60
|
+
if (mcp) name = `mcp__${cursorServer(mcp[1])}__${mcp[1]}`;
|
|
61
|
+
output = typeof p.tool_output === "string" ? (parseJson(p.tool_output) ?? p.tool_output) : p.tool_output;
|
|
62
|
+
ok = succeeded(output);
|
|
63
|
+
} else if (agent === "copilot") {
|
|
64
|
+
const r = p.tool_result ?? {};
|
|
65
|
+
output = { output: r.text_result_for_llm ?? "" };
|
|
66
|
+
ok = r.result_type == null || r.result_type === "success";
|
|
67
|
+
} else {
|
|
68
|
+
ok = succeeded(output);
|
|
69
|
+
}
|
|
70
|
+
const edits = name === "apply_patch" ? patchFiles(String(input.input ?? input.patch ?? input.command ?? "")) : ["Edit", "Write", "MultiEdit"].includes(name) && pathOf(input) ? [String(pathOf(input))] : [];
|
|
71
|
+
return { name, input, output, ok, edits };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @typedef {{ code: number, stdout?: string, stderr?: string }} HookResult */
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The answer to a refused pre-tool call, as the agent reads it: Cursor a JSON permission, Codex
|
|
78
|
+
* its JSON deny (an exit code would not survive Windows PowerShell, which turns any failure into
|
|
79
|
+
* 1), Copilot exit 2 with the reason on stderr (all documented).
|
|
80
|
+
* @param {string} agent
|
|
81
|
+
* @param {string} reason
|
|
82
|
+
* @returns {HookResult}
|
|
83
|
+
*/
|
|
84
|
+
function deny(agent, reason) {
|
|
85
|
+
const text = `[trellis] ${reason}`;
|
|
86
|
+
if (agent === "cursor") return { code: 0, stdout: `${JSON.stringify({ permission: "deny", user_message: text, agent_message: text })}\n` };
|
|
87
|
+
if (agent === "codex") return { code: 0, stdout: `${JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: text } })}\n` };
|
|
88
|
+
return { code: 2, stderr: `${text}\n` };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The repository's config, read as it is (the commit checkpoint judges the rest). @param {import("../core/context.mjs").Context} ctx */
|
|
92
|
+
const configOf = (ctx) => parseJson(ctx.read(CONFIG_FILE));
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {string} agent
|
|
96
|
+
* @param {string} event pre-tool | post-tool
|
|
97
|
+
* @param {Record<string, any>} payload
|
|
98
|
+
* @param {import("../core/context.mjs").Context} ctx
|
|
99
|
+
* @param {NodeJS.ProcessEnv} env
|
|
100
|
+
* @returns {HookResult}
|
|
101
|
+
*/
|
|
102
|
+
export function agentHook(agent, event, payload, ctx, env) {
|
|
103
|
+
if (!ADAPTED.includes(agent)) return { code: 2, stderr: `[trellis] unknown agent "${agent}" (known: ${ADAPTED.join(", ")}, and Claude Code without --agent).\n` };
|
|
104
|
+
const call = normalize(agent, payload);
|
|
105
|
+
const config = configOf(ctx);
|
|
106
|
+
if (event === "pre-tool") {
|
|
107
|
+
if (call.name === "Bash") {
|
|
108
|
+
const why = tampering(String(call.input.command ?? ""));
|
|
109
|
+
if (why) return deny(agent, why);
|
|
110
|
+
}
|
|
111
|
+
if (!call.edits.length) return { code: 0 };
|
|
112
|
+
const window = readWindow(ctx.root, env);
|
|
113
|
+
if (window.status === "tampered") return deny(agent, "The evidence log was altered; nothing in it can be trusted. Tell the person.");
|
|
114
|
+
const ledger = { ...emptyLedger(), graph: window.events.some((e) => e.kind === "graph"), schema: window.events.some((e) => e.kind === "schema") };
|
|
115
|
+
const enabled = new Set(enabledTools(ctx, config).map((e) => e.tool.id));
|
|
116
|
+
for (const file of call.edits) {
|
|
117
|
+
const path = relativeTo(file, ctx.root, ctx.platform) ?? file;
|
|
118
|
+
const refusal = gate(path, ledger, enabled, config?.agents, { graphReady: ctx.exists("graphify-out/graph.json") });
|
|
119
|
+
if (refusal?.mode === "block") return deny(agent, refusal.reason);
|
|
120
|
+
}
|
|
121
|
+
return { code: 0 };
|
|
122
|
+
}
|
|
123
|
+
if (event === "post-tool") {
|
|
124
|
+
const feed = (/** @type {Omit<import("../core/evidence.mjs").Event, "agent">} */ e) => appendEvent(ctx.root, { ...e, agent }, env);
|
|
125
|
+
// That an agent is at work, once per window: it makes the change an agent's (tests-protected,
|
|
126
|
+
// escalation), even when it edits through the shell and looks nothing up.
|
|
127
|
+
const window = readWindow(ctx.root, env);
|
|
128
|
+
if (window.status === "ok" && !window.events.some((e) => e.agent === agent)) feed({ kind: "agent", tool: call.name });
|
|
129
|
+
if (!call.ok) return { code: 0 };
|
|
130
|
+
const use = { name: call.name, input: call.input };
|
|
131
|
+
const none = new Set();
|
|
132
|
+
if (call.name === "Bash" && graphAnswered(use, call.output, ctx)) feed({ kind: "graph", tool: "graphify", subject: String(call.input.command).slice(0, 300) });
|
|
133
|
+
else if (isContext7(none, use)) feed({ kind: "docs", tool: call.name, subject: JSON.stringify(call.input).toLowerCase().slice(0, 500) });
|
|
134
|
+
else if (isSchemaLookup(none, use)) feed({ kind: "schema", tool: call.name });
|
|
135
|
+
else if (isBrowser(none, use) && /browser_(snapshot|take_screenshot)$/.test(call.name)) {
|
|
136
|
+
const blobs = uiBlobs(ctx);
|
|
137
|
+
if (blobs) feed({ kind: "browser", tool: call.name, blobs });
|
|
138
|
+
}
|
|
139
|
+
return { code: 0 };
|
|
140
|
+
}
|
|
141
|
+
return { code: 0 };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The git blob of each changed UI file as it is on disk now; null when git cannot tell. @param {import("../core/context.mjs").Context} ctx */
|
|
145
|
+
export function uiBlobs(ctx) {
|
|
146
|
+
try {
|
|
147
|
+
/** @type {Record<string, string>} */
|
|
148
|
+
const blobs = {};
|
|
149
|
+
for (const c of worktreeDiff(ctx.root, treeOf(ctx.root, "HEAD"))) if (c.newBlob && matchesAny(c.path, DEFAULTS.uiPaths)) blobs[c.path] = c.newBlob;
|
|
150
|
+
return blobs;
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|