Skip to content

feat: enforce per-method effect/network policy on the Solution contract (#289) - #292

Merged
antoinetoussaint-byte merged 3 commits into
issue-287-new-agent-kind-codefly-solution-registry-entry-protofrom
issue-289-solution-declare-enforce-per-method-effect-network-policy
Aug 16, 2026
Merged

feat: enforce per-method effect/network policy on the Solution contract (#289)#292
antoinetoussaint-byte merged 3 commits into
issue-287-new-agent-kind-codefly-solution-registry-entry-protofrom
issue-289-solution-declare-enforce-per-method-effect-network-policy

Conversation

@antoinetoussaint-byte

@antoinetoussaint-byteantoinetoussaint-byte commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Closes#289.

Stacked on #288 (adds the Solution contract); base this PR on issue-287-... until #288 merges, then it retargets main.

Summary

  • The Solution RPCs (Package pushes an OCI artifact, Render/Create/Update mutate the filesystem) previously documented their side effects in comments only — the decorative-annotation smell New agent kind: codefly:solution — registry entry, proto enum, manifest schema, proto service #287 deliberately avoided. This binds a machine-readable ceiling to each RPC and enforces it at the host's dispatch boundary.
  • Adds a solution_method_policy extension mirroring provider_method_policy, with solution-specific vocabularies (SolutionNetworkMode, SolutionEffect) ordered so a host ceiling admits every mode/effect at or below it. Each RPC is annotated with its true network reach and state effect.
  • Adds the host-side dispatch gate and wires it in: solution.EnforcingClientInterceptor is installed in agents/manager.loader's grpc.NewClient dial options — the single factory for every agent connection. It reads each outgoing Solution RPC's declared policy and the ceiling stamped on the call context (solution.WithCeiling), and fails the call closed (PermissionDenied) before it crosses the wire when the declared ceiling exceeds the admitted one, or when no ceiling is present. It no-ops for every non-Solution method, so it is safe on all existing agent connections.

Ceiling is per-call, not per-dial

AgentConn.GRPCConn hands out one long-lived connection reused across operations, so the admitted ceiling belongs to the operation (the call), not the transport (the dial). The interceptor therefore reads the ceiling from the call context; a Solution RPC issued without one is refused.

What this gate does and does not do

The gate constrains what the host chooses to invoke — it does not, and under this contract cannot, constrain what a solution executor actually does inside a handler. Unlike provider.proto (where ProviderHost brokers the provider's side effects and the host enforces at the point of effect), the Solution contract has no host-brokered callback path, so a plugin's real filesystem/registry writes are unmediated. Enforcing declared effects against plugin behavior would require a broker this contract does not define.

Test plan

  • go build ./...
  • go test -race ./solution/... — contract test asserts every RPC carries the expected policy; TestSolutionContractIsUnaryOnly guards the invariant the unary-only gate relies on; TestSolutionMethodPolicyAxesAreCoherent asserts registry-network ⟺ registry-effect so an incoherent annotation (e.g. push-while-offline) fails the build; admission unit tests cover both axes and every fail-closed path; a real bufconn gRPC server verifies an over-ceiling Package and a no-ceiling Create are denied before reaching the server while an at-ceiling Create is admitted; a pass-through test confirms non-Solution methods dispatch with no ceiling.
  • go test ./agents/manager/ — the dial-path change leaves existing agent loading green (interceptor no-ops for non-Solution methods).
  • go test ./provider/ — provider contract unaffected.
  • codefly generate proto --local reproduces the committed generated code. Verified empirically that the committed solution_grpc.pb.go and a fresh regen differ only in the protoc-gen-go-grpc header line (v1.6.1 committed vs v1.6.2 locally installed) — bodies are byte-identical — so the header is reverted to match feat: add codefly:solution agent kind (#287) #288's baseline rather than churn 14 unrelated _grpc.pb.go files. The go-grpc version pin lives in the codefly binary, not this repo.

…ct (#289)
Bind a solution_method_policy extension (network reach + state effect
ceiling) to every Solution RPC, mirroring provider_method_policy, and add
the host-side enforcement point that reads and enforces it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…only invariant
Review found the policy control was framed as effect enforcement against the
plugin, when it only gates whether the host dispatches a call. The Solution
contract has no host-brokered callback path (unlike provider's ProviderHost),
so a plugin's real filesystem/registry writes are unmediated — the gate cannot
constrain them. Reword the package and interceptor docs to say what the gate
does and does not do, and name the #290 wiring as its integration point so it
isn't silently left uninstalled.
The interceptor is unary-only; a future streaming Solution RPC would dispatch
unchecked. Add TestSolutionContractIsUnaryOnly so adding one fails the build
until a stream gate exists, instead of silently bypassing enforcement.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… per-call ceiling
Review pushback was right: the previous refutations were dodges. Fixing the
real causes.
Finding 2 (no consumer): the host's single agent-connection factory is
agents/manager.loader (grpc.NewClient), and the connection it returns is
long-lived and reused across operations (AgentConn.GRPCConn). The interceptor
was built in a vacuum against neither fact. A ceiling baked in at dial time is
therefore the wrong shape — the ceiling belongs to the operation, which varies
per call over one shared connection. Carry it in the call context
(WithCeiling/CeilingFrom), install EnforcingClientInterceptor in the loader dial
options so it is genuinely wired (no-ops for non-Solution methods, safe on every
agent), and fail closed when a Solution RPC carries no ceiling.
Finding 4 (correlated axes): keeping two axes is only honest if they cannot go
incoherent. Add TestSolutionMethodPolicyAxesAreCoherent asserting registry
network iff registry effect, so an annotation like {OFFLINE, REGISTRY_WRITE}
(push while offline) fails the build instead of being silently accepted by the
ordered ceiling checks.
Finding 5 (reproducibility): verified empirically that committed v1.6.1 and
freshly regenerated v1.6.2 differ only in the header line — bodies byte-
identical — so matching #288's baseline is correct, not a Frankenstein file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte merged commit ca6e289 into issue-287-new-agent-kind-codefly-solution-registry-entry-protoAug 16, 2026
Sign up for freeto 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

@antoinetoussaint-byte