@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
package/README.md
CHANGED
|
@@ -16,6 +16,15 @@ The suite is intentionally self-contained — it does NOT depend on the referenc
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
|
|
20
|
+
## Suite 2.0.0 (v2 charter Phase 3; RFC 0168) — what changed for hosts
|
|
21
|
+
|
|
22
|
+
- **The contract is a peer package.** `@openwop/openwop-conformance@2.x` no longer vendors `api/` and `schemas/`; it declares `@openwop/spec-artifacts` as an exact-pinned peer dependency (install both), resolves the installed peer through Node's resolver, and refuses to start when the peer's version or `CORPUS-STAMP.json` digest differs from `dist/spec-artifacts.lock.json` (RFC 0168 §D.2). `schemas/CORPUS-STAMP.json` in this package is a copy of the peer's stamp, kept at the path hosts already read for provenance.
|
|
23
|
+
- **`--target-major 1|2`** (RFC 0168 §D.3): one package runs the scenarios for either protocol major; `scenario-majors.json` names each file's majors; the default comes from the host's root `preferredVersion` (RFC 0179), else `max(protocolVersions[])`, else 1.
|
|
24
|
+
- **`req()` is the only assertion form** (RFC 0168 §A.1): every assertion carries a requirement id; `requirements.json` lists every id; `requirement-aliases.json` maps a reworded id to its successor. `scripts/check-req-only.mjs` is the gate.
|
|
25
|
+
- **Corpus-coherence scenarios never run against a host.** They live in `src/coherence/`, run in the spec repo's CI (`scripts/check-spec-coherence.mjs`) and produce `evidence/corpus-ledger.json`; a host bundle never contains them. The `--offline` set is `fixtures-valid` only.
|
|
26
|
+
- **1.x continues on `release/1.x`** (`openwop-conformance/v1.16x.y` tags); pre-release 2.x versions publish under the npm dist-tag `next`.
|
|
27
|
+
|
|
19
28
|
## Quickstart
|
|
20
29
|
|
|
21
30
|
Two ways to run: the friendly `openwop-conformance` CLI (recommended for
|
|
@@ -103,7 +112,7 @@ Exit code is non-zero on any failed assertion. `--certify` distinguishes: `0`
|
|
|
103
112
|
|
|
104
113
|
## What's Covered
|
|
105
114
|
|
|
106
|
-
The current suite has
|
|
115
|
+
The current suite has 494 scenario files under `src/scenarios/`.
|
|
107
116
|
- 2026-09-03 (suite `1.157.0 -> 1.158.0`, gap G17): NEW `idempotency-concurrent-claim.test.ts` — drives the new `host-sample-test-seams.md` §25 concurrent duplicate-delivery seam for the RFC 0150 §B / `idempotency.md` §"Concurrent duplicates (Layer 2)" atomic-claim MUST, which is unconditional and had no witness of any kind. Asserts every executor mints the SAME `logicalInvocationId` **before** asserting `delivered === 1` — without the identity check a host passes by minting different ids and never colliding, one effect because nothing raced. Not profile-gated and so not opt-out-able (the obligation is unconditional); an unmounted seam records `blocked`, which is not certifiable. Graduates `layer2-invocation-claim-atomic` reference-impl -> protocol.
|
|
108
117
|
- 2026-08-19 (suite `1.137.0 → 1.138.0`): NEW `durability-poison-exhaustion.test.ts` — RFC 0158 §C.8, the FIRST row of that RFC's conformance table to land. Asserts what `failure-path.test.ts` cannot: not just that deterministically failing work reaches terminal, but that attempts STOP — counted on the log, re-counted after a scaled quiet window, asserted unchanged. A host still redelivering records more. Seam-gated on the existing event-log seam (`blocked` = unobservable, not unmet) and outside every profile floor.
|
|
109
118
|
- 2026-08-19 (suite `1.136.15 → 1.137.0`): NEW `replay-fanout-suppression.test.ts` — capability-gated on `webhooks.supported`, **outside every profile floor**; witnesses `replay.md` §"Host-initiated fan-out is an external effect", which was the largest normative MUST NOT on the replay surface with no scenario and no SECURITY invariant. Three legs in ONE `it` against ONE receiver and ONE subscription — a positive control, the MUST NOT, and a `branch` boundary leg — because "no delivery arrived" passes identically when delivery never worked, so absence is asserted only after presence is proven on that exact wiring. A host with an SSRF guard correctly refuses the loopback receiver and records `blocked`: **unobservable, not unmet.**
|
|
@@ -448,7 +457,7 @@ Server-required (added in 1.7.0):
|
|
|
448
457
|
| ------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
449
458
|
| **Redaction** | [`capabilities.md`](../spec/v1/capabilities.md) §"Secrets" + NFR-7 + §"aiProviders" | Vendor-neutral assertions that the server doesn't leak secret material. Three scenario groups: (a) discovery shape contract — `secrets` + `aiProviders` advertisements are well-formed regardless of `secrets.supported`; when `supported === true`, scopes MUST be non-empty + `resolution === 'host-managed'`; `byok ⊆ supported`. (b) bearer-token redaction — invalid Bearer canary in `Authorization` header is not echoed in the 401 response body. (c) credentialRef echo control — gated on `secrets.supported === true`; canary planted in `configurable.ai.credentialRef` MUST NOT appear in any RunEvent payload (poll-based capture; transport-agnostic). Uses runtime-built canary fixtures (`lib/canaries.ts`) that defeat static secret scanners. 6 scenarios. |
|
|
450
459
|
|
|
451
|
-
Current source tree:
|
|
460
|
+
Current source tree: 494 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
|
|
452
461
|
|
|
453
462
|
## Remaining Gaps
|
|
454
463
|
|
package/dist/cli.js
CHANGED
|
@@ -34,8 +34,9 @@
|
|
|
34
34
|
import { spawnSync } from 'node:child_process';
|
|
35
35
|
import { fileURLToPath } from 'node:url';
|
|
36
36
|
import { dirname, resolve as resolvePath, join } from 'node:path';
|
|
37
|
+
import { createRequire } from 'node:module';
|
|
37
38
|
import { createHash } from 'node:crypto';
|
|
38
|
-
import { readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs';
|
|
39
|
+
import { existsSync, readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs';
|
|
39
40
|
import { tmpdir } from 'node:os';
|
|
40
41
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
41
42
|
import addFormats from 'ajv-formats';
|
|
@@ -43,7 +44,8 @@ import { SCHEMAS_DIR } from './lib/paths.js';
|
|
|
43
44
|
import { readLedgerFile } from './lib/requirement-ledger.js';
|
|
44
45
|
import { deriveRequirementDispositions } from './lib/scenario-disposition.js';
|
|
45
46
|
import { scrubEvidence, evidenceSecretsFromEnv, verifyBundleV2 } from './lib/certification-bundle-verify.js';
|
|
46
|
-
import {
|
|
47
|
+
import { publicKeyFromPrivate, signBundleV3, verifierSign, verifyBundleV3, witnessDigest } from './lib/certification-bundle-v3.js';
|
|
48
|
+
import { deriveProfiles, isCoreStandard, agentPlatformStatus, DEPRECATED_PROFILE_ALIASES, PROFILE_FLOOR_SCENARIOS, } from './lib/profiles.js';
|
|
47
49
|
function parseArgs(argv) {
|
|
48
50
|
let baseUrl;
|
|
49
51
|
let apiKey;
|
|
@@ -56,7 +58,11 @@ function parseArgs(argv) {
|
|
|
56
58
|
// Suite 1.152.0: bundle v2 (RFC 0148) is the default. v1 stays reachable via
|
|
57
59
|
// `--bundle-version 1` through the RFC 0148 migration window (ends
|
|
58
60
|
// 2026-11-10) and is removed at v2.0 (spec/v1/deprecations.json).
|
|
59
|
-
let bundleVersion = '
|
|
61
|
+
let bundleVersion = '3';
|
|
62
|
+
let hostBuild;
|
|
63
|
+
let evidenceTier = 'self';
|
|
64
|
+
let signingKeyPath, signingKeyId, verifierKeyPath, verifierKeyId;
|
|
65
|
+
let targetMajor;
|
|
60
66
|
let maxWorkers = parseMaxWorkers(process.env.OPENWOP_MAX_WORKERS, 'OPENWOP_MAX_WORKERS');
|
|
61
67
|
for (let i = 0; i < argv.length; i++) {
|
|
62
68
|
const arg = argv[i] ?? '';
|
|
@@ -88,6 +94,15 @@ function parseArgs(argv) {
|
|
|
88
94
|
case '--api-key':
|
|
89
95
|
apiKey = nextValue();
|
|
90
96
|
break;
|
|
97
|
+
case '--target-major': {
|
|
98
|
+
const v = argv[++i];
|
|
99
|
+
if (v !== '1' && v !== '2') {
|
|
100
|
+
process.stderr.write(`openwop-conformance: --target-major must be 1 or 2 (got ${String(v)})\n`);
|
|
101
|
+
process.exit(2);
|
|
102
|
+
}
|
|
103
|
+
targetMajor = v === '2' ? 2 : 1;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
91
106
|
case '--filter':
|
|
92
107
|
filter = nextValue();
|
|
93
108
|
break;
|
|
@@ -100,17 +115,47 @@ function parseArgs(argv) {
|
|
|
100
115
|
implVersion = nextValue();
|
|
101
116
|
break;
|
|
102
117
|
case '--bundle-version': {
|
|
103
|
-
const v =
|
|
104
|
-
if (v !== '
|
|
105
|
-
process.stderr.write(
|
|
118
|
+
const v = argv[++i];
|
|
119
|
+
if (v !== '2' && v !== '3') {
|
|
120
|
+
process.stderr.write(`openwop-conformance: --bundle-version must be 2 or 3 (got ${String(v)}); v1 is gone in suite 2.0.0 (RFC 0168 §E.3)\n`);
|
|
106
121
|
process.exit(2);
|
|
107
122
|
}
|
|
123
|
+
if (v === '2')
|
|
124
|
+
process.stderr.write('openwop-conformance: --bundle-version 2 is DEPRECATED — v2 bundles stop substantiating a certification at v1 end-of-support (RFC 0168 §E.3); the default is 3.\n');
|
|
108
125
|
bundleVersion = v;
|
|
109
|
-
|
|
110
|
-
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case '--host-build': {
|
|
129
|
+
const v = argv[++i] ?? '';
|
|
130
|
+
const mm = /^(image-digest|commit|artifact-sha256):(.+)$/.exec(v);
|
|
131
|
+
if (!mm) {
|
|
132
|
+
process.stderr.write(`openwop-conformance: --host-build must be <image-digest|commit|artifact-sha256>:<id> (got ${JSON.stringify(v)})\n`);
|
|
133
|
+
process.exit(2);
|
|
111
134
|
}
|
|
135
|
+
hostBuild = { kind: mm[1], id: mm[2] };
|
|
112
136
|
break;
|
|
113
137
|
}
|
|
138
|
+
case '--evidence-tier': {
|
|
139
|
+
const v = argv[++i];
|
|
140
|
+
if (v !== 'self' && v !== 'steward' && v !== 'independent') {
|
|
141
|
+
process.stderr.write('openwop-conformance: --evidence-tier must be self, steward or independent\n');
|
|
142
|
+
process.exit(2);
|
|
143
|
+
}
|
|
144
|
+
evidenceTier = v;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case '--signing-key':
|
|
148
|
+
signingKeyPath = argv[++i];
|
|
149
|
+
break;
|
|
150
|
+
case '--signing-key-id':
|
|
151
|
+
signingKeyId = argv[++i];
|
|
152
|
+
break;
|
|
153
|
+
case '--verifier-key':
|
|
154
|
+
verifierKeyPath = argv[++i];
|
|
155
|
+
break;
|
|
156
|
+
case '--verifier-key-id':
|
|
157
|
+
verifierKeyId = argv[++i];
|
|
158
|
+
break;
|
|
114
159
|
case '--certify':
|
|
115
160
|
certify = nextValue();
|
|
116
161
|
break;
|
|
@@ -133,6 +178,13 @@ function parseArgs(argv) {
|
|
|
133
178
|
implVersion,
|
|
134
179
|
certify,
|
|
135
180
|
bundleVersion,
|
|
181
|
+
targetMajor,
|
|
182
|
+
hostBuild,
|
|
183
|
+
evidenceTier,
|
|
184
|
+
signingKeyPath,
|
|
185
|
+
signingKeyId,
|
|
186
|
+
verifierKeyPath,
|
|
187
|
+
verifierKeyId,
|
|
136
188
|
maxWorkers,
|
|
137
189
|
};
|
|
138
190
|
}
|
|
@@ -169,7 +221,14 @@ Implementation labels (cosmetic — surface in failure messages):
|
|
|
169
221
|
--impl-version <version> Implementation version (env: OPENWOP_IMPLEMENTATION_VERSION)
|
|
170
222
|
|
|
171
223
|
Certification (RFC 0089):
|
|
172
|
-
--
|
|
224
|
+
--target-major <1|2> Suite 2.0.0 (RFC 0168 §D.3): run the scenarios for this protocol
|
|
225
|
+
major. Default: the host's preferredVersion (RFC 0179), else
|
|
226
|
+
max(protocolVersions[]), else 1. Selection is scenario-majors.json.
|
|
227
|
+
--host-build <k>:<id> v3: the build identity (image-digest|commit|artifact-sha256), or OPENWOP_HOST_BUILD.
|
|
228
|
+
--signing-key <pem> v3: Ed25519 private key (PKCS8 PEM) that signs the bundle, or OPENWOP_BUNDLE_SIGNING_KEY.
|
|
229
|
+
--signing-key-id <id> v3: the keyId the host publishes for that key, or OPENWOP_BUNDLE_SIGNING_KEY_ID.
|
|
230
|
+
--evidence-tier <t> v3: self | steward | independent (independent needs --verifier-key/--verifier-key-id).
|
|
231
|
+
--bundle-version <2|3> Certification bundle format. Default 2 (corrected 2026-09-03, RFC 0168 §E.4; the text said 1 while the code set 2). Version 2 (RFC 0148
|
|
173
232
|
§C) records per-requirement DISPOSITIONS instead of pass/fail/skip
|
|
174
233
|
file lists, so "we could not check" stops being indistinguishable
|
|
175
234
|
from "checked and it holds". See the note it prints.
|
|
@@ -231,6 +290,58 @@ function claimedProfilesFor(doc) {
|
|
|
231
290
|
profiles.push('openwop-agent-platform');
|
|
232
291
|
return profiles;
|
|
233
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* The v2 profile registry is a set of predicates over the DECLARATION
|
|
295
|
+
* (RFC 0169 §C.1): every listed family present as a record, every listed
|
|
296
|
+
* metadata key present. The v1 derivation cannot stand in — `isCore` wants a
|
|
297
|
+
* root `protocolVersion` plus `supportedEnvelopes`/`schemaVersions`/`limits`,
|
|
298
|
+
* shapes a closed v2 root does not have — so a major-2 run claimed NOTHING and
|
|
299
|
+
* no v2 host could ever certify. Falls back to the empty set only when the
|
|
300
|
+
* registry is genuinely absent from the layout, and says so.
|
|
301
|
+
*/
|
|
302
|
+
function claimedProfilesForV2(doc, conformanceRoot) {
|
|
303
|
+
// Resolve the peer the way `lib/paths.ts` does — through Node's resolver from
|
|
304
|
+
// this package — instead of guessing directory shapes. Hand-rolled candidates
|
|
305
|
+
// found the registry in a repo checkout and missed it in every published
|
|
306
|
+
// install, where npm hoists the peer to a SIBLING package dir: the probe that
|
|
307
|
+
// walked one level up landed on the `@openwop/` scope directory, not a
|
|
308
|
+
// package, so a real host run silently claimed nothing.
|
|
309
|
+
const candidates = [];
|
|
310
|
+
try {
|
|
311
|
+
const req = createRequire(resolvePath(conformanceRoot, 'package.json'));
|
|
312
|
+
candidates.push(resolvePath(dirname(req.resolve('@openwop/spec-artifacts/package.json')), 'spec', 'v2', 'profiles.json'));
|
|
313
|
+
}
|
|
314
|
+
catch { /* not installed as a package; the repo-layout candidates below */ }
|
|
315
|
+
candidates.push(resolvePath(conformanceRoot, 'spec', 'v2', 'profiles.json'), resolvePath(conformanceRoot, '..', 'spec', 'v2', 'profiles.json'), resolvePath(conformanceRoot, '..', 'spec-artifacts', 'spec', 'v2', 'profiles.json'));
|
|
316
|
+
const found = candidates.find((c) => existsSync(c));
|
|
317
|
+
if (found === undefined) {
|
|
318
|
+
process.stderr.write('openwop-conformance --certify: spec/v2/profiles.json not found in this layout; claimedProfiles is empty (RFC 0169 §C.1).\n');
|
|
319
|
+
return [];
|
|
320
|
+
}
|
|
321
|
+
let registry;
|
|
322
|
+
try {
|
|
323
|
+
registry = JSON.parse(readFileSync(found, 'utf8'));
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
process.stderr.write(`openwop-conformance --certify: ${found} is unreadable; claimedProfiles is empty.\n`);
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
const root = doc;
|
|
330
|
+
const isRecord = (k) => {
|
|
331
|
+
const v = root[k];
|
|
332
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
333
|
+
};
|
|
334
|
+
const out = [];
|
|
335
|
+
for (const p of registry.profiles ?? []) {
|
|
336
|
+
if (typeof p.id !== 'string')
|
|
337
|
+
continue;
|
|
338
|
+
const families = Array.isArray(p.predicate?.families) ? p.predicate.families.map(String) : [];
|
|
339
|
+
const metadata = Array.isArray(p.predicate?.metadata) ? p.predicate.metadata.map(String) : [];
|
|
340
|
+
if (families.every(isRecord) && metadata.every((k) => root[k] !== undefined))
|
|
341
|
+
out.push(p.id);
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
234
345
|
/**
|
|
235
346
|
* Reduce a vitest JSON report into a per-scenario-file terminal state, keyed by
|
|
236
347
|
* the test-file basename (e.g. `discovery.test.ts`) to align with the basenames
|
|
@@ -272,15 +383,60 @@ function scenarioStatesFromReport(report) {
|
|
|
272
383
|
return states;
|
|
273
384
|
}
|
|
274
385
|
/** Generate + validate + write an RFC 0089 conformance certification bundle. */
|
|
386
|
+
/**
|
|
387
|
+
* Suite 2.0.0 — resolve the target major (RFC 0168 §D.3 / RFC 0179): the flag,
|
|
388
|
+
* else the host's root `preferredVersion`, else max(protocolVersions[]), else 1.
|
|
389
|
+
* Returns the major and the scenario files that target it (scenario-majors.json).
|
|
390
|
+
*/
|
|
391
|
+
async function resolveTargetMajor(args, baseUrl, apiKey, conformanceRoot) {
|
|
392
|
+
let major = args.targetMajor;
|
|
393
|
+
let source = 'flag';
|
|
394
|
+
let dualStack = false;
|
|
395
|
+
if (major === undefined && baseUrl) {
|
|
396
|
+
try {
|
|
397
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, '')}/.well-known/openwop`, { headers: apiKey ? { authorization: `Bearer ${apiKey}` } : {} });
|
|
398
|
+
const doc = (await res.json());
|
|
399
|
+
const pv = doc.preferredVersion ?? (doc.protocolVersions ?? []).map((v) => v).sort((a, b) => Number(b.split('.')[0]) - Number(a.split('.')[0]))[0];
|
|
400
|
+
if ((doc.protocolVersions ?? []).some((v) => v.startsWith('2.')))
|
|
401
|
+
dualStack = true;
|
|
402
|
+
if (pv) {
|
|
403
|
+
major = Number(pv.split('.')[0]) >= 2 ? 2 : 1;
|
|
404
|
+
source = doc.preferredVersion ? 'preferredVersion' : 'max(protocolVersions[])';
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
catch { /* unreachable host: the default below; the run itself will report the failure */ }
|
|
408
|
+
}
|
|
409
|
+
if (major === undefined) {
|
|
410
|
+
major = 1;
|
|
411
|
+
source = 'default';
|
|
412
|
+
}
|
|
413
|
+
// Through the overlap `preferredVersion` names the 1.x member (versioning.md
|
|
414
|
+
// §1.1), so auto-detection on a dual-stack host resolves to major 1 by design.
|
|
415
|
+
// Say so, or an operator reads a v1 run as the host's only option.
|
|
416
|
+
if (major === 1 && dualStack)
|
|
417
|
+
source += ' (the host also serves 2.x — pass --target-major 2 to measure it)';
|
|
418
|
+
const manifest = JSON.parse(readFileSync(resolvePath(conformanceRoot, 'scenario-majors.json'), 'utf8'));
|
|
419
|
+
const files = Object.entries(manifest.majors).filter(([, m]) => m.includes(major)).map(([f]) => `src/scenarios/${f}`);
|
|
420
|
+
return { major, files, source };
|
|
421
|
+
}
|
|
275
422
|
async function runCertify(args, baseUrl, apiKey) {
|
|
276
423
|
const outPath = args.certify;
|
|
277
424
|
if (outPath === undefined)
|
|
278
425
|
process.exit(2);
|
|
279
426
|
// (a) Fetch /.well-known/openwop verbatim + its canonical-JSON SHA-256.
|
|
427
|
+
// The target is resolved FIRST: `/.well-known/openwop` is one resource whose
|
|
428
|
+
// representation the `OpenWOP-Version` header selects, and through the overlap
|
|
429
|
+
// the header-less representation is the v1 document (spec/v2/core/versioning.md
|
|
430
|
+
// §1.1). Fetching it header-less on a major-2 run captured the v1 rendering, so
|
|
431
|
+
// `claimedProfiles` came out as the v1 set and the bundle described a contract
|
|
432
|
+
// the run never measured.
|
|
433
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
434
|
+
const conformanceRoot = resolvePath(here, '..');
|
|
435
|
+
const target = await resolveTargetMajor(args, baseUrl, apiKey, conformanceRoot);
|
|
280
436
|
const discoveryUrl = `${baseUrl.replace(/\/$/, '')}/.well-known/openwop`;
|
|
281
437
|
let document;
|
|
282
438
|
try {
|
|
283
|
-
const resp = await fetch(discoveryUrl, { headers: { Accept: 'application/json' } });
|
|
439
|
+
const resp = await fetch(discoveryUrl, { headers: { Accept: 'application/json', ...(target.major === 2 ? { 'OpenWOP-Version': '2.0' } : {}) } });
|
|
284
440
|
if (!resp.ok) {
|
|
285
441
|
process.stderr.write(`openwop-conformance --certify: GET ${discoveryUrl} returned HTTP ${resp.status}.\n`);
|
|
286
442
|
process.exit(2);
|
|
@@ -293,11 +449,9 @@ async function runCertify(args, baseUrl, apiKey) {
|
|
|
293
449
|
}
|
|
294
450
|
const sha256 = createHash('sha256').update(canonicalJSON(document)).digest('hex');
|
|
295
451
|
// (b) Derive claimedProfiles from the captured document.
|
|
296
|
-
const claimedProfiles = claimedProfilesFor(document);
|
|
452
|
+
const claimedProfiles = target.major === 2 ? claimedProfilesForV2(document, conformanceRoot) : claimedProfilesFor(document);
|
|
297
453
|
// (c) Run the suite, capturing per-scenario terminal state via the vitest
|
|
298
454
|
// JSON reporter. server-targeted scenarios live under src/scenarios/.
|
|
299
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
300
|
-
const conformanceRoot = resolvePath(here, '..');
|
|
301
455
|
const reportDir = mkdtempSync(join(tmpdir(), 'owp-certify-'));
|
|
302
456
|
const reportFile = join(reportDir, 'vitest-report.json');
|
|
303
457
|
// RFC 0148 §A ledger sink (S6): every scenario file records its disposition
|
|
@@ -312,6 +466,8 @@ async function runCertify(args, baseUrl, apiKey) {
|
|
|
312
466
|
env.OPENWOP_IMPLEMENTATION_NAME = args.impl;
|
|
313
467
|
if (args.implVersion)
|
|
314
468
|
env.OPENWOP_IMPLEMENTATION_VERSION = args.implVersion;
|
|
469
|
+
env.OPENWOP_TARGET_MAJOR = String(target.major);
|
|
470
|
+
process.stderr.write(`openwop-conformance --certify: target major ${target.major} (${target.source}); ${target.files.length} scenario file(s)\n`);
|
|
315
471
|
const vitestArgs = [
|
|
316
472
|
'vitest',
|
|
317
473
|
'run',
|
|
@@ -320,6 +476,8 @@ async function runCertify(args, baseUrl, apiKey) {
|
|
|
320
476
|
'--reporter=json',
|
|
321
477
|
`--outputFile=${reportFile}`,
|
|
322
478
|
...maxWorkersArgs(args.maxWorkers),
|
|
479
|
+
...(args.filter ? ['--testNamePattern', args.filter] : []),
|
|
480
|
+
...target.files,
|
|
323
481
|
];
|
|
324
482
|
const runResult = spawnSync('npx', vitestArgs, { cwd: conformanceRoot, env, stdio: 'inherit' });
|
|
325
483
|
if (runResult.error) {
|
|
@@ -413,6 +571,90 @@ async function runCertify(args, baseUrl, apiKey) {
|
|
|
413
571
|
' evidence is inspectable, but it does not certify and this process exits 3.\n' +
|
|
414
572
|
rejectedProfiles.map((v) => ` - ${v.profile}: ${v.unclassified.join(', ')}\n`).join(''));
|
|
415
573
|
}
|
|
574
|
+
if (args.bundleVersion === '3') {
|
|
575
|
+
// ── Bundle v3 (RFC 0168 §E) ─────────────────────────────────────────────
|
|
576
|
+
const build = args.hostBuild ?? (() => {
|
|
577
|
+
const mm = /^(image-digest|commit|artifact-sha256):(.+)$/.exec(process.env['OPENWOP_HOST_BUILD'] ?? '');
|
|
578
|
+
return mm ? { kind: mm[1], id: mm[2] } : undefined;
|
|
579
|
+
})();
|
|
580
|
+
const signingKeyPem = args.signingKeyPath ? readFileSync(args.signingKeyPath, 'utf8') : process.env['OPENWOP_BUNDLE_SIGNING_KEY'];
|
|
581
|
+
const keyId = args.signingKeyId ?? process.env['OPENWOP_BUNDLE_SIGNING_KEY_ID'];
|
|
582
|
+
if (!build || !signingKeyPem || !keyId) {
|
|
583
|
+
process.stderr.write('openwop-conformance --certify: a v3 bundle needs --host-build <kind>:<id> (or OPENWOP_HOST_BUILD), --signing-key <pem> (or OPENWOP_BUNDLE_SIGNING_KEY) and --signing-key-id (or OPENWOP_BUNDLE_SIGNING_KEY_ID) — an unsigned bundle does not exist in v3 (RFC 0168 §E.2).\n');
|
|
584
|
+
process.exit(2);
|
|
585
|
+
}
|
|
586
|
+
const rows3 = derived.requirements.map((r) => ({ id: r.requirementId, scenario: r.scenarioId, result: r.disposition, ...(r.assertionCount === undefined ? {} : { assertions: r.assertionCount }), ...(r.detail === undefined ? {} : { detail: r.detail }) }));
|
|
587
|
+
const totals3 = derived.totals;
|
|
588
|
+
const doc3 = document;
|
|
589
|
+
const protocolVersions = Array.isArray(doc3['protocolVersions']) ? doc3['protocolVersions'] : [String(doc3['protocolVersion'] ?? '')];
|
|
590
|
+
const preferredVersion = typeof doc3['preferredVersion'] === 'string' ? doc3['preferredVersion'] : (protocolVersions[0] ?? '');
|
|
591
|
+
// witnessCount: executed-pass rows on the profile's floor (PROFILE_FLOOR_SCENARIOS required + requiredAnyPrefix).
|
|
592
|
+
const witnessCountFor = (profile) => {
|
|
593
|
+
const floor = PROFILE_FLOOR_SCENARIOS[profile];
|
|
594
|
+
if (!floor)
|
|
595
|
+
return 0;
|
|
596
|
+
const onFloor = (scenario) => floor.required.includes(scenario) || (floor.requiredAnyPrefix ?? []).some((pre) => scenario.startsWith(pre));
|
|
597
|
+
return derived.requirements.filter((r) => r.disposition === 'executed-pass' && onFloor(r.scenarioId)).length;
|
|
598
|
+
};
|
|
599
|
+
const claimed3 = claimedProfiles.filter((p) => !(p in DEPRECATED_PROFILE_ALIASES)).map((p) => ({ id: p, evidenceTier: args.evidenceTier, witnessCount: witnessCountFor(p), certified: !notHeld.has(p) && !rejectedProfiles.some((v) => v.profile === p) && totals3.blocked === 0 }));
|
|
600
|
+
let relaxations;
|
|
601
|
+
if (process.env['OPENWOP_HOST_RELAXATIONS']) {
|
|
602
|
+
try {
|
|
603
|
+
relaxations = JSON.parse(process.env['OPENWOP_HOST_RELAXATIONS']);
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
process.stderr.write('openwop-conformance --certify: OPENWOP_HOST_RELAXATIONS is not JSON\n');
|
|
607
|
+
process.exit(2);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
const lockPath = resolvePath(conformanceRoot, 'dist', 'spec-artifacts.lock.json');
|
|
611
|
+
const lock = existsSync(lockPath) ? JSON.parse(readFileSync(lockPath, 'utf8')) : undefined;
|
|
612
|
+
const nonPass = rows3.filter((r) => r.result !== 'executed-pass');
|
|
613
|
+
const unsigned = {
|
|
614
|
+
bundleVersion: '3',
|
|
615
|
+
generatedAt: new Date().toISOString(),
|
|
616
|
+
suite: { name: '@openwop/openwop-conformance', version, targetMajor: target.major, specArtifactsVersion: lock?.version ?? 'repo-layout', ...(lock ? { stampSha256: lock.stampSha256 } : {}) },
|
|
617
|
+
host: { name: host.name, version: host.version, ...(host.vendor ? { vendor: host.vendor } : {}), build, signingKeyId: keyId, ...(relaxations && relaxations.length ? { relaxations } : {}) },
|
|
618
|
+
discovery: { url: discoveryUrl, sha256, protocolVersions, preferredVersion },
|
|
619
|
+
claimedProfiles: claimed3,
|
|
620
|
+
results: { totals: totals3, requirements: rows3 },
|
|
621
|
+
witnessSha256: witnessDigest(rows3),
|
|
622
|
+
assertionCount: rows3.reduce((n, r) => n + (r.assertions ?? 0), 0),
|
|
623
|
+
...(nonPass.length ? { detail: { nonPass: nonPass.map((r) => ({ id: r.id, result: r.result, reason: r.detail ?? '' })) } } : {}),
|
|
624
|
+
};
|
|
625
|
+
const signature = signBundleV3(unsigned, signingKeyPem, keyId);
|
|
626
|
+
const v3 = { ...unsigned, signature };
|
|
627
|
+
if (args.evidenceTier === 'independent') {
|
|
628
|
+
const vk = args.verifierKeyPath ? readFileSync(args.verifierKeyPath, 'utf8') : process.env['OPENWOP_BUNDLE_VERIFIER_KEY'];
|
|
629
|
+
const vkId = args.verifierKeyId ?? process.env['OPENWOP_BUNDLE_VERIFIER_KEY_ID'];
|
|
630
|
+
if (!vk || !vkId) {
|
|
631
|
+
process.stderr.write('openwop-conformance --certify: --evidence-tier independent needs --verifier-key and --verifier-key-id (RFC 0168 §E.2)\n');
|
|
632
|
+
process.exit(2);
|
|
633
|
+
}
|
|
634
|
+
v3.verifierSignature = verifierSign(unsigned, vk, vkId);
|
|
635
|
+
}
|
|
636
|
+
const secrets3 = evidenceSecretsFromEnv(process.env, [apiKey, signingKeyPem]);
|
|
637
|
+
const scrubbed3 = scrubEvidence(v3, secrets3);
|
|
638
|
+
const v3Out = scrubbed3.value;
|
|
639
|
+
const audit3 = verifyBundleV3(v3Out, { hostPublicKeyPem: publicKeyFromPrivate(signingKeyPem) });
|
|
640
|
+
const emitterDefects = audit3.rejections.filter((r) => !['blocked-certified', 'relaxed-profile-certified', 'independent-unverifiable'].includes(r.kind));
|
|
641
|
+
if (emitterDefects.length > 0) {
|
|
642
|
+
process.stderr.write('openwop-conformance --certify: assembled v3 bundle FAILED self-verification (emitter defect):\n' + emitterDefects.map((r) => ` - [${r.kind}] ${r.detail}`).join('\n') + '\n');
|
|
643
|
+
process.exit(2);
|
|
644
|
+
}
|
|
645
|
+
const v3Schema = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'v2', 'certification-bundle.schema.json'), 'utf8'));
|
|
646
|
+
const v3Ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
647
|
+
addFormats(v3Ajv);
|
|
648
|
+
v3Ajv.addSchema(JSON.parse(readFileSync(join(SCHEMAS_DIR, 'v2', 'ids.schema.json'), 'utf8')));
|
|
649
|
+
const v3Validate = v3Ajv.compile(v3Schema);
|
|
650
|
+
if (!v3Validate(v3Out)) {
|
|
651
|
+
process.stderr.write('openwop-conformance --certify: assembled v3 bundle FAILED schema validation:\n' + `${JSON.stringify(v3Validate.errors, null, 2)}\n`);
|
|
652
|
+
process.exit(2);
|
|
653
|
+
}
|
|
654
|
+
writeFileSync(outPath, `${JSON.stringify(v3Out, null, 2)}\n`);
|
|
655
|
+
process.stdout.write(`openwop-conformance --certify: wrote bundle v3 → ${outPath} (${rows3.length} requirement rows, ${v3Out.assertionCount} assertions, witness ${v3Out.witnessSha256.slice(0, 12)}, signed by ${keyId}; certified: ${audit3.certifiedProfiles.join(', ') || 'none'})\n`);
|
|
656
|
+
process.exit(rejectedProfiles.length > 0 ? 3 : failed.length > 0 ? 1 : 0);
|
|
657
|
+
}
|
|
416
658
|
if (args.bundleVersion === '2') {
|
|
417
659
|
const scenarioIds = [...passed, ...failed, ...skipped].sort();
|
|
418
660
|
const manifestSha = createHash('sha256').update(scenarioIds.join('\n'), 'utf8').digest('hex');
|
|
@@ -420,7 +662,7 @@ async function runCertify(args, baseUrl, apiKey) {
|
|
|
420
662
|
// the discovery it saw. Two runs against differently-configured hosts are
|
|
421
663
|
// different evidence, and this is what says so.
|
|
422
664
|
const configSha = createHash('sha256')
|
|
423
|
-
.update(`${
|
|
665
|
+
.update(`${baseUrl}\n${sha256}\n${process.env['OPENWOP_REQUIRE_BEHAVIOR'] ?? ''}`, 'utf8')
|
|
424
666
|
.digest('hex');
|
|
425
667
|
// Rows come from the ledger (S6). A file that recorded nothing is
|
|
426
668
|
// `blocked` — unclassified — and, if it sits on a claimed floor, rejected above.
|
|
@@ -571,6 +813,12 @@ async function main() {
|
|
|
571
813
|
// ancestor config (e.g., a parent monorepo's vite.config.ts) when
|
|
572
814
|
// the conformance package is used as a workspace member.
|
|
573
815
|
const vitestArgs = ['run', '--config', resolvePath(conformanceRoot, 'vitest.config.ts')];
|
|
816
|
+
const target = await resolveTargetMajor(args, env.OPENWOP_BASE_URL, env.OPENWOP_API_KEY, conformanceRoot);
|
|
817
|
+
env.OPENWOP_TARGET_MAJOR = String(target.major);
|
|
818
|
+
if (!args.offline) {
|
|
819
|
+
process.stderr.write(`openwop-conformance: target major ${target.major} (${target.source}); ${target.files.length} scenario file(s)\n`);
|
|
820
|
+
vitestArgs.push(...target.files);
|
|
821
|
+
}
|
|
574
822
|
if (args.offline) {
|
|
575
823
|
// Suite 1.154.0: the offline set is a DECLARED property of the package —
|
|
576
824
|
// exactly the server-free scenarios that ship in the tarball and run in
|
|
@@ -0,0 +1,142 @@
|
|
|
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 } from 'node:crypto';
|
|
21
|
+
export const SIGNATURE_OVER = ['witnessSha256', 'host.build', 'suite.version', 'discovery.sha256'];
|
|
22
|
+
/** Deterministic JSON: keys sorted at every level, no whitespace. */
|
|
23
|
+
export function canonicalJSON(value) {
|
|
24
|
+
if (Array.isArray(value))
|
|
25
|
+
return `[${value.map(canonicalJSON).join(',')}]`;
|
|
26
|
+
if (value !== null && typeof value === 'object') {
|
|
27
|
+
return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${canonicalJSON(value[k])}`).join(',')}}`;
|
|
28
|
+
}
|
|
29
|
+
return JSON.stringify(value);
|
|
30
|
+
}
|
|
31
|
+
/** RFC 0148 §C — the digest over the reporter record (the requirement rows). */
|
|
32
|
+
export function witnessDigest(rows) {
|
|
33
|
+
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 }) }));
|
|
34
|
+
return createHash('sha256').update(canonicalJSON(canonicalRows), 'utf8').digest('hex');
|
|
35
|
+
}
|
|
36
|
+
/** The bytes the attestation covers. */
|
|
37
|
+
export function attestationPayload(b) {
|
|
38
|
+
return Buffer.from(canonicalJSON({ witnessSha256: b.witnessSha256, 'host.build': b.host.build, 'suite.version': b.suite.version, 'discovery.sha256': b.discovery.sha256 }), 'utf8');
|
|
39
|
+
}
|
|
40
|
+
function toBase64url(buf) { return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); }
|
|
41
|
+
function fromBase64url(s) { return Buffer.from(s.replace(/-/g, '+').replace(/_/g, '/'), 'base64'); }
|
|
42
|
+
export function signBundleV3(bundle, privateKeyPem, keyId) {
|
|
43
|
+
const key = createPrivateKey(privateKeyPem);
|
|
44
|
+
if (key.asymmetricKeyType !== 'ed25519')
|
|
45
|
+
throw new Error(`bundle v3 signing key must be Ed25519, got ${String(key.asymmetricKeyType)}`);
|
|
46
|
+
const sig = edSign(null, attestationPayload(bundle), key);
|
|
47
|
+
return { alg: 'ed25519', keyId, sig: toBase64url(sig), over: [...SIGNATURE_OVER] };
|
|
48
|
+
}
|
|
49
|
+
export function verifierSign(bundle, privateKeyPem, keyId) {
|
|
50
|
+
const key = createPrivateKey(privateKeyPem);
|
|
51
|
+
return { alg: 'ed25519', keyId, sig: toBase64url(edSign(null, attestationPayload(bundle), key)) };
|
|
52
|
+
}
|
|
53
|
+
export function publicKeyFromPrivate(privateKeyPem) {
|
|
54
|
+
return createPublicKey(createPrivateKey(privateKeyPem)).export({ type: 'spki', format: 'pem' });
|
|
55
|
+
}
|
|
56
|
+
export function verifyBundleV3(bundle, opts = {}) {
|
|
57
|
+
const rejections = [];
|
|
58
|
+
if (bundle.bundleVersion !== '3')
|
|
59
|
+
rejections.push({ kind: 'not-v3', detail: `bundleVersion is ${JSON.stringify(bundle.bundleVersion)}, expected "3"` });
|
|
60
|
+
const rows = bundle.results?.requirements ?? [];
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
for (const r of rows) {
|
|
63
|
+
if (seen.has(r.id))
|
|
64
|
+
rejections.push({ kind: 'duplicate-requirement', detail: `${r.id}: more than one row (RFC 0148 §A)` });
|
|
65
|
+
seen.add(r.id);
|
|
66
|
+
if (r.result !== 'executed-pass' && !(r.detail && r.detail.trim()))
|
|
67
|
+
rejections.push({ kind: 'reason-missing', detail: `${r.id}: '${r.result}' recorded without a reason` });
|
|
68
|
+
if (r.result === 'executed-pass' && !(r.assertions && r.assertions > 0))
|
|
69
|
+
rejections.push({ kind: 'vacuous-pass', detail: `${r.id}: executed-pass with no assertions is a witness of nothing (RFC 0148 §C)` });
|
|
70
|
+
}
|
|
71
|
+
const count = (d) => rows.filter((r) => r.result === d).length;
|
|
72
|
+
const expected = { executedPass: count('executed-pass'), executedFail: count('executed-fail'), skipped: count('skipped'), inapplicable: count('inapplicable'), blocked: count('blocked') };
|
|
73
|
+
for (const k of Object.keys(expected))
|
|
74
|
+
if (bundle.results?.totals?.[k] !== expected[k])
|
|
75
|
+
rejections.push({ kind: 'totals-mismatch', detail: `totals.${k} is ${String(bundle.results?.totals?.[k])} but the rows count ${expected[k]}` });
|
|
76
|
+
const digest = witnessDigest(rows);
|
|
77
|
+
if (bundle.witnessSha256 !== digest)
|
|
78
|
+
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)})` });
|
|
79
|
+
const assertions = rows.reduce((n, r) => n + (r.assertions ?? 0), 0);
|
|
80
|
+
if (bundle.assertionCount !== assertions)
|
|
81
|
+
rejections.push({ kind: 'assertion-count', detail: `assertionCount is ${String(bundle.assertionCount)} but the rows sum to ${assertions}` });
|
|
82
|
+
const nonPass = rows.filter((r) => r.result !== 'executed-pass');
|
|
83
|
+
if (nonPass.length > 0 && !bundle.detail)
|
|
84
|
+
rejections.push({ kind: 'detail-missing', detail: `${nonPass.length} non-pass row(s) and no detail block` });
|
|
85
|
+
if (!bundle.host?.build?.kind || !bundle.host?.build?.id)
|
|
86
|
+
rejections.push({ kind: 'build-missing', detail: 'host.build.{kind,id} is required — a bundle attributes to a BUILD, not a commit' });
|
|
87
|
+
// Signature
|
|
88
|
+
let signatureVerified = false;
|
|
89
|
+
const sig = bundle.signature;
|
|
90
|
+
if (!sig || sig.alg !== 'ed25519' || !sig.keyId || !sig.sig)
|
|
91
|
+
rejections.push({ kind: 'signature-missing', detail: 'signature.{alg: ed25519, keyId, sig} is required in v3' });
|
|
92
|
+
else if (JSON.stringify(sig.over) !== JSON.stringify(SIGNATURE_OVER))
|
|
93
|
+
rejections.push({ kind: 'signature-over', detail: `signature.over must be ${JSON.stringify(SIGNATURE_OVER)}` });
|
|
94
|
+
else if (opts.hostPublicKeyPem) {
|
|
95
|
+
const key = createPublicKey(opts.hostPublicKeyPem);
|
|
96
|
+
signatureVerified = edVerify(null, attestationPayload(bundle), key, fromBase64url(sig.sig));
|
|
97
|
+
if (!signatureVerified)
|
|
98
|
+
rejections.push({ kind: 'signature-invalid', detail: 'the attestation does not verify under the host key' });
|
|
99
|
+
}
|
|
100
|
+
// Independent tier
|
|
101
|
+
let verifierSignatureVerified = false;
|
|
102
|
+
const claimsIndependent = (bundle.claimedProfiles ?? []).some((p) => p.evidenceTier === 'independent');
|
|
103
|
+
if (claimsIndependent) {
|
|
104
|
+
const vs = bundle.verifierSignature;
|
|
105
|
+
if (!vs || !vs.keyId || !vs.sig)
|
|
106
|
+
rejections.push({ kind: 'independent-unsigned', detail: 'evidenceTier independent requires a verifierSignature (RFC 0168 §E.2)' });
|
|
107
|
+
else if (vs.keyId === sig?.keyId || vs.keyId === bundle.host?.signingKeyId)
|
|
108
|
+
rejections.push({ kind: 'independent-self-signed', detail: 'the verifier key must be distinct from the host key (RFC 0148 R5)' });
|
|
109
|
+
else if (opts.verifierPublicKeyPem) {
|
|
110
|
+
verifierSignatureVerified = edVerify(null, attestationPayload(bundle), createPublicKey(opts.verifierPublicKeyPem), fromBase64url(vs.sig));
|
|
111
|
+
if (!verifierSignatureVerified)
|
|
112
|
+
rejections.push({ kind: 'verifier-signature-invalid', detail: 'the verifier attestation does not verify' });
|
|
113
|
+
}
|
|
114
|
+
else
|
|
115
|
+
rejections.push({ kind: 'independent-unverifiable', detail: 'an independent claim needs the verifier public key to verify; refused, not assumed' });
|
|
116
|
+
}
|
|
117
|
+
// Relaxations: a relaxed obligation's profile cannot certify (RFC 0173 §A.2).
|
|
118
|
+
const relaxed = new Set((bundle.host?.relaxations ?? []).map((r) => r.obligation.split('.')[0]));
|
|
119
|
+
const certifiedProfiles = [];
|
|
120
|
+
for (const p of bundle.claimedProfiles ?? []) {
|
|
121
|
+
if (p.certified && relaxed.size > 0 && [...relaxed].some((o) => p.id.includes(o)))
|
|
122
|
+
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)` });
|
|
123
|
+
else if (p.certified)
|
|
124
|
+
certifiedProfiles.push(p.id);
|
|
125
|
+
}
|
|
126
|
+
if (expected.blocked > 0 && certifiedProfiles.length > 0)
|
|
127
|
+
rejections.push({ kind: 'blocked-certified', detail: `${expected.blocked} blocked row(s): a bundle with blocked > 0 does not certify (RFC 0168 §E.1)` });
|
|
128
|
+
// RFC 0168 §E.2: the verifier REFUSES, it does not warn — but a refusal is
|
|
129
|
+
// scoped to what it names. A rejection carrying a `profile` removes that
|
|
130
|
+
// profile only (a relaxation on one obligation does not poison an unrelated
|
|
131
|
+
// one, RFC 0173 §A.2); a rejection that names no profile is a statement about
|
|
132
|
+
// the bundle — a bad witness digest, an unverifiable signature, a self-signed
|
|
133
|
+
// `independent` claim, a blocked row — and nothing in it certifies.
|
|
134
|
+
const bundleWide = rejections.filter((r) => !('profile' in r) || !r.profile);
|
|
135
|
+
const scoped = new Set(rejections.map((r) => r.profile).filter((p) => typeof p === 'string'));
|
|
136
|
+
return {
|
|
137
|
+
rejections,
|
|
138
|
+
signatureVerified,
|
|
139
|
+
verifierSignatureVerified,
|
|
140
|
+
certifiedProfiles: bundleWide.length > 0 ? [] : certifiedProfiles.filter((p) => !scoped.has(p)),
|
|
141
|
+
};
|
|
142
|
+
}
|
package/dist/lib/paths.js
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
import { existsSync } from 'node:fs';
|
|
37
37
|
import { fileURLToPath } from 'node:url';
|
|
38
|
+
import { createRequire } from 'node:module';
|
|
38
39
|
import { dirname, join, resolve as pathResolve } from 'node:path';
|
|
39
40
|
// `dirname(fileURLToPath(import.meta.url))` for an ESM module compiled or
|
|
40
41
|
// run from `src/lib/paths.ts` returns `<pkg>/src/lib/`. The conformance
|
|
@@ -43,7 +44,22 @@ import { dirname, join, resolve as pathResolve } from 'node:path';
|
|
|
43
44
|
// identical between the two; only the parent of `<pkg>` differs.
|
|
44
45
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
45
46
|
const PKG_ROOT = pathResolve(HERE, '..', '..');
|
|
46
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Suite 2.0.0 (RFC 0168 §D.2): in the published layout the CONTRACT (api/ and
|
|
49
|
+
* schemas/) is the `@openwop/spec-artifacts` peer package, not files vendored
|
|
50
|
+
* into this tarball. Resolve its root through Node's resolver from this package
|
|
51
|
+
* so the host's installed peer is what the suite validates against.
|
|
52
|
+
*/
|
|
53
|
+
function resolvePeerRoot() {
|
|
54
|
+
try {
|
|
55
|
+
const req = createRequire(join(PKG_ROOT, 'package.json'));
|
|
56
|
+
return dirname(req.resolve('@openwop/spec-artifacts/package.json'));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function resolveFromRoot(root, layout, contractRoot = root) {
|
|
47
63
|
// Two on-disk shapes for the layout root:
|
|
48
64
|
// - Repo: <root>/schemas, <root>/api, <root>/conformance/fixtures,
|
|
49
65
|
// <root>/conformance/{fixtures.md,coverage.md}, <root>/spec/v1/*.md
|
|
@@ -52,8 +68,8 @@ function resolveFromRoot(root, layout) {
|
|
|
52
68
|
// <root>/fixtures.md (when bundled), no spec/v1.
|
|
53
69
|
// Probe by checking whether `schemas/` lives at the conformance pkg root
|
|
54
70
|
// (vendored) vs one level up (repo).
|
|
55
|
-
const schemasDir = join(
|
|
56
|
-
const apiDir = join(
|
|
71
|
+
const schemasDir = join(contractRoot, 'schemas');
|
|
72
|
+
const apiDir = join(contractRoot, 'api');
|
|
57
73
|
const repoFixturesDir = join(root, 'conformance', 'fixtures');
|
|
58
74
|
const vendoredFixturesDir = join(root, 'fixtures');
|
|
59
75
|
const fixturesDir = existsSync(repoFixturesDir) ? repoFixturesDir : vendoredFixturesDir;
|
|
@@ -131,7 +147,13 @@ function resolveLayout() {
|
|
|
131
147
|
if (parentHasSchemas) {
|
|
132
148
|
return resolveFromRoot(parent, 'repo');
|
|
133
149
|
}
|
|
150
|
+
const peer = resolvePeerRoot();
|
|
151
|
+
if (peer) {
|
|
152
|
+
return resolveFromRoot(PKG_ROOT, 'published', peer);
|
|
153
|
+
}
|
|
134
154
|
if (pkgHasSchemas) {
|
|
155
|
+
// A pre-2.0 tarball layout (schemas vendored in-package); kept so an old
|
|
156
|
+
// layout still resolves, but the 2.x stamp check refuses to run without the peer.
|
|
135
157
|
return resolveFromRoot(PKG_ROOT, 'published');
|
|
136
158
|
}
|
|
137
159
|
// Neither — return the published-style resolution rooted at PKG_ROOT
|