Skip to content

generate runnables: a solution's REST routes become Runnables, and so tools, as a module's methods already do #1184

Closed

Description

@antoinetoussaint-byte

What this is for

Every gRPC method a module serves behind the authority hop already becomes a
Runnable, and so a tool, with nothing hand-written: codefly generate runnables
("Derive … Runnable packages, and so tools, from a module's served methods …
Every Runnable is a tool"), from core/runnable/derive.go, which takes the
method's authority.hop.v1.policy and the message descriptors and derives the
tool's name (the snake_cased method), its description (the method's leading doc
comment), its effect and its schemas. generate runnable-bindings then prepares
one value per operation per environment, keyed MODULE__OPERATION, and Robin
opens a binding's own tool exposure as a chat tool (obin-ai/module-robin#520).

A solution's REST routes get none of this, although a solution declares
everything the derivation needs. The goal is the same sentence for both sides:
every gRPC method of a module and every REST route of a solution becomes a tool
by becoming a Runnable.

What already exists

  • core can derive a Runnable from a REST operation:
    runnable/operation_openapi.go → PackageFromOpenAPIOperation, marked with
    the vendor extension x-codefly-operation (runnable/openapi_marker.go),
    whose value is "the proto3 JSON form of codefly.runnable.v0.Operation — the
    same field names … so the marker and the method option are one schema read two
    ways rather than two schemas that drift".
  • a solution already declares the inputs. solution-runtime-go's
    operations.go, type Operation: Path, Method, a required Summary, the
    Request/Response zero values the artifact's schemas are derived from, and a
    required Policy — which is authority.hop.v1.Policy written per route and
    published as x-codefly-authority-policy. The same declaration the module
    side derives everything from.

The three gaps

  1. No command calls core's REST derivation. git grep PackageFromOpenAPIOperation / ParseOpenAPIDocument / OpenAPIOperationMarker
    over this repo's main returns nothing. generate runnables takes
    [module] and reads only the gRPC catalog and FileDescriptorSet.
  2. The artifact a solution publishes is the wrong dialect and lacks the
    marker.
    solutions/proposals/contracts/api/backend/http/openapi.json on
    obin-ai/core-solutions#451 is Swagger 2.0 (swagger, definitions,
    consumes/produces), 16 paths, and its only per-operation extension is
    x-codefly-authority-policy. Core's parser wants OpenAPI major 3 and
    #/components/schemas/.
  3. A read route cannot be an operation at all today.
    openAPIOperationMethods admits only POST and PUT, because "an operation
    applies an effect and is retried under a declared policy, which GET and
    DELETE do not describe". But the module side derives read-only operations
    happily — "a policy requiring only 'read' derives a read-only tool that is
    safe to re-run and carries no receipt" — and a read is more retry-safe over
    GET, not less. The asymmetry is what bites: in obin-ai/core-solutions'
    releases solution the tool-worthy routes are exactly the GETs (/recorded,
    /history), while its POSTs are the ones a tool must never call
    (/record, /link/begin, /chat/install).

Proposed work, by owner

  • codefly-dev/solution-runtime-go: emit the interface artifact as OpenAPI
    3.1, and write x-codefly-operation beside the x-codefly-authority-policy
    it already writes, for every operation that declares a Policy. The tool's
    description is the Summary the operation is already required to carry, and
    the execution bounds are core's own defaults, so the runtime invents no
    vocabulary.
  • codefly-dev/core: admit GET for an operation whose declared effect is
    read-only and whose policy requires only reads, so a solution's reads can be
    operations at all (gap 3). If that is refused, say so and solutions will have
    to publish their reads as POST, which changes every page's fetch.
  • this repo: let generate runnables take a solution and write the same
    contracts/runnables tree from its document, and let generate runnable-bindings prepare them keyed SOLUTION__OPERATION. Same two
    commands, no new concept.
  • obin-ai/module-robin and obin-ai/module-runtime: corrected 2026-10-26,
    after feat(runnable): a REST read over GET is an operation, and Swagger 2.0 is read as itself core#852 was written. I first wrote that nothing in Robin
    changes. That is true for a module's Connect operation and false for a
    solution's REST route
    : Robin's runnable_catalog.go:94 skips every binding
    whose call is not Connect, toolowner/operation.go always POSTs a Connect
    procedure, and module-runtime's pkg/runnable/transport.go bodiedMethods
    admits only POST/PUT/PATCH/DELETE. So a solution's read is derivable and
    preparable after the three PRs below, and still not callable until those
    two owners admit a GET over a rest call. What stays true: the bundle side
    needs nothing — tools are declared by reference ({name, contract: runnable, service}) and resolved from the composed binding at import.

Why it matters now

A solution's only tool surface today is MCP (solution-runtime-go/mcp.go,
ServeMCP), which Robin does not read: obin-ai/core-solutions' releases
solution serves read_board and component_history there, any MCP client the
host admits can call them, and its own Robin chat cannot. With this, the chat
reads the solution's own routes the same way it reads a module's methods.

Audited 2026-10-26 against cli main (e4de4a8cd), core main (89bf6c4a),
solution-runtime-go main (e53e044) and robin main (cf25e8bf).

Activity

  1. antoinetoussaint-byte commented on Oct 28, 2026

    @antoinetoussaint-byte
    ContributorAuthor

    A lazybox agent is working on this. claude (Opus 5.5) started at 2026-10-26 17:26 UTC, last renewed the claim at 2026-10-29 09:14 UTC, and the claim lapses at 2026-10-29 10:14 UTC unless it is renewed again. A claim is an assertion with an expiry, not proof that a process is alive.

    claim detail
    Lazybox-Claim-Device: effacd542b611010656e
    Lazybox-Claim-Session: 050593d5cd
    Lazybox-Claim-Workspace: github-codefly-dev-cli-1184
    Lazybox-Claim-Agent: claude
    Lazybox-Claim-Model: Opus 5.5
    Lazybox-Claim-Started: 2026-10-26T17:26:58Z
    Lazybox-Claim-Heartbeat: 2026-10-29T09:14:01Z
    Lazybox-Claim-Expires: 2026-10-29T10:14:01Z
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    workingClaimed by a lazybox agent; cleared when the session ends.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions