Skip to content

fix(gitops): a host receives its module's table less the services that stand in for it; a gateway route carries its proto method's policy - #1189

Merged
antoinetoussaint-byte merged 1 commit into
mainfrom
fix/host-method-table
Oct 27, 2026
Merged

antoinetoussaint-byte merged 1 commit into
mainfrom
fix/host-method-table

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor

What this fixes

codefly-dev/authority#33 ships modules/authority/module.package.codefly.yaml. A saas-hosted run needs that package to admit the authority module, because its hop has been module-identity since authority#28. Once the package exists, the CLI's strict permission preflight refuses the module on a saas host. That preflight lives in pkg/gitops/permission_validation.go and runs on deploy gitops, run cluster, deploy module/service and ci deploy. The refusal has two causes:

  1. Services that implement host interfaces were counted as methods the host fronts. The authority's local service implements codefly.dev/authority, composition, installation and session. Its 32 methods (27 gRPC procedures, 5 HTTP routes) went into the table the manifest carries to saas/accounts. A saas host never runs local: the workspace binds those interfaces to accounts. A call to local is the host's to answer, never one the host decides for the authority. local has to stay in the module interface, because its implements: list is how it acts as host when there is no saas.
  2. A proxy face published a second copy of a route, without a policy. The hop's http endpoint publishes its service's REST-gateway OpenAPI document (openapi/api.swagger.json). That document is the proto rendered for HTTP, and it has no place for x-codefly-authority-policy. Marking HopService.Version with any_checked_caller (the console's pattern) then fails with conflicting policies for GET /version: the proto's route is marked and the gateway's copy is not.

The change

  • gitops.HostMethodPolicies: the table a module's manifest carries to the environment's host. It is the package's table without the services that stand in for that host. A stand-in is a service with both properties:

    • an exported interface endpoint of it implements a Codefly interface that the host.delivery service also implements (version ignored);
    • it does not declare module-identity.

    The preflight holds this table, the native composition delivery sends it (cmd/run/launcher.go), and a caller's binding resolves against it (moduleAuthority callees). So the three uses cannot disagree.

    • A module-identity service is always in the table. Every service of a module that implements nothing the host implements is too, so those modules are unchanged.
    • require_method_policies: true still holds the whole package, stand-ins included, because that is what a module that sets it asked for.
    • A host the workspace does not compose cannot be read, so nothing is set aside toward it. That keeps the existing strictest-host rule.
  • Conflicts are refused per table (ServedMethodPolicies.Table), not across services that never meet in one. local and the hop both serve GET /version, and only the hop is in the saas host's table. Two services in one table that serve one route with different policies are still refused.

  • A gateway route carries its proto method's policy. I picked deriving the policy over not publishing the gateway document. The CLI's contract model already makes the proto the one place a gRPC service declares a route's policy: methodpolicy.contractMethods lists each google.api.http route under the method's policy. The OpenAPI document is a rendering of that proto. Rule, within one service:

    • an OpenAPI route that declares nothing, and that one of the service's proto methods binds, takes that method's policy;
    • an OpenAPI route that declares a policy of its own is a declaration, and is still refused when it differs from the proto's;
    • a route no proto method binds is listed as the document declares it.

    Not publishing the document would also drop the hop's http face from the package catalog, and with it the face's reach and route listing. It would also do nothing for a rest endpoint, which publishes the same gateway document.

Fix or hack: fix, at the place that owns the behaviour: the reader of the method table in pkg/gitops.
Reused: HostImplements' reading of the host's interface endpoints, isModuleNotFound, the existing methodpolicy.Contract reader and ValidateMethodPolicies. Not added: no new configuration key, no per-module exception, and no change to the authority's declarations.

Why not "identity services only"

On a saas host, the identity-only rule and this one give the same result for the authority: the hop's two methods. Identity-only is too narrow in general, though. A service that only receives calls needs no module identity (identity means minting the module's own Work Context), yet its published policies are what a caller's binding resolves against. module-runtime's qualification graph has one: owners/rest-owner is not module-identity, and its marked routes are what the runtime worker's binding resolves against. Identity-only would drop those policies from the delivered table and from callee resolution. This rule sets aside only services that implement the host's own interfaces.

Evidence

Strict-check harness: gitops.CheckCompositionPermissions(ctx, ws, env("local"), strict=true), the call renderComposition makes. It ran on obin-ai/platform-core at PR #114's current head 29e8d280 (newer than 552a055e), host saas/accounts, env local, pins materialized with codefly ci plan --all. The authority module was overridden through codefly.local.yaml resolve.authority.path.

