Skip to content

feat(runnable): a REST read over GET is an operation, and Swagger 2.0 is read as itself - #852

Merged
antoinetoussaint-byte merged 4 commits into
mainfrom
feat/runnable-openapi-reads
Oct 26, 2026
Merged

antoinetoussaint-byte merged 4 commits into
mainfrom
feat/runnable-openapi-reads

Conversation

@antoinetoussaint-byte

@antoinetoussaint-byte antoinetoussaint-byte commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

First of three PRs for codefly-dev/cli#1184 — every gRPC method of a module and every REST route of a solution becomes a tool by becoming a Runnable. The module half is already automatic; this makes a solution's routes derivable at all.

1. A REST read over GET is an operation. openAPIOperationMethods admitted only POST and PUT ("an operation applies an effect and is retried under a declared policy, which GET and DELETE do not describe"), while the module side already derives read-only operations ("a policy requiring only 'read' derives a read-only tool that is safe to re-run and carries no receipt"). A read is the safest operation to re-run, so GET is now admitted as a read only (readOnlyRoute): RECOVERY_RECOMPUTE, a read-only tool, and invoke scopes requiring nothing but read. Mutations keep their body, their receipt rules (untouched) and POST/PUT; DELETE and PATCH stay refused.

  • A GET carries no body, so its query is its input (queryPayload): one object whose members are its query parameters, each a scalar, optional exactly when the parameter is; a GET with no parameter takes {}. Arrays, objects, nullables, a body, path parameters and path templates are refused by name.
  • input_message is required by the package schema (min_len: 1). No component describes a query, so a GET records the identity the document publishes it under, its operationId; a GET without one is refused. (I chose this over relaxing the validated field, which would be a schema change.)
  • HTTPRoute.verb in proto/codefly/runnable/v0/prepared.proto admitted only POST/PUT too, so a derived GET could not be prepared. It now admits GET (Go and Python regenerated with codefly generate proto … --local and --template buf.gen.python.yaml; proto/buf.lock restored, since only make buf-dep-update moves it). VerifyPrepared holds a delivered GET binding to the same read rule, so an installer does not trust that a generator applied it.

2. Dialect: core reads Swagger 2.0. I took the "core reads what exists" option. Evidence:

  • solution-runtime-go/operations.go InterfaceDocument deliberately renders Swagger 2.0 — "the dialect the platform's proto-generated modules emit, so one reader handles every repository's artifact".
  • Core's own OpenAPICombinator writes 2.0 (this file's previous comment said so), and protoc-gen-openapiv2 output is 2.0.
  • The committed artifacts in obin-ai/core-solutions (solutions/{proposals,wiki}/contracts/api/backend/http/openapi.json on chore(deps): bump the docker-major group across 2 directories with 2 updates #451) are 2.0, and the CLI's route-policy reader (pkg/methodpolicy) already reads them as they are. Migrating to OpenAPI 3 would churn every solution's committed contract and collide with chore(deps): bump the docker-major group across 2 directories with 2 updates #451, for no change in what the document says.

ParseOpenAPIDocument now moves what Swagger spells differently — the in: body parameter, a response's schema, consumes/produces, typed parameters, #/definitions/ — into the OpenAPI 3 places before anything reads the document (fromSwagger). Every rule after that reads both alike, and TestTheTwoDialectsDeriveOneOperation holds the same operation in both dialects to a byte-identical package. Any other swagger version is still refused by name.

3. DefaultRouteOperation and RouteMarker. DefaultRouteOperation(method, path, authority, audience, name, description) is DefaultOperation's REST twin (core's default bounds, "503" as the HTTP spelling of UNAVAILABLE). RouteMarker(method, path, policyJSON, …) writes it as the x-codefly-operation marker from the x-codefly-authority-policy JSON an owner already publishes, byte-stably. That is how solution-runtime-go marks a route without inventing a bound — and without constructing core's wire messages, which its typed-boundary gates refuse in that package. A policy requiring nothing, a mutation over GET or a path template is ErrNotDerivable.

Classification

Fix at the owner: core owns the derivation, the bounded profile, the prepared-binding schema and the defaults. No stopgap.

Docs

