Skip to content

Boundary register: verify cross-boundary identifiers against records - #24

Merged
jmc-wander merged 4 commits into
mainfrom
feat/boundary-register
Oct 14, 2026
Merged

jmc-wander merged 4 commits into
mainfrom
feat/boundary-register

Conversation

@jmc-wander

@jmc-wander jmc-wander commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Why

The same class of defect keeps reaching production: identifiers that two parties must agree on, which nobody checked. Examples:

  • a gateway queried tables its database role was never granted
  • an alert policy filtered on uptime_url while every uptime check emitted cloud_run_revision
  • a readiness probe called services without a credential, every call was refused, and each one still counted as "up"
  • log formatters wrote level while the platform reads only severity
  • an uptime check targeted a service that does not exist
  • a project-wide invoker grant broke a per-service isolation claim

Before this change, Pact had no contract field for these identifiers, and validate_cross_component_interfaces only compared names the LLM wrote and only warned. The new rule is that an identifier is never assumed. It must cite a record that can be checked.

What

  • src/pact/boundary_register.py: the boundary-register/1 format, shared with the factory and Constrain (same field names). It has lenient Pydantic models and a mechanical verifier with no LLM:
    • exact, case-sensitive equal relations, with a "near-miss" note when values differ only by case or separators
    • absent relations for isolation: the subject must not appear as a token in any denies-side record
    • every side needs at least one record; a side with none is UNVERIFIED and is an error
    • file records: sha256 of the whole file, and the cited line must contain the value as a token (mea-auth does not match mea-auth-runtime)
    • observation records: sha256, plus the command and an RFC3339 observed_at
    • placeholder values are refused (TODO, TBD, <...>, ???, unknown, assumed)
    • record paths are confined to the project root: absolute paths, .. and symlinks that escape are refused
    • a stale digest is an error that names the file and says to refresh the register from the source
  • contracts.py: validate_boundary_register() and find_boundary_usages(), wired into validate_all_contracts() as hard errors. That means pact validate, the decomposer gate and MCP pact_validate all enforce it.
    • A boundary that is used must be registered. Usage means an external:<system> in dependencies, requires or side-effect targets; a network_call side effect; or a database_*, http_* or external_* data-access side effect.
    • The error names the exact usage that triggered it.
    • Projects with no cross-boundary usage pass unchanged.
  • ComponentContract.boundary_identifiers: per-contract entries with the same shape. Entry ids must be unique across the register file and all contracts.
  • CLI: new pact boundary verify [project], which prints the entry id, side and reason for each failure and exits 1 on any error. pact validate now also exits 1 when the gate fails.
  • Prompts: interview, shaper and contract author share BOUNDARY_RULE_PROMPT: cite the record or raise an open question, never guess. The interview step asks a question for each identifier that has no record. The contract author receives the verified register to copy from.
  • Implementers: registered literals appear in tier 1 of the handoff brief (never trimmed by compression) and in render_dependency_map.
  • Docs: a section in README and CLAUDE.md, and a new REVIEW.md.

Tests

  • tests/test_boundary_register.py: 63 tests. Mutation checks confirm each group goes red when its feature is removed (gate wiring, token boundary, stale digest, absence check, rendering).
  • Full suite: 2465 passed with SIGNET_SESSION unset.
  • One test, tests/test_agent_runtime.py::...test_fallback_session_id_is_stable_and_path_scoped, fails whenever SIGNET_SESSION is set in the environment. It fails the same way on main (1 failed, 2401 passed), so it is not caused by this change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Sy7XemWaqWysBtf6KHX7T7


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

jmc-wander and others added 2 commits October 14, 2026 15:02
…ecords

Mismatched identifiers across boundaries (a role never granted the table it
queries, an alert filtering on a resource type nothing emits, a log formatter
writing "level" where the platform reads "severity", an uptime check on a
service name that does not exist, a project-wide grant breaking an isolation
claim) are defects no component-local test catches. Pact now refuses to
assume such values.

- src/pact/boundary_register.py: boundary-register/1 schema (shared with the
  factory and Constrain), mechanical verifier (token match, sha256 digest,
  placeholder refusal, root-confined paths, equal vs absent relations,
  near-miss detection), renderers, and the shared prompt rule.
- contracts.py: validate_boundary_register() + find_boundary_usages(), wired
  into validate_all_contracts() as hard errors; a used boundary (external:
  names, network_call, database_/http_/external_ side effects) must be
  registered. Projects without cross-boundary usage are unaffected.
- ComponentContract.boundary_identifiers carries the same entry shape.
- cli/mcp/decomposer pass the project root to the gate; pact validate exits 1
  on failure; new `pact boundary verify [project]`.
- Interview, shaper and contract author prompts: cite the record or raise an
  open question; the contract author sees the verified register.
- Handoff briefs render registered literals in tier 1 (never compressed);
  render_dependency_map includes them.
- tests/test_boundary_register.py (63 tests); README, CLAUDE.md, REVIEW.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`pact boundary cite --kind file|observation --path <rel> --value <literal>
[--line N] [--command "<cmd>"] [project]` reads the file under the project
root with the verifier's path safety, hashes the whole file, finds (or
confirms) the line containing the value as a token, requires --command and
stamps observed_at (UTC RFC3339) for observations, and prints the record as
JSON. It refuses with exit 1 when the literal is not where the record would
claim, so every produced record round-trips through the verifier.

The shared prompt rule now says records come from `pact boundary cite`,
never typed. README/CLAUDE.md updated; 7 new tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@adaptcom adaptcom Bot 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.

