docs(plans): starter packs security and re-check fixes

Saved templates: owner-scoped path, file checks before read, provenance
reduced to id and version, agent verbs refuse origin saved. Pack, board
and workflow writes stay owner only until row 193. Board and workflow
verbs, writer path, read paths and owner lists now sit in named slices.
SP5 matches organization.set (decision-required). SP9 red test is red
today. Kind removal moves to a follow-up row; SP10 is the conversion.
This commit is contained in:
jason.woltje committed 2026-11-12 14:57:58 -06:00
1 parent 2404729232
commit dd878cefcf
1 file changed
+319 -149
+319 -149
View File
@@ -57,7 +57,7 @@ Status means: **exists** (built and usable by a pack as is), **partial**
| Loading system types | `packages/plugins/src/system-types.mjs:5` `installedSystemTypes` | exists | Any refused module, or keys other than `description,id,label,toolsets`, throws `system-type-invalid`. |
| Org enable | `plugins['system-type']` in the org file; the kind is checked by `checkPlugins` (`packages/plugins/src/kinds.mjs:146-148`, `unknown-kind`, called at `packages/organization/src/organization-config.mjs:358`) and each module at `organization-config.mjs:360-365`; fenced by `systemTypesFence`, `packages/organization/src/organization.mjs:126-131` (`organization-system-types-human-only`) | exists | Read back by `system-types.current`, `packages/bus/src/runtime.mjs:904`. Console `GET/POST /api/system-types` (`packages/webui/src/system-types-http.mjs:4-9`), right `org.setup` (`packages/organization/src/org-rights.mjs:90`). |
| Project selection | `project.system-types[.set]`, `packages/bus/src/broker.mjs:8476-8495`; table `project_system_types` in `packages/bus/src/migrations/3t-3u.sql` (append-only) | exists | Human only. CLI `mosaic project system-types` and `system-types-set`, `packages/cli/src/project.mjs:17-18`. |
| Other system-type surfaces | CLI `mosaic org system-types installed\|enabled` (`packages/cli/src/org.mjs:5,15,49,51`); channel verbs (`packages/plugins/channel/cli/index.mjs:9-10`); Console page `packages/webui/src/public/system-types.js` with hooks in `workspace.js:173,193-194`, `chat.js:623`, `serve.mjs:23,85-86,826`, `webui/src/cli.mjs:9,193`; refusal `system-type-not-enabled` (`packages/bus/src/refusals.mjs:183`) | exists | All label plumbing; none affects an agent. |
| Other system-type surfaces | CLI `mosaic org system-types installed\|enabled` (`packages/cli/src/org.mjs:5,15,49,51`); channel verbs (`packages/plugins/channel/cli/index.mjs:9-10`); Console page `packages/webui/src/public/system-types.js` with hooks in `workspace.js:173,193-194`, `chat.js:623`, `serve.mjs:23,85-86,826`, `webui/src/cli.mjs:9,193`; refusal `system-type-not-enabled` (`packages/bus/src/refusals.mjs:183`); bus reads `system-types.installed` and `system-types.current` with the `installedSystemTypes` import (`packages/bus/src/runtime.mjs:30,903-904`); right `'system-types.console': 'org.setup'` (`packages/organization/src/org-rights.mjs:90`); the `project` dispatch list (`packages/cli/src/cli.mjs:781`) | exists | All label plumbing; none affects an agent. |
| System type effect on agents | launcher, harness, `packages/organization/src/resolve.mjs` | missing | No launch path reads system types (no hits in those files). Every module's `toolsets` is empty. |
| Shipped role templates | `roles/*.json` + `.md` | exists | V2 roles: assistant, chief-executive-officer, chief-financial-officer, chief-information-officer, chief-information-security-officer, chief-marketing-officer, chief-of-staff, coder, companion, continuity, cto, fact-checker, ideas, merge-gate, pm, proofreader, research, reviewer, security-reviewer. `researcher.json` is V1 and refused by `readTemplate`. |
| Template identity | `packages/organization/src/definitions.mjs:174,180` `templateHash`, `templateHashes` | exists | Name plus sha256. |
@@ -117,10 +117,27 @@ plug-in: the instance owner does it at the host (`installPlugin`, R10), under
the same host-authority warning. `checkPack` guards what a pack copies into
an organization; it does not contain the code that produced it. Someone who
wants to share a pack without that trust shares a saved template file, which
is data only. A
saved template is data only. It is read with `parseJsonFileText`
(`packages/organization/src/util.mjs:66`), never imported or run, capped at
256 KiB, and passes `checkPack` with origin `saved`.
is data only.
A saved template is read, never imported or run. `parseJsonFileText`
(`packages/organization/src/util.mjs:66`) is a bare `JSON.parse` with no file
checks, so `readSavedTemplate` does them first:
- the account and the template name each match `NAME`
(`packages/plugins/src/kinds.mjs:20`) before any path is built, so `..`
and `/` never reach the file system;
- the path resolves only under `<configDir>/templates/<caller account>/`;
`saved:<name>` never names another account's file;
- `lstat` refuses a symlink, a file not owned by the instance's uid, and a
mode wider than 0600; the account directory is created 0700;
- the size is checked before the read and refuses above 256 KiB;
- the parsed document passes `checkPack` with origin `saved`.
Saved templates are visible only to their saver. The `packs` reads list
shipped and installed packs only; saved ones appear only through
`template list`, which is owner only and lists the caller's own. The agent
verbs refuse origin `saved` with `human-required`, so an agent in one
organization can never preview the owner's role text from another.
The origin is set by the code that read the document, from where it was
read: `shipped` for a core module, `installed` for a PL1 module, `saved` for a
@@ -183,7 +200,8 @@ Pack definitions carry no `rev`. Apply sets `rev: 1` before
refuses with `starter-pack-invalid` unless all of these hold:
- Exact top-level keys as above. Origin `saved` also allows one more key,
`packs` (provenance text, section 5). Nothing else differs by origin except
`packs`, a list of at most 32 `{id, version}` entries (`id` matching
`NAME`, `version` semver, no other key). Nothing else differs by origin except
the `from` rule below. `scope` is `org` or `project`.
- `id` matches `NAME` (`packages/plugins/src/kinds.mjs:20`). `version` is a
semver string.
@@ -276,10 +294,12 @@ and authorize every action against that session's own effective role and
project sandbox, as every bus verb does today. A step's `roles` names are
labels for planning, never authority.
Workflow text reaches other agents' prompts, so its writers matter. Only a
person with `org.hire` or a holder of `organization.change` writes it, and
the chief can already message any agent, so this opens no new channel. The
text still gets the held-credential scan and a size cap (20 steps, titles up to 120
Workflow text reaches other agents' prompts, so its writers matter. Only
the owner (a person with `org.hire` once row 193 lands) or a holder of
`organization.change` writes it. The chief can already message any agent,
so a workflow is a standing copy of text the chief could send anyway, but
it persists in every matching prompt. That is why it renders as quoted
data under a fixed heading, below the contract (SP7). The text still gets the held-credential scan and a size cap (20 steps, titles up to 120
characters, description up to 1,000).
### Versioning
@@ -350,15 +370,22 @@ exactly as if a person had made them by hand. The steps:
| Layer | Name |
|---|---|
| CLI (owner verb) | `mosaic org pack list`, `mosaic org pack preview ID PACK [--project P]`, `mosaic org pack apply ID PACK --digest D [--project P]` |
| Console route | `GET /api/packs`, `POST /api/orgs/<id>/packs/preview`, `POST /api/orgs/<id>/packs/apply` |
| Bus ops | `packs` (read), `pack.preview.console`, `pack.apply.console`; and for agents, `pack.preview` and `pack.apply` for a role whose file lists `organization.change` |
| `VERB_RIGHTS` | `packs` to `board.read`; `pack.preview.console` and `pack.apply.console` to `org.hire`, plus an owner check when the pack's origin is `saved` |
| CLI (owner verb) | `mosaic org pack list`, `mosaic org pack preview ID PACK [--project P]`, `mosaic org pack apply ID PACK --digest D [--project P]`; `mosaic org board set ID FILE [--project P]`; `mosaic org workflow set ID NAME FILE [--project P]`, `mosaic org workflow remove ID NAME [--project P]` |
| Console route | `GET /api/packs`, `POST /api/orgs/<id>/packs/preview`, `POST /api/orgs/<id>/packs/apply`, `POST /api/orgs/<id>/board`, `POST /api/orgs/<id>/workflows/<name>` |
| Bus ops | `packs.console` and, for agents, `packs` (reads), `pack.preview.console`, `pack.apply.console`, `board.set.console`, `workflow.set.console`; and for agents, `pack.preview`, `pack.apply`, `board.set` and `workflow.set` for a role whose file lists `organization.change` |
| `VERB_RIGHTS` | `packs` and `packs.console` to `board.read`. `pack.preview.console`, `pack.apply.console`, `board.set.console` and `workflow.set.console` to `null` (owner only) |
`org.hire` fits because the only parts of a pack that could ever matter for
capability are role definitions, and T1 already gives definition writes to
`org.hire`. Board and workflows carry no capability. A saved pack applies
only to an organization its saver owns (section 5).
Owner only, for now, because that is what the bus does today for definition
writes. `VERB_RIGHTS` maps the T1 definition verbs to `org.hire`, but the
bus still lets only the organization's one human through until row 193
(`broker.mjs:6303-6308`, refusing `human-required`), and a non-owner
member's Console session reaches only the four hire verbs
(`broker.mjs:6221`). The pack verbs follow the same rule, so this design
widens no human boundary. When row 193 opens definition writes to
`org.hire` holders, these four verbs move to `org.hire` in the same change.
Board and workflow writes go through the same writer op as apply
(`applyPack`, with only the board and workflow keys in the plan), so they
share its key fence.
### The chief applying a pack
@@ -367,7 +394,10 @@ decision (section 5). The agent `pack.apply` respects that.
Who may call it: any role whose file lists `organization.change`, the same
test `organization.set` uses (`broker.mjs:6248-6255,6321`). The shipped chief
holds it. The chief could hire another agent holding it as a plain hire,
holds it. A role without it gets what `organization.set` gives it today:
`decision-required` (`#classification`, `broker.mjs:4743`; asserted in
`packages/bus/tests/chief-of-staff.test.mjs:255-261`), so the existing
decision path stays and no new gate appears. The chief could hire another agent holding it as a plain hire,
since step 7 does not flag a line the chief already holds; that agent could
then change boards and workflows too, which carry no capability. "The chief"
in section 4 means any holder of `organization.change`.
@@ -377,6 +407,10 @@ What it does:
- it writes `board` and `workflows` directly under `organization.change`,
the same way `organization.set` writes limits today
(`broker.mjs:6247-6340`), through `applyPack`;
- it refuses a pack of origin `saved` with `human-required` (section 2);
- a workflow step that names a definition this apply cannot write is left
out, and preview marks that workflow, so an agent apply never leaves a
step naming a role that does not exist;
- it writes no definitions. Each proposed hire is raised by the chief (only
the chief raises, `role-not-chief`). For a template-reference definition
the role is `{template, sha256}`, which the `launch.noHire` check reads
@@ -391,7 +425,11 @@ What it does:
pin that no longer matches refuses `decision-stale` and a definition whose
`rev` moved refuses `definition-rev-conflict`, both existing checks
(`hire-decisions.mjs:225-226`). An inline entry whose hash differs from the
recorded one refuses the new `starter-pack-proposal-stale`.
recorded one refuses the new `starter-pack-proposal-stale`. For an inline
entry the broker also refuses at raise when the entry's `from` template is
in `launch.noHire`, with `definition-not-hireable`, the code
`hire-decisions.mjs:219` gives for a template, since the `{inline}` path
itself skips that check.
### A second pack, and re-applying
@@ -411,8 +449,9 @@ Items merge by name and nothing is overwritten:
| Proposed hire | an instance or open hire decision by that name | dropped from the new proposals |
`removed` keeps a deliberate removal from coming back on its own: the
earlier `packs` entries act as the tombstone. The person may bring the item
back on purpose with `--as` and a new name, which the preview shows as an
earlier `packs` entries act as the tombstone. The tombstone is per pack id:
pack B naming an item pack A added and the owner removed is a plain `add`.
The person may bring the item back on purpose with `--as` and a new name, which the preview shows as an
`add`. The same rule covers a board the organization removed after this pack
added it.
@@ -441,13 +480,13 @@ and change decisions stay chief-only.
| Change | A person does it directly | The chief does it | The limit at that step |
|---|---|---|---|
| Import a role template | `definition.import` (T1, `org.hire`) | Inside a hire or change decision, role `{template, sha256}` | `checkComposition`. Nothing runs until a hire. |
| Import a role template | `definition.import` (T1; `org.hire` in `VERB_RIGHTS`, owner at the bus until row 193) | Inside a hire or change decision, role `{template, sha256}` | `checkComposition`. Nothing runs until a hire. |
| Customize or invent a role | `definition.edit` or `invent` (T1) | Inside a hire or change decision, role `{inline}` | `checkComposition`; lines above the chief are flagged and only the owner approves them (R8). Editing a definition in use refuses, so a running agent never widens without a change decision. |
| Hire, fire, change an agent | `hire.start.console` (`org.hire`) | `hire.raise` (only the chief: `role-not-chief`, `broker.mjs:6583`; hired agents never hold `role.hire`, R9) | Broker-computed flagged and credential lines; authority autonomy level; root needs the instance owner. |
| Apply a pack | `pack.apply.console` (`org.hire`) | `pack.apply` under `organization.change`, no definitions | As above for every hire it later proposes. |
| Apply a pack | `pack.apply.console` (owner; `org.hire` once row 193 lands) | `pack.apply` under `organization.change`, no definitions | As above for every hire it later proposes. |
| Import a workflow or board from a pack | Part of apply | Part of the chief's apply | No capability. |
| Create or edit a workflow | `workflow.set.console` (`org.hire`) | `workflow.set` under `organization.change` | Steps name existing definitions, templates or `human`, and the five states. A workflow grants nothing; agents read it as guidance below the contract. |
| Change board labels or order | `board.set.console` (`org.hire`) | `board.set` under `organization.change` | The five states stay. |
| Create or edit a workflow | `workflow.set.console` (owner; `org.hire` once row 193 lands) | `workflow.set` under `organization.change` | Steps name existing definitions, templates or `human`, and the five states. A workflow grants nothing; agents read it as guidance below the contract. |
| Change board labels or order | `board.set.console` (owner; `org.hire` once row 193 lands) | `board.set` under `organization.change` | The five states stay. |
| Project agents, board and workflow | The same verbs with `--project` | The same, under `organization.change` | Project agent entries refuse instance credential material (`BOUND_KEYS`, `agent-levels.mjs:21`). |
| Enable a skill on the machine | `scripts/skill.sh activate` (instance owner) | Not available | Machine-scoped. |
| Install a pack plug-in | `installPlugin` (instance owner, R10) | Not available | Host authority. |
@@ -504,7 +543,11 @@ the exact JSON that will be written; save takes its digest.
the chief or the assistant, which every organization already has.
- `board` and `workflows`.
- `skills`: the union of the captured roles' `skills`.
- `packs`: the ids and versions applied before, as provenance text.
- `packs`: `{id, version}` for each pack applied before, and nothing else.
The org's own entries carry `by`, `project`, `at`, `added` and
`proposedHires`; none of those is copied. Applying the template records
one new entry in the target organization's `packs` with origin `saved`;
the template's own `packs` list is not copied into it.
The result is a pack document read later with origin `saved` (section 2). A
role in a saved template may lack `from` (an invented role) and the document
@@ -562,8 +605,9 @@ says, which is why save is owner only and shows the exact JSON first.
--from FILE`: the base organization with human, chief and assistant, as
today.
2. `mosaic org pack apply ID saved:<name> --digest D`: the same preview and
apply as a shipped pack. A saved pack applies only to an organization the
caller owns.
apply as a shipped pack. `saved:<name>` resolves only under the caller's
own account directory (section 2), and the caller must own the target
organization.
3. The chief raises the proposed hires.
There is no create-time shortcut that skips the hire decisions.
@@ -594,10 +638,13 @@ reasons:
drops the key, and its trail event records the old list as evidence.
Nothing is applied for it, since the old modules carried nothing. The
Console offers the matching packs once.
2. A later slice removes the kind once no org file on the instance names
it. A print-only conversion in the style of `migrateLaunch`
2. A print-only conversion in the style of `migrateLaunch`
(`packages/organization/src/migrate-launch.mjs`, decision 145) covers a
file that was never written in between.
file that is never written in between; it is built in this go (SP10).
Removing the kind from `KINDS` is not in this go. It is a follow-up row
with its own go, after one release, once each instance's owner has run
the conversion or every org file has been written. A build session
cannot see other instances' org files, so it cannot make that check.
- **`project_system_types`.** The table stays, rows and triggers unchanged
(write-once, R2). `project.system-types.set` refuses with a new code,
`verb-retired` (not found today). The read verb keeps working for history
@@ -605,8 +652,10 @@ reasons:
- **Code removed.** `systemTypesFence`, `system-types.console`, the system
types Console route and page, the CLI words (`mosaic org system-types`,
`mosaic project system-types`), the channel verbs,
`system-type-not-enabled`, and `installedSystemTypes`, each with its tests
(section 1 lists every file). The five modules move to
`system-type-not-enabled`, `installedSystemTypes` and its bus reads, each
with its tests (section 1's three system-type rows list every file).
`systemTypesFence` is replaced by a fence that lets any writer drop the
key and refuses any write that adds or changes a name (`verb-retired`). The five modules move to
`packages/plugins/starter-pack/`.
### REQ-SYS-1 and REQ-SYS-2
@@ -729,8 +778,9 @@ proposals. The workflow's "relevant officer" step lists all four in its
## 8. Build slices
The slices run in order, SP1 to SP10, each landing with its own tests and
the full suites green. They are written so that one go from Jason covers all
of them; nothing in a slice waits on a further design choice.
the full suites green. One go from Jason covers all ten. Nothing in a slice
waits on a further design choice, a release, or data on a live instance.
Removing `system-type` from `KINDS` is not in this go (section 6, step 2).
**What the go covers.** No slice edits a file under `roles/` or `contracts/`.
The repository has no separate schema files; the schemas are the validators
@@ -738,47 +788,66 @@ named below, and every change to one is listed in its slice. The changes
that need sign-off are these:
- a new plug-in kind and a new `switch` value (SP1);
- new org file keys (SP3);
- new org file keys `packs`, `board` and `workflows`, and project keys
`board` and `workflows` (SP3);
- new verbs in `VERB_RIGHTS` (`packages/organization/src/org-rights.mjs:34`)
and in `CONSOLE_VERBS` (`packages/bus/src/broker.mjs:181`) (SP4, SP8);
- an agent authority surface for `organization.change` holders (SP5);
and `CONSOLE_VERBS` (`packages/bus/src/broker.mjs:181`): `packs`,
`packs.console`, `pack.preview.console`, `pack.apply.console`,
`board.set.console`, `workflow.set.console` (SP4), and
`template.save.console`, `template.list.console` (SP8). Every write verb
is owner only (`null`) until row 193;
- agent verbs `packs`, `pack.preview`, `pack.apply`, `board.set` and
`workflow.set` under `organization.change` (SP5);
- new text in agents' generated prompts (SP7);
- data leaving an organization (SP8);
- a PRD wording change and a retired kind (SP9).
- data leaving an organization into a file (SP8);
- a PRD wording change, a retired kind and a changed fence (SP9).
New refusal codes are added to `packages/bus/src/refusals.mjs` by the slice
that first uses them: `starter-pack-invalid`,
`starter-pack-template-changed`, `pack-plan-stale`,
`starter-pack-proposal-stale`, `template-secret-suspected` and
`verb-retired`.
that first uses them: `starter-pack-invalid`, `starter-pack-not-found`,
`starter-pack-template-changed` (SP1); `pack-plan-stale` (SP4);
`starter-pack-proposal-stale` (SP5); `template-secret-suspected` (SP8);
`verb-retired` (SP9).
**Owner boundary.** "Owns" lists the files a slice may change. A slice that
needs a change outside its list stops and reports; it does not widen
itself. Tests for a slice live beside the files it owns.
**Owner boundary.** "Owns" lists the files a slice may change. Where a
shared file is listed with a scope in brackets, the slice changes only those
lines. A slice that needs a change outside its list stops and reports; it
does not widen itself. Tests for a slice live beside the files it owns.
### SP1: the pack kind and its checker
- Owns: `packages/plugins/src/starter-pack.mjs` (new),
`packages/plugins/src/kinds.mjs`, `packages/plugins/src/loader.mjs` (only
if the listing needs a hook), `packages/bus/src/refusals.mjs`,
`packages/plugins/tests/starter-pack.test.mjs` (new).
`packages/plugins/src/kinds.mjs`, `packages/bus/src/refusals.mjs` (the
three SP1 codes), `packages/plugins/tests/starter-pack.test.mjs` (new).
- Schema change: `KINDS` gains `"starter-pack"` with `interfaceVersion: 1`,
`exports: ["starterPack"]` and a new `switch: "none"`. `namesFor` refuses
any setting for a `none` kind with `bad-setting`, so an org file can never
switch a pack on through `plugins`.
switch a pack on through `plugins`. This touches nothing else: `switch` is
read only in `namesFor` (`kinds.mjs:123,128`), and the loader starts each
kind at `names = []` and calls `namesFor` only when the key is present
(`loader.mjs:190-195`), so a `none` kind loads no names and `loader.mjs`
needs no change.
- Builds: `checkPack(doc, {origin})` and `narrowsTemplate(role, template)`
as section 2 states; `installedStarterPacks({dataRoot})`, which lists
shipped and installed packs through `loadInstalled` and sets the origin;
`readSavedTemplate(path)` through `parseJsonFileText` with the 256 KiB cap.
`readSavedTemplate({configDir, account, name})` with every file check in
section 2 (names against `NAME` first, `lstat`, no symlink, owner uid, mode
0600 or narrower, 256 KiB checked before the read), then
`parseJsonFileText` and `checkPack` with origin `saved`.
- Acceptance: a valid pack loads. Each of these refuses with its code: a
forbidden key, a stale template hash, a role wider than its template, a
credential line wider than its template, a root hire, a ninth hire, a hire
naming no pack definition, a bad state, a duplicate column. A shipped or
installed pack with a `packs` key or a role without `from` refuses; a
saved one with either passes. `plugins: {"starter-pack": ["coding"]}`
refuses `bad-setting`.
- Red before: a pack whose definition carries `copyBindings` passes (there
is no `checkPack`, so nothing refuses it).
saved one with either passes. A saved `packs` list with a 33rd entry, an
entry with a key other than `id` and `version`, or a version that is not
semver refuses. `readSavedTemplate` refuses a name of `..`, a name holding
`/`, a symlink, a 0644 file and a 300 KiB file, and a missing file
refuses `starter-pack-not-found`.
- Red before: `checkPack` refuses a pack whose definition carries
`copyBindings` with `starter-pack-invalid` (today the module does not
exist); `plugins: {"starter-pack": ["coding"]}` refuses `bad-setting`
(today `checkPlugins` refuses `unknown-kind`, `kinds.mjs:146-148`, so the
assertion fails).
### SP2: the five shipped packs
@@ -799,7 +868,8 @@ itself. Tests for a slice live beside the files it owns.
### SP3: org file keys and the writer op
- Owns: `packages/organization/src/organization-config.mjs`,
`packages/organization/src/organization.mjs`, their tests.
`packages/organization/src/organization.mjs` (`applyPack` only), their
tests.
- Schema change: `TOP_KEYS` (`organization-config.mjs:32`) gains `packs`,
`board` and `workflows`. The project entry keys (`organization-config.mjs:306`)
gain `board` and `workflows`. Shapes, each refusing unknown keys:
@@ -812,151 +882,250 @@ itself. Tests for a slice live beside the files it owns.
org definition, a V2 template or `human`;
- `packs`: a list of `{id, version, sha256, origin, scope, project, at, by, added, proposedHires}`,
origin one of `shipped`, `installed`, `saved`.
- Builds: `applyPack(id, plan, {revision, actor, assertClean})` on the writer
with the rules in section 3: one commit, `rev: 1` on each added
definition, `checkDefinition` with `assertClean`, `definition-in-use` as
`setDefinition` refuses it, `human-required` when an agent actor writes a
definition, and a refusal for any key outside its list.
- Acceptance: valid shapes load and each invalid one refuses; `applyPack`
- Builds:
- the reader keeps `packs`, `board` and `workflows` on the loaded
organization object, and `board` and `workflows` on each `projects[id]`.
Today the copy after `organization-config.mjs:306` keeps only `root`,
`repo`, `remote` and `git`, so widening the key list alone would load
the keys and then lose them;
- `applyPack(id, plan, {revision, actor, assertClean})` on the writer with
the rules in section 3: one commit, `rev: 1` on each added definition,
`checkDefinition` with `assertClean`, `definition-in-use` as
`setDefinition` refuses it, `human-required` when an agent actor writes
a definition. Plan keys are limited to `definitions`, `board`,
`workflows`, `projects.<p>.board`, `projects.<p>.workflows` and
`packs`; any other refuses.
- A board write or a workflow write is an `applyPack` call whose plan
holds only that key and no `packs` entry. The generic `write()` cannot
carry it, because it refuses any change under `projects`
(`projectsProtected`, `organization.mjs:383-391`).
- Acceptance: valid shapes load and each invalid one refuses; the loaded
object and each `projects[id]` keep `board` and `workflows`; `applyPack`
writes everything or nothing (a fault injected after the definitions
leaves the file unchanged); an agent actor with a definition in the plan
refuses `human-required`.
refuses `human-required`; a plan holding `plugins`, `roles`, `limits` or
`credentials` refuses; a board-only plan changes only `board`.
- Red before: a file with a `board` key refuses today as an unknown top key
(`organization-config.mjs:259`); the test expects it to load.
### SP4: preview and apply for people
### SP4: preview, apply, board and workflow writes for people
- Owns: `packages/bus/src/broker.mjs` (the new verbs only),
- Owns: `packages/bus/src/broker.mjs` (the six SP4 verbs only),
`packages/bus/src/runtime.mjs` (the new `packs`, `pack.preview`,
`pack.apply`, `board.set` and `workflow.set` ops only),
`packages/bus/src/refusals.mjs` (`pack-plan-stale`),
`packages/organization/src/org-rights.mjs`, `packages/cli/src/org.mjs`
(the `pack` words), `packages/webui/src/serve.mjs` (the two routes), their
tests.
(the `pack`, `board` and `workflow` words), `packages/webui/src/packs-http.mjs`
(new: the routes), `packages/webui/src/serve.mjs` (route hookup only),
`packages/webui/src/cli.mjs` (caller wiring only), their tests.
- Schema change: `VERB_RIGHTS` gains `packs: 'board.read'`,
`'pack.preview.console': 'org.hire'` and `'pack.apply.console': 'org.hire'`.
`CONSOLE_VERBS` gains the two console verbs. A saved-origin pack also
needs the caller to be the owner.
- Builds: `mosaic org pack list|preview|apply`, `POST /api/orgs/<id>/packs/preview`
and `POST /api/orgs/<id>/packs/apply`, and the merge table in section 3,
including `kept`, `--as` and the `removed` tombstone.
`'packs.console': 'board.read'`, and `null` (owner only) for
`pack.preview.console`, `pack.apply.console`, `board.set.console` and
`workflow.set.console`. `CONSOLE_VERBS` gains `packs.console` and those
four. Owner only matches the definition writes today
(`broker.mjs:6221,6303-6308`); when row 193 opens those to `org.hire`,
the four move with them.
- Builds: `mosaic org pack list|preview|apply`, `mosaic org board set`,
`mosaic org workflow set|remove`; routes `GET /api/packs`,
`POST /api/orgs/<id>/packs/preview`, `POST /api/orgs/<id>/packs/apply`,
`POST /api/orgs/<id>/board` and `POST /api/orgs/<id>/workflows/<name>`;
the merge table in section 3, including `kept`, `--as` and the per-pack
`removed` tombstone; the held-credential check at preview and at apply;
`saved:<name>` through `readSavedTemplate` with the caller's own account.
Board and workflow writes call `applyPack` with a one-key plan (SP3).
- Acceptance: preview writes nothing and lists flagged lines, credential
lines and `noHire` marks; a pack string holding a held credential value
refuses at preview and at apply; apply writes only `add` items and the
`packs` entry with `added` and pinned `proposedHires`; a stale digest
refuses `pack-plan-stale`; a second apply of the same pack is all `same`;
an edited definition is `kept`; an item an earlier apply added and the
owner then removed is `removed`; apply raises no decision.
- Red before: `pack.apply.console` by a member without `org.hire` refuses
`right-required`, and the verb is unknown today.
owner then removed is `removed`, while a different pack naming it adds
it; apply raises no decision; a non-owner member's `pack.apply.console`,
`board.set.console` and `workflow.set.console` refuse `human-required`;
owner B naming `saved:x` that only owner A saved gets
`starter-pack-not-found`; `packs.console` lists no saved template;
`board.set.console` changes only `board`; a workflow step naming an
unknown role refuses.
- Red before: the owner calls `pack.apply.console` for `coding` and the
test expects a `packs` entry in the org file; today the verb is unknown.
### SP5: apply and board and workflow writes for agents
### SP5: the same writes for agents, and the hire pin check
- Owns: `packages/bus/src/broker.mjs` (the agent verbs and the hire pin
check only), their tests.
- Schema change: none to files. Authority: agent verbs `pack.preview`,
`pack.apply`, `board.set` and `workflow.set` for a role whose file lists
`organization.change`, the same test `organization.set` uses
(`broker.mjs:6247-6255`).
- Builds: the agent apply in section 3 (no definitions, `human-required` if
the plan has one); the pin check when the chief raises a proposal
(`decision-stale`, `definition-rev-conflict`, and
`starter-pack-proposal-stale` when an inline entry's canonical hash
differs; the broker checks that hash, then passes `{inline}` on).
- Acceptance: an agent without `organization.change` refuses
`not-authorized`; an agent apply with a definition in the plan refuses
`human-required`; a hire the chief raises from a proposal carries the
broker-computed lines; a non-chief raise still refuses `role-not-chief`;
a proposal whose pinned template moved refuses `decision-stale`.
- Red before: a coder session calling `pack.apply` gets an unknown-verb
refusal today; the test expects `not-authorized`.
- Owns: `packages/bus/src/broker.mjs` (the five agent verbs and the hire
pin check only), `packages/bus/src/runtime.mjs` (the agent actor on the
SP4 ops only), `packages/bus/src/refusals.mjs`
(`starter-pack-proposal-stale`), their tests.
- Schema change: none to files. Authority: agent verbs `packs`,
`pack.preview`, `pack.apply`, `board.set` and `workflow.set` for a role
whose file lists `organization.change`, the same test `organization.set`
uses (`broker.mjs:6247-6255`). A role without it gets
`decision-required`, as `organization.set` gives today
(`#classification`, `broker.mjs:4743`).
- Builds: the agent apply in section 3: no definitions (`human-required` if
the plan has one), origin `saved` refused with `human-required`, workflow
steps naming an unwritten definition left out and marked in preview; the
pin check when the chief raises a proposal (`decision-stale`,
`definition-rev-conflict`, `starter-pack-proposal-stale` when an inline
entry's canonical hash differs, and `definition-not-hireable` when an
inline entry's `from` is in `launch.noHire`; the broker checks the hash,
then passes `{inline}` on).
- Acceptance: a coder without `organization.change` gets
`decision-required` for `pack.apply`, `board.set` and `workflow.set`, the
same as `packages/bus/tests/chief-of-staff.test.mjs:255-261` asserts for
`organization.set`; the chief's `board.set` and `workflow.set` write; an
agent apply with a definition in the plan refuses `human-required`; an
agent naming `saved:x` refuses `human-required`; a hire the chief raises
from a proposal carries the broker-computed lines; a non-chief raise
still refuses `role-not-chief`; a proposal whose pinned template moved
refuses `decision-stale`, one whose definition `rev` moved refuses
`definition-rev-conflict`, an inline one whose hash differs refuses
`starter-pack-proposal-stale`, and an inline one whose `from` is in
`launch.noHire` refuses `definition-not-hireable`.
- Red before: the chief calls `pack.apply` for `coding` and the test
expects the org file to gain its `board`; today the verb is unknown.
### SP6: Console pack picker, board labels and workflow editor
- Owns: `packages/webui/src/public/tasks-board.js`, a new
`packages/webui/src/public/packs.js`, the workflow view, their tests.
- Schema change: none.
- Builds: pack picker, preview and apply through SP4; labels and order from
`board` when the view is filtered to one org or project; `layout` as a
default under the viewer's saved `localStorage` choice; a workflow list and
editor through `workflow.set` or the owner path.
`packages/webui/src/public/packs.js`, a new
`packages/webui/src/public/workflows.js`, `packages/webui/src/public/workspace.js`
(the page hooks only), `packages/webui/src/workspace-read.mjs` (the
`board` field only), `packages/webui/src/packs-http.mjs` (the workflow
read route only), their tests.
- Schema change: none. The workspace read gains a `board` field per org.
- Builds: the server side first. `readOrg` already holds the org document
(`member.doc`, `workspace-read.mjs:140`), so the per-org head gains
`board` and each project row its project `board`, and a new
`GET /api/orgs/<id>/workflows` returns the org and project workflows from
the same document. Then the pages: pack picker, preview and apply through
SP4; labels and order from `board` when the view is filtered to one org or
project; `layout` as a default under the viewer's saved `localStorage`
choice; a workflow list and an editor that saves through
`workflow.set.console`.
- Acceptance: a view filtered to an org with a `board` shows its labels; the
all-orgs view keeps today's labels; a saved viewer layout wins over the
pack default.
pack default; a label or workflow title holding `<img src=x onerror=...>`
renders as text (`esc()`, `tasks-board.js:108-113`).
- Red before: a view filtered to an org with a `board` shows the fixed labels
(`tasks-board.js:21`).
### SP7: workflow guidance in the prompt
- Owns: `packages/harness/src/bundle.mjs` (`prompt`, line 271), its tests.
- Owns: `packages/organization/src/resolve.mjs` (`resolveInstance`, the
`workflows` field only), `packages/harness/src/bundle.mjs` (`prompt`,
line 271), their tests.
- Schema change: none to files. New prompt text, which is why it needs
sign-off.
- Builds: the read-only workflows section in section 2, for the chief, roles
holding `task.create` or `task.assign`, and roles a step names (their own
steps only). The section says workflows grant nothing and the contract and
rails win.
- Builds: `resolveInstance` puts `workflows` on `resolved`: the org's, plus
the project's when it is given a project, the project's winning by name.
The launcher calls it with no project today (`cli/src/launcher.mjs:401`),
so this go delivers org workflows to prompts, and project workflows reach
a prompt only once a launch names its project; this slice does not change
the launcher. `prompt` renders the read-only workflows section in section
2 for the chief, roles holding `task.create` or `task.assign`, and roles a
step names (their own steps only). Titles and descriptions appear as
quoted data under a fixed heading, never as headings of their own. The
section says workflows grant nothing and the contract and rails win.
- Acceptance: a chief bundle for an org with a workflow contains the
section; a role no step names and without those actions gets none; the
contract text is byte-identical to before.
section; a role no step names and without those actions gets none; a
named role sees only its own steps; a description holding
`## Contract` stays inside the quoted block; the contract text is
byte-identical to before.
- Red before: a chief bundle for an org with a `change` workflow has no
workflows section.
### SP8: saving a template
- Owns: `packages/organization/src/template-save.mjs` (new),
`packages/bus/src/broker.mjs` (`template.save.console` only),
`packages/bus/src/broker.mjs` (the two template verbs only),
`packages/bus/src/runtime.mjs` (the save and list ops only),
`packages/bus/src/refusals.mjs` (`template-secret-suspected`),
`packages/organization/src/org-rights.mjs`, `packages/cli/src/org.mjs`
(the `template save|list` words), `packages/webui/src/serve.mjs` (the
routes), their tests.
- Schema change: `VERB_RIGHTS` gains `'template.save.console': null` (owner
only). The saved file is a pack document read with origin `saved`; it adds
no new schema.
(the `template save|list` words), `packages/webui/src/template-http.mjs`
(new), `packages/webui/src/serve.mjs` (route hookup only),
`packages/webui/src/cli.mjs` (caller wiring only), their tests.
- Schema change: `VERB_RIGHTS` gains `'template.save.console': null` and
`'template.list.console': null` (owner only); `CONSOLE_VERBS` gains both.
The saved file is a pack document read with origin `saved`; it adds no
new schema.
- Builds: capture, strip, placeholder and preview rules in section 5; the
file at `<configDir>/templates/<account>/<name>.json`, mode 0600, written
with `wx`; `list` shows only the caller's own.
file at `<configDir>/templates/<account>/<name>.json`, directory 0700,
file 0600, written with `wx`, name and account checked against `NAME`
first; `packs` written as at most 32 `{id, version}` entries; save
refuses more than 8 hires and any role with a `from` that
`narrowsTemplate` refuses; `list` shows only the caller's own.
- Acceptance: the file has none of the stripped keys and keeps role
credential request lines; instance names, org and project ids, the owner's
account and project roots become placeholders; the preview lists every
kept free-text field; a seeded held credential value refuses
`template-secret-suspected` naming the field path; a second save to the
same name refuses; applying a saved template goes through SP4 with the
owner check.
same name refuses; a name of `..` refuses; a ninth hire refuses; each
saved `packs` entry holds only `id` and `version`; a saved template
applies through SP4 for its saver.
- Red before: save an org whose instance has `credentials` and
`copyBindings` and assert neither appears in the output; the verb does not
exist today.
### SP9: retire system types, step 1
- Owns: `packages/plugins/src/kinds.mjs`, `packages/plugins/src/system-types.mjs`
(deleted), `packages/organization/src/organization-config.mjs` and
`organization.mjs` (the retired-key handling only), the system-type code in
`packages/bus/src/broker.mjs`, `packages/cli/src/org.mjs`,
`packages/plugins/channel/cli/index.mjs` and the Console files listed in
section 1, `packages/plugins/system-type/` (deleted), `docs/prd/mosaic-stack.md`
(REQ-SYS-1/2 only), their tests.
- Owns, each scoped to its system-type lines:
- `packages/plugins/src/kinds.mjs`; `packages/plugins/src/system-types.mjs`
and `packages/plugins/system-type/` (deleted);
- `packages/organization/src/organization-config.mjs` and
`packages/organization/src/organization.mjs` (the retired key and
`systemTypesFence`);
- `packages/organization/src/org-rights.mjs` (`system-types.console`,
line 90);
- `packages/bus/src/broker.mjs`, `packages/bus/src/runtime.mjs` (the
import at line 30 and the reads at 903-904) and
`packages/bus/src/refusals.mjs` (`system-type-not-enabled` out,
`verb-retired` in);
- `packages/cli/src/org.mjs`, `packages/cli/src/project.mjs` and
`packages/cli/src/cli.mjs` (the dispatch list at line 781);
- `packages/plugins/channel/cli/index.mjs`;
- the Console files in section 1's system-type rows;
- `docs/prd/mosaic-stack.md` (REQ-SYS-1/2 only);
- their tests.
- Schema change: `system-type` stays in `KINDS` with `retired: true`.
`checkPlugins` accepts a valid list for it, the reader ignores it, and a
write that adds a name refuses `verb-retired`. The next writer commit drops
the key and its trail event keeps the old list. `project_system_types` is
unchanged (no DDL). The PRD text is section 6's.
- Builds: the removals in section 6; `project.system-types.set` refuses
`verb-retired`; the read verb stays.
`checkPlugins` accepts any list for it without loading modules, the reader
ignores it, and `systemTypesFence` becomes a fence that lets any writer
drop the key and refuses a write that adds or changes a name
(`verb-retired`). The next writer commit drops the key and its trail event
keeps the old list. `project_system_types` is unchanged (no DDL). The PRD
text is section 6's.
- Builds: the removals in section 6; `project.system-types.set`,
`system-types.installed` and `system-types.current` refuse
`verb-retired`; the `project.system-types` read stays.
- Acceptance: an org file with `plugins['system-type']` loads with the key
ignored; the next commit drops it and its trail event holds the old list;
`project_system_types` rows are unchanged; no system-type route, CLI word
or channel verb is left.
- Red before: a fixture org holding `["coding"]` must still load once the
system-type modules are gone; dropping the kind outright would fail it
with `unknown-kind`.
ignored; the next commit by an agent or a person drops it and its trail
event holds the old list; `project_system_types` rows are unchanged; no
system-type route, CLI word or channel verb is left, and
`grep -rln "installedSystemTypes\|system-type-not-enabled" packages`
finds nothing outside tests.
- Red before: an org file with `plugins['system-type']: ["no-such-type"]`
loads with the key ignored (today it refuses "names unknown module",
`organization-config.mjs:365`); a human write that adds `coding` refuses
`verb-retired` (today it is accepted).
### SP10: remove the retired kind
### SP10: the print-only conversion
- Owns: `packages/plugins/src/kinds.mjs`, a print-only conversion beside
`packages/organization/src/migrate-launch.mjs`, their tests.
- Schema change: `system-type` leaves `KINDS`.
- Runs: one release after SP9, and only once no org file on the instance
names `system-type`. The slice checks that first and stops if one does.
- Acceptance: the conversion prints the SP9 fixture without the key; with
no file naming the kind, removal leaves every org file loading.
- Owns: `packages/organization/src/migrate-system-type.mjs` (new, beside
`migrate-launch.mjs`), `packages/cli/src/org.mjs` (the
`migrate-system-type --file ORGANIZATION` word, which prints and writes
nothing, as `mosaic limits migrate` does at `packages/cli/src/limits.mjs:62-72`),
their tests.
- Schema change: none. `system-type` stays in `KINDS` as retired.
- Builds: a conversion that returns the org file without
`plugins['system-type']` and names the old list on stderr.
- Acceptance: the conversion prints the SP9 fixture without the key and
leaves the input file byte-identical; a file without the key prints
unchanged.
- Red before: the conversion does not exist.
Follow-up row, not in this go: remove `system-type` from `KINDS` after one
release, with its own go, once each instance's owner has run the conversion
or every org file has been written since SP9.
## 9. Open questions for Jason
Each answer changes what gets built. The slices above follow the
@@ -974,7 +1143,8 @@ recommendation; a different answer changes the slice named.
`organization.change` grant, with no decision. Recommendation: yes. They
carry no capability, the trail records each change, and a decision here
would be a new gate. Any role listing `organization.change` gets the same
writes, so the rule is the grant, not the chief's name. Role and hire
writes, so the rule is the grant, not the chief's name. A role without it
gets `decision-required`, as `organization.set` gives today. Role and hire
changes stay decisions, and only the chief raises hires. A no drops the
agent writes from SP5 and leaves agents to propose board and workflow
changes in a message to the owner.