eval-quality 0.2.0 → 0.5.0
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/README.md +17 -12
- package/corpus/dev/README.md +3 -2
- package/corpus/dev/compile-seal-example/brief.json +1 -1
- package/corpus/dev/compile-seal-example/contract.json +1 -1
- package/corpus/dev/contracts/absent-collection-locations.json +1 -1
- package/corpus/dev/contracts/absent-sibling-groups.json +1 -1
- package/corpus/dev/contracts/absent-success-indicator.json +1 -1
- package/corpus/dev/contracts/empty-channel-roles.json +1 -1
- package/corpus/dev/contracts/empty-collection-locations.json +1 -1
- package/corpus/dev/contracts/empty-request-shapes.json +1 -1
- package/corpus/dev/contracts/empty-sibling-groups.json +1 -1
- package/corpus/dev/contracts/fragment-selection.json +1 -0
- package/corpus/dev/contracts/no-collection-quantifier.json +1 -1
- package/corpus/dev/contracts/no-operation-inventory.json +1 -1
- package/corpus/dev/contracts/no-read-back-relation.json +1 -1
- package/corpus/dev/contracts/no-state-change-marker.json +1 -1
- package/corpus/dev/contracts/no-type-violating-step.json +1 -1
- package/corpus/dev/contracts/per-key-split-oracles.json +1 -1
- package/corpus/dev/contracts/review-corpus.json +1 -0
- package/corpus/dev/contracts/satisfied-declarations.json +1 -1
- package/corpus/dev/contracts/single-required-response-key.json +1 -1
- package/corpus/dev/contracts/split-indicator-oracle.json +1 -1
- package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -1
- package/corpus/dev/contracts/unnamed-reference-set.json +1 -1
- package/corpus/dev/contracts/wrong-cardinality-form.json +1 -1
- package/corpus/dev/index.json +1 -1
- package/dist/adapters/command-line-adapter.d.ts +44 -0
- package/dist/adapters/command-line-adapter.js +284 -0
- package/dist/adapters/command-target-policy.d.ts +36 -0
- package/dist/adapters/command-target-policy.js +33 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.js +1 -0
- package/dist/cli/render.d.ts +2 -5
- package/dist/cli/render.js +51 -1
- package/dist/core/compile/bindings.d.ts +26 -7
- package/dist/core/compile/bindings.js +49 -32
- package/dist/core/compile/compile.d.ts +12 -0
- package/dist/core/compile/compile.js +28 -1
- package/dist/core/compile/excluded-content.d.ts +11 -0
- package/dist/core/compile/excluded-content.js +42 -0
- package/dist/core/compile/expression-legality.d.ts +2 -2
- package/dist/core/compile/expression-legality.js +58 -12
- package/dist/core/compile/interface-inventory.d.ts +34 -1
- package/dist/core/compile/interface-inventory.js +88 -11
- package/dist/core/compile/reachability.d.ts +12 -2
- package/dist/core/compile/reachability.js +123 -36
- package/dist/core/compile/schema-version.d.ts +2 -0
- package/dist/core/compile/schema-version.js +25 -0
- package/dist/core/compile/sensitivity-witness.d.ts +31 -12
- package/dist/core/compile/sensitivity-witness.js +110 -23
- package/dist/core/compile/step-reference.d.ts +2 -0
- package/dist/core/compile/step-reference.js +49 -0
- package/dist/core/coverage/operations.d.ts +62 -0
- package/dist/core/coverage/operations.js +57 -0
- package/dist/core/coverage/relevance.d.ts +4 -2
- package/dist/core/coverage/relevance.js +22 -23
- package/dist/core/coverage/satisfaction.d.ts +2 -2
- package/dist/core/coverage/satisfaction.js +73 -48
- package/dist/core/declared-inputs.d.ts +83 -4
- package/dist/core/declared-inputs.js +105 -8
- package/dist/core/evaluate/evidence-resolution.d.ts +9 -12
- package/dist/core/evaluate/evidence-resolution.js +70 -10
- package/dist/core/evaluate/operators.d.ts +22 -0
- package/dist/core/evaluate/operators.js +17 -1
- package/dist/core/evaluate/resolution.d.ts +14 -3
- package/dist/core/evaluate/resolution.js +63 -3
- package/dist/core/excluded-content.d.ts +65 -0
- package/dist/core/excluded-content.js +113 -0
- package/dist/core/failure-codes.d.ts +2 -2
- package/dist/core/failure-codes.js +5 -2
- package/dist/core/ingest/conditions.d.ts +1 -1
- package/dist/core/ingest/ingest.js +8 -0
- package/dist/core/preflight/plan.d.ts +7 -5
- package/dist/core/preflight/plan.js +75 -32
- package/dist/core/preflight/projection.d.ts +6 -3
- package/dist/core/preflight/projection.js +22 -2
- package/dist/core/preflight/reduce.js +47 -7
- package/dist/core/preflight/witness-evidence.d.ts +5 -5
- package/dist/core/preflight/witness-evidence.js +63 -20
- package/dist/core/schemas/artifact.d.ts +683 -35
- package/dist/core/schemas/constraint-ledger.js +11 -0
- package/dist/core/schemas/defect-signature.d.ts +438 -16
- package/dist/core/schemas/defect-signature.js +58 -10
- package/dist/core/schemas/eval-contract.d.ts +517 -12
- package/dist/core/schemas/eval-contract.js +18 -2
- package/dist/core/schemas/evidence-artifact.d.ts +14 -4
- package/dist/core/schemas/evidence-artifact.js +1 -1
- package/dist/core/schemas/interface.d.ts +741 -13
- package/dist/core/schemas/interface.js +120 -7
- package/dist/core/schemas/isolation-manifest.js +16 -9
- package/dist/core/schemas/plan.d.ts +217 -3
- package/dist/core/schemas/plan.js +22 -1
- package/dist/core/schemas/pointer.d.ts +47 -1
- package/dist/core/schemas/pointer.js +89 -8
- package/dist/core/schemas/port-messages.d.ts +194 -10
- package/dist/core/schemas/port-messages.js +73 -6
- package/dist/core/schemas/probe-body.d.ts +18 -0
- package/dist/core/schemas/probe-body.js +13 -0
- package/dist/core/schemas/probe-policy.d.ts +36 -0
- package/dist/core/schemas/probe-policy.js +44 -0
- package/dist/core/schemas/probe.d.ts +122 -3
- package/dist/core/schemas/probe.js +1 -1
- package/dist/core/schemas/sealed-run-record.d.ts +131 -9
- package/dist/core/schemas/sealed-run-record.js +56 -11
- package/dist/core/schemas/sensitivity-witness.d.ts +108 -6
- package/dist/core/schemas/sensitivity-witness.js +61 -5
- package/dist/core/score/bindings.d.ts +2 -2
- package/dist/core/score/bindings.js +25 -13
- package/dist/core/score/qualification.d.ts +5 -5
- package/dist/core/score/qualification.js +67 -37
- package/dist/core/score/quotation.d.ts +1 -24
- package/dist/core/score/quotation.js +29 -4
- package/dist/core/score/score.js +28 -2
- package/dist/core/score/strength.d.ts +9 -0
- package/dist/core/score/strength.js +35 -1
- package/dist/core/score/witness.d.ts +21 -0
- package/dist/core/score/witness.js +9 -4
- package/dist/core/seal/derived-reference.d.ts +2 -9
- package/dist/core/seal/derived-reference.js +62 -23
- package/dist/core/seal/plan-index.d.ts +18 -3
- package/dist/core/seal/plan-index.js +44 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/ports/environment-probe-port.d.ts +60 -4
- package/dist/testing/conformance.d.ts +2 -1
- package/dist/testing/conformance.js +1 -0
- package/dist/testing/index.d.ts +3 -3
- package/dist/testing/index.js +1 -1
- package/dist/testing/probe-conformance.d.ts +56 -5
- package/dist/testing/probe-conformance.js +196 -5
- package/package.json +1 -1
- package/schemas/eval-contract.schema.json +1356 -464
- package/schemas/evidence-artifact.schema.json +59 -29
- package/schemas/isolation-manifest.schema.json +17 -10
- package/schemas/probe.schema.json +197 -71
- package/schemas/rubric.schema.json +1 -1
- package/schemas/sealed-run-record.schema.json +296 -39
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment-probe port over a real child process, for the `cli`
|
|
3
|
+
* mechanism, which `probe-conformance.ts` had documented as unauthorizable
|
|
4
|
+
* and unshipped. `evaluateCommandTarget` closes the authorization half; this
|
|
5
|
+
* file closes the execution half.
|
|
6
|
+
*
|
|
7
|
+
* Every rule below is what "what's a command allowed to do" resolves to, a
|
|
8
|
+
* decision settled here in the implementation rather than as an architecture
|
|
9
|
+
* revision:
|
|
10
|
+
*
|
|
11
|
+
* 1. `target` is spawned directly with an argv array (`shell: false`,
|
|
12
|
+
* Node's own default, stated anyway since it is the one thing this
|
|
13
|
+
* adapter must never turn off). No channel value is ever concatenated
|
|
14
|
+
* into a shell string; a value containing `;`, `$(...)`, or a quote
|
|
15
|
+
* reaches the child as one literal argv element, not a metacharacter.
|
|
16
|
+
* 2. `argument` and `option` build the argv the same way a well-behaved CLI
|
|
17
|
+
* parser reads one: options first as `--{key}` (a boolean `true` is a
|
|
18
|
+
* bare flag, `false` is omitted, anything else gets one value token),
|
|
19
|
+
* positionals after in the record's own key order. `environment` passes
|
|
20
|
+
* through as declared, plus the host's own `PATH` so a `target` naming a
|
|
21
|
+
* bare command still resolves; a declared `PATH` key wins over that
|
|
22
|
+
* default. `stdin` is written and the stream is closed; `absent` closes
|
|
23
|
+
* it with nothing written.
|
|
24
|
+
* 3. `maxElapsedMs` and `maxOutputBytes` are enforced by this adapter, not
|
|
25
|
+
* borrowed from `AbortSignal`: exceeding either kills the process with
|
|
26
|
+
* `SIGKILL` and throws `budget-exhausted`, exactly as an HTTP cap does.
|
|
27
|
+
* `maxOutputBytes` applies independently to stdout, to stderr, and to
|
|
28
|
+
* each artifact file read back after exit.
|
|
29
|
+
* 4. A non-zero exit is an observation, never a fault, matching AD-10's rule
|
|
30
|
+
* for an HTTP status. Only a policy denial, a cap, an abort, or a failure
|
|
31
|
+
* to start the process throws. `exitCode` is negative when a signal
|
|
32
|
+
* ended the process, the same convention `CommandProbeObservation`
|
|
33
|
+
* documents for itself.
|
|
34
|
+
*/
|
|
35
|
+
import { spawn } from 'node:child_process';
|
|
36
|
+
import { open } from 'node:fs/promises';
|
|
37
|
+
import { constants as osConstants } from 'node:os';
|
|
38
|
+
import { isAbsolute, resolve as resolvePath } from 'node:path';
|
|
39
|
+
import { RuntimeFault } from '../core/schemas/faults.js';
|
|
40
|
+
import { probeParsers } from '../ports/environment-probe-port.js';
|
|
41
|
+
import { evaluateCommandTarget } from './command-target-policy.js';
|
|
42
|
+
import { runPortMethod } from './port-boundary.js';
|
|
43
|
+
function stringifyScalar(value) {
|
|
44
|
+
if (typeof value === 'string')
|
|
45
|
+
return value;
|
|
46
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
47
|
+
return String(value);
|
|
48
|
+
return JSON.stringify(value);
|
|
49
|
+
}
|
|
50
|
+
/** Options first as `--{key}`, positionals after, both in the record's own key order. Exported for its own unit tests: this is the one place shell-injection safety is decided. */
|
|
51
|
+
export function buildArgv(channels) {
|
|
52
|
+
const optionTokens = [];
|
|
53
|
+
for (const [key, value] of Object.entries(channels.option)) {
|
|
54
|
+
if (value === false)
|
|
55
|
+
continue;
|
|
56
|
+
optionTokens.push(`--${key}`);
|
|
57
|
+
if (value !== true)
|
|
58
|
+
optionTokens.push(stringifyScalar(value));
|
|
59
|
+
}
|
|
60
|
+
const argumentTokens = Object.values(channels.argument).map(stringifyScalar);
|
|
61
|
+
return [...optionTokens, ...argumentTokens];
|
|
62
|
+
}
|
|
63
|
+
function buildEnv(declared) {
|
|
64
|
+
const base = {};
|
|
65
|
+
if (process.env.PATH !== undefined)
|
|
66
|
+
base.PATH = process.env.PATH;
|
|
67
|
+
return { ...base, ...declared };
|
|
68
|
+
}
|
|
69
|
+
function buildStdin(stdin) {
|
|
70
|
+
if (stdin.kind === 'absent')
|
|
71
|
+
return { kind: 'absent' };
|
|
72
|
+
if (stdin.kind === 'json')
|
|
73
|
+
return { kind: 'json', value: stdin.value };
|
|
74
|
+
return { kind: 'text', value: stdin.value };
|
|
75
|
+
}
|
|
76
|
+
/** A heuristic, not a declared content type: the port carries none for a process stream. JSON-shaped text reads as `json`; anything else as `text`. */
|
|
77
|
+
function bodyFromText(text) {
|
|
78
|
+
if (text.length === 0)
|
|
79
|
+
return { kind: 'text', value: text };
|
|
80
|
+
try {
|
|
81
|
+
return { kind: 'json', value: JSON.parse(text) };
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return { kind: 'text', value: text };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function capped(detail) {
|
|
88
|
+
return new RuntimeFault('budget-exhausted', 'CommandProbeRequest', detail);
|
|
89
|
+
}
|
|
90
|
+
function forbidden(detail) {
|
|
91
|
+
return new RuntimeFault('forbidden-target', 'ProbeRequest', detail);
|
|
92
|
+
}
|
|
93
|
+
function writeStdin(child, stdin) {
|
|
94
|
+
const stream = child.stdin;
|
|
95
|
+
if (stream === null)
|
|
96
|
+
return;
|
|
97
|
+
// A child that exits before reading stdin closes its end of the pipe, and
|
|
98
|
+
// the pending `end()` write below then raises EPIPE on this stream, not on
|
|
99
|
+
// `child` itself. With no listener that is an unhandled 'error' and crashes
|
|
100
|
+
// the host process; the run itself is unaffected, since `close` still fires
|
|
101
|
+
// and carries the exit code this adapter already reads it from.
|
|
102
|
+
stream.on('error', () => { });
|
|
103
|
+
if (stdin.kind === 'absent') {
|
|
104
|
+
stream.end();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
stream.end(stdin.kind === 'json' ? JSON.stringify(stdin.value) : String(stdin.value));
|
|
108
|
+
}
|
|
109
|
+
/** `-signalNumber` when a signal ended the process, matching `CommandProbeObservation.exitCode`'s own documented convention. */
|
|
110
|
+
function exitCodeOf(code, signalName) {
|
|
111
|
+
if (code !== null)
|
|
112
|
+
return code;
|
|
113
|
+
if (signalName === null)
|
|
114
|
+
return -1;
|
|
115
|
+
const numeric = osConstants.signals[signalName];
|
|
116
|
+
return numeric === undefined ? -1 : -numeric;
|
|
117
|
+
}
|
|
118
|
+
async function runChildProcess(request, signal) {
|
|
119
|
+
return new Promise((settlePromise, rejectPromise) => {
|
|
120
|
+
const child = spawn(request.target, [...request.subcommandPath, ...request.argv], {
|
|
121
|
+
cwd: request.cwd,
|
|
122
|
+
env: request.env,
|
|
123
|
+
shell: false,
|
|
124
|
+
signal,
|
|
125
|
+
});
|
|
126
|
+
let stdout = '';
|
|
127
|
+
let stderr = '';
|
|
128
|
+
let settled = false;
|
|
129
|
+
let timedOut = false;
|
|
130
|
+
let overCap = false;
|
|
131
|
+
const finish = (action) => {
|
|
132
|
+
if (settled)
|
|
133
|
+
return;
|
|
134
|
+
settled = true;
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
action();
|
|
137
|
+
};
|
|
138
|
+
const timer = setTimeout(() => {
|
|
139
|
+
timedOut = true;
|
|
140
|
+
child.kill('SIGKILL');
|
|
141
|
+
}, request.maxElapsedMs);
|
|
142
|
+
const capture = (chunk, append, currentLength) => {
|
|
143
|
+
if (currentLength() > request.maxOutputBytes)
|
|
144
|
+
return;
|
|
145
|
+
append(chunk.toString('utf8'));
|
|
146
|
+
if (currentLength() > request.maxOutputBytes) {
|
|
147
|
+
overCap = true;
|
|
148
|
+
child.kill('SIGKILL');
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
child.stdout?.on('data', (chunk) => capture(chunk, (next) => {
|
|
152
|
+
stdout += next;
|
|
153
|
+
}, () => Buffer.byteLength(stdout, 'utf8')));
|
|
154
|
+
child.stderr?.on('data', (chunk) => capture(chunk, (next) => {
|
|
155
|
+
stderr += next;
|
|
156
|
+
}, () => Buffer.byteLength(stderr, 'utf8')));
|
|
157
|
+
child.once('error', (error) => {
|
|
158
|
+
finish(() => rejectPromise(error));
|
|
159
|
+
});
|
|
160
|
+
child.once('close', (code, signalName) => {
|
|
161
|
+
finish(() => {
|
|
162
|
+
if (timedOut) {
|
|
163
|
+
rejectPromise(capped(`exceeded maxElapsedMs (${request.maxElapsedMs}ms) and was killed`));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (overCap) {
|
|
167
|
+
rejectPromise(capped(`stdout or stderr exceeded maxOutputBytes (${request.maxOutputBytes}) and the process was killed`));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
settlePromise({
|
|
171
|
+
exitCode: exitCodeOf(code, signalName),
|
|
172
|
+
stdout,
|
|
173
|
+
stderr,
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
writeStdin(child, request.stdin);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/** Reads at most `maxBytes + 1` bytes rather than the whole file: an oversize artifact is capped before its bytes are loaded, not after, so a run that wrote past the limit cannot make this adapter allocate the excess first. */
|
|
181
|
+
async function readArtifactFile(path, maxBytes) {
|
|
182
|
+
let handle;
|
|
183
|
+
try {
|
|
184
|
+
handle = await open(path, 'r');
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
if (error.code === 'ENOENT') {
|
|
188
|
+
return { present: false, text: '', truncated: false };
|
|
189
|
+
}
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const buffer = Buffer.alloc(maxBytes + 1);
|
|
194
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, 0);
|
|
195
|
+
const truncated = bytesRead > maxBytes;
|
|
196
|
+
const text = buffer
|
|
197
|
+
.subarray(0, Math.min(bytesRead, maxBytes))
|
|
198
|
+
.toString('utf8');
|
|
199
|
+
return { present: true, text, truncated };
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
await handle.close();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The real mechanism: an actual child process, an actual file read. Exported,
|
|
207
|
+
* unlike the other three shipped adapters' defaults, because AD-37's own
|
|
208
|
+
* conformance subject for this port has to exercise real process behaviour
|
|
209
|
+
* (a real timeout, a real output cap, a real argv) the way the `api` arm's
|
|
210
|
+
* subject exercises a real loopback server; a synthetic mechanism would prove
|
|
211
|
+
* nothing about the one thing this adapter exists to get right.
|
|
212
|
+
*/
|
|
213
|
+
export const nodeCommandMechanism = {
|
|
214
|
+
run: runChildProcess,
|
|
215
|
+
readArtifact: readArtifactFile,
|
|
216
|
+
};
|
|
217
|
+
export function createCommandLineAdapter(policy, mechanism = nodeCommandMechanism) {
|
|
218
|
+
return {
|
|
219
|
+
probe: (request, signal) => runPortMethod({
|
|
220
|
+
request,
|
|
221
|
+
requestParser: probeParsers.request,
|
|
222
|
+
responseParser: probeParsers.response,
|
|
223
|
+
requestPath: 'ProbeRequest',
|
|
224
|
+
responsePath: 'ProbeObservation',
|
|
225
|
+
signal,
|
|
226
|
+
mechanism: async (parsed, innerSignal) => {
|
|
227
|
+
if (parsed.kind !== 'cli') {
|
|
228
|
+
// This adapter authorizes no `api` target, so an `api` request
|
|
229
|
+
// meets the same "the mapping names nothing" denial an
|
|
230
|
+
// unmapped interfaceId would, before any process spawns.
|
|
231
|
+
throw forbidden('this adapter runs cli requests only; no api target is ever authorized');
|
|
232
|
+
}
|
|
233
|
+
const decision = evaluateCommandTarget(policy, {
|
|
234
|
+
interfaceId: parsed.interfaceId,
|
|
235
|
+
executable: parsed.executable,
|
|
236
|
+
subcommandPath: parsed.subcommandPath,
|
|
237
|
+
});
|
|
238
|
+
if (!decision.allowed)
|
|
239
|
+
throw forbidden(decision.detail);
|
|
240
|
+
const { authorization } = decision;
|
|
241
|
+
const runResult = await mechanism.run({
|
|
242
|
+
target: authorization.target,
|
|
243
|
+
subcommandPath: parsed.subcommandPath,
|
|
244
|
+
argv: buildArgv(parsed.channels),
|
|
245
|
+
env: buildEnv(parsed.channels.environment),
|
|
246
|
+
stdin: buildStdin(parsed.channels.stdin),
|
|
247
|
+
cwd: authorization.cwd,
|
|
248
|
+
maxElapsedMs: authorization.maxElapsedMs,
|
|
249
|
+
maxOutputBytes: authorization.maxOutputBytes,
|
|
250
|
+
}, innerSignal);
|
|
251
|
+
const artifactEntries = await Promise.all(Object.entries(authorization.artifacts).map(async ([artifactId, relativePath]) => {
|
|
252
|
+
const absolute = isAbsolute(relativePath)
|
|
253
|
+
? relativePath
|
|
254
|
+
: resolvePath(authorization.cwd, relativePath);
|
|
255
|
+
const read = await mechanism.readArtifact(absolute, authorization.maxOutputBytes);
|
|
256
|
+
if (read.truncated) {
|
|
257
|
+
throw capped(`artifact "${artifactId}" exceeded maxOutputBytes (${authorization.maxOutputBytes})`);
|
|
258
|
+
}
|
|
259
|
+
return [artifactId, read];
|
|
260
|
+
}));
|
|
261
|
+
return { parsed, runResult, artifactEntries };
|
|
262
|
+
},
|
|
263
|
+
assemble: (raw) => {
|
|
264
|
+
const { parsed, runResult, artifactEntries } = raw;
|
|
265
|
+
const observation = {
|
|
266
|
+
kind: 'cli',
|
|
267
|
+
probeId: parsed.probeId,
|
|
268
|
+
interfaceId: parsed.interfaceId,
|
|
269
|
+
operationId: parsed.operationId,
|
|
270
|
+
exitCode: runResult.exitCode,
|
|
271
|
+
stdout: bodyFromText(runResult.stdout),
|
|
272
|
+
stderr: bodyFromText(runResult.stderr),
|
|
273
|
+
artifacts: Object.fromEntries(artifactEntries.map(([id, read]) => [
|
|
274
|
+
id,
|
|
275
|
+
read.present
|
|
276
|
+
? bodyFromText(read.text)
|
|
277
|
+
: { kind: 'absent' },
|
|
278
|
+
])),
|
|
279
|
+
};
|
|
280
|
+
return observation;
|
|
281
|
+
},
|
|
282
|
+
}),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AD-35's default-deny decision for the `cli` mechanism, as a pure function.
|
|
3
|
+
*
|
|
4
|
+
* Lives under `adapters/` rather than beside `core/probe/target-policy.ts`
|
|
5
|
+
* (the `api` evaluator), even though the two are otherwise siblings: the
|
|
6
|
+
* repository's mechanically-enforced dependency direction permits
|
|
7
|
+
* `adapters/` to import `core/schemas` but not `core/`, so a
|
|
8
|
+
* target-authorization decision an adapter calls has to be adapter-owned
|
|
9
|
+
* infrastructure, not a `core/` module. `target-policy.ts` itself is never
|
|
10
|
+
* imported from `src/adapters/` for the same reason — no shipped `api`
|
|
11
|
+
* adapter exists to call it, which is the gap this file closes for `cli`.
|
|
12
|
+
*/
|
|
13
|
+
import type { CommandTargetAuthorization, CommandTargetPolicy } from '../core/schemas/probe-policy.ts';
|
|
14
|
+
/** Why a command target was denied. Thrown as the single AD-28 `forbidden-target` fault, same as the HTTP reasons. */
|
|
15
|
+
export declare const COMMAND_DENIAL_REASONS: readonly ['interface-not-authorized', 'executable-not-authorized', 'subcommand-not-authorized'];
|
|
16
|
+
export type CommandDenialReason = (typeof COMMAND_DENIAL_REASONS)[number];
|
|
17
|
+
export type CommandResolvedTarget = {
|
|
18
|
+
readonly interfaceId: string;
|
|
19
|
+
readonly executable: string;
|
|
20
|
+
readonly subcommandPath: readonly string[];
|
|
21
|
+
};
|
|
22
|
+
export type CommandPolicyDecision = {
|
|
23
|
+
readonly allowed: true;
|
|
24
|
+
readonly authorization: CommandTargetAuthorization;
|
|
25
|
+
} | {
|
|
26
|
+
readonly allowed: false;
|
|
27
|
+
readonly reason: CommandDenialReason;
|
|
28
|
+
readonly detail: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The interface-and-executable check runs first, so an unmapped pair never
|
|
32
|
+
* reaches the subcommand comparison. Where several authorizations name one
|
|
33
|
+
* pair, each is tried in declaration order and the first that allows wins;
|
|
34
|
+
* mirrors `evaluateTarget`'s own ordering rule for the same reason.
|
|
35
|
+
*/
|
|
36
|
+
export declare function evaluateCommandTarget(policy: CommandTargetPolicy, target: CommandResolvedTarget): CommandPolicyDecision;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Why a command target was denied. Thrown as the single AD-28 `forbidden-target` fault, same as the HTTP reasons. */
|
|
2
|
+
export const COMMAND_DENIAL_REASONS = [
|
|
3
|
+
'interface-not-authorized',
|
|
4
|
+
'executable-not-authorized',
|
|
5
|
+
'subcommand-not-authorized',
|
|
6
|
+
];
|
|
7
|
+
function subcommandPathsMatch(a, b) {
|
|
8
|
+
return (a.length === b.length && a.every((segment, index) => segment === b[index]));
|
|
9
|
+
}
|
|
10
|
+
function deny(reason, detail) {
|
|
11
|
+
return { allowed: false, reason, detail };
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The interface-and-executable check runs first, so an unmapped pair never
|
|
15
|
+
* reaches the subcommand comparison. Where several authorizations name one
|
|
16
|
+
* pair, each is tried in declaration order and the first that allows wins;
|
|
17
|
+
* mirrors `evaluateTarget`'s own ordering rule for the same reason.
|
|
18
|
+
*/
|
|
19
|
+
export function evaluateCommandTarget(policy, target) {
|
|
20
|
+
const matchingPair = policy.authorizations.filter((authorization) => authorization.interfaceId === target.interfaceId &&
|
|
21
|
+
authorization.executable === target.executable);
|
|
22
|
+
if (matchingPair.length === 0) {
|
|
23
|
+
return deny(policy.authorizations.some((authorization) => authorization.interfaceId === target.interfaceId)
|
|
24
|
+
? 'executable-not-authorized'
|
|
25
|
+
: 'interface-not-authorized', `no authorization names interface "${target.interfaceId}" and executable "${target.executable}" together`);
|
|
26
|
+
}
|
|
27
|
+
for (const authorization of matchingPair) {
|
|
28
|
+
if (authorization.permittedSubcommandPaths.some((permitted) => subcommandPathsMatch(permitted, target.subcommandPath))) {
|
|
29
|
+
return { allowed: true, authorization };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return deny('subcommand-not-authorized', `subcommand path [${target.subcommandPath.join(', ')}] is not among the authorized paths for interface "${target.interfaceId}" and executable "${target.executable}"`);
|
|
33
|
+
}
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* subpath and the root barrel keeps the `root -> adapters` edge the matrix
|
|
5
5
|
* does not grant.
|
|
6
6
|
*/
|
|
7
|
+
export type { CommandMechanism, CommandRunRequest, CommandRunResult, } from './command-line-adapter.ts';
|
|
8
|
+
export { createCommandLineAdapter, nodeCommandMechanism, } from './command-line-adapter.ts';
|
|
7
9
|
export type { CorpusMechanism } from './local-corpus-adapter.ts';
|
|
8
10
|
export { createLocalCorpusAdapter } from './local-corpus-adapter.ts';
|
|
9
11
|
export type { FileSystemMechanism } from './node-file-system-adapter.ts';
|
package/dist/adapters/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { createCommandLineAdapter, nodeCommandMechanism, } from './command-line-adapter.js';
|
|
1
2
|
export { createLocalCorpusAdapter } from './local-corpus-adapter.js';
|
|
2
3
|
export { createNodeFileSystemAdapter } from './node-file-system-adapter.js';
|
|
3
4
|
export { createSystemClockAdapter } from './system-clock-adapter.js';
|
package/dist/cli/render.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The four shapes the binary writes, and the exit-code table two documents
|
|
3
|
-
* share. Every line the CLI emits is produced here, so a format change is one
|
|
4
|
-
* file.
|
|
5
|
-
*/
|
|
6
1
|
import { type Diagnostic } from '../application/index.ts';
|
|
7
2
|
/**
|
|
8
3
|
* Delegates to `serializeArtifact`; the canonical bytes are not re-derived
|
|
@@ -13,6 +8,8 @@ export declare function renderArtifact(artifact: unknown, artifactPath: string):
|
|
|
13
8
|
export declare function renderDiagnostic(diagnostic: Diagnostic): string;
|
|
14
9
|
/**
|
|
15
10
|
* `eval-quality: <code>: <artifactPath>: <detail>` for either error class.
|
|
11
|
+
* A fault carrying a Zod error as its cause prints that error's issues under
|
|
12
|
+
* the first line, one indented `<location>: <message>` per issue.
|
|
16
13
|
* Anything else falls back to `String(error)`, which is what a defect in our
|
|
17
14
|
* own code looks like from outside.
|
|
18
15
|
*/
|
package/dist/cli/render.js
CHANGED
|
@@ -3,8 +3,55 @@
|
|
|
3
3
|
* share. Every line the CLI emits is produced here, so a format change is one
|
|
4
4
|
* file.
|
|
5
5
|
*/
|
|
6
|
+
import { z } from 'zod';
|
|
6
7
|
import { RuntimeFault, StructuralFailure, serializeArtifact, } from '../application/index.js';
|
|
7
8
|
const PREFIX = 'eval-quality';
|
|
9
|
+
/**
|
|
10
|
+
* How many parse issues the renderer prints before it stops and counts the
|
|
11
|
+
* rest.
|
|
12
|
+
* An author fixing a contract reads the first few and edits; a wall of four
|
|
13
|
+
* hundred lines from one missing required key teaches nothing and buries the
|
|
14
|
+
* first line, which is the one that names the code.
|
|
15
|
+
*/
|
|
16
|
+
const ISSUE_LIMIT = 20;
|
|
17
|
+
/**
|
|
18
|
+
* An RFC 6901 pointer over the failing value, so the location the renderer
|
|
19
|
+
* prints is the same spelling the contract itself uses to address evidence.
|
|
20
|
+
* A numeric segment is an array index and is printed as one.
|
|
21
|
+
*/
|
|
22
|
+
function issueLocation(path) {
|
|
23
|
+
if (path.length === 0)
|
|
24
|
+
return '(root)';
|
|
25
|
+
return path
|
|
26
|
+
.map((segment) => String(segment).replaceAll('~', '~0').replaceAll('/', '~1'))
|
|
27
|
+
.map((segment) => `/${segment}`)
|
|
28
|
+
.join('');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The issue list a failed parse already carries, which the first line of the
|
|
32
|
+
* error alone does not expose.
|
|
33
|
+
* A contract author outside this repository has the published schema and this
|
|
34
|
+
* message and nothing else, so a parse failure that names no field costs a
|
|
35
|
+
* bisect over the whole document.
|
|
36
|
+
* Issues are sorted by location so two runs over the same input print the same
|
|
37
|
+
* bytes.
|
|
38
|
+
* A Zod issue message names the expectation and never echoes the value that
|
|
39
|
+
* failed, which matters because this goes to stderr and a contract declares an
|
|
40
|
+
* environment channel; the one thing a message does quote is an unrecognized
|
|
41
|
+
* key name, which is a field name.
|
|
42
|
+
*/
|
|
43
|
+
function renderParseIssues(cause) {
|
|
44
|
+
if (!(cause instanceof z.ZodError))
|
|
45
|
+
return '';
|
|
46
|
+
const lines = cause.issues
|
|
47
|
+
.map((issue) => ` ${issueLocation(issue.path)}: ${issue.message}`)
|
|
48
|
+
.sort();
|
|
49
|
+
const shown = lines.slice(0, ISSUE_LIMIT);
|
|
50
|
+
const hidden = lines.length - shown.length;
|
|
51
|
+
if (hidden > 0)
|
|
52
|
+
shown.push(` ... and ${hidden} more`);
|
|
53
|
+
return shown.length === 0 ? '' : `\n${shown.join('\n')}`;
|
|
54
|
+
}
|
|
8
55
|
/**
|
|
9
56
|
* Delegates to `serializeArtifact`; the canonical bytes are not re-derived
|
|
10
57
|
* here, so the text written to stdout is the text `digestArtifact` hashes.
|
|
@@ -18,6 +65,8 @@ export function renderDiagnostic(diagnostic) {
|
|
|
18
65
|
}
|
|
19
66
|
/**
|
|
20
67
|
* `eval-quality: <code>: <artifactPath>: <detail>` for either error class.
|
|
68
|
+
* A fault carrying a Zod error as its cause prints that error's issues under
|
|
69
|
+
* the first line, one indented `<location>: <message>` per issue.
|
|
21
70
|
* Anything else falls back to `String(error)`, which is what a defect in our
|
|
22
71
|
* own code looks like from outside.
|
|
23
72
|
*/
|
|
@@ -27,7 +76,8 @@ export function renderError(error) {
|
|
|
27
76
|
const detail = error.message.startsWith(prefix)
|
|
28
77
|
? error.message.slice(prefix.length)
|
|
29
78
|
: error.message;
|
|
30
|
-
|
|
79
|
+
const issues = renderParseIssues(error.cause);
|
|
80
|
+
return `${PREFIX}: ${error.code}: ${error.artifactPath}: ${detail}${issues}`;
|
|
31
81
|
}
|
|
32
82
|
return `${PREFIX}: ${String(error)}`;
|
|
33
83
|
}
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import type { EvalContract } from '../schemas/eval-contract.ts';
|
|
2
2
|
import type { InteractionStep } from '../schemas/plan.ts';
|
|
3
|
-
import {
|
|
3
|
+
import type { InputChannelName } from '../schemas/pointer.ts';
|
|
4
4
|
import { type EvidenceTarget } from '../seal/plan-index.ts';
|
|
5
|
+
/**
|
|
6
|
+
* The one channel a captured pointer may name: whichever channel the
|
|
7
|
+
* referenced operation's own response descriptor describes.
|
|
8
|
+
*
|
|
9
|
+
* `ResponseDescriptor` declares `requiredKeys`, `permittedKeys`, `types`,
|
|
10
|
+
* `successIndicator`, `channelRoles`, and `collectionLocations`, and every one
|
|
11
|
+
* of them is about the channel the operation nominates. Off an interface that
|
|
12
|
+
* speaks HTTP that is the response body; off a command it is the stream the
|
|
13
|
+
* operation names. `response-headers` and `response-status` are never it,
|
|
14
|
+
* because `Observation.responseHeaders` admits objects, arrays, numbers, and
|
|
15
|
+
* `null`, so a header capture compiled as a `string` could resolve to an
|
|
16
|
+
* object at score time.
|
|
17
|
+
*/
|
|
5
18
|
/** One `{ captured }` binding, resolved to the pointer target it addresses. */
|
|
6
19
|
export type CapturedBinding = {
|
|
7
|
-
readonly transportChannel:
|
|
20
|
+
readonly transportChannel: InputChannelName;
|
|
8
21
|
readonly key: string;
|
|
9
22
|
readonly pointer: string;
|
|
10
23
|
readonly target: EvidenceTarget;
|
|
@@ -45,11 +58,17 @@ export declare function capturedBindings(step: InteractionStep): readonly Captur
|
|
|
45
58
|
export declare function checkBindingCycle(contract: EvalContract): void;
|
|
46
59
|
/**
|
|
47
60
|
* `captured-channel-undeclared`: a captured pointer naming any AD-26 channel
|
|
48
|
-
* but
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
61
|
+
* but the one the referenced operation's response descriptor describes. That
|
|
62
|
+
* is `response-body` off an interface that speaks HTTP and whichever channel
|
|
63
|
+
* `descriptorChannel` nominates off one that runs behind a command, which is
|
|
64
|
+
* the same rule read against the declaration rather than assumed.
|
|
65
|
+
*
|
|
66
|
+
* Every other channel is refused for a reason that does not depend on the
|
|
67
|
+
* kind: `call-inputs` addresses a step's own request, and `response-headers`,
|
|
68
|
+
* `response-status`, and `exit-code` have no declared structure to give a
|
|
69
|
+
* captured value a type. On the `artifact` channel the identifier is compared
|
|
70
|
+
* too, so a capture from a file the operation writes but does not describe is
|
|
71
|
+
* refused alongside one from a file it never writes.
|
|
53
72
|
*/
|
|
54
73
|
export declare function checkCapturedChannel(contract: EvalContract): void;
|
|
55
74
|
/**
|