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
|
@@ -31,10 +31,11 @@ export type EnvironmentProbePort = {
|
|
|
31
31
|
};
|
|
32
32
|
/** the boundary parsers `application/` and the conformance suite validate with. */
|
|
33
33
|
export declare const probeParsers: {
|
|
34
|
-
readonly request: import("zod").ZodObject<{
|
|
34
|
+
readonly request: import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
|
|
35
35
|
probeId: import("zod").ZodString;
|
|
36
36
|
interfaceId: import("zod").ZodString;
|
|
37
37
|
operationId: import("zod").ZodString;
|
|
38
|
+
kind: import("zod").ZodLiteral<"api">;
|
|
38
39
|
method: import("zod").ZodEnum<{
|
|
39
40
|
DELETE: "DELETE";
|
|
40
41
|
GET: "GET";
|
|
@@ -56,11 +57,33 @@ export declare const probeParsers: {
|
|
|
56
57
|
kind: import("zod").ZodLiteral<"absent">;
|
|
57
58
|
}, import("zod/v4/core").$strict>], "kind">;
|
|
58
59
|
}, import("zod/v4/core").$strict>;
|
|
59
|
-
}, import("zod/v4/core").$strict
|
|
60
|
-
readonly response: import("zod").ZodObject<{
|
|
60
|
+
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
61
61
|
probeId: import("zod").ZodString;
|
|
62
62
|
interfaceId: import("zod").ZodString;
|
|
63
63
|
operationId: import("zod").ZodString;
|
|
64
|
+
kind: import("zod").ZodLiteral<"cli">;
|
|
65
|
+
executable: import("zod").ZodString;
|
|
66
|
+
subcommandPath: import("zod").ZodArray<import("zod").ZodString>;
|
|
67
|
+
channels: import("zod").ZodObject<{
|
|
68
|
+
argument: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodType<import("../core/schemas/primitives.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../core/schemas/primitives.ts").JsonValue, unknown>>>;
|
|
69
|
+
option: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodType<import("../core/schemas/primitives.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../core/schemas/primitives.ts").JsonValue, unknown>>>;
|
|
70
|
+
environment: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodString>;
|
|
71
|
+
stdin: import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
|
|
72
|
+
kind: import("zod").ZodLiteral<"json">;
|
|
73
|
+
value: import("zod").ZodType<import("../core/schemas/primitives.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../core/schemas/primitives.ts").JsonValue, unknown>>;
|
|
74
|
+
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
75
|
+
kind: import("zod").ZodLiteral<"text">;
|
|
76
|
+
value: import("zod").ZodString;
|
|
77
|
+
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
78
|
+
kind: import("zod").ZodLiteral<"absent">;
|
|
79
|
+
}, import("zod/v4/core").$strict>], "kind">;
|
|
80
|
+
}, import("zod/v4/core").$strict>;
|
|
81
|
+
}, import("zod/v4/core").$strict>], "kind">;
|
|
82
|
+
readonly response: import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
|
|
83
|
+
probeId: import("zod").ZodString;
|
|
84
|
+
interfaceId: import("zod").ZodString;
|
|
85
|
+
operationId: import("zod").ZodString;
|
|
86
|
+
kind: import("zod").ZodLiteral<"api">;
|
|
64
87
|
status: import("zod").ZodInt;
|
|
65
88
|
headers: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodString>;
|
|
66
89
|
body: import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
|
|
@@ -72,5 +95,38 @@ export declare const probeParsers: {
|
|
|
72
95
|
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
73
96
|
kind: import("zod").ZodLiteral<"absent">;
|
|
74
97
|
}, import("zod/v4/core").$strict>], "kind">;
|
|
75
|
-
}, import("zod/v4/core").$strict
|
|
98
|
+
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
99
|
+
probeId: import("zod").ZodString;
|
|
100
|
+
interfaceId: import("zod").ZodString;
|
|
101
|
+
operationId: import("zod").ZodString;
|
|
102
|
+
kind: import("zod").ZodLiteral<"cli">;
|
|
103
|
+
exitCode: import("zod").ZodInt;
|
|
104
|
+
stdout: import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
|
|
105
|
+
kind: import("zod").ZodLiteral<"json">;
|
|
106
|
+
value: import("zod").ZodType<import("../core/schemas/primitives.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../core/schemas/primitives.ts").JsonValue, unknown>>;
|
|
107
|
+
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
108
|
+
kind: import("zod").ZodLiteral<"text">;
|
|
109
|
+
value: import("zod").ZodString;
|
|
110
|
+
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
111
|
+
kind: import("zod").ZodLiteral<"absent">;
|
|
112
|
+
}, import("zod/v4/core").$strict>], "kind">;
|
|
113
|
+
stderr: import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
|
|
114
|
+
kind: import("zod").ZodLiteral<"json">;
|
|
115
|
+
value: import("zod").ZodType<import("../core/schemas/primitives.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../core/schemas/primitives.ts").JsonValue, unknown>>;
|
|
116
|
+
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
117
|
+
kind: import("zod").ZodLiteral<"text">;
|
|
118
|
+
value: import("zod").ZodString;
|
|
119
|
+
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
120
|
+
kind: import("zod").ZodLiteral<"absent">;
|
|
121
|
+
}, import("zod/v4/core").$strict>], "kind">;
|
|
122
|
+
artifacts: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
|
|
123
|
+
kind: import("zod").ZodLiteral<"json">;
|
|
124
|
+
value: import("zod").ZodType<import("../core/schemas/primitives.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../core/schemas/primitives.ts").JsonValue, unknown>>;
|
|
125
|
+
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
126
|
+
kind: import("zod").ZodLiteral<"text">;
|
|
127
|
+
value: import("zod").ZodString;
|
|
128
|
+
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
129
|
+
kind: import("zod").ZodLiteral<"absent">;
|
|
130
|
+
}, import("zod/v4/core").$strict>], "kind">>;
|
|
131
|
+
}, import("zod/v4/core").$strict>], "kind">;
|
|
76
132
|
};
|
|
@@ -7,13 +7,14 @@ export type ConformanceOutcome = {
|
|
|
7
7
|
readonly passed: boolean;
|
|
8
8
|
readonly detail: string;
|
|
9
9
|
};
|
|
10
|
-
export type ConformancePort = 'corpus' | 'clock' | 'file-system' | 'environment-probe';
|
|
10
|
+
export type ConformancePort = 'corpus' | 'clock' | 'file-system' | 'environment-probe' | 'command-probe';
|
|
11
11
|
/** how many outcomes a complete run of each port produces. Asserted as literals by fixture 58. */
|
|
12
12
|
export declare const CONFORMANCE_OUTCOME_COUNTS: {
|
|
13
13
|
readonly corpus: 6;
|
|
14
14
|
readonly clock: 6;
|
|
15
15
|
readonly 'file-system': 12;
|
|
16
16
|
readonly 'environment-probe': 19;
|
|
17
|
+
readonly 'command-probe': 15;
|
|
17
18
|
};
|
|
18
19
|
export type ConformanceReport = {
|
|
19
20
|
readonly subject: string;
|
|
@@ -19,6 +19,7 @@ export const CONFORMANCE_OUTCOME_COUNTS = {
|
|
|
19
19
|
clock: 6,
|
|
20
20
|
'file-system': 12,
|
|
21
21
|
'environment-probe': 19,
|
|
22
|
+
'command-probe': 15,
|
|
22
23
|
};
|
|
23
24
|
/** "Promptly" with no bound is unfalsifiable. One second clears any real adapter's abort latency and sits well inside a default test timeout. */
|
|
24
25
|
export const DEFAULT_ABORT_BUDGET_MS = 1000;
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
export type { RuntimeFaultCode } from '../core/schemas/faults.ts';
|
|
14
14
|
export { RUNTIME_FAULT_CODES, RuntimeFault } from '../core/schemas/faults.ts';
|
|
15
15
|
export type { ClockReadRequest, ClockReadResponse, CorpusResolveRequest, CorpusResolveResponse, FileReadRequest, FileReadResponse, FileWriteRequest, FileWriteResponse, ProbeObservation, ProbeObservedBody, ProbeRequest, ProbeRequestBody, } from '../core/schemas/port-messages.ts';
|
|
16
|
-
export type { ProbeTargetAuthorization, ProbeTargetPolicy, } from '../core/schemas/probe-policy.ts';
|
|
16
|
+
export type { CommandTargetAuthorization, CommandTargetPolicy, ProbeTargetAuthorization, ProbeTargetPolicy, } from '../core/schemas/probe-policy.ts';
|
|
17
17
|
export type { ClockPort } from '../ports/clock-port.ts';
|
|
18
18
|
export { clockReadParsers } from '../ports/clock-port.ts';
|
|
19
19
|
export type { CorpusPort } from '../ports/corpus-port.ts';
|
|
@@ -24,5 +24,5 @@ export type { FileSystemPort } from '../ports/file-system-port.ts';
|
|
|
24
24
|
export { fileReadParsers, fileWriteParsers, } from '../ports/file-system-port.ts';
|
|
25
25
|
export type { BuiltSubject, ConformanceOutcome, ConformancePort, ConformanceReport, PortSubject, ScenarioKind, } from './conformance.ts';
|
|
26
26
|
export { CONFORMANCE_OUTCOME_COUNTS, formatConformanceReport, runClockPortConformance, runCorpusPortConformance, runFileSystemPortConformance, } from './conformance.ts';
|
|
27
|
-
export type { ProbeSubject } from './probe-conformance.ts';
|
|
28
|
-
export { runEnvironmentProbePortConformance } from './probe-conformance.ts';
|
|
27
|
+
export type { CommandProbeSubject, ProbeSubject } from './probe-conformance.ts';
|
|
28
|
+
export { runCommandLineProbeConformance, runEnvironmentProbePortConformance, } from './probe-conformance.ts';
|
package/dist/testing/index.js
CHANGED
|
@@ -16,4 +16,4 @@ export { corpusResolveParsers } from '../ports/corpus-port.js';
|
|
|
16
16
|
export { probeParsers } from '../ports/environment-probe-port.js';
|
|
17
17
|
export { fileReadParsers, fileWriteParsers, } from '../ports/file-system-port.js';
|
|
18
18
|
export { CONFORMANCE_OUTCOME_COUNTS, formatConformanceReport, runClockPortConformance, runCorpusPortConformance, runFileSystemPortConformance, } from './conformance.js';
|
|
19
|
-
export { runEnvironmentProbePortConformance } from './probe-conformance.js';
|
|
19
|
+
export { runCommandLineProbeConformance, runEnvironmentProbePortConformance, } from './probe-conformance.js';
|
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AD-35's
|
|
3
|
-
* subject supplies its policy and one request per denial,
|
|
4
|
-
* how its own interface-to-target mapping is wired.
|
|
2
|
+
* AD-35's extra assertions for the environment-probe port, one arm per
|
|
3
|
+
* mechanism. The subject supplies its policy and one request per denial,
|
|
4
|
+
* since only it knows how its own interface-to-target mapping is wired.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* `runEnvironmentProbePortConformance` is the `api` arm: thirteen assertions,
|
|
7
|
+
* every scenario HTTP (redirects, methods, schemes, an anomalous status).
|
|
8
|
+
* `runCommandLineProbeConformance` is the `cli` arm, added once
|
|
9
|
+
* `CommandTargetPolicy` gave the mechanism something to authorize: an
|
|
10
|
+
* unmapped interface, an unmapped executable, an unauthorized subcommand path,
|
|
11
|
+
* a non-zero exit read as an observation, a shell-metacharacter argument
|
|
12
|
+
* proven to reach the process as one literal token rather than a shell
|
|
13
|
+
* expansion, a declared artifact captured, and both caps enforced. The two
|
|
14
|
+
* arms are separate functions rather than one, because their subjects need
|
|
15
|
+
* disjoint fixtures (an HTTP redirect chain has no command analogue, and a
|
|
16
|
+
* subcommand allowlist has no HTTP one) and a subject presenting for one
|
|
17
|
+
* mechanism is not asked to fake the other's scenarios.
|
|
18
|
+
*
|
|
19
|
+
* Each assertion in either arm builds its own `'resolves'` subject and reads
|
|
7
20
|
* `underlyingCalls()` from a counter starting at zero, so every count below is
|
|
8
21
|
* absolute.
|
|
9
22
|
*/
|
|
10
23
|
import type { ProbeRequest } from '../core/schemas/port-messages.ts';
|
|
11
|
-
import type { ProbeTargetPolicy } from '../core/schemas/probe-policy.ts';
|
|
24
|
+
import type { CommandTargetPolicy, ProbeTargetPolicy } from '../core/schemas/probe-policy.ts';
|
|
12
25
|
import type { ConformanceReport, PortSubject } from './conformance.ts';
|
|
13
26
|
export type ProbeSubject = PortSubject<ProbeRequest> & {
|
|
14
27
|
readonly policy: ProbeTargetPolicy;
|
|
@@ -43,3 +56,41 @@ export type ProbeSubject = PortSubject<ProbeRequest> & {
|
|
|
43
56
|
* stays declared and adapter-enforced.
|
|
44
57
|
*/
|
|
45
58
|
export declare function runEnvironmentProbePortConformance(subject: ProbeSubject): Promise<ConformanceReport>;
|
|
59
|
+
/**
|
|
60
|
+
* The `cli` arm's subject. Every field is a request `evaluateCommandTarget`
|
|
61
|
+
* decides one specific way against the subject's own `policy`, the same
|
|
62
|
+
* division `ProbeSubject` draws for the `api` arm: the subject knows how its
|
|
63
|
+
* mapping is wired, the suite only knows what each request should produce.
|
|
64
|
+
*/
|
|
65
|
+
export type CommandProbeSubject = PortSubject<ProbeRequest> & {
|
|
66
|
+
readonly policy: CommandTargetPolicy;
|
|
67
|
+
/** a request the policy ALLOWS, run against the subject's own fixture executable. */
|
|
68
|
+
readonly authorizedRequest: ProbeRequest;
|
|
69
|
+
/** an interfaceId no authorization names. */
|
|
70
|
+
readonly unmappedInterfaceRequest: ProbeRequest;
|
|
71
|
+
/** an interfaceId the policy names, paired with an executable it never names. */
|
|
72
|
+
readonly unmappedExecutableRequest: ProbeRequest;
|
|
73
|
+
/** an interface-executable pair the policy names, with a subcommandPath no authorization for it permits. */
|
|
74
|
+
readonly unauthorizedSubcommandRequest: ProbeRequest;
|
|
75
|
+
/** authorized, and answered by the fixture exiting non-zero. */
|
|
76
|
+
readonly nonZeroExitRequest: ProbeRequest;
|
|
77
|
+
/** authorized, carrying a shell-metacharacter value on one argument channel key. */
|
|
78
|
+
readonly injectionRequest: ProbeRequest;
|
|
79
|
+
/** the exact literal value `injectionRequest` carries, so the assertion can confirm the fixture received it byte for byte. */
|
|
80
|
+
readonly injectionArgumentValue: string;
|
|
81
|
+
/** authorized, and its authorization declares one artifact identifier the fixture writes when run. */
|
|
82
|
+
readonly artifactRequest: ProbeRequest;
|
|
83
|
+
readonly artifactId: string;
|
|
84
|
+
readonly artifactExpectedText: string;
|
|
85
|
+
/** authorized against a maxElapsedMs the fixture is made to sleep past. */
|
|
86
|
+
readonly overElapsedRequest: ProbeRequest;
|
|
87
|
+
/** authorized against a maxOutputBytes the fixture is made to write past. */
|
|
88
|
+
readonly overOutputRequest: ProbeRequest;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Fifteen outcomes: the six shared assertions plus the nine above. Unlike the
|
|
92
|
+
* `api` arm, every cap here has an assertion: a command subject's fixture
|
|
93
|
+
* script can be told to overrun a byte cap directly, with no oversize-request
|
|
94
|
+
* problem to work around.
|
|
95
|
+
*/
|
|
96
|
+
export declare function runCommandLineProbeConformance(subject: CommandProbeSubject): Promise<ConformanceReport>;
|
|
@@ -23,9 +23,9 @@ const PROBE_ASSERTIONS = [
|
|
|
23
23
|
request: (subject) => subject.faultingRequest,
|
|
24
24
|
expectation: {
|
|
25
25
|
kind: 'resolves',
|
|
26
|
-
check: (observation) => observation.status === 500
|
|
26
|
+
check: (observation) => observation.kind === 'api' && observation.status === 500
|
|
27
27
|
? undefined
|
|
28
|
-
: `observed status ${observation.status}, expected 500`,
|
|
28
|
+
: `observed ${observation.kind === 'api' ? `status ${observation.status}` : 'a command observation'}, expected status 500`,
|
|
29
29
|
},
|
|
30
30
|
},
|
|
31
31
|
{
|
|
@@ -114,14 +114,43 @@ const PROBE_ASSERTIONS = [
|
|
|
114
114
|
expectation: { kind: 'rejects', code: CAPPED },
|
|
115
115
|
},
|
|
116
116
|
];
|
|
117
|
-
function checkResolved(assertion, expectation, value) {
|
|
117
|
+
function checkResolved(assertion, expectation, value, request) {
|
|
118
118
|
const parsed = probeParsers.response.safeParse(value);
|
|
119
119
|
if (!parsed.success) {
|
|
120
120
|
return titledOutcome(assertion.id, assertion.title, false, 'the resolved value is not a schema-valid ProbeObservation');
|
|
121
121
|
}
|
|
122
|
+
// Schema validity is not correlation. Both messages are unions now, so an
|
|
123
|
+
// adapter can answer a command request with a schema-valid HTTP observation
|
|
124
|
+
// and satisfy every assertion below without running a command or consulting
|
|
125
|
+
// a policy. The four echoed fields are what tie one answer to one question,
|
|
126
|
+
// and `ProbeRequest.probeId`'s own description already says the port returns
|
|
127
|
+
// them unchanged.
|
|
128
|
+
const mismatch = echoMismatch(request, parsed.data);
|
|
129
|
+
if (mismatch !== undefined) {
|
|
130
|
+
return titledOutcome(assertion.id, assertion.title, false, mismatch);
|
|
131
|
+
}
|
|
122
132
|
const complaint = expectation.check?.(parsed.data);
|
|
123
133
|
return titledOutcome(assertion.id, assertion.title, complaint === undefined, complaint ?? '');
|
|
124
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Which of the four echoed fields the observation failed to return unchanged,
|
|
137
|
+
* or `undefined` when it answered the question it was asked.
|
|
138
|
+
*/
|
|
139
|
+
function echoMismatch(request, observation) {
|
|
140
|
+
const echoed = [
|
|
141
|
+
['kind', request.kind, observation.kind],
|
|
142
|
+
['probeId', request.probeId, observation.probeId],
|
|
143
|
+
['interfaceId', request.interfaceId, observation.interfaceId],
|
|
144
|
+
['operationId', request.operationId, observation.operationId],
|
|
145
|
+
];
|
|
146
|
+
for (const [field, asked, answered] of echoed) {
|
|
147
|
+
if (asked === answered)
|
|
148
|
+
continue;
|
|
149
|
+
return `observed ${field} "${answered}" for a request carrying "${asked}", so the answer does not correlate with the question`;
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
/** Reads only `id` and `title`, so both arms' assertion shapes satisfy it structurally. */
|
|
125
154
|
function checkRejected(assertion, expectedCode, error) {
|
|
126
155
|
const view = faultView(error);
|
|
127
156
|
if (view === undefined) {
|
|
@@ -153,12 +182,13 @@ async function runProbeAssertion(assertion, subject) {
|
|
|
153
182
|
if (!run.ok) {
|
|
154
183
|
return titledOutcome(assertion.id, assertion.title, false, run.detail);
|
|
155
184
|
}
|
|
156
|
-
const
|
|
185
|
+
const request = assertion.request(subject);
|
|
186
|
+
const settled = await settle(run.built.port(request, new AbortController().signal));
|
|
157
187
|
let result;
|
|
158
188
|
if (assertion.expectation.kind === 'resolves') {
|
|
159
189
|
result =
|
|
160
190
|
settled.kind === 'resolved'
|
|
161
|
-
? checkResolved(assertion, assertion.expectation, settled.value)
|
|
191
|
+
? checkResolved(assertion, assertion.expectation, settled.value, request)
|
|
162
192
|
: titledOutcome(assertion.id, assertion.title, false, `rejected with ${describeThrown(settled.error)} instead of returning an observation`);
|
|
163
193
|
}
|
|
164
194
|
else {
|
|
@@ -184,3 +214,164 @@ export async function runEnvironmentProbePortConformance(subject) {
|
|
|
184
214
|
}
|
|
185
215
|
return reportOf(subject.name, 'environment-probe', [...shared, ...additional]);
|
|
186
216
|
}
|
|
217
|
+
const COMMAND_ASSERTIONS = [
|
|
218
|
+
{
|
|
219
|
+
id: 'command/allow-authorized-invocation',
|
|
220
|
+
title: 'an explicitly authorized command runs and is observed',
|
|
221
|
+
request: (subject) => subject.authorizedRequest,
|
|
222
|
+
expectation: { kind: 'resolves' },
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: 'command/observe-nonzero-exit',
|
|
226
|
+
title: 'a non-zero exit from an authorized command is an observation, not a fault',
|
|
227
|
+
request: (subject) => subject.nonZeroExitRequest,
|
|
228
|
+
expectation: {
|
|
229
|
+
kind: 'resolves',
|
|
230
|
+
check: (observation) => observation.kind === 'cli' && observation.exitCode !== 0
|
|
231
|
+
? undefined
|
|
232
|
+
: `observed ${observation.kind === 'cli' ? `exit code ${observation.exitCode}` : 'an api observation'}, expected a non-zero exit`,
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: 'command/deny-unmapped-interface',
|
|
237
|
+
title: 'an interface no authorization names is refused before a process spawns',
|
|
238
|
+
request: (subject) => subject.unmappedInterfaceRequest,
|
|
239
|
+
expectation: { kind: 'rejects', code: DENIED },
|
|
240
|
+
expectedCalls: () => 0,
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: 'command/deny-unmapped-executable',
|
|
244
|
+
title: 'an executable the interface is never paired with is refused before a process spawns',
|
|
245
|
+
request: (subject) => subject.unmappedExecutableRequest,
|
|
246
|
+
expectation: { kind: 'rejects', code: DENIED },
|
|
247
|
+
expectedCalls: () => 0,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: 'command/deny-unauthorized-subcommand',
|
|
251
|
+
title: 'a subcommand path outside the authorized set is refused before a process spawns',
|
|
252
|
+
request: (subject) => subject.unauthorizedSubcommandRequest,
|
|
253
|
+
expectation: { kind: 'rejects', code: DENIED },
|
|
254
|
+
expectedCalls: () => 0,
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
// The strongest available proof that no shell ever reads a channel value:
|
|
258
|
+
// an argument built to look like a command substitution, checked to reach
|
|
259
|
+
// the process as the one, unmodified literal token it was declared as.
|
|
260
|
+
id: 'command/argument-passed-literally',
|
|
261
|
+
title: 'a shell-metacharacter argument reaches the process as one unmodified literal token, never a shell expansion',
|
|
262
|
+
request: (subject) => subject.injectionRequest,
|
|
263
|
+
expectation: {
|
|
264
|
+
kind: 'resolves',
|
|
265
|
+
check: (observation, subject) => {
|
|
266
|
+
if (observation.kind !== 'cli' || observation.stdout.kind !== 'json') {
|
|
267
|
+
return 'the observation carries no parsed stdout to check the received argument against';
|
|
268
|
+
}
|
|
269
|
+
const argv = observation.stdout.value.argv;
|
|
270
|
+
const received = Array.isArray(argv) ? argv : [];
|
|
271
|
+
return received.length === 1 &&
|
|
272
|
+
received[0] === subject.injectionArgumentValue
|
|
273
|
+
? undefined
|
|
274
|
+
: `the process observed argv ${JSON.stringify(received)}, expected exactly one element equal to the declared literal`;
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
id: 'command/capture-declared-artifact',
|
|
280
|
+
title: "a declared artifact identifier reads the file the run wrote, keyed by the operation's own identifier",
|
|
281
|
+
request: (subject) => subject.artifactRequest,
|
|
282
|
+
expectation: {
|
|
283
|
+
kind: 'resolves',
|
|
284
|
+
check: (observation, subject) => {
|
|
285
|
+
if (observation.kind !== 'cli') {
|
|
286
|
+
return 'the observation is not a command observation';
|
|
287
|
+
}
|
|
288
|
+
const artifact = observation.artifacts[subject.artifactId];
|
|
289
|
+
return artifact?.kind === 'text' &&
|
|
290
|
+
artifact.value === subject.artifactExpectedText
|
|
291
|
+
? undefined
|
|
292
|
+
: `artifacts["${subject.artifactId}"] was ${JSON.stringify(artifact)}, expected the fixture's declared text`;
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
id: 'command/cap-elapsed',
|
|
298
|
+
title: 'a process past maxElapsedMs is capped, not left to finish',
|
|
299
|
+
request: (subject) => subject.overElapsedRequest,
|
|
300
|
+
expectation: { kind: 'rejects', code: CAPPED },
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
id: 'command/cap-output-bytes',
|
|
304
|
+
title: 'output past maxOutputBytes is capped, not silently truncated and returned',
|
|
305
|
+
request: (subject) => subject.overOutputRequest,
|
|
306
|
+
expectation: { kind: 'rejects', code: CAPPED },
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
function checkCommandResolved(assertion, expectation, value, request, subject) {
|
|
310
|
+
const parsed = probeParsers.response.safeParse(value);
|
|
311
|
+
if (!parsed.success) {
|
|
312
|
+
return titledOutcome(assertion.id, assertion.title, false, 'the resolved value is not a schema-valid ProbeObservation');
|
|
313
|
+
}
|
|
314
|
+
const mismatch = echoMismatch(request, parsed.data);
|
|
315
|
+
if (mismatch !== undefined) {
|
|
316
|
+
return titledOutcome(assertion.id, assertion.title, false, mismatch);
|
|
317
|
+
}
|
|
318
|
+
const complaint = expectation.check?.(parsed.data, subject);
|
|
319
|
+
return titledOutcome(assertion.id, assertion.title, complaint === undefined, complaint ?? '');
|
|
320
|
+
}
|
|
321
|
+
/** `checkCalls`'s own logic, typed against `CommandAssertion`/`CommandProbeSubject` rather than the `api` arm's pair: a function parameter type is checked contravariantly, so the two assertion shapes cannot share one checker. */
|
|
322
|
+
function checkCommandCalls(assertion, subject, built, base) {
|
|
323
|
+
if (assertion.expectedCalls === undefined)
|
|
324
|
+
return base;
|
|
325
|
+
const expected = assertion.expectedCalls(subject);
|
|
326
|
+
if (typeof expected === 'string') {
|
|
327
|
+
return { ...base, passed: false, detail: expected };
|
|
328
|
+
}
|
|
329
|
+
const actual = countCalls(built);
|
|
330
|
+
if (typeof actual === 'string') {
|
|
331
|
+
return { ...base, passed: false, detail: actual };
|
|
332
|
+
}
|
|
333
|
+
if (actual === expected)
|
|
334
|
+
return base;
|
|
335
|
+
return {
|
|
336
|
+
...base,
|
|
337
|
+
passed: false,
|
|
338
|
+
detail: `${base.passed ? '' : `${base.detail}; `}underlyingCalls() was ${actual}, expected ${expected}`,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
async function runCommandAssertion(assertion, subject) {
|
|
342
|
+
const run = await buildScenario(subject, 'resolves');
|
|
343
|
+
if (!run.ok) {
|
|
344
|
+
return titledOutcome(assertion.id, assertion.title, false, run.detail);
|
|
345
|
+
}
|
|
346
|
+
const request = assertion.request(subject);
|
|
347
|
+
const settled = await settle(run.built.port(request, new AbortController().signal));
|
|
348
|
+
let result;
|
|
349
|
+
if (assertion.expectation.kind === 'resolves') {
|
|
350
|
+
result =
|
|
351
|
+
settled.kind === 'resolved'
|
|
352
|
+
? checkCommandResolved(assertion, assertion.expectation, settled.value, request, subject)
|
|
353
|
+
: titledOutcome(assertion.id, assertion.title, false, `rejected with ${describeThrown(settled.error)} instead of returning an observation`);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
result =
|
|
357
|
+
settled.kind === 'rejected'
|
|
358
|
+
? checkRejected(assertion, assertion.expectation.code, settled.error)
|
|
359
|
+
: titledOutcome(assertion.id, assertion.title, false, `resolved instead of rejecting with "${assertion.expectation.code}"`);
|
|
360
|
+
}
|
|
361
|
+
result = checkCommandCalls(assertion, subject, run.built, result);
|
|
362
|
+
return withDispose(result, await disposeScenario(run.built));
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Fifteen outcomes: the six shared assertions plus the nine above. Unlike the
|
|
366
|
+
* `api` arm, every cap here has an assertion: a command subject's fixture
|
|
367
|
+
* script can be told to overrun a byte cap directly, with no oversize-request
|
|
368
|
+
* problem to work around.
|
|
369
|
+
*/
|
|
370
|
+
export async function runCommandLineProbeConformance(subject) {
|
|
371
|
+
const shared = await runSharedAssertions('probe', subject, probeParsers.response);
|
|
372
|
+
const additional = [];
|
|
373
|
+
for (const assertion of COMMAND_ASSERTIONS) {
|
|
374
|
+
additional.push(await runCommandAssertion(assertion, subject));
|
|
375
|
+
}
|
|
376
|
+
return reportOf(subject.name, 'command-probe', [...shared, ...additional]);
|
|
377
|
+
}
|
package/package.json
CHANGED