Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Changelog

## Unreleased — every read a solution serves is a Runnable operation, and so a tool

- **The artifact marks each read (`x-codefly-operation`).** An operation whose
policy requires only `read` is published with core's marker beside its
`x-codefly-authority-policy`, written by core from that policy
(`runnable.RouteMarker`, core#852, merged as `518714d0`): core's default bounds, the
`Summary` as the tool's description, the operationId as its name, and
`Manifest.ID` as the audience. `codefly generate runnables` derives a
Runnable from it (codefly-dev/cli#1184). Mutations, any-checked-caller
routes, streams, the MCP route and relayed routes are not marked.
- **`Operation.Exclude`** keeps a read from every agent.
- **Commit `Server.InterfaceArtifact`, never `InterfaceDocument`**, which marks
nothing (it does not know the audience): a solution committing its output
publishes no tool. `codefly generate runnables` names each such read.
- **Every defined object closes `additionalProperties`**, which core's bounded
profile requires of a fixed shape. Every consumer's committed artifact
changes on regeneration.
- **Core moves to `v0.19.7-0.20261026182446-518714d046af`**, the commit that
merged core#852, for `runnable.RouteMarker`.

## Unreleased — a child names the owner its parent is selected to

- **A child of a person's context at an installation names the
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,33 @@ ending in `/` serves its subtree and is written `/…/{path=**}`. The passthroug
routes `InterfaceArtifact` publishes beside the solution's own are any checked
caller's: see [why](#letting-the-page-call-a-consumed-module-the-passthrough).

**Every read is a Runnable operation, and so a tool.** Beside the policy of
each operation whose policy requires only `read`, `InterfaceArtifact` writes
core's `x-codefly-operation` marker — the proto3 JSON of
`codefly.runnable.v0.Operation` that core itself derives from that policy
(`runnable.RouteMarker`): recomputed on an uncertain outcome, core's default
bounds, invoked under exactly the scopes the policy requires, for the audience
this runtime's hop checks (`Manifest.ID`), presented as a read-only tool named
by the operationId and described by the `Summary`. `codefly generate runnables`
then derives a Runnable package from it, as it does from every method a module
serves behind the hop; a `GET`'s query parameters are its input. A mutation is
not marked: it is resolved by an effect receipt, read through a receipt route
this runtime does not serve, so it stays a route its own page calls. Neither is
a route any checked caller may call, a stream, the MCP route or a relayed route.
`Operation.Exclude` keeps a read from every agent, as
`codefly.runnable.v0.exclude` keeps a module's method. Every object the artifact
defines closes `additionalProperties` — a struct marshals exactly its fields
and protojson refuses an undeclared one — which is what lets a Runnable
contract describe it.

> **Commit `Server.InterfaceArtifact`, never `InterfaceDocument`.**
> `InterfaceDocument` marks nothing — the marker names the audience the serving
> runtime's hop admits, which only the `Server` knows — so a solution that
> commits its output publishes no tool, silently, however many reads it serves.
> `InterfaceDocument` stays for documenting or validating a list of operations.
> `codefly generate runnables` names each read of a document that marks no route
> at all, which is this trap's signature (codefly-dev/cli#1185).

> **Dependency pins.** `go.mod` records the exact Core, SDK and authority
> client revisions used by this runtime, including unreleased commit
> pseudo-versions. No local `replace` directive is shipped. Core owns capability
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
connectrpc.com/connect v1.21.0
github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027012935-f41cc2baa2ec
github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261027012935-f41cc2baa2ec
github.com/codefly-dev/core v0.19.7-0.20261026092055-b23dc13543b2
github.com/codefly-dev/core v0.19.7-0.20261026182446-518714d046af
github.com/codefly-dev/sdk-go v0.4.1-0.20261018052022-70dc7a546458
github.com/codefly-dev/sdk-go/workcontext v0.1.1-0.20261018052022-70dc7a546458
github.com/modelcontextprotocol/go-sdk v1.8.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-2026102701
github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027012935-f41cc2baa2ec/go.mod h1:4hRjiWuJOKzrEk8AS87pbWCbpiEmhEvQBGQaJo5Hmkg=
github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261027012935-f41cc2baa2ec h1:QhncdaWaMERAGpZthlSXjKroXNl/L8llPtBMX+e5w/A=
github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261027012935-f41cc2baa2ec/go.mod h1:NqCbvIWbkbCzSpUUYQjP8Jv+LpFnDRbVhPdAj67DHhE=
github.com/codefly-dev/core v0.19.7-0.20261026092055-b23dc13543b2 h1:I1wL0ANXhTdQuEAnxXF+s4+rKILlBu/QHILq4Skyi3I=
github.com/codefly-dev/core v0.19.7-0.20261026092055-b23dc13543b2/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A=
github.com/codefly-dev/core v0.19.7-0.20261026182446-518714d046af h1:JLTecMs4Oja+fl51chLyGw262S4yz7jzEEiFxoq7Viw=
github.com/codefly-dev/core v0.19.7-0.20261026182446-518714d046af/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A=
github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ=
github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg=
github.com/codefly-dev/sdk-go v0.4.1-0.20261018052022-70dc7a546458 h1:hi+KhotTLMOJ/PUSzVO/Axfho7yZ2UOjrCFfrLI1EKU=
Expand Down
5 changes: 4 additions & 1 deletion mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,10 @@ func (s *Server) mcpOperation() Operation {
Request: json.RawMessage{},
Response: json.RawMessage{},
Policy: s.mcp.policy,
Handler: func(context.Context, *Gateway) (any, error) { return nil, errors.New("served by the MCP transport") },
// The body is a JSON-RPC message for any of the server's tools, not
// one operation's input: each tool is its own surface.
Exclude: true,
Handler: func(context.Context, *Gateway) (any, error) { return nil, errors.New("served by the MCP transport") },
}
}

Expand Down
102 changes: 95 additions & 7 deletions operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"
"unicode"

corerunnable "github.com/codefly-dev/core/runnable"
"google.golang.org/protobuf/reflect/protoreflect"
)

