Repository navigation
feat(add): scaffold a solution in the one shape a solution has (#1215) - #1216
Conversation
`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>
|
Two open items resolved, and the PR is out of draft.
|
| 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-auditflaked downloadinggo-m1cpu@v0.2.2, which cancelscoverageandracemid-test — three reds, one causegenerate (0)flaked downloadingauthority/libraries/delivery/go, anddocker-generateonly 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:
- A solution-go service agent: codefly add solution writes the paperwork, nothing writes the code #1217 — a
solution-goservice agent, so something scaffolds the backend's code and not just its paperwork. No core change and no new protocol:--agenthere already accepts it. - obin-ai/core-solutions#466 — the epic conforming the seven solutions that do not match, with the gap ledger.
wikiandproposalsalready match, which is what makes the shape two independent confirmations rather than one reading.
🤖 Generated with Claude Code
Closes #1215.
codefly add solution <name> --package-id <owner>/<name>creates a solution. core already specifies all of this —resources.SolutionAgent, thecore/solution/manifestschema, andLifecycle.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:solutionagent.(a). (b) is what
Lifecycle.Createpromises, 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) makescreatereal now, against a schema that already exists, and leaves (b) open: nothing here forecloses an executor, and--executorwrites 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/yamleditfor node-preserving edits,shared.WriteFileAtomic,cli.GetCurrentVersion, andpkg/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:
solution.codefly.yamlmanifest.ManifestValidate+manifest.LoadConsumptionon the written bytesmodule.contract.codefly.yamlmodulecontract.Contractmodulecontract.Loadon diskmodule.package.codefly.yamlcomposition.PackageManifestPackageManifest.Validatesolution.frontend.codefly.yamlsolutionhost.Frontendsolutionhost.ParseFrontendDeliberately not reused:
authoredDependencyrestates the four fields of a service-dependency an author writes, becauseresources.EndpointReferencerenders an emptyapi:that says nothing. That is the same shape, for the same reason, ascmd/sync/solution_sdk.go'sapplyServiceDependencies— 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/backendmodule-identity, one publichttpendpoint atexposure: nonewith a/healthprobeservices/webservices/fe-remote./Pageremote. An npm package, not a Codefly service; not scaffoldedThe 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 declaresinterface: codefly.dev/authority@…andauthority/hopaskind: 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: truein 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.entryManifestresolves a module's solution manifest only for the serviceservice-entrynames, so getting it wrong boots the solution with noapi.consumesprojection and leaves every consumed route unrouted.The solution manifest names no executor
manifest.Validaterequires an agent of kindcodefly:solutionat a concrete semver. No executor protocol exists (core/solution/holds onlymanifest/andpolicy.go), andcodefly-dev/solution-generic— which the shipped manifests name at0.0.1— has no releases, so naming it resolves toagent: binary not foundat 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.goandpkg/solutionrun/federation.goall read a shipped manifest throughLoadConsumptionor a lenient decode, andLoadConsumption's own doc says a manifest whose agent block does not passLoad"asks for no authority because of it".--executorwrites one when one exists.The five traps
check-consumed-apis' "a backend manifest for every solution manifest" always holds.--temporary-portsbreaks${endpoint:…}. 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:go build ./...,go test ./cmd/...(exit 0),golangci-lint run cmd/add/...(0 issues),docs/cli-reference.mdregenerated.What I did not verify
goandnextjswere 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.check_consumed_apisskipped its runtime-pin check, because a fresh scaffold has noservices/backend/go.mod. That pin is the Go agent's and the author's.check-boundariesran 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-idis required with no default.obin/wikiandobin/lastlogin-goare owned byobinwhile the repo isobin-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-versionis required when scaffolding with a build from source, which reports versiondevelopment. A guessed floor would claim compatibility with a release that may not exist.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-buildera backend — is the issue's own carve-out: one PR per solution, inobin-ai/core-solutions, now that the shape is checkable rather than a survey. No issues have been filed for them.🤖 Generated with Claude Code