Skip to content

fix(add,run,doctor): make a scaffolded solution one that actually runs - #1228

Merged
antoinetoussaint-byte merged 9 commits into
mainfrom
solution-run-it-locally
Oct 29, 2026
Merged

antoinetoussaint-byte merged 9 commits into
mainfrom
solution-run-it-locally

Conversation

@antoinetoussaint-byte

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

Copy link
Copy Markdown
Contributor

codefly add solution had 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:

$ codefly add solution notes --package-id obin/notes
  resolved solution-go → 0.0.2
  wrote contracts/api and the package's api-contracts entry
$ codefly doctor solution --dir modules/notes
  OK  notes — conforms to the solution shape

$ codefly run service backend          # wiki playground = local dev authority
  delivered module=notes workloads=1 disposition=accepted
  permissions: role installation/installer × notes@1 ×
               GET /v1/notes/overview: covered statically
  notes/backend: Running in 5.6s

GET /health                 200
GET /v1/notes/overview      401 {"error":"no Work Context: this page is reached
                                 through the authority's edge, which signs the person in"}

That permissions: 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 → 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

# What How it showed
1 Every invocation failed. Step 3 read the module off a workspace handle loaded before add module wrote it cannot find module <name> in <ws>, present: [], after creating the module and both services
2 The rollback had the same bug left the half-built module its own doc comment promised it never would
3 --agent python was advertised and cannot work the generic agent "defines no project template"
4 The authority refuses a scaffolded solution declares 0 role(s) and none is "installer", at runtime-run, after the whole composition booted
5 doctor solution called a working shell empty looked beside the manifest; the nextjs agent's default puts the app under code/. The layouts differ by spec.source-dir
6 The default agent scaffolded a service, not a solution no declared operation, no published policy, no page remote; delivery refused the result
7 npm ci can never work on a fresh scaffold refuses without a lockfile; and nativeBuildEnvironment stripped NODE_AUTH_TOKEN, which the agent's own .npmrc reads — 401 Unauthorized whatever the operator exported
8 The package carried no contract generate contracts refused (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 sign

The 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 the solution-go agent's — codefly-dev/service-solution-go, now at v0.0.2, whose base/ is a real backend on solution-runtime-go with operations.go, required per-route policy, a rendered interface document and templates/page/. The CLI's job is the shape, the documents, and refusing the wrong agent:

$ codefly add solution demo --package-id obin/demo --agent go
--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

A refusal, not a substitution: which agent to run is the author's statement. It happens before the first write.

doctor solution gains two rules, and they re-sort the fleet

Both 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:

before after
9/9 OK wiki, proposals OK; 7 FAIL

installer_role_missing and backend_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's modulecontract.InstallerRole, Contract, Role, CeilingScope; composition.PackageManifest; resources.LoadFromDir; yamledit; shared.WriteFileAtomic. The rollback is cmd/add/module.go's, applied where it was needed.

Not verified

  • No page rendered in a browser. The remote builds and delivery is accepted; nothing here renders it.
  • /assets/mf-manifest.json is 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.
  • No authenticated call. The 401 proves the route is protected; getting a 200 needs a signed-in viewer through the edge, which I did not stage.
  • The python-fastapi path scaffolds and conforms but was never booted.

Docs updated: docs/commands.md, docs/cli-reference.md.

🤖 Generated with Claude Code

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 antoinetoussaint-byte changed the title fix(add): make a scaffolded solution one that actually runs, and give it something to run fix(add): make a scaffolded solution one that actually runs Oct 29, 2026
antoinetoussaint-byte and others added 5 commits October 28, 2026 22:11
…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>
@antoinetoussaint-byte antoinetoussaint-byte changed the title fix(add): make a scaffolded solution one that actually runs fix(add,run,doctor): make a scaffolded solution one that actually runs Oct 29, 2026
antoinetoussaint-byte and others added 3 commits October 28, 2026 23:31
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
antoinetoussaint-byte marked this pull request as ready for review October 29, 2026 13:23
@antoinetoussaint-byte
antoinetoussaint-byte added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit 3fc4407 Oct 29, 2026
19 checks passed
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>
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