Uh oh!
There was an error while loading. Please reload this page.
refactor: collapse the Solution two-API split onto solution.Client (#295) - #296
Merged
antoinetoussaint-byte merged 3 commits intoAug 17, 2026
Conversation
) The typed solution.Client makes the operation ceiling a required, type-level argument, but the exported WithCeiling let a caller stamp a ceiling onto a raw solutionv0.SolutionClient and bypass that guarantee. Unexport WithCeiling so Client is the single canonical dispatch path: a caller holding the raw client can no longer reach any ceiling above the least-privilege default, so every mutating RPC fails closed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#295) The Client doc asserted it was "the only supported way to dispatch a Solution RPC," which overclaimed: the raw generated client can still dispatch the read-only GetSolutionInformation under the interceptor's least-privilege default ceiling, and the very next paragraph already qualified the claim to "anything beyond the read-only advertisement." Reconcile the first sentence with that reality — Client is the only path for effectful RPCs; the read stays reachable via the raw client by design — so the invariant isn't read as stronger than it is. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4 tasks
#295) Client's fail-closed guarantee hung on an invariant it could not check: that the connection was dialed with EnforcingClientInterceptor. A Client built over a plain connection stamped the ceiling onto a context nobody read and dispatched every RPC — including Package — unchecked. NewClient takes any grpc.ClientConnInterface and cannot introspect a connection's interceptor chain, so the precondition was silent. Enforce the ceiling inside each Client method, before dispatch, reusing the same policyFor/admits check and PermissionDenied shape as the interceptor. The guarantee is now intrinsic to Client regardless of how the connection was dialed; the interceptor stays as defense in depth and to gate callers that reach for the raw generated client. Client still stamps the ceiling so the interceptor, when present, admits the same call instead of defaulting it to least privilege. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 tasks
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes#295.
The four remaining tasks from #295 do not depend on this change and cannot be done in
core(they need the cross-repo CLI caller); they are carried forward in #297 so closing #295 on merge drops nothing.Summary
solution.Clientmakes the operation ceiling a required, type-level argument; the exportedWithCeilinglet any caller stamp a ceiling onto a rawsolutionv0.SolutionClientand bypass that guarantee. This unexportsWithCeilingsoClient(solution.NewClient) is the single path that can dispatch an effectful Solution RPC — a caller holding the raw generated client can only ever reach the least-privilege default ceiling (enough for the read-only advertisement, by design), so every mutating RPC fails closed.Client's ceiling guarantee intrinsic: each method checks the declared policy against the ceiling before dispatch, so it holds even on a connection whose dial did not installEnforcingClientInterceptor(whichNewClientcannot introspect). The interceptor stays as defense in depth and to gate callers that reach for the raw client.solution.Clientinstead of the removedsolution.WithCeiling.Not addressed here (tracked in #297)
The issue itself states every remaining open question "only becomes answerable once a real caller exists," and the caller — the CLI solution command group (task #1) — lives in the separate
codeflybinary repo, not incore:coreto wire.Inspect < Scaffold < Render < Publishtiers are internally coherent (guarded byTestOperationCeilingsAdmitExactlyTheirRPCs); whether a package+render deploy wants a tier belowCeilingPublishcan only be judged against the real caller's operations.Renderis annotatedREGISTRY_READ(set in solution: declare & enforce per-method effect/network policy on the Solution contract #289). Confirming or flipping it toOFFLINErequires the host executor implementation, which is not in this repo.Making
Clientcanonical now — before task #1's caller exists — is the right sequencing: there is no consumer to break, and the future caller is forced onto the guaranteed path.Test plan
go build ./...go vet ./solution/... ./agents/manager/...go test ./solution/... ./agents/manager/...TestClientRequiresCeilingPerCallcovers the canonicalClientpath (Create admitted under a scaffold ceiling, Package denied before the wire).TestClientEnforcesWithoutDialInterceptorcovers aClientover a connection with no interceptor: Create admitted, Package refused before the wire byClientitself. Verified failing against the pre-fix code.TestEnforcingClientInterceptorDefaultsToLeastPrivilegecovers the raw-client path that can no longer stamp a ceiling: read-only advertisement admitted, mutating RPC denied with a message namingsolution.Client.🤖 Generated with Claude Code