Skip to content

fix(core): recognise trust-task-error at any 0.x, not just 0.1 and 0.2 - #115

Merged
stormer78 merged 1 commit into
mainfrom
tt-error-03
Aug 14, 2026
Merged

fix(core): recognise trust-task-error at any 0.x, not just 0.1 and 0.2#115
stormer78 merged 1 commit into
mainfrom
tt-error-03

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

fix(core): recognise trust-task-error at any 0.x, not just 0.1 and 0.2

isTrustTaskErrorType enumerated two framework error-document versions.
trust-tasks-rs has emitted trust-task-error/0.3 since its 0.3 release —
it carries the §8.2 inResponseTo member, and 0.2's payload schema is
additionalProperties: false, so the document cannot claim to be 0.2. The
VTA moved to 0.4 and every rejection it sends has been unrecognised here since.

Unrecognised does not mean "reported with the wrong wording". An error document
this predicate rejects is not an error at all: parseTrustTaskReply falls
through to the success branch and returns the ErrorPayload as the operation's
result. So a refused task arrived at the calling page as a completed one. A
webvh/dids/update the VTA had refused ("did not found: SCID … not found",
returned 422) rendered "your agent signed and published the update" in the
hosting UI, and nothing was published.

A requireConsent refusal is the same document, which is the more serious half.
requestTask recognises consent inside a thrown VtaClientError; with no
throw it resolved as accepted instead, so the cross-device match code never
rendered and the approver was never asked. That is silent on both sides — the
requester sees success, the approver sees nothing — and it would have been the
first thing hit by the next VTA to configure an approver set.

Match the framework slug at any 0.x minor instead, which is what SPEC.md §5.2's
forward-minor rule asks of a consumer, so the next minor cannot break this the
same way. 1.x is deliberately excluded: a major bump is where the payload shape
may genuinely change, and that should come back through a code change rather than
be decoded under 0.x assumptions. All four consumers — the REST channel,
parseTrustTaskReply, the approver's decision-outcome reader and push-gateway
registration — already routed through this one predicate, so they are all fixed.

Why no test caught it: every test that built an error document hard-coded
/0.1, a version no VTA emits. rejecting() now defaults to what actually
ships and the consent tests run across all three versions, so the suite
exercises the wire it will meet. Reverting the predicate fails nine tests,
including the pre-existing consent ones.

`isTrustTaskErrorType` enumerated two framework error-document versions.
`trust-tasks-rs` has emitted `trust-task-error/0.3` since its 0.3 release —
it carries the §8.2 `inResponseTo` member, and `0.2`'s payload schema is
`additionalProperties: false`, so the document cannot claim to be `0.2`. The
VTA moved to 0.4 and every rejection it sends has been unrecognised here since.

Unrecognised does not mean "reported with the wrong wording". An error document
this predicate rejects is not an error at all: `parseTrustTaskReply` falls
through to the success branch and returns the `ErrorPayload` as the operation's
*result*. So a refused task arrived at the calling page as a completed one. A
`webvh/dids/update` the VTA had refused ("did not found: SCID … not found",
returned 422) rendered "your agent signed and published the update" in the
hosting UI, and nothing was published.

A `requireConsent` refusal is the same document, which is the more serious half.
`requestTask` recognises consent inside a *thrown* `VtaClientError`; with no
throw it resolved as `accepted` instead, so the cross-device match code never
rendered and the approver was never asked. That is silent on both sides — the
requester sees success, the approver sees nothing — and it would have been the
first thing hit by the next VTA to configure an approver set.

Match the framework slug at any `0.x` minor instead, which is what SPEC.md §5.2's
forward-minor rule asks of a consumer, so the next minor cannot break this the
same way. `1.x` is deliberately excluded: a major bump is where the payload shape
may genuinely change, and that should come back through a code change rather than
be decoded under 0.x assumptions. All four consumers — the REST channel,
`parseTrustTaskReply`, the approver's decision-outcome reader and push-gateway
registration — already routed through this one predicate, so they are all fixed.

Why no test caught it: every test that built an error document hard-coded
`/0.1`, a version no VTA emits. `rejecting()` now defaults to what actually
ships and the consent tests run across all three versions, so the suite
exercises the wire it will meet. Reverting the predicate fails nine tests,
including the pre-existing consent ones.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 merged commit ebebb50 into main Aug 14, 2026
3 checks passed
@stormer78
stormer78 deleted the tt-error-03 branch August 14, 2026 02:58
stormer78 added a commit to affinidi/affinidi-webvh-service that referenced this pull request Aug 14, 2026
Every routed rejection goes out through `TrustTask::reject_with`, which stamps
whatever version `trust-tasks-rs` emits — `trust-task-error/0.3` since the
framework's own 0.3 release, for the §8.2 `inResponseTo` member that `0.2`'s
`additionalProperties: false` payload schema cannot admit. But four paths in
this service are **unrouted**: the body never parsed into a Trust Task, so there
is no request document to reject from and the Type URI has to be written out by
hand. All four wrote `0.1`:

  * `routes::trust_tasks::error_type_uri`
  * `routes::auth::trust_task_malformed`
  * `messaging::body_parse_error`
  * the TSP transport's parse-failure reply

So which version a caller saw depended on whether its request happened to parse.
That is a trap for exactly the consumer that pins a version, and it is not
hypothetical — the trap sprang twice this week. A client enumerating `0.1` and
`0.2` decodes a `0.3` rejection as a **success**, because an unrecognised error
document falls through to the success branch and its payload is returned as the
operation's result (#160 here, OpenVTC/vta-browser-plugin#115 in the wallet).
Emitting two versions is worse than emitting the wrong one: whichever a consumer
pins, it is right half the time, and the half it gets wrong is silent.

