Repository navigation
feat(generate): a solution's routes become Runnables, and so tools, as a module's methods do (#1184) - #1185
Conversation
…t trap Core is pinned to the merge commit of core#852 rather than its PR head. README, CHANGELOG and InterfaceDocument's doc say it plainly: a solution that commits InterfaceDocument's output publishes no tool; commit Server.InterfaceArtifact's. codefly generate runnables names each read of a document that marks no route (codefly-dev/cli#1185). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…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>
… 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>
2d995ca to
1fb4420
Compare
|
Triaged against today's main ( Checked, so it is not taken on trust:
What it needs: merge On the repo's one open PR. It should not fold into #1197. The two are disjoint in responsibility — #1197 merges each composed module's The executor gap this PR names is still exactly as described, and it is what stands between these prepared bindings and a tool call in a chat. Verified today at both owners:
Neither has an owner working on it as far as I can see. |
Summary
Closes #1184 — the third of three PRs. With codefly-dev/core#852 (GET reads are operations; Swagger 2.0 is read as itself;
RouteMarker) and codefly-dev/solution-runtime-go#77 (the artifact marks every read), the same two commands now turn a solution's REST reads into Runnables, and so tools, as they already do a module's gRPC methods. No flag names a route, as none names a method.generate runnables <solution>walks eachopenapiendpoint of the module's API catalog (a solution backend'shttpendpoint, asgenerate contractsalready publishes it) and derives every route markedx-codefly-operationwith core'sPackageFromOpenAPIOperation. It goes through the same writers a method does —deriveMethod's location, refusal sorting and file/index writing are extracted intoderivation.location,.admittedand.record, which bothderiveMethodand the newderiveRoutecall — so a route and a method land in one tree, one index, one--checkand one ownership rule. A route's directory is its operationId (backend/http/Releases_Get_V1_Releases_History/), its release name the route's words (backend-http-releases-get-v1-releases-history). A marked route core refuses (a GET that would mutate, a payload outside the profile) fails the command by name and writes nothing, exactly as a method carrying the option does. The report shows originmarker.generate runnable-bindingsneeded one change:callTargetrouted onlyrestendpoints to a REST call, and a solution's backend endpoint isapi: http. Anhttpendpoint is now called the same way — still only when it publishes anopenapicontract, whichverifyPublishedContractalready requires. Keys areSOLUTION__OPERATIONby the existingRunnableBindingKey(module.Name, entry.Name).InterfaceDocumenttrap is named. A document that marks no route although its routes declare policies requiring onlyread— what solution-runtime-go'sInterfaceDocument(which marks nothing) or a runtime from before solution-runtime-go#77 produces — gets a "not an operation" notice per read namingServer.InterfaceArtifact, rather than an empty index in silence. A notice, not a refusal: every solution still on such a runtime is in exactly this state. A document that marks some reads and not others says nothing (that isOperation.Exclude).v0.19.7-0.20261026182446-518714d046af, feat(runnable): a REST read over GET is an operation, and Swagger 2.0 is read as itself core#852's merge commit onmain;pkg/conformance/matrix.json'sv0.19.7line is unchanged.Classification
Fix at the owner: the CLI owns running core's derivation over a module's published contracts and preparing bindings. No stopgap. The dialect question was settled in core#852 — core reads Swagger 2.0, the artifacts are not migrated (evidence there: solution-runtime-go renders 2.0 on purpose, core's
OpenAPICombinatorand protoc-gen-openapiv2 write 2.0, core-solutions' committed contracts are 2.0, and migrating collides with obin-ai/core-solutions#451).Docs
docs/commands.md(generate runnables: a solution's routes),docs/runnable.md(derived packages from a solution's routes,<SOLUTION>__<OPERATION>),docs/cli-reference.md(regenerated from the command help).Test plan
GOWORK=off go test ./... -count=1on the518714d0pin: green exceptpkg/gateway/dockerexec, whose Docker daemon went down locally when the disk filled (it passed in the earlier full run ond2cf8206, whose tree is identical);cmd/run's 30 msTestANativeDeliveryAtTheDeadline…failed once under load and passes alone (×5) and as a package.golangci-lint run ./cmd/generate/...— 0 issues.cmd/generate/runnables_solution_test.go: a releases-shaped Swagger 2.0 contract marked by core'sRouteMarker→ exactly the two GET reads derive (tree, index row, package verifies, query as input,RECOVERY_RECOMPUTE, audience,"503", read-only tool described by the Summary), idempotent; a marked GET that would mutate fails the command naming the route and writes nothing;runnable-bindingsprepares exactly two values keyedRELEASES__…, each aGETREST call at the resolved local address with a read-only tool.TestTheCommittedArtifactBecomesTools).cmd/generate/testdata/solution-runnables/api.swagger.jsonis the releases backend's artifact exactly asServer.InterfaceArtifactrenders it on solution-runtime-go#77 (every operation the releases solution serves with the policy it declares on the authority runtime, plus its MCP server; unedited). The test commits it where a backend does (services/backend/openapi/api.swagger.json) and runs the whole chain:generate contractspublishes it byte for byte ascontracts/api/backend/http/openapi.json(vendor extensions included),generate runnablesderives exactlyGET /v1/releases/historyandGET /v1/releases/recordedfrom what that wrote, andgenerate runnable-bindingsprepares exactlyRELEASES__BACKEND_HTTP_RELEASES_GET_V1_RELEASES_HISTORYand…_RECORDED, each a read-onlyGET. It is a snapshot of the shape; solution-runtime-go's ownTestTheArtifactMarksEveryReadAsARunnableOperationholdsInterfaceArtifactitself to deriving in core.TestAnArtifactThatMarksNothingNamesItsReads: the same artifact with its markers stripped derives nothing and names both reads (and no write). I disabled the notice and watched it fail, then restored it.End to end against the releases solution (acceptance)
In a scratch copy of obin-ai/core-solutions
main(6edcb4b7)solutions/releases: its real response types and its realoperations()list, each given the policy it would declare on the authority runtime (readsrecords.records:read;/recordread+write;/link/*datasource;/chat/installrobin.intents:import,activate;/status,/chatany checked caller;/releasesdatasource.sources:invoke), rendered byServer.InterfaceArtifacton solution-runtime-go#77, then thecodeflybinary built from this branch:/record,/link/begin,/link/complete,/chat/installand the other GETs derive nothing. Deviations of the scratch copy frommain, all because releases is not on the authority runtime yet: a minimalworkspace.codefly.yaml(no platform-core import), the backend'sservice-dependenciesremoved, aninterface:block forbackend/httpadded (as proposals declares on #451), andcontractsadded toartifact-roots.The executor gap: what each owner must admit (not opened, no issues filed)
Nothing calls a prepared REST binding yet, so these tools are prepared but unreachable from a chat. Two owners each need one change. The encoding is core's (docs/runnable.md, "The REST reader";
HTTPRoute.verbinprepared.proto): aGETcarries the input object as its query, each member one parameter of that name, a string as written, a number or a boolean in its JSON spelling, no body.POST/PUTsend the plain JSON body.corerunnable.VerifyPreparedalready guarantees a deliveredGETis a read (recompute, read-only tool,read-only invoke scopes), so neither executor needs a receipt route for it. The solution side already accepts the call: solution-runtime-go reads the Work Context fromx-codefly-work-context(the header core's served seam names) and checks it for itsManifest.IDaudience against the route"GET /v1/releases/history", which is the prepared operation's spelling.cf25e8bf).services/chat/code/runnable_catalog.go:94must stop skipping a binding whose call isrest(prepared.GetCall().GetConnect() == nil { continue }), or a solution's tool is dropped silently.internal/toolowner/operation.goNewOperationmust accept aPreparedCall_Rest(today it reads onlyGetConnect().GetProcedure()and refuses anything not starting with/).sendmust issue the route's own verb: for aGET, the input as the query and no body. It must not send the Connect protocol header, and must read the owner's answer as plain JSON with outcomes by HTTP status (retryable_codesare statuses such as"503"), not as a Connect error document.c.call, which should become the operation spelling.GET+RECOVERY_RECOMPUTEonly.85942d79).modules/runtime/services/tasks/code/pkg/runnable/transport.go:routes()refuses any verb outsidebodiedMethods(POST/PUT/PATCH/DELETE). It must admitGETfor a recomputed binding.receiptURL it computes (path + ReceiptSuffix) is never read for a recompute.Also outstanding, outside this PR: the releases solution must move onto the authority runtime (v0.0.15 → a release with solution-runtime-go#77) and declare its policies, and the regeneration of core-solutions' committed artifacts waits on obin-ai/core-solutions#451.
🤖 Generated with Claude Code