Files
jason.woltje e6ab8bc1fd feat(bus,plugins,organization,cli,webui): SP9 remove system types outright, bus schema 3v-3w drops project_system_types (#1873)
Jason's 2026-11-12 ruling: the only deployed system is the dev environment on
dragon-lin, so the system-type plug-in kind goes with no retired kind, fence,
conversion or new refusal code.

- plugins: system-type leaves KINDS; system-types.mjs and the five modules are
  deleted. An org file naming the kind refuses unknown-kind.
- organization: the plugins.system-type check, systemTypesFence and the
  system-types.console right are removed.
- bus: system-types.console, project.system-types[.set], the system-type half
  of the tester check, the runtime reads and system-type-not-enabled are
  removed. Step 3v-3w drops project_system_types, its index, its four guards
  and its rows; SCHEMA_VERSION is 3w (32 tables, 126 triggers, 5 views).
- cli: mosaic org system-types and mosaic project system-types[-set] are
  removed; stack-release COUNTED and the budget supervisor's closed list move
  to 3w.
- channel: the project.system-types verbs are removed.
- webui: the System types page, route, dashboard row, chips and project
  selection are removed.
- docs: REQ-SYS-1/2 now describe starter packs; README system-type text and
  the requirements titles follow.
2026-11-12 21:02:46 -06:00

687 lines
40 KiB
Markdown

# Organization
`@mosaic/organization`: role definitions, the business and project files, the
variable registry and the resolver (slice 1 row S1, #1518). Rename design r2,
stage B moved it here from `packages/business`. That path and `@mosaic/business`
stay as the old names: each module there re-exports the module of the same name
here (`src/business.mjs` re-exports `src/organization-config.mjs`), and its
`src/cli.mjs` runs this package's `src/cli.mjs`. Brief: `docs/plans/2026-10-04_slice-1.md`,
row S1. Design: `agents/darkwing/work/slice1-data-model-2026-10-04.md` and its
addenda A and B. Configuration readers refuse on any problem. Runtime
organization and accounts stores provide explicit locked writes; this package
never opens a token file.
```sh
scripts/mosaic org config validate mosaic-stack
scripts/mosaic org config resolve mosaic-stack coder --project stack
node --test packages/organization/tests/
```
`mosaic business validate|resolve` is the old spelling, with the same
arguments. Both spellings run `runConfigCommand` in `src/config-command.mjs`,
so status, stdout and stderr are the same. The one exception is a usage
error: its line names the spelling you used (`mosaic org config: usage: ...`).
Every other stderr line, refusals and warnings included, starts
`mosaic business: ` in both spellings, so the diagnostics stay byte for byte
what the old command printed. Importing `src/config-command.mjs` runs nothing and never
exits the process; `runConfigCommand(argv, {stdout, stderr, env, spelling})`
returns the exit code. `src/cli.mjs` is the thin executable that the old
spelling runs. `mosaic org validate FILE` is different: it checks a draft file
by path and prints `valid` (packages/cli/README.md).
## Files
| File | Who writes it | Holds |
|---|---|---|
| `roles/<name>.json` | reviewed commits | a role definition: tools, network, authority, credential scopes |
| `roles/<name>.md` | reviewed commits | the duty contract a version 2 role file names |
| `<configDir>/businesses/<id>.json` | Jason | role instances, holders, bots, token references, arbiters, projects, launch limits |
| `<root>/.mosaic/project.json` | reviewed commits in that project | project variables, per-instance project variables |
`<configDir>` is the directory holding the system config: the directory of
`$MOSAIC_CONFIG`, or `~/.config/mosaic-dev`. Role files come from `roles/`
in this repository, or `$MOSAIC_ROLES_DIR` if set.
### Role files, version 2
```json
{
"roleVersion": 2, "name": "coder", "title": "Coder", "contract": "coder.md",
"tools": ["read", "write", "edit", "bash", "grep", "find", "ls"],
"network": "api-only",
"authority": {
"withinRole": ["task.update.assigned", "git.push.working", "review.request", "message.send"],
"crossRole": ["task.scope.change"]
},
"credentials": [
{ "service": "gitea", "scopes": ["write:issue", "write:repository", "read:user"] },
{ "service": "vikunja", "scopes": { "tasks": ["read_one", "update"], "tasks_comments": ["create"], "projects": ["views_buckets_tasks"] } }
]
}
```
- `name` matches the file name. `contract` is a Markdown file name in the
same directory, not a path. It must exist, be a regular file and not be
empty.
- `authority` names actions from the closed list in `src/vocabulary.mjs`.
`withinRole` actions run without asking. `crossRole` actions need the
arbiter. Any action not listed is gated: it goes to Jason, or to the
arbiter for its domain when the raise cites the project's current spec
commit (PRD 1.5 REQ-DEC-2). The nine hard waits in `GATED_ONLY`
(REQ-WAIT-1: `spend`, `data.delete`, `network.expose`, `account.create`,
`credential.mint`, `message.external`, `git.push.protected`,
`git.push.remote`, `deploy`) always go to Jason and can't appear in
either list (REQ-WAIT-2). `git.push.remote` joined in row 199 (#1697).
`GATED_ONLY` is every action that `ROUTED` doesn't name, so an action
added to the vocabulary is a hard wait until a PRD says otherwise and the
commit adds it to `ROUTED` (REQ-WAIT-3). `git.merge.protected`,
`policy.change`, `prd.approve` and `role.revoke` left the list in 1.5
(row 139, #1633).
- Gitea scopes are `read:<category>` or `write:<category>`, one level per
category. `all` and `admin` are refused. Vikunja scopes map a route group
to verbs, and only the pairs in `VIKUNJA_GRANTABLE` pass.
- Version 1 files (`roleVersion`, `name`, `tools`, `network`) still load.
They have no authority and no credentials, and a business can't use them
for a role instance.
`scripts/mosaic-task.mjs resolve-role` checks both versions through this
package and prints `MOSAIC_ROLE_CONTRACT` for version 2.
### Business file
`examples/mosaic-stack.example.json` is a complete example. As shipped it
refuses on purpose: every `botId` is 0 and every date is `YYYY-MM-DD`. Copy
it to `<configDir>/businesses/mosaic-stack.json`, put in the bot ids and
token dates from `docs/guides/slice-1-identities.md`, fix the token paths
and run `validate`. The file must belong to you and must not be writable
by group or other. The loader checks the owner and mode on the descriptor
it reads from, so the file can't be swapped between the check and the
read.
The business-level `tracker` is optional (Q247). Omit it for a business with
no tracker sync: validate and bus boot need no sync credential, and the
Console shows no sync credential entry. The loader represents omission as
`tracker: null`; writing a literal `null` in the business file still refuses.
A present tracker keeps its existing bot, credential and validation rules.
Tracker variables alone do not enable sync when the tracker block is absent.
Each role instance names a `definition` (a version 2 role file), an
optional `holder`, its Vikunja bot (`tracker`, required when the definition
uses Vikunja), a credential reference for each service the definition
lists, and optional agent-layer `vars`. Several instances may share one
definition. Bot names and bot ids are unique across the instances and the
sync bot.
A credential reference names a token file by absolute path, or an
environment variable by name, plus `rotateBy` for Gitea or `expires` for
Vikunja. The checks use `lstat` and `realpath` only. A token file must be a
regular file you own, mode 600 or tighter, not empty, and outside the
repository, `dataRoot` and every project root. A Vikunja token past
`expires` refuses. One within seven days of it, or a Gitea token past
`rotateBy`, prints a warning.
`launch` lets one instance (`by`) start the listed instances. `models` maps
selection keys to exact `{provider, model}` pairs; each role
`vars.model` selects a key. `limits` accepts optional `total`,
`providers: {provider: count}` and `models: {provider: {model: count}}`.
Counts are whole numbers of zero or more with no policy ceiling; absent
means unlimited and zero stops new launches. The example starts with four
Opus and four Sonnet sessions. The old `max` shape requires print-only
`mosaic limits migrate` before the new copy can load it. `by` must name an
instance whose definition holds `role.launch` within-role; otherwise the
file refuses. Only that instance keeps `role.launch`. Every other instance
loses it, so for them it's gated.
`launch.account` (optional, row 126) is `{name, credential}`: the account
the business's Claude Code launches spend, an identifier, and the
environment variables of one login set joined with `+` (for example
`CLAUDE_CODE_OAUTH_TOKEN`), never a value. Which sets exist is the
launcher's check. `launch.limits.tokens.daily` maps an account to
`{limit, since, reset, timeZone}`: `reset` is `HH:MM`, `timeZone` an IANA
name `Intl` accepts. A daily quota for any name but `launch.account.name`
refuses the file, since it would count nothing.
`launch.limits.tokens.keys` holds token limits by key name. A leaf named by a
twelve-hex model key id with `{limit, since, reset, timeZone}` is a daily quota
on one agent's model key (row 333); a plain `{limit, since}` leaf stays a
cumulative account limit, even when its name is twelve hex characters. Only the
human sets or resets a model-key quota. The settings writer stamps `since` as
for every token limit (a new limit gets now, a changed one keeps its time, a
caller's different time refuses, a reset moves it to now), with one rule for a
new model-key quota: it takes the earliest `since` among the old file's
model-key quotas whose ids are in the same agent's key lineage, so moving a
quota to the agent's new key id keeps its count, and takes now only when none
of them holds one. A quota removed in one write and added back in a later one
is new and gets now. The bus host passes `keyLineage(business, keyId)`, which
answers ids only from the bus's own credential records. A writer built without
it, a lookup that throws, or an answer that is neither an empty list nor a list
of key ids holding the asked id refuses a write that adds a model-key quota with `proxy-key-refused`.
A write that adds none never calls it.
`launch.threadIdleSeconds` (optional, row 121) is how long a thread's
session runs with nothing pending and no new message or open before the
launcher stops it. Absent means 900 (`THREAD_IDLE_SECONDS` in
`src/organization-config.mjs`). A set value is a positive integer with no upper bound;
anything else refuses the file. The key is accepted whether or not the file
declares a thread instance. A thread instance is one whose definition is
`assistant`. The launcher starts it once per thread, when the thread has a
pending message or was opened, and never on its own.
`plugins` (optional) switches on plug-in modules, one key per kind from
`packages/plugins/src/kinds.mjs`, each a list of module names matching
`^[a-z][a-z0-9-]*$` with no name twice. An unknown kind or a bad name
refuses the file. Absent means nothing is switched on. Whether a named
module exists and loads is the plug-in loader's check, at load time; see
`packages/plugins/README.md`.
### Chief-of-staff and assistant instances
Two roles stand above the PM (PRD 1.2; row 119, #1613).
The `assistant` role (`roles/assistant.json`, contract
`roles/assistant.md`, REQ-ASSIST-1) is the user's main agent: one per user
per organization. It talks with the user and passes work to the chief of
staff. It holds `message.send` within role and nothing else: no
credentials, no crossRole action, read-only tools. It doesn't direct
agents, create or assign tasks, or launch sessions. The broker's send
limits for it are row 120 (REQ-ASSIST-2).
The `chief-of-staff` role (`roles/chief-of-staff.json`, contract
`roles/chief-of-staff.md`, REQ-COS-1) runs the organization: it hires and
terminates role instances, owns the structure, takes work from the
assistant and never talks to the user. It holds `message.send` and
`role.launch` within role (decision 139), plus `role.end` and
`organization.change` (row 123): it ends a run with a reason and no decision
(no shipped role is granted `role.revoke`, so it stays gated), and it changes the organization
file through the broker's `organization.set`, limited to the keys the human's
`limits.set` takes; the harness's `end_run` and `change_organization` tools
call them (row 132). Under Jason's ruling Q169 (row 155, #1649) the chief of
staff is the one agent that ends the assistant, which ends every assistant
thread run, and it configures the assistant through `assistants` (Assistant
settings, below) and names instances through `set_name`. PRD 1.7 REQ-COS-3
(row 194) grants it `role.hire` and `role.fire` within role: it hires and
fires with a reason and no decision, through the broker's `instance.hire` and
`instance.fire`, which the harness's `hire` and `fire` tools call. It has the same read-only tools. When the business declares a library, its one credential reference is gitea, for the read grant the library mirror fetches with (row 285, amendment 1, mos-1580). With no library it holds no credential references. The broker holds any token, never the agent. It is the only shipped role that holds `role.launch`, so
it is the only role `launch.by` can name. The PM holds no `role.launch`
(REQ-LAUNCH-1); it asks the chief of staff for a session.
The owner adds both instances under `roles` in the business file, next to
the others, and points `launch.by` at the chief of staff. Initial role entry
and schema migration installation are human steps; later launch settings edits
use the shared, broker-backed organization writer. The instance names are the owner's choice; these are the
example file's:
```json
"chief": { "definition": "chief-of-staff", "vars": { "model": "opus" } },
"assistant": { "definition": "assistant", "holder": "mos", "vars": { "model": "opus" } }
...
"launch": { "by": "chief", "instances": ["coder", "reviewer"], "models": {
"opus": { "provider": "anthropic", "model": "claude-opus-5-5" },
"sonnet": { "provider": "anthropic", "model": "claude-sonnet-5-5" }
}, "limits": { "models": { "anthropic": { "claude-opus-5-5": 4, "claude-sonnet-5-5": 4 } } } }
```
Neither entry has a `tracker` or credential references in this example, which has
no library. A business with a library must give the chief exactly one `gitea`
reference; a business without a library must give it none (mos-1580). The
assistant has none. A
`{ "store": true, "rotateBy": "YYYY-MM-DD" }` reference works as well as a
file. Neither form creates an account, mints a token or grants anything. A session that launches needs a harness and a model, so give each
instance `vars` (`harness`, `model`) as the PM has, unless the system's
execution settings cover it. A `launch.by` that names a PM, an assistant or any other instance
whose role lacks `role.launch` is refused. A file that still names the PM
fails to load with the shipped roles, so edit the file before a copy with
these roles goes live. `packages/organization/tests/chief-of-staff.test.mjs` and
`assistant.test.mjs` check each role against its approved list and load it
through `loadBusiness`; `role.test.mjs` loads every file in `roles/`
through the validator.
### Security-reviewer and merge-gate instances
Two roles finish the coding workflow next to `coder` and `reviewer`
(REQ-ROLE-5; row 149, #1643).
The `security-reviewer` role (`roles/security-reviewer.json`, contract
`roles/security-reviewer.md`) reviews a candidate for security only. It
holds `review.verdict` within role and nothing else: read-only tools
(read, grep, find, ls), no shell, no write tools, no credentials. The
verdict reaches the role that asked through `review.verdict` itself, so the
role needs no `message.send`. Without it the runner's automatic reply to
a message is refused; the verdict still arrives.
The `merge-gate` role (`roles/merge-gate.json`, contract
`roles/merge-gate.md`) lands approved work. Its one grant is
`git.merge.protected` as a cross-role action, the grant row 137's land op
checks. It raises the merge decision itself with domain `technical`,
because only the raising instance and thread can use an approval
(REQ-DEC-5), and lands once the technical arbiter resolves it. It has the
same read-only tools and no credentials.
Both declare messaging lists. The technical arbiter must be able to
message merge-gate, or the raise is refused (row 133): the shipped lists
include `cto`, so a business whose technical arbiter is another
definition has to change merge-gate's lists by a reviewed commit. Because
of those lists, neither role can be named in `arbiters`; the business
file is refused.
No verb adds a role instance. `organization.set`'s `instances` only sets
the model of an instance the business file already declares
(`launch-settings.mjs`), so the human adds both entries by editing the
business file, then runs `validate`. No shipped business declares either
role yet; adding them to a live business is a separate step with its own
go. The entries are:
```json
"merge-gate": { "definition": "merge-gate" },
"security-reviewer": { "definition": "security-reviewer" }
```
`packages/organization/tests/workflow-roles.test.mjs` checks both against
their approved lists, the arbiter refusal and the proactive line in every
contract; `packages/bus/tests/workflow-roles.test.mjs` checks the merge
route and the verdict path on the broker.
### Project file
```json
{ "projectVersion": 1, "id": "stack",
"vars": { "tracker.project": 3, "git.workingBranch": "refactor", "git.protectedBranches": ["main", "next"] },
"roles": { "coder": { "vars": { "limits.tools": ["read", "edit", "bash"] } } } }
```
`id` must match the business file's name for the project. A missing
project file is a warning in `validate` and an error in
`resolve --project`.
## Variables
Every key is declared once in `src/vars.mjs` with its type, the layers
that may set it and its merge rule. An unknown key refuses, and so does a
key set at a layer it doesn't belong to.
Layers, least specific first: defaults, system (the system config), business
(`vars`), project (`vars`, then `roles.<instance>.vars`), agent (the business
file's `roles.<instance>.vars`). For a plain key the most specific layer
wins. `limits.tools`, `limits.network` and `limits.authority` intersect:
each layer that sets one narrows it, and the result also narrows the role
definition. `limits.authority` is an allowlist, so a role action it leaves
out becomes gated.
`bus.tester` is business-only and defaults to false. Set to `true`, the bus
binds a tester from `tester-cli.mjs` for PM tests; it reads the value at bus
start, so a change takes a bus restart.
A managed session's route is three agent vars (row 65, REQ-PLUG-2):
`harness` names a harness plug-in module (the loader's name pattern),
`model` is a `launch.models` key, and `thinking` is one of `off`,
`minimal`, `low`, `medium`, `high`, `xhigh` and `max`. Whether the module
exists, is switched on and applies the level is checked at launch and at a
write, never at load. `fallback.harness`, `fallback.model` and
`fallback.thinking` state a second route; a layer sets all three or none,
and a partial set refuses. No layer falls back to the system's
`execution.adapter` or `execution.model`, which stay for `run-task.sh`.
`resolve` prints `provenance` beside `vars`. For a plain key it's the
layer that set the value (`default`, `system`, `business:<id>`,
`project:<id>`, `project:<id>:roles.<instance>`,
`business:<id>:roles.<instance>`). For a limit it's the list of layers
that narrowed it.
## API
```js
import {
loadOrganization, loadProject, systemVars, resolveInstance, classify,
loadRole, validateRoleDocument, checkCredentialRef,
} from "../packages/organization/src/index.mjs";
const business = loadOrganization("mosaic-stack", { rolesDir }); // frozen
const project = loadProject(business.projects.stack.root); // frozen
const coder = resolveInstance({ system: systemVars(config), business, project, instance: "coder" });
classify(coder, "git.push.working"); // "within" | "cross" | "gated"
```
`resolveInstance` returns a frozen record: `business`, `project`,
`instance`, `definition`, `holder`, `contract` (absolute path), `vars`,
`provenance`, `limits` (`tools`, `network`, `authority.withinRole`,
`authority.crossRole`), `credentials` (references only), `tracker` (`bot`,
`botId` or null), `launch` (the launch block, or null) and `digest`, the
SHA-256 of the record's canonical JSON. `classify` refuses an action
outside the vocabulary. `launch` is set exactly when `classify(record,
"role.launch")` is `within`. If `limits.authority` leaves out
`role.launch` for the launcher, `launch` is null. A launcher should
still ask `classify`, not read `launch` alone.
An assistant also receives `coordinator`: the validated business's `launch.by`
as `instance`, and that instance's configured `identity.name` as `name` (or
null). With no launch block, `coordinator` is null. This routing context is
included in the resolved digest and the generated session prompt; it does not
grant authority. The name is descriptive, not a broker display-name alias;
the assistant addresses messages to the exact instance key.
Errors are `OrganizationConfigError` with `exitCode` 2 (invalid) or 4 (a
required file missing). `BusinessError` is the same constructor under its old
name, and an error's `name` is still `BusinessError`. `OrganizationError`
(`src/organization.mjs`, the organization writer's error with a `code`) is a
different class. `mosaic org config` adds 3 for a system config problem and 4
for usage. The old names `loadBusiness`, `validateBusinessDocument` and
`businessFilePath` are the same functions as `loadOrganization`,
`validateOrganizationDocument` and `organizationFilePath`.
## Limits
- The action vocabulary lives here, on the host. Neither workers nor
container images read it, so it isn't under `contracts/`.
- The checks run as you, against files you can change. They catch
mistakes; they don't stop someone who already runs as your user.
- A token file can change between `validate` and the moment it's opened.
The code that opens it at run time repeats these checks (rows S2 and S3).
- Slice 1 knows two services and one tracker kind. A new one is a
reviewed change to `src/vocabulary.mjs` and `src/vars.mjs`.
## Organization writes
`src/organization.mjs` is the trusted host write module. The caller supplies
a verified actor and the SHA-256 revision read earlier. It validates the whole
document, and again as the bytes it will write read back as JSON, so a value
JSON can't carry (a `Date`, a `Map`) refuses instead of landing as something
else; `changes.jsonl` logs those bytes. It refuses stale revisions and concurrent writers, keeps exact previous
bytes under `businesses/.history/<id>/<transaction>/previous.json`, then publishes
a fsynced temporary file by atomic rename. `changes.jsonl` records prepared and
committed transactions with actor, hashes and old/new values per changed field.
Write-time recovery compares prepared hashes with current bytes; uncertain or
torn evidence refuses organization writes. Bus startup, Settings reads and
launches keep using the whole validated file on disk, without consulting receipts. A stale writer lock also refuses: an operator must
confirm that its old writer is gone before clearing it; the host does not
race another recovering process by deleting locks automatically. Settings reads never repair evidence themselves.
The narrow `setProjectRemote` host method can fill only a missing remote on an
existing software project. It checks the actor, revision, normalized HTTPS
repository URL and the exact document delta under the same lock. It refuses
libraries, credentials in URLs, replacement and clearing. It does not contact
the remote or grant publishing authority. The Console setter is currently
closed until session liveness can be verified at the commit boundary.
`launch-settings.mjs` restricts human edits to limits, model table, instance
model selections, routes and assistant settings, and checks every route before publication. `plugin-settings.mjs`
is the only writer of `plugins`: the host calls it, and no broker verb, `mosaic`
command or Settings form reaches it. A refused `plugins` write throws
`invalid-plugins` or `invalid-request` before the writer runs and logs nothing. Migration
uses none of this: it only prints a converted document (decision 145).
### Routes (row 65, #1558, mos-0840)
`launch-settings.mjs` also has the routing table's read and write. The bus
reaches them through the human-only verbs `routes` and `routes.set`. Neither
is in `ORGANIZATION_KEYS`, so `organization.set` and the chief of staff's
`change_organization` can't send a `routes` key. Through `limits.set` the
chief changes an instance's `model` through `instances`, and the assistant's
whole vars through `assistants` (Q169). Both writes run the same
`checkRoutes`.
`routes(id)` returns the revision, the switched-on harnesses
(`plugins.harness`), the `launch.models` keys, `levels` (each harness the
file names or switches on, with the levels its module applies, or null for
a module that didn't load) and one entry per instance: `harness`, `model`,
`thinking` (null when unset) and `fallback` (`{harness, model, thinking}`
or null). `setRoutes(id, args, actor)` takes:
```json
{ "revision": "<64 hex>", "routes": { "coder": { "harness": "pi", "model": "glm-5.3-flash", "thinking": "high", "fallback": null } } }
```
Each named instance's route vars are replaced in one write, and a null
`fallback` clears all three fallback vars. It refuses, writing and logging
nothing:
| Code | When |
|---|---|
| `invalid-request` | a key other than `revision` and `routes`, a bad revision, an empty `routes`, or a route without exactly `harness`, `model`, `thinking` and `fallback` as strings |
| `BusinessError` (the bus answers `invalid-request`) | the next document fails `validateBusinessDocument`: a level no harness has, a harness name outside the pattern, or a `model` that isn't a `launch.models` key |
| `organization-unknown-instance` | an instance the file lacks |
| `organization-partial-fallback` | a fallback that isn't null and isn't exactly three strings |
| `organization-no-change` | the file already says that |
| `unknown-model` | a `fallback.model` that isn't a `launch.models` key |
| `harness-off` | a changed route's harness isn't in `plugins.harness` |
| `harness-refused` | a changed route's harness module didn't load |
| `thinking-unsupported` | a changed route's module doesn't apply its level |
| `revision-conflict` | the file changed since the read |
`checkRoutes` is shared: `instances.set` and `limits.set` run it too, so no
write of the route vars skips it. It checks a route only when the write
changes its harness or thinking, so a model change still saves while
`plugins.harness` is unset, and that instance's launch refuses
`harness-off` instead. The model check covers every set model and
fallback model. A route var
the instance doesn't set is left to the launch, which refuses `no-route`.
The bus host passes `levels` from `harnessLevels` (`packages/bus`); a writer
built without it answers null for every harness.
### Assistant settings (row 155, #1649)
Jason's ruling Q169 lets the human and the chief of staff configure the
assistant. The update key is `assistants`, next to `limits`, `models` and
`instances`:
```json
{ "revision": "<64 hex>", "assistants": { "assistant": { "vars": { "model": "opus" }, "tracker": null } } }
```
- The key maps an instance to any of `vars`, `tracker` and `definition`. A
given field replaces that field of the entry, `tracker: null` removes the
tracker, and a field left out stays. An empty entry is `invalid-request`.
- The instance must exist (`invalid-request`) and its definition must be
`THREAD_DEFINITION` from `vocabulary.mjs`, or the write refuses
`organization-not-assistant`.
- A given `definition` must be `THREAD_DEFINITION` too, or
`organization-definition-refused`. Today that makes it a no-op, so a change
can't strip the protection `END_PROTECTED` gives the assistant.
- `holder` and `credentials` aren't accepted; they stay hand edits.
- After applying every key, `set` refuses `organization-definition-refused` if
any instance's definition differs from the file's, so no key gives an
instance an `END_PROTECTED` definition after a decision about it was resolved.
- The whole next document still goes through `validateBusinessDocument` and the
model check, the same as a hand edit.
`read` returns `assistants` as well: each assistant instance's `definition`,
`vars` (`{}` when absent) and `tracker` (`null` when absent). The human sends
the patch with `mosaic limits set --file` or the Console's Assistant section;
the chief of staff sends it with `change_organization`. The chief of staff
reads the revision with `organization.revision` (row 134), which returns the
revision alone.
`changes.jsonl` logs the actor: the human's id, or `chief-of-staff.<instance>`.
For inherited selections, print-only migration reads the existing system config
through its normal validator and requires an exact provider/model match. Missing
or invalid config refuses; it never changes the system config. Explicit role
models retain their external model and use the human-supplied provider table.
### Role instances (row 165, #1662)
Q171 and REQ-ROLE-3 let the human add a role instance and change an existing
one's definition, vars and launch membership without a hand edit.
`src/instance-settings.mjs` is that writer. The bus reaches it through the
human-only verbs `instances` and `instances.set`. Neither is in the broker's
`ORGANIZATION_KEYS`, so `organization.set` and the chief of staff's
`change_organization` can't reach it (sage-0918).
`read` returns the file's revision, each instance (`definition`, `vars`,
`launched`, `launcher`, `protected`), whether the file has a launch block, and
each definition in the roles directory with the services its credentials name
and whether `add` and `set` accept it. `set` takes:
```json
{ "revision": "<64 hex>", "instance": "sec-1", "op": "add", "definition": "security-reviewer", "vars": {}, "launch": true }
```
- `add` needs `definition`; `change` needs at least one of `definition`,
`vars` and `launch`. `vars` replaces the instance's vars whole. `launch`
adds the instance to `launch.instances` or removes it. Any other key is
`invalid-request`, so `holder`, `credentials` and `tracker` stay hand edits.
There is no delete.
- An instance named `human` or `tester` refuses with `invalid-request`. The
broker refuses a business whose roles use either name when it loads, so
such an add would stop the bus at its next start (row 165 r2).
- A definition that declares credentials refuses with
`organization-credentials-out-of-scope` (the brief's named refusal, sage-0922).
So does a definition change on an instance whose definition declares them,
because its credentials would bind to the new definition's authority. The
writer never writes an instance without its binding. A vars or launch change
on such an instance succeeds and leaves `credentials` and `tracker` as they
are.
- An `END_PROTECTED` definition, given or held, refuses with
`organization-definition-refused`; the assistant changes through
`assistants` above.
- `organization-instance-exists`, `organization-unknown-instance`,
`organization-launch-not-configured` and `organization-no-change` refuse
what their names say.
- The whole next document goes through `validateBusinessDocument`, the route
checks (`checkRoutes`, under "Routes" above), and each project file's id and instance resolution
(`organization-project-mismatch`, `organization-unresolvable-instance`),
the checks `mosaic org config validate` runs.
- The organization writer then refuses a stale revision
(`revision-conflict`) and logs the change with the actor.
A refusal writes nothing and logs nothing. The answer is the read with the
`transaction` and `restart`. `restart` is true after an add or a definition
change: the running broker keeps the roles it loaded at start. The writer
doesn't restart anything. The CLI (`mosaic org instance add|set`, or the old
spelling `mosaic business instance add|set`) and the
Console Settings page send the patch.
### Organizations (development release)
`org-runtime.json` beside the instance config is private runtime metadata. It
reserves each organization ID and its derived `<dataRoot>/org-runtime/<id>`
root, which carries an instance-and-ID marker. It is not a system config.
The organization service writes the existing `businesses/<id>.json` format so
`loadBusiness` can read it. Organization updates require the current revision;
a conflicting revision refuses. A document or marker that disagrees with the
registry refuses rather than falling back to another organization.
Use `mosaic org template render ID --fixture local --output FILE`, edit the
non-secret JSON if needed, then `mosaic org validate FILE` and
`mosaic org create ID --from FILE`. Show and list use `mosaic org show ID
[--json]` and `mosaic org list [--json]`; update with `mosaic org update ID
--from FILE --if-revision N`. The development template identifies the local
owner, references reviewed roles, and declares the `work` project. Its path is
replaced with a root derived from the selected instance and organization.
Initialize an organization checkout with `mosaic org queue init ID --local-only
--accept-local-loss-risk` and a project repository with `mosaic org project repo
init ID PROJECT --local-only --accept-local-loss-risk`. The explicit acceptance
records that this machine may hold the only copy. Alternatively use `--remote
URL` in place of both local-only flags. Both modes create a local repository and
genesis commit. Existing paths are never overwritten. `mosaic org queue show ID
--json` and `mosaic org project repo show ID PROJECT --json` read the receipts
back; the project form also reports the binding as it is now (`current`).
Remote mode (#1873 P1-6) accepts `https://HOST/OWNER/REPO` and ssh
(`ssh://[USER@]HOST[:PORT]/PATH` or `USER@HOST:PATH`) only. It refuses
`ext::` and other transport helpers, `file://`, local paths, values that begin
with `-`, any URL that carries a password or token, and any https URL that
carries a user name (`src/vocabulary.mjs` `parseRepoRemote`). The checkout's `origin` is set to the
URL and checked once with `git ls-remote` (`src/repo-remote.mjs`): no
credential helper, no askpass, no terminal prompt, no redirects,
`GIT_ALLOW_PROTOCOL=https:ssh`, no git or ssh config files, no ssh agent or key,
and a 15 second limit. The receipt's `check.result` is `reachable`,
`needs-credential` (the check sends none) or `unreachable` with git's error
summarized. The ssh check accepts any host key, so an ssh answer shows only that
something at that address speaks git; its message adds "the ssh host key was not
verified". `check.url` names the URL it ran against, and `repo show` reports a
check only for the URL recorded now. The check never refuses the setup, and
nothing is pushed. A copy credential binding (CB lanes) supplies credentials
for https Gitea remotes only; an ssh or other https remote that needs a
credential gets none from it. Tests reach a local bare repository only through `repoRemoteTestSeam`,
which no source module may import.
`mosaic org project repo set ID PROJECT (--local-only --accept-local-loss-risk |
--remote URL) --if-revision N [--json]` (#1873 P1-7) changes an initialized
project's repository: a new URL, or remote to local only and back. It runs the
same URL rules and check, sets or removes `origin`, records the binding in the
organization file's `projects.PROJECT.git`, and writes an
`org.project.repo.set` receipt. A stale `--if-revision` is `revision-conflict`.
While a copy binding that names the project is approved (`pending`) or active
(`bound`), repo set is refused with `project-repo-bound`, before any check or
change to `origin`, even for the URL already recorded: the binding grants the
repositories its projects name at credential issue, and its decision records
only project names (#1873 F7). The message names the copies. Revoke the copy
binding first (the chief raises an unbind decision and the owner approves it),
then set the repository, then ask the chief to raise a new binding decision. An
`unbound` binding, or none, does not refuse. A binding decision raised but not
yet approved is not read here.
Queue review transports and transitions are not provided by this setup.
The local development owner is accepted only through the existing named-instance
selection on this release's throwaway development instances. Receipt subjects
name that principal, not Jason. The dev-purpose bootstrap fence is a separate
follow-up before person-used deployment.
## Instance accounts (Phase M2)
`src/accounts.mjs` validates `<configDir>/accounts.json` without a dependency or
JSON schema engine, as `access.mjs` does. `checkAccounts(document, {providers})`
and `loadAccounts(file, {providers})` return frozen documents. Providers are the
configured `console.sso.providers` objects (including issuer). Reads refuse an
unsafe config directory, unsafe file, unknown field or malformed row. Account
ids may be organization humans. `accountsRepairs(document)` reports unknown
roles and rights retained on read for rollback; these rows must grant nothing.
M4 supplies the authorization evaluator; M2 alone grants no Console or bus access.
`withAccounts(ctx, {revision, verb, actor?, registry?, init?}, mutate)` takes
`accounts.lock`, reloads, checks the expected file revision, awaits `mutate` on a
copy, maintains `authGen` and membership `rev`, validates and atomically replaces
0600 bytes. Init publishes with a no-clobber hard link; replacements use rename.
Both fsync the config directory before the receipt is appended. `ctx` contains `configDir`, `instance`, and `providers`. Init requires
revision 0 and an absent file. Other writes require an existing valid file. Writes
refuse unknown rights/roles on new or changed rows. Unchanged rollback rows pass
through exactly, so they cannot block account revocation or another row's repair. Role templates are never evaluated as authority by this store.
For a caller already holding accounts.lock,
`withAccountsLock(ctx, handle => withAccountsLocked(handle, options, mutate))`
reuses that live opaque handle. Never acquire an earlier lock from the callback.
The config path must have no symlink ancestors, including the home directory.
The order is lifecycle, registry, accounts. Registry-dependent writes pass the
live document from `withRegistry`; `readRegistryLocked(registry, ctx?)` verifies
the lock and rereads its persisted snapshot without relocking. A changed registry
or an instance mismatch refuses before account publication. Callbacks cannot
reuse a released handle, or run two writes at once through one handle.
Each committed write appends and fsyncs one receipt under the accounts lock in
`accounts-receipts.jsonl`: effective OS uid, actor id, verb, old/new file revisions, changed
account ids with changed field names, and before/after membership rights, scope
and revisions. No account
name, subject, invite binding, hash, raw token or cookie enters the receipt. The
store preflights the receipt destination before replacing accounts. As with
other two-file journals, a crash or IO failure between replacement and receipt
append can leave committed state without its receipt; an observed append failure
has `committed: true` and must be inspected before retrying. It never rolls back
committed authority silently or fabricates a success receipt.
### Organization autonomy (AU)
The optional `autonomy` object selects `in the loop`, `near the loop` or
`out of the loop` for `cross-role`, `merge`, `policy` and `authority`.
Without it, cross-role, merge and policy retain their arbiter routing and
plain hire/fire/change retain person approval. Only the owner Console writes
this setting; every change records the authenticated owner and writer receipt.
In the loop, the arbiter resolves and a person confirms or returns the decision
with a reason. Near the loop, the agent's resolution takes effect and the owner
receives a Console notice. `reverseWindowHours` is an integer from 1 to 168
(default 24). `notices: true` is required near the loop. `authorityResolver`
names a distinct organization instance holding `decision.resolve.authority`
withinRole; flagged grants, credential lines and root agents always retain
owner approval. No setting changes hard waits or the PRD request-mode switch.
Moving toward the loop needs no prerequisites. Moving away requires the class's
resolver, inverse, notices and review/limit prerequisites. Moving from near to
out also requires an actual near-loop resolution and a later owner trail review. Authority near requires an enabled limit check and an aggregate quota covering every selectable model (or the shared account when every enabled harness uses it). Empty budgets and existing-instance caps do not cover future hires. Authority out refuses `authority-limits-unavailable` until per-definition limits exist. A task inverse restores its prior
stored fields if no later edit intervened; a merge inverse lands a gated revert
commit. Hire/fire/change inverses reuse the existing lifecycle and writer paths,
never restore credential bindings, and refuse stale targets. Authority inverses check the window immediately before file publication. After a durable commit, completion-event, reload or relaunch failures are reported as recovery metadata. The inverse is recorded as committed; if that trail write itself fails, the response says `trailPending` rather than recording a false reversal failure. A refusal after stop/revoke leaves the agent stopped; inspect its trail before relaunching. External task
adapters without host-owned inverse evidence refuse near-loop consumption.
After the pinned window, use an ordinary new decision.
Policy has no effect executor. Per the Lead ruling, its in and existing out
levels work, while near refuses `policy-reverse-unavailable` and directs the
owner to choose in or out. Policy near and its reverse wait for that executor.