Repository navigation
feat(list): runnables carry the tool they are offered as, and each one's grant - #1207
Merged
antoinetoussaint-byte merged 2 commits intoOct 27, 2026
Merged
Conversation
…e's grant Picking which tools a solution gets, and noticing that a module version offers a new one, had all its parts and no way to read them: the catalog is contracts/runnables/index.json (which arrives inside a third-party module package, so a released module ships its own tool list), the answer is runnable-scope-selections keyed MODULE__OPERATION, and the judgement is SelectRunnableBindings. Nothing joined them, so "what tools does this module offer and which has this environment granted?" meant reading generated JSON by hand, and a new operation in a new module version was invisible. `codefly list runnables` now carries both halves of that question. - The owner's exposure, repeated and never completed: the model-facing name and the explicit effect. An operation with no exposure is listed without one — absent means not exposed, an unstated effect is not read-only — and `--tools` keeps only the exposed ones, which is what a composition can grant. - With `--env`, each derived operation carries the configuration key its authority is answered under and what that environment answered: `selected` (a binding is prepared), `declined` (answered [], granted nothing), or `needs-scopes` (its owner asks for scope slots nobody answered, which `generate runnable-bindings` refuses by name). `needs-scopes` is the piece that was missing: a module version that adds an operation adds a row nothing answers, so the new tool shows up as work to do rather than as a silent omission, and the listing names the key the grant is written under. No new state and no second storage format — the judgement is the generator's own rule, read through the environment the workspace already declares. An environment the workspace does not declare is an error rather than a listing whose grant column is quietly absent. docs/commands.md gains the flags and the grant vocabulary; the CLI reference is regenerated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…swer exists Review r2 found the GRANT column agreeing with a generator whose rules it only partly implemented. Three shapes of one defect, all of them reporting a grant on configurations `generate runnable-bindings` refuses — wrong in the one place an operator looks to check their work, which is worse than having no column. **A present but insufficient selection read `selected`.** The state came from `len(selections) == 0`, so any non-empty answer was a grant. The real judgement is core's ResolveScopeSlots, which refuses a slot left unselected, a slot named that the operation does not declare, a selection with no invoke scope, an invoke scope missing an action the owner requires, and a missing lookup scope where the slot asks for one. An operation declaring `collection` and `source` in an environment answering only `collection` reported as granted while the run failed. The listing now builds the operation's spec and resolves the selection exactly as the generator does, and reports `invalid` carrying core's own reason. **An illegal decline read `declined`.** generate's declinableOperation refuses `[]` against an operation with no required scope slot — "there is nothing to decline; remove the entry" — so that an empty list typed against the wrong key cannot hide. That rule is now applied where the state is computed. **An answer naming no operation was invisible.** The listing walked the catalogue only, so a mistyped key — the likeliest mistake with a hand-typed key — produced an answer to no question while its operation still read as unanswered, and nothing pointed at the typo. Those keys are now reported beside the rows, as `unanswered_keys` in --json. The shared vocabulary moved to where both callers can hold one spelling: pkg/runnables now owns BindingKey, Operation.Spec (was cmd/generate's operationSpec, which took nothing from the endpoint descriptor but the method spelling) and GrantOf, with cmd/generate.RunnableBindingKey a thin alias for its own call sites. cmd/list no longer imports cmd/generate — a sibling command package was the wrong home for a spelling the listing must match exactly, and linking it ran five unrelated init()s. Two output contracts restored: - The tool columns appear only with --tools or --env, so the default table is byte-identical to before and anything parsing it keeps working. - `tool` and `grant` live on the listing's own row type instead of the shared Identity, so `show runnable --json` no longer states the same exposure twice (once promoted, once under `operation`). show is untouched again. - With --env, --json emits an object (runnables, unanswered_keys); without it, the array it has always emitted. Four new tests cover what the first shape got wrong: a partially answered operation, a selection naming an undeclared slot, a decline of an operation with nothing to decline, and an answer that names no operation. The first three fail when the resolve and the decline rule are reverted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
crisfiorini-oss
pushed a commit
to crisfiorini-oss/cli
that referenced
this pull request
Oct 28, 2026
… each one's grant (codefly-dev#1207)" (codefly-dev#1210) This reverts commit ad957ae. Co-authored-by: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Choosing which tools a solution gets — and noticing that a new module version offers one more — had every part already built and no way to read them together:
contracts/runnables/index.json, which "in a composed workspace arrives inside a third-party module package", so a released module already ships its own tool list;runnable-scope-selections, keyedMODULE__OPERATION, per environment, where[]declines;ResolveScopeSlotsplus generate's owndeclinableOperation/unknownSelectionKey.Nothing joined them. "What does records 0.5.1 offer, and which of it has staging granted?" meant reading generated JSON by hand, and an operation added by a new module version was invisible: unanswered looks exactly like absent.
What this adds
codefly list runnablesalready lists derived operations beside declared ones. It now carries the two facts a picker needs.The owner's exposure, with
--toolsor--env, repeated and never completed — the model-facing name and the explicit effect. An operation with no exposure is listed without one: absent means not exposed, an unstated effect is not read-only.--toolskeeps only the exposed ones, which is what a composition can grant as a tool.The grant, with
--env, judged by the rulesgenerate runnable-bindingsjudges it by rather than by whether an answer exists:selecteddeclined[]— installed for no resource, granted no authority, offered as no toolneeds-scopesinvalidneeds-scopesis the piece that was missing. A module version that adds an operation adds a row nothing answers, so a new tool surfaces as work to do, and the listing names the key —DOCUMENTS__RUNTIME_WORKER_GRPC_APPLY_TEXT— that the grant is written under.An answer naming no operation is reported beside the rows (
unanswered_keysin--json) rather than dropped: a mistyped key answers no question and leaves its operation reading as unanswered, which is the one thing a listing must not hide.The grant is the generator's own judgement, not a second opinion
The first shape of this change decided the state from
len(selections) == 0, so any non-empty answer read asselected. A deep review (persisted as reportr2, fixes asx2) found that wrong on three configurations, all of them ones a person actually produces:collectionandsourcein an environment answering onlycollection→ reported granted, run refused with "declares required scope slot "source" but no ScopeSelection was supplied for it";slot:name, or a selection with an emptyinvokelist → same;[]against an operation with no slots → reporteddeclined, whiledeclinableOperationrefuses it with "there is nothing to decline; remove the entry".So the listing now builds the operation's core spec and calls
ResolveScopeSlotsitself, and reportsinvalidwith core's own words. That was possible without duplicating anything becauseoperationSpectook nothing from the endpoint descriptor but the method spelling, which the listing already holds: it moved to(*runnables.Operation).Spec, alongsideBindingKeyandGrantOf, inpkg/runnableswhere both callers hold one spelling.cmd/generate.RunnableBindingKeyis now a thin alias, andcmd/listno longer importscmd/generateat all.Output contracts kept
--toolsor--env, so anything parsing it keeps working.--jsonwithout--envis still the array it always was; the object (runnables,unanswered_keys) appears only under the new flag.toolandgrantlive on the listing's own row type rather than the sharedIdentity, soshow runnable --jsonis untouched (git diff origin/main -- cmd/show/is empty) and does not state the same exposure twice.Validation
go build ./...,go vet ./cmd/list/ ./cmd/generate/ ./pkg/runnables/— clean.gofmtclean.golangci-lint run ./cmd/list/... ./cmd/generate/... ./pkg/runnables/...— 0 issues.go test ./cmd/list/ ./cmd/generate/ ./pkg/runnables/... ./cmd/show/ -count=1— all ok.docs/commands.mdcarries the flags, the four-state grant table and theunanswered_keysbehaviour;docs/cli-reference.mdregenerated through its own gate (TestCLIReferenceIsCurrent).cmd/list/runnables_grants_test.goover a fixture whose operation declares a tool and a required slot: the exposure as declared;needs-scopesnaming the open slot;selectedon a resolving answer;declinedon[];invalidon a partial answer, on an undeclared slot name, and on an illegal decline; an orphan key reported while its operation still reads unanswered; the default table's columns; the array-shaped--json; an undeclared environment refused; and--toolslisting nothing when the owner exposed nothing. Reverting the resolve and the decline rule fails three of them — they are regression tests, not decoration.Not in scope
This reads the answer; it does not write one. Granting is still editing
runnable-scope-selectionsby hand. Acodefly grant runnable <key> --envthat writes the selection, and the console page that would consume this--json, are the next steps and belong in their own changes.🤖 Generated with Claude Code