Expand Down Expand Up @@ -67,6 +68,12 @@ type Operation struct {
// Policy is what a call to the route must hold, as the authority applies
// it: required on every operation the backend serves.
Policy *Policy
// Exclude keeps the route from every agent, as
// codefly.runnable.v0.exclude keeps a module's method: the artifact marks
// it as no Runnable operation even when its policy is a read. The MCP
// route and every relayed route carry it, being a protocol and another
// module's call rather than this backend's own operation.
Exclude bool
// Exactly one of Handler and RequestHandler answers the route.
Handler Handler
RequestHandler RequestHandler
Expand Down Expand Up @@ -106,6 +113,64 @@ func AnyCheckedCaller() *Policy { return &Policy{AnyCheckedCaller: true} }
// one the CLI reads route policies from.
const PolicyExtension = "x-codefly-authority-policy"

// OperationExtension marks an operation as a Runnable one, and so a tool:
// core's x-codefly-operation, whose value is the proto3 JSON of
// codefly.runnable.v0.Operation. `codefly generate runnables` derives a
// Runnable package from every operation carrying it, as it does from every
// method a module serves behind the hop.
const OperationExtension = corerunnable.OpenAPIOperationMarker

// operationMarker is the x-codefly-operation an operation publishes beside its
// policy, or nil when it is no Runnable operation. It is core's own default
// for the route, written by core from the policy this operation already
// publishes (corerunnable.RouteMarker), so the runtime invents no bound: the
// tool is named by the operationId and described by the Summary the operation
// is required to carry, and audience is the principal this runtime's hop
// checks a Work Context against.
//
// Only a read is marked. A mutation is resolved by its effect receipt, read
// through a receipt route this runtime does not serve, so marking one would
// publish a tool whose uncertain outcome nothing could resolve; it stays a
// route its own page calls. A policy any checked caller satisfies requires
// nothing to mint a call's capability from, a stream is not one finite call,
// and a relayed subtree is a path template — core derives none of them. An
// operation the owner excludes is never marked. A marked read whose payload
// core cannot carry is refused by `codefly generate runnables`, naming the
// route and why, rather than silently left unmarked here: the owner either
// fixes the shape or excludes the route.
func operationMarker(op Operation, method, route, id, audience string) (json.RawMessage, error) {
if audience == "" || op.Exclude || op.Streaming || !op.Policy.readOnly() {
return nil, nil
}
policy, err := json.Marshal(op.Policy)
if err != nil {
return nil, err
}
marker, err := corerunnable.RouteMarker(method, route, policy, audience, id, op.Summary)
if errors.Is(err, corerunnable.ErrNotDerivable) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("operation %s %s: %w", strings.ToUpper(method), route, err)
}
return marker, nil
}

// readOnly reports a policy whose every requirement is the one read action, as
// core reads one (corerunnable.Authority.ReadOnly): a call holding nothing but
// read cannot change what it reads.
func (p *Policy) readOnly() bool {
if p == nil || p.AnyCheckedCaller || len(p.Requires) == 0 {
return false
}
for _, r := range p.Requires {
if len(r.Actions) != 1 || r.Actions[0] != corerunnable.ReadOnlyScopeAction {
return false
}
}
return true
}

