@openwop/openwop-conformance 1.163.0 → 2.0.0-rc.1
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 +11 -2
- package/dist/cli.js +262 -14
- package/dist/lib/certification-bundle-v3.js +142 -0
- package/dist/lib/paths.js +25 -3
- package/dist/lib/requirement-ids.js +57 -4
- package/dist/lib/requirement-ledger.js +1 -0
- package/dist/lib/scenario-disposition.js +6 -1
- package/dist/lib/spec-coherence.js +13 -32
- package/dist/spec-artifacts.lock.json +5 -0
- package/package.json +11 -33
- package/requirements.json +22892 -11701
- package/scenario-majors.json +1493 -0
- package/schemas/CORPUS-STAMP.json +137 -5
- package/src/cli.ts +236 -19
- package/src/global-setup.ts +3 -2
- package/src/lib/certification-bundle-v3.ts +175 -0
- package/src/lib/corpus-stamp.ts +29 -1
- package/src/lib/driver.ts +9 -1
- package/src/lib/era2-seed.ts +197 -0
- package/src/lib/paths.ts +25 -3
- package/src/lib/requirement-ids.ts +54 -4
- package/src/lib/requirement-ledger.ts +7 -1
- package/src/lib/scenario-disposition.ts +6 -1
- package/src/lib/seams.ts +45 -0
- package/src/lib/spec-coherence.ts +15 -32
- package/src/lib/v2.ts +62 -0
- package/src/scenarios/a2a-1-0-agent-card.test.ts +17 -16
- package/src/scenarios/a2a-1-0-task-roundtrip.test.ts +11 -10
- package/src/scenarios/a2a-card-runtime-consistency.test.ts +18 -17
- package/src/scenarios/a2a-peer-authority.test.ts +7 -6
- package/src/scenarios/a2a-task-roundtrip.test.ts +30 -29
- package/src/scenarios/a2a-version-negotiation.test.ts +16 -14
- package/src/scenarios/a2ui-surface-degrades.test.ts +5 -3
- package/src/scenarios/a2ui-surface-delta-transport.test.ts +35 -33
- package/src/scenarios/a2ui-surface-replay.test.ts +6 -4
- package/src/scenarios/a2ui-surface-shape.test.ts +11 -10
- package/src/scenarios/a2ui-surface-version-refusal.test.ts +6 -4
- package/src/scenarios/a2ui-untrusted-blocks-approval.test.ts +5 -3
- package/src/scenarios/agent-capability-degraded-projection.test.ts +12 -11
- package/src/scenarios/agent-channel-dispatch.test.ts +18 -16
- package/src/scenarios/agent-deployment-lifecycle.test.ts +20 -19
- package/src/scenarios/agent-deployment-shape.test.ts +22 -24
- package/src/scenarios/agent-eval-run.test.ts +19 -18
- package/src/scenarios/agent-eval-suite-shape.test.ts +20 -22
- package/src/scenarios/agent-live-allowlist-enforced.test.ts +6 -5
- package/src/scenarios/agent-live-invocation-bracket.test.ts +13 -12
- package/src/scenarios/agent-live-runtime-shape.test.ts +10 -12
- package/src/scenarios/agent-live-structured-output.test.ts +7 -6
- package/src/scenarios/agent-loop-iteration-monotonic.test.ts +3 -3
- package/src/scenarios/agent-loop-stateful-resume.test.ts +2 -2
- package/src/scenarios/agent-loop-version5-shape.test.ts +4 -4
- package/src/scenarios/agent-loop-workspace-snapshot.test.ts +3 -3
- package/src/scenarios/agent-manifest-role-profile.test.ts +9 -9
- package/src/scenarios/agent-manifest-runtime.test.ts +13 -12
- package/src/scenarios/agent-org-chart-scoping.test.ts +14 -13
- package/src/scenarios/agent-org-chart-shape.test.ts +11 -13
- package/src/scenarios/agent-platform-aggregate-evidence.test.ts +5 -4
- package/src/scenarios/agent-platform-profile.test.ts +18 -19
- package/src/scenarios/agent-requires-capabilities-shape.test.ts +4 -4
- package/src/scenarios/agent-roster-attribution.test.ts +18 -17
- package/src/scenarios/agent-roster-shape.test.ts +17 -19
- package/src/scenarios/agent-verifier-shape.test.ts +11 -11
- package/src/scenarios/agentConfidenceEscalation.test.ts +2 -1
- package/src/scenarios/agentMemoryCrossTenantIsolation.test.ts +6 -5
- package/src/scenarios/agentMemoryRedactionContract.test.ts +2 -1
- package/src/scenarios/agentMemoryRoundTrip.test.ts +2 -1
- package/src/scenarios/agentMemoryTtlExpiry.test.ts +4 -3
- package/src/scenarios/agentMessageReducer.test.ts +2 -1
- package/src/scenarios/agentMetadata.test.ts +2 -1
- package/src/scenarios/agentPackCatalog.test.ts +23 -21
- package/src/scenarios/agentPackExport.test.ts +4 -2
- package/src/scenarios/agentPackHandoffSchemaValidation.test.ts +5 -4
- package/src/scenarios/agentPackInstall.test.ts +4 -2
- package/src/scenarios/agentPackProvenance.test.ts +2 -1
- package/src/scenarios/agentReasoningEvents.test.ts +3 -2
- package/src/scenarios/agentReasoningStreaming.test.ts +11 -10
- package/src/scenarios/agents-run-tool-allowlist.test.ts +14 -12
- package/src/scenarios/ai-envelope-shape.test.ts +31 -29
- package/src/scenarios/aiEnvelope.capBreached.test.ts +26 -24
- package/src/scenarios/aiEnvelope.contractRefusal.test.ts +36 -34
- package/src/scenarios/aiEnvelope.correlationReplay.test.ts +17 -15
- package/src/scenarios/aiEnvelope.redaction.test.ts +22 -20
- package/src/scenarios/aiEnvelope.schemaDrift.test.ts +18 -16
- package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +38 -36
- package/src/scenarios/aiEnvelope.universalKinds.test.ts +31 -29
- package/src/scenarios/aiproviders-input-shape.test.ts +5 -5
- package/src/scenarios/aiproviders-realtimevoice-shape.test.ts +14 -16
- package/src/scenarios/aiproviders-selfhosted-honesty.test.ts +6 -4
- package/src/scenarios/aiproviders-selfhosted-shape.test.ts +15 -17
- package/src/scenarios/aiproviders-speechsynth-shape.test.ts +6 -8
- package/src/scenarios/aiproviders-subscription-scope.test.ts +7 -8
- package/src/scenarios/anonymous-actor-audit-opaque.test.ts +9 -8
- package/src/scenarios/anonymous-actor-default-deny.test.ts +10 -9
- package/src/scenarios/anonymous-actor-egress-guarded.test.ts +6 -5
- package/src/scenarios/anonymous-actor-no-secret-reach.test.ts +7 -6
- package/src/scenarios/anonymous-actor-shape.test.ts +17 -19
- package/src/scenarios/anonymous-actor-write-gated.test.ts +9 -8
- package/src/scenarios/append-ordering.test.ts +7 -5
- package/src/scenarios/approval-gate-events.test.ts +4 -3
- package/src/scenarios/approval-gate-flow.test.ts +4 -3
- package/src/scenarios/approval-payload.test.ts +5 -4
- package/src/scenarios/artifact-auth.test.ts +7 -6
- package/src/scenarios/artifact-type-pack-install.test.ts +6 -6
- package/src/scenarios/artifact-type-pack-manifest-validation.test.ts +9 -8
- package/src/scenarios/artifact-type-registration-source.test.ts +20 -20
- package/src/scenarios/artifact-type-store-emission.test.ts +15 -18
- package/src/scenarios/artifact-type-store-without-render.test.ts +4 -4
- package/src/scenarios/audit-log-integrity.test.ts +8 -6
- package/src/scenarios/auth-api-key-rotation.test.ts +11 -9
- package/src/scenarios/auth-mtls.test.ts +22 -20
- package/src/scenarios/auth-oauth2-client-credentials.test.ts +16 -14
- package/src/scenarios/auth-oidc-user-bearer.test.ts +26 -24
- package/src/scenarios/auth-saml-profile.test.ts +14 -12
- package/src/scenarios/auth-scim-profile.test.ts +3 -2
- package/src/scenarios/auth-subject-link-key-class.test.ts +8 -7
- package/src/scenarios/auth-subject-link.test.ts +11 -10
- package/src/scenarios/auth.test.ts +5 -4
- package/src/scenarios/authorization-fail-closed.test.ts +3 -2
- package/src/scenarios/authorization-roles-shape.test.ts +9 -7
- package/src/scenarios/blob-cross-tenant-isolation.test.ts +8 -6
- package/src/scenarios/blob-presign-expiry.test.ts +12 -10
- package/src/scenarios/blob-roundtrip.test.ts +0 -0
- package/src/scenarios/budget-enforcement.test.ts +15 -14
- package/src/scenarios/budget-policy-shape.test.ts +18 -19
- package/src/scenarios/bulk-cancel.test.ts +9 -8
- package/src/scenarios/byok-auth-modes.test.ts +13 -14
- package/src/scenarios/byok-roundtrip.test.ts +12 -10
- package/src/scenarios/cache-cross-tenant-isolation.test.ts +8 -6
- package/src/scenarios/cache-ttl-expiry.test.ts +7 -5
- package/src/scenarios/callai-multimodal.test.ts +5 -3
- package/src/scenarios/cancellation.test.ts +6 -5
- package/src/scenarios/cap-breach.test.ts +15 -14
- package/src/scenarios/capability-arm-witness.test.ts +5 -3
- package/src/scenarios/certification-bundle-non-vacuous.test.ts +20 -19
- package/src/scenarios/certification-bundle-redaction.test.ts +11 -10
- package/src/scenarios/certification-bundle-v2.test.ts +11 -10
- package/src/scenarios/chain-compensation-expansion.test.ts +14 -13
- package/src/scenarios/chain-produced-var-roundtrip.test.ts +13 -14
- package/src/scenarios/chain-subchain-cycle-rejected.test.ts +9 -10
- package/src/scenarios/chain-subchain-fanout.test.ts +8 -9
- package/src/scenarios/chain-subchain-sibling.test.ts +14 -16
- package/src/scenarios/channel-presence-behavioral.test.ts +9 -7
- package/src/scenarios/channel-presence-shape.test.ts +11 -12
- package/src/scenarios/channel-ttl.test.ts +6 -5
- package/src/scenarios/chat-card-pack-execution.test.ts +4 -4
- package/src/scenarios/chat-card-pack-manifest-validation.test.ts +9 -8
- package/src/scenarios/commitment-fired.test.ts +6 -5
- package/src/scenarios/compensation-behavior.test.ts +24 -23
- package/src/scenarios/compensation-recovery.test.ts +28 -27
- package/src/scenarios/configurable-schema.test.ts +8 -6
- package/src/scenarios/conformance-advertised-seam-required.test.ts +6 -5
- package/src/scenarios/conformance-execution-witness.test.ts +9 -8
- package/src/scenarios/connection-pack-apihosts.test.ts +19 -17
- package/src/scenarios/connection-pack-manifest-valid.test.ts +12 -11
- package/src/scenarios/connection-pack-no-credential-material.test.ts +8 -6
- package/src/scenarios/connection-pack-reach-exclusive.test.ts +5 -4
- package/src/scenarios/connection-pack-write-reconsent.test.ts +6 -4
- package/src/scenarios/connection-provider-resolution.test.ts +18 -16
- package/src/scenarios/connector-manifest-validity.test.ts +8 -7
- package/src/scenarios/context-budget-transcript-bound.test.ts +17 -15
- package/src/scenarios/context-summarization-replay.test.ts +14 -12
- package/src/scenarios/contract-provenance.test.ts +18 -18
- package/src/scenarios/conversation-turn-model-provenance-shape.test.ts +14 -15
- package/src/scenarios/conversationCapabilityNegotiation.test.ts +2 -1
- package/src/scenarios/conversationLifecycle.test.ts +2 -1
- package/src/scenarios/conversationReplayDeterminism.test.ts +4 -2
- package/src/scenarios/conversationVsLegacySuspend.test.ts +2 -1
- package/src/scenarios/core-standard-profile.test.ts +8 -9
- package/src/scenarios/cost-attribution.test.ts +23 -21
- package/src/scenarios/credential-payload-redaction.test.ts +4 -3
- package/src/scenarios/credentials-capability-shape.test.ts +5 -4
- package/src/scenarios/cross-engine-append-behavior.test.ts +11 -10
- package/src/scenarios/cross-engine-append-ordering.test.ts +4 -3
- package/src/scenarios/cross-host-ancestry-endpoint.test.ts +11 -10
- package/src/scenarios/cross-host-causation-shape.test.ts +7 -6
- package/src/scenarios/cross-workspace-isolation.test.ts +4 -3
- package/src/scenarios/data-residency-admission.test.ts +15 -14
- package/src/scenarios/deadletter-capability-shape.test.ts +6 -4
- package/src/scenarios/deadletter-retry-exhaustion.test.ts +4 -3
- package/src/scenarios/debug-bundle-truncation.test.ts +9 -7
- package/src/scenarios/debugBundle.test.ts +25 -23
- package/src/scenarios/discovery-families-view.test.ts +5 -4
- package/src/scenarios/discovery.test.ts +30 -29
- package/src/scenarios/dispatch-cross-worker-handoff.test.ts +10 -8
- package/src/scenarios/dispatch-fanout-parallel.test.ts +24 -22
- package/src/scenarios/dispatch-input-mapping.test.ts +12 -10
- package/src/scenarios/dispatch-output-mapping.test.ts +9 -7
- package/src/scenarios/dispatch-per-item-input.test.ts +22 -20
- package/src/scenarios/dispatchLoop.test.ts +3 -2
- package/src/scenarios/distillation-index-roundtrip.test.ts +3 -3
- package/src/scenarios/distillation-secret-carryforward.test.ts +2 -2
- package/src/scenarios/distillation-shape.test.ts +4 -4
- package/src/scenarios/distillation-stable-archive.test.ts +6 -6
- package/src/scenarios/distillation-token-budget.test.ts +5 -5
- package/src/scenarios/durability-poison-exhaustion.test.ts +7 -6
- package/src/scenarios/egress-audience-binding.test.ts +11 -10
- package/src/scenarios/egress-decision-content-free.test.ts +6 -5
- package/src/scenarios/egress-provenance-shape.test.ts +17 -18
- package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +27 -25
- package/src/scenarios/envelope-nl-to-format-engaged.test.ts +19 -17
- package/src/scenarios/envelope-reasoning-secret-redaction.test.ts +28 -26
- package/src/scenarios/envelope-reasoning-shape.test.ts +19 -17
- package/src/scenarios/envelope-recovery-applied.test.ts +24 -22
- package/src/scenarios/envelope-refusal-shape.test.ts +31 -29
- package/src/scenarios/envelope-rendering-hint.test.ts +5 -4
- package/src/scenarios/envelope-retry-attempted.test.ts +33 -31
- package/src/scenarios/envelope-retry-exhausted.test.ts +22 -20
- package/src/scenarios/envelope-tier-one-subset-static.test.ts +9 -7
- package/src/scenarios/envelope-truncated.test.ts +19 -17
- package/src/scenarios/envelope-truncation-cap-exhaustion.test.ts +19 -17
- package/src/scenarios/envelope-variant-discriminator-static.test.ts +4 -3
- package/src/scenarios/errors.test.ts +20 -19
- package/src/scenarios/eventOrdering.test.ts +16 -14
- package/src/scenarios/exec-not-protocol-tier.test.ts +5 -7
- package/src/scenarios/experimental-tier-shape.test.ts +17 -15
- package/src/scenarios/export-bundle-portability.test.ts +17 -17
- package/src/scenarios/failure-path.test.ts +7 -6
- package/src/scenarios/feedback-capability-shape.test.ts +4 -3
- package/src/scenarios/feedback-correction-redaction.test.ts +2 -1
- package/src/scenarios/feedback-cross-tenant-isolation.test.ts +2 -1
- package/src/scenarios/feedback-fork-not-copied.test.ts +3 -2
- package/src/scenarios/feedback-on-terminal-run.test.ts +3 -2
- package/src/scenarios/feedback-record-and-list.test.ts +2 -1
- package/src/scenarios/feedback-unsupported-501.test.ts +2 -1
- package/src/scenarios/fixtures-gating.test.ts +29 -28
- package/src/scenarios/fixtures-valid.test.ts +16 -15
- package/src/scenarios/form-content-instantiation.test.ts +15 -14
- package/src/scenarios/frontend-plugin-packs.test.ts +29 -27
- package/src/scenarios/fs-path-traversal.test.ts +14 -12
- package/src/scenarios/goal-standing-continuation.test.ts +21 -21
- package/src/scenarios/grpc-transport.test.ts +9 -7
- package/src/scenarios/heartbeat-capability-shape.test.ts +3 -3
- package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +2 -2
- package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +3 -3
- package/src/scenarios/heartbeat-runtime-bound.test.ts +2 -2
- package/src/scenarios/highConcurrency.test.ts +14 -13
- package/src/scenarios/host-callback-declaration.test.ts +8 -7
- package/src/scenarios/http-client-ssrf.test.ts +7 -5
- package/src/scenarios/i18n-negotiation.test.ts +14 -12
- package/src/scenarios/idempotency-concurrent-claim.test.ts +14 -13
- package/src/scenarios/idempotency-key-determinism.test.ts +20 -18
- package/src/scenarios/idempotency.test.ts +8 -7
- package/src/scenarios/idempotencyRetry.test.ts +13 -11
- package/src/scenarios/identity-owner-shape.test.ts +6 -5
- package/src/scenarios/identity-passthrough.test.ts +4 -3
- package/src/scenarios/interrupt-approval.test.ts +6 -5
- package/src/scenarios/interrupt-approver-routing.test.ts +15 -15
- package/src/scenarios/interrupt-auth-required-resume.test.ts +5 -3
- package/src/scenarios/interrupt-clarification.test.ts +4 -3
- package/src/scenarios/interrupt-external-event-correlation.test.ts +6 -5
- package/src/scenarios/interrupt-parent-child-cascade.test.ts +4 -3
- package/src/scenarios/interrupt-quorum-resolution.test.ts +5 -4
- package/src/scenarios/interrupt-token-matrix.test.ts +8 -6
- package/src/scenarios/interruptRace.test.ts +12 -10
- package/src/scenarios/kv-atomic-increment.test.ts +11 -9
- package/src/scenarios/kv-cas.test.ts +14 -12
- package/src/scenarios/kv-cross-tenant-isolation.test.ts +12 -10
- package/src/scenarios/kv-ttl-expiry.test.ts +7 -5
- package/src/scenarios/localized-content-delivery.test.ts +24 -26
- package/src/scenarios/maliciousManifest.test.ts +10 -8
- package/src/scenarios/mcp-2026-07-28-discover.test.ts +15 -14
- package/src/scenarios/mcp-cache-tenant-scope.test.ts +6 -5
- package/src/scenarios/mcp-current-auth-boundary.test.ts +7 -6
- package/src/scenarios/mcp-discoverability.test.ts +9 -7
- package/src/scenarios/mcp-extension-opacity.test.ts +7 -6
- package/src/scenarios/mcp-mrtr-roundtrip.test.ts +18 -17
- package/src/scenarios/mcp-server-elicitation-bridge.test.ts +13 -12
- package/src/scenarios/mcp-server-prompt-roundtrip.test.ts +11 -10
- package/src/scenarios/mcp-server-resource-roundtrip.test.ts +11 -10
- package/src/scenarios/mcp-server-sampling-bridge.test.ts +12 -11
- package/src/scenarios/mcp-server-tool-roundtrip.test.ts +14 -13
- package/src/scenarios/mcp-server-untrusted-args.test.ts +14 -13
- package/src/scenarios/mcp-stateless-request.test.ts +11 -10
- package/src/scenarios/mcp-tool-roundtrip.test.ts +4 -3
- package/src/scenarios/mcp-toolcall-redaction.test.ts +4 -3
- package/src/scenarios/mcp-version-negotiation.test.ts +15 -14
- package/src/scenarios/media-url-inline-cap.test.ts +21 -19
- package/src/scenarios/memory-attribution-emits-on-write.test.ts +5 -5
- package/src/scenarios/memory-attribution-no-content.test.ts +4 -4
- package/src/scenarios/memory-attribution-replay-stable.test.ts +4 -3
- package/src/scenarios/memory-attribution-shape.test.ts +4 -3
- package/src/scenarios/memory-attribution-tenant-scoped.test.ts +3 -3
- package/src/scenarios/memory-capability-model-shape.test.ts +14 -15
- package/src/scenarios/memory-compaction-event-emitted.test.ts +7 -6
- package/src/scenarios/memory-compaction-provenance-tag.test.ts +3 -2
- package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +7 -6
- package/src/scenarios/memory-consolidation-idempotent.test.ts +5 -4
- package/src/scenarios/memory-consolidation-shape.test.ts +9 -11
- package/src/scenarios/memory-degraded-projection.test.ts +10 -9
- package/src/scenarios/memory-injection-budget.test.ts +15 -14
- package/src/scenarios/metric-emission.test.ts +4 -3
- package/src/scenarios/model-capability-insufficient.test.ts +19 -17
- package/src/scenarios/model-capability-substituted.test.ts +20 -18
- package/src/scenarios/multi-agent-confidence-escalation.test.ts +12 -11
- package/src/scenarios/multi-agent-handoff-state-machine.test.ts +10 -9
- package/src/scenarios/multi-agent-memory-lifecycle.test.ts +11 -10
- package/src/scenarios/multi-node-ordering.test.ts +4 -3
- package/src/scenarios/multi-party-conversation-behavioral.test.ts +6 -6
- package/src/scenarios/multi-party-conversation-shape.test.ts +21 -23
- package/src/scenarios/multi-region-idempotency-behavior.test.ts +16 -15
- package/src/scenarios/multi-region-idempotency.test.ts +6 -5
- package/src/scenarios/node-failed-payload-shape.test.ts +5 -4
- package/src/scenarios/node-module-required-capabilities-shape.test.ts +10 -8
- package/src/scenarios/oauth-authorization-code-roundtrip.test.ts +5 -4
- package/src/scenarios/oauth-capability-shape.test.ts +5 -4
- package/src/scenarios/oauth-connector-redaction.test.ts +4 -3
- package/src/scenarios/openapi-resolved-paths.test.ts +6 -5
- package/src/scenarios/orchestratorConservativePath.test.ts +2 -1
- package/src/scenarios/orchestratorDispatch.test.ts +2 -1
- package/src/scenarios/orchestratorTermination.test.ts +2 -1
- package/src/scenarios/org-position-no-authority-escalation.test.ts +2 -2
- package/src/scenarios/otel-collector-canary-inspection.test.ts +8 -7
- package/src/scenarios/otel-emission-grpc.test.ts +3 -2
- package/src/scenarios/otel-emission.test.ts +6 -5
- package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +7 -6
- package/src/scenarios/otel-trace-propagation.test.ts +2 -1
- package/src/scenarios/owner-subject-echo.test.ts +13 -12
- package/src/scenarios/owner-subject-shape.test.ts +11 -10
- package/src/scenarios/pack-manifest-extension-opacity.test.ts +8 -8
- package/src/scenarios/pack-registry-isolation.test.ts +4 -3
- package/src/scenarios/pack-registry-publish.test.ts +65 -63
- package/src/scenarios/pack-registry.test.ts +38 -36
- package/src/scenarios/pause-resume.test.ts +18 -16
- package/src/scenarios/policies.test.ts +20 -18
- package/src/scenarios/production-backpressure.test.ts +18 -16
- package/src/scenarios/production-retention-expiry.test.ts +8 -6
- package/src/scenarios/profile-discovery-core-alias.test.ts +13 -11
- package/src/scenarios/profileDerivation.test.ts +34 -33
- package/src/scenarios/prompt-all-four-kinds-events.test.ts +11 -9
- package/src/scenarios/prompt-composed-secret-redaction.test.ts +11 -9
- package/src/scenarios/prompt-composed-trust-marker.test.ts +10 -8
- package/src/scenarios/prompt-end-to-end-events.test.ts +17 -15
- package/src/scenarios/prompt-list-and-fetch.test.ts +16 -14
- package/src/scenarios/prompt-mutable-lifecycle.test.ts +13 -11
- package/src/scenarios/prompt-mutation-workspace-membership-enforced.test.ts +6 -4
- package/src/scenarios/prompt-pack-install.test.ts +14 -12
- package/src/scenarios/prompt-prefix-cache.test.ts +19 -17
- package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +11 -9
- package/src/scenarios/prompt-render-deterministic.test.ts +19 -17
- package/src/scenarios/prompt-resolution-chain-agent-intrinsic.test.ts +7 -5
- package/src/scenarios/prompt-resolution-chain-event.test.ts +12 -10
- package/src/scenarios/prompt-resolution-chain-fallback-cascade.test.ts +10 -8
- package/src/scenarios/prompt-resolution-chain-node-wins.test.ts +8 -6
- package/src/scenarios/prompt-template-shape.test.ts +30 -28
- package/src/scenarios/proposal-reviewable-learning.test.ts +19 -19
- package/src/scenarios/protocol-versions-array.test.ts +10 -9
- package/src/scenarios/provider-usage.test.ts +17 -15
- package/src/scenarios/providerPolicyEnforcement.test.ts +16 -14
- package/src/scenarios/purpose-propagation.test.ts +23 -21
- package/src/scenarios/queue-ack-nack-dlq.test.ts +12 -10
- package/src/scenarios/queue-cross-tenant-isolation.test.ts +8 -6
- package/src/scenarios/queue-publish-consume-roundtrip.test.ts +10 -8
- package/src/scenarios/rate-limit-envelope.test.ts +6 -5
- package/src/scenarios/redaction.test.ts +13 -11
- package/src/scenarios/redactionAdversarial.test.ts +11 -9
- package/src/scenarios/registry-public.test.ts +12 -11
- package/src/scenarios/replay-divergence-at-refusal.test.ts +16 -15
- package/src/scenarios/replay-fanout-suppression.test.ts +16 -15
- package/src/scenarios/replay-fork-arbitrary.test.ts +24 -23
- package/src/scenarios/replay-fork.test.ts +20 -19
- package/src/scenarios/replay-llm-cache-key-portable.test.ts +11 -9
- package/src/scenarios/replay-llm-cache-key.test.ts +22 -21
- package/src/scenarios/replay-observable-sequence-determinism.test.ts +4 -3
- package/src/scenarios/replay-retention-expiry.test.ts +11 -9
- package/src/scenarios/replay-side-effect-suppression.test.ts +21 -19
- package/src/scenarios/replayDeterminism.test.ts +13 -12
- package/src/scenarios/requirement-ledger.test.ts +15 -14
- package/src/scenarios/restart-during-run.test.ts +3 -2
- package/src/scenarios/route-coverage.test.ts +20 -19
- package/src/scenarios/run-diff.test.ts +22 -20
- package/src/scenarios/run-execution-bounds-shape.test.ts +14 -12
- package/src/scenarios/run-transport-economy.test.ts +13 -11
- package/src/scenarios/runner-ledger.test.ts +17 -16
- package/src/scenarios/runs-lifecycle.test.ts +7 -6
- package/src/scenarios/runtime-capabilities.test.ts +12 -10
- package/src/scenarios/runtime-requires-install-gate.test.ts +8 -8
- package/src/scenarios/runtime-requires-shape.test.ts +7 -6
- package/src/scenarios/safefetch-behavior.test.ts +22 -21
- package/src/scenarios/safefetch-live-audit.test.ts +14 -13
- package/src/scenarios/sandbox-memory-cap.test.ts +2 -1
- package/src/scenarios/sandbox-mvp-behavior.test.ts +24 -23
- package/src/scenarios/sandbox-no-host-fs-escape.test.ts +3 -2
- package/src/scenarios/sandbox-timeout-cap.test.ts +2 -1
- package/src/scenarios/sandbox-wasm-isolation.test.ts +14 -15
- package/src/scenarios/sandbox-wasm-timeout.test.ts +5 -6
- package/src/scenarios/scheduling-capability-shape.test.ts +8 -6
- package/src/scenarios/scheduling-cron-fires-once.test.ts +8 -6
- package/src/scenarios/search-bm25-roundtrip.test.ts +8 -6
- package/src/scenarios/secret-leakage-otel-attribute.test.ts +22 -20
- package/src/scenarios/self-hosted-runner.test.ts +26 -24
- package/src/scenarios/semantic-digest-vectors.test.ts +8 -7
- package/src/scenarios/speech-synthesis-roundtrip.test.ts +9 -7
- package/src/scenarios/speech-synthesis-unadvertised.test.ts +5 -3
- package/src/scenarios/sql-injection-rejection.test.ts +8 -6
- package/src/scenarios/sql-transaction-atomicity.test.ts +10 -8
- package/src/scenarios/staleClaim.test.ts +5 -4
- package/src/scenarios/stream-modes-buffer.test.ts +8 -7
- package/src/scenarios/stream-modes-mixed.test.ts +10 -9
- package/src/scenarios/stream-modes.test.ts +12 -11
- package/src/scenarios/stream-subscribe-from-beginning.test.ts +11 -9
- package/src/scenarios/stream-text-fixture.test.ts +14 -13
- package/src/scenarios/streamReconnect.test.ts +13 -11
- package/src/scenarios/strict-behavior-gate.test.ts +9 -8
- package/src/scenarios/subrun-approval-fail-closed.test.ts +2 -2
- package/src/scenarios/subrun-approval-gate.test.ts +3 -3
- package/src/scenarios/subrun-attestation-shape.test.ts +2 -2
- package/src/scenarios/subrun-checksum-stable.test.ts +4 -4
- package/src/scenarios/subworkflow-input-mapping.test.ts +16 -14
- package/src/scenarios/subworkflow.test.ts +10 -9
- package/src/scenarios/table-cross-tenant-isolation.test.ts +8 -6
- package/src/scenarios/table-cursor-pagination.test.ts +8 -6
- package/src/scenarios/table-schema-enforcement.test.ts +7 -5
- package/src/scenarios/test-seam-unauthenticated.test.ts +2 -2
- package/src/scenarios/tool-catalog-compact-projection.test.ts +10 -8
- package/src/scenarios/tool-catalog-projection.test.ts +11 -9
- package/src/scenarios/tool-descriptor-shape.test.ts +14 -15
- package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +3 -3
- package/src/scenarios/tool-hooks-content-free.test.ts +4 -4
- package/src/scenarios/tool-hooks-failure-honesty.test.ts +4 -4
- package/src/scenarios/tool-hooks-rate-limit.test.ts +2 -2
- package/src/scenarios/tool-hooks-secret-redaction.test.ts +2 -2
- package/src/scenarios/tool-hooks-shape.test.ts +3 -3
- package/src/scenarios/tool-session-lifecycle.test.ts +14 -13
- package/src/scenarios/trigger-bridge-delivery.test.ts +14 -14
- package/src/scenarios/trigger-bridge-shape.test.ts +19 -20
- package/src/scenarios/trigger-ingestion.test.ts +19 -17
- package/src/scenarios/trigger-stream-cdc-sources.test.ts +16 -14
- package/src/scenarios/v2-approver-enforced.test.ts +97 -0
- package/src/scenarios/v2-assurance-downgrade-audited.test.ts +75 -0
- package/src/scenarios/v2-bundle-v3-signed.test.ts +96 -0
- package/src/scenarios/v2-capabilities-root-closed.test.ts +63 -0
- package/src/scenarios/v2-capability-record-shape.test.ts +104 -0
- package/src/scenarios/v2-chain-pin-exact.test.ts +118 -0
- package/src/scenarios/v2-coherence-not-in-bundle.test.ts +98 -0
- package/src/scenarios/v2-compensation-read-projection.test.ts +85 -0
- package/src/scenarios/v2-configurable-closed.test.ts +67 -0
- package/src/scenarios/v2-dual-stack-negotiation.test.ts +118 -0
- package/src/scenarios/v2-effect-identity-business-key.test.ts +132 -0
- package/src/scenarios/v2-effect-seam-manifest.test.ts +163 -0
- package/src/scenarios/v2-enum-growth-rule.test.ts +69 -0
- package/src/scenarios/v2-era-key.test.ts +86 -0
- package/src/scenarios/v2-error-registry.test.ts +89 -0
- package/src/scenarios/v2-event-type-closed.test.ts +97 -0
- package/src/scenarios/v2-fork-a-v1-run.test.ts +109 -0
- package/src/scenarios/v2-form-when-reuses-edge-conditions.test.ts +70 -0
- package/src/scenarios/v2-header-scheme.test.ts +67 -0
- package/src/scenarios/v2-id-grammar.test.ts +77 -0
- package/src/scenarios/v2-idempotency-key-grammar.test.ts +65 -0
- package/src/scenarios/v2-interrupt-token-scheme.test.ts +55 -0
- package/src/scenarios/v2-lane-issuer-advertised.test.ts +62 -0
- package/src/scenarios/v2-legacy-profiles-absent.test.ts +78 -0
- package/src/scenarios/v2-manifest-ceiling-refused.test.ts +130 -0
- package/src/scenarios/v2-manifest-hatch-carried.test.ts +120 -0
- package/src/scenarios/v2-min-client-version.test.ts +48 -0
- package/src/scenarios/v2-minimum-version-refused.test.ts +141 -0
- package/src/scenarios/v2-mrtr-rounds-ceiling.test.ts +95 -0
- package/src/scenarios/v2-negotiation-authenticated.test.ts +125 -0
- package/src/scenarios/v2-negotiation-decided-emitted.test.ts +149 -0
- package/src/scenarios/v2-no-transport-advertisement.test.ts +57 -0
- package/src/scenarios/v2-owner-subject-required.test.ts +72 -0
- package/src/scenarios/v2-pack-isolation.test.ts +147 -0
- package/src/scenarios/v2-payload-registry-closed.test.ts +110 -0
- package/src/scenarios/v2-peer-dependency-declared.test.ts +132 -0
- package/src/scenarios/v2-pinned-run-disposition.test.ts +124 -0
- package/src/scenarios/v2-poll-cursor-v2.test.ts +114 -0
- package/src/scenarios/v2-preferred-version-default.test.ts +83 -0
- package/src/scenarios/v2-profiles-derived-only.test.ts +70 -0
- package/src/scenarios/v2-provider-conflict.test.ts +115 -0
- package/src/scenarios/v2-refresh-sla.test.ts +78 -0
- package/src/scenarios/v2-relaxation-recorded.test.ts +134 -0
- package/src/scenarios/v2-revocation-honored.test.ts +66 -0
- package/src/scenarios/v2-subject-link-record.test.ts +102 -0
- package/src/scenarios/v2-unmapped-type-refused.test.ts +95 -0
- package/src/scenarios/v2-v1-events-translated.test.ts +105 -0
- package/src/scenarios/v2-v1-signed-webhook-accepted.test.ts +109 -0
- package/src/scenarios/v2-webhook-durable-delivery.test.ts +238 -0
- package/src/scenarios/v2-well-known-one-resource.test.ts +129 -0
- package/src/scenarios/vector-knn-roundtrip.test.ts +9 -7
- package/src/scenarios/verifier-gating.test.ts +8 -6
- package/src/scenarios/version-fold.test.ts +9 -8
- package/src/scenarios/version-negotiation.test.ts +12 -11
- package/src/scenarios/voice-bargein-no-partial-leak.test.ts +8 -6
- package/src/scenarios/voice-event-payloads-shape.test.ts +10 -11
- package/src/scenarios/voice-interim-not-durable.test.ts +7 -5
- package/src/scenarios/voice-streamref-tenant-bound.test.ts +5 -3
- package/src/scenarios/voice-synthesis-streaming.test.ts +11 -9
- package/src/scenarios/voice-transcription-streaming.test.ts +8 -6
- package/src/scenarios/voice-transcription-unadvertised.test.ts +5 -3
- package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +3 -2
- 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 +5 -4
- package/src/scenarios/wasm-pack-replay-determinism.test.ts +2 -1
- package/src/scenarios/webhook-negative.test.ts +11 -9
- package/src/scenarios/webhook-receiver-adversarial.test.ts +8 -7
- package/src/scenarios/webhook-sig-algorithm.test.ts +5 -3
- package/src/scenarios/webhook-signed-delivery.test.ts +16 -15
- package/src/scenarios/webhook-tenant-isolation.test.ts +16 -14
- package/src/scenarios/workflow-chain-deferred-parameters.test.ts +28 -30
- package/src/scenarios/workflow-chain-expansion.test.ts +21 -20
- package/src/scenarios/workflow-chain-host-expansion.test.ts +21 -20
- package/src/scenarios/workflow-chain-pack-manifest-validation.test.ts +14 -12
- package/src/scenarios/workflow-chain-pack-signature-verification.test.ts +5 -4
- package/src/scenarios/workflow-chain-unresolvable-typeid.test.ts +9 -8
- package/src/scenarios/workflow-primary-output-annotation.test.ts +7 -6
- package/src/scenarios/workflow-variable-format.test.ts +24 -24
- package/src/scenarios/workload-identity-behavior.test.ts +15 -13
- package/src/scenarios/workload-identity-chain-bounds.test.ts +18 -17
- package/src/scenarios/workspace-behavior.test.ts +19 -18
- package/src/scenarios/workspace-capability-shape.test.ts +3 -2
- package/src/scenarios/workspace-cross-tenant-isolation-blackbox.test.ts +6 -5
- package/src/scenarios/workspace-cross-tenant-isolation.test.ts +7 -6
- package/src/scenarios/x-openwop-form-pack-manifest.test.ts +8 -7
- package/src/setup.ts +44 -6
- package/vitest.config.ts +5 -1
- package/api/.redocly.lint-ignore.yaml +0 -22
- package/api/asyncapi.yaml +0 -834
- package/api/grpc/openwop.proto +0 -251
- package/api/openapi.yaml +0 -2693
- package/api/redocly.yaml +0 -23
- package/schemas/README.md +0 -144
- package/schemas/a2a-task-state.schema.json +0 -78
- package/schemas/a2ui-surface-delta-frame.schema.json +0 -48
- package/schemas/agent-deployment-transition.schema.json +0 -49
- package/schemas/agent-deployment.schema.json +0 -54
- package/schemas/agent-eval-suite.schema.json +0 -140
- package/schemas/agent-inventory-response.schema.json +0 -115
- package/schemas/agent-manifest.schema.json +0 -156
- package/schemas/agent-org-chart.schema.json +0 -82
- package/schemas/agent-ref.schema.json +0 -63
- package/schemas/agent-roster-entry.schema.json +0 -81
- package/schemas/agent-roster-response.schema.json +0 -21
- package/schemas/ai-envelope.schema.json +0 -134
- package/schemas/annotation-create.schema.json +0 -37
- package/schemas/annotation.schema.json +0 -56
- package/schemas/artifact-type-pack-manifest.schema.json +0 -244
- package/schemas/audit-verify-result.schema.json +0 -90
- package/schemas/budget-policy.schema.json +0 -18
- package/schemas/capabilities.schema.json +0 -4160
- package/schemas/certification-bundle-v2.schema.json +0 -118
- package/schemas/channel-presence-payload.schema.json +0 -41
- package/schemas/channel-written-payload.schema.json +0 -55
- package/schemas/chat-card-pack-manifest.schema.json +0 -253
- package/schemas/compact-tool-descriptor.schema.json +0 -51
- package/schemas/compensation-policy.schema.json +0 -64
- package/schemas/conformance-certification-bundle.schema.json +0 -86
- package/schemas/connection-pack-manifest.schema.json +0 -353
- package/schemas/conversation-event.schema.json +0 -168
- package/schemas/conversation-turn.schema.json +0 -117
- package/schemas/core-conformance-mock-agent-config.schema.json +0 -152
- package/schemas/credential-provenance.schema.json +0 -18
- package/schemas/credential-reference.schema.json +0 -21
- package/schemas/debug-bundle.schema.json +0 -196
- package/schemas/dispatch-config.schema.json +0 -112
- package/schemas/envelopes/clarification.request.schema.json +0 -52
- package/schemas/envelopes/error.schema.json +0 -30
- package/schemas/envelopes/media.audio.schema.json +0 -38
- package/schemas/envelopes/media.file.schema.json +0 -37
- package/schemas/envelopes/media.image.schema.json +0 -33
- package/schemas/envelopes/schema.request.schema.json +0 -26
- package/schemas/envelopes/schema.response.schema.json +0 -22
- package/schemas/envelopes/ui.a2ui-surface.schema.json +0 -154
- package/schemas/error-envelope.schema.json +0 -25
- package/schemas/eval-summary.schema.json +0 -92
- package/schemas/export-bundle.schema.json +0 -66
- package/schemas/form-content-pack-manifest.schema.json +0 -243
- package/schemas/frontend-plugin-manifest.schema.json +0 -109
- package/schemas/goal.schema.json +0 -104
- package/schemas/heartbeat-evaluated.schema.json +0 -14
- package/schemas/heartbeat-state-changed.schema.json +0 -14
- package/schemas/localized-content-language-settings.schema.json +0 -26
- package/schemas/localized-content-page-response.schema.json +0 -60
- package/schemas/localized-content-page.schema.json +0 -62
- package/schemas/localized-content-section.schema.json +0 -51
- package/schemas/memory-entry.schema.json +0 -36
- package/schemas/memory-list-options.schema.json +0 -37
- package/schemas/node-pack-manifest.schema.json +0 -654
- package/schemas/orchestrator-decision.schema.json +0 -90
- package/schemas/org-chart-responsibility-view.schema.json +0 -26
- package/schemas/pack-lockfile.schema.json +0 -108
- package/schemas/prompt-kind.schema.json +0 -8
- package/schemas/prompt-pack-manifest.schema.json +0 -117
- package/schemas/prompt-ref.schema.json +0 -40
- package/schemas/prompt-template.schema.json +0 -149
- package/schemas/proposal.schema.json +0 -84
- package/schemas/registry-version-manifest.schema.json +0 -208
- package/schemas/residency.schema.json +0 -16
- package/schemas/run-ancestry-response.schema.json +0 -54
- package/schemas/run-diff-response.schema.json +0 -64
- package/schemas/run-event-payloads.schema.json +0 -4452
- package/schemas/run-event.schema.json +0 -211
- package/schemas/run-options.schema.json +0 -80
- package/schemas/run-orchestrator-decided-event.schema.json +0 -20
- package/schemas/run-snapshot.schema.json +0 -148
- package/schemas/security-advisory.schema.json +0 -109
- package/schemas/self-hosted-runner-dispatch-frame.schema.json +0 -79
- package/schemas/self-hosted-runner-registration.schema.json +0 -53
- package/schemas/self-hosted-runner-result-frame.schema.json +0 -38
- package/schemas/subject.schema.json +0 -56
- package/schemas/suspend-request.schema.json +0 -207
- package/schemas/tool-descriptor.schema.json +0 -63
- package/schemas/trigger-event.schema.json +0 -222
- package/schemas/trigger-subscription-registration.schema.json +0 -67
- package/schemas/trigger-subscription.schema.json +0 -26
- package/schemas/ui-plugin-message.schema.json +0 -90
- package/schemas/workflow-chain-pack-manifest.schema.json +0 -615
- package/schemas/workflow-definition.schema.json +0 -850
- package/schemas/workload-identity.schema.json +0 -79
- package/schemas/workspace-file-create.schema.json +0 -20
- package/schemas/workspace-file.schema.json +0 -39
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Certification bundle v3 (RFC 0168 §E; schemas/v2/certification-bundle.schema.json).
|
|
3
|
+
*
|
|
4
|
+
* What v3 adds over v2 and why:
|
|
5
|
+
* - a closed root and `witnessSha256` over the reporter record (RFC 0148 §C
|
|
6
|
+
* asked for it; the v2 schema listed it optional and no emitter wrote it);
|
|
7
|
+
* - `assertionCount`, `host.build`, per-profile `evidenceTier`/`witnessCount`,
|
|
8
|
+
* `detail` (required when any result is not executed-pass);
|
|
9
|
+
* - an Ed25519 attestation over the canonical JSON of
|
|
10
|
+
* { witnessSha256, host.build, suite.version, discovery.sha256 } — the same
|
|
11
|
+
* commit rebuilt once scored 283/22 and 303/2 and nothing in the bundle could
|
|
12
|
+
* tell them apart; the build id plus the signature can;
|
|
13
|
+
* - `evidenceTier: independent` REQUIRES a verifier signature under a key id
|
|
14
|
+
* distinct from the host's (RFC 0148 R5: a malicious generator fabricates
|
|
15
|
+
* witnesses — a self-signed "independent" claim is refused, not warned).
|
|
16
|
+
*
|
|
17
|
+
* This module is schema-agnostic on purpose (the schema check is a separate step
|
|
18
|
+
* in cli.ts) and pure: it never reads the environment.
|
|
19
|
+
*/
|
|
20
|
+
import { createHash, createPrivateKey, createPublicKey, sign as edSign, verify as edVerify, type KeyObject } from 'node:crypto';
|
|
21
|
+
|
|
22
|
+
export type BundleV3Result = 'executed-pass' | 'executed-fail' | 'skipped' | 'inapplicable' | 'blocked';
|
|
23
|
+
|
|
24
|
+
export interface BundleV3Requirement {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly scenario: string;
|
|
27
|
+
readonly result: BundleV3Result;
|
|
28
|
+
readonly assertions?: number;
|
|
29
|
+
readonly detail?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface BundleV3Profile {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly evidenceTier: 'self' | 'steward' | 'independent';
|
|
34
|
+
readonly witnessCount: number;
|
|
35
|
+
readonly certified: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface BundleV3Relaxation { readonly obligation: string; readonly durability: 'session' | 'deployment' | 'persisted'; readonly reason: string }
|
|
38
|
+
export interface BundleV3Signature { readonly alg: 'ed25519'; readonly keyId: string; readonly sig: string; readonly over: readonly string[]; readonly verifierKeyId?: string }
|
|
39
|
+
export interface BundleV3 {
|
|
40
|
+
bundleVersion: '3';
|
|
41
|
+
generatedAt: string;
|
|
42
|
+
suite: { name: '@openwop/openwop-conformance'; version: string; targetMajor: 1 | 2; specArtifactsVersion: string; stampSha256?: string };
|
|
43
|
+
host: { name: string; version: string; vendor?: string; build: { kind: 'image-digest' | 'commit' | 'artifact-sha256'; id: string }; signingKeyId?: string; relaxations?: BundleV3Relaxation[] };
|
|
44
|
+
discovery: { url: string; sha256: string; protocolVersions: string[]; preferredVersion: string };
|
|
45
|
+
claimedProfiles: BundleV3Profile[];
|
|
46
|
+
results: { totals: Record<'executedPass' | 'executedFail' | 'skipped' | 'inapplicable' | 'blocked', number>; requirements: BundleV3Requirement[] };
|
|
47
|
+
witnessSha256: string;
|
|
48
|
+
assertionCount: number;
|
|
49
|
+
detail?: { nonPass: { id: string; result: string; reason: string }[] };
|
|
50
|
+
signature: BundleV3Signature;
|
|
51
|
+
verifierSignature?: { alg: 'ed25519'; keyId: string; sig: string };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const SIGNATURE_OVER = ['witnessSha256', 'host.build', 'suite.version', 'discovery.sha256'] as const;
|
|
55
|
+
|
|
56
|
+
/** Deterministic JSON: keys sorted at every level, no whitespace. */
|
|
57
|
+
export function canonicalJSON(value: unknown): string {
|
|
58
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJSON).join(',')}]`;
|
|
59
|
+
if (value !== null && typeof value === 'object') {
|
|
60
|
+
return `{${Object.keys(value as Record<string, unknown>).sort().map((k) => `${JSON.stringify(k)}:${canonicalJSON((value as Record<string, unknown>)[k])}`).join(',')}}`;
|
|
61
|
+
}
|
|
62
|
+
return JSON.stringify(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** RFC 0148 §C — the digest over the reporter record (the requirement rows). */
|
|
66
|
+
export function witnessDigest(rows: readonly BundleV3Requirement[]): string {
|
|
67
|
+
const canonicalRows = [...rows].sort((a, b) => a.id.localeCompare(b.id)).map((r) => ({ id: r.id, scenario: r.scenario, result: r.result, ...(r.assertions === undefined ? {} : { assertions: r.assertions }), ...(r.detail === undefined ? {} : { detail: r.detail }) }));
|
|
68
|
+
return createHash('sha256').update(canonicalJSON(canonicalRows), 'utf8').digest('hex');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The bytes the attestation covers. */
|
|
72
|
+
export function attestationPayload(b: Pick<BundleV3, 'witnessSha256' | 'host' | 'suite' | 'discovery'>): Buffer {
|
|
73
|
+
return Buffer.from(canonicalJSON({ witnessSha256: b.witnessSha256, 'host.build': b.host.build, 'suite.version': b.suite.version, 'discovery.sha256': b.discovery.sha256 }), 'utf8');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function toBase64url(buf: Buffer): string { return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); }
|
|
77
|
+
function fromBase64url(s: string): Buffer { return Buffer.from(s.replace(/-/g, '+').replace(/_/g, '/'), 'base64'); }
|
|
78
|
+
|
|
79
|
+
export function signBundleV3(bundle: Omit<BundleV3, 'signature'>, privateKeyPem: string, keyId: string): BundleV3Signature {
|
|
80
|
+
const key = createPrivateKey(privateKeyPem);
|
|
81
|
+
if (key.asymmetricKeyType !== 'ed25519') throw new Error(`bundle v3 signing key must be Ed25519, got ${String(key.asymmetricKeyType)}`);
|
|
82
|
+
const sig = edSign(null, attestationPayload(bundle), key);
|
|
83
|
+
return { alg: 'ed25519', keyId, sig: toBase64url(sig), over: [...SIGNATURE_OVER] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function verifierSign(bundle: Pick<BundleV3, 'witnessSha256' | 'host' | 'suite' | 'discovery'>, privateKeyPem: string, keyId: string): NonNullable<BundleV3['verifierSignature']> {
|
|
87
|
+
const key = createPrivateKey(privateKeyPem);
|
|
88
|
+
return { alg: 'ed25519', keyId, sig: toBase64url(edSign(null, attestationPayload(bundle), key)) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function publicKeyFromPrivate(privateKeyPem: string): string {
|
|
92
|
+
return createPublicKey(createPrivateKey(privateKeyPem)).export({ type: 'spki', format: 'pem' }) as string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface VerifyV3Options {
|
|
96
|
+
/** The host's public key (PEM). Without it the signature's bytes are checked for shape only and the verdict says `signatureVerified: false`. */
|
|
97
|
+
readonly hostPublicKeyPem?: string;
|
|
98
|
+
/** The verifier's public key (PEM) — required to verify an `independent` claim. */
|
|
99
|
+
readonly verifierPublicKeyPem?: string;
|
|
100
|
+
}
|
|
101
|
+
export interface V3Rejection { readonly kind: string; readonly detail: string; readonly profile?: string }
|
|
102
|
+
export interface V3Verdict {
|
|
103
|
+
readonly rejections: V3Rejection[];
|
|
104
|
+
readonly signatureVerified: boolean;
|
|
105
|
+
readonly verifierSignatureVerified: boolean;
|
|
106
|
+
readonly certifiedProfiles: string[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function verifyBundleV3(bundle: BundleV3, opts: VerifyV3Options = {}): V3Verdict {
|
|
110
|
+
const rejections: V3Rejection[] = [];
|
|
111
|
+
if (bundle.bundleVersion !== '3') rejections.push({ kind: 'not-v3', detail: `bundleVersion is ${JSON.stringify(bundle.bundleVersion)}, expected "3"` });
|
|
112
|
+
const rows = bundle.results?.requirements ?? [];
|
|
113
|
+
const seen = new Set<string>();
|
|
114
|
+
for (const r of rows) {
|
|
115
|
+
if (seen.has(r.id)) rejections.push({ kind: 'duplicate-requirement', detail: `${r.id}: more than one row (RFC 0148 §A)` });
|
|
116
|
+
seen.add(r.id);
|
|
117
|
+
if (r.result !== 'executed-pass' && !(r.detail && r.detail.trim())) rejections.push({ kind: 'reason-missing', detail: `${r.id}: '${r.result}' recorded without a reason` });
|
|
118
|
+
if (r.result === 'executed-pass' && !(r.assertions && r.assertions > 0)) rejections.push({ kind: 'vacuous-pass', detail: `${r.id}: executed-pass with no assertions is a witness of nothing (RFC 0148 §C)` });
|
|
119
|
+
}
|
|
120
|
+
const count = (d: BundleV3Result): number => rows.filter((r) => r.result === d).length;
|
|
121
|
+
const expected = { executedPass: count('executed-pass'), executedFail: count('executed-fail'), skipped: count('skipped'), inapplicable: count('inapplicable'), blocked: count('blocked') };
|
|
122
|
+
for (const k of Object.keys(expected) as (keyof typeof expected)[]) if (bundle.results?.totals?.[k] !== expected[k]) rejections.push({ kind: 'totals-mismatch', detail: `totals.${k} is ${String(bundle.results?.totals?.[k])} but the rows count ${expected[k]}` });
|
|
123
|
+
const digest = witnessDigest(rows);
|
|
124
|
+
if (bundle.witnessSha256 !== digest) rejections.push({ kind: 'witness-digest', detail: `witnessSha256 ${String(bundle.witnessSha256).slice(0, 12)} does not equal the digest of the rows (${digest.slice(0, 12)})` });
|
|
125
|
+
const assertions = rows.reduce((n, r) => n + (r.assertions ?? 0), 0);
|
|
126
|
+
if (bundle.assertionCount !== assertions) rejections.push({ kind: 'assertion-count', detail: `assertionCount is ${String(bundle.assertionCount)} but the rows sum to ${assertions}` });
|
|
127
|
+
const nonPass = rows.filter((r) => r.result !== 'executed-pass');
|
|
128
|
+
if (nonPass.length > 0 && !bundle.detail) rejections.push({ kind: 'detail-missing', detail: `${nonPass.length} non-pass row(s) and no detail block` });
|
|
129
|
+
if (!bundle.host?.build?.kind || !bundle.host?.build?.id) rejections.push({ kind: 'build-missing', detail: 'host.build.{kind,id} is required — a bundle attributes to a BUILD, not a commit' });
|
|
130
|
+
|
|
131
|
+
// Signature
|
|
132
|
+
let signatureVerified = false;
|
|
133
|
+
const sig = bundle.signature;
|
|
134
|
+
if (!sig || sig.alg !== 'ed25519' || !sig.keyId || !sig.sig) rejections.push({ kind: 'signature-missing', detail: 'signature.{alg: ed25519, keyId, sig} is required in v3' });
|
|
135
|
+
else if (JSON.stringify(sig.over) !== JSON.stringify(SIGNATURE_OVER)) rejections.push({ kind: 'signature-over', detail: `signature.over must be ${JSON.stringify(SIGNATURE_OVER)}` });
|
|
136
|
+
else if (opts.hostPublicKeyPem) {
|
|
137
|
+
const key: KeyObject = createPublicKey(opts.hostPublicKeyPem);
|
|
138
|
+
signatureVerified = edVerify(null, attestationPayload(bundle), key, fromBase64url(sig.sig));
|
|
139
|
+
if (!signatureVerified) rejections.push({ kind: 'signature-invalid', detail: 'the attestation does not verify under the host key' });
|
|
140
|
+
}
|
|
141
|
+
// Independent tier
|
|
142
|
+
let verifierSignatureVerified = false;
|
|
143
|
+
const claimsIndependent = (bundle.claimedProfiles ?? []).some((p) => p.evidenceTier === 'independent');
|
|
144
|
+
if (claimsIndependent) {
|
|
145
|
+
const vs = bundle.verifierSignature;
|
|
146
|
+
if (!vs || !vs.keyId || !vs.sig) rejections.push({ kind: 'independent-unsigned', detail: 'evidenceTier independent requires a verifierSignature (RFC 0168 §E.2)' });
|
|
147
|
+
else if (vs.keyId === sig?.keyId || vs.keyId === bundle.host?.signingKeyId) rejections.push({ kind: 'independent-self-signed', detail: 'the verifier key must be distinct from the host key (RFC 0148 R5)' });
|
|
148
|
+
else if (opts.verifierPublicKeyPem) {
|
|
149
|
+
verifierSignatureVerified = edVerify(null, attestationPayload(bundle), createPublicKey(opts.verifierPublicKeyPem), fromBase64url(vs.sig));
|
|
150
|
+
if (!verifierSignatureVerified) rejections.push({ kind: 'verifier-signature-invalid', detail: 'the verifier attestation does not verify' });
|
|
151
|
+
} else rejections.push({ kind: 'independent-unverifiable', detail: 'an independent claim needs the verifier public key to verify; refused, not assumed' });
|
|
152
|
+
}
|
|
153
|
+
// Relaxations: a relaxed obligation's profile cannot certify (RFC 0173 §A.2).
|
|
154
|
+
const relaxed = new Set((bundle.host?.relaxations ?? []).map((r) => r.obligation.split('.')[0]));
|
|
155
|
+
const certifiedProfiles: string[] = [];
|
|
156
|
+
for (const p of bundle.claimedProfiles ?? []) {
|
|
157
|
+
if (p.certified && relaxed.size > 0 && [...relaxed].some((o) => p.id.includes(o))) rejections.push({ kind: 'relaxed-profile-certified', profile: p.id, detail: `${p.id} is marked certified while a relaxation on ${[...relaxed].join(', ')} is recorded (RFC 0173 §A.2)` });
|
|
158
|
+
else if (p.certified) certifiedProfiles.push(p.id);
|
|
159
|
+
}
|
|
160
|
+
if (expected.blocked > 0 && certifiedProfiles.length > 0) rejections.push({ kind: 'blocked-certified', detail: `${expected.blocked} blocked row(s): a bundle with blocked > 0 does not certify (RFC 0168 §E.1)` });
|
|
161
|
+
// RFC 0168 §E.2: the verifier REFUSES, it does not warn — but a refusal is
|
|
162
|
+
// scoped to what it names. A rejection carrying a `profile` removes that
|
|
163
|
+
// profile only (a relaxation on one obligation does not poison an unrelated
|
|
164
|
+
// one, RFC 0173 §A.2); a rejection that names no profile is a statement about
|
|
165
|
+
// the bundle — a bad witness digest, an unverifiable signature, a self-signed
|
|
166
|
+
// `independent` claim, a blocked row — and nothing in it certifies.
|
|
167
|
+
const bundleWide = rejections.filter((r) => !('profile' in r) || !r.profile);
|
|
168
|
+
const scoped = new Set(rejections.map((r) => r.profile).filter((p): p is string => typeof p === 'string'));
|
|
169
|
+
return {
|
|
170
|
+
rejections,
|
|
171
|
+
signatureVerified,
|
|
172
|
+
verifierSignatureVerified,
|
|
173
|
+
certifiedProfiles: bundleWide.length > 0 ? [] : certifiedProfiles.filter((p) => !scoped.has(p)),
|
|
174
|
+
};
|
|
175
|
+
}
|
package/src/lib/corpus-stamp.ts
CHANGED
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
|
|
27
27
|
import { createHash } from 'node:crypto';
|
|
28
28
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
29
|
-
import {
|
|
29
|
+
import { createRequire } from 'node:module';
|
|
30
|
+
import { join, relative, sep, dirname } from 'node:path';
|
|
30
31
|
|
|
31
32
|
export interface CorpusStamp {
|
|
32
33
|
readonly suiteVersion?: string;
|
|
@@ -73,6 +74,33 @@ export function digestVendoredFiles(root: string): Record<string, string> {
|
|
|
73
74
|
* Compare the stamp's `files` map against the bytes on disk. Pure: reads,
|
|
74
75
|
* never writes, never throws on a mismatch — the caller decides how loud.
|
|
75
76
|
*/
|
|
77
|
+
/**
|
|
78
|
+
* Suite 2.0.0 (RFC 0168 §D.2): the contract is the `@openwop/spec-artifacts` peer.
|
|
79
|
+
* The suite embeds, at pack time, `dist/spec-artifacts.lock.json` =
|
|
80
|
+
* { version, stampSha256 } for the peer it was built against; at start it
|
|
81
|
+
* resolves the installed peer, recomputes its stamp digest, and refuses to run
|
|
82
|
+
* on a version or digest mismatch (a floating peer makes a bundle
|
|
83
|
+
* irreproducible). The peer's own files are then verified against its stamp.
|
|
84
|
+
*/
|
|
85
|
+
export function verifyPeerContract(pkgRoot: string): StampVerdict {
|
|
86
|
+
const lockPath = join(pkgRoot, 'dist', 'spec-artifacts.lock.json');
|
|
87
|
+
if (!existsSync(lockPath)) return { kind: 'not-applicable', reason: 'dist/spec-artifacts.lock.json is absent — a repo checkout, not a packed 2.x suite' };
|
|
88
|
+
let lock: { version: string; stampSha256: string };
|
|
89
|
+
try { lock = JSON.parse(readFileSync(lockPath, 'utf8')); } catch { return { kind: 'mismatch', missing: ['dist/spec-artifacts.lock.json'], altered: [], extra: [] }; }
|
|
90
|
+
let peerRoot: string;
|
|
91
|
+
try { peerRoot = dirname(createRequire(join(pkgRoot, 'package.json')).resolve('@openwop/spec-artifacts/package.json')); }
|
|
92
|
+
catch { return { kind: 'mismatch', missing: ['node_modules/@openwop/spec-artifacts (peer dependency not installed)'], altered: [], extra: [] }; }
|
|
93
|
+
const stampPath = join(peerRoot, 'CORPUS-STAMP.json');
|
|
94
|
+
if (!existsSync(stampPath)) return { kind: 'mismatch', missing: ['@openwop/spec-artifacts/CORPUS-STAMP.json'], altered: [], extra: [] };
|
|
95
|
+
const stamp = JSON.parse(readFileSync(stampPath, 'utf8')) as { package: string; version: string; files: Record<string, string> };
|
|
96
|
+
const digest = createHash('sha256').update(JSON.stringify({ package: stamp.package, version: stamp.version, files: stamp.files })).digest('hex');
|
|
97
|
+
if (stamp.version !== lock.version || digest !== lock.stampSha256) return { kind: 'mismatch', missing: [], altered: [`@openwop/spec-artifacts ${stamp.version} (digest ${digest.slice(0, 12)}) ≠ the suite's lock ${lock.version} (${lock.stampSha256.slice(0, 12)})`], extra: [] };
|
|
98
|
+
const missing: string[] = []; const altered: string[] = [];
|
|
99
|
+
for (const [rel, d] of Object.entries(stamp.files)) { const p = join(peerRoot, ...rel.split('/')); if (!existsSync(p)) missing.push(rel); else if (sha256File(p) !== d) altered.push(rel); }
|
|
100
|
+
if (missing.length || altered.length) return { kind: 'mismatch', missing, altered, extra: [] };
|
|
101
|
+
return { kind: 'verified', files: Object.keys(stamp.files).length };
|
|
102
|
+
}
|
|
103
|
+
|
|
76
104
|
export function verifyCorpusStamp(root: string, layout: string): StampVerdict {
|
|
77
105
|
if (layout !== 'published') {
|
|
78
106
|
return { kind: 'not-applicable', reason: `layout is '${layout}' — the vendored copy exists only in the published package` };
|
package/src/lib/driver.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { loadEnv } from './env.js';
|
|
13
|
+
import { seamPath, targetMajor } from './seams.js';
|
|
13
14
|
|
|
14
15
|
export interface OpenWOPResponse {
|
|
15
16
|
readonly status: number;
|
|
@@ -37,10 +38,17 @@ class OpenWOPDriver {
|
|
|
37
38
|
const env = loadEnv();
|
|
38
39
|
// An absolute URL is used as-is (a host may advertise its MCP server mount or
|
|
39
40
|
// an A2A endpoint on another origin); a path is joined to the base URL.
|
|
40
|
-
|
|
41
|
+
// Suite 2.0.0: under target major 2 a v1 seam path is rewritten to its
|
|
42
|
+
// api/seams-v2.yaml address (RFC 0168 §C.2) and every request names the
|
|
43
|
+
// contract it speaks with `OpenWOP-Version` (RFC 0172 §A.3) unless the
|
|
44
|
+
// scenario set one itself (the negotiation scenarios do).
|
|
45
|
+
const major = targetMajor();
|
|
46
|
+
const effectivePath = major === 2 ? seamPath(path) : path;
|
|
47
|
+
const url = /^https?:\/\//i.test(effectivePath) ? effectivePath : `${env.baseUrl}${effectivePath}`;
|
|
41
48
|
|
|
42
49
|
const headers: Record<string, string> = {
|
|
43
50
|
Accept: 'application/json',
|
|
51
|
+
...(major === 2 && !Object.keys(init.headers ?? {}).some((h) => h.toLowerCase() === 'openwop-version') ? { 'OpenWOP-Version': '2.0' } : {}),
|
|
44
52
|
...(init.headers ?? {}),
|
|
45
53
|
};
|
|
46
54
|
if (init.body !== undefined && headers['Content-Type'] === undefined) {
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suite 2.0.0 — the era-`2` event-log seed for the RFC 0176 persisted-data
|
|
3
|
+
* scenarios (`v2-v1-events-translated`, `v2-unmapped-type-refused`,
|
|
4
|
+
* `v2-fork-a-v1-run`, `v2-pinned-run-disposition`).
|
|
5
|
+
*
|
|
6
|
+
* A v2 host reading a run written by a v1 host translates every event through
|
|
7
|
+
* `spec/v2/event-codemap.json` at the storage boundary (persistence.md §The
|
|
8
|
+
* reader rule). To witness that from outside, the suite needs a run whose
|
|
9
|
+
* persisted log is in v1 vocabulary — which only a host that predates the cut
|
|
10
|
+
* has, unless a seam writes one. The seam these scenarios drive (seams-profile
|
|
11
|
+
* gated, RFC 0168 §C):
|
|
12
|
+
*
|
|
13
|
+
* POST /conformance/seams/sample/event-log/seed
|
|
14
|
+
* { eventLogSchemaVersion: 2, status, events: [{ type, sequence, payload, timestamp?, causationId? }] }
|
|
15
|
+
* → 201 { runId }
|
|
16
|
+
*
|
|
17
|
+
* The host MUST persist the rows verbatim (v1 `type` strings, the given
|
|
18
|
+
* `sequence` space, no era stamp — the run reads as era `2` by the absent-⇒-`2`
|
|
19
|
+
* rule) and MUST NOT translate them at write time; the point of the seam is
|
|
20
|
+
* that the read projection does the work. `status` is the snapshot status the
|
|
21
|
+
* seeded run holds (`completed` for a finished log, `running` for a run the
|
|
22
|
+
* pinned-run disposition must decide). The seam is catalogued in
|
|
23
|
+
* `api/seams-v2.yaml` (`seedEra2EventLog`); a host answering `404` /
|
|
24
|
+
* `403` / `405` records `blocked` naming it (never a pass), and a host that
|
|
25
|
+
* answers anything but `201 { runId }` records `blocked` with the status.
|
|
26
|
+
*
|
|
27
|
+
* The scenarios name the seam by its v1-shaped path
|
|
28
|
+
* (`/v1/host/sample/event-log/seed`); under target major 2 the driver rewrites
|
|
29
|
+
* it to the address above (lib/seams.ts).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
import { driver, type OpenWOPResponse } from './driver.js';
|
|
35
|
+
import { loadEnv } from './env.js';
|
|
36
|
+
import { seamPath, seamsProfileAdvertised } from './seams.js';
|
|
37
|
+
import { readErrorCode } from './error-envelope.js';
|
|
38
|
+
import { V1_DIR } from './paths.js';
|
|
39
|
+
|
|
40
|
+
export const SEED_PATH_V1 = '/v1/host/sample/event-log/seed';
|
|
41
|
+
export const SEED_PATH = seamPath(SEED_PATH_V1);
|
|
42
|
+
|
|
43
|
+
export interface SeedEvent {
|
|
44
|
+
readonly type: string;
|
|
45
|
+
readonly sequence: number;
|
|
46
|
+
readonly payload: Record<string, unknown>;
|
|
47
|
+
readonly timestamp?: string;
|
|
48
|
+
readonly causationId?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ReadEvent {
|
|
52
|
+
readonly type?: unknown;
|
|
53
|
+
readonly sequence?: unknown;
|
|
54
|
+
readonly payload?: unknown;
|
|
55
|
+
readonly eventId?: unknown;
|
|
56
|
+
readonly timestamp?: unknown;
|
|
57
|
+
readonly causationId?: unknown;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type Seeded =
|
|
61
|
+
| { readonly ok: true; readonly runId: string }
|
|
62
|
+
| { readonly ok: false; readonly kind: 'blocked' | 'inapplicable' | 'skipped'; readonly reason: string };
|
|
63
|
+
|
|
64
|
+
async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
|
|
65
|
+
try { return await fn(); } catch { return null; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The gate every era-2 scenario applies before touching the seam. */
|
|
69
|
+
export function era2Gate(doc: Record<string, unknown> | null): Seeded | null {
|
|
70
|
+
if (!doc) return { ok: false, kind: 'blocked', reason: 'discovery unreachable' };
|
|
71
|
+
if (!seamsProfileAdvertised(doc)) {
|
|
72
|
+
return { ok: false, kind: 'blocked', reason: `seams profile not advertised (conformance.seamsProfile !== openwop-conformance-seams-v2) — an era-2 log can only be seeded through ${SEED_PATH}` };
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Seed an era-2 log in v1 vocabulary; the reason names the seam when it is absent. */
|
|
78
|
+
export async function seedEra2Log(events: readonly SeedEvent[], status: 'completed' | 'running' | 'paused' = 'completed'): Promise<Seeded> {
|
|
79
|
+
const res = await http(() => driver.post(SEED_PATH_V1, { eventLogSchemaVersion: 2, status, events }));
|
|
80
|
+
if (res === null) return { ok: false, kind: 'blocked', reason: `${SEED_PATH} unreachable (fetch failed)` };
|
|
81
|
+
if (res.status === 404 || res.status === 403 || res.status === 405) {
|
|
82
|
+
return { ok: false, kind: 'blocked', reason: `no seam seeds an era-2 event log — ${SEED_PATH} answered ${res.status} (uncatalogued in api/seams-v2.yaml; the reader rule is seam-gated, RFC 0176 falsifiability §A.3)` };
|
|
83
|
+
}
|
|
84
|
+
const runId = (res.json as { runId?: unknown } | undefined)?.runId;
|
|
85
|
+
if (res.status !== 201 || typeof runId !== 'string') {
|
|
86
|
+
return { ok: false, kind: 'blocked', reason: `${SEED_PATH} answered ${res.status} ${readErrorCode(res.json) ?? ''} without a 201 { runId } — the seed contract is not honoured`.trim() };
|
|
87
|
+
}
|
|
88
|
+
return { ok: true, runId };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** `GET /runs/{runId}/events/poll` under OpenWOP-Version: 2.0 (the driver adds the header). */
|
|
92
|
+
export async function pollEvents(runId: string, query = ''): Promise<OpenWOPResponse | null> {
|
|
93
|
+
return http(() => driver.get(`/runs/${encodeURIComponent(runId)}/events/poll?timeout=1${query}`));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function eventsOf(body: unknown): ReadEvent[] {
|
|
97
|
+
const events = (body as { events?: unknown } | undefined)?.events;
|
|
98
|
+
return Array.isArray(events) ? (events as ReadEvent[]) : [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* `GET /runs/{runId}/events?streamMode=debug` under OpenWOP-Version: 2.0 —
|
|
103
|
+
* `debug` admits every event (events.md §Stream modes), so a `version.pinned`
|
|
104
|
+
* or a vendor row is visible. lib/sse.ts sends no version header, and a
|
|
105
|
+
* header-less request selects the v1 representation (RFC 0172 §A.3), so the
|
|
106
|
+
* stream is read here with the header the scenario is speaking.
|
|
107
|
+
*/
|
|
108
|
+
export async function streamEvents(runId: string, timeoutMs = 8_000): Promise<{ status: number; events: ReadEvent[] } | null> {
|
|
109
|
+
const env = loadEnv();
|
|
110
|
+
const ctl = new AbortController();
|
|
111
|
+
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
|
112
|
+
try {
|
|
113
|
+
const res = await fetch(`${env.baseUrl}/runs/${encodeURIComponent(runId)}/events?streamMode=debug`, {
|
|
114
|
+
headers: { Accept: 'text/event-stream', Authorization: `Bearer ${env.apiKey}`, 'OpenWOP-Version': '2.0' },
|
|
115
|
+
signal: ctl.signal,
|
|
116
|
+
});
|
|
117
|
+
if (res.status !== 200 || res.body === null) return { status: res.status, events: [] };
|
|
118
|
+
let text = '';
|
|
119
|
+
try { text = await res.text(); } catch (err) { if ((err as { name?: string }).name !== 'AbortError') throw err; }
|
|
120
|
+
const events: ReadEvent[] = [];
|
|
121
|
+
for (const frame of text.split(/\r?\n\r?\n/)) {
|
|
122
|
+
const data = frame.split(/\r?\n/).filter((l) => l.startsWith('data:')).map((l) => l.slice(5).replace(/^ /, '')).join('\n');
|
|
123
|
+
if (data.length === 0) continue;
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(data) as unknown;
|
|
126
|
+
for (const e of Array.isArray(parsed) ? parsed : [parsed]) if (e && typeof e === 'object') events.push(e as ReadEvent);
|
|
127
|
+
} catch { /* a keep-alive or a non-JSON frame */ }
|
|
128
|
+
}
|
|
129
|
+
return { status: res.status, events };
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if ((err as { name?: string }).name === 'AbortError') return { status: 0, events: [] };
|
|
132
|
+
return null;
|
|
133
|
+
} finally {
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** `POST /runs/{runId}:fork`; null when the request could not be made. */
|
|
139
|
+
export async function forkRun(runId: string, body: Record<string, unknown>): Promise<OpenWOPResponse | null> {
|
|
140
|
+
return http(() => driver.post(`/runs/${encodeURIComponent(runId)}:fork`, body));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The v1→v2 rows the seeds use, taken from `spec/v2/event-codemap.json` when the
|
|
145
|
+
* corpus is present (the published tarball ships the codemap beside the
|
|
146
|
+
* schemas; a layout without it falls back to the same rows spelled here, which
|
|
147
|
+
* is what the codemap says for them — `counts.renamed: 36`).
|
|
148
|
+
*/
|
|
149
|
+
const FALLBACK: ReadonlyArray<readonly [string, string]> = [
|
|
150
|
+
['run.started', 'run.started'],
|
|
151
|
+
['run.completed', 'run.completed'],
|
|
152
|
+
['run.cancelled', 'run.cancelled'],
|
|
153
|
+
['version.pinned', 'version.pinned'],
|
|
154
|
+
['agent.toolCalled', 'agent.tool-called'],
|
|
155
|
+
['agent.toolReturned', 'agent.tool-returned'],
|
|
156
|
+
['run.resuming', 'run.resume-started'],
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
let codemap: Map<string, string> | undefined;
|
|
160
|
+
export function codemapV1toV2(): Map<string, string> {
|
|
161
|
+
if (codemap) return codemap;
|
|
162
|
+
const m = new Map<string, string>(FALLBACK);
|
|
163
|
+
const candidate = V1_DIR ? join(V1_DIR, '..', 'v2', 'event-codemap.json') : null;
|
|
164
|
+
if (candidate && existsSync(candidate)) {
|
|
165
|
+
try {
|
|
166
|
+
const doc = JSON.parse(readFileSync(candidate, 'utf8')) as { rows?: Array<{ v1?: unknown; v2?: unknown }> };
|
|
167
|
+
for (const r of doc.rows ?? []) if (typeof r.v1 === 'string' && typeof r.v2 === 'string') m.set(r.v1, r.v2);
|
|
168
|
+
} catch { /* the fallback rows stand */ }
|
|
169
|
+
}
|
|
170
|
+
codemap = m;
|
|
171
|
+
return m;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** A minimal era-2 log in v1 vocabulary: two renamed rows between run.started and run.completed. */
|
|
175
|
+
export function v1FixtureLog(workflowId = 'conformance-noop'): SeedEvent[] {
|
|
176
|
+
const t0 = Date.parse('2026-01-15T10:00:00.000Z');
|
|
177
|
+
const ts = (i: number) => new Date(t0 + i * 1000).toISOString();
|
|
178
|
+
return [
|
|
179
|
+
{ type: 'run.started', sequence: 0, payload: { workflowId }, timestamp: ts(0) },
|
|
180
|
+
{ type: 'agent.toolCalled', sequence: 1, payload: { agentId: 'conformance-agent', toolName: 'echo', callId: 'call-0001' }, timestamp: ts(1) },
|
|
181
|
+
{ type: 'agent.toolReturned', sequence: 2, payload: { agentId: 'conformance-agent', toolName: 'echo', callId: 'call-0001', status: 'ok' }, timestamp: ts(2) },
|
|
182
|
+
{ type: 'run.completed', sequence: 3, payload: { durationMs: 3000 }, timestamp: ts(3) },
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The observable part of an event for RFC 0041 §C byte-equivalence: type, sequence, payload (clock fields and minted ids excluded). */
|
|
187
|
+
export function observable(e: ReadEvent): string {
|
|
188
|
+
return JSON.stringify({ type: e.type, sequence: e.sequence, payload: canonical(e.payload) });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function canonical(v: unknown): unknown {
|
|
192
|
+
if (Array.isArray(v)) return v.map(canonical);
|
|
193
|
+
if (v !== null && typeof v === 'object') {
|
|
194
|
+
return Object.fromEntries(Object.keys(v as Record<string, unknown>).sort().map((k) => [k, canonical((v as Record<string, unknown>)[k])]));
|
|
195
|
+
}
|
|
196
|
+
return v;
|
|
197
|
+
}
|
package/src/lib/paths.ts
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
import { existsSync } from 'node:fs';
|
|
38
38
|
import { fileURLToPath } from 'node:url';
|
|
39
|
+
import { createRequire } from 'node:module';
|
|
39
40
|
import { dirname, join, resolve as pathResolve } from 'node:path';
|
|
40
41
|
|
|
41
42
|
// `dirname(fileURLToPath(import.meta.url))` for an ESM module compiled or
|
|
@@ -77,7 +78,22 @@ interface ResolvedLayout {
|
|
|
77
78
|
readonly layout: 'env-override' | 'repo' | 'published';
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Suite 2.0.0 (RFC 0168 §D.2): in the published layout the CONTRACT (api/ and
|
|
83
|
+
* schemas/) is the `@openwop/spec-artifacts` peer package, not files vendored
|
|
84
|
+
* into this tarball. Resolve its root through Node's resolver from this package
|
|
85
|
+
* so the host's installed peer is what the suite validates against.
|
|
86
|
+
*/
|
|
87
|
+
function resolvePeerRoot(): string | null {
|
|
88
|
+
try {
|
|
89
|
+
const req = createRequire(join(PKG_ROOT, 'package.json'));
|
|
90
|
+
return dirname(req.resolve('@openwop/spec-artifacts/package.json'));
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveFromRoot(root: string, layout: ResolvedLayout['layout'], contractRoot: string = root): ResolvedLayout {
|
|
81
97
|
// Two on-disk shapes for the layout root:
|
|
82
98
|
// - Repo: <root>/schemas, <root>/api, <root>/conformance/fixtures,
|
|
83
99
|
// <root>/conformance/{fixtures.md,coverage.md}, <root>/spec/v1/*.md
|
|
@@ -86,8 +102,8 @@ function resolveFromRoot(root: string, layout: ResolvedLayout['layout']): Resolv
|
|
|
86
102
|
// <root>/fixtures.md (when bundled), no spec/v1.
|
|
87
103
|
// Probe by checking whether `schemas/` lives at the conformance pkg root
|
|
88
104
|
// (vendored) vs one level up (repo).
|
|
89
|
-
const schemasDir = join(
|
|
90
|
-
const apiDir = join(
|
|
105
|
+
const schemasDir = join(contractRoot, 'schemas');
|
|
106
|
+
const apiDir = join(contractRoot, 'api');
|
|
91
107
|
const repoFixturesDir = join(root, 'conformance', 'fixtures');
|
|
92
108
|
const vendoredFixturesDir = join(root, 'fixtures');
|
|
93
109
|
const fixturesDir = existsSync(repoFixturesDir) ? repoFixturesDir : vendoredFixturesDir;
|
|
@@ -166,7 +182,13 @@ function resolveLayout(): ResolvedLayout {
|
|
|
166
182
|
if (parentHasSchemas) {
|
|
167
183
|
return resolveFromRoot(parent, 'repo');
|
|
168
184
|
}
|
|
185
|
+
const peer = resolvePeerRoot();
|
|
186
|
+
if (peer) {
|
|
187
|
+
return resolveFromRoot(PKG_ROOT, 'published', peer);
|
|
188
|
+
}
|
|
169
189
|
if (pkgHasSchemas) {
|
|
190
|
+
// A pre-2.0 tarball layout (schemas vendored in-package); kept so an old
|
|
191
|
+
// layout still resolves, but the 2.x stamp check refuses to run without the peer.
|
|
170
192
|
return resolveFromRoot(PKG_ROOT, 'published');
|
|
171
193
|
}
|
|
172
194
|
// Neither — return the published-style resolution rooted at PKG_ROOT
|
|
@@ -24,6 +24,20 @@
|
|
|
24
24
|
* `~3`, … (vitest allows duplicate titles; the ledger does not allow duplicate
|
|
25
25
|
* ids). A scenario MAY override the derived id for the current test with
|
|
26
26
|
* `req()` when it wants a hand-authored, registry-listed id.
|
|
27
|
+
*
|
|
28
|
+
* requireExplicitIds (suite 2.0.0, RFC 0168 §A.1): from 2.0.0 `req(id, section,
|
|
29
|
+
* requirement)` is the ONLY assertion-message form in `src/scenarios` and
|
|
30
|
+
* `src/coherence` — `driver.describe(section, requirement)` is gone from every
|
|
31
|
+
* scenario, and `scripts/check-req-only.mjs` (root) fails the gate when a
|
|
32
|
+
* scenario reintroduces it, when an `expect` carries a bare string message,
|
|
33
|
+
* when an `it` body returns without `softSkip` / `seamAbsent`, or when two
|
|
34
|
+
* `it`s in one file share an explicit id. Every `it` therefore names the
|
|
35
|
+
* requirement it witnesses on the wire; the title-derived allocator below is
|
|
36
|
+
* the fallback for an `it` that makes no cited assertion at all. The ids the
|
|
37
|
+
* 2.0.0 sweep wrote are exactly the ids the allocator derived in 1.x (same
|
|
38
|
+
* stem + slug + `~n` order), so no bundle id changed at the cut-over; an `it`
|
|
39
|
+
* with an interpolated title carries a hand-minted id from the static parts of
|
|
40
|
+
* its title, unique within the file.
|
|
27
41
|
*/
|
|
28
42
|
|
|
29
43
|
export const IT_ID_PREFIX = 'openwop.it.';
|
|
@@ -60,6 +74,27 @@ export function scenarioFileOfItId(requirementId: string): string | null {
|
|
|
60
74
|
return `${rest.slice(0, dot)}.test.ts`;
|
|
61
75
|
}
|
|
62
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Every FILE-derived id back to its file: the per-test `openwop.it.<stem>.<title>`
|
|
79
|
+
* plus the per-file rows the ledger also carries, `openwop.scenario.<stem>` and
|
|
80
|
+
* `openwop.floor.<stem>`. Gate rows (`openwop.profile.*`, `openwop.family.*`),
|
|
81
|
+
* explicit `req()` ids and the `openwop.floor.any.<prefix>` GROUP name no single
|
|
82
|
+
* file and return null. A caller that understood only `it` ids read a per-file
|
|
83
|
+
* row as an un-cited requirement and demanded a `req()` literal that, by
|
|
84
|
+
* construction, never exists.
|
|
85
|
+
*/
|
|
86
|
+
export function scenarioFileOfId(requirementId: string): string | null {
|
|
87
|
+
const it = scenarioFileOfItId(requirementId);
|
|
88
|
+
if (it !== null) return it;
|
|
89
|
+
for (const prefix of ['openwop.scenario.', 'openwop.floor.']) {
|
|
90
|
+
if (!requirementId.startsWith(prefix)) continue;
|
|
91
|
+
const rest = requirementId.slice(prefix.length);
|
|
92
|
+
if (rest.length === 0 || rest.startsWith('any.')) return null;
|
|
93
|
+
return `${rest}.test.ts`;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
63
98
|
/**
|
|
64
99
|
* Collision suffixing: the first occurrence keeps the bare id, later ones get
|
|
65
100
|
* `~2`, `~3`, … Keyed per file so a worker running many files never crosses
|
|
@@ -70,7 +105,11 @@ export class ItIdAllocator {
|
|
|
70
105
|
|
|
71
106
|
allocate(scenarioFile: string, title: string): string {
|
|
72
107
|
const base = itRequirementId(scenarioFile, title);
|
|
73
|
-
|
|
108
|
+
let n = (this.seen.get(base) ?? 0) + 1;
|
|
109
|
+
// An id a sibling `it` already claimed through `req()` never reached this
|
|
110
|
+
// allocator, so a same-titled `it` that cites nothing must skip past it —
|
|
111
|
+
// otherwise the first explicit `exists` and the derived `exists` collide.
|
|
112
|
+
while (claimedExplicit.has(n === 1 ? base : `${base}~${n}`)) n++;
|
|
74
113
|
this.seen.set(base, n);
|
|
75
114
|
return n === 1 ? base : `${base}~${n}`;
|
|
76
115
|
}
|
|
@@ -85,22 +124,33 @@ export class ItIdAllocator {
|
|
|
85
124
|
// ---------------------------------------------------------------------------
|
|
86
125
|
|
|
87
126
|
let explicitId: string | null = null;
|
|
127
|
+
/** Every id handed to `req()` in this worker — consulted by `ItIdAllocator`. */
|
|
128
|
+
const claimedExplicit = new Set<string>();
|
|
88
129
|
|
|
89
130
|
/**
|
|
90
131
|
* Attach a hand-authored requirement id to the CURRENT test. The per-`it`
|
|
91
132
|
* ledger row for this test is recorded under `id` instead of the derived
|
|
92
|
-
* title id. Returns the
|
|
93
|
-
*
|
|
133
|
+
* title id. Returns the SAME message shape `driver.describe` returns —
|
|
134
|
+
* `[<impl>@<version>] <section>: <requirement>` — so a failure still names
|
|
135
|
+
* the implementation under test:
|
|
94
136
|
*
|
|
95
137
|
* expect(x, req('openwop.auth.subject-link.leaver-deny', 'auth-profiles.md §Subject linking', 'deactivation MUST deny')).toBe(true)
|
|
96
138
|
*
|
|
139
|
+
* The implementation label is read straight from `OPENWOP_IMPLEMENTATION_NAME`
|
|
140
|
+
* / `OPENWOP_IMPLEMENTATION_VERSION` with the defaults `env.ts` uses (`unknown`)
|
|
141
|
+
* rather than through `loadEnv()`, so a suite self-test can call `req()`
|
|
142
|
+
* without `OPENWOP_BASE_URL` set.
|
|
143
|
+
*
|
|
97
144
|
* The id MUST be listed in `conformance/requirements.json` (the generator
|
|
98
145
|
* collects `req(` first-argument literals); an unlisted id fails the registry
|
|
99
146
|
* check, so a hand id cannot drift from the registry silently.
|
|
100
147
|
*/
|
|
101
148
|
export function req(id: string, specSection: string, requirement: string): string {
|
|
102
149
|
explicitId = id;
|
|
103
|
-
|
|
150
|
+
claimedExplicit.add(id);
|
|
151
|
+
const impl = process.env['OPENWOP_IMPLEMENTATION_NAME']?.trim() ?? 'unknown';
|
|
152
|
+
const version = process.env['OPENWOP_IMPLEMENTATION_VERSION']?.trim() ?? 'unknown';
|
|
153
|
+
return `[${impl}@${version}] ${specSection}: ${requirement}`;
|
|
104
154
|
}
|
|
105
155
|
|
|
106
156
|
/** setup.ts reads and clears the override after each test. */
|
|
@@ -61,6 +61,11 @@ export interface LedgerEntry {
|
|
|
61
61
|
* program exists to make visible; the runner treats it as unclassified for
|
|
62
62
|
* a claimed profile's floor. */
|
|
63
63
|
readonly assertionCount?: number;
|
|
64
|
+
/** The scenario file that recorded it. An explicit `req()` id carries no file
|
|
65
|
+
* in its own text, so without this the `--certify` runner cannot attribute
|
|
66
|
+
* the row to a file and drops it — which is how every explicit requirement id
|
|
67
|
+
* went missing from bundle v3 while the per-`it` ids came through. */
|
|
68
|
+
readonly scenarioFile?: string;
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
const ledger = new Map<string, LedgerEntry>();
|
|
@@ -82,7 +87,7 @@ export function recordRequirement(
|
|
|
82
87
|
requirementId: string,
|
|
83
88
|
disposition: Disposition,
|
|
84
89
|
detail?: string,
|
|
85
|
-
extras?: { assertionCount?: number },
|
|
90
|
+
extras?: { assertionCount?: number; scenarioFile?: string },
|
|
86
91
|
): void {
|
|
87
92
|
const prior = ledger.get(requirementId);
|
|
88
93
|
if (prior !== undefined && prior.disposition !== disposition) {
|
|
@@ -102,6 +107,7 @@ export function recordRequirement(
|
|
|
102
107
|
disposition,
|
|
103
108
|
...(detail === undefined ? {} : { detail }),
|
|
104
109
|
...(extras?.assertionCount === undefined ? {} : { assertionCount: extras.assertionCount }),
|
|
110
|
+
...(extras?.scenarioFile === undefined ? {} : { scenarioFile: extras.scenarioFile }),
|
|
105
111
|
};
|
|
106
112
|
ledger.set(requirementId, entry);
|
|
107
113
|
journal.push(entry);
|
|
@@ -124,6 +124,7 @@ export function resolveFileRecord(
|
|
|
124
124
|
// them. See lib/spec-coherence.ts for why not a new disposition value.
|
|
125
125
|
if (
|
|
126
126
|
specCoherenceFile !== undefined
|
|
127
|
+
&& process.env.OPENWOP_CORPUS_GATE !== '1' // suite 2.0.0: under the corpus gate the coherence scenario IS the subject
|
|
127
128
|
&& SPEC_COHERENCE_SCENARIOS.has(specCoherenceFile)
|
|
128
129
|
&& !states.includes('fail')
|
|
129
130
|
&& assertionCount === 0
|
|
@@ -292,7 +293,11 @@ export function deriveRequirementDispositions(
|
|
|
292
293
|
// them. This is the granularity RFC 0148 §A describes and the G8 fix.
|
|
293
294
|
const emitted = new Set(rows.map((r) => r.requirementId));
|
|
294
295
|
for (const e of [...ledger].sort((a, b) => a.requirementId.localeCompare(b.requirementId))) {
|
|
295
|
-
|
|
296
|
+
// Prefer the file the entry recorded: an explicit `req()` id
|
|
297
|
+
// (`openwop.requirement.…`) is authored, not file-derived, so deriving a
|
|
298
|
+
// file from the id alone returned null and the row was dropped — every
|
|
299
|
+
// explicit requirement id was missing from bundle v3 for that reason.
|
|
300
|
+
const file = e.scenarioFile ?? scenarioFileOfItId(e.requirementId);
|
|
296
301
|
// Attribute only to files this run reported on: a worker's ledger can carry
|
|
297
302
|
// rows from files outside the certified set (the suite's own lib tests, or
|
|
298
303
|
// a filtered run), and those are not evidence about the host.
|