Skip to content

feat(add): scaffold a solution in the one shape a solution has (#1215) - #1216

Merged
antoinetoussaint-byte merged 1 commit into
mainfrom
issue-1215-codefly-add-solution-core-specifies-solution-scaffolding
Oct 28, 2026
Merged

antoinetoussaint-byte merged 1 commit into
mainfrom
issue-1215-codefly-add-solution-core-specifies-solution-scaffolding

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor

Closes #1215.

codefly add solution <name> --package-id <owner>/<name> creates a solution. core already specifies all of this — resources.SolutionAgent, the core/solution/manifest schema, and Lifecycle.Create, which nothing implements — so this is an implementation, not a design.

Classification: fix

At the place that owns the behaviour. The gap was a missing CLI verb, and the verb is in the CLI. No core change, no new agent, no new repo.

Option (a), and why not (b)

Issue #1215 offered (a) compose existing verbs in the CLI, or (b) add an executor protocol to core and build a real codefly:solution agent.

(a). (b) is what Lifecycle.Create promises, but it commits the fleet to a wire protocol before anything needs one, and it cannot ship without a published executor — so every solution stays hand-assembled until a cross-repo chain lands. (a) makes create real now, against a schema that already exists, and leaves (b) open: nothing here forecloses an executor, and --executor writes one into the manifest the day it exists.

Reuse — what was reused, and what deliberately was not

Reused: addModule (its rollback included), addService, common.LoadWorkspace, common.NewContext/SignalContext, resources.ParseAgent, pkg/cli/yamledit for node-preserving edits, shared.WriteFileAtomic, cli.GetCurrentVersion, and pkg/solutionrun.DeclaredFrontend.

Every document is built from core's own type and read back through core's own reader, so the schema stays the single source of truth and a file that would be refused later is refused at generation:

document built from validated by
solution.codefly.yaml manifest.Manifest Validate + manifest.LoadConsumption on the written bytes
module.contract.codefly.yaml modulecontract.Contract modulecontract.Load on disk
module.package.codefly.yaml composition.PackageManifest PackageManifest.Validate
solution.frontend.codefly.yaml solutionhost.Frontend solutionhost.ParseFrontend

Deliberately not reused: authoredDependency restates the four fields of a service-dependency an author writes, because resources.EndpointReference renders an empty api: that says nothing. That is the same shape, for the same reason, as cmd/sync/solution_sdk.go's applyServiceDependencies — the other place this repo appends one.

Nothing hardcodes what the system resolves: the package's contract ranges come from composition.DefaultSupportedContracts, and its Codefly floor from the running build.

One shape, not two

The issue described a gateway shape (8 of 9) and an edge shape (1). I first built both behind --shape. That was wrong, and the owner corrected it: there is one shape, every solution has a backend, and the nine existing solutions get conformed to it.

The shape is solutions/wiki's — the solution that has been running longest — not lastlogin's, which is the demo:

services/backend entry service; module-identity, one public http endpoint at exposure: none with a /health probe
services/web the shell that serves the page where no host shell mounts it. Scaffolded and composed, not published — a host that federates the remote needs no shell
services/fe-remote the ./Page remote. An npm package, not a Codefly service; not scaffolded

The rule the backend lives under is documented, not made a second variant. A module call the authority holds to a person — admitted only "in person, with no intervening actor" — is made by the page with that person's own Work Context, never relayed through the backend. solutions/robin-builder's README is explicit that what the authority refuses is the relay, and that it dropped its backend because that cell also had no host gateway at all. So the entry service declares interface: codefly.dev/authority@… and authority/hop as kind: external, and no module edge — every consumed module goes through the hop.

Two things taken from wiki that lastlogin does not have, and that a scaffold modelled on lastlogin would have shipped broken:

  • owner_aware: true in the contract. core: "Solutions must declare it to receive person capabilities from the authority." Without it the solution can act for nobody, silently. Pinned by a test.
  • service-entry: backend. pkg/solutionrun.entryManifest resolves a module's solution manifest only for the service service-entry names, so getting it wrong boots the solution with no api.consumes projection and leaves every consumed route unrouted.

The solution manifest names no executor

manifest.Validate requires an agent of kind codefly:solution at a concrete semver. No executor protocol exists (core/solution/ holds only manifest/ and policy.go), and codefly-dev/solution-generic — which the shipped manifests name at 0.0.1 — has no releases, so naming it resolves to agent: binary not found at the first command that runs it.

So the generated manifest omits the key and says why in its own header. Every other rule of the schema is still checked, by validating a copy carrying a stand-in agent — mirroring core's own consumptionValidationPrincipal, which stands in a placeholder for the same reason. Nothing refuses the result: cmd/sync/solution_sdk.go, pkg/gitops/authority.go and pkg/solutionrun/federation.go all read a shipped manifest through LoadConsumption or a lenient decode, and LoadConsumption's own doc says a manifest whose agent block does not pass Load "asks for no authority because of it". --executor writes one when one exists.

The five traps

  1. Configuration groups are the consumers' set. Not scaffoldable — it is the composition's. Printed as the first next step, with the three errors trimming one produces.
  2. An edge solution must not ship a solution manifest. Moot by construction: there is one shape and it has a backend, so check-consumed-apis' "a backend manifest for every solution manifest" always holds.
  3. --temporary-ports breaks ${endpoint:…}. Printed in the next steps.
  4. The office converters need an image the CLI never passes (a local run of a container-only service should build the agent's recipe and pass the image #932). Printed in the next steps, naming the issue.
  5. A module pin must be a default-branch commit. Printed in the next steps.

Verification

Ran against the real emitted files, in a throwaway tree holding core-solutions' actual gate scripts from origin/main:

check-consumed-apis: solutions/scaffolded/solution.codefly.yaml: 2 delivered binding(s)   exit=0
check-solution-mesh: no solution holds a module-routing credential                        exit=0
check-boundaries:    clean outside the baseline                                           exit=0

go build ./..., go test ./cmd/... (exit 0), golangci-lint run cmd/add/... (0 issues), docs/cli-reference.md regenerated.

What I did not verify

  • The two service agents were never run. go and nextjs were not invoked, so the end-to-end scaffold is untested. What is tested is the document rendering and the manifest edits, the latter against a hand-written stand-in for what an agent writes.
  • No generated solution was booted. Nothing here was run against a host or an authority.
  • check_consumed_apis skipped its runtime-pin check, because a fresh scaffold has no services/backend/go.mod. That pin is the Go agent's and the author's.
  • check-boundaries ran with an emptied baseline: the real baseline names files this synthetic tree does not have, and the gate reports a baselined-but-missing file as a failure. The denylist itself was fully applied.

Open

  • --package-id is required with no default. obin/wiki and obin/lastlogin-go are owned by obin while the repo is obin-ai/core-solutions, so nothing derivable reaches it, and a guess would be committed and wrong. If that identity lives somewhere readable, say where and it becomes a default.
  • --minimum-codefly-version is required when scaffolding with a build from source, which reports version development. A guessed floor would claim compatibility with a release that may not exist.
  • MCP: deliberately not exposed. The command's one required input is a publishing identity an agent must not invent, and its value is in decisions about the composition that an agent cannot supply.

Docs

docs/commands.md (the verb, the shape, the backend rule, every flag), docs/cli-reference.md (regenerated), AGENTS.md (How-To Index).

Size

~900 lines across two non-test files, comment-heavy in this repo's style; 389 lines of tests. Over the ~500 guideline, under the 2,000 stop-and-ask line.

Out of scope

Conforming the nine existing solutions to this shape — including giving robin-builder a backend — is the issue's own carve-out: one PR per solution, in obin-ai/core-solutions, now that the shape is checkable rather than a survey. No issues have been filed for them.

🤖 Generated with Claude Code

`codefly add solution <name>` creates a solution: the backend a host
installs, the shell that serves its page, and the four documents core
already specifies. core declares `Lifecycle.Create` and nothing implements
it, so this fills in the implementation rather than designing a feature.

There is ONE shape, taken from `obin-ai/core-solutions`' `wiki` — the
solution that has been running longest — not a flag offering two. A second
shape beside it is what produced nine solutions that do not look alike.

The rule the backend lives under is documented rather than made a variant: a
module call the authority holds to a person, admitted only "in person, with
no intervening actor", is made by the page with that person's own Work
Context and never relayed through the backend. That is why the entry service
declares the authority interface and the authority's hop and no module edge
at all.

Composes existing verbs instead of inventing a protocol: `add module`, then
`add service` per service. Every document is built from core's own type and
read back through core's own reader — `manifest.LoadConsumption`,
`modulecontract.Load`, `PackageManifest.Validate`,
`solutionhost.ParseFrontend` — so the schema stays the single source of
truth and a file that would be refused later is refused at generation.

The solution manifest names no executor. core requires an agent of kind
codefly:solution and publishes no executor protocol; the agent the shipped
manifests name has no releases. Every other rule is still checked, against a
stand-in, mirroring core's own consumptionValidationPrincipal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor Author

Two open items resolved, and the PR is out of draft.

--package-id stays required

I looked for something to derive it from and there is nothing honest:

package ids in use obin/wiki, obin/lastlogin-go, obin/proposals — owner obin
the git remote obin-ai/core-solutions — owner obin-ai, repo core-solutions
the workspace name wiki, lastlogin-go — the solution, never the publisher

Nothing reaches obin from any of them. obin-ai -> obin is a guess that happens to work for one org and silently produces a wrong published identity everywhere else — and a wrong one is committed, rendered into the presence, and discovered by a host failing to list the solution.

One required flag is cheaper than that. The error says what it is and why nothing can derive it.

If the publishing identity ever becomes readable — an org-level setting, a field in the workspace — this becomes a default in one line.

CI

The five red checks on the first run were all one cause, proxy.golang.org returning stream error: INTERNAL_ERROR; received from peer:

  • bootstrap-audit flaked downloading go-m1cpu@v0.2.2, which cancels coverage and race mid-test — three reds, one cause
  • generate (0) flaked downloading authority/libraries/delivery/go, and docker-generate only reads the shards' outcome

Nothing in this diff touches cmd/generate, pkg/gitops or any dependency. Rerun, not chased.

Downstream

This PR defines the shape; the work it unblocks is filed and two agents are on it:

🤖 Generated with Claude Code

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.

codefly add solution: core specifies solution scaffolding, nothing implements it

1 participant