Skip to content

feat(provider-tangle): derive retained control from deployment capability discovery - #165

Merged
drewstone merged 3 commits into
mainfrom
feat/154-consume-capability-discovery
Aug 13, 2026
Merged

feat(provider-tangle): derive retained control from deployment capability discovery#165
drewstone merged 3 commits into
mainfrom
feat/154-consume-capability-discovery

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

Problem

The Tangle provider decided its capabilities by measuring itself — it minted a throwaway SandboxInstance and checked whether the npm package it imported had cancelRun. The code's own comment admitted the limit: deployment truth "needs the sidecar capability endpoint and is a follow-up."

So a modern SDK talking to an older sidecar advertised retained control, the runtime accepted it, a real sandbox got created, and only then did it fail (#154).

Change

The provider now asks the deployment. Composing an environment reads box.capabilities() once (published in @tangle-network/sandbox@0.22.0) and derives the claim from that document:

  • retainedControl requires dispatch.runControlRef and all three cancel.* flags; every one of the seven flags is consumed, and forcing any of them true reds the suite.
  • Unknown means claim nothing — an SDK older than 0.22.0, a sandbox not running, a null document, or a failed read all yield no claim, so the runtime refuses before a sandbox is spent.
  • A transport failure on capability discovery degrades to unknown instead of failing create(). An earlier revision destroyed a freshly created sandbox on a transient 500; that cost 25–40s of cold provisioning and is now impossible.
  • The adapter surface stays the ceiling: a deployment answer can only narrow, never widen.

AgentEnvironment gains an optional readonly capabilities? — the document for that environment. One provider reaches deployments of different ages, so a provider-level document can only state what holds before an environment exists. This is the contract fix for the original complaint that provider.capabilities() was the only document a consumer could read.

Verification

Two adversarial review rounds; both are reflected here.

  • The two documents are now bound by a mutation-proven relation test: the sandbox stage never claims beyond the client stage, the stages agree on every claim the deployment does not decide, and the exposed surface follows the sandbox document. Two disagreement mutations that the previous suite passed 68/68 now red.
  • The shared conformance helper is not weakened: each environment-level check binds to the document describing that environment, and runAgentEnvironmentProviderConformance passes against an SDK-backed client whose deployment returns null (dispatch undefined ⇔ streaming.detach false).
  • The abort re-throw on capability discovery is pinned — deleting it reds exactly one test.
  • Suite 69/69; pnpm -r check-types; check:package-artifacts (9 packages / 26 exports / 52 targets); check:control-artifacts 129/129 — the last builds and packs the provider, so the fix is proven through the published artifact.

Scope

Retained-control half only. The interaction half stays unclaimed and is blocked upstream on tangle-network/agent-dev-container#5421 (the SDK cannot aim an answer at a specific interaction, and discards the server's resolution verdict) — shipping it would deliver wrong answers to running agents.

Refs #154

…lity discovery

Composing an environment reads the deployment's capability document once
and derives the retained-control claim from it, instead of from the linked
Sandbox SDK's method surface. The SDK probe measured the SDK class, which
says nothing about what the connected service honors.

retainedControl needs dispatch.runControlRef with cancel.canonicalRunCancellation,
cancel.digestBound and cancel.idempotent. streaming.detach needs
dispatch.runControlRef alone, because detached dispatch carries the caller's
exact reference and refuses a receipt that does not echo it. A missing flag
is unknown, and unknown is never a claim.

Four inputs claim nothing: an SDK older than 0.22.0, a sandbox that is not
running, a null document, and a document that leaves a required flag unset.
Those environments omit dispatch and their sessions omit cancelRun. A
malformed document throws, and create() deletes the sandbox it just made.

The client stage keeps its adapter-surface probe as an upper bound, because
no deployment exists to ask before a sandbox is created. The adapter surface
stays the ceiling: a deployment claim can only narrow it.

The peer floor stays at 0.19.6. Capability discovery is feature-detected, so
a consumer on an older SDK degrades to no retained control instead of
failing to load.
A capability the connected deployment decides cannot be stated before an
environment exists, because one provider reaches deployments of different
ages. The provider document said a run could detach while the environment
it created omitted dispatch, so runAgentEnvironmentProviderConformance
failed with "streaming.detach requires dispatch()" for an SDK-backed
client whose deployment returns null.

AgentEnvironment gains an optional capabilities document. The Tangle
provider publishes the narrowed document there, frozen, and derives the
session grant from it, so the operations an environment exposes and the
document a caller reads are the same fact. The conformance suites bind
every environment-scoped check to that document when an environment
publishes one, and to the provider document otherwise.

The provider document stays the adapter's ceiling. agent-runtime asserts
the retained-control block from provider.capabilities() before create, so
a fail-closed provider document would refuse retained runs against every
deployment, including the ones that back them.

Every flag the capability document carries now gates the claims it backs.
streaming.detach and streaming.turnIdempotency need dispatch.runControlRef
with dispatch.executionIdOnAdmission; streaming.replay needs
runs.eventReplay; retainedControl needs those with the three cancel flags
and runs.executionScopedStatus. Detach also needs a session handle,
because a detached run is reachable only through one.

A failed capability read no longer fails create() and no longer deletes a
sandbox that a cold provision has just paid for. It claims nothing and
reports the failure on the warning channel. The caller's own abort still
propagates.
…aborted read

The two capability stages had no asserted relation. Every exposure check
binds to the document an environment publishes, which leaves the provider
document unchecked against it: an environment could claim what the client
stage does not carry, or drop a claim the client stage advertises, and no
test failed. Two mutations proved the gap, each of them green on the whole
suite.

The client-stage and sandbox-stage documents are now paired over both
answers a deployment can give to an SDK-backed client. The sandbox stage
claims nothing beyond the client stage, the two agree on every claim the
deployment does not decide, and the exposed operations follow the
sandbox-stage document. Equality across the stages stays unasserted: the
client stage runs before any sandbox exists, so a deployment that discloses
nothing would drag the provider document down and refuse retained runs
against every deployment.

A read that fails while the caller's abort lands now propagates the abort
at the capability boundary and reaches no warning channel. The provider
re-checks the signal after composition, so that boundary was unpinned.

The README flag table stated part of what four flags gate. Every flag in it
gates sessions.continue, retainedControl, and session.cancelRun. The README
and the changeset also counted a document that leaves a flag unset among
the inputs that claim nothing; such a document drops the claims that flag
gates and keeps the rest.
@drewstone
drewstone merged commit bdb076b into main Aug 13, 2026
1 check passed

@tangletools tangletools left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Auto-approved drewstone PR — b3106e14

This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.

This approval is provisional. It rests on the audit running. If the audit cannot run — for example the CLI bridge rejects it — this approval is dismissed rather than left standing, so an unrun check never reads as a passing one.

tangletools · auto-approval · reason: drewstone_author · 2026-08-13T21:21:18Z

@tangletools tangletools mentioned this pull request Aug 13, 2026
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.

2 participants