@openwop/openwop-conformance 2.43.0 → 2.43.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/CHANGELOG.md +16 -0
- package/README.md +3 -3
- package/dist/cli.js +15 -12
- package/dist/lib/certification-bundle-v3.js +19 -0
- package/dist/spec-artifacts.lock.json +2 -2
- package/fixtures/conformance-approval-reject-loopback.json +33 -0
- package/fixtures/conformance-approval-reject-routed.json +32 -0
- package/fixtures/conformance-approval-timeout-approve.json +46 -0
- package/fixtures/conformance-approval-timeout.json +28 -0
- package/fixtures.md +57 -1
- package/package.json +2 -2
- package/requirements.json +193 -30
- package/scenario-majors.json +5 -2
- package/schemas/CORPUS-STAMP.json +44 -44
- package/src/cli.ts +15 -12
- package/src/lib/certification-bundle-v3.ts +25 -0
- package/src/lib/standard-webhooks.ts +53 -0
- package/src/scenarios/agent-org-chart-scoping.test.ts +63 -0
- package/src/scenarios/v2-agent-org-chart-served-shape.test.ts +63 -0
- package/src/scenarios/v2-approval-reject-disposition.test.ts +106 -6
- package/src/scenarios/v2-error-registry.test.ts +59 -0
- package/src/scenarios/v2-manifest-ceiling-refused.test.ts +21 -9
- package/src/scenarios/v2-manifest-hatch-carried.test.ts +23 -7
- package/src/scenarios/v2-pack-isolation.test.ts +10 -6
- package/src/scenarios/v2-peer-dependency-declared.test.ts +18 -6
- package/src/scenarios/v2-webhook-secret-rotation.test.ts +28 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# `@openwop/openwop-conformance` Changelog
|
|
2
2
|
|
|
3
|
+
## [2.43.1] — 2026-09-28 — major-1 certification works again, RFC 0223's routed and timed-out rejects are witnessed, and four Class 3 corrections
|
|
4
|
+
|
|
5
|
+
- **`v2-manifest-hatch-carried`'s agent fixture is schema-valid.** Its `agents[0]` had neither `systemPrompt` nor `systemPromptRef`, and `agent-manifest` (v1 and v2) requires exactly one. A conforming host therefore had to refuse the pack for a reason unrelated to the `x-` hatch, and `0177.manifest-hatch-carried.agents-x-field` could not pass on any host. The fixture now carries `systemPrompt`. Validated against `schemas/v2/agent-manifest.schema.json`: invalid before, valid after. Reported by MyndHyve.
|
|
6
|
+
- **RFC 0223 G1 and G2: the routed and the timed-out approval reject are witnessed** (#1700). Two new fixtures and two new rows in `v2-approval-reject-disposition.test.ts` (target major 2). Each row is `inapplicable` on a host that does not advertise its fixture.
|
|
7
|
+
- `conformance-approval-reject-routed` and `0223.reject-routed`: the gate has an `any_failed` edge to `on-reject` and an `all_success` edge to `on-accept`. Rejected, the gate still fails as a node (`approval_rejected`, `retryable: false`), `on-reject` completes, `on-accept` never runs, and the run completes. `node.skipped` is not required, because `interrupt.md` §Rejection says the edge is not satisfied, not how a host records it.
|
|
8
|
+
- `conformance-approval-timeout` and `0223.timeout-rejects`: the gate has a 1500 ms `timeoutMs` and no `onTimeout`. Left unresolved, the host resolves it with `action: "timeout"`, `decision: "rejected"`, `reason: "timeout"`, the gate fails with `approval_rejected`, and `run.failed` names it.
|
|
9
|
+
- Witnessed on the v2 reference host (openwop-examples `777b485`) with both fixtures added to its honoured set: all six rows `executed-pass`. Sabotaged, emptying the host's failure-admitting `triggerRule` set fails `0223.reject-routed`, and short-circuiting its timeout sweep fails `0223.timeout-rejects`. The host registers the two ids in a follow-up.
|
|
10
|
+
- **The pack isolation and manifest rows no longer gate on `packs`** (#1706, Class 3). `packs` advertises registry resolution, which a host that only loads local packs correctly omits, so these rows went unmeasured on exactly that host. `v2-pack-isolation` gates on `sandbox` alone. `v2-manifest-ceiling-refused`, `v2-manifest-hatch-carried` and `v2-peer-dependency-declared` gate on the seams-profile install path, and a publish-seam `404` records `inapplicable` when `packs` is absent and `blocked` when it is advertised. Requirement ids are unchanged.
|
|
11
|
+
- **A timeout never grants an approval gate** (#1696, Class 3). New fixture `conformance-approval-timeout-approve` (1500 ms `timeoutMs`, `onTimeout: "approve"`) and row `0223.timeout-never-grants` in `v2-approval-reject-disposition` (`inapplicable` without the fixture). The timed-out gate MUST resolve rejected (`action: timeout`, `decision: rejected`), fail with `approval_rejected`, and the run MUST fail rather than complete. Measured on the v2 reference host: `executed-fail` today, because its sweep skips a non-`reject` `onTimeout` and the run waits forever. It passes with that exclusion dropped, which is its follow-up.
|
|
12
|
+
- **An interrupt `key` is per visit** (#1697, Class 3). New fixture `conformance-approval-reject-loopback` (`start` → `gate`, `gate` → `revise` on `any_failed`, `revise` → `gate`) and row `0223.reject-loopback-reasks`. Rejected once, the gate is asked again under a different `key`, the run is `waiting-approval` again, and only one `interrupt.resolved` is recorded. The fixture is advertised only by a host that runs cycles; everyone else records `inapplicable`.
|
|
13
|
+
- **`v2-error-registry` checks the codes on events** (#1698, Class 3). New leg `openwop.requirement.errors.event-code-registered`. It runs `conformance-failure` and checks every `error.code` on `run.failed`, `node.failed` and the snapshot against the registry and the vendor pattern (the org must be registered in `declaration.json`). It is ADVISORY in 2.43.1: a violation records `executed-pass` with a `partial-witness` detail naming the codes, and becomes a failure in a later patch. On the v2 reference host it currently names `fixture_failure`. `fixtures.md`'s `core.fail` now throws `example.conformance_failure`.
|
|
14
|
+
- **RFC 0201 §E.20's post-overlap rule is witnessable on a long overlap.** New OPTIONAL seams-profile seam `POST /conformance/seams/sample/webhooks/rotation-overlap { webhookId, overlapSeconds }` (`host-sample-test-seams.md` §29; `api/seams-v2.yaml` `shortenRotationOverlap`) shortens the rotation overlap in progress on one subscription by moving the stored expiry the production signer reads. It is tenant-checked like `rotateWebhookSecret` and only ever shortens. `v2-webhook-secret-rotation` calls it only when the advertised `overlapSeconds` outlasts the wait cap: it sets 3 s, waits past the reported expiry, and asserts that only the new secret signs. Dispositions: seam served → real assertion; served but contract broken (wrong expiry, or a foreign-tenant id not refused `403 id_tenant_mismatch`) → `blocked`; not served, or no seams profile → the pre-seam partial witness, never `blocked`. Witnessed on the v2 reference host (openwop-examples#117) with a 3600 s overlap: `executed-pass` (17 assertions); seam unmounted → partial witness; a signer that ignores the expiry → `executed-fail`; a seam that misreports the expiry → `blocked`.
|
|
15
|
+
- **`--certify --bundle-version 3` no longer refuses its own bundle when a secret is redacted from a row.** The emitter digested and signed the raw requirement rows, then ran `scrubEvidence` over the finished bundle. A configured secret (the handed `--api-key`, an `OPENWOP_*` key/token/secret env value, or the canary) found in a row's `detail` or `evidence` was redacted AFTER `witnessSha256` was taken, so the written rows no longer hashed to it and self-verification exited 2 with `[witness-digest]` and no bundle. openwop-app hit it at major 1 on 2.43.0. The new `sealBundleV3` scrubs first, then digests and signs, so the signed rows are the shipped rows; the v3 path now also prints the `REDACTED … field(s)` line the v2 path always had. Reproduced on published 2.43.0 against the v1 SQLite reference host at major 1 with an `OPENWOP_*_SECRET` value that occurs in 274 row details: exit 2, `witnessSha256 0657c889365e` (the clean run's digest) against rows digesting to `803b02c17414`. With the fix the same run writes a bundle `--verify` accepts. New self-tests in `certification-bundle-v3.test.ts` pin both orders. Sabotaged, digesting the pre-scrub rows fails the new test. No wire, schema or digest-preimage change.
|
|
16
|
+
- **The org-chart read pair's served bodies are validated against their schemas** (RFC 0087 §A/§D). Only a server-free probe compiled `agent-org-chart.schema.json`; the live scenario walked the tree and member refs and never validated the body. openwop-app served its stored record (`{ tenantId, departments, members, updatedAt }`: `owner` missing, two extra keys on a closed object) at both majors, and every row passed (fixed in openwop-app #4189). New row `openwop.it.agent-org-chart-scoping.serves-the-org-chart-read-pair-in-the-exact-schema-shape` (major 1, same `openwop-org-chart-scoping` gate) and new file `v2-agent-org-chart-served-shape.test.ts` (major 2, `inapplicable` unless the v2 `agents` record carries `orgChart`) validate `GET /agents/org-chart` against `agent-org-chart.schema.json` and, when `responsibilityView` is advertised, `GET /agents/org-chart/{departmentId}` against `org-chart-responsibility-view.schema.json`. Witnessed on openwop-app `c4aad98c2` in-process (one seeded department): `executed-pass` at both majors; the pre-fix `routes/orgChart.ts` (`553355847`) → `executed-fail` at both majors on `owner` + extra keys while the old tree row still passed; an extra key on the department view alone → `executed-fail` at both majors. openwop-app does not advertise `agents` at major 2, so the v2 row records `inapplicable` there; the major-2 witness advertised `agents.orgChart` via a local, uncommitted discovery patch.
|
|
17
|
+
- **Suite `2.43.1`**. This adds scenario rows and fixtures, the same class as the 2.42.8 and 2.42.9 scenario changes. The only packed contract file that changes is `spec/v1/gaps.json`, where G1 and G2 close. No wire change. `@openwop/spec-artifacts` moves in lockstep at the same exact pin.
|
|
18
|
+
|
|
3
19
|
## [2.43.0] — 2026-09-28 — four RFCs: approval reject, registry operations, the audit anomaly shape, and a v2 home for audit-log integrity
|
|
4
20
|
|
|
5
21
|
- **RFC 0222: v2 registry lifecycle and signing keys, witnessed against a registry.** New `v2-registry-lifecycle.test.ts` (target major 2). It reads a registry, not the host: it runs when `OPENWOP_REGISTRY_URL` names one or `OPENWOP_TEST_PUBLIC_REGISTRY=true` names `packs.openwop.dev`, records `inapplicable` otherwise, and resolves every path through `.well-known` `endpoints.v2`. Leg 1 (`openwop.requirement.0222.yanked-version-lifecycle`): every pack index agrees with its version manifests on `yanked` and `versionDeprecated`, never names a yanked version `latest` while an unyanked one exists, and a yanked version's tarball and signature stay served. Leg 2 (`openwop.requirement.0222.signing-keys-cover-served-versions`): every served version names a key `signingKeys[]` still lists, whose `permittedNamespaces` admit it, and its integrity and signature over the in-tarball `pack.json` verify, whatever the key's `status`. Passes on `packs.openwop.dev` (198 versions; leg 1 `partial-witness`, no v2 version is yanked) and on a local tree with a yanked version; fails on seven sabotages, including the registry's previous `build-index.mjs`.
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# --legacy-peer-deps is REQUIRED, not optional: the exact peer pin is what npm's
|
|
12
12
|
# default resolver refuses. npm 10.9 fails outright with
|
|
13
13
|
# "Cannot read properties of null (reading 'edgesOut')" — use npm >= 11.
|
|
14
|
-
npm install --legacy-peer-deps @openwop/openwop-conformance@2.43.
|
|
14
|
+
npm install --legacy-peer-deps @openwop/openwop-conformance@2.43.1 @openwop/spec-artifacts@2.43.1
|
|
15
15
|
# or run without install:
|
|
16
16
|
npx @openwop/openwop-conformance --base-url https://api.example.com --api-key hk_test_...
|
|
17
17
|
```
|
|
@@ -135,7 +135,7 @@ Exit code is non-zero on any failed assertion. `--certify` distinguishes: `0`
|
|
|
135
135
|
|
|
136
136
|
## What's Covered
|
|
137
137
|
|
|
138
|
-
The current suite has
|
|
138
|
+
The current suite has 571 scenario files under `src/scenarios/`.
|
|
139
139
|
- 2026-09-23 (suite 2.37.0 cycle, RFC 0213): NEW `v2-sse-last-event-id-cursor.test.ts` (a `Last-Event-ID` past the log is an exclusive cursor; a malformed id, when refused, is `400 validation_error`; the cursor never changes the answer for an unknown or foreign-tenant run — public test of `event-cursor-after-authorization`), `v2-idempotency-in-flight.test.ts` (five concurrent same-key creates yield one run; each loser is a marked replay or `409 idempotency_in_flight` with no retry timing in `details`; `partial-witness` when no loser was refused in flight) and `v2-interrupt-resolve-terminal.test.ts` (a run-scoped resolve after cancel or completion is `409 interrupt_already_resolved`, never `interrupt_cancelled`). All three sit off the core-standard floor until measured on the three bundle hosts.
|
|
140
140
|
- 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.
|
|
141
141
|
- 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.
|
|
@@ -481,7 +481,7 @@ Server-required (added in 1.7.0):
|
|
|
481
481
|
| ------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
482
482
|
| **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. |
|
|
483
483
|
|
|
484
|
-
Current source tree:
|
|
484
|
+
Current source tree: 571 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
|
|
485
485
|
|
|
486
486
|
## Remaining Gaps
|
|
487
487
|
|
package/dist/cli.js
CHANGED
|
@@ -45,7 +45,7 @@ import { readLedgerFile } from './lib/requirement-ledger.js';
|
|
|
45
45
|
import { deriveRung, emittedByNewerSuite } from './lib/durability-evidence.js';
|
|
46
46
|
import { deriveRequirementDispositions } from './lib/scenario-disposition.js';
|
|
47
47
|
import { scrubEvidence, evidenceSecretsFromEnv, verifyBundleV2 } from './lib/certification-bundle-verify.js';
|
|
48
|
-
import { publicKeyFromPrivate,
|
|
48
|
+
import { publicKeyFromPrivate, sealBundleV3, verifierSign, verifyBundleV3 } from './lib/certification-bundle-v3.js';
|
|
49
49
|
import { canonicalJSON, parseIJson } from './lib/jcs.js';
|
|
50
50
|
import { deriveProfiles, isCoreStandard, agentPlatformStatus, DEPRECATED_PROFILE_ALIASES, PROFILE_FLOOR_SCENARIOS, } from './lib/profiles.js';
|
|
51
51
|
import { setV2ProfileFloors, v2ProfileFloorFiles } from './lib/requirement-registry.js';
|
|
@@ -687,7 +687,7 @@ async function runCertify(args, baseUrl, apiKey) {
|
|
|
687
687
|
if (rows3.some((r) => r.id.startsWith('openwop.requirement.0158.') && r.result === 'executed-pass' && r.id !== 'openwop.requirement.0158.poison-exhaustion')) {
|
|
688
688
|
process.stderr.write(`openwop-conformance --certify: RFC 0158 rung — ${rung3.rung ?? 'NONE'} (${rung3.why})\n`);
|
|
689
689
|
}
|
|
690
|
-
const
|
|
690
|
+
const draft = {
|
|
691
691
|
bundleVersion: '3',
|
|
692
692
|
generatedAt: new Date().toISOString(),
|
|
693
693
|
suite: { name: '@openwop/openwop-conformance', version, targetMajor: target.major, specArtifactsVersion: lock?.version ?? 'repo-layout', ...(lock ? { stampSha256: lock.stampSha256 } : {}) },
|
|
@@ -699,7 +699,6 @@ async function runCertify(args, baseUrl, apiKey) {
|
|
|
699
699
|
discovery: { url: discoveryUrl, sha256, protocolVersions, preferredVersion, document },
|
|
700
700
|
claimedProfiles: claimed3,
|
|
701
701
|
results: { totals: totals3, requirements: rows3 },
|
|
702
|
-
witnessSha256: witnessDigest(rows3, relaxations, args.colocatedCompanion ? 'colocated-companion' : undefined),
|
|
703
702
|
assertionCount: rows3.reduce((n, r) => n + (r.assertions ?? 0), 0),
|
|
704
703
|
...(nonPass.length ? { detail: { nonPass: nonPass.map((r) => ({ id: r.id, result: r.result, reason: r.detail ?? '' })) } } : {}),
|
|
705
704
|
// RFC 0158 §D: claimed ONLY when these rows support it. The verifier
|
|
@@ -707,8 +706,18 @@ async function runCertify(args, baseUrl, apiKey) {
|
|
|
707
706
|
// claimed more would be writing a bundle its own `--verify` rejects.
|
|
708
707
|
...(rung3.rung === null ? {} : { durability: { rung: rung3.rung } }),
|
|
709
708
|
};
|
|
710
|
-
|
|
711
|
-
|
|
709
|
+
// The published keyId is never a secret, whichever variable carried it
|
|
710
|
+
// (evidenceSecretsFromEnv: the `_ID` exclusion is the rule, `except` is
|
|
711
|
+
// the belt for a host that names its env var differently). Scrubbed BEFORE
|
|
712
|
+
// the digest and the signature (sealBundleV3), so the signed rows are the
|
|
713
|
+
// written rows.
|
|
714
|
+
const secrets3 = evidenceSecretsFromEnv(process.env, [apiKey, signingKeyPem], [keyId]);
|
|
715
|
+
const sealed = sealBundleV3(draft, secrets3, signingKeyPem, keyId);
|
|
716
|
+
const { unsigned } = sealed;
|
|
717
|
+
const v3Out = sealed.bundle;
|
|
718
|
+
if (sealed.redactedAt.length > 0) {
|
|
719
|
+
process.stderr.write(`openwop-conformance --certify: REDACTED ${sealed.redactedAt.length} evidence field(s) that carried a configured secret or the conformance canary: ${sealed.redactedAt.slice(0, 8).join(', ')}${sealed.redactedAt.length > 8 ? ', …' : ''}\n`);
|
|
720
|
+
}
|
|
712
721
|
if (args.evidenceTier === 'independent') {
|
|
713
722
|
const vk = args.verifierKeyPath ? readFileSync(args.verifierKeyPath, 'utf8') : process.env['OPENWOP_BUNDLE_VERIFIER_KEY'];
|
|
714
723
|
const vkId = args.verifierKeyId ?? process.env['OPENWOP_BUNDLE_VERIFIER_KEY_ID'];
|
|
@@ -716,14 +725,8 @@ async function runCertify(args, baseUrl, apiKey) {
|
|
|
716
725
|
process.stderr.write('openwop-conformance --certify: --evidence-tier independent needs --verifier-key and --verifier-key-id (RFC 0168 §E.2)\n');
|
|
717
726
|
process.exit(2);
|
|
718
727
|
}
|
|
719
|
-
|
|
728
|
+
v3Out.verifierSignature = verifierSign(unsigned, vk, vkId);
|
|
720
729
|
}
|
|
721
|
-
// The published keyId is never a secret, whichever variable carried it
|
|
722
|
-
// (evidenceSecretsFromEnv: the `_ID` exclusion is the rule, `except` is
|
|
723
|
-
// the belt for a host that names its env var differently).
|
|
724
|
-
const secrets3 = evidenceSecretsFromEnv(process.env, [apiKey, signingKeyPem], [keyId]);
|
|
725
|
-
const scrubbed3 = scrubEvidence(v3, secrets3);
|
|
726
|
-
const v3Out = scrubbed3.value;
|
|
727
730
|
const audit3 = verifyBundleV3(v3Out, { hostPublicKeyPem: publicKeyFromPrivate(signingKeyPem) });
|
|
728
731
|
// `relaxed-profile-certified` is no longer excused: the emitter now computes
|
|
729
732
|
// the relaxation into `certified`, so if the verifier still finds that
|
|
@@ -22,6 +22,7 @@ import { profileDerivable } from './profiles.js';
|
|
|
22
22
|
import { checkRungClaim } from './durability-evidence.js';
|
|
23
23
|
import { profilesDeniedByObservedRelaxation, profilesRelaxedBy, v2RegistryAvailable } from './v2-profiles.js';
|
|
24
24
|
import { canonicalJSON, codeUnitCompare, JcsRefusal } from './jcs.js';
|
|
25
|
+
import { scrubEvidence } from './certification-bundle-verify.js';
|
|
25
26
|
export const SIGNATURE_OVER = ['witnessSha256', 'host.build', 'suite.version', 'discovery.sha256'];
|
|
26
27
|
/**
|
|
27
28
|
* RFC 0212 §A — the bytes every attestation and digest here covers are RFC 8785
|
|
@@ -82,6 +83,24 @@ export function verifierSign(bundle, privateKeyPem, keyId) {
|
|
|
82
83
|
const key = createPrivateKey(privateKeyPem);
|
|
83
84
|
return { alg: 'ed25519', keyId, sig: toBase64url(edSign(null, attestationPayload(bundle), key)) };
|
|
84
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Scrub, THEN digest, THEN sign (RFC 0148 §C + RFC 0168 §E.2).
|
|
88
|
+
*
|
|
89
|
+
* Until 2.43.1 the emitter computed `witnessSha256` over the raw rows, signed,
|
|
90
|
+
* and only then ran `scrubEvidence` over the finished bundle. A secret found in
|
|
91
|
+
* a row's `detail` or `evidence` was redacted AFTER the digest was taken, so
|
|
92
|
+
* the written rows no longer hashed to `witnessSha256` and the emitter's own
|
|
93
|
+
* self-verification refused the bundle (`[witness-digest]`, exit 2, no bundle)
|
|
94
|
+
* — openwop-app at major 1 on suite 2.43.0, 2026-09-28. The redaction was
|
|
95
|
+
* right and the check was right; the order was wrong. Everything the digest and
|
|
96
|
+
* the attestation cover is now scrubbed before either is computed, so the
|
|
97
|
+
* signed bytes are the shipped bytes.
|
|
98
|
+
*/
|
|
99
|
+
export function sealBundleV3(draft, secrets, privateKeyPem, keyId) {
|
|
100
|
+
const { value: scrubbed, redactedAt } = scrubEvidence(draft, secrets);
|
|
101
|
+
const unsigned = { ...scrubbed, witnessSha256: witnessDigest(scrubbed.results.requirements, scrubbed.host.relaxations, scrubbed.host.deployment) };
|
|
102
|
+
return { unsigned, bundle: { ...unsigned, signature: signBundleV3(unsigned, privateKeyPem, keyId) }, redactedAt };
|
|
103
|
+
}
|
|
85
104
|
export function publicKeyFromPrivate(privateKeyPem) {
|
|
86
105
|
return createPublicKey(createPrivateKey(privateKeyPem)).export({ type: 'spki', format: 'pem' });
|
|
87
106
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "conformance-approval-reject-loopback",
|
|
3
|
+
"name": "Conformance: Approval (reject loops back)",
|
|
4
|
+
"version": "1.0",
|
|
5
|
+
"description": "openwop#1697. A rejected approval gate routed over an any_failed edge to revise, which loops back to the gate (bounded by settings.maxLoopbackIterations 2). Rejected once, the gate is visited again: the host MUST raise a new interrupt.requested with a different key and wait for a new decision, never replaying the first rejection. Conformance scenario: v2-approval-reject-disposition.test.ts.",
|
|
6
|
+
"nodes": [
|
|
7
|
+
{ "id": "start", "typeId": "core.noop", "name": "Start", "position": { "x": 0, "y": 0 }, "config": {}, "inputs": {} },
|
|
8
|
+
{
|
|
9
|
+
"id": "gate",
|
|
10
|
+
"typeId": "core.approvalGate",
|
|
11
|
+
"name": "Approval Gate",
|
|
12
|
+
"position": { "x": 200, "y": 0 },
|
|
13
|
+
"config": {
|
|
14
|
+
"title": "Conformance approval (reject loops back)",
|
|
15
|
+
"description": "Conformance suite — reject once; the gate is asked again.",
|
|
16
|
+
"actions": ["accept", "reject"]
|
|
17
|
+
},
|
|
18
|
+
"inputs": {}
|
|
19
|
+
},
|
|
20
|
+
{ "id": "revise", "typeId": "core.noop", "name": "Revise", "position": { "x": 400, "y": 0 }, "config": {}, "inputs": {} }
|
|
21
|
+
],
|
|
22
|
+
"edges": [
|
|
23
|
+
{ "id": "start-gate", "sourceNodeId": "start", "targetNodeId": "gate", "triggerRule": "any_success" },
|
|
24
|
+
{ "id": "gate-revise", "sourceNodeId": "gate", "targetNodeId": "revise", "triggerRule": "any_failed" },
|
|
25
|
+
{ "id": "revise-gate", "sourceNodeId": "revise", "targetNodeId": "gate", "triggerRule": "any_success" }
|
|
26
|
+
],
|
|
27
|
+
"triggers": [
|
|
28
|
+
{ "id": "manual", "type": "manual", "enabled": true }
|
|
29
|
+
],
|
|
30
|
+
"variables": [],
|
|
31
|
+
"metadata": { "tags": ["conformance", "hitl"] },
|
|
32
|
+
"settings": { "timeout": 0, "maxLoopbackIterations": 2 }
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "conformance-approval-reject-routed",
|
|
3
|
+
"name": "Conformance: Approval (reject routed)",
|
|
4
|
+
"version": "1.0",
|
|
5
|
+
"description": "RFC 0223 G1. An approval gate with two outgoing edges: an any_failed edge to on-reject and an all_success edge to on-accept. Rejected, the gate fails with approval_rejected, on-reject runs, on-accept never runs, and the run completes. Conformance scenario: v2-approval-reject-disposition.test.ts.",
|
|
6
|
+
"nodes": [
|
|
7
|
+
{
|
|
8
|
+
"id": "gate",
|
|
9
|
+
"typeId": "core.approvalGate",
|
|
10
|
+
"name": "Approval Gate",
|
|
11
|
+
"position": { "x": 0, "y": 0 },
|
|
12
|
+
"config": {
|
|
13
|
+
"title": "Conformance approval (reject routed)",
|
|
14
|
+
"description": "Conformance suite — reject to take the any_failed route.",
|
|
15
|
+
"actions": ["accept", "reject"]
|
|
16
|
+
},
|
|
17
|
+
"inputs": {}
|
|
18
|
+
},
|
|
19
|
+
{ "id": "on-reject", "typeId": "core.noop", "name": "On reject", "position": { "x": 200, "y": -80 }, "config": {}, "inputs": {} },
|
|
20
|
+
{ "id": "on-accept", "typeId": "core.noop", "name": "On accept", "position": { "x": 200, "y": 80 }, "config": {}, "inputs": {} }
|
|
21
|
+
],
|
|
22
|
+
"edges": [
|
|
23
|
+
{ "id": "gate-on-reject", "sourceNodeId": "gate", "targetNodeId": "on-reject", "triggerRule": "any_failed" },
|
|
24
|
+
{ "id": "gate-on-accept", "sourceNodeId": "gate", "targetNodeId": "on-accept", "triggerRule": "all_success" }
|
|
25
|
+
],
|
|
26
|
+
"triggers": [
|
|
27
|
+
{ "id": "manual", "type": "manual", "enabled": true }
|
|
28
|
+
],
|
|
29
|
+
"variables": [],
|
|
30
|
+
"metadata": { "tags": ["conformance", "hitl"] },
|
|
31
|
+
"settings": { "timeout": 0 }
|
|
32
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "conformance-approval-timeout-approve",
|
|
3
|
+
"name": "Conformance: Approval (timeout, onTimeout approve)",
|
|
4
|
+
"version": "1.0",
|
|
5
|
+
"description": "RFC 0223 / openwop#1696. An approval gate with a 1500 ms timeoutMs and onTimeout: approve. Nobody resolves it: a timeout never grants, so the host's timer resolves the gate rejected (action timeout, decision rejected, reason timeout), the gate fails with approval_rejected and the run fails. Conformance scenario: v2-approval-reject-disposition.test.ts.",
|
|
6
|
+
"nodes": [
|
|
7
|
+
{
|
|
8
|
+
"id": "gate",
|
|
9
|
+
"typeId": "core.approvalGate",
|
|
10
|
+
"name": "Approval Gate",
|
|
11
|
+
"position": {
|
|
12
|
+
"x": 0,
|
|
13
|
+
"y": 0
|
|
14
|
+
},
|
|
15
|
+
"config": {
|
|
16
|
+
"title": "Conformance approval (timeout, onTimeout approve)",
|
|
17
|
+
"description": "Conformance suite — do not resolve; the gate times out and MUST NOT be granted.",
|
|
18
|
+
"actions": [
|
|
19
|
+
"accept",
|
|
20
|
+
"reject"
|
|
21
|
+
],
|
|
22
|
+
"timeoutMs": 1500,
|
|
23
|
+
"onTimeout": "approve"
|
|
24
|
+
},
|
|
25
|
+
"inputs": {}
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"edges": [],
|
|
29
|
+
"triggers": [
|
|
30
|
+
{
|
|
31
|
+
"id": "manual",
|
|
32
|
+
"type": "manual",
|
|
33
|
+
"enabled": true
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"variables": [],
|
|
37
|
+
"metadata": {
|
|
38
|
+
"tags": [
|
|
39
|
+
"conformance",
|
|
40
|
+
"hitl"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"settings": {
|
|
44
|
+
"timeout": 0
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "conformance-approval-timeout",
|
|
3
|
+
"name": "Conformance: Approval (timeout)",
|
|
4
|
+
"version": "1.0",
|
|
5
|
+
"description": "RFC 0223 G2. An approval gate with a 1500 ms timeoutMs and no onTimeout. Nobody resolves it: the host's timer resolves the gate rejected (action timeout, decision rejected, reason timeout), the gate fails with approval_rejected and the run fails naming it. Conformance scenario: v2-approval-reject-disposition.test.ts.",
|
|
6
|
+
"nodes": [
|
|
7
|
+
{
|
|
8
|
+
"id": "gate",
|
|
9
|
+
"typeId": "core.approvalGate",
|
|
10
|
+
"name": "Approval Gate",
|
|
11
|
+
"position": { "x": 0, "y": 0 },
|
|
12
|
+
"config": {
|
|
13
|
+
"title": "Conformance approval (timeout)",
|
|
14
|
+
"description": "Conformance suite — do not resolve; the gate times out.",
|
|
15
|
+
"actions": ["accept", "reject"],
|
|
16
|
+
"timeoutMs": 1500
|
|
17
|
+
},
|
|
18
|
+
"inputs": {}
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"edges": [],
|
|
22
|
+
"triggers": [
|
|
23
|
+
{ "id": "manual", "type": "manual", "enabled": true }
|
|
24
|
+
],
|
|
25
|
+
"variables": [],
|
|
26
|
+
"metadata": { "tags": ["conformance", "hitl"] },
|
|
27
|
+
"settings": { "timeout": 0 }
|
|
28
|
+
}
|
package/fixtures.md
CHANGED
|
@@ -49,6 +49,10 @@ All fixtures MUST advertise:
|
|
|
49
49
|
| Approval | `conformance-approval` | Verifies HITL approval interrupt + resume | `completed` after resolve | unbounded (suspends) |
|
|
50
50
|
| Approval (refine) | `conformance-approval-refine` | RFC 0183 — refine resolution carries action + refineFeedback |
|
|
51
51
|
| Approval (edit-accept) | `conformance-approval-edit-accept` | RFC 0183 — edit-accept resolution carries action + editedArtifactData |
|
|
52
|
+
| Approval (reject routed) | `conformance-approval-reject-routed` | RFC 0223 G1 — a rejected gate is routed over an `any_failed` edge; its `all_success` sibling never runs | `completed` after reject | unbounded (suspends) |
|
|
53
|
+
| Approval (timeout) | `conformance-approval-timeout` | RFC 0223 G2 — a 1500 ms `timeoutMs`, no `onTimeout`: the host's timer resolves the gate rejected | `failed` (`approval_rejected`) after the timeout | ~1.5 s + the host's sweep |
|
|
54
|
+
| Approval (timeout, onTimeout approve) | `conformance-approval-timeout-approve` | openwop#1696 — a 1500 ms `timeoutMs` with `onTimeout: approve`: a timeout never grants, so the gate still resolves rejected | `failed` (`approval_rejected`) after the timeout | ~1.5 s + the host's sweep |
|
|
55
|
+
| Approval (reject loops back) | `conformance-approval-reject-loopback` | openwop#1697 — a rejected gate routed back to itself through `revise` is asked again under a new `key` | `waiting-approval` again after one reject (the suite cancels) | unbounded (suspends) |
|
|
52
56
|
| Clarification | `conformance-clarification` | Verifies HITL clarification interrupt + resume | `completed` after resolve | unbounded (suspends) |
|
|
53
57
|
| Clarification (nested schema) | `conformance-clarification-nested` | RFC 0199 §D.2(d) probe — an answer schema an MCP mount MUST NOT bridge in form mode | `completed` after resolve | unbounded (suspends) |
|
|
54
58
|
| Clarification (sensitive field) | `conformance-clarification-sensitive` | RFC 0199 §D.2(d) probe — `format: "password"`; form mode MUST be refused | `completed` after resolve | unbounded (suspends) |
|
|
@@ -211,6 +215,54 @@ The `messages`-mode stream fixture (AI token streaming) is covered by the determ
|
|
|
211
215
|
5. An `edit-accept` resolution supplying no `editedArtifactData` MUST be refused.
|
|
212
216
|
- **Why it is separate**: same reason as `conformance-approval-refine` — widening a registered fixture's `actions` forces every host to re-register a test-only change.
|
|
213
217
|
|
|
218
|
+
### `conformance-approval-reject-routed`
|
|
219
|
+
|
|
220
|
+
- **Purpose**: witness that a rejected approval gate is a failed source a workflow can route (RFC 0223 §A, gap G1; `spec/v2/core/interrupt.md` §Rejection).
|
|
221
|
+
- **Inputs**: none.
|
|
222
|
+
- **Behavior**:
|
|
223
|
+
1. Run starts and reaches the `gate` (`core.approvalGate`, actions `accept | reject`).
|
|
224
|
+
2. Run status MUST be `waiting-approval`.
|
|
225
|
+
3. Client POSTs `{action: 'reject'}` to the interrupt.
|
|
226
|
+
4. `gate` MUST emit `node.failed` with `error.code` `approval_rejected` and `retryable: false`.
|
|
227
|
+
5. `on-reject` (incoming edge `triggerRule: any_failed`) MUST run to `node.completed`.
|
|
228
|
+
6. `on-accept` (incoming edge `triggerRule: all_success`) MUST NOT run: no `node.started` or `node.completed` for it. A host MAY record `node.skipped`; the spec does not require it.
|
|
229
|
+
7. Run reaches `completed`.
|
|
230
|
+
- **Terminal status (after reject)**: `completed`.
|
|
231
|
+
- **Why it is separate from `conformance-approval`**: that fixture has no edges, and giving it some would force every host to re-register a registered definition.
|
|
232
|
+
|
|
233
|
+
### `conformance-approval-timeout`
|
|
234
|
+
|
|
235
|
+
- **Purpose**: witness the timeout disposition (RFC 0223 §A, gap G2).
|
|
236
|
+
- **Inputs**: none.
|
|
237
|
+
- **Behavior**:
|
|
238
|
+
1. Run starts and reaches the `gate` (`core.approvalGate`, `config.timeoutMs: 1500`, no `onTimeout`). The host carries the node's `timeoutMs` onto the interrupt payload.
|
|
239
|
+
2. Run status MUST be `waiting-approval`.
|
|
240
|
+
3. Nobody resolves it. After the deadline, the host MUST resolve the gate itself: `interrupt.resolved` with `action: "timeout"`, `decision: "rejected"`, `reason: "timeout"`.
|
|
241
|
+
4. `gate` MUST emit `node.failed` with `error.code` `approval_rejected`, and `run.failed` MUST carry `approval_rejected` with `failedNodeId: "gate"`.
|
|
242
|
+
- **Terminal status**: `failed`.
|
|
243
|
+
- **Timing**: the suite waits up to 20 s (scaled) for the terminal state; a host's timer granularity is its own.
|
|
244
|
+
|
|
245
|
+
### `conformance-approval-timeout-approve`
|
|
246
|
+
|
|
247
|
+
- **Purpose**: witness that a timeout never grants a gate, whatever `onTimeout` holds (`spec/v2/core/interrupt.md` §Rejection; RFC 0223 §C as corrected by openwop#1696).
|
|
248
|
+
- **Inputs**: none.
|
|
249
|
+
- **Behavior**: as `conformance-approval-timeout`, with `config.onTimeout: "approve"`. After the deadline, the host MUST resolve the gate rejected (`action: "timeout"`, `decision: "rejected"`, `reason: "timeout"`), `gate` MUST fail with `approval_rejected`, and the run MUST NOT complete.
|
|
250
|
+
- **Terminal status**: `failed`.
|
|
251
|
+
- **Why it is separate**: `conformance-approval-timeout` witnesses the absent case; this one witnesses the value that used to read as permission to fail open.
|
|
252
|
+
|
|
253
|
+
### `conformance-approval-reject-loopback`
|
|
254
|
+
|
|
255
|
+
- **Purpose**: witness that `key` is per visit (`spec/v2/core/interrupt.md` §Re-entry and resume values, openwop#1697).
|
|
256
|
+
- **Inputs**: none.
|
|
257
|
+
- **Graph**: `start` (`core.noop`) → `gate` (`any_success`); `gate` → `revise` (`core.noop`, `any_failed`); `revise` → `gate` (`any_success`). `settings.maxLoopbackIterations: 2`.
|
|
258
|
+
- **Behavior**:
|
|
259
|
+
1. Run reaches `gate` and MUST be `waiting-approval`.
|
|
260
|
+
2. Client POSTs `{action: 'reject'}`. `gate` fails with `approval_rejected`, and `revise` runs.
|
|
261
|
+
3. `gate` runs again. The host MUST raise a second `interrupt.requested` for `gate` with a different `key`, and the run MUST be `waiting-approval` again.
|
|
262
|
+
4. The first rejection MUST NOT be replayed into the second visit: only one `interrupt.resolved` is on the log.
|
|
263
|
+
- **Terminal status**: none reached by the suite, which cancels the run.
|
|
264
|
+
- **Advertise only if** the host runs cycles. A host that does not MUST NOT advertise the fixture, and the row is `inapplicable`.
|
|
265
|
+
|
|
214
266
|
### `conformance-clarification`
|
|
215
267
|
|
|
216
268
|
- **Purpose**: verify HITL clarification interrupt + resume.
|
|
@@ -524,7 +576,7 @@ The fixtures reference these typeIds:
|
|
|
524
576
|
| `core.noop` | noop, multi-node, approval, clarification, cancellable | Immediate completion, no output |
|
|
525
577
|
| `core.identity` | identity | Echo `input.payload` to `output.payload` |
|
|
526
578
|
| `core.delay` | delay, cancellable | Sleep `config.delayMs` ms |
|
|
527
|
-
| `core.fail` | failure | Throw with `code: "
|
|
579
|
+
| `core.fail` | failure | Throw with `code: "example.conformance_failure"` (a vendor code under the registered `example` org; errors.md §The registry, openwop#1698), message: "Intentional conformance failure" |
|
|
528
580
|
| `core.approvalGate` | approval | Call `ctx.interrupt({kind: 'approval', ...})` |
|
|
529
581
|
| `core.clarificationGate` | clarification | Call `ctx.interrupt({kind: 'clarification', ...})` |
|
|
530
582
|
| `conformance.requiresMissing` | capability-missing | Declares `requires: ['conformance.never-provided']`; engine MUST refuse dispatch. Opt-in fixture registration is recommended so production deployments don't expose the fixture surface. |
|
|
@@ -588,6 +640,10 @@ conformance/
|
|
|
588
640
|
conformance-approval.json
|
|
589
641
|
conformance-approval-refine.json
|
|
590
642
|
conformance-approval-edit-accept.json
|
|
643
|
+
conformance-approval-reject-routed.json
|
|
644
|
+
conformance-approval-timeout.json
|
|
645
|
+
conformance-approval-timeout-approve.json
|
|
646
|
+
conformance-approval-reject-loopback.json
|
|
591
647
|
conformance-clarification.json
|
|
592
648
|
conformance-multi-node.json
|
|
593
649
|
conformance-idempotent.json
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.43.
|
|
3
|
+
"version": "2.43.1",
|
|
4
4
|
"description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -56,6 +56,6 @@
|
|
|
56
56
|
"@openwop/spec-artifacts": "file:../spec-artifacts"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@openwop/spec-artifacts": "2.43.
|
|
59
|
+
"@openwop/spec-artifacts": "2.43.1"
|
|
60
60
|
}
|
|
61
61
|
}
|