// validate reports a policy that is absent, both things at once, or names a
// kind without an action. The vocabulary itself is the authority's to check.
func (p *Policy) validate() error {
Expand Down Expand Up @@ -243,9 +308,20 @@ type InterfaceInfo struct {

// InterfaceDocument renders ops as a Swagger 2.0 document — the dialect the
// platform's proto-generated modules emit, so one reader handles every
// repository's artifact — indented, with a trailing newline.
// repository's artifact — indented, with a trailing newline. It marks no
// operation as a Runnable one: a marker names the audience the serving
// runtime's hop admits, which only the Server knows. A solution that commits
// this output therefore publishes no tool at all; the artifact a solution
// commits is Server.InterfaceArtifact's, and this is for documenting or
// validating a list of operations.
func InterfaceDocument(info InterfaceInfo, ops []Operation) ([]byte, error) {
doc, err := swaggerDocument(info, ops)
return interfaceDocument(info, ops, "")
}

// interfaceDocument is InterfaceDocument marking each Runnable operation for
// audience (operationMarker).
func interfaceDocument(info InterfaceInfo, ops []Operation, audience string) ([]byte, error) {
doc, err := swaggerDocument(info, ops, audience)
if err != nil {
return nil, err
}
Expand All @@ -256,11 +332,12 @@ func InterfaceDocument(info InterfaceInfo, ops []Operation) ([]byte, error) {
return append(out, '\n'), nil
}

func swaggerDocument(info InterfaceInfo, ops []Operation) (map[string]any, error) {
func swaggerDocument(info InterfaceInfo, ops []Operation, audience string) (map[string]any, error) {
definitions := map[string]any{
"Error": map[string]any{
"type": "object",
"properties": map[string]any{"error": map[string]any{"type": "string"}},
"type": "object",
"properties": map[string]any{"error": map[string]any{"type": "string"}},
"additionalProperties": false,
},
}
paths := map[string]map[string]any{}
Expand Down Expand Up @@ -299,6 +376,13 @@ func swaggerDocument(info InterfaceInfo, ops []Operation) (map[string]any, error
if op.Policy != nil {
endpoint[PolicyExtension] = op.Policy
}
marker, err := operationMarker(op, method, route, id, audience)
if err != nil {
return nil, err
}
if marker != nil {
endpoint[OperationExtension] = marker
}
parameters := []any{}
if op.Request != nil {
parameters = append(parameters, map[string]any{"name": "body", "in": "body", "required": true, "schema": valueSchema(op.Request, definitions)})
Expand Down Expand Up @@ -510,7 +594,9 @@ func structSchema(t reflect.Type, definitions map[string]any) map[string]any {
}
properties[name] = schemaFor(field.Type, definitions)
}
return map[string]any{"type": "object", "properties": properties}
// A struct marshals exactly its fields, so the object is closed: that is
// what makes it a fixed shape a Runnable contract can describe.
return map[string]any{"type": "object", "properties": properties, "additionalProperties": false}
}

// messageSchema describes a generated message as its protobuf JSON: lowerCamel
Expand All @@ -530,7 +616,9 @@ func messageSchema(md protoreflect.MessageDescriptor, definitions map[string]any
fd := fields.Get(i)
properties[fd.JSONName()] = fieldSchema(fd, definitions)
}
definitions[name] = map[string]any{"type": "object", "properties": properties}
// protojson refuses a field the message does not declare, so the
// object is closed, as a struct's is.
definitions[name] = map[string]any{"type": "object", "properties": properties, "additionalProperties": false}
}
return map[string]any{"$ref": "#/definitions/" + name}
}
Expand Down
5 changes: 4 additions & 1 deletion passthrough.go
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,9 @@ func PassthroughOperations(modules ...ConsumedModule) ([]Operation, error) {
// Any checked caller: the child issued through the binding and the
// module's own method policy gate the call (see above).
Policy: AnyCheckedCaller(),
// Another module's call: it is a tool through that module's own
// derived operation, never twice through this relay.
Exclude: true,
})
}
return ops, nil
Expand Down Expand Up @@ -608,7 +611,7 @@ func (s *Server) InterfaceArtifact(dir string, info InterfaceInfo) ([]byte, erro
op.Path = s.authorityRoute() + op.Path
ops = append(ops, op)
}
return InterfaceDocument(info, ops)
return interfaceDocument(info, ops, s.manifest.ID)
}

// ServiceVersion is the version the service manifest (service.codefly.yaml)
Expand Down
Loading
Loading