Confidence Score: 3/5

Summary

Adds a mechanical boundary-register gate (file+contract entries, sha256-pinned records) as hard errors in validate_all_contracts, CLI and MCP. Code is sound and well-tested; coverage is per-component not per-usage, and a prompt instruction conflicts with the duplicate-id rule.

Important Files Changed

File Overview
src/pact/boundary_register.py New mechanical verifier and models; placeholder, line-count and absent-mode edge cases
src/pact/contracts.py Hard-error gate wired into validate_all_contracts; coverage per-component, regex wider than docs
src/pact/schemas.py Adds boundary_identifiers list to ComponentContract with default
src/pact/cli.py New pact boundary verify; pact validate now exits 1 on failure
src/pact/decomposer.py Interview prompt rule; register rendered to contract author; gate receives project_dir
src/pact/mcp_server.py pact_validate passes project_dir to gate
src/pact/implementer.py Loads register entries into every handoff path
src/pact/interface_stub.py Boundary literals rendered in tier 1 and dependency map
src/pact/agents/code_author.py Threads boundary_entries into handoff brief
src/pact/agents/contract_author.py Boundary rule in system prompt; copy-verbatim instruction conflicts with duplicate-id gate
src/pact/agents/shaper.py Appends boundary rule to shaper system prompt
tests/test_boundary_register.py 63 tests; none for per-usage coverage or denies-value semantics
README.md Boundary register section; under-states side-effect prefixes gated
CLAUDE.md Command and architecture notes; same prefix under-statement
REVIEW.md New automated-review guidance for the repo

Findings

  • Gate now fails every existing project declaring database_/external_ side effects, after full decomposition LLM spend (scheduler state.fail); add CHANGELOG/migration note and a record sha256 helper.

Reviewed 1ebec55...17f4825 · ↻ Re-run review · View in Adapt

Comment thread src/pact/contracts.py
covered = any(cid in ps and external in ps for ps in party_sets)
need = f"an entry with sides for both '{cid}' and '{external}'"
else:
covered = any(cid in ps and len(ps) > 1 for ps in party_sets)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-external usages are covered by any entry where cid has a counterparty; one log-field entry satisfies database_read, network_call and storage_write; match coverage per usage.

with rationale if domains is non-empty"""
with rationale if domains is non-empty
- boundary_identifiers: for every identifier this component shares with another
party (other components or external:<system>), add a boundary-register/1 entry

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Author is told to copy register entries verbatim, but reusing an entry id in boundary_identifiers trips the duplicate-id hard error; instruct 'reference, do not re-add' or tolerate identical copies.

Comment thread src/pact/contracts.py
# cite a record that still verifies. Mechanical; no LLM; errors, never warnings.

# DataAccessSideEffect.type prefixes that cross a process/system boundary.
_BOUNDARY_SIDE_EFFECT_RE = re.compile(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Regex also gates db_, api_, auth_, storage_, secret_, queue_, iam_ prefixes while README and CLAUDE.md promise only database_/http_/external_; align docs or narrow the regex.

Comment thread src/pact/boundary_register.py Outdated

# A value nobody can cite. "unknown"/"assumed"/"placeholder" are refused as the
# whole value; TODO/TBD/FIXME as a word anywhere; <...> and ??? anywhere.
_PLACEHOLDER_WHOLE_RE = re.compile(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Placeholder rules refuse legitimate literals: whole-value NA/n/a (region enums) and \bTODO|TBD\b inside todo-service or tbd-queue, plus List; anchor the patterns or allow-list.

if kind == "file":
if line_no is None:
return errors
lines = text.split("\n")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

text.split('\n') counts a trailing newline as a line: a 2-line file reports '3 lines' and line 3 passes absent-mode bounds; use splitlines().

errors.extend(_verify_record(
rec, where=where, value=side["value"], files=files, mode="present",
))
elif role == "denies" and isinstance(subject, str) and subject:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Denies side's own value is never checked and absence is tested over the whole record, so a policy dump where the subject holds any other role fails; scope to that value.

if kind == "file":
line = rec.get("line")
if isinstance(line, bool) or not isinstance(line, int) or line < 1:
errors.append(f"{where}: file record needs 'line' as a 1-based integer (got {line!r})")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Denies-side file records must supply a line that is never read in absent mode; drop the requirement there or document what line to cite.

sides: list[BoundarySide] = []


class BoundaryRegister(BaseModel):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

BoundaryRegister model is never used; load_register parses the mapping by hand. Use it for parsing or delete it.

Comment thread src/pact/contracts.py

# Verify all entries together so ids are unique across file and contracts.
all_entries = list(file_entries)
for _, entries in contract_entries:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Contract origin cid is discarded, so errors for embedded entries never name the contract and duplicates always say 'also defined in register'; pass origin per contract.

jmc-wander and others added 2 commits October 14, 2026 15:22
…ry/Constrain

- Token match: '.' and '/' that occur in the value act as joiners, so a
  joiner followed by an identifier character continues the token
  (foo.bar is not in foo.bar.baz; /health is not in /health/ready), while
  an undotted name still matches after an attribute dot (entry.severity).
- Placeholder: whole value todo|tbd|tbc|assumed|placeholder|changeme|xxx+|?+
  (case-insensitive, trimmed) or any <...> angle placeholder; unknown, none,
  null and n/a are real enum members, not placeholders.
- Prompt rule: a denies side's value names the policy/resource whose record
  must exclude the subject.
- Shared test vectors added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jmc-wander
jmc-wander merged commit dc84fb5 into main Oct 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant