Row 79 round 1, owner Rocko, candidate manifest 16b12387 on 331d037c.
Approved by Dewey (27611); Sage's gate green.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
167 lines
7.7 KiB
Markdown
167 lines
7.7 KiB
Markdown
# Business
|
|
|
|
Role definitions, the business and project files, the variable registry and
|
|
the resolver (slice 1 row S1, #1518). 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 business validate mosaic-stack
|
|
scripts/mosaic business resolve mosaic-stack coder --project stack
|
|
node --test packages/business/tests/
|
|
```
|
|
|
|
## 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. The nine
|
|
always-gated actions (`deploy`, `spend`, `git.push.protected`,
|
|
`git.merge.protected`, `credential.mint`, `message.external`,
|
|
`policy.change`, `prd.approve`, `role.revoke`) can't appear in either list.
|
|
- 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.
|
|
|
|
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, up to `max`
|
|
sessions per model family (Opus 4, Sonnet 4 at most). `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.
|
|
|
|
### 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.
|
|
|
|
`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 {
|
|
loadBusiness, loadProject, systemVars, resolveInstance, classify,
|
|
loadRole, validateRoleDocument, checkCredentialRef,
|
|
} from "../packages/business/src/index.mjs";
|
|
|
|
const business = loadBusiness("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.
|
|
|
|
Errors are `BusinessError` with `exitCode` 2 (invalid) or 4 (a required
|
|
file missing). The CLI adds 3 for a system config problem and 4 for usage.
|
|
|
|
## 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`.
|