Research / Distribution and placement
P32 · full · draft
Seams as Synthesized Providers: Interception, Typed Remote Clients, A2A Transport and Safe Adapter Synthesis
partial full Distribution and placement
State of the work. Seam row, remote row, A2A binding and adapter predicate are shipped with tests; seam runtime behaviour, transport-failure withdrawal and the observer value are not established, and there is no evaluation.
This is a draft, not peer reviewed. Repository references link to
revl at commit 45bec431f.
Pitch
A seam and a remote client are the same operation today, "synthesize a provider from a service declaration and put it in the row table", and a mock is designed to join it. Interception and remoteness therefore become admission facts that the unchanged checker already knows how to judge. Adapter synthesis is a separate path (src/revl/adapt.py) that the paper proposes to unify, not a caller of the same function.
Abstract
Middleware systems add interception, remote calls and interface adaptation as separate mechanisms: filter chains with a next() to forget, stub generators whose output is trusted, and adapters written by hand. Each has its own failure class and its own place in the trust story. We describe a composition language in which interception and remote providers are callers of one function, synthesize_provider(service, kind, params), over a table of named composition rows. The function emits ordinary source from a service declaration, and the ordinary compiler then checks it, so a synthesizer bug cannot make the checker admit ill-typed or out-of-bound code; it can still emit well-typed code with the wrong behaviour, which the checker does not catch. An interception row addresses a provision edge, is derived rather than written (no next()), has two kinds (observe, decide) and no rewrite. A remote row is a synthesized provider whose transport failure is declared to be provider withdrawal (the runtime cascade is not yet wired); it synthesizes no inverse, so the language's LIFO teardown guarantee is untouched, and its returns are untrusted by construction. A named wire, through a2a, binds A2A 1.0.0 and refuses every other transport by name. Safe adapter synthesis is a closed catalogue of total transformations under five safety clauses, proposed rather than applied silently, and admitted by the same gate as hand-written code. We report what is shipped (the seam row, the remote row, the A2A binding with its boundary gates, the adapter predicate with a 100-pair return-type dichotomy sweep), what is not (the full observer value, the widening of the emission bound, rename mapping), and where the documentation lags the code. We give no performance numbers.
Problem and claim
Interception, remote stubs and adapters each break a property that a checked composition language is trying to keep. A middleware chain lets a plugin forget to call the next handler or call it with other arguments. A generated remote client usually looks like a local call and is trusted as if it were one. An adapter written by hand, or inferred silently by a resolver, can change meaning without any reviewable artifact.
The claim is structural. If each of these is a synthesizer caller over a row table, then three consequences follow and can be checked in the tree. First, soundness is inherited: the synthesized text is compiled by the ordinary compiler, so G2 (one provider per key and realm), G3 and G4 (emission bounds) run unchanged (the function that returns the source is src/revl/synthesize.py:1652-1670; the gate code itself is not at that path and was not checked here). Second, the interesting decisions become small refusals with names: rewrite has no spelling, a remote method with no declared emission bound is not remotable, an address with userinfo is refused, an unknown transport is refused. Third, remoteness stops being a wiring fact. A consumer's source is identical whether its provider is local or remote.
The claim is not that revl has a unified synthesizer for four kinds today. The synthesizer function ships three kinds (remote, seam, host; src/revl/synthesize.py:117). The config overlay and the mock-requires driver are separate code paths. The paper must say "one synthesizer, three callers, with the overlay and mock designed to join" or fix the code first.
Contributions
- Interception as an edge-addressed row: a
seamrow names a(key, realm)provision edge, its forwarder is derived from the service declaration so there is nonext()to forget, the observer never receives the inner handle,observeanddecideexist andrewritehas no spelling, with the refusal argument stated (src/revl/parser.py:6162,docs/design/424-dsh-language-gaps.md:369-420). - The remote row as an admission fact: the provider is synthesized, transport failure is declared withdrawal by default (the runtime cascade is not yet wired,
docs/composition-rows.md:341-346), every method must declare an emission bound, no inverse is synthesized so G7's LIFO teardown has nothing to walk, an address with userinfo or a path is refused, and the returned value isUntrusted[T](docs/composition-rows.md:263-320,src/revl/synthesize.py:1-80). - A named wire with a boundary:
through a2amaps the canonical envelope onto A2A 1.0.0message/send, refuses other transports by name, and joins gates thatUntrusted[T]cannot give (correlation identity, envelope shape, scrubbing of peer-authored text from caller-visible failures) (docs/design/439-a2a-transport-binding.md:45-80,docs/design/439-a2a-transport-binding.md:199-300;src/revl/a2a_boundary.py:129,src/revl/a2a_boundary.py:197,src/revl/a2a_boundary.py:259,src/revl/a2a_boundary.py:413). - Safe adapter synthesis: a decidable predicate over two service declarations and an opt-in map, a closed refusal enum, a catalogue-version-pinned derivation hash, adapters proposed by
revl adaptand the resolver rather than applied silently, and a standing test that the predicate and the gate never disagree (src/revl/adapt.py:56-120,src/revl/adapt.py:704-743;tests/test_296_synthesis_catalogue.py:175-300). - The unifying observation, with the caveat above: seam, remote and host providers are callers of one function over the row table, and config and mock are designed to join it (adapter synthesis is separate) (
docs/design/424-dsh-language-gaps.md:681-700,src/revl/synthesize.py:1-30).
Evidence on main
Tree: revl main at commit 45bec431f. Counts are def test_ functions; no test was run for this extract.
| claim | path:line or test | status | note |
|---|---|---|---|
synthesize_provider dispatches remote, seam, host | src/revl/synthesize.py:117 (KINDS), src/revl/synthesize.py:1652-1670; callers src/revl/composition.py:904 (remote), src/revl/composition.py:1001 (host), src/revl/composition.py:1244 (seam) | shipped (3 kinds) | the module docstring table (src/revl/synthesize.py:10-17) still lists seam as "designed" although it is shipped, and lists the config overlay as a kind and mock as "SHIPPED"; neither the overlay nor the mock driver calls it (a search for synthesize_provider under src finds only composition.py and docstrings) |
| Config overlay and mock are callers of the same function | none found; mock-requires lives in src/revl/mocks.py:238 | designed | the biggest wording risk: the unification is a design, not code |
Seam row parses, contextual keywords only, rewrite has no spelling | src/revl/parser.py:1159-1190, src/revl/parser.py:6162; tests/test_424_seam_row.py (18): test_seam_row_parses_with_contextual_keywords_only, test_rewrite_has_no_spelling, test_seam_kind_must_be_observe_or_decide | shipped | |
Forwarder is derived; observe and decide bodies; observer never holds inner handle | src/revl/synthesize.py:1345-1470 (_seam_header, _seam_source); tests test_the_synthesized_forwarder_is_ordinary_source_on_no_disk, test_decide_seam_compiles_on_a_result_service | shipped | |
decide admitted only on Result methods | src/revl/synthesize.py:1317 (check_decidable); test test_decide_on_a_non_result_service_is_refused_naming_the_method | shipped | |
Seam through reach must stay inside the wrapped service's own emission bound | src/revl/composition.py:1118 (_check_seam_through); tests test_a_seam_through_reach_outside_the_service_bound_is_refused, test_a_narrow_service_bound_refuses_the_forwarder | shipped as the fallback | the widening (checking the forwarder against through instead of the service) is explicitly not made (src/revl/synthesize.py:1290-1293 for the decision note and src/revl/synthesize.py:1402-1405 for the emitted comment) |
Observer value is Untrusted[Value] | src/revl/synthesize.py:1413-1418 passes the operation name as a Str only, and says the full value is a later slice | designed | the code does not yet pass arguments to the observer |
Transitive capability attribution through a seam (seam:@label:key token) | docs/design/424-dsh-language-gaps.md (decision D-424b.6); code comment src/revl/synthesize.py:1404-1405 says the token is not made here | designed | |
| Runtime half of the seam ("observer sees one call in order") executed | tests/test_424_seam_row.py:1-30 docstring: "pinned at the source level" because the environment has no cordis-py runtime | partial | the claim is checked on generated source, not on a running composition |
| Same-key interposition is not expressible; distinct-key is | docs/composition-rows.md:702-748; tests/test_424_b1_interposition.py (5), tests/test_interposition_b1.py (4); src/revl/composition.py:1190-1212 hint text | shipped (as a documented limit) | docs/composition-rows.md:705-707 says the seam row "is not built" (line 707); it is built. Stale doc |
| Remote row: contextual keywords, needs an address, consumer unchanged, one provider resolved | tests/test_424_remote_row.py (23): test_consumer_compiles_unchanged_and_resolves_one_provider, test_a_remote_row_needs_a_peer_address | shipped | |
| Remotable means every method declares an emission bound | src/revl/synthesize.py:312 (check_remotable); test_a_plain_fn_service_is_refused_naming_the_method | shipped | |
| No inverse synthesized; header says so | src/revl/synthesize.py:1-80; test_no_inverse_is_synthesized_and_the_row_says_so (tests/test_424_remote_row.py:354) | shipped | the argument that G7 stays intact is in prose; no mechanized statement in this tree |
| Userinfo, URL and path refused in the peer address | src/revl/synthesize.py:1720 (check_address); test_a_peer_address_carrying_userinfo_is_refused, test_a_url_is_refused_and_a_bad_authority_is_refused | shipped | |
| Reach token folded from the host alone | src/revl/synthesize.py:241 (cap_token); test_the_reach_token_is_folded_from_the_host_alone | shipped | |
on_failure(withdraw) is the default; fault raised | tests test_on_failure_withdraw_is_the_default_and_raises_a_fault, test_on_failure_result_is_refused_on_a_non_result_method; docs/composition-rows.md:320-348 (runtime half at line 342) | partial | the doc says the runtime half (fault to provider withdrawal through the reactive path) is armed today by the placement bridge's monitor connection, which a synthesized row does not join, and that wiring it is the next step. The A2A design note cites src/revl/run.py:1870 as the withdrawal site; whether that closes the gap was not traced. Treat as open until checked |
| Returned values are untrusted | src/revl/synthesize.py:823-841, src/revl/synthesize.py:1230; tests test_the_canonical_wire_taints_the_returned_value, test_a_local_provider_of_the_same_service_is_not_tainted (tests/test_424_remote_row.py:544, tests/test_424_remote_row.py:567), test_a_remote_a2a_result_cannot_reach_an_authority_sink, test_an_endorse_on_the_flow_path_admits_the_remote_a2a_result (tests/test_439_a2a_transport.py:311, tests/test_439_a2a_transport.py:325) | shipped | docs/composition-rows.md:499 "Values are not tainted yet" is stale |
Named transport: only the canonical wire and through a2a (and a REST variant); others refused by name | src/revl/synthesize.py:1673 (check_transport), _A2A_TRANSPORTS; test_a_named_through_transport_is_refused | shipped | |
A2A message/send mapping, Str and Bytes modalities only | src/revl/synthesize.py:217 (_A2A_MODALITY), src/revl/synthesize.py:611 (_py_body_a2a); docs/design/439-a2a-transport-binding.md:45-80; tests/test_439_a2a_transport.py (112) | shipped | the supported subset is text-in text-out plus inline file bytes; a non-terminal reply faults |
| Boundary gates: correlation identity, JSON-RPC shape, peer text scrubbed of call arguments | src/revl/a2a_boundary.py:129 (py_correlation), src/revl/a2a_boundary.py:197 (py_funnel), src/revl/a2a_boundary.py:259 (py_envelope_gates), src/revl/a2a_boundary.py:413 (py_task_identity_gate), src/revl/a2a_boundary.py:456 (ts_funnel); tests/test_439_a2a_boundary.py (14) | shipped | the ts importer bodies join it too (ts_funnel is called from src/revl/import_a2a.py:664 and src/revl/import_a2a.py:926). The remote row's synthesized A2A body is py only (src/revl/synthesize.py:611; there is no ts builder). docs/design/439-a2a-transport-binding.md:267 ("NOT joined yet" on the ts importer) is stale against lines 724-727 |
| Version honesty: claim is exactly "A2A 1.0.0"; a card with another or no version is refused | src/revl/import_a2a.py:185 (A2A_VERSION), src/revl/import_a2a.py:321 (_version); tests/test_import_a2a.py:183, tests/test_import_a2a.py:189 | shipped for the importer | for a remote row there is no admission-time mismatch (the row states no version); a mismatch surfaces as a fault at first crossing (docs/design/439-a2a-transport-binding.md:351-392). Admission-time version refusal holds only on the importer path |
| A2A crossing is on the G8 audit surface | test_the_a2a_crossing_is_on_the_g8_audit_surface, test_all_four_task_crossings_are_on_the_g8_audit_surface (tests/test_439_a2a_transport.py) | shipped | |
Four-op Task lifecycle projection (_start, _poll, _reply, _cancel) | src/revl/a2a_task.py; docs/design/439-a2a-task-lifecycle.md; tests/test_439_a2a_task.py (12) | shipped (py) | out of this paper's core; Stream[T] sugar, push notifications and durable task ids are open (docs/design/439-a2a-transport-binding.md:645-700) |
| Adapter predicate, five clauses S1 to S5, catalogue B1 to B6, closed clause enum | src/revl/adapt.py:56 (CATALOGUE_VERSION), src/revl/adapt.py:59 (CLAUSES), src/revl/adapt.py:239 (compatible_total), src/revl/adapt.py:704 (bridge_plan); docs/design/296-adapter-synthesis.md:77-470 | shipped | |
Synthesis (render_adapter), derivation hash, adapter marking, chain depth | src/revl/adapt.py:743, src/revl/adapt.py:971, src/revl/adapt.py:999-1050; tests/test_adapter_synthesis.py (3), tests/test_adapter_predicate.py (26), tests/test_adapter_carry.py (6) | shipped | |
Resolver reports compatible-with-adapter, ranks below direct; revl adapt --check | src/revl/__main__.py:1378; tests/test_296_resolve_adapter_ranking.py (17) | shipped | revl diff showing the adapter as an added component remains a TODO in the module docstring (src/revl/adapt.py:36-38) |
| Predicate and gate never disagree | tests/test_296_synthesis_catalogue.py:195 (test_dichotomy_auto_sweep_never_disagrees), tests/test_296_synthesis_catalogue.py:254 (test_dichotomy_optin_matrix_never_disagrees) | partial | the sweep is 10 x 10 return types in a single-method service (_RETURNS, line 180); it does not generate parameter-position or arity cases |
| Synthesized body equals a hand-written wrapper | test_synthesized_body_is_byte_identical_to_a_handwritten_wrapper (tests/test_296_synthesis_catalogue.py:159) | shipped | the twin test for the "no gate learns what an adapter is" claim |
| Folds see through the adapter | tests/test_296_folds_see_through.py (7); src/revl/adapt.py:30-34 | shipped | |
| Rename mapping, auto-adapt mode | docs/design/296-adapter-synthesis.md:877-880, docs/design/296-adapter-synthesis.md:905 | designed (explicitly deferred) |
Numbers that exist
- Test function counts: seam row 18, remote row 23, B1 interposition 5 + 4, A2A transport 112, A2A boundary 14, A2A task 12, A2A importer 71, adapter tests 296-series 34 (7 + 17 + 10) plus 35 in the adapter-named files (26 + 6 + 3). Counts of tests, not coverage.
- The dichotomy sweep covers 10 return types squared = 100 ordered pairs for the no-opt-in half (
_RETURNS,tests/test_296_synthesis_catalogue.py:180-185). The test asserts at least 8 admitted and at least 8 refused; the actual split is not stored in the tree and was not computed here. - No latency, size or user-study numbers exist for any of these constructs.
Paper outline
- Introduction. Three failure classes (forgotten
next(), trusted stubs, silent adapters) and the one-function answer. - Background: services, providers, emission bounds, the row table and G2/G3/G4/G7 in the form this paper needs.
- The synthesizer: signature, why soundness is inherited, the kinds, what it refuses.
- Interception: edge unit, derived forwarder, observe and decide, why no rewrite, distinct-key cost, the
throughfallback and the widening not taken. - Remote providers: admission facts, withdrawal, no inverse, untrusted returns, addresses.
- The A2A binding: envelope mapping, boundary gates, why
Untrusted[T]is not enough, version honesty. - Safe adapter synthesis: predicate, clauses, catalogue, proposed-not-silent, dichotomy test.
- Evaluation: refusal catalogue per construct, dichotomy sweep, expressiveness study (if produced).
- Related work, limits.
Threats to validity and limits
- Documentation lags code in at least three places named above. A reader of
docs/composition-rows.mdwill see "not built" for a shipped construct. - The seam's emission bound is not widened. The forwarder must fit inside the wrapped service's own declared bound, so inserting a seam currently requires a service that already declares a wide
emission[...](testtest_a_narrow_service_bound_refuses_the_forwarder). The design note itself calls the widening a decision to be taken rather than accepted (docs/design/424-dsh-language-gaps.md:721-745) and says the cost is a weakening of transitive purity for plainfnmethods across a seamed edge. - Interposition is distinct-key only. The wrapped provider must be re-keyed in its own source; the same-key shape compiles and its body is silently discarded by the reference driver (
docs/composition-rows.md:739-748). A third party still cannot intercept an unmodified provider. - The A2A binding covers a text and inline-bytes subset, one round trip to a terminal state for the simple form, root-endpoint only;
DataPart,urifile replies, streaming, push and durable task ids are open (docs/design/439-a2a-transport-binding.md:466-700). - A remote row says nothing about the peer. There is deliberately no "verified remote" badge (
docs/composition-rows.md:295-300). - The adapter predicate reads only declarations, never bodies; defaults come only from an absence-shaped table; "
Strto empty string" is refused by design. Name-only method matching in v1. - "Total" and "never stuck" for adapters is argued from the catalogue (S1), not mechanized here. The dichotomy sweep is evidence against predicate/gate disagreement on 100 return pairs, not a proof.
- Seam runtime behaviour is checked on generated source only, and the withdrawal cascade on transport failure is not confirmed.
- Comparison with middleware, AOP and stub-generator literature is from general knowledge and marked for citation checking. No head-to-head experiment exists.
Fact-check
Claims in this draft were checked against revl main at commit 45bec431f: 33 checked, 11 corrected.
Cite this draft
@misc{revlp32,
author = {Thomas Moussajee},
title = {Seams as Synthesized Providers: Interception, Typed Remote Clients, A2A Transport and Safe Adapter Synthesis},
year = {2026},
howpublished = {\url{https://inso1337.github.io/revl/research/P32.html}},
note = {Research draft, not peer reviewed. Written against revl commit 45bec431f. Licensed CC BY 4.0.}
}
Text licensed CC BY 4.0, author Thomas Moussajee.