Build review (changes 8) and security review (changes 4):
- P1-1: an owner-approved fire no longer writes the revoke before the
apply (decision-stale). The fire stops the copy's runs, the fire is
written, then the retire revoke starts for the fired instance.
- P2 (both): archive and delete from the CLI and the Console pass the
organization's managed Gitea remote step (copyRemoteFor). A remote
step that was not attempted or failed keeps the record revoking with
failed remote, so the bus finishes it after restore; no done tombstone
over a live token.
- Security P2-1: a bound server that cannot be read is a failed remote
step, not not-managed. updateOrg refuses project-repo-bound when a
project a copy binding grants loses or changes its remote or git.
- P3-1 (security): a delete whose organization file cannot be read says
copiesUnreadable, never an empty copy list.
- P3-2 (security): the remote step requires the record's incarnation to
equal the registry's live one.
- P3-5 (build): the archive's slot discard checks each parent directory
(no symlink, own uid, private mode).
- P3-6 (build): resume keeps credential-revoke-failed as the gate code
while a failed op runs again.
- P3-4 (build): a test pins the refused-begin reopen guard.
A copy binding grants the repositories its projects' remotes name when a
credential is issued, and its decision records only project names. repo set
(CLI, route, Settings tab) is now refused with project-repo-bound while a
pending or bound copy binding names the project, before any check, origin
change or write, and again under the lock. The message names the copies and
the next step: revoke the binding, set the repository, raise a new binding
decision. An unbound binding, or none, behaves as before. Bindings are read
from the loader-validated document; binding, launcher and broker code are
unchanged. New refusal-table entry for project-repo-bound.
The needs-credential line no longer promises that a binding will supply
one; it says the check sends none. A recorded check is shown only for
the URL it checked. The Settings section says when the git origin differs
from the recorded repository, and a save repairs it. The URL rule message
names what is refused. Tests cover a document write that fails after
origin is set, the drift line and its repair, and Quick Setup in remote
mode.
Remote mode sets the project checkout's origin and checks it once with
git ls-remote, with no credential, no prompt, a protocol allowlist of
https and ssh and a 15 second limit. The receipt reports reachable,
needs-credential or unreachable with git's error summarized. Nothing is
pushed. URLs are https or ssh only; ext:: and other helpers, file://,
local paths, option-like values and credential-bearing URLs are refused.
`mosaic org project repo set` changes a project's repository later, both
ways between remote and local only, through the same rules and check,
with a revision guard and an org.project.repo.set receipt. The Console's
organization project Settings tab edits it through
/api/orgs/ID/repo/project/PROJECT; Quick Setup accepts ssh and shows the
check.
P1-4: org restore starts the bus as mosaic bus start ID --dev-local-owner
--detach would, and its receipt names the bus. Where that start is refused
(deployed or stamped instance, agent run) restore refuses before changing
anything; --no-bus keeps the old restore. The Console Restore action
restores and starts the bus with one receipt, and offers Restart bus when
the start fails after a good restore.
P1-5: Quick Setup refreshes the sidebar organization list, and the
organization dashboard redraws on the config event Restart bus already
dispatches, so the bus line and System types follow without a reload.
A stop that may already have signalled the host (a timeout or an
unexpected error) is trailed as failed, not refused; only exit 3 comes
before the signal. The response's stopped flag follows what busStop
reports, so a host that exited on its own between the read and the stop
is not reported as stopped by this press.
POST /api/orgs/ID/stop/bus runs busStop the way mosaic bus stop does: a
graceful SIGTERM close that leaves no host.lock or writer.lock, so an
offline schema migration can open the store straight after. It follows
restartBus for authority, same-origin checks, trail and receipt. A bus
already stopped answers done with state stopped and changes nothing; a
host identity the Console cannot prove refuses with every file unchanged.
The organization header gains a Stop bus button beside Restart bus, one
press, sharing its receipt and busy state. The restart icon now lives
inside its button, so a wide receipt no longer pulls it off the button.
CB0: registry incarnation minted at reserve (legacy entries get one at
the first binding proposal, under the registry lock); copyId, source
and definition pin, binding generation; a pure resolver where
effective = role intersect approved and only a bound, matching,
unshadowed record resolves.
CB1: copyBindings in the organization file (ids, scope names, project
ids and integers only). applyCopyBindingDecision is its only writer,
behind a module-private capability every other commit is fenced
against. A binding is an owner-only subtype of the hire decision:
only the chief raises it, no autonomy level, resolver agent, member
right, tester or reader resolves it. Digest, expiry, single
consumption, revision, generation, incarnation and definition pin are
rechecked before any effect; a bind writes pending, an unbind the
unbound tombstone, receipts are paths only. Nothing writes bound, so
nothing launches on a binding yet (CB2/CB3). The org service and the
Console update path keep copyBindings server-held. Eight new refusal
codes with next steps.
- busRecover: a live pid with the recorded start time is the recorded process, so it refuses
("bus host identity is uncertain") whatever its argv. It used to read a mismatching argv as a dead
host. The pinned recovery test now covers a reused pid (live, other start time) as the wrong
process, and a new test covers the refusal.
- busRecover clears through setAside: each inspected entry is renamed aside and checked by device
and inode; a swapped entry puts everything back and refuses before any receipt or deletion, and
the deletion names only the aside entries.
- mosaic bus recover refuses inside an agent run (refuseInsideAgent), like mosaic org reload. The
Console-start revive does nothing when the Console's own environment carries an agent marker.
- Tests: org-recovery (identity, agent refusal), org-revive (setAside swap), bus-revive (markers);
the other reviveBuses tests pass an explicit env.
- The Console-start revive asks busRevive for pm:false; reviveStartOptions holds the rule (never
wider than the dead host's record) and is tested on its own. A dropped --pm is logged with the
way back.
- A Restart bus press waits for that organization's start-up revive (revivePending) before it reads
the bus, so it no longer races it for the lifecycle lock; a press that still meets org-locked gets
409 bus-busy, nothing changed.
- busRevive marks an error from busStart after a successful clear (error.recovered); the trail, the
Console log and the press response then say "failed", not "refused". A non-CliError logs its code.
- Tests: bus-revive.test.mjs (new), org-admin and org-revive cases for the above.
A Console unit stop (KillMode=mixed) SIGKILLs the bus hosts the Console
started, leaving host.json, host.lock and the socket behind, and the
dev Console's Restart bus refused 409 on that stale state.
- busRevive (org-bus): busRecover, then busStart detached on the
options the dead host recorded. busRecover still refuses a live host
pid, a lock that is not the dead host's, a live writer pid, or a
socket that answers or is uncertain.
- reviveBuses (webui): when a dev-local-owner Console starts, a bus that
answers is left (reattached); a bus with stale state goes to busRevive
with pm:false, trailed as bus.revive before anything changes.
- Restart bus (org-admin): for the exact dev owner only, a stale-state
refusal goes to busRevive; a signed-in owner keeps the refusal.
- The receipt reads "Dead bus host cleared; bus started".
Conflicts kept both sides: rename stage B organization paths and the
relative-launch re-exec with F3 imports, F2 onboarding script and the
north-star fence exemption with F3 console-restart.js and ORG_OWNER_WRITE,
and the /api/config fields from both.
Self-service operation from the WebUI, for a dev instance first.
- Settings > Console > Restart Console. A foreground `mosaic console
start` now supervises the webui: the child exits 75 after answering a
restart and the supervisor relaunches it with the same argv. Org buses
keep running. /api/config carries a per-process boot id, so the open
page polls until a new process answers and then reloads. A detached
Console has no supervisor and answers 409.
- Restart bus on every "Restart needed" receipt and on the bus-down
banner: POST /api/orgs/<id>/restart/bus restarts a running bus or
starts a stopped one, then shows a done receipt with the time.
- Settings > Routing: after a save, the page lists how the change
reaches each agent (next start, session end, thread idle) and offers
"Restart <agent> now" for a running one. A session is terminated with
the reason "route change" and launched again; a thread is terminated.
- Organization settings: the signed-in owner's settings, routing,
agent, plug-in and Model proxy writes now reach the selected org's
bus instead of answering 403. Org mode exists only under
--dev-local-owner. Proxy setup stays a terminal step; a dev-only note
names the two commands.
Every control needs the signed-in owner session, the page's exact
Origin and Sec-Fetch-Site: same-origin. The tester listener, granted
users, API tokens, agents and bus roles are refused before any work.
Each use is written to <dataRoot>/console/actions.jsonl (0600, no
symlinks) with who, when, what and the outcome. The Console and bus
restart trails fail closed. No change to credential handling, the
broker, the bus tenant boundary or v2 schemas.
git mv packages/business -> packages/organization (@mosaic/organization). The
old package stays as one-line re-export shims of each moved module, with
src/business.mjs re-exporting src/organization-config.mjs and src/cli.mjs
forwarding to the moved executable. BusinessError is OrganizationConfigError
(one constructor, name kept BusinessError); OrganizationError stays distinct.
The bus adapter gains organization-context.mjs (busOrganization), with
business.mjs (busBusiness) as the same function.
Import consumers, manifests, the lockfile, scripts/mosaic, stack-copy.sh
COPY_PATHS (both roots), seat sandbox RUNNER_FILES and the copy closure
walker (organization-imports.mjs, both roots required) move to the new path.
v1 serialization, v1 writers and the stage A fixtures are unchanged; the stage
A contract tests stay where they were and pass as before.
mosaic org delete ID --confirm ID --if-revision N, and POST /api/orgs/:id/delete,
mark the organization as deleting, stop its bus, move its files into
<dataRoot>/deleted/<ID>-<stamp>/ with a receipt, then drop it from the registry
and append deleted.log. A failure stops at its step and a rerun resumes. Bus
start refuses an organization that is being deleted. The settings page asks
twice: a panel that lists the effects, then typing the ID before Delete
permanently is enabled. Purge and remote revocation are follow-ups.
A failed bus start now carries the host's own refusal line, read from host.log; repository paths in messages are shown relative and URLs are kept. Setup cards keep the chosen mode, remote and risk box across a re-render, and a client-side refusal is shown on the card.
org-admin.mjs serves the S6 routes over the C2 organization service:
template, validation with JSON paths, create, edit with if-revision, and
guided setup steps that read their receipt back before acting and never
reinitialize. Receipts reach the browser with host paths relative to the
data and config directories.