`trust-tasks-rs` keeps `trust_task_error_type_uri()` `pub(crate)`, so the value
cannot be read back from the framework. It is now named once for the workspace,
in `did_hosting_common::server::trust_tasks::framework_error_type_uri`, and a
test compares it against the Type URI a real `reject_with` produces. A framework
bump now fails that test rather than re-splitting the service into two dialects.

Test assertions that hard-coded `0.1` now assert against that function instead
of a literal, so they cannot drift away from what the service emits — the same
reason the JS suites were changed in #160: a test that exercises a wire nobody
speaks is how a version pin survives unnoticed.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
stormer78 added a commit to OpenVTC/verifiable-trust-infrastructure that referenced this pull request Aug 14, 2026
…ocument (#973)

* fix(trust-tasks): stop emitting two versions of the framework error document

Both services routed their rejections through `TrustTask::reject_with`, which
stamps whatever version `trust-tasks-rs` emits — `trust-task-error/0.3` since
the framework's own 0.3 release. But each service also has one *unrouted* path,
for a body that never parsed into a Trust Task at all, and with no request
document to reject from it had to write the Type URI out by hand. Both wrote
`0.1`.

So a single service spoke two dialects, distinguished only by whether the
request happened to parse. That is a trap for exactly the consumer that pins a
version, and it is not hypothetical: a client enumerating `0.1`/`0.2` read every
`0.3` rejection as a **success**, because an unrecognised error document falls
through to the success branch and its payload is returned as the operation's
result (OpenVTC/vta-browser-plugin#115, affinidi/affinidi-webvh-service#160).
The version a service emits is wire contract; emitting two is worse than
emitting the wrong one, because whichever a consumer pins is right half the time.

`trust-tasks-rs` keeps `trust_task_error_type_uri()` `pub(crate)`, so the value
cannot be read from the framework. Each service now names it once, in
`framework_error_type_uri()` beside the unrouted builder, and a test compares
that against the Type URI a real `reject_with` produces. A framework bump now
fails a test instead of silently re-splitting the service in two. A second test
asserts the bytes on the wire carry it, not just the value we compute.

Test fixtures that stood in for a peer's rejection were built at `0.1` — a
version no peer on trust-tasks-rs 0.4 sends. They now use what a peer actually
emits. Those assertions pass either way (the matchers key on the slug, which is
the right way to match), but a suite that exercises a wire nobody speaks is how
the client-side version pin survived this long unnoticed.

Left alone deliberately: `vtc-service::messaging`'s `.unwrap_or(…/0.1)` default,
which labels an inbound document that carries no `type` at all. It is not an
emitted document, and every consumer of that label matches on the slug.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>

* fix(vtc): name one spelling of the framework error URI, as the census requires

The census (`vtc-service/tests/trust_task_manifest.rs`) failed on the previous
commit, and it was right twice.

**One URI per family.** `UNPUBLISHED_CANONICAL_OK` pins `trust-task-error/` at
exactly one URI. The previous commit moved the emitted version to `0.3` but left
a `0.1` literal in `messaging.rs` — the `.unwrap_or(…)` that labels an inbound
reply carrying no `type` at all. That was a deliberate call ("not an emitted
document, every consumer matches on the slug") and it was the wrong one: two
spellings in the crate is exactly the condition this whole change set exists to
remove, whether or not each one reaches the wire. The default now goes through
`framework_error_type_uri`, so the crate holds a single literal.

**A test probe is a binding.** The new test typed its throwaway request document
`spec/vtc/join/request/0.1`, which the registry does not publish. The census
scans source literals including test code, because a `trusttasks.org/spec/` URI
written down anywhere is a claim that the registry serves it. Retyped to
`spec/acl/list/0.1`, which this crate already binds, with a comment saying why
so the next person does not reach for a plausible-looking invention.

Neither is a wire change. Both are the census doing its job.

Worth recording why CI found this and local runs did not: the census lives in a
crate the change barely touches, and its scan roots are `vtc-service/src`,
`vta-sdk/src`, `cnm-cli/src` and `vtc-service/admin-ui/src` — `vta-service/src`
is not among them, so the identical edit there is invisible to it. Per-crate
runs of the crates you edited cannot see any of that. `cargo test --workspace`,
which is what CI runs, can — and now passes.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>

---------

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
stormer78 added a commit that referenced this pull request Aug 17, 2026
* release(tsp-js): @openvtc/vti-tsp-js 0.2.0

Publishes the pure-TS HPKE swap (#116) and the new `./hpke` subpath export.

Minor rather than patch: `sealBase`/`openBase` and the `./hpke` export are
new public surface. The auth-mode `seal`/`open` signatures and the wire bytes
are unchanged, so existing callers need no changes.

`@openvtc/pnm-core` cannot be published until this is on npm — it imports
`@openvtc/vti-tsp-js/hpke`, and the published 0.1.0 exports map has only
".".

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>

* release(core): @openvtc/pnm-core 0.4.0

Ships the HPKE consolidation (#116) and the trust-task-error 0.x fix (#115).

Minor rather than patch: `@hpke/*` leaves the runtime dependency set and the
`@openvtc/vti-tsp-js` range gains a floor.

The range moves from `*` to `^0.2.0`. `*` expressed no minimum, so it was
satisfied by the published 0.1.0 — which has no `./hpke` export — and a
consumer whose lockfile pinned 0.1.0 would have installed this release against
it and failed to resolve the import.

**Do not publish this until `@openvtc/vti-tsp-js@0.2.0` is on npm** (#117).

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>

---------

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant