@openwop/openwop-conformance 1.106.1 → 1.124.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 +3 -3
- package/coverage.md +6 -0
- package/dist/cli.js +89 -45
- package/dist/lib/certification-bundle-verify.js +274 -0
- package/dist/lib/profiles.js +102 -19
- package/dist/lib/requirement-ledger.js +182 -0
- package/dist/lib/requirement-registry.js +110 -0
- package/dist/lib/scenario-disposition.js +249 -0
- package/dist/lib/soft-skip.js +82 -0
- package/fixtures/conformance-a2a-task-roundtrip.json +1 -1
- package/fixtures.md +1 -1
- package/package.json +1 -1
- package/schemas/CORPUS-STAMP.json +2 -2
- package/schemas/README.md +1 -0
- package/schemas/capabilities.schema.json +2 -2
- package/schemas/compensation-policy.schema.json +64 -0
- package/schemas/run-snapshot.schema.json +5 -0
- package/schemas/workflow-chain-pack-manifest.schema.json +125 -0
- package/schemas/workflow-definition.schema.json +4 -0
- package/src/cli.ts +99 -45
- package/src/lib/a2a-fake-peer.ts +290 -110
- package/src/lib/certification-bundle-verify.ts +356 -0
- package/src/lib/error-envelope.ts +77 -0
- package/src/lib/llm-cache-key-recipe.ts +6 -5
- package/src/lib/mcp-fake-server.ts +235 -72
- package/src/lib/profiles.ts +132 -19
- package/src/lib/requirement-ledger.ts +98 -1
- package/src/lib/requirement-registry.ts +47 -4
- package/src/lib/scenario-disposition.ts +291 -0
- package/src/lib/soft-skip.ts +89 -0
- package/src/lib/workflow-chain-expansion.ts +189 -0
- package/src/scenarios/a2a-1-0-agent-card.test.ts +179 -0
- package/src/scenarios/a2a-1-0-task-roundtrip.test.ts +109 -0
- package/src/scenarios/a2a-card-runtime-consistency.test.ts +167 -0
- package/src/scenarios/a2a-peer-authority.test.ts +99 -0
- package/src/scenarios/a2a-task-roundtrip.test.ts +18 -6
- package/src/scenarios/agent-loop-iteration-monotonic.test.ts +3 -2
- package/src/scenarios/agent-loop-stateful-resume.test.ts +3 -2
- package/src/scenarios/agent-loop-version5-shape.test.ts +2 -1
- package/src/scenarios/agent-loop-workspace-snapshot.test.ts +4 -3
- package/src/scenarios/approval-gate-flow.test.ts +5 -4
- package/src/scenarios/artifact-type-store-without-render.test.ts +2 -1
- package/src/scenarios/auth-scim-profile.test.ts +5 -4
- package/src/scenarios/authorization-fail-closed.test.ts +4 -3
- package/src/scenarios/certification-bundle-non-vacuous.test.ts +249 -0
- package/src/scenarios/certification-bundle-redaction.test.ts +182 -0
- package/src/scenarios/certification-floor-enforcement.test.ts +30 -6
- package/src/scenarios/chain-compensation-expansion.test.ts +243 -0
- package/src/scenarios/commitment-fired.test.ts +3 -2
- package/src/scenarios/compensation-behavior.test.ts +63 -0
- package/src/scenarios/compensation-profile.test.ts +144 -1
- package/src/scenarios/conformance-advertised-seam-required.test.ts +118 -0
- package/src/scenarios/conformance-execution-witness.test.ts +104 -0
- package/src/scenarios/conversationCapabilityNegotiation.test.ts +3 -2
- package/src/scenarios/core-manifest-and-extension-registry.test.ts +26 -0
- package/src/scenarios/credential-payload-redaction.test.ts +4 -3
- package/src/scenarios/credentials-capability-shape.test.ts +4 -3
- package/src/scenarios/cross-engine-append-behavior.test.ts +5 -4
- package/src/scenarios/cross-engine-append-ordering.test.ts +3 -2
- package/src/scenarios/cross-host-ancestry-endpoint.test.ts +5 -4
- package/src/scenarios/cross-host-causation-shape.test.ts +3 -2
- package/src/scenarios/cross-workspace-isolation.test.ts +4 -3
- package/src/scenarios/data-residency-admission.test.ts +6 -6
- package/src/scenarios/deadletter-retry-exhaustion.test.ts +5 -4
- package/src/scenarios/distillation-index-roundtrip.test.ts +3 -2
- package/src/scenarios/distillation-secret-carryforward.test.ts +3 -2
- package/src/scenarios/distillation-shape.test.ts +2 -1
- package/src/scenarios/distillation-stable-archive.test.ts +4 -3
- package/src/scenarios/distillation-token-budget.test.ts +4 -3
- package/src/scenarios/envelope-recovery-applied.test.ts +3 -2
- package/src/scenarios/envelope-refusal-shape.test.ts +4 -3
- package/src/scenarios/error-envelope-canonical-shape.test.ts +64 -0
- package/src/scenarios/feedback-correction-redaction.test.ts +4 -3
- package/src/scenarios/feedback-cross-tenant-isolation.test.ts +4 -3
- package/src/scenarios/feedback-fork-not-copied.test.ts +6 -5
- package/src/scenarios/feedback-on-terminal-run.test.ts +4 -3
- package/src/scenarios/feedback-record-and-list.test.ts +4 -3
- package/src/scenarios/feedback-unsupported-501.test.ts +3 -2
- package/src/scenarios/fixtures-valid.test.ts +16 -18
- package/src/scenarios/fs-path-traversal.test.ts +3 -2
- package/src/scenarios/heartbeat-capability-shape.test.ts +2 -1
- package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +4 -3
- package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +6 -5
- package/src/scenarios/heartbeat-runtime-bound.test.ts +4 -3
- package/src/scenarios/mcp-2026-07-28-discover.test.ts +207 -0
- package/src/scenarios/mcp-cache-tenant-scope.test.ts +66 -0
- package/src/scenarios/mcp-current-auth-boundary.test.ts +55 -0
- package/src/scenarios/mcp-extension-opacity.test.ts +79 -0
- package/src/scenarios/mcp-mrtr-roundtrip.test.ts +140 -0
- package/src/scenarios/mcp-stateless-request.test.ts +77 -0
- package/src/scenarios/mcp-tool-roundtrip.test.ts +4 -3
- package/src/scenarios/mcp-toolcall-redaction.test.ts +2 -1
- package/src/scenarios/memory-attribution-emits-on-write.test.ts +6 -5
- package/src/scenarios/memory-attribution-no-content.test.ts +4 -3
- package/src/scenarios/memory-attribution-replay-stable.test.ts +6 -5
- package/src/scenarios/memory-attribution-tenant-scoped.test.ts +4 -3
- package/src/scenarios/memory-compaction-event-emitted.test.ts +3 -2
- package/src/scenarios/memory-compaction-provenance-tag.test.ts +6 -5
- package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +4 -3
- package/src/scenarios/memory-consolidation-idempotent.test.ts +4 -3
- package/src/scenarios/memory-injection-budget.test.ts +4 -3
- package/src/scenarios/metric-emission.test.ts +4 -3
- package/src/scenarios/multi-agent-confidence-escalation.test.ts +5 -4
- package/src/scenarios/multi-agent-handoff-state-machine.test.ts +4 -3
- package/src/scenarios/multi-agent-memory-lifecycle.test.ts +5 -4
- package/src/scenarios/multi-party-conversation-behavioral.test.ts +3 -2
- package/src/scenarios/multi-region-idempotency-behavior.test.ts +7 -6
- package/src/scenarios/multi-region-idempotency.test.ts +4 -3
- package/src/scenarios/normative-example-extraction.test.ts +242 -0
- package/src/scenarios/oauth-authorization-code-roundtrip.test.ts +4 -3
- package/src/scenarios/oauth-capability-shape.test.ts +4 -3
- package/src/scenarios/oauth-connector-redaction.test.ts +4 -3
- package/src/scenarios/openapi-asyncapi-sdk-parity.test.ts +309 -0
- package/src/scenarios/org-position-no-authority-escalation.test.ts +2 -1
- package/src/scenarios/otel-emission-grpc.test.ts +4 -3
- package/src/scenarios/otel-emission.test.ts +4 -3
- package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +5 -4
- package/src/scenarios/otel-trace-propagation.test.ts +4 -3
- package/src/scenarios/pack-registry-isolation.test.ts +4 -3
- package/src/scenarios/pack-registry-publish.test.ts +65 -3
- package/src/scenarios/pack-registry.test.ts +22 -0
- package/src/scenarios/profile-discovery-core-alias.test.ts +137 -0
- package/src/scenarios/profileDerivation.test.ts +2 -1
- package/src/scenarios/provider-usage.test.ts +2 -2
- package/src/scenarios/rate-limit-envelope.test.ts +2 -1
- package/src/scenarios/registry-public.test.ts +6 -5
- package/src/scenarios/replay-divergence-at-refusal.test.ts +9 -8
- package/src/scenarios/replay-llm-cache-key-portable.test.ts +19 -13
- package/src/scenarios/replay-llm-cache-key.test.ts +120 -47
- package/src/scenarios/replay-observable-sequence-determinism.test.ts +5 -4
- package/src/scenarios/replay-side-effect-suppression.test.ts +20 -1
- package/src/scenarios/requirement-ledger.test.ts +29 -6
- package/src/scenarios/runner-ledger.test.ts +279 -0
- package/src/scenarios/runtime-requires-install-gate.test.ts +6 -5
- package/src/scenarios/sandbox-memory-cap.test.ts +3 -2
- package/src/scenarios/sandbox-mvp-behavior.test.ts +11 -10
- package/src/scenarios/sandbox-no-host-fs-escape.test.ts +2 -1
- package/src/scenarios/sandbox-timeout-cap.test.ts +3 -2
- package/src/scenarios/stream-text-fixture.test.ts +5 -4
- package/src/scenarios/subrun-approval-fail-closed.test.ts +3 -2
- package/src/scenarios/subrun-approval-gate.test.ts +4 -3
- package/src/scenarios/subrun-attestation-shape.test.ts +2 -1
- package/src/scenarios/subrun-checksum-stable.test.ts +4 -3
- package/src/scenarios/table-schema-enforcement.test.ts +2 -2
- package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +3 -2
- package/src/scenarios/tool-hooks-content-free.test.ts +3 -2
- package/src/scenarios/tool-hooks-rate-limit.test.ts +3 -2
- package/src/scenarios/tool-hooks-secret-redaction.test.ts +3 -2
- package/src/scenarios/tool-hooks-shape.test.ts +2 -1
- package/src/scenarios/trigger-bridge-delivery.test.ts +3 -2
- package/src/scenarios/version-fold.test.ts +5 -4
- package/src/scenarios/voice-streamref-tenant-bound.test.ts +2 -1
- package/src/scenarios/voice-transcription-streaming.test.ts +2 -1
- package/src/scenarios/voice-transcription-unadvertised.test.ts +2 -1
- package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +5 -4
- package/src/scenarios/wasm-pack-invoke-completed.test.ts +3 -2
- package/src/scenarios/wasm-pack-invoke-suspended.test.ts +3 -2
- package/src/scenarios/wasm-pack-load.test.ts +4 -3
- package/src/scenarios/wasm-pack-memory-cap.test.ts +4 -3
- package/src/scenarios/wasm-pack-replay-determinism.test.ts +3 -2
- package/src/scenarios/webhook-signed-delivery.test.ts +4 -3
- package/src/scenarios/workflow-primary-output-annotation.test.ts +13 -23
- package/src/scenarios/workload-identity-behavior.test.ts +7 -4
- package/src/scenarios/workspace-cross-tenant-isolation-blackbox.test.ts +3 -2
- package/src/scenarios/workspace-cross-tenant-isolation.test.ts +3 -2
- package/src/setup.ts +95 -1
|
@@ -171,6 +171,93 @@
|
|
|
171
171
|
"$ref": "#/$defs/ProducedVariable"
|
|
172
172
|
},
|
|
173
173
|
"description": "RFC 0133 §2.2. OPTIONAL. Run-scoped values a node writes to the executor's variable bag during the run and downstream nodes read by name via a `{ type:\"variable\", variableName }` input binding — where the value is NOT an author-time `parameter` and rides no typed output port. Prefer edges (a typed `sourceOutput`→`targetInput` hand-off) wherever a node exposes a value on a port; `producedVariables` is only for values written to the run bag with no typed port. On expansion the host emits these into `WorkflowDefinition.variables[]` as run-scoped entries (name + type, no value). Any `{ type:\"variable\" }` read of an UNDECLARED name (not here and not a materialized parameter) is a `variable_undeclared` manifest error. See `workflow-chain-packs.md` §\"Produced (run-scoped) variables (RFC 0133)\"."
|
|
174
|
+
},
|
|
175
|
+
"compensation": {
|
|
176
|
+
"type": "object",
|
|
177
|
+
"description": "RFC 0157 (RFC 0013 revision × RFC 0151 §B) — the chain-level compensation POLICY, MIRROR of `compensation-policy.schema.json` (copied inline; a conformance leg asserts equality apart from this description). On expansion it becomes the registered definition's `settings.compensation`: if the parent has none, the host MUST copy it; if the parent already carries one that is not deep-equal, expansion MUST fail `chain_compensation_policy_conflict` — a merged policy nobody wrote is exactly the guess-at-a-contract failure the policy exists to prevent. Absent ⇒ the chain expresses no policy and inherits the parent's (or none).",
|
|
178
|
+
"additionalProperties": false,
|
|
179
|
+
"required": [
|
|
180
|
+
"triggers"
|
|
181
|
+
],
|
|
182
|
+
"properties": {
|
|
183
|
+
"profileVersion": {
|
|
184
|
+
"type": "string",
|
|
185
|
+
"pattern": "^[1-9][0-9]*$",
|
|
186
|
+
"description": "The RFC 0151 profile version this policy was authored against. OPTIONAL; when present it MUST equal the host's advertised `capabilities.compensation.profileVersion` or registration fails, because `profileVersion` is part of the inverse-action identity (§C) and a workflow authored under one ordering rule MUST NOT mint identities under another."
|
|
187
|
+
},
|
|
188
|
+
"orderingModel": {
|
|
189
|
+
"type": "string",
|
|
190
|
+
"enum": [
|
|
191
|
+
"reverse-completion",
|
|
192
|
+
"dependency-graph"
|
|
193
|
+
],
|
|
194
|
+
"default": "reverse-completion",
|
|
195
|
+
"description": "RFC 0151 §A/§C. `reverse-completion` (default) orders inverse actions by descending durable forward-completion sequence; `dependency-graph` MUST be a DAG and preserve reverse dependency order. The value MUST be one of the host's advertised `capabilities.compensation.orderingModels[]`; a host MUST refuse the workflow at registration otherwise, so an unwind never discovers at failure time that its ordering rule is unimplemented."
|
|
196
|
+
},
|
|
197
|
+
"triggers": {
|
|
198
|
+
"type": "array",
|
|
199
|
+
"minItems": 1,
|
|
200
|
+
"uniqueItems": true,
|
|
201
|
+
"items": {
|
|
202
|
+
"type": "string",
|
|
203
|
+
"enum": [
|
|
204
|
+
"node-failure",
|
|
205
|
+
"run-cancel",
|
|
206
|
+
"cap-breach",
|
|
207
|
+
"operator-request"
|
|
208
|
+
]
|
|
209
|
+
},
|
|
210
|
+
"description": "Which events start an unwind. REQUIRED and closed — a policy that names no trigger is not a policy, and an open string here would let a host and an author disagree about a word. `node-failure`: a node reaches terminal failure after its own RFC 0009 retry policy and the run cannot continue. `run-cancel`: an RFC 0094 cancel is accepted while committed effects exist. `cap-breach`: an RFC 0058 / RFC 0084 `cap.breached` hard stop. `operator-request`: an authorized operator explicitly requests an unwind (§E) — MUST be authorized per RFC 0049, binding tenant, principal, action, and plan version. A trigger not listed here does NOT start an unwind; the run ends with its committed effects in place and `compensationStatus: none`, which is the honest shape — no generic rollback is inferred from an undeclared trigger (RFC 0151 §Compatibility)."
|
|
211
|
+
},
|
|
212
|
+
"retry": {
|
|
213
|
+
"type": "object",
|
|
214
|
+
"additionalProperties": false,
|
|
215
|
+
"description": "Defaults for inverse actions whose node-level `compensation.retry` is absent. A node's own bounds always win. Retries reuse the inverse action's stable identity (§C) — never a fresh one, or a transient refund failure mints a second refund.",
|
|
216
|
+
"properties": {
|
|
217
|
+
"maxAttempts": {
|
|
218
|
+
"type": "integer",
|
|
219
|
+
"minimum": 1
|
|
220
|
+
},
|
|
221
|
+
"backoffMs": {
|
|
222
|
+
"type": "integer",
|
|
223
|
+
"minimum": 0
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
"timeoutMs": {
|
|
228
|
+
"type": "integer",
|
|
229
|
+
"minimum": 1,
|
|
230
|
+
"description": "Wall-clock ceiling per inverse-action attempt, in milliseconds. A timed-out attempt counts against `retry.maxAttempts`; it is not a distinct outcome."
|
|
231
|
+
},
|
|
232
|
+
"exhaustedDisposition": {
|
|
233
|
+
"type": "string",
|
|
234
|
+
"enum": [
|
|
235
|
+
"record-outcome",
|
|
236
|
+
"manual-intervention"
|
|
237
|
+
],
|
|
238
|
+
"default": "record-outcome",
|
|
239
|
+
"description": "RFC 0151 §E — what the host does when an inverse action exhausts its retries. Either way the run MUST route to RFC 0053 dead-letter handling and the run's `compensationStatus` follows the §D fold (`partial` or `failed`). `record-outcome` (default): record the failure and continue unwinding the remaining actions. `manual-intervention`: additionally emit `compensation.manual_intervention_required` and stop issuing further inverse actions until an authorized operator retries, skips with recorded justification, substitutes a registered action, or terminates as uncompensated — the rollup reads `manual` while that is unresolved. Requires `capabilities.compensation.manualIntervention: true`; a host that does not advertise it MUST refuse a policy that selects it."
|
|
240
|
+
},
|
|
241
|
+
"approvalScope": {
|
|
242
|
+
"type": "string",
|
|
243
|
+
"enum": [
|
|
244
|
+
"declared",
|
|
245
|
+
"all"
|
|
246
|
+
],
|
|
247
|
+
"default": "declared",
|
|
248
|
+
"description": "RFC 0151 §E. `declared` (default): only inverse actions whose node sets `compensation.requiresApproval: true` create an RFC 0051 approval interrupt. `all`: every inverse action does, regardless of the node's declaration. This value can only ESCALATE — there is no `none`, because a policy MUST NOT strip an approval a node declared for itself (RFC 0147 R9: an inverse action can itself be harmful)."
|
|
249
|
+
},
|
|
250
|
+
"onParentCancel": {
|
|
251
|
+
"type": "string",
|
|
252
|
+
"enum": [
|
|
253
|
+
"continue",
|
|
254
|
+
"pause",
|
|
255
|
+
"manual"
|
|
256
|
+
],
|
|
257
|
+
"default": "continue",
|
|
258
|
+
"description": "RFC 0151 §C — cancellation of the parent run MUST NOT silently abandon an active compensation. `continue`: the unwind runs to its terminal rollup. `pause`: emit `compensation.paused` and hold for authorized intervention (the run's own `status` carries the wait). `manual`: emit `compensation.manual_intervention_required` and stop; rollup reads `manual`. `pause`/`manual` require `capabilities.compensation.manualIntervention: true`."
|
|
259
|
+
}
|
|
260
|
+
}
|
|
174
261
|
}
|
|
175
262
|
},
|
|
176
263
|
"additionalProperties": false,
|
|
@@ -365,6 +452,44 @@
|
|
|
365
452
|
"description": "Per-port input wiring. String values MAY contain `{{params.<name>}}` placeholders.",
|
|
366
453
|
"additionalProperties": true,
|
|
367
454
|
"$comment": "Open by design — port shapes are per-typeId, same reasoning as `config` above."
|
|
455
|
+
},
|
|
456
|
+
"compensation": {
|
|
457
|
+
"type": "object",
|
|
458
|
+
"description": "RFC 0157 (RFC 0013 revision × RFC 0151 §B) — MIRROR of `workflow-definition.schema.json#/$defs/WorkflowNode/properties/compensation`, copied inline so this manifest stays self-contained (no cross-file `$ref`); a conformance leg asserts the two stay byte-equal apart from this description. The inverse action for this fragment node. `nodeTypeId` MUST resolve at expansion (step 3) exactly as `typeId` does — `chain_unresolvable_typeid`; `inputMapping` strings MAY carry `{{params.<name>}}` and MUST be substituted at expansion (step 5); node-id references inside `inputMapping` MUST be rewritten with the expansion prefix (step 6). Survives expansion verbatim into the registered `WorkflowNode.compensation`.",
|
|
459
|
+
"additionalProperties": false,
|
|
460
|
+
"required": [
|
|
461
|
+
"nodeTypeId"
|
|
462
|
+
],
|
|
463
|
+
"properties": {
|
|
464
|
+
"nodeTypeId": {
|
|
465
|
+
"type": "string",
|
|
466
|
+
"minLength": 1,
|
|
467
|
+
"description": "Node type implementing the inverse action. MUST resolve at registration time."
|
|
468
|
+
},
|
|
469
|
+
"inputMapping": {
|
|
470
|
+
"type": "object",
|
|
471
|
+
"description": "Derived from recorded facts only — node outputs and run inputs already in the event log.",
|
|
472
|
+
"additionalProperties": true
|
|
473
|
+
},
|
|
474
|
+
"retry": {
|
|
475
|
+
"type": "object",
|
|
476
|
+
"additionalProperties": false,
|
|
477
|
+
"properties": {
|
|
478
|
+
"maxAttempts": {
|
|
479
|
+
"type": "integer",
|
|
480
|
+
"minimum": 1
|
|
481
|
+
},
|
|
482
|
+
"backoffMs": {
|
|
483
|
+
"type": "integer",
|
|
484
|
+
"minimum": 0
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
"requiresApproval": {
|
|
489
|
+
"type": "boolean",
|
|
490
|
+
"description": "RFC 0151 §C — an inverse action can itself be harmful (RFC 0147 R9), so it may be gated behind the same approval surface as a forward effect."
|
|
491
|
+
}
|
|
492
|
+
}
|
|
368
493
|
}
|
|
369
494
|
},
|
|
370
495
|
"additionalProperties": false
|
|
@@ -814,6 +814,10 @@
|
|
|
814
814
|
"type": "integer",
|
|
815
815
|
"minimum": 1,
|
|
816
816
|
"default": 5
|
|
817
|
+
},
|
|
818
|
+
"compensation": {
|
|
819
|
+
"$ref": "compensation-policy.schema.json",
|
|
820
|
+
"description": "RFC 0151 §B — the workflow-level compensation policy (WHEN an unwind starts and HOW it runs). OPTIONAL and capability-gated: a host that advertises `capabilities.compensation` MUST validate it at registration; a host that does not MUST refuse the workflow with `capability_required` rather than accept a policy it will never honour. Node-level `nodes[].compensation` declarations remain valid without it — they describe the inverse action; this key requests the unwind."
|
|
817
821
|
}
|
|
818
822
|
}
|
|
819
823
|
}
|
package/src/cli.ts
CHANGED
|
@@ -33,10 +33,14 @@ import { tmpdir } from 'node:os';
|
|
|
33
33
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
34
34
|
import addFormats from 'ajv-formats';
|
|
35
35
|
import { SCHEMAS_DIR } from './lib/paths.js';
|
|
36
|
+
import { readLedgerFile } from './lib/requirement-ledger.js';
|
|
37
|
+
import { deriveRequirementDispositions } from './lib/scenario-disposition.js';
|
|
38
|
+
import { scrubEvidence, evidenceSecretsFromEnv, verifyBundleV2 } from './lib/certification-bundle-verify.js';
|
|
36
39
|
import {
|
|
37
40
|
deriveProfiles,
|
|
38
41
|
isCoreStandard,
|
|
39
42
|
agentPlatformStatus,
|
|
43
|
+
DEPRECATED_PROFILE_ALIASES,
|
|
40
44
|
type DiscoveryPayload,
|
|
41
45
|
} from './lib/profiles.js';
|
|
42
46
|
|
|
@@ -284,9 +288,14 @@ async function runCertify(args: ParsedArgs, baseUrl: string, apiKey: string): Pr
|
|
|
284
288
|
const conformanceRoot = resolvePath(here, '..');
|
|
285
289
|
const reportDir = mkdtempSync(join(tmpdir(), 'owp-certify-'));
|
|
286
290
|
const reportFile = join(reportDir, 'vitest-report.json');
|
|
291
|
+
// RFC 0148 §A ledger sink (S6): every scenario file records its disposition
|
|
292
|
+
// (and assertion count) here; the runner reads it after the run so bundle v2
|
|
293
|
+
// rows come from what scenarios RECORDED, not from per-file pass/fail/skip.
|
|
294
|
+
const ledgerFile = join(reportDir, 'requirement-ledger.jsonl');
|
|
287
295
|
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
288
296
|
env.OPENWOP_BASE_URL = baseUrl;
|
|
289
297
|
env.OPENWOP_API_KEY = apiKey;
|
|
298
|
+
env.OPENWOP_LEDGER_PATH = ledgerFile;
|
|
290
299
|
if (args.impl) env.OPENWOP_IMPLEMENTATION_NAME = args.impl;
|
|
291
300
|
if (args.implVersion) env.OPENWOP_IMPLEMENTATION_VERSION = args.implVersion;
|
|
292
301
|
|
|
@@ -307,6 +316,7 @@ async function runCertify(args: ParsedArgs, baseUrl: string, apiKey: string): Pr
|
|
|
307
316
|
}
|
|
308
317
|
|
|
309
318
|
let report: VitestJsonReport;
|
|
319
|
+
const ledgerEntries = readLedgerFile(ledgerFile);
|
|
310
320
|
try {
|
|
311
321
|
report = JSON.parse(readFileSync(reportFile, 'utf8')) as VitestJsonReport;
|
|
312
322
|
} catch (err) {
|
|
@@ -382,6 +392,21 @@ async function runCertify(args: ParsedArgs, baseUrl: string, apiKey: string): Pr
|
|
|
382
392
|
// a large `blocked` count and therefore does NOT certify. That is not a defect
|
|
383
393
|
// in the emitter. It is the true state of the evidence, which v1 was unable to
|
|
384
394
|
// represent and therefore reported as a clean skip.
|
|
395
|
+
// (d1) RFC 0148 acceptance item 2 — requirement-level dispositions from the
|
|
396
|
+
// ledger, and rejection of unclassified returns for a claimed profile.
|
|
397
|
+
const derived = deriveRequirementDispositions(states, ledgerEntries, claimedProfiles, document as Record<string, unknown>);
|
|
398
|
+
const notHeld = new Set(derived.verdicts.filter((v) => v.runtimeDerived && !v.held).map((v) => v.profile));
|
|
399
|
+
const rejectedProfiles = derived.verdicts.filter((v) => v.unclassified.length > 0);
|
|
400
|
+
if (rejectedProfiles.length > 0) {
|
|
401
|
+
process.stderr.write(
|
|
402
|
+
'openwop-conformance --certify: REJECTING certification — a claimed profile has UNCLASSIFIED floor requirements\n' +
|
|
403
|
+
' (no disposition recorded, or an executed-pass with assertionCount 0 — a witness of nothing). RFC 0148 §A\n' +
|
|
404
|
+
' resolves an unclassified requirement to `blocked`, never to a pass; the bundle is still written so the\n' +
|
|
405
|
+
' evidence is inspectable, but it does not certify and this process exits 3.\n' +
|
|
406
|
+
rejectedProfiles.map((v) => ` - ${v.profile}: ${v.unclassified.join(', ')}\n`).join(''),
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
385
410
|
if (args.bundleVersion === '2') {
|
|
386
411
|
const scenarioIds = [...passed, ...failed, ...skipped].sort();
|
|
387
412
|
const manifestSha = createHash('sha256').update(scenarioIds.join('\n'), 'utf8').digest('hex');
|
|
@@ -392,28 +417,15 @@ async function runCertify(args: ParsedArgs, baseUrl: string, apiKey: string): Pr
|
|
|
392
417
|
.update(`${args.baseUrl}\n${sha256}\n${process.env['OPENWOP_REQUIRE_BEHAVIOR'] ?? ''}`, 'utf8')
|
|
393
418
|
.digest('hex');
|
|
394
419
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
...
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
disposition: 'executed-fail' as const,
|
|
405
|
-
detail: 'the scenario executed and failed',
|
|
406
|
-
})),
|
|
407
|
-
...skipped.map((f) => ({
|
|
408
|
-
requirementId: `openwop.scenario.${f.replace(/\.test\.ts$/, '')}`,
|
|
409
|
-
scenarioId: f,
|
|
410
|
-
disposition: 'blocked' as const,
|
|
411
|
-
detail:
|
|
412
|
-
'runner cannot classify a skipped file: the RFC 0148 §A requirement ledger is not ' +
|
|
413
|
-
'populated during a run, so skipped/inapplicable/blocked are indistinguishable here. ' +
|
|
414
|
-
'§A resolves an unclassifiable requirement to `blocked`, never to a pass.',
|
|
415
|
-
})),
|
|
416
|
-
];
|
|
420
|
+
// Rows come from the ledger (S6). A file that recorded nothing is
|
|
421
|
+
// `blocked` — unclassified — and, if it sits on a claimed floor, rejected above.
|
|
422
|
+
const requirements = derived.requirements.map((r) => ({
|
|
423
|
+
requirementId: r.requirementId,
|
|
424
|
+
scenarioId: r.scenarioId,
|
|
425
|
+
disposition: r.disposition,
|
|
426
|
+
...(r.detail === undefined ? {} : { detail: r.detail }),
|
|
427
|
+
...(r.assertionCount === undefined ? {} : { assertionCount: r.assertionCount }),
|
|
428
|
+
}));
|
|
417
429
|
|
|
418
430
|
const v2 = {
|
|
419
431
|
bundleVersion: '2' as const,
|
|
@@ -422,48 +434,81 @@ async function runCertify(args: ParsedArgs, baseUrl: string, apiKey: string): Pr
|
|
|
422
434
|
suite: { package: '@openwop/openwop-conformance' as const, version },
|
|
423
435
|
host,
|
|
424
436
|
discovery: { url: discoveryUrl, sha256, document },
|
|
425
|
-
claimedProfiles
|
|
437
|
+
// RFC 0155 §E: canonical ids only in `claimedProfiles`; a deprecated
|
|
438
|
+
// alias that also derives (`openwop-core`, always alongside
|
|
439
|
+
// `openwop-discovery-core`) is reported in `aliases`, never as a claim.
|
|
440
|
+
// A runtime-derived profile (`PROFILE_FLOOR_SCENARIOS[p].runtimeDerived`,
|
|
441
|
+
// today `openwop-node-packs`) is claimed only when the host HOLDS it — every
|
|
442
|
+
// floor row a witnessed pass. Its discovery predicate is `openwop-core`, so
|
|
443
|
+
// deriving the claim from discovery alone made every core host "claim" a
|
|
444
|
+
// registry it never advertised (RFC 0025: the read surface has no advert).
|
|
445
|
+
claimedProfiles: claimedProfiles.filter((p) => !(p in DEPRECATED_PROFILE_ALIASES) && !notHeld.has(p)),
|
|
446
|
+
...(claimedProfiles.some((p) => p in DEPRECATED_PROFILE_ALIASES)
|
|
447
|
+
? { aliases: claimedProfiles.filter((p) => p in DEPRECATED_PROFILE_ALIASES) }
|
|
448
|
+
: {}),
|
|
426
449
|
results: {
|
|
427
|
-
totals:
|
|
428
|
-
executedPass: passed.length,
|
|
429
|
-
executedFail: failed.length,
|
|
430
|
-
skipped: 0,
|
|
431
|
-
inapplicable: 0,
|
|
432
|
-
blocked: skipped.length,
|
|
433
|
-
},
|
|
450
|
+
totals: derived.totals,
|
|
434
451
|
requirements,
|
|
435
452
|
},
|
|
436
453
|
scenarioManifestSha256: manifestSha,
|
|
437
454
|
targetConfigurationSha256: configSha,
|
|
438
455
|
};
|
|
439
456
|
|
|
457
|
+
// RFC 0148 §C: secret canaries never enter evidence. Scrub the finished
|
|
458
|
+
// document with the credential this run was handed, every OPENWOP_*
|
|
459
|
+
// key/token/secret in the environment, and the conformance canary — a
|
|
460
|
+
// scenario's `detail` string, the captured discovery document, or a host
|
|
461
|
+
// field could carry any of them, and the emitter is the last place that
|
|
462
|
+
// can guarantee they do not ship.
|
|
463
|
+
const secrets = evidenceSecretsFromEnv(process.env, [apiKey]);
|
|
464
|
+
const scrubbed = scrubEvidence(v2, secrets);
|
|
465
|
+
const v2Out = scrubbed.value;
|
|
466
|
+
if (scrubbed.redactedAt.length > 0) {
|
|
467
|
+
process.stderr.write(
|
|
468
|
+
`openwop-conformance --certify: REDACTED ${scrubbed.redactedAt.length} evidence field(s) that carried a configured secret or the conformance canary: ${scrubbed.redactedAt.slice(0, 8).join(', ')}${scrubbed.redactedAt.length > 8 ? ', …' : ''}\n`,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
// Self-audit with the consumer verifier: the emitter MUST NOT write a
|
|
472
|
+
// document the verifier would reject on shape (duplicate rows, totals that
|
|
473
|
+
// disagree with rows, unknown dispositions, a canary). Rejections scoped to
|
|
474
|
+
// a claimed profile (unwitnessed / vacuous) are the exit-3 case below and
|
|
475
|
+
// are reported through `derived`; a bundle-wide rejection here is a bug in
|
|
476
|
+
// this emitter and exits 2.
|
|
477
|
+
const selfAudit = verifyBundleV2(v2Out);
|
|
478
|
+
if (selfAudit.rejections.length > 0) {
|
|
479
|
+
process.stderr.write(
|
|
480
|
+
'openwop-conformance --certify: assembled v2 bundle FAILED self-verification (emitter defect):\n' +
|
|
481
|
+
selfAudit.rejections.map((r) => ` - [${r.kind}] ${r.detail}`).join('\n') +
|
|
482
|
+
'\n',
|
|
483
|
+
);
|
|
484
|
+
process.exit(2);
|
|
485
|
+
}
|
|
486
|
+
|
|
440
487
|
const v2Schema = JSON.parse(
|
|
441
488
|
readFileSync(join(SCHEMAS_DIR, 'certification-bundle-v2.schema.json'), 'utf8'),
|
|
442
489
|
) as Record<string, unknown>;
|
|
443
490
|
const v2Ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
444
491
|
addFormats(v2Ajv);
|
|
445
492
|
const v2Validate = v2Ajv.compile(v2Schema);
|
|
446
|
-
if (!v2Validate(
|
|
493
|
+
if (!v2Validate(v2Out)) {
|
|
447
494
|
process.stderr.write(
|
|
448
495
|
'openwop-conformance --certify: assembled v2 bundle FAILED schema validation:\n' +
|
|
449
496
|
`${JSON.stringify(v2Validate.errors, null, 2)}\n`,
|
|
450
497
|
);
|
|
451
498
|
process.exit(2);
|
|
452
499
|
}
|
|
453
|
-
writeFileSync(outPath, `${JSON.stringify(
|
|
500
|
+
writeFileSync(outPath, `${JSON.stringify(v2Out, null, 2)}\n`);
|
|
454
501
|
process.stdout.write(
|
|
455
502
|
`openwop-conformance --certify: wrote bundle v2 to ${outPath}\n` +
|
|
456
503
|
` host: ${host.name}@${host.version}\n` +
|
|
457
|
-
` executed-pass ${
|
|
458
|
-
(
|
|
459
|
-
? `
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
` defect in this emitter.\n`
|
|
464
|
-
: ''),
|
|
504
|
+
` executed-pass ${derived.totals.executedPass} / executed-fail ${derived.totals.executedFail} / skipped ${derived.totals.skipped} / inapplicable ${derived.totals.inapplicable} / blocked ${derived.totals.blocked}\n` +
|
|
505
|
+
(derived.ledgerPresent
|
|
506
|
+
? ` dispositions come from the RFC 0148 §A ledger (${ledgerEntries.length} entries recorded by the scenarios)\n`
|
|
507
|
+
: ` NOTE: no ledger was recorded — every skipped file is 'blocked' (unclassifiable), which is the honest reading\n`) +
|
|
508
|
+
(derived.totals.blocked > 0 ? ` a bundle with blocked > 0 does NOT certify — that is the state of the evidence, not a defect in this emitter\n` : '') +
|
|
509
|
+
derived.verdicts.map((v) => ` ${v.profile}: ${v.runtimeDerived && !v.held ? `not held (runtime-derived — dropped from claimedProfiles; floor rows not witnessed passes: ${v.blocking.join(', ')})` : v.certifiable ? 'certifiable' : 'NOT certifiable'}${v.unclassified.length > 0 ? ` (unclassified: ${v.unclassified.length})` : ''}\n`).join(''),
|
|
465
510
|
);
|
|
466
|
-
process.exit(failed.length > 0 ? 1 : 0);
|
|
511
|
+
process.exit(rejectedProfiles.length > 0 ? 3 : failed.length > 0 ? 1 : 0);
|
|
467
512
|
}
|
|
468
513
|
|
|
469
514
|
// (e) Validate against the bundle schema BEFORE writing.
|
|
@@ -473,7 +518,14 @@ async function runCertify(args: ParsedArgs, baseUrl: string, apiKey: string): Pr
|
|
|
473
518
|
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
474
519
|
addFormats(ajv);
|
|
475
520
|
const validate = ajv.compile(schema);
|
|
476
|
-
|
|
521
|
+
const bundleScrub = scrubEvidence(bundle, evidenceSecretsFromEnv(process.env, [apiKey]));
|
|
522
|
+
if (bundleScrub.redactedAt.length > 0) {
|
|
523
|
+
process.stderr.write(
|
|
524
|
+
`openwop-conformance --certify: REDACTED ${bundleScrub.redactedAt.length} evidence field(s) that carried a configured secret or the conformance canary\n`,
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
const bundleOut = bundleScrub.value;
|
|
528
|
+
if (!validate(bundleOut)) {
|
|
477
529
|
process.stderr.write(
|
|
478
530
|
'openwop-conformance --certify: assembled bundle FAILED schema validation:\n' +
|
|
479
531
|
`${JSON.stringify(validate.errors, null, 2)}\n`,
|
|
@@ -481,7 +533,7 @@ async function runCertify(args: ParsedArgs, baseUrl: string, apiKey: string): Pr
|
|
|
481
533
|
process.exit(2);
|
|
482
534
|
}
|
|
483
535
|
|
|
484
|
-
writeFileSync(outPath, `${JSON.stringify(
|
|
536
|
+
writeFileSync(outPath, `${JSON.stringify(bundleOut, null, 2)}\n`);
|
|
485
537
|
process.stdout.write(
|
|
486
538
|
`openwop-conformance --certify: wrote certification bundle to ${outPath}\n` +
|
|
487
539
|
` host: ${host.name}@${host.version}\n` +
|
|
@@ -489,8 +541,10 @@ async function runCertify(args: ParsedArgs, baseUrl: string, apiKey: string): Pr
|
|
|
489
541
|
` results: ${passed.length} passed / ${failed.length} failed / ${skipped.length} skipped\n`,
|
|
490
542
|
);
|
|
491
543
|
// Exit code mirrors the suite outcome: a failing run still produces a bundle
|
|
492
|
-
// (the failures are recorded), but the process exit reflects pass/fail
|
|
493
|
-
|
|
544
|
+
// (the failures are recorded), but the process exit reflects pass/fail — and
|
|
545
|
+
// 3 when a claimed profile has unclassified floor requirements (RFC 0148 §A;
|
|
546
|
+
// v1 bundles cannot express the distinction, so the exit code carries it).
|
|
547
|
+
process.exit(rejectedProfiles.length > 0 ? 3 : failed.length > 0 ? 1 : 0);
|
|
494
548
|
}
|
|
495
549
|
|
|
496
550
|
async function main(): Promise<never> {
|