Skip to content

feat(list): runnables carry the tool they are offered as, and each one's grant - #1207

Merged
antoinetoussaint-byte merged 2 commits into
mainfrom
feat/list-runnables-tools-and-grants
Oct 27, 2026
Merged

antoinetoussaint-byte merged 2 commits into
mainfrom
feat/list-runnables-tools-and-grants

Conversation

@antoinetoussaint-byte

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

Copy link
Copy Markdown
Contributor

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:

  • the catalog is 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;
  • the answer is runnable-scope-selections, keyed MODULE__OPERATION, per environment, where [] declines;
  • the judgement is core's ResolveScopeSlots plus generate's own declinableOperation / 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 runnables already lists derived operations beside declared ones. It now carries the two facts a picker needs.

The owner's exposure, with --tools or --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. --tools keeps only the exposed ones, which is what a composition can grant as a tool.

The grant, with --env, judged by the rules generate runnable-bindings judges it by rather than by whether an answer exists:

grant meaning
selected its answer resolves every scope slot the owner declared, or it declares none
declined answered [] — installed for no resource, granted no authority, offered as no tool
needs-scopes its owner asks for scope slots nobody has answered
invalid generation refuses this answer, and the row carries core's reason

needs-scopes is 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_keys in --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 as selected. A deep review (persisted as report r2, fixes as x2) found that wrong on three configurations, all of them ones a person actually produces:

  • an operation declaring collection and source in an environment answering only collection → reported granted, run refused with "declares required scope slot "source" but no ScopeSelection was supplied for it";
  • a typo in a slot: name, or a selection with an empty invoke list → same;
  • [] against an operation with no slots → reported declined, while declinableOperation refuses it with "there is nothing to decline; remove the entry".

So the listing now builds the operation's core spec and calls ResolveScopeSlots itself, and reports invalid with core's own words. That was possible without duplicating anything because operationSpec took nothing from the endpoint descriptor but the method spelling, which the listing already holds: it moved to (*runnables.Operation).Spec, alongside BindingKey and GrantOf, in pkg/runnables where both callers hold one spelling. cmd/generate.RunnableBindingKey is now a thin alias, and cmd/list no longer imports cmd/generate at all.

Output contracts kept

  • The default table is unchanged — the tool columns appear only with --tools or --env, so anything parsing it keeps working.
  • --json without --env is still the array it always was; the object (runnables, unanswered_keys) appears only under the new flag.
  • tool and grant live on the listing's own row type rather than the shared Identity, so show runnable --json is 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. gofmt clean. 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.md carries the flags, the four-state grant table and the unanswered_keys behaviour; docs/cli-reference.md regenerated through its own gate (TestCLIReferenceIsCurrent).
  • Twelve tests in cmd/list/runnables_grants_test.go over a fixture whose operation declares a tool and a required slot: the exposure as declared; needs-scopes naming the open slot; selected on a resolving answer; declined on []; invalid on 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 --tools listing 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-selections by hand. A codefly grant runnable <key> --env that 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

antoinetoussaint-byte and others added 2 commits October 27, 2026 19:07
…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>
@antoinetoussaint-byte
antoinetoussaint-byte added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit ad957ae Oct 27, 2026
23 of 25 checks passed
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>
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