Skip to content

Resolve tool availability against the boundary the distribution declares - #171

Merged
KimGLee merged 1 commit into
mainfrom
fix/distribution-aware-interface-projection
Aug 26, 2026
Merged

Resolve tool availability against the boundary the distribution declares#171
KimGLee merged 1 commit into
mainfrom
fix/distribution-aware-interface-projection

Conversation

@KimGLee

@KimGLee KimGLee commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Closes #170.

What was wrong

distribution-boundary.yaml says which files never reach an adopter runtime. Nothing in the interface compile chain read it. So the closed interface policy required every adopter to supply a tool the same repository had excluded, and compile_cli_contract failed closed when the adopter correctly did not have it. This repository cannot observe that, because it holds every tool it declares.

Both declarations were internally correct, which is why neither was the thing to change. The boundary excludes a tool whose only job is copying a template tree that is also excluded. The interface policy is a closed enumeration of the call surface, and that tool genuinely has one here. What was missing was anything that read both — grep found exactly one module in Tools/ that reads the boundary at all, and it was not in this chain.

The resolver, and what it refuses to do

Tools/tool_availability.py is the single reader. Its answer is deliberately not "skip whatever is absent". Absence is evidence of two opposite things — a tool the boundary excludes, and a carried tool that failed to arrive — and a rule that cannot tell them apart converts the second into a silent pass. partition() returns three sets, and the third (unregistered-absent) stays a refusal that names the tool, the boundary and the target.

The projection target is declared, never inferred. A distribution mid-checkout would otherwise identify itself as an adopter runtime and excuse exactly the absence that needs reporting. --projection-target is required to write. With --check it falls back to the target the stored artifact records — reading a declaration, not guessing at one — and an artifact that records no target is refused rather than assigned one.

The artifact now says whose projection it is

Four bindings, each with its own remedy, each reported as itself rather than as "stale":

binding what a mismatch means
projection_target another projection, not a stale copy of this one
distribution_boundary.sha256 the boundary moved; every derived artifact is stale
included_tools compiled somewhere else; cannot stand in for a local build
excluded_tools which absences this target was permitted

One thing the issue did not ask for, and why it is here

The foreign-artifact case was detectable but never detected: no adopter runs the interface checks. run_gates preflighted compose_vocab and compose_page_contract and stopped there. Both interface artifacts now join that freshness set — which is what turns "regenerate in the target repository" from documentation into a gate an adopter actually trips.

render_interface_projection needed no change. It already consumed only the compiled contract, so requirement 6 was already met.

Acceptance

Fourteen cases, all derived from the declaration rather than naming a tool. A test that spelled the name would keep passing after the declaration changed and would be asserting its own memory. One case fails loudly if the boundary ever declares no tool module at all, so the rest cannot go quietly vacuous.

  • source-distribution includes every boundary-excluded tool, and excludes nothing.
  • A generic carried-runtime fixture — built by applying the boundary to a copy of Tools/ — compiles, renders, and answers a real MCP initialize and tools/list, with every served tool backed by an implementation.
  • Deleting a carried tool from that fixture still fails the compile, and the refusal names it.
  • A source projection cannot be compiled where a declared tool is absent: the permission belongs to the target, not to the repository.
  • Boundary drift marks the artifact stale, with the two hashes.
  • Another target's artifact reports as another projection.
  • A tampered included_tools reports as compiled elsewhere.

Verified before and after on the fixture from the issue: initialize went from -32002 to OK, with 39 tools served and no missing implementations.

Full suite 1,890 tests, no new failures. No adopter-specific case appears in the code or the tests.

The distribution boundary says which files never reach an adopter runtime.
Nothing in the interface compile chain read it, so the closed interface
policy required every adopter to supply a tool the same repository had
excluded from adopter runtimes, and the compiler failed closed when the
adopter correctly did not have it. This repository could not see the
failure, because it holds every tool it declares.

Both declarations were internally correct, which is why neither was the
thing to change. The boundary excludes a tool whose only job is copying a
template tree that is also excluded; the interface policy is a closed
enumeration of the call surface, and that tool genuinely has one here.
What was missing was anything that read both.

tool_availability is that reader, and the only one. Its answer is not
"skip whatever is absent": absence is evidence of two opposite things -- a
tool the boundary excludes, and a carried tool that failed to arrive -- and
a rule that cannot tell them apart turns the second into a silent pass. It
partitions a declared tool list into included, boundary-excluded, and
unregistered-absent, and the third stays a refusal that names the tool, the
boundary and the target.

The projection target is declared, never inferred. A distribution
mid-checkout would otherwise identify itself as an adopter runtime and
excuse exactly the absence that needs reporting, so --projection-target is
required to write. With --check it falls back to the target the stored
artifact records, which is reading a declaration rather than guessing at
one, and an artifact recording no target is refused rather than assigned
one.

The compiled contract now binds four things that let it answer whose
projection it is: the target, the boundary path and hash, and the included
and excluded tool sets. Each has its own remedy and each says so rather
than reporting "stale": another target is another projection, a changed
boundary hash makes every derived artifact stale, and a recorded tool set
that disagrees with this repository was compiled somewhere else and cannot
stand in for a local build.

That last case was detectable but never detected, because no adopter runs
the interface checks -- run_gates preflighted compose_vocab and
compose_page_contract and stopped there. Both interface artifacts now join
that freshness set, which is what turns "regenerate in the target
repository" from documentation into a gate an adopter trips.

render_interface_projection needed no change; it already consumed only the
compiled contract.

Fourteen cases, all derived from the declaration rather than naming a tool:
a test that spelled the name would keep passing after the declaration
changed and would be asserting its own memory. The generic carried-runtime
fixture is built by applying the boundary to a copy of Tools/, compiles,
renders, and answers a real MCP initialize and tools/list with every served
tool backed by an implementation. Deleting a carried tool from it still
fails the compile.

Full suite 1,890 tests, no new failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@KimGLee KimGLee added the bug Something isn't working label Aug 26, 2026
@KimGLee
KimGLee merged commit ebdc18c into main Aug 26, 2026
12 checks passed
@KimGLee
KimGLee deleted the fix/distribution-aware-interface-projection branch August 26, 2026 03:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The interface compile chain does not consume the distribution boundary, so an adopter is required to supply a tool the boundary excludes

1 participant