Repository navigation
fix(add,run,doctor): make a scaffolded solution one that actually runs - #1228
Merged
Merged
Conversation
Five defects, each found by running `codefly add solution` for the first
time rather than by reading it. The command had shipped without ever
being executed end to end.
1. It failed on EVERY invocation. Step 3 read the scaffolded module off
the workspace handle loaded before `add module` wrote it, so it died
with "cannot find module <name> in <ws>, present: []" after creating
the module and both services. `add service` re-reads the workspace for
exactly this reason, which is why the two services landed and the
module did not.
2. The rollback had the same bug, so the failure above left a half-built
module behind — the state the function's own doc comment promised it
would never leave.
3. `--agent python` was advertised in the help, the example and the
command reference, and can never work: the generic `python` agent runs
a Python service but defines no project template ("use a concrete
specialization"). `python-fastapi` is the one that scaffolds.
4. The contract declared no roles, so the authority REFUSES the solution:
"declares 0 role(s) and none is \"installer\": a solution declares an
installer role". That refusal lands at runtime-run, after the whole
composition has booted, with every document reading fine until then.
The scaffold now writes the installer role and the scope_ceiling it
sits within, derived from the one resource a scaffold knows the
solution has: its page.
5. `doctor solution` reported a freshly scaffolded, working page shell as
having no app. It looked for package.json beside the manifest; the
nextjs agent's default puts the app under `code/`. The two layouts in
use differ by exactly `spec.source-dir`, so the check now asks the
manifest instead of assuming.
`doctor solution` also gains the installer-role check. It is the rule the
authority states verbatim and nothing else catches, and it re-sorts the
fleet: of the nine solutions, only wiki and proposals — the two that are
actually run — declare the role. The other seven were reported conformant
by a checker blind to the thing that refuses them.
Verified by running, not by reading: a scaffolded Go solution boots
against the real authority composition ("wikitrial/backend: Running in
5.6s"), and a scaffolded python-fastapi solution scaffolds and conforms.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
antoinetoussaint-byte
force-pushed
the
solution-run-it-locally
branch
from
October 29, 2026 01:50
f90d7cb to
9df7ed3
Compare
…e one
`codefly add solution --agent go` scaffolded a Go SERVICE into the shape
of a solution: a net/http mux serving /health, no declared operation, no
published policy on anything, and no page remote. Every document it wrote
was valid and nothing said a word — and then delivery refused the result,
"declares a frontend and holds no npm project to build it from". The
default was that agent, so this is what `add solution` produced.
The default is now `solution-go`, which builds on solution-runtime-go:
the viewer's Work Context, the authority's check against each route's
published policy, the passthrough that relays a module call AS THE
VIEWER, the interface document, and the ./Page remote a host federates.
Naming the service agent of a language is REFUSED rather than quietly
upgraded, and the refusal names the agent to use:
--agent go is the SERVICE agent: it scaffolds a service, not a
solution — no declared operation, no published policy, no page
remote, and delivery refuses the result. Pass --agent solution-go
Which agent to run is the author's statement about what they want built;
substituting another would be the CLI deciding what they meant. The check
runs with the other input validation, before the first write, so the
mistake costs nothing rather than half a module.
An agent this build does not know is accepted as named: a publisher may
ship a solution agent newer than this CLI, and an allow-list would make
the CLI the gatekeeper of a registry it does not own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two defects, both found by running a solution the CLI had just scaffolded. Its backend booted against the local authority and answered /health, and delivery then refused the page — twice, for two different reasons, neither of them the page's fault. 1. `npm ci` cannot run without a lockfile, by design: "loadVirtual requires existing shrinkwrap file". A freshly scaffolded page remote has none, so the one command the build ever ran was the one command that could not work on it. It now installs with `npm ci` where a lock exists and `npm install` where none does. The fallback is safe because this runs in STAGING, a copy: the lock npm resolves is written to the copy and the author's tree is untouched, and nothing changes for a project that has a lock. 2. nativeBuildEnvironment is an allow-list and NODE_AUTH_TOKEN was not on it. A solution's page depends on the @codefly-dev kits from GitHub Packages, and the .npmrc the agent writes beside it reads ${NODE_AUTH_TOKEN} — so the CLI was stripping the one variable that file needs. Every install failed "401 Unauthorized ... unauthenticated" whatever the operator exported, and nothing said the CLI had removed it. The allow-list still drops everything it does not name. Found in this order: the lockfile failure hid the credential one, which is why both are here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The scaffolded package declared artifact-roots: [services], so
`codefly generate contracts` refused outright:
the package cannot carry the backend/http contract at
contracts/api/backend/http/openapi.json; add "contracts" under
artifact-roots
Not a packaging detail. The CLI derives the authority's per-route method
policies from the contract the package carries (gitops
ServiceMethodPolicies reads api-contracts), so with no contract the
authority was delivered the module's ceilings and no method policy at
all:
permissions: WARN notes@1: no delivered method policies; declared
ceilings alone do not admit a call
The backend served its route, the route declared its policy, and every
call would still have been refused — the only sign a warning in the
authority's log.
With contracts declared, `codefly generate contracts` completes, the
package carries the document with its digest, and the authority binds
the role to the route:
permissions: role installation/installer × notes@1 ×
GET /v1/notes/overview: covered statically; identity, installation
and resource checks still apply
That line is the whole chain closing: this command writes the installer
role and the <name>.page ceiling, the solution agent's route requires
<name>.page:read, generate publishes the document, and the authority
derives the policy from it.
wiki declares the same two roots, which is why it works.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The authority derives the method policy of every route from the contract
the package carries. A package that declares none is delivered the
module's scope ceilings and no policy at all, and says so once:
permissions: WARN <solution>@1: no delivered method policies;
declared ceilings alone do not admit a call
Everything else looks right. The documents are valid, the backend boots,
the route is served and declares its policy — and no call to it can ever
be admitted. The only sign is that line, in the authority's log, during
a run that nobody may do.
Same split as the installer role, and the same two solutions on the
right side of it: of the nine in core-solutions, wiki and proposals
carry the contract and the other seven do not.
The fixture gains the contract and the file behind it, because the
package loader checks every artifact root exists on disk.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
What `add solution` left behind was the nextjs agent's generic web app:
placeholder login, signup and dashboard routes, and a home page reading
"Your app is running. Edit src/app/page.tsx to get started." Nothing in
it was a solution, and the solution's own ./Page — written by the
solution agent, the module a host federates — was never mounted
anywhere.
No agent can fix that. The solution agent runs for the BACKEND and the
nextjs agent runs after it, so anything the solution agent wrote into
the shell is overwritten. This command is the only step after both.
The shell now mounts the remote itself — `dynamic(() => import(
"<name>-fe-remote/Page"), { ssr: false })` — rather than reimplementing
it, so the shell and a host cannot show two different pages. It reads
the viewer from the authority edge's /auth/session, calls the backend at
the one same-origin /v1/<name>, holds no token, and reports the
authority's own words when the solution is not installed for the person.
THE LOGIN ROUTES ARE DELETED, not filled in. A solution has no sign-in
of its own: the edge is the browser's origin and signs the person in. A
solution that grew one would be a second front door onto the same data.
Three things only running it could show, each now encoded:
- Linking the remote into the shell's package.json invalidates the
lockfile the agent ships, and its runner installs with `npm ci`:
"npm ci failed: exit status 1" at runtime-run. The scaffold relocks.
- wiki's next.config.ts cannot be copied. It configures `webpack`, and
that is a hard ERROR on the Next this agent scaffolds — Next 16 runs
Turbopack by default and refuses a webpack config it was not told to
use. wiki works because it is on an older Next.
- The Turbopack build root must be the SOLUTION'S, not the app's. The
remote is linked as a symlink to ../../fe-remote, and Turbopack will
not follow a symlink out of its root: "the symlink target leaves the
configured filesystem roots", served as a 500 while the link, the
package's exports map and the file are all correct.
Verified on an untouched scaffold, run against the local dev authority:
the page answers 200 with no resolution error and no starter-app text.
Not verified: the page was fetched, not opened in a browser. It is
client-rendered, so painting its content needs a signed-in viewer
through the edge.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A scaffolded solution could be built, judged and booted — and there was
no way to OPEN it. Every solution in the fleet ships a playground that
runs it on the development authority; this command shipped none, so the
composition had to be reconstructed by hand. I did that four times
tonight and sent the wrong URL twice, which is the evidence it belongs
here.
The one fact that is not derivable from any file:
PLAYGROUND_SERVICES = authority/edge <name>/web <name>/backend
THE EDGE IS NAMED. It is not a dependency of the backend and nothing
elects it as ingress, so neither `run solution` nor `run workspace`
starts it — the fleet's own Makefiles list it first and so does this
one. Without it the page is served on no origin that can sign anyone in.
Neither wrong address fails in a way that says what is wrong:
- the shell's own port: the page asks the edge for /auth/session on
the origin it was served from, finds nothing, and reports "This page
is not behind its sign-in edge".
- the authority's REST port: that is the authority's API, not the
edge. Every browser path 404s.
The authority is pinned by COMMIT, resolved from its default branch at
scaffold time: the repository publishes no module release — its only
tags are its Go libraries' — and a PR head reads identically in the
manifest and dies on squash-merge. --authority-version overrides it, and
a pin that cannot be resolved warns and skips the playground rather than
failing a correct solution.
Local only: in a cell the host's gateway is the origin and issues the
Work Contexts. The README says so.
Verified from nothing: `codefly add solution notes`, then `make
playground`.
edge listening on :14581, routing 1 module(s)
/ 200 the solution's page
/auth/sign-in 200 the dev authority's sign-in
/auth/session 401 until you sign in
/v1/notes/overview 401 the backend, behind its policy
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A solution could be scaffolded, run and signed into, and the authority
still refused every capability at it:
audience "notes" is not installed in this organization, so there is
no installation to seal the capability to
The fixture names who exists and the authority's own vocabulary only;
WHICH SOLUTIONS ARE INSTALLED is each composed module's fragment to say,
and a scaffolded solution shipped none. So the page could render nothing
but its not-installed branch, for everyone, always.
configurations/local/fixture-composition.yaml now declares the
installation, and the contract declares a `viewer` role beside
`installer` — the organization is installed as viewer, and a role the
contract does not name is a role the authority never issues.
TEAMS ARE WHAT ADMIT ANYONE BUT THE OWNER, which cost a run to learn.
With `role` and `owner` alone the owner gets in and every other person
signs in and is refused by name, "<person> holds no role on <solution>".
wiki's installation carries `teams: [finance, engineering]` and that is
the half I had dropped.
Verified against the running authority, every person of the fixture:
maya (finance) installed /v1/notes/overview 200
remi (engineering) installed 200
alex (finance, engineering) installed 200
dev (engineering) installed 200
no-roles (no team) refused 403 — holds no role
no-roles is meant to be refused: the fixture carries a person in no team
so a real refusal is visible, and the page's not-installed branch is
reachable on purpose.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
antoinetoussaint-byte
marked this pull request as ready for review
October 29, 2026 13:23
antoinetoussaint-byte
added a commit
that referenced
this pull request
Oct 29, 2026
docs/commands.md: main (#1228) removed the section on holding an operation to the environment it selected, which this branch had revised; kept this branch's revision, since main's side of the hunk carried no content. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.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.
codefly add solutionhad never been run end to end. This is every CLI fix that running it required, in one PR.The bar, and it is met. Scaffold with the CLI, then run the result against the local dev authority — no manual steps between:
That
permissions:line is the whole chain closing: this command writes the installer role and the<name>.pageceiling → the solution agent's route requires<name>.page:read→ the contract is published → the authority derives the method policy and binds the role to the route. The 401 is the correct answer to a bare curl — the route is served and protected, and a viewer's Work Context exists only through the edge.Eight defects, every one found by running
add modulewrote itcannot find module <name> in <ws>, present: [], after creating the module and both services--agent pythonwas advertised and cannot workdeclares 0 role(s) and none is "installer", at runtime-run, after the whole composition booteddoctor solutioncalled a working shell emptycode/. The layouts differ byspec.source-dirnpm cican never work on a fresh scaffoldnativeBuildEnvironmentstrippedNODE_AUTH_TOKEN, which the agent's own.npmrcreads —401 Unauthorizedwhatever the operator exportedgenerate contractsrefused (add "contracts" under artifact-roots), so the authority got ceilings and no method policy: every call to every route refused, with one log warning as the only signThe agent, not the CLI
An earlier revision scaffolded an example backend and page as string literals in
cmd/add. Reverted. A solution's code is thesolution-goagent's — codefly-dev/service-solution-go, now at v0.0.2, whosebase/is a real backend onsolution-runtime-gowithoperations.go, required per-route policy, a rendered interface document andtemplates/page/. The CLI's job is the shape, the documents, and refusing the wrong agent:A refusal, not a substitution: which agent to run is the author's statement. It happens before the first write.
doctor solutiongains two rules, and they re-sort the fleetBoth are the silent-at-build-time, refused-at-runtime class the checker exists for. Of the nine solutions in core-solutions, the same two pass both — wiki and proposals, the only two that are actually run:
installer_role_missingandbackend_contract_missing. The other seven are tracked by the already-open conformance issues (core-solutions #467–#473) and are not in this PR.Reuse
add module/add service's own paths;cmd/generate's contract generation behind one new exported entry point; core'smodulecontract.InstallerRole,Contract,Role,CeilingScope;composition.PackageManifest;resources.LoadFromDir;yamledit;shared.WriteFileAtomic. The rollback iscmd/add/module.go's, applied where it was needed.Not verified
/assets/mf-manifest.jsonis 404 on the backend even though the bundle builds and staged. I do not know whether the host serves it instead or this is a ninth defect, and I did not guess.python-fastapipath scaffolds and conforms but was never booted.Docs updated:
docs/commands.md,docs/cli-reference.md.🤖 Generated with Claude Code