toolroll 0.9.33 → 0.9.36
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 +57 -0
- package/README.md +7 -0
- package/dist/assignment.d.ts +1 -0
- package/dist/assignment.js +2 -2
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +17 -17
- package/dist/browser-crew.js +1 -0
- package/dist/browser-workspace.d.ts +5 -3
- package/dist/builder.js +4 -7
- package/dist/chat-acceptance.d.ts +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +124 -63
- package/dist/contracts/contract-test.d.ts +45 -0
- package/dist/contracts/contract-test.js +69 -0
- package/dist/contracts/contract.d.ts +87 -0
- package/dist/contracts/contract.js +308 -0
- package/dist/contracts/flow-propose.d.ts +325 -0
- package/dist/contracts/flow-propose.js +41 -0
- package/dist/contracts/flow.d.ts +19400 -0
- package/dist/contracts/flow.js +359 -0
- package/dist/contracts/handoff.d.ts +91 -0
- package/dist/contracts/handoff.js +82 -0
- package/dist/contracts/plan.d.ts +58 -0
- package/dist/contracts/plan.js +67 -0
- package/dist/contracts/proof.d.ts +136 -0
- package/dist/contracts/proof.js +110 -0
- package/dist/contracts/verification-receipt.d.ts +148 -0
- package/dist/contracts/verification-receipt.js +87 -0
- package/dist/demo.js +28 -11
- package/dist/desktop-update.d.ts +1 -1
- package/dist/evidence.d.ts +3 -40
- package/dist/evidence.js +7 -3
- package/dist/flow-gallery.d.ts +3 -2
- package/dist/flow-share.d.ts +10 -32
- package/dist/flow-share.js +65 -83
- package/dist/flow-starters.d.ts +2 -1
- package/dist/flow-triggers.d.ts +15 -72
- package/dist/flow-triggers.js +94 -83
- package/dist/flows.d.ts +37 -218
- package/dist/flows.js +350 -385
- package/dist/mate-tools.js +14 -44
- package/dist/mcp-connect.d.ts +3 -0
- package/dist/mcp-connect.js +16 -4
- package/dist/operate.js +83 -12
- package/dist/plan.d.ts +12 -12
- package/dist/plan.js +109 -72
- package/dist/planner-source.js +2 -1
- package/dist/planner.js +55 -25
- package/dist/process-recovery-eligibility.d.ts +2 -2
- package/dist/process-recovery-eligibility.js +13 -8
- package/dist/project-admission.d.ts +93 -0
- package/dist/project-admission.js +134 -0
- package/dist/project-delete-ui.js +5 -2
- package/dist/proof.d.ts +15 -91
- package/dist/proof.js +90 -249
- package/dist/result-evidence-readers.d.ts +42 -0
- package/dist/result-evidence-readers.js +91 -0
- package/dist/runner.d.ts +10 -0
- package/dist/runner.js +16 -0
- package/dist/scope.js +4 -3
- package/dist/serve.d.ts +4 -0
- package/dist/serve.js +112 -160
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +2 -0
- package/dist/store.js +4 -0
- package/dist/surface.js +16 -3
- package/dist/task-review-brief.d.ts +75 -0
- package/dist/task-review-brief.js +179 -0
- package/dist/text-limits.d.ts +87 -0
- package/dist/text-limits.js +86 -0
- package/dist/verification-evidence.js +24 -12
- package/dist/when-html.d.ts +2 -0
- package/dist/when-html.js +13 -0
- package/package.json +3 -2
package/dist/proof.js
CHANGED
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
* so the review itself stays testable without a filesystem or a database.
|
|
20
20
|
*/
|
|
21
21
|
import { hasForbiddenControls } from "./decision.js";
|
|
22
|
-
import {
|
|
22
|
+
import { contractProblemOf } from "./contracts/contract.js";
|
|
23
|
+
import { PROOF_LIMITS, proofPayloadBody, readProofPayload } from "./contracts/proof.js";
|
|
24
|
+
export { PROOF_LIMITS } from "./contracts/proof.js";
|
|
23
25
|
/** Shared by preflight and final adjudication; presentation text is never a rubric. */
|
|
24
26
|
function criterionContractProblems(approved, answer) {
|
|
25
27
|
if (answer === undefined)
|
|
@@ -57,248 +59,96 @@ export function proofSubmissionProblems(proof, rubric) {
|
|
|
57
59
|
problems.push(...failedMetChecks(proof).map(failedCheckWords));
|
|
58
60
|
return problems;
|
|
59
61
|
}
|
|
60
|
-
/** Caps are BYTES of UTF-8, matching the scout report's rule: a payload is
|
|
61
|
-
* the same size whatever script it is written in. */
|
|
62
|
-
export const PROOF_LIMITS = {
|
|
63
|
-
payload: 64 * 1024,
|
|
64
|
-
criteria: 12,
|
|
65
|
-
criterionId: 40,
|
|
66
|
-
criterionStatement: 300,
|
|
67
|
-
criterionHow: 500,
|
|
68
|
-
evidencePerCriterion: 4,
|
|
69
|
-
evidenceRef: 300,
|
|
70
|
-
checks: 12,
|
|
71
|
-
checkCommand: 300,
|
|
72
|
-
checkSummary: 300,
|
|
73
|
-
changed: 64,
|
|
74
|
-
changedPath: 300,
|
|
75
|
-
caveats: 8,
|
|
76
|
-
caveat: 300,
|
|
77
|
-
screenshots: 8,
|
|
78
|
-
screenshotPath: 300,
|
|
79
|
-
screenshotCaption: 300,
|
|
80
|
-
};
|
|
81
62
|
function refuse(reason, message) {
|
|
82
63
|
return { ok: false, problems: [{ reason, message }] };
|
|
83
64
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
if (hasForbiddenControls(value)) {
|
|
107
|
-
problems.push({ reason: `${field}-controls`, message: `${field} carries control characters that could become terminal escapes` });
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
return value;
|
|
111
|
-
}
|
|
112
|
-
/** A repository-relative path: no leading slash, no drive letter, no `.`/`..`
|
|
113
|
-
* segment, no backslash, no control character. The same shape
|
|
114
|
-
* `readVerifiedArtifact` enforces on an evidence key, applied here to a
|
|
115
|
-
* path an agent claims rather than one the machine already wrote. */
|
|
116
|
-
function relativePath(value, field, cap, problems) {
|
|
117
|
-
const raw = prose(value, field, cap, problems);
|
|
118
|
-
if (raw === null)
|
|
119
|
-
return null;
|
|
120
|
-
const segments = raw.split("/");
|
|
121
|
-
const wellFormed = !raw.startsWith("/") &&
|
|
122
|
-
segments.every(segment => segment.length > 0 && segment !== "." && segment !== ".." && !segment.includes("\\"));
|
|
123
|
-
if (!wellFormed) {
|
|
124
|
-
problems.push({ reason: `${field}-not-relative`, message: `${field} must be a normalized repository-relative path (got ${describe(value)})` });
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
return raw;
|
|
128
|
-
}
|
|
129
|
-
/** `evidence` is optional on a criterion (absent → `[]`, exactly like every
|
|
130
|
-
* other list here) — the mandatory PRESENCE of an answer for a SIGNED
|
|
131
|
-
* criterion is `adjudicate`'s concern, not the parser's; this only proves
|
|
132
|
-
* the shape of what is there. */
|
|
133
|
-
function parseEvidenceRefs(value, field, problems) {
|
|
134
|
-
if (value === undefined || value === null)
|
|
135
|
-
return [];
|
|
136
|
-
if (!Array.isArray(value)) {
|
|
137
|
-
problems.push({ reason: `bad-${field}`, message: `${field} must be an array (got ${describe(value)})` });
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
if (value.length > PROOF_LIMITS.evidencePerCriterion) {
|
|
141
|
-
problems.push({ reason: `${field}-too-many`, message: `${field} lists ${value.length} — cap is ${PROOF_LIMITS.evidencePerCriterion}` });
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
const refs = [];
|
|
145
|
-
let bad = false;
|
|
146
|
-
for (const [index, entry] of value.entries()) {
|
|
147
|
-
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
148
|
-
problems.push({ reason: `${field}[${index}]-shape`, message: `${field}[${index}] must be an object` });
|
|
149
|
-
bad = true;
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
const one = entry;
|
|
153
|
-
const kind = one["kind"];
|
|
154
|
-
if (typeof kind !== "string" || !EVIDENCE_KINDS.includes(kind)) {
|
|
155
|
-
problems.push({
|
|
156
|
-
reason: `${field}[${index}]-bad-kind`,
|
|
157
|
-
message: `${field}[${index}].kind must draw from ${EVIDENCE_KINDS.join(", ")} (got ${describe(kind)})`,
|
|
158
|
-
});
|
|
159
|
-
bad = true;
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
const ref = prose(one["ref"], `${field}[${index}].ref`, PROOF_LIMITS.evidenceRef, problems);
|
|
163
|
-
if (ref === null) {
|
|
164
|
-
bad = true;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
refs.push({ kind: kind, ref });
|
|
168
|
-
}
|
|
169
|
-
return bad ? null : refs;
|
|
170
|
-
}
|
|
171
|
-
function parseCriteria(value, problems) {
|
|
172
|
-
if (value === undefined || value === null)
|
|
173
|
-
return [];
|
|
174
|
-
if (!Array.isArray(value)) {
|
|
175
|
-
problems.push({ reason: "bad-criteria", message: `criteria must be an array (got ${describe(value)})` });
|
|
176
|
-
return [];
|
|
177
|
-
}
|
|
178
|
-
if (value.length > PROOF_LIMITS.criteria) {
|
|
179
|
-
problems.push({ reason: "criteria-too-many", message: `criteria lists ${value.length} — cap is ${PROOF_LIMITS.criteria}` });
|
|
180
|
-
return [];
|
|
181
|
-
}
|
|
182
|
-
const criteria = [];
|
|
183
|
-
const seen = new Set();
|
|
184
|
-
for (const [index, entry] of value.entries()) {
|
|
185
|
-
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
186
|
-
problems.push({ reason: `criteria[${index}]-shape`, message: `criteria[${index}] must be an object` });
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
const one = entry;
|
|
190
|
-
const id = prose(one["id"], `criteria[${index}].id`, PROOF_LIMITS.criterionId, problems);
|
|
191
|
-
if (id !== null && seen.has(id)) {
|
|
192
|
-
problems.push({ reason: `criteria[${index}]-duplicate-id`, message: `criterion id "${id}" appears twice` });
|
|
193
|
-
}
|
|
194
|
-
else if (id !== null) {
|
|
195
|
-
seen.add(id);
|
|
196
|
-
}
|
|
197
|
-
const statement = prose(one["statement"], `criteria[${index}].statement`, PROOF_LIMITS.criterionStatement, problems);
|
|
198
|
-
const how = prose(one["how"], `criteria[${index}].how`, PROOF_LIMITS.criterionHow, problems);
|
|
199
|
-
const verdict = one["verdict"];
|
|
200
|
-
if (verdict !== "met" && verdict !== "not-met" && verdict !== "not-checked" && verdict !== "pending-verification") {
|
|
201
|
-
problems.push({
|
|
202
|
-
reason: `criteria[${index}]-bad-verdict`,
|
|
203
|
-
message: `criteria[${index}].verdict must be "met", "not-met", "not-checked", or "pending-verification" (got ${describe(verdict)})`,
|
|
204
|
-
});
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
const evidence = parseEvidenceRefs(one["evidence"], `criteria[${index}].evidence`, problems);
|
|
208
|
-
if (id === null || statement === null || how === null || evidence === null)
|
|
209
|
-
continue;
|
|
210
|
-
criteria.push({ id, statement, how, verdict, evidence });
|
|
211
|
-
}
|
|
212
|
-
return criteria;
|
|
213
|
-
}
|
|
214
|
-
function parseChecks(value, problems) {
|
|
215
|
-
if (value === undefined || value === null)
|
|
216
|
-
return [];
|
|
217
|
-
if (!Array.isArray(value)) {
|
|
218
|
-
problems.push({ reason: "bad-checks", message: `checks must be an array (got ${describe(value)})` });
|
|
219
|
-
return [];
|
|
220
|
-
}
|
|
221
|
-
if (value.length > PROOF_LIMITS.checks) {
|
|
222
|
-
problems.push({ reason: "checks-too-many", message: `checks lists ${value.length} — cap is ${PROOF_LIMITS.checks}` });
|
|
223
|
-
return [];
|
|
224
|
-
}
|
|
225
|
-
const checks = [];
|
|
226
|
-
for (const [index, entry] of value.entries()) {
|
|
227
|
-
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
228
|
-
problems.push({ reason: `checks[${index}]-shape`, message: `checks[${index}] must be an object` });
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
const one = entry;
|
|
232
|
-
const command = prose(one["command"], `checks[${index}].command`, PROOF_LIMITS.checkCommand, problems);
|
|
233
|
-
const summary = prose(one["summary"], `checks[${index}].summary`, PROOF_LIMITS.checkSummary, problems);
|
|
234
|
-
const exitCode = one["exitCode"];
|
|
235
|
-
if (typeof exitCode !== "number" || !Number.isInteger(exitCode) || exitCode < 0 || exitCode > 255) {
|
|
236
|
-
problems.push({
|
|
237
|
-
reason: `checks[${index}]-bad-exit-code`,
|
|
238
|
-
message: `checks[${index}].exitCode must be an integer 0-255 (got ${describe(exitCode)})`,
|
|
65
|
+
/** Every text field of a proof body (version 2 shape) that the schema saw as a string. */
|
|
66
|
+
function textFields(body) {
|
|
67
|
+
const out = [];
|
|
68
|
+
const text = (path, value, bytes, relative) => {
|
|
69
|
+
if (typeof value === "string")
|
|
70
|
+
out.push({ path, value, bytes, ...(relative ? { relative } : {}) });
|
|
71
|
+
};
|
|
72
|
+
const records = (field) => {
|
|
73
|
+
const list = body[field];
|
|
74
|
+
return Array.isArray(list) ? list.flatMap((one, index) => (typeof one === "object" && one !== null && !Array.isArray(one) ? [[index, one]] : [])) : [];
|
|
75
|
+
};
|
|
76
|
+
for (const [index, one] of records("criteria")) {
|
|
77
|
+
text(`criteria[${index}].id`, one["id"], PROOF_LIMITS.criterionId);
|
|
78
|
+
text(`criteria[${index}].statement`, one["statement"], PROOF_LIMITS.criterionStatement);
|
|
79
|
+
text(`criteria[${index}].how`, one["how"], PROOF_LIMITS.criterionHow);
|
|
80
|
+
const evidence = one["evidence"];
|
|
81
|
+
if (Array.isArray(evidence)) {
|
|
82
|
+
evidence.forEach((ref, at) => {
|
|
83
|
+
if (typeof ref === "object" && ref !== null)
|
|
84
|
+
text(`criteria[${index}].evidence[${at}].ref`, ref["ref"], PROOF_LIMITS.evidenceRef);
|
|
239
85
|
});
|
|
240
|
-
continue;
|
|
241
86
|
}
|
|
242
|
-
if (command === null || summary === null)
|
|
243
|
-
continue;
|
|
244
|
-
checks.push({ command, summary, exitCode });
|
|
245
87
|
}
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (value === undefined || value === null)
|
|
250
|
-
return [];
|
|
251
|
-
if (!Array.isArray(value)) {
|
|
252
|
-
problems.push({ reason: `bad-${field}`, message: `${field} must be an array (got ${describe(value)})` });
|
|
253
|
-
return [];
|
|
88
|
+
for (const [index, one] of records("checks")) {
|
|
89
|
+
text(`checks[${index}].command`, one["command"], PROOF_LIMITS.checkCommand);
|
|
90
|
+
text(`checks[${index}].summary`, one["summary"], PROOF_LIMITS.checkSummary);
|
|
254
91
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
92
|
+
for (const field of ["changed", "caveats"]) {
|
|
93
|
+
const list = body[field];
|
|
94
|
+
if (Array.isArray(list))
|
|
95
|
+
list.forEach((one, index) => text(`${field}[${index}]`, one, field === "changed" ? PROOF_LIMITS.changedPath : PROOF_LIMITS.caveat));
|
|
258
96
|
}
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (item !== null)
|
|
263
|
-
out.push(item);
|
|
97
|
+
for (const [index, one] of records("screenshots")) {
|
|
98
|
+
text(`screenshots[${index}].path`, one["path"], PROOF_LIMITS.screenshotPath, true);
|
|
99
|
+
text(`screenshots[${index}].caption`, one["caption"], PROOF_LIMITS.screenshotCaption);
|
|
264
100
|
}
|
|
265
101
|
return out;
|
|
266
102
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const screenshots = [];
|
|
279
|
-
const seen = new Set();
|
|
280
|
-
for (const [index, entry] of value.entries()) {
|
|
281
|
-
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
282
|
-
problems.push({ reason: `screenshots[${index}]-shape`, message: `screenshots[${index}] must be an object` });
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
const one = entry;
|
|
286
|
-
const path = relativePath(one["path"], `screenshots[${index}].path`, PROOF_LIMITS.screenshotPath, problems);
|
|
287
|
-
const caption = prose(one["caption"], `screenshots[${index}].caption`, PROOF_LIMITS.screenshotCaption, problems);
|
|
288
|
-
if (path !== null && seen.has(path)) {
|
|
289
|
-
problems.push({ reason: `screenshots[${index}]-duplicate-path`, message: `screenshot path "${path}" appears twice` });
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
if (path === null || caption === null)
|
|
103
|
+
/**
|
|
104
|
+
* The rules JSON Schema cannot state, in plain code with path-named errors: blank text, UTF-8 byte limits, control
|
|
105
|
+
* characters, normalized repository-relative screenshot paths (no leading slash, `.`/`..` segment or backslash — the
|
|
106
|
+
* shape `readVerifiedArtifact` enforces on an evidence key), and unique criterion ids and screenshot paths. A field
|
|
107
|
+
* the schema already refused is not reported twice.
|
|
108
|
+
*/
|
|
109
|
+
function proofRuleProblems(body, contract) {
|
|
110
|
+
const named = (path) => contract.some(problem => problem.message.startsWith(`${path}:`));
|
|
111
|
+
const problems = [];
|
|
112
|
+
for (const field of textFields(body)) {
|
|
113
|
+
if (named(field.path))
|
|
293
114
|
continue;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
115
|
+
const { path, value } = field;
|
|
116
|
+
if (value.trim() === "")
|
|
117
|
+
problems.push({ reason: `missing-${path}`, message: `${path}: must not be blank` });
|
|
118
|
+
else if (Buffer.byteLength(value, "utf8") > field.bytes)
|
|
119
|
+
problems.push({ reason: `${path}-too-long`, message: `${path}: over ${field.bytes.toLocaleString("en-US")} bytes` });
|
|
120
|
+
else if (hasForbiddenControls(value))
|
|
121
|
+
problems.push({ reason: `${path}-controls`, message: `${path}: carries control characters that could become terminal escapes` });
|
|
122
|
+
else if (field.relative && (value.startsWith("/") || !value.split("/").every(segment => segment.length > 0 && segment !== "." && segment !== ".." && !segment.includes("\\")))) {
|
|
123
|
+
problems.push({ reason: `${path}-not-relative`, message: `${path}: must be a normalized repository-relative path` });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const unique = (field, key, what) => {
|
|
127
|
+
const list = body[field];
|
|
128
|
+
if (!Array.isArray(list) || named(field))
|
|
129
|
+
return;
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
list.forEach((one, index) => {
|
|
132
|
+
const value = typeof one === "object" && one !== null ? one[key] : undefined;
|
|
133
|
+
if (typeof value !== "string")
|
|
134
|
+
return;
|
|
135
|
+
if (seen.has(value))
|
|
136
|
+
problems.push({ reason: `${field}[${index}]-duplicate-${key}`, message: `${field}[${index}].${key}: ${what} ${JSON.stringify(value)} appears twice` });
|
|
137
|
+
seen.add(value);
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
unique("criteria", "id", "criterion id");
|
|
141
|
+
unique("screenshots", "path", "screenshot path");
|
|
142
|
+
return problems;
|
|
298
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Read a proof: the payload byte cap and JSON first, then the proof schema (version 2 as itself, the version 1 a
|
|
146
|
+
* builder writes upgraded, a newer one refused plainly), then the plain-code rules. Every problem names its path and
|
|
147
|
+
* is reported at once; fail closed.
|
|
148
|
+
*/
|
|
299
149
|
export function parseProof(raw) {
|
|
300
150
|
if (Buffer.byteLength(raw, "utf8") > PROOF_LIMITS.payload) {
|
|
301
|
-
return refuse("too-large", `
|
|
151
|
+
return refuse("too-large", `payload: over ${PROOF_LIMITS.payload.toLocaleString("en-US")} bytes`);
|
|
302
152
|
}
|
|
303
153
|
let parsed;
|
|
304
154
|
try {
|
|
@@ -307,29 +157,20 @@ export function parseProof(raw) {
|
|
|
307
157
|
catch (error) {
|
|
308
158
|
return refuse("not-json", `the payload is not JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
309
159
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const checks = parseChecks(body["checks"], problems);
|
|
320
|
-
const changed = parseStringList(body["changed"], "changed", PROOF_LIMITS.changed, PROOF_LIMITS.changedPath, problems);
|
|
321
|
-
const caveats = parseStringList(body["caveats"], "caveats", PROOF_LIMITS.caveats, PROOF_LIMITS.caveat, problems);
|
|
322
|
-
const screenshots = parseScreenshots(body["screenshots"], problems);
|
|
323
|
-
if (problems.length > 0)
|
|
160
|
+
const body = proofPayloadBody(parsed);
|
|
161
|
+
if (body === null)
|
|
162
|
+
return refuse("not-an-object", "payload: must be one JSON object");
|
|
163
|
+
const read = readProofPayload(parsed);
|
|
164
|
+
const contract = read.ok ? [] : read.issues.map(contractProblemOf);
|
|
165
|
+
if (contract.some(problem => problem.reason === "newer-version" || problem.reason === "bad-version"))
|
|
166
|
+
return { ok: false, problems: contract };
|
|
167
|
+
const problems = [...contract, ...proofRuleProblems(body, contract)];
|
|
168
|
+
if (!read.ok || problems.length > 0)
|
|
324
169
|
return { ok: false, problems };
|
|
325
|
-
return {
|
|
326
|
-
ok: true,
|
|
327
|
-
proof: { version: 1, criteria, checks, changed, caveats, screenshots },
|
|
328
|
-
};
|
|
170
|
+
return { ok: true, proof: read.value };
|
|
329
171
|
}
|
|
330
|
-
/**
|
|
331
|
-
*
|
|
332
|
-
* is exactly what this parser admitted, key order and all. */
|
|
172
|
+
/** Serialize the validated shape, never the agent's raw bytes (the scout report's rule, `scout.ts`): what is stored
|
|
173
|
+
* and later hash-verified is exactly what the schema admitted, in its key order. */
|
|
333
174
|
export function serializeProof(proof) {
|
|
334
175
|
return JSON.stringify(proof, null, 2);
|
|
335
176
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Shared verified readers for result screens and the compact CLI review. */
|
|
2
|
+
import type { Artifact } from "./store.js";
|
|
3
|
+
export declare const SCREENSHOT_CAPTURE: RegExp;
|
|
4
|
+
/** What the run page shows of the terminal diff — verified bytes or a named problem, never silence. */
|
|
5
|
+
export type TerminalDiffView = {
|
|
6
|
+
patch: {
|
|
7
|
+
text: string;
|
|
8
|
+
truncated: boolean;
|
|
9
|
+
artifactId: number;
|
|
10
|
+
} | {
|
|
11
|
+
problem: string;
|
|
12
|
+
} | null;
|
|
13
|
+
stat: {
|
|
14
|
+
base: string;
|
|
15
|
+
head: string;
|
|
16
|
+
fileCount: number;
|
|
17
|
+
additions: number;
|
|
18
|
+
deletions: number;
|
|
19
|
+
binaryCount: number;
|
|
20
|
+
filesTruncated: boolean;
|
|
21
|
+
files: {
|
|
22
|
+
path: string;
|
|
23
|
+
additions: number | null;
|
|
24
|
+
deletions: number | null;
|
|
25
|
+
renamedFrom?: string;
|
|
26
|
+
}[];
|
|
27
|
+
} | {
|
|
28
|
+
problem: string;
|
|
29
|
+
} | null;
|
|
30
|
+
};
|
|
31
|
+
/** A compact, typed view of the agent-authored handoff. Older v1
|
|
32
|
+
* artifacts simply have no lists, while v2 can present the useful answer
|
|
33
|
+
* before the raw diff and evidence below it. */
|
|
34
|
+
export type StructuredHandoffView = {
|
|
35
|
+
conclusion: string;
|
|
36
|
+
changes: string[];
|
|
37
|
+
verification: string[];
|
|
38
|
+
followUps: string[];
|
|
39
|
+
};
|
|
40
|
+
export declare function structuredHandoffView(artifacts: Artifact[], root: string, displayText?: (text: string) => string): StructuredHandoffView | null;
|
|
41
|
+
/** A failed capture or unverifiable bytes keep their problem; absence returns null. */
|
|
42
|
+
export declare function terminalDiffView(artifacts: Artifact[], root: string): TerminalDiffView | null;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readVerifiedArtifact } from "./evidence.js";
|
|
2
|
+
import { hasForbiddenControls } from "./decision.js";
|
|
3
|
+
import { parseHandoffArtifact } from "./contracts/handoff.js";
|
|
4
|
+
const CAPTURE_EXIT = /\(exit ([0-9]{1,4})\)\s*$/;
|
|
5
|
+
export const SCREENSHOT_CAPTURE = /^agent-claimed screenshot at (.+) \(validated (?:png|jpeg)\)/;
|
|
6
|
+
const oneLineOf = (text, cap) => {
|
|
7
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
8
|
+
return flat.length <= cap ? flat : `${flat.slice(0, cap - 1)}…`;
|
|
9
|
+
};
|
|
10
|
+
export function structuredHandoffView(artifacts, root, displayText = text => text) {
|
|
11
|
+
const artifact = [...artifacts].reverse().find(one => one.kind === "handoff");
|
|
12
|
+
if (artifact === undefined)
|
|
13
|
+
return null;
|
|
14
|
+
const read = readVerifiedArtifact(root, artifact);
|
|
15
|
+
if (!read.ok)
|
|
16
|
+
return null;
|
|
17
|
+
const parsed = parseHandoffArtifact(read.content.toString("utf8"));
|
|
18
|
+
if (!parsed.ok)
|
|
19
|
+
return null;
|
|
20
|
+
const handoff = parsed.value;
|
|
21
|
+
const conclusion = oneLineOf(displayText(handoff.conclusion), 600);
|
|
22
|
+
if (conclusion === "" || hasForbiddenControls(conclusion))
|
|
23
|
+
return null;
|
|
24
|
+
const list = (items) => (items ?? [])
|
|
25
|
+
.filter(one => one.trim() !== "" && !hasForbiddenControls(one))
|
|
26
|
+
.slice(0, 8)
|
|
27
|
+
.map(one => oneLineOf(one, 240));
|
|
28
|
+
return { conclusion, changes: list(handoff.changes), verification: list(handoff.verification), followUps: list(handoff.followUps) };
|
|
29
|
+
}
|
|
30
|
+
/** A failed capture or unverifiable bytes keep their problem; absence returns null. */
|
|
31
|
+
export function terminalDiffView(artifacts, root) {
|
|
32
|
+
const patchArtifact = artifacts.find(one => one.kind === "terminal-diff");
|
|
33
|
+
const statArtifact = artifacts.find(one => one.kind === "diff-stat");
|
|
34
|
+
if (patchArtifact === undefined && statArtifact === undefined)
|
|
35
|
+
return null;
|
|
36
|
+
const view = { patch: null, stat: null };
|
|
37
|
+
if (patchArtifact !== undefined) {
|
|
38
|
+
const exit = CAPTURE_EXIT.exec(patchArtifact.capture);
|
|
39
|
+
if (exit !== null && exit[1] !== "0") {
|
|
40
|
+
view.patch = { problem: `capture failed — ${patchArtifact.capture}` };
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const read = readVerifiedArtifact(root, patchArtifact);
|
|
44
|
+
view.patch = read.ok
|
|
45
|
+
? { text: read.content.toString("utf8"), truncated: patchArtifact.truncated, artifactId: patchArtifact.id }
|
|
46
|
+
: { problem: `stored but unverifiable — ${read.problem}` };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (statArtifact !== undefined) {
|
|
50
|
+
const exit = CAPTURE_EXIT.exec(statArtifact.capture);
|
|
51
|
+
if (exit !== null && exit[1] !== "0") {
|
|
52
|
+
view.stat = { problem: `capture failed — ${statArtifact.capture}` };
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const read = readVerifiedArtifact(root, statArtifact);
|
|
56
|
+
if (!read.ok) {
|
|
57
|
+
view.stat = { problem: `stored but unverifiable — ${read.problem}` };
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(read.content.toString("utf8"));
|
|
62
|
+
// Every field this page renders is type-proved (arc 6, finding 7):
|
|
63
|
+
// "an object with a base key" was accepting any shape at all.
|
|
64
|
+
const count = (value) => typeof value === "number" && Number.isFinite(value);
|
|
65
|
+
const wellFormed = parsed !== null &&
|
|
66
|
+
typeof parsed === "object" &&
|
|
67
|
+
typeof parsed["base"] === "string" &&
|
|
68
|
+
typeof parsed["head"] === "string" &&
|
|
69
|
+
count(parsed["fileCount"]) &&
|
|
70
|
+
count(parsed["additions"]) &&
|
|
71
|
+
count(parsed["deletions"]) &&
|
|
72
|
+
count(parsed["binaryCount"]) &&
|
|
73
|
+
typeof parsed["filesTruncated"] === "boolean" &&
|
|
74
|
+
Array.isArray(parsed["files"]) &&
|
|
75
|
+
parsed["files"].every(one => one !== null &&
|
|
76
|
+
typeof one === "object" &&
|
|
77
|
+
typeof one["path"] === "string" &&
|
|
78
|
+
(count(one["additions"]) || one["additions"] === null) &&
|
|
79
|
+
(count(one["deletions"]) || one["deletions"] === null));
|
|
80
|
+
view.stat = wellFormed
|
|
81
|
+
? parsed
|
|
82
|
+
: { problem: "stat is not the shape this page knows" };
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
view.stat = { problem: "stat did not parse as JSON" };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return view;
|
|
91
|
+
}
|
package/dist/runner.d.ts
CHANGED
|
@@ -104,6 +104,16 @@ export declare function addRunnerReposAuthed(store: Store, args: {
|
|
|
104
104
|
token: string;
|
|
105
105
|
repos: readonly string[];
|
|
106
106
|
}, now: Date): AuthResult;
|
|
107
|
+
/**
|
|
108
|
+
* Narrow a live runner's binding when a project is removed, under the same
|
|
109
|
+
* credential and transaction as addRunnerReposAuthed. `up` calls it only
|
|
110
|
+
* after that project's watch has stopped admitting work.
|
|
111
|
+
*/
|
|
112
|
+
export declare function removeRunnerRepoAuthed(store: Store, args: {
|
|
113
|
+
name: string;
|
|
114
|
+
token: string;
|
|
115
|
+
repo: string;
|
|
116
|
+
}, now: Date): AuthResult;
|
|
107
117
|
/**
|
|
108
118
|
* The one runner-name rule (arc 2 finding 13): nonempty, control-free, at
|
|
109
119
|
* most 60 characters — the console form's rule, now shared by every door.
|
package/dist/runner.js
CHANGED
|
@@ -142,6 +142,22 @@ export function addRunnerReposAuthed(store, args, now) {
|
|
|
142
142
|
};
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Narrow a live runner's binding when a project is removed, under the same
|
|
147
|
+
* credential and transaction as addRunnerReposAuthed. `up` calls it only
|
|
148
|
+
* after that project's watch has stopped admitting work.
|
|
149
|
+
*/
|
|
150
|
+
export function removeRunnerRepoAuthed(store, args, now) {
|
|
151
|
+
return store.transact(() => {
|
|
152
|
+
const auth = authenticate(store, args.name, args.token);
|
|
153
|
+
if (!auth.ok)
|
|
154
|
+
return auth;
|
|
155
|
+
const bound = store.bindRunnerRepos(args.name, auth.runner.repos.filter(one => one !== args.repo), now);
|
|
156
|
+
if (!bound.ok)
|
|
157
|
+
return { ok: false, reason: bound.reason };
|
|
158
|
+
return { ok: true, runner: { ...auth.runner, repos: bound.repos } };
|
|
159
|
+
});
|
|
160
|
+
}
|
|
145
161
|
/**
|
|
146
162
|
* The one runner-name rule (arc 2 finding 13): nonempty, control-free, at
|
|
147
163
|
* most 60 characters — the console form's rule, now shared by every door.
|
package/dist/scope.js
CHANGED
|
@@ -30,6 +30,7 @@ import { claimActor, currentActor, parseLeadToken } from "./actor.js";
|
|
|
30
30
|
*/
|
|
31
31
|
import { createHash, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
|
|
32
32
|
import { hasForbiddenControls } from "./decision.js";
|
|
33
|
+
import { TEXT_LIMITS } from "./text-limits.js";
|
|
33
34
|
import { NO_READINESS, exactModelId, isRiskLevel, legOf, projectRoute, routeDigestOf, routeFromJson, routeWords, } from "./phase-routing.js";
|
|
34
35
|
/** Same shape as a runner's credential, for the same reasons. */
|
|
35
36
|
function mintToken() {
|
|
@@ -399,11 +400,11 @@ export function profileFromJson(json) {
|
|
|
399
400
|
export const EVIDENCE_KINDS = ["check", "screenshot", "changed-path", "manual-review"];
|
|
400
401
|
export const ACCEPTANCE_LIMITS = {
|
|
401
402
|
criteria: 12,
|
|
402
|
-
id:
|
|
403
|
+
id: TEXT_LIMITS.acceptanceIdBytes,
|
|
403
404
|
// A statement is one testable outcome; three hundred bytes forced people to
|
|
404
405
|
// drop the qualifying clause that made it testable.
|
|
405
|
-
statement:
|
|
406
|
-
how:
|
|
406
|
+
statement: TEXT_LIMITS.acceptanceStatementBytes,
|
|
407
|
+
how: TEXT_LIMITS.acceptanceHowBytes,
|
|
407
408
|
evidenceKinds: EVIDENCE_KINDS.length,
|
|
408
409
|
};
|
|
409
410
|
function acceptanceDescribe(value) {
|
package/dist/serve.d.ts
CHANGED
|
@@ -145,6 +145,10 @@ export type ServeOptions = {
|
|
|
145
145
|
desktopIdentity?: string;
|
|
146
146
|
/** Trusted native admission callback; enrollment rows alone never supply this authority. */
|
|
147
147
|
additionalProjectRepos?: () => readonly string[];
|
|
148
|
+
/** The co-located `up`'s exact admitted repositories, read on every request: its startup projects plus those
|
|
149
|
+
* added since (`repos add`, the lead, the console) and minus those removed. When given it replaces `repos` as
|
|
150
|
+
* the exact part of the ceiling, so an addition or removal shows without a restart. */
|
|
151
|
+
admittedRepos?: () => readonly string[];
|
|
148
152
|
connectionProbe?: typeof execRun;
|
|
149
153
|
connectionHome?: string;
|
|
150
154
|
/** Test seam for Chat's first tasks: the `gh` and `git grep` reads. */
|