Adds the system-type kind with five modules (coding, creative-writing, marketing, montage, business-management), an org-level enabled list validated fail-closed, project selections limited to the org's list, read verbs, and bus migration 3t-3u. REQ-SYS-2 toolsets are designed and deferred: they need a role authority contract.
586 lines
33 KiB
Markdown
586 lines
33 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. The package reads files and refuses on any problem. It
|
|
never writes, and it 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
|
|
https://HOST/OWNER/REPO` in place of both local-only flags. Both modes create a
|
|
local repository and genesis commit; remote mode records the URL without a
|
|
push. Existing paths are never overwritten. `mosaic org queue show ID --json`
|
|
and `mosaic org project repo show ID PROJECT --json` read the receipts back.
|
|
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.
|
|
|
|
The optional `plugins.system-type` list enables installed system-type modules for
|
|
an organization. Missing means none. Unknown ids refuse validation. Changes
|
|
are owner-only; project selections are separate bus settings and a selection
|
|
whose type is disabled remains recorded as inactive.
|