docs/runnable.md (the two-builders table, "The REST reader": GET as a read, its query as input, both dialects), docs/runnable-binding-delivery.md (rest call verbs, the installer's GET rule).

Test plan

  • go test -race ./runnable/... ./internal/ciguard/... — green; the existing TestPackageFromOpenAPIOperationIsByteStable digest is unchanged.
  • New: runnable/operation_openapi_route_test.go — dialect drift gate, Swagger held to the same refusals, GET read derivation (/history query → input, /recorded → {}), every way a GET is refused, DefaultRouteOperation/RouteMarker (marker round-trips to the hand-written fixture's digest), prepared GET held to being a read. I removed the VerifyPrepared GET guard and watched TestAPreparedGetIsHeldToBeingARead fail, then restored it.
  • make buf-lint, make buf-breaking (no breaking change against origin/main), and the Python contract tests with CI's exact command — green.
  • golangci-lint is not in core's CI; locally one new note: goconst on the literal "object" (the projection beside it spells it the same way).

Not verified

  • The full go test ./... of core: I ran ./runnable/..., ./internal/ciguard/..., ./generated/... and go build ./....
  • Nothing executes a prepared GET yet. The two executors still refuse it: obin-ai/module-runtime pkg/runnable/transport.go bodiedMethods (POST/PUT/PATCH/DELETE), and obin-ai/module-robin runnable_catalog.go:94 skips every binding whose call is not Connect (prepared.GetCall().GetConnect() == nil), and toolowner/operation.go always POSTs a Connect procedure. Reported on feat(generate): a solution's routes become Runnables, and so tools, as a module's methods do (#1184) cli#1185; no issue filed without the owner's go-ahead.

Order

Merge this first. codefly-dev/solution-runtime-go#77 and codefly-dev/cli#1185 pin this branch's head d2cf8206, and re-pin to this PR's merge commit before they land (a squash would orphan d2cf8206).

Part of codefly-dev/cli#1184.

🤖 Generated with Claude Code

antoinetoussaint-byte and others added 4 commits October 26, 2026 13:39
… is read as itself

A solution's tool-worthy routes are its GET reads, and openAPIOperationMethods
admitted only POST and PUT. A GET is now admitted as a read only — recompute,
a read-only tool, invoke scopes requiring nothing but read — with its query
parameters as its input object and its operationId as the recorded input
identity. Mutations keep their body, receipt and POST/PUT.

ParseOpenAPIDocument reads Swagger 2.0, the dialect solution-runtime-go,
protoc-gen-openapiv2 and core's OpenAPICombinator publish, by moving its body
parameter, response schemas, consumes/produces and definitions into the
OpenAPI 3 places; the same operation in either dialect derives a
byte-identical package.

DefaultRouteOperation is DefaultOperation's REST twin: the marker an owner
writes from a route's authority policy, so it states core's defaults.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he policy JSON its owner publishes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r holds it to being a read

HTTPRoute.verb admits GET (Go and Python regenerated through the proto
companion; buf.lock untouched). VerifyPrepared refuses a GET whose policy is
not a read, the rule the derivation applies.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte merged commit 518714d into main Oct 26, 2026
10 checks passed
antoinetoussaint-byte added a commit to codefly-dev/cli that referenced this pull request Oct 27, 2026
…s a module's methods do (#1184)

`generate runnables` derives every route a module's OpenAPI contract marks
x-codefly-operation through the same location, file and index writers a
method goes through (deriveOpenAPIEndpoint / deriveRoute), one directory per
route named by its operationId. solution-runtime-go marks each read a solution
backend serves, so its GET reads become read-only Runnables whose query is
their input. `generate runnable-bindings` prepares them keyed
SOLUTION__OPERATION with a rest route; callTarget now calls an http endpoint
that publishes OpenAPI exactly as it calls a rest one. Core pinned to the
codefly-dev/core#852 head (GET operations, Swagger 2.0, RouteMarker).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
crisfiorini-oss pushed a commit to crisfiorini-oss/cli that referenced this pull request Oct 27, 2026
…s a module's methods do (codefly-dev#1184) (codefly-dev#1185)

* feat(generate): a solution's routes become Runnables, and so tools, as a module's methods do (codefly-dev#1184)

`generate runnables` derives every route a module's OpenAPI contract marks
x-codefly-operation through the same location, file and index writers a
method goes through (deriveOpenAPIEndpoint / deriveRoute), one directory per
route named by its operationId. solution-runtime-go marks each read a solution
backend serves, so its GET reads become read-only Runnables whose query is
their input. `generate runnable-bindings` prepares them keyed
SOLUTION__OPERATION with a rest route; callTarget now calls an http endpoint
that publishes OpenAPI exactly as it calls a rest one. Core pinned to the
codefly-dev/core#852 head (GET operations, Swagger 2.0, RouteMarker).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(generate): gate a solution's committed artifact end to end; name the InterfaceDocument trap

TestTheCommittedArtifactBecomesTools runs generate contracts, runnables and
runnable-bindings on the releases backend's artifact exactly as
solution-runtime-go's Server.InterfaceArtifact renders it. A document that
marks no route although its routes declare read-only policies now names each
such read as not an operation. Core pinned to 518714d0, core#852's merge
commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
antoinetoussaint-byte added a commit to gael-obin/core that referenced this pull request Oct 27, 2026
… clock (codefly-dev#802)

Bump version/info.codefly.yaml 0.19.6 → 0.19.7; version-tag.yml cuts the tag
from the merged commit once the suite is green.

Carries codefly-dev#802 (kern.bootsessionuuid BootID, which service-postgres#199 needs as
a release), codefly-dev#806, codefly-dev#820/codefly-dev#821, codefly-dev#824, codefly-dev#829, codefly-dev#830, codefly-dev#835–codefly-dev#852, codefly-dev#856. Agent contract:
protocol 1 / startup 2 unchanged; adds the optional operation contract
runtime-backend-declaration/v1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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