authority CLI main 89c76fde this branch
#33 c5273b74 + Version marked any_checked_caller (local worktree, codefly generate contracts authority) refused: module package codefly/authority: conflicting policies for GET /version passes
#33 c5273b74 unmodified refused: 34 methods (every local method, plus the hop's two) refused, only the delivered identity service: /api.HopService/Version declares no permission policy, GET /version declares no permission policy
main 49101ae1 passes passes

Every other module of that composition is unchanged. Each one's contract-publishing services are all module-identity, except saas, which is the host's own module.

Unit tests, each confirmed to fail with its rule removed:

  • TestAServiceStandingInForTheHostIsNotInTheTableItsManifestCarries
    • a stand-in's unmarked methods, and a route it declares differently from the module's own service, pass toward a composition host;
    • MethodPolicies (the whole package) still reports the conflict;
    • the module's own identity service is still refused by name when unmarked;
    • a stand-in that declares module-identity is refused by name;
    • a service implementing an interface the host does not implement is not set aside.
  • TestAGatewayRouteCarriesThePolicyOfTheProtoMethodItRenders
    • the gateway copy takes the proto's policy whichever contract is listed first;
    • a route the proto does not bind stays unmarked;
    • a differing declaration is refused, and an identical one is accepted.

Local gates: go vet and go test -race -count=1 on ./pkg/gitops, ./cmd/run and ./cmd/generate pass. golangci-lint run ./... (local v2.15.0) is clean on the changed packages. Its three remaining findings are errcheck in cmd/terminal.go, which main also has under v2.15.0. CI pins v2.14.0.

Not verified

  • A cell render (codefly deploy gitops) or codefly run cluster end to end. The harness calls the same function those paths call, but no cluster ran.
  • A native codefly run delivering the narrowed table to accounts. The delivery now reads HostMethodPolicies, and cmd/run tests pass, but no saas-hosted run was started here.
  • The authority playground (host authority/local) only partly. Its pins are not materialized here, so the strict check stops on those modules. It reports no authority-module error on this branch, while main reports conflicting policies for GET /version.
  • A module deployed from its own repository to a cell whose host the workspace does not compose: nothing is set aside there (strictest host). The authority deployed that way would still be held on local.

What codefly-dev/authority#33 must add once this merges

Mark the hop's one method in modules/authority/services/hop/proto/api.proto with option (authority.hop.v1.policy) = {any_checked_caller: true};. That needs authority/hop/v1/policy.proto vendored next to it, as the console does (services/api/proto/authority/hop/v1/policy.proto plus its drift test). Then re-run codefly generate contracts authority, which rewrites the hop's contract.binpb, its proto snapshot, the catalog and the manifest digest. Nothing on local and nothing in the hop's OpenAPI document. With that, the strict preflight passes on platform-core with the saas host.

🤖 Generated with Claude Code

…t stand in for it, and a gateway route carries its proto method's policy

The strict permission preflight (render, run cluster, deploy module/service,
ci deploy) refused the authority module on a saas host once it ships a
package (codefly-dev/authority#33): its local service, which implements the
host interfaces and never runs on a saas host, put 32 unmarked methods into
the table the manifest carries; and marking the hop's Version made it worse,
"conflicting policies for GET /version", because the hop's http face publishes
the REST gateway's OpenAPI rendering of the same route, which cannot carry
the policy option.

- HostMethodPolicies is the table a module's manifest carries to env's host:
  the package's table less the services that stand in for that host (an
  exported endpoint implementing a Codefly interface the host.delivery service
  implements, and no module-identity). The preflight holds it, the native
  composition delivery sends it, and a caller's binding resolves against it.
  A module-identity service is always in it; require_method_policies still
  holds the whole package.
- Conflicts are refused per table (ServedMethodPolicies.Table), not across
  services that never meet in one.
- Within a service, an OpenAPI route that declares nothing and that one of
  the service's proto methods binds (google.api.http) carries that method's
  policy; a differing declaration is still refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
antoinetoussaint-byte added a commit to codefly-dev/authority that referenced this pull request Oct 27, 2026
tools/packagegate now reads the published contracts the way the CLI's
delivery reads them (codefly-dev/cli#1189's HostMethodPolicies): each
served procedure, its google.api.http routes and each OpenAPI operation,
a gateway route inheriting the policy of the proto method that binds it,
and a service that stands in for a host (no module identity, an endpoint
implementing a Codefly interface) left out. Any other method with no
policy is refused by name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit c791071 Oct 27, 2026
14 checks passed
Sign up for free to 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