146 KiB
Case 2 design: the creative writing hub (r11)
Gosalyn, 2026-10-20 (from date -u). Track T2 in jason-master-restart projects/mosaic-mvp/PLAN.md,
Vikunja #416, DECISIONS Q176-Q178. The requirement is Jason's text in TARGET.md (addendum 2026-10-20).
This is a design only. No product code changes here.
Base: refactor at 9ae8c73ac. r1 cited 5d019d876. git diff --stat 5d019d876 9ae8c73ac -- packages adapters roles scripts is empty (Mos in mos-0980, Webby in webby-0001), so every file:line holds at
9ae8c73ac. Paths are relative to the repository root.
Ruling after r11: embeds and attachments (row 212 N3)
Webby's row 212 review (webby-0041, N3) found that the C2-01 reader reads ![[...]] embeds as links, so
an embedded image, which is unmapped and so never indexed, counts as R4. Mos asked for a ruling from me
or Sage (mos-1110). Ruling: a wikilink or embed whose target ends in an extension other than .md, in
any case, refers to an attachment. The reader doesn't follow it, raises no problem for it, and the
survey counts it as an attachment. An embed with no extension or with .md embeds a note and resolves
like any link, R4 included. Section 1.3 says so. Row 212 lands as approved, and the change is C2-05a's,
the first row that reads the R4 count.
Ruling after r11: the Console starts at boot (Q189)
Jason answered Q11 with "yes - automatic" (DECISIONS Q189, mos-1123). The Console on .54 starts at boot,
and the install starts it. This reverses mos-1097 item 1, so B1 under "Changes from r10" no longer
holds. The Console unit renders with WantedBy=multi-user.target, the same as the bus unit. Step 2c
enables both and starts the bus and then the Console. Step 9 no longer has Jason start the Console, and
a reboot brings it back with no human step. Row list C2-13a, section 9.3 and step 9 say so. Mos's
forgery question (mos-1097 item 2, #430) is unchanged, and so is the Console port below 1024
(mos-1110). With a boot start the port squat window is shorter, a crash's RestartSec=15 and each
re-pin, but it is still there. The C2-13a brief carries this when Sage re-briefs it into the installer
row (I2, Vikunja #432).
Ruling after r11: the sync's askpass helper (row 222)
Before building C2-03a, Megavolt compared 7.2's GIT_ASKPASS helper with row 199's credential helper
(megavolt-0060, asked by sage-1487). As written, 7.2 left three gaps. git's default
http.followRedirects=initial follows a 302 to another host. A helper that prints the token answers any
prompt, with no host check. And git asks askpass for the username too, so the token went out as the
username. Dewey (dewey-0765) and I (gosalyn-0068) accepted the fix, and Sage ruled it into the C2-03a brief
(a304dad94).
The argv is SAFE, then -c protocol.https.allow=always, -c http.followRedirects=false and
-c credential.helper=, then -C <mirror>. The env adds LC_ALL=C, because the helper matches git's
prompt text and git translates that text. The helper answers only the exact Username and Password
prompts for the configured host and user, and exits 1 on anything else. The URL carries no userinfo. The
helper reads the slot path, so the bus never holds the value, and that is why it doesn't use row 199's
fd 3. Section 7.2 says so.
Ruling after r11: playgrounds at three levels (mos-1233)
TARGET.md:92-95 asks for a playground "where new universes can be created", a playground "for books in a universe", and says "universe-level and book-level playgrounds must be provided". 1.1 put every playground prefix inside a book folder (B1 under "Changes from r2"), so a playground with no book, and one with no universe yet, had no place. Mos asked for this ruling (mos-1233, Vikunja 464). Ruling: a playground has one of three levels, set by where its prefix sits.
- Book level. A prefix in
universes.<u>.playgroundthat lies inside one of<u>'s book folders, as today. Its files have universe<u>and that book. - Universe level. A prefix in
universes.<u>.playgroundthat lies outside every book folder of every universe. Its files have universe<u>and booknull. - New universe. A draft universe is a
universesentry with"kind": "draft", anameand one or moreplaygroundprefixes, and nobooks. Its prefixes lie outside every book folder. Every file under them is playground, with universe<draft id>and booknull. She starts a new universe there. When it is real, she edits the business file to turn it into a plain universe with books and moves the files in Obsidian. M9 keeps agents off that edit, as for every mapping change.
The validator replaces library-playground-outside-book with two refusals. A prefix that lies inside
another universe's book folder refuses library-playground-other-universe. A draft with books, or
with no playground prefix, refuses library-mapping-invalid. The rule that no prefix holds another, and
no prefix is listed twice, covers all three levels. A prefix equal to a book folder is still refused.
A prefix that holds a book folder refuses too, since prefixes are checked first and the book would turn
into playground: library-playground-other-universe when the book belongs to another entry, and
library-mapping-invalid when it is the entry's own. A hybrid's sources can't name a draft, since a
draft has no canon.
kind is absent for a plain universe, "hybrid" or "draft", and any other value refuses
library-mapping-invalid. A draft takes the keys kind, name and playground only, and playground
must name at least one prefix. A plain universe still takes name, books and playground, and a
hybrid keeps its key list.
REQ-LIB-2's scope rule still reads from the path alone, and a playground at any level is never canon.
The rule checks prefixes first. A .md file under any universe's or draft's playground prefix is
playground, with that entry's universe and the book folder that holds the prefix, or null. Otherwise
a .md file in a book folder is canon. Everything else is unmapped. Prefixes never overlap, so one
prefix at most matches. The lookups' canon default, playground-cited-as-canon and the Console's scope
badge apply at every level unchanged.
REQ-LIB-3's one-universe rule holds as written, because a draft is a universe entry. Every lookup still
names one universe, and its results never leave it. A lookup in a draft with the canon default returns
nothing, so a lookup there passes scope: "playground". A universe-level playground file comes back from
a lookup with book: null, and a lookup that names a book leaves it out. library_manuscript needs a
book, so it doesn't read universe-level or draft files. citation-cross-universe treats a draft as its
own universe. R5 covers a canon file that refers into any playground, and R6 covers one that refers into
a draft. library_universe on a draft returns its kind, name and prefixes.
The build is a new row, C2-01c, in packages/library mapping.mjs and scope.mjs, with a test per
level, per new refusal and per lookup rule above. It goes before C2-05a and C2-06 read classify, or into
their candidates if they've started. C2-12's setup sheet gets one line per level. Sections 1.1, 1.2 and 8
say so.
C2-01c's brief names every reader of a universe's books and of classify(...).book, so a draft with no
books and a null book reach no code that assumes them. At refactor cca206cdc these are:
mapping.mjs:73-78, which buildsbooksandbookOwner, andscope.mjs:11-18, which C2-01c rewrites;reader.mjs:182and:194, which copybookinto every file record, and:208, which readssourcesand works for a draft as it is;survey.mjs:66andmirror.mjs:246, which read onlyscope. The rows not landed are C2-05a (row 216), with its book filter,library_manuscriptand thelibrary_universereply, and C2-06 (row 217), with the guard's universe check. Each takes a null book through C2-01c's tests, or C2-01c goes into its candidate. The tests atmapping.test.mjs:16-20pinlibrary-playground-outside-bookand change with this row.
Webby's round d1 on this ruling (webby-0088, REVIEW-d1 at webby/reviews 35130c26b) found the missing
refusal for a prefix that holds a book folder (B1), and asked for the unmapped row, the kind values,
the readers and the section 8 line (N1-N4). All five are in the text above, in 1.2 and in 8.
Ruling after r11: idea items capped at 3 of 120 characters (Q219)
Jason answered Q26 with "yes" (DECISIONS Q219, mos-1299, Vikunja 465, ludwig-0014 Q4). An idea item
is at most 120 characters, at most 3 per reply. The rest of M5 is unchanged: 10 items and 1,200
characters of item text per reply, 300 for fact, finding, correction and question, and the
correction edit distance. The residual in 5.1 is now 360 characters of free suggestion text per reply,
and 10 step 8 is closed by Jason. C2-06 hasn't landed, so it builds with the new numbers, and its tests
refuse one past each new boundary and accept at it.
Ruling after r11: reply limits are settings (Q222)
Jason ruled Q222 (DECISIONS Q222, mos-1302): "Things should NOT be hardcoded." Q219 stands for now, and its
numbers become the defaults. Every M5 limit is a setting in a new top-level replyLimits object of the
business file:
| Key | Default | Valid value |
|---|---|---|
itemsPerReply |
10 | an integer from 1 to 1,000 |
itemTextPerReply |
1,200 | an integer from 1 to 100,000 |
ideasPerReply |
3 | an integer from 0 to 1,000 |
ideaLength |
120 | an integer from 1 to 100,000 |
itemLength |
300, for fact, finding, correction and question |
an integer from 1 to 100,000 |
correctionMinDistance |
12 | an integer from 0 to 100,000 |
correctionPercent |
25 | an integer from 0 to 100 |
A correction's to is within edit distance max(correctionMinDistance, correctionPercent% of
from) of its from.
Where. The object sits beside projects, not inside the library entry, because M9 keeps projects to
hand edits and Mos asked for a CLI verb. TOP_KEYS (packages/business/src/business.mjs:24) gains replyLimits, and
validateBusinessDocument accepts any JSON value under it. A bad hand edit then never stops her business
from loading.
Who changes them (M11). Only the business file's human, through a new human verb library.limits.set
with revision and replyLimits. She runs it as mosaic limits replies --file <json>, a new verb in
packages/cli/src/limits.mjs beside mosaic limits set, and later from the Console. The verb replaces the whole object, and a key left out means its default. It sits outside
ORGANIZATION_KEYS and TESTER_ORGANIZATION_VERBS, as instances.set does (broker.mjs:219, :139,
sage-0918). An agent session gets human-required and the tester gets tester-refused. A value outside the
table refuses invalid-request, and so does a key not in the table. Nothing is written.
The fence that covers every other path sits in organizationWriter.write
(packages/business/src/organization.mjs:180), next to M9. It compares against the file on disk, old
in write, never against the incoming document. A write whose replyLimits differs from
old.replyLimits, compared in canonical JSON, refuses organization-reply-limits-human-only unless both
hold:
actorequalsold.human. A write that setshumanto its own actor in the same document still compares against the oldhuman, so it refuses.actorisn'ttesterand isn't<definition>.<role>for any role inold.roles, the two shapes of an agent actor (broker.mjs:4201,:4206,:4306).ID_PATTERN(vocabulary.mjs:199) allows a dottedhumanid, and the broker refuses onlytester(broker.mjs:328), so without this a human id such asassistant.beanwould let that agent through.
As M9 does (organization.mjs:191-193, :199-200), the fence checks document and then written, the
JSON round trip, so a value with a toJSON can't differ between the check and the disk. That stops the
chief of staff's organization.set, any actor's limits.set, instances.set, routes.set and
plugins.set, the tester, and a direct call to write. Every setter builds its document with
structuredClone of the file, so none of them changes replyLimits today and the fence refuses no
ordinary write.
Read at check time. The guard reads replyLimits from the business file on each check, through the
organization reader, not from the broker's copy loaded at start (broker.mjs:419, read by #business
at :570). A change applies to the next reply with no restart.
A failed read never skips the check. If the business file is missing, can't be read, isn't JSON or isn't
an object at check time, the guard checks the reply at the defaults, except requireCitation, which
resolves to true (webby-0151 R2, ruling after r11), and library.status shows
reply-limits-unreadable. The reply is neither refused for that alone nor let through unchecked.
Fallback. Each key resolves on its own. The guard reads a key only if Object.hasOwn(replyLimits, key),
and uses it only if Number.isInteger(value) is true and the value is within the table's range. Anything
else resolves to its default: a missing key, null, a boolean, a string such as "5", an array such as
[7], an object, a fraction, NaN, a negative number, a value below the minimum or above the ceiling.
Every key resolves to its default when replyLimits is missing or isn't a plain object, an array
included. The guard never compares a value it hasn't checked with Number.isInteger, so JavaScript's
coercion of true or [7] to a number can't reach a range check. Unknown keys are ignored when read. The
guard has no path without a number, so no value reads as "no limit". library.status shows the limits in
force and names each key that fell back.
The ceilings are validity bounds, not policy: they keep a value from reading as unlimited. 100,000
characters is far past any reply, and Mos can change them. At the ceilings the human can make the guard
loose. The idea residual is ideasPerReply times ideaLength, up to 100,000,000 characters at both
ceilings, and correctionPercent 100 lets a correction replace its whole from. Cites stay required at
every value. library.status shows the residual, and the setup sheet says so in one line.
What agents see. The contracts don't state numbers. A refusal reason names the key and its value in force, so the runner's retry (C2-07) tells the agent the limit it broke.
Hold, now lifted. Q29 is answered (Q223, Q226). The next ruling replaces this table's seven keys with
requireCitation and citationsPerReply, and C2-06 builds that.
Ruling after r11: agents never write story (Q223, Q226)
Jason answered Q29, and Mos's reading of it, Q30, is confirmed (DECISIONS Q223 and Q226, mos-1305, mos-1309). F40 to F43 come with it, and mos-1308 sets the tool set. This ruling follows my impact list, gosalyn-0148, with Mos's eight points, Mos's rulings on my flags in mos-1320, and Webby's round, webby-0151 R1 to R3 and N1 to N5. It replaces D3, D5, M5, most of 5.2, the Q222 hold and 7.3's "after the slice". Sections 0, 1.1, 1.2, 2.1, 3, 4, 5, 7, 8, 9.2 and 10 say so.
Story and supporting. Her chapters and story files stay read-only to agents. A human marks folders as
supporting through a new supporting list in each universe entry, beside playground (1.1). An agent
may create, edit and delete .md files under a supporting prefix, and nothing else in her repo. A folder
she hasn't marked is story. A folder in both playground and supporting is writable. A playground
folder alone isn't (Mos point 4). The prefix rules are the playground's: a prefix inside a book folder is
book level, one outside every book folder is universe level, no prefix holds a book folder or another
supporting prefix, and one inside or holding another universe's book folder refuses the business file
with library-supporting-other-universe. M9 already keeps the list to hand edits, since it sits in
projects.
Chat. A reply is free text of any length, within the broker's message limits as they are today. The item
list goes, and with it the item kinds, CREATIVE_KINDS, the M5 caps, the three notices,
cited-reply-required, reply-kind-not-allowed and correction-unbound. A reply may carry citations
(5.2). Jason's "NEVER" now reads as story in her files, and M12 enforces it in code.
Citations are tunable (F43). replyLimits keeps the Q222 mechanism: the object, library.limits.set, M11
and the read at check time. The seven Q222 keys go, since their subject is gone. The keys now are:
| Key | Default | Valid value |
|---|---|---|
requireCitation |
false |
true or false |
citationsPerReply |
20 | an integer from 1 to 1,000 |
The fallback rule holds. A key is read only if Object.hasOwn finds it, requireCitation only when
typeof gives boolean, and citationsPerReply only when Number.isInteger is true and the value is in
range. Anything else resolves to the default, and library.status names the key. So a bad
requireCitation hand edit reads as off, which is Jason's default and not a weaker guard than he chose.
The verb refuses it with invalid-request. One case differs from the defaults. When the business file
can't be read at check time (the Q222 rule), requireCitation resolves to true, so a setting she turned
on doesn't fail open, and citationsPerReply takes its default. library.status shows
reply-limits-unreadable, and the runner's fixed texts still pass (webby-0151 R2). When
requireCitation is true, a reply with no citation refuses citation-missing. When a reply carries
citations, the four checks in 5.2 run whatever the setting.
Links. Wiki links already in her files stay. The hub never rewrites a file it wasn't asked to write, and
agents can't write a chapter at all, so they add no link inside one. Their cross-links go in supporting
docs, and Obsidian's graph shows those. The [CH] alternative, items 42 to 46 of my impact list, is
dropped. A Console brain map is a later option and has no row.
The write path. Two agent verbs, each with a typed tool:
library.doc.write {universe, path, base, content}, toollibrary_doc_write.baseis the blob id the agent read, fromlibrary_get, ornullto create.library.doc.delete {universe, path, base}, toollibrary_doc_delete.
Only a session of a CREATIVE_DEFINITIONS instance holding the new action library.write and a live
Gitea grant on the library project may call them (library-write-refused, library-write-no-grant).
The tester gets tester-refused. M12 checks the path before anything else. content is UTF-8 with no
NUL, at most 1 MiB, the reader's R1 limit. An agent's call passes the runner's tool socket and the bus
socket, and both cap a request at 64 KiB today (runner.mjs:294, server.mjs:62 at aca16404c). So an
agent write over that refuses request-too-large before the broker sees it, and the 1 MiB limit binds
only her saves. library.file.save joins the project.file.save exception at client.mjs:19 and
server.mjs:9 and :62, with the same PROJECT_SAVE_REQUEST_MAX.
Each write is a plumbing commit on the bare mirror, with no working tree. Under the library lock the bus
fetches, reads the path at the remote tip, and compares it with base:
- Equal, or both absent for a create: it writes the blob, builds the tree from the tip's tree with
GIT_INDEX_FILEpointing at a temporary index outside the mirror, and runscommit-treewith the tip as the one parent. The author and committer are the seat'sidentity. The message is a fixed line,Mosaic: write <path>orMosaic: delete <path>, and a trailerMosaic-Agent: <business>/<instance>. The agent supplies no message text. - Not equal: it refuses
stale-baseand writes nothing. - It pushes
<commit>:refs/heads/<branch>with no+, so the push is fast-forward only (D6). A rejection for a moved tip fetches once more and goes back to step 1 one time. A blob still equal tobasere-applies on the new tip. Otherwise the write refusesstale-base. - Any other push failure refuses
library-push-failed, and the commit is left unreferenced. Nothing is kept to push later, so there is nounpushedstate and C2-17 drops. - After the push, the bus moves
refs/remotes/library/<branch>and the served ref to the new commit, which is a fast-forward of the served commit, rebuilds the index, recordslastPushin the sync state and stores a notice.
The notice is a message from the writing instance to human in that instance's thread. The broker
builds its body, {"notice": "library-write", "op", "path", "commit"}, and no agent text reaches it.
The Console shows it as a fixed line with a Revert button (7.4).
library.revert {commit} is the human's. It refuses library-revert-not-agent unless commit carries
this business's Mosaic-Agent trailer and is reachable from the served commit. For the one path that
commit touched, it puts back the parent's blob, or deletes the path if the parent had none, as a new
commit by the same steps, with base the commit's own blob. A later change to that path makes it refuse
stale-base. A revert is her act, so its author and committer are her editor, and it pushes with slot
library-human, as her saves do. It still works after the agent's grant is revoked or the seat is fired.
A cite to a file whose last commit at the cited commit has a Mosaic-Agent trailer is labelled
agent-written (Mos point 3). The broker reads the trailer with git log -1 --format=%(trailers:key=Mosaic-Agent,valueonly) <commit> -- <path>,
and the label rides in the stored reply's check result, so no agent can set it.
M12 is the fence. A new pure function in packages/library, supportingPath(mapping, universe, path, tree), and the broker calls it on every agent write and delete. It refuses library-write-story-path
for a path that isn't a .md file under a supporting prefix of universe. That covers an unmapped path,
a .canvas or .base file, an attachment, any story or canon path, a playground path that isn't also
supporting, and a path under another universe's prefix. It also refuses an absolute path, a .., . or
empty segment, a backslash, a control character, a path whose any prefix in the tip's tree is a symlink
or a gitlink, a path that differs only in case from a tree entry it doesn't equal, and a path over 1,024
bytes. A delete also needs the path to exist at the tip as a 100644 blob.
Who writes, with what (F40). The shared bot account goes. Each writing seat writes with its own Gitea account and token from the per-seat grant path (Q190 O1, #436b, row 236). The token stays in the bus's credential store, out of every sandbox (#436b section 3c), and no agent session holds it. The askpass helper from 7.2 reads the seat's store file for that one push, with the seat's Gitea user as the only username it answers.
The chief of staff of her business is enrolled at install and grants the other seats through the
existing step: the chief raises a credential.mint decision with a gitea grant for books, and the
executor runs on its existing approve (436b section 4, step 0). Whether each such grant also needs
Jason's Approve is Q31, asked of him. This design adds no step for it, and a yes adds none here.
The executor today forks each project and refuses any other reach (436b steps 4 and 5). A library
project can't work that way, since the writes must land in her repo. C2-21 changes the two steps for a
project of kind: "library" only:
- The raise check.
#giteaRole(broker.mjs:2122-2135ataca16404c) refusesgitea-not-declaredfor any creative role, whatever its definition declares (mos-1163). F40 narrows that ruling. A creative role passes only when every project the raise names iskind: "library". A raise for a creative role that names any other project still refusesgitea-not-declared, so a creative seat never gets a fork. The other checks stay: the definition's gitea credential,identity.giteaand a non-emptypublish, which C2-08b supplies. Mos ruled the narrowing in mos-1320 item 2: mos-1163 still covers every gitea project exceptkind: "library", and covers host grants whole. The host grant writer, the executor's step 1 recheck andhost.runkeep refusing a creative role withhost-creative-role. The comment atbroker.mjs:2120cites mos-1320. - Step 4 adds the seat's user as a collaborator on the project's own repository with the admin token,
PUT /repos/{owner}/{repo}/collaborators/{user}, in place of a fork. The permission iswritefor aCREATIVE_DEFINITIONSinstance andreadfor any other, the chief included. The definition decides it, and the raise can't name one. - Step 5 accepts that repository in the reach list when its
permissionsshow exactly that level, withadminfalse. Anything wider still refusesgitea-reach-wider. - The rendered block names the repository and the level instead of a fork URL, and adds the line "The token can write every path of this repository. Only the bus's path check limits it to supporting folders."
- A revoke deletes the token as today, which ends the write. The collaborator entry stays, as forks do.
A fire's delete sets
prohibit_loginas today.
Two facts aren't proven yet, and C2-21's first test proves them on a scratch Gitea 1.27.1 before any code,
as F1 to F11 were: that the admin token's scopes (436b 3b) can add a collaborator to a repository the
admin account doesn't own, and that /user/repos with the seat token shows that repository with the
granted permissions. If either fails, C2-21 stops and goes to Mos.
The mirror's fetch uses the chief's token, whose grant is read, and the chief's user in the askpass
helper. That replaces the bot slot library-gitea from C2-03a. C2-03b makes the change. The fetch falls
back to slot library-gitea only while the bus has never recorded a gitea grant event for the chief.
The check reads the append-only events table (packages/bus/schema.sql:131-132 at aca16404c) for any
GRANT_EVIDENCE event (broker.mjs:246) with method gitea and agent the chief. It never reads
gitea_grants, whose row a revoke and a grant-lost delete (broker.mjs:2785, :2609), and which has
no state column (schema.sql:469-480). It names no project, since grant.applied and grant.revoked
carry none (:2469, :2787), so a chief grant on any project ends the fallback, which fails safe
(webby-0156 R4). An install that runs before C2-21 still syncs. Once such an event exists, a revoked
or held grant, or a fired chief, refuses the fetch with library-fetch-no-grant, and library.status
shows it. A write, a revert and a save never fall back to the slot. When library.status shows the fetch
on the chief's grant, Jason deletes slot library-gitea (10 step 4b, webby-0151 R3).
Her edits (F41). She edits in the Console. Each save is one commit by the same steps, through the human
verb library.file.save {path, base, content}. The author is a new editor object in the library entry,
{name, email}, which the setup sheet asks her for, and the push uses her own Gitea token from a new slot
library-human (mos-1320 item 3). A save or a revert with no editor refuses library-mapping-invalid.
One with slot library-human missing or failing the helper's file checks refuses library-human-missing
before any git call. Her saves reach any mapped .md file. M12's other path rules hold for them, and only
the supporting rule doesn't, since the files are hers. They still refuse
stale-base when the file moved, because an agent write or an Obsidian push can race her save. She sees
an agent's write at once in the Console, and Obsidian sees it on its next pull. Agents see her save at
once too, since the bus moves the served ref after its own push. Her Obsidian pushes reach them through
the 60-second sync. The editor is a Q195 add-on, so C2-11a and C2-11b are slice rows that don't gate
10-27.
Rows (F42). C2-02, C2-03b and C2-11b join the slice, and C2-11a with them, since C2-11b needs it. Sage
queues them. C2-07 folds into C2-06. C2-17 drops. Five new rows carry what landed or built rows can't:
C2-01d, the supporting list and scope, C2-05c, the supporting scope and blob in the lookups,
C2-08b, the role changes, C2-12b, the setup sheet rewrite, and C2-21, the grant change. C2-05b is row
224, built to its earlier brief, and it lands as built (sage-2833). Its tools already match M3 below.
Section 8 lists them. Mos asked for a PRD row too: REQ-LIB-6 rewritten to Q223 and Q226, before C2-01d
and C2-02 (mos-1320 item 8).
M3 (mos-1308, Mos point 8). A creative session's typed tools are exactly the five lookups,
send_message, library_doc_write and library_doc_delete. companion and research also get
web_search and web_fetch through the existing web grant. So the per-role snapshot holds eight names,
or ten for those two. mos-1308 said six or eight, before Q226 added the two write tools, and Mos
accepted eight and ten in mos-1320 item 4. None of read_spec, list_agents, inbox, trail,
write_record, raise_decision or a tracker read reaches a creative session.
packages/harness/src/tools.mjs at aca16404c adds all but raise_decision to every session, and
already skips raise_decision for a creative definition (:241, webby-0151 N3).
Residual. A supporting doc can hold any text, a drafted scene included, and code can't tell a scene from a note. Every agent write is visible as a notice and a trailer, and she can revert it. Nothing an agent writes lands in a story path (M12). At Gitea the seat token can write any path of her repo, and the bus's check is the only path limit. The token never leaves the bus, so the limit holds while the bus code does. C2-21's scratch Gitea run also records two facts that don't gate it: whether a protected file pattern refuses a seat user's push to a story path while her push still passes (mos-1320 item 5), and whether branch protection refuses a seat user's force-push while her normal push passes (webby-0151 N4). If the first works, Mos files a task for it as a second fence after the slice. The second would back D6 at the remote and wouldn't narrow the paths.
Changes from r10
Webby returned r10 with B1-B3 and N1-N5 (webby-0036, agents/webby/work/case2-r10/REVIEW.md at
2beb49dae). Mos ruled on B1 in mos-1097.
- B1. The Console unit renders with no
[Install]section. Jason starts it at step 9 and after each boot. Decisions 94 and 102 keep the start with Jason, and Mos reads his grant to Mos in 102 as covering dragon-lin only. The bus unit stays boot-enabled, since it holds no human capability. Step 9 states the tradeoff. The r10 paragraph below, which let a boot start stand in for the human's start, no longer holds. - Mos's forgery question (mos-1097 item 2). New section 9.3 answers it from the code: which processes
run as
mosaic, what a session can reach from inside bwrap, the narrowest form by 10-27, and what closes the rest. It also adds one finding of mine, a port squat while the Console is down, and the fix the C2-13a brief now carries, a privileged port. - B2. The render has a rule for every token,
@PATH@included, and refuses any token left over (C2-13a brief). - B3. The brief lists the exact hardening lines, none of which restricts namespaces or system calls. Step 9 checks that one session starts under the bus unit.
- Split (sage-1480). C2-13 is now C2-13a, the unit render and the host precheck with no library dependency, which row 195 chains on, and C2-13b, the install and the precheck's library checks. The precheck file is shared: C2-13a creates it and C2-13b adds to it. C2-04's owner is Herb, not Rocko (sage-1482).
- N1-N5 are in the C2-13a brief: the bus unit's
WantedBy=line is replaced, not added. There is noAfter=, with the reason. The serve checks are copied intocase2-unit.sh, and the serve flags are named. There is no--checkout, so the queue panel is out. Two units naming different copy ids refuse.
Changes from r9
Darkwing's row 195 question (darkwing to gosalyn, after Duckworth's B1) found a gap. r9 named one
system unit, mosaic-stack.service, and never said what it runs. The stack is two units today: the bus
host (packages/cli/systemd/mosaic-bus.service.in) and the Console
(packages/webui/systemd/mosaic-webui.service.in). The slice needs both, because C2-10b is Console
chat. Now 2c renders both from those templates as system units, mosaic-bus@<business>.service and
mosaic-webui.service, with the same copy-id rule, and no new mosaic-stack.service.in exists. The
bus unit carries the EnvironmentFile= line. The Console unit sets no credential variable, as its
template says. The node path and the Console's serve flags are install inputs, fixed across re-pins.
The copy id is the only value a re-pin changes.
One thing this makes explicit. The Console template says whoever starts it holds the human's capability,
so Jason starts it, or Mos under lead decision 102. A system unit that systemd starts at boot holds that
capability with no human starting it. On her host the human is Melanie, signing in through step 5. So
the step 9 go is the human's start, and a reboot restarts the Console under that same go. If Mos or
Webby reads decisions 62 and 94 as needing a human at every start, the Console unit gets no [Install]
section and a human starts it after each boot.
Changes from r8
Jason's Q7 answer, "Q7: yes" (DECISIONS Q186, mos-1090, sage-1469). O4 is the model path.
- Model path (9). For 10-27, Bean and Chet both run harness pi with provider
zaionglm-5.3. Pi reads the key fromZAI_API_KEY, and row A puts that name inENV_ALLOW(packages/seat/src/session.mjs:83-88). Chet moves to his ChatGPT/Codex login after 10-27, in row B (Vikunja #428), which I design. Running a pi role still waits on #383. - 10 step 2. 2b no longer leaves "which model accounts" open. New 2d: Jason places the hub's own key on
the existing Z.ai account, with a spend cap if the account offers one. Whose account it is stays his
input.
melanie's sudo can read the key until #423 step 5. - Two consequences in C2-13. A pi launch has no credential check, so the precheck checks the key file
by name and mode and never prints the value. The claude marker prompt now runs only when some
creative instance's harness is claude-code, because with the model path above the host may have no
Claude login. The unit template's
EnvironmentFile=line is one of the fixed linescase2-unit.shchecks. - C2-20 (9.1). The dry run also counts lines naming a Hermes tool or channel, counts only (gosalyn-0030, mos-1087).
Changes from r7
Webby's review (webby-0025, CHANGES, notes at agents/webby/work/case2-r7/REVIEW.md, 19bb3ae36).
- B1. r7 had root install a unit file that
mosaicwrote. By 2c, package code has run asmosaicin 2b and could rewrite that file toUser=rootor anotherExecStart, so the user without sudo had a path to root, and again on every re-install. Nowmosaicwrites no unit. In 2c, root works only from a root-owned checkout of the reviewed commit outside/srv/mosaic. It renders the unit from that checkout's template, with the user and home as inputs, and checksUser=,Group=,ExecStart=and the hardening lines against fixed values. It then writes/etc/systemd/system/mosaic-stack.serviceasroot:root0644, and it never usessystemctl link. Webby suggested the template in the release. The release sits under/srv/mosaic, wheremosaiccan write it, so I used a root-owned checkout instead. 2c refuses an installed unit that differs from the render, and a symlink at the unit path. C2-13 tests a unit edited toUser=root, and 2c refuses it. - N1.
mosaichas no keys and no login shell, so 2b runs assudo -u mosaic -Hfromlocaladmin. Nobody adds a key tomosaicto make 2b work.
Mos lifted the mos-1050 hold on step 1 after Webby passed it (mos-1074). Section 10 still goes to Jason one step at a time.
Changes from r6
Webby's review (webby-0023, CHANGES, notes at agents/webby/work/case2-r6/REVIEW.md, 216302d65).
- B1. r6 said the install creates
mosaic, but step 2 and C2-13 run the install as<user>. A user can't create itself, andmosaicwithout sudo can't install a system unit. The #421 plan that owned those root steps stopped. They are now their own items in section 10: 2a createsmosaicand/srv/mosaic, and 2c installs and enables the unit after the install. ops-01 owns them (mos-1064) and runs them under Jason's go for step 2, over ssh as[email protected]with NOPASSWD sudo. #423 step 1 creates that account. They use neithermelanie's sudo nor pve root. So the install depends on #423 step 1 and on nothing else in #423. C2-13 writes the unit file and doesn't install it. r8 replaces that, becausemosaicmust write no unit (webby-0025). Section 9 cites Q183 for the layout, not the stopped plan. - Mos, mos-1064 item 3. r6 said Jason's acceptance covers
melanie's sudo. It covers the factory's paths to .54 (Q182, Q183): pve root, ssh asmelanie, and the root her sudo gives through those paths. It doesn't cover the Hermes processes, which run asmelanieand could use her sudo to read/srv/mosaic. That part is Mos's residual. Q181 keeps Hermes running, and #423 step 5 removes her sudo once Bean and Chet are live. Section 9 and the r5 notes now say so. - N1. A system user always has its primary group. Section 9, the C2-13 row and the precheck now say no
supplementary groups, and the precheck checks that
id -Gprints exactly one entry.
The mos-1050 hold on step 1 stays until Webby approves r7 (mos-1064).
Changes from r5
Mos, mos-1057, after Jason confirmed Q4 (DECISIONS Q183, read at DECISIONS.md:1533-1545). The #421
plan stopped and #421 is closed, so the install on .54 no longer waits on it. Only step 1 of section 10
and the isolation paragraph of section 9 change. The hold from mos-1050 lifts for step 1 once Webby
approves this diff. Steps 2 to 9 still go to Jason.
- Step 1 now says the install creates
mosaicwithout sudo, FED R8 records Jason's acceptance, and the in-VM tightening on .54 is Vikunja #423, which doesn't gate the install. - Section 9 no longer counts on ops-01's step 2b to remove
melanie's sudo. Jason's acceptance in FED R8 covers the factory's paths to .54. The Hermes processes that could use her sudo are Mos's residual until #423 step 5 (r7, mos-1064).
Changes from r4
Webby's review (webby-0020, CHANGES, notes at agents/webby/work/case2-r4/REVIEW.md, 0597cc198). I
re-read each cited line at 9ae8c73ac, and each one holds.
- B1. r4 said no verb writes
launch.modelsfor the human, and that Jason edits it by hand. That was wrong, and I told Mos the same wrong thing in gosalyn-0014.limits.setis a human verb (broker.mjs:1248) with the sameORGANIZATION_KEYS,modelsincluded (:145).organization.setislimits.setfor a role (:1249-1251), and both reach the sameset()(:1284,launch-settings.mjs:150-169). The actor rule was already right, because it keys on the actor and not the verb. Section 4 now names both verbs on theassistants,instancesandmodelspaths. M10 and C2-08 add the case that shows the check keys on the actor: the human'slimits.setchange to Bean's key works, next to the chief of staff'sorganization.setrefusal of the same change. - N1. Pi also uses the path as the prompt when the file doesn't exist (
existsSyncfalse,resource-loader.js:21,:30), with no warning. The adapter's check is[ -r ], which covers both cases, and C2-08 tests a missing file as well as an unreadable one. - N2. C2-13 runs one marker prompt through the host's claude with
--system-prompt-fileand checks that the default prompt is replaced, not appended to. It doesn't rely on--helpalone.
Changes from r3
Webby's review (webby-0017, CHANGES, notes at agents/webby/work/case2-r3/REVIEW.md, fcac66f7b), with
Mos's rulings (mos-1051). I re-read each cited line at 9ae8c73ac, and each one holds.
- B1.
routes.setwas a fourth path. The tester may call it (broker.mjs:103), andsetRoutes(launch-settings.mjs:240-277) checks no definition before its write, so the tester could change a creative instance's harness, model and thinking. r3's "could change no creative instance" was false withbus.testeron. All creative-instance checks are now one rule: the write must come from the business file'shuman. Role sessions write as<definition>.<instance>(broker.mjs:1278) and the tester astester(:1272), so neither passes. The path list in 4 names all five paths, and M10 has a test for each (4, 5.1). - N1, ruled refuse (mos-1051 item 2).
organization.set'smodelskey (launch-settings.mjs:169) could change the model behind alaunch.modelskey that Bean or Chet names. A change or removal of such a key now refuses, with a test and a mutant. r4 said the human never reaches this write. She does, throughlimits.set, and r5 corrects it. Ending a run stays with the chief of staff (mos-1038 item 2). - N2 (mos-1051 item 3). Before Jason's go, C2-20 makes a dry run over her persona files that reports
only counts: files, lines, and lines starting
#or##. If the heading rule would refuse a real persona, the rule changes before the import. Any rewrite of her text needs Melanie's say (9.1). - N3 (mos-1051 item 4). The persona text sits in two more places, and 4 names both.
prompt.mdis written 0600 into each run's bundle and kept until prune (bundle.mjs:241-244,:265). Both adapters pass it as an argument (adapters/claude/adapter.sh:79,:82,adapters/pi/adapter.sh:106,:121), so any local user on the host can read it in/proc/<pid>/cmdline. The adapters now pass the file, not its text. Pi 0.85.1 reads--system-promptas a file when the value is an existing path. Claude's help names--system-prompt-file. C2-08 does this, and C2-13 checks the run root's mode. - Webby's answers. Keying on the actor is enough, and C2-08 runs the tester cases through the broker
with a bound tester session, so a change at
broker.mjs:1272fails a test. The heading refusal fails closed, and N2 covers its risk.
Mos, mos-1038 items 2 and 3. The chief of staff keeps ending runs, which matches TARGET.md:66, and a per-agent forbid comes through Jason (4). ops-01 makes the 16 GiB, 6-core change at 2026-10-21T09:00Z (9).
Section 10 step 1 is on hold (mos-1050). Jason isn't pursuing #421 and accepts the risk, and Mos is confirming that reading with him. The step keeps its r3 wording until he does. If he confirms, step 1 becomes "Jason's written acceptance of the factory paths to .54 is recorded", and section 9's isolation paragraph changes with it.
Changes from r2
Mos, mos-1031 (Jason's answer to Q2, DECISIONS Q181). The host is now fixed (9, 10):
<host>is 10.1.1.54, W-Melanie-OC, VM 1012.<user>ismosaic, a system user with no sudo, no groups and no keys. Its home is/srv/mosaic, mode 0700. It runs as the system unitmosaic-stack.servicewithProtectHome, so the config directory and the token slot live under/srv/mosaic. ops-01's plan, section 3, Vikunja #421.- ops-01 sets VM 1012 to 16 GiB fixed with 6 cores, which covers my 4 GB estimate. The precheck still prints free memory.
- The B7 residual from
melanie's sudo is no longer Jason's to accept. Step 2b of the plan removes her sudo and lxd. Section 10 step 1 becomes "Jason approves the tightening plan (#421)".
Webby's review (webby-0009, CHANGES, notes at agents/webby/work/case2-r2/REVIEW.md, 162ce46db). I re-read
each cited line at 9ae8c73ac, and each one holds.
- B1. A playground prefix now sits inside a universe entry and must lie inside one of that universe's
book folders, so a playground file has a universe and a book. A prefix outside one refuses the
business file. Playground, like canon, is
.mdonly, so a.canvasunder a playground prefix is unmapped (1.1, 1.2). - B2. The persona text no longer goes in the business file. It lives in a 0600 file per instance under
<configDir>/personas/, and the business file holds onlypersona.nameandpersona.sha256. So the tester'sinstancesread, the Console's andchanges.jsonlcarry the digest, never the text. The modes of the business file,.historyandchanges.jsonlare stated from code and tested in C2-08, and the C2-13 precheck checks them and refusesbus.testeron her business (4). - B3.
launch-settings.mjs:140,:186and:191stayTHREAD_DEFINITION, so the chief of staff'sassistantskey never reaches Bean or Chet, with a C2-08 refusal test.organization.set'sinstanceskey refuses a creative instance for any actor.instances.setgets one branch, so the human can add or change a creative instance without a hand edit, and the tester can't. That is how a model change, a persona change or a later specialist happens (4, 10 step 7). This also moves C2-08 from Haiku to Opus, because it now changes who may write the business file (M10 in 5.1). - B4. The persona block goes between the contract and "## This session", fenced like skills, and the bundle refuses text that holds the end marker or a level 1 or 2 heading (4).
- N1.
library_get'slinks_outandlinks_indrop other-universe and unmapped targets (3). - N2. The setup sheet says moving a note into a book folder makes it canon (7.1).
- N3. M9 sits in
organizationWriter.write(organization.mjs:177), compares all ofprojects, hybrid entries included, and its test callswritedirectly (1.1, 5.1). - N4. Obsidian resolves a shortest-path link across her whole vault,
~/src, and the reader sees one repo, so the two can resolve one link to different files. Stated in 1.3 and the setup sheet. - Webby's answers to my three questions: reporting is enough for 1.3, M9 holds today, and the persona order is B4. I take all three as written.
Mos, mos-1033. B2 and B3 are required, and the chief of staff gets no path to rewrite Bean's or Chet's persona, because the personas are Melanie's. With B3 above, only the human changes a persona or a creative instance, and no organization read or history line holds persona text. C2-20 reads the same things as in r2. Only its output changes, to the persona file and its digest (9.1).
A correction to r2. r2 said that with END_PROTECTED set to THREAD_DEFINITIONS, only the human could
end Melanie's agents. That's wrong. An END_PROTECTED instance is ended by the chief of staff or by a
decision the human resolved (broker.mjs:148-150, Q169). Ending a run changes no persona (4).
Changes from r1
Jason's rulings (DECISIONS Q180, relayed in mos-1014) come first, because they set the slice:
- She talks to her agents inside Mosaic Stack. The 10-27 slice has no Discord dependency. Its chat channel is the Console (C2-10b), which renders cited items and shows sync status, so B1 and B2 are solved there. The M5 guard and cite check cover it through D8. The Discord relay (C2-18) moves after the slice as an option, and M8 stays in the slice so no Discord path can skip the guard (5.1, 7.6).
- Bean and Chet become Mosaic agents with the same names and personalities. They are two instances of
a new creative definition,
companion, and each carries its persona in a new agent-layer var (4). New row C2-20 reads only the persona definitions of the two Hermes profiles on 10.1.1.54 (default and chet) and confirms which profile is which name. Her Hermes memory store (188 facts) and chat history stay where they are unless she chooses otherwise through Jason. - Hermes keeps running until Bean and Chet are live in Mosaic. Nothing in this design stops, changes or shares anything with Hermes, its bots or their tokens (9).
- The host is a variable,
<host>, with the instance under its own user<user>without sudo. Mos's reading, which he is confirming with Jason, is her VM 10.1.1.54 with RAM added (9, 10).
Mos's host check (mos-1000, jason-master-restart projects/mosaic-mvp/MELANIE-HOST-RECON.md) changed
the data model:
- Her library already exists. Her books repo on git.mosaicstack.dev has 203 Markdown files and 3,486
commits. obsidian-git syncs it, and each book folder has its own subfolders. r1 invented a new
universes/tree and alibrary.json. r2 drops both and reads her repo as it is. A mapping in the business file names her book folders, her type folders and her playground folders (1.1). Her repo gets no new file. - Canon and playground map onto her layout by path prefix. A file under a configured book folder is canon. A file under a configured playground prefix is playground. Anything else is unmapped, and agents can't see or cite it (1.2).
- Her rules aren't mine to impose on 3,486 commits of her work. The validator now reports problems and never holds back her latest commit. The scope guard in code is the path rule plus the cite check, and neither depends on her files passing a rule (1.3).
- In the slice the bus never writes to her repo. Agents can't write anyway (D3), and the Console editor comes after the slice. So the slice reads a bare mirror through a read-only token, and pushing moves to later rows (C2-02, C2-03b).
- Her vault root is
~/src, one level above the books repo, so Obsidian's absolute links may start with the repo's folder name. The link resolver strips a configuredvaultPrefixand also resolves Obsidian's shortest-path form. A link into another of her repos never resolves. That keeps bean-brain out by construction, and the bus token reaches only the books repo (mos-1000 item 3). - The instance has its own user and no shared state with anything else on its host. Row 190 doesn't
block it. C2-13 delivers an install script and a precheck that Jason or his delegate runs on
<host>(9, 10). - Mos, mos-0992 item 3. Any Discord output must go only through the broker's stored, guarded messages.
Today's connector posts its own pi engine's text straight to Discord
(
packages/discord/src/connector.mjs:381,:386-387), so it would skip every guard. M8, in the slice, refuses that engine on a library business. The later relay (C2-18) posts only stored messages, and its tests prove it (5.1, 7.6). It would need a new Discord application, never her two Hermes bots. - Mos, mos-0992 item 4 and mos-1000 item 5. The B5 caps have numbers, and the residual is stated in numbers in 5.1.
- Mos, mos-0980 item 4. Section 10 names the Gitea host and lists Jason's steps. C2-13, and anything that creates an account, contacts Melanie or touches her repo's access list, waits for Jason.
Webby's review (webby-0001, CHANGES, notes at agents/webby/work/case2-r1/REVIEW.md, f9d218ab1). I
re-read every line Webby cites, and each one holds. Where her existing repo changed what an item means,
the line says so.
- B1. The slice now holds C2-09a:
#threaded, the launcher andthread.createreadTHREAD_DEFINITIONS, and the chat channel picks the agent.room.*is C2-09b, outside the slice (6, 8, 9). - B2. The slice holds the Console renderer and status line (C2-10b, 7.4). It renders cited items and
reads the new human verb
library.status(7.2). - B3.
decision.raiseis refused for creative definitions, and their typed tool set dropsraise_decision. An agent asks with aquestionitem, which the guard checks (5.1 M6). - B4. On a library business the conversation reader leaves creative sessions out of
catalogue()and refuses to open one by id (5.1 M7, C2-10a in the slice). - B5. At most 10 items and 1,200 characters of item text per reply. An
ideaitem is at most 200 characters, at most 5 per reply. Other kinds are at most 300 (5.1). Q219 later set ideas to at most 3 of 120 characters (ruling after r11), and Q222 made every limit a setting with those defaults. - B6.
correction.frommust equal one cite'squote, inside that cite's lines (5.2). - B7. The broker keeps fetched page text per session, bounded and in memory, not in a table. The bundle
refuses the
harness-builtinweb source for creative definitions (5.2, 5.3). - B8. Split by path. The slice reads blobs from a bare mirror and never checks out a working tree, so
symlinks, gitlinks,
.gitattributesfilters and.gitmodulescan't act. The reader reads only100644blobs up to 1 MiB and reports the rest (rule R1). r1's S0 rule moves to the bus write path (C2-02), where it guards the working tree. Whether her repo holds any such entry is a C2-13 survey fact. If it does, C2-02 needs a ruling before it can write. - B9. Moves to C2-03b with the push path. The slice never writes, so there is no write to refuse.
- B10. One function,
syncCommand, builds the production argv and env. Tests assert both exactly. The file-protocol switch is a module parameter that no business file field or bus verb can reach (7.2). - B11. C2-04 lists every consumer of
root, and each one skips a library project with its own test (2.1). - B12. Path fields are now her keys
appears_inandrelationships, plus the business file mapping. They are indexed like links, and scope comes from the target's path.library_manuscripttakesscopeand labels its result. A hybrid is a business file entry (1.4). Agents can't write the business file (M9), sosanctionedand the hand-edit question go away. - N1. Blob reads are cited at
read.mjs:19-26. - N2.
END_PROTECTEDbecomesTHREAD_DEFINITIONS, so no organization write can end Melanie's agents (4). - N3. The runner's two failure texts become the fixed notices
no-answerandturn-failed. The guard accepts exactly three notice bodies (5.2). - N4. Promotion moves to the write rows (C2-19), as a multi-path commit.
- N5. Her file names can have capitals and spaces, and they aren't mine to rename. Two paths that differ only in case are reported (R7) and both are left out of lookups.
- N6. A creative role must have a
messagingkey (4). - N7. Row order is redone for the read-only slice (8).
- N8. The C2-06 gate runs the guard through both send paths and with a non-creative sender (8).
- N9.
room.opennames its refusalroom-thread-name-taken(6).
0. Summary
Melanie's writing already lives in a git repository, her books repo on git.mosaicstack.dev, and her
Obsidian syncs it with obsidian-git. The hub reads that repo. The bus keeps a bare mirror of it, fetched
with the chief of staff's read token, and the business file maps her folders onto books, types, canon,
playground and supporting folders. Agents never write her story. They read it through typed lookup
tools, and they may create, edit and delete .md files only in folders she marked as supporting. Each
such write is a commit she sees as a notice and can revert. An agent's reply is free text. Any citation
it carries must be real, and the broker checks each one before it stores the message. Each creative
agent holds its own thread.
Bean and Chet, her two agents from Hermes, keep their names and personalities as Mosaic agents, and the
specialist roles sit beside them. She talks to them in the Console and sees only stored messages that
passed the guard. Hermes keeps running until she has moved over.
Decisions (D-codes are this document's own):
- D1. Her repo stays as it is. The design adapts to her layout through a mapping in the business file, which agents can't write.
- D2. Scope comes from the path. No file content can change a file's scope.
- D3. Agents never write story. An agent writes only
.mdfiles under a supporting prefix, and M12 enforces that in code (ruling after r11, Q223, Q226). Canon, playground that isn't also supporting, and everything else stay read-only to agents. - D4. One chat is a project. Each creative agent holds its own thread in it. The
threadstable and its triggers don't change (section 6). - D5. The bus writes to her repo only as plumbing commits on the bare mirror, pushed fast-forward only: an agent's supporting doc write, the human's revert and her own Console save (ruling after r11).
- D6. The Gitea remote is where the bus and Obsidian meet. The bus only fast-forwards and never merges.
- D7. Anything fetched from the remote is untrusted input. The bus reads it as blobs, never checks it out and never runs anything from it.
- D8. Every channel to Melanie shows only messages the broker stored after the guard. A channel that shows a model's raw output is refused on a library business.
1. Data model
1.1 Her repo and the mapping
From mos-1000: each book folder has subfolders for characters, chapters, chapter summaries, locations,
research and a tracker. Front matter keys seen are type, name, appears_in and relationships. The
repo also has .base and .canvas files. I haven't seen the exact folder names, and I shouldn't read
her text to find them. The survey command from C2-01 prints names and counts only (10 step 7).
The mapping is a new form of business file project entry. Business files live in
<configDir>/businesses/<id>.json (packages/business/src/business.mjs:1-3, :30-32):
"projects": {
"books": {
"kind": "library",
"remote": "https://git.mosaicstack.dev/<owner>/<repo>.git",
"branch": "main",
"vaultPrefix": "<repo folder name under ~/src>",
"universes": {
"<u>": {"name": "<display name>", "books": ["<book folder>", "<book folder>"],
"playground": ["<book folder>/<playground folder>/"],
"supporting": ["<book folder>/<notes folder>/"]}
},
"editor": {"name": "<her name>", "email": "<her email>"},
"types": {"<characters folder>": "character", "<locations folder>": "place",
"<research folder>": "research", "<tracker folder>": "tracker",
"<chapter summaries folder>": "summary"},
"manuscript": ["<chapters folder>"]
}
}
- Every value in
<>comes from the survey and Melanie's say, through Jason (10 step 7). universesgroups her book folders. If she has one world, it is one universe holding every book folder. Jason'suniverse.book.{character,place,...}then reads as universe, book folder, type folder.playgroundis a list of path prefixes inside each universe entry (B1). A prefix inside one of that universe's book folders is book level. A prefix outside every book folder is universe level. A"kind": "draft"entry has prefixes and no books, and is where a new universe starts. No prefix may hold another, or hold a book folder. A prefix inside, or holding, another universe's book folder refuses the business file withlibrary-playground-other-universe, so every playground file has one universe, and the one-universe cite check (5.2) has something to compare (ruling after r11, three levels). Her repo has no playground today as far as I know. The setup sheet suggests one folder per book, for example<book>/playground/. Until she makes one, everything mapped is canon.supportingis a second list of path prefixes in each universe entry (ruling after r11, Q223). An agent may write.mdfiles under a supporting prefix and nowhere else (M12). The prefix rules are the playground's, and the refusal for another universe islibrary-supporting-other-universe. A prefix may sit in both lists, and that folder is writable. A playground prefix alone isn't. Her repo has no supporting folder until she names one, and until then agents write nothing.editoris the name and email her Console saves commit under (F41). The setup sheet asks her for it.typesmaps a type folder name to a type. A file's front mattertype, if present, is shown next to it but doesn't change scope.
Agents can't change this mapping. No organization verb writes projects today: organization.mjs has no
write of it, and instance-settings.mjs:141 only reads proposed.projects. An organization write sends
a whole proposed document, though, so C2-04 adds a refusal, organization-projects-protected, for any
organization write whose projects differ from the file's (M9). Every setter builds its document from
the file and changes only its own keys (broker.mjs:1261-1271), so the rule holds today. It goes in
organizationWriter.write (organization.mjs:177), the one place every setter's write passes
(instance-settings.mjs:154, launch-settings.mjs:207, :277, plugin-settings.mjs:60). It compares all
of projects in canonical JSON, hybrid entries included, and its test calls write directly (N3). The
mapping changes only by a hand edit of the file.
1.2 Scope
Each path in her repo, relative to the repo root, gets one scope:
| Scope | Rule |
|---|---|
supporting |
A .md file that starts with a supporting prefix of any universe entry, drafts included. This comes first, so a folder in both lists is supporting. Agents may write it (M12) |
playground |
Otherwise, a .md file that starts with a playground prefix of any universe entry, drafts included |
canon |
Otherwise, a .md file inside a configured book folder |
unmapped |
Everything else: top-level notes, .base, .canvas (under a playground or supporting prefix too), attachments, anything outside a book folder and outside every playground and supporting prefix |
Lookups default to canon and return playground or supporting files only on request. Unmapped files are
never returned or citable. The universe of a canon file is the universe whose books list holds its book
folder. The universe of a playground or supporting file is the entry whose prefix holds it, and its book
is the book folder that holds the prefix, or null at universe level and in a draft (ruling after r11,
three levels).
1.3 Reading and reporting
The reader works on a commit, not a working tree. It lists the tree with git ls-tree -r -z and reads
blobs with cat-file, as blob() does in packages/project/src/read.mjs:19-26. That function accepts
100644 or 100755 (:21). The library reader is stricter.
None of these rules holds back her latest commit. Each produces a problem in library.status, with path
and line where there is one, and the index leaves the affected file or link out.
| Code | Rule | Effect |
|---|---|---|
| R1 | Only 100644 blobs up to 1 MiB are read. Symlinks, gitlinks, executables and larger blobs aren't |
not indexed |
| R2 | A path is unmapped (1.2) | not indexed |
| R3 | Front matter the reader can't parse fully. It reads scalars, quoted strings, flow lists and block lists of scalars or wikilinks. Anything else, nested maps for example, stays raw text | indexed by path, file name and body |
| R4 | A link or path field that resolves to no file, or to more than one | link not followed |
| R5 | A canon file refers into the playground | reported only. Lookups and the guard still go by the target's path |
| R6 | A canon file refers into another universe | reported only. Lookups never cross universes (3) |
| R7 | Two paths differ only in case | both not indexed |
Links are [[target]], [[target|label]] and [[target#heading]]. The resolver tries, in order, a
path from the repo root, the same path with vaultPrefix/ stripped, a path relative to the linking file,
and Obsidian's shortest-path form, a base name that matches exactly one indexed file. A link whose target
is in another of her repos under ~/src resolves to nothing (R4). appears_in and relationships
values are read as links when they are wikilinks, and as names otherwise. A ![[target]] embed of a
note resolves like a link. A link or embed whose target ends in an extension other than .md refers to
an attachment: it isn't followed and raises no problem (ruling after r11).
One known difference (N4). Obsidian resolves a shortest-path link across her whole vault, ~/src, and
the reader sees only the books repo. A base name that is unique in the repo but also exists in another
of her repos can resolve to different files in Obsidian and in the hub. The reader can't detect that,
because it never sees the other repos. A link written as a full path resolves the same in both, and the
setup sheet says so (C2-12).
Why reporting is enough for separation. The guarantee Jason asked for is that agents keep playground and canon apart. The code that does that is the scope rule, the lookups' default and the cite check in 5.2, which refuses a playground or supporting cite marked as canon. None of those needs her files to pass a rule. Refusing her latest commit would only stop the agents seeing her newest work.
1.4 Hybrid crossover (after the slice)
A hybrid is a business file universe entry with kind: "hybrid", sources (two or more non-hybrid
universes), books (its own folders), constraints and notes. Agents can't change projects (M9), so
a hybrid exists only by a human edit of the business file. In a hybrid's scope, lookups return its own
files plus the sources' canon, each labelled with its source universe. Constraint kinds checked in code:
{"exclude": "<source path or source/type>"}. Lookups in the hybrid never return it.{"pin": {"entity": path, "field": name, "value": v}}. Reported (R-class) when a hybrid file changes it.
Plain-word constraints go in notes. The continuity role reads them and cites them in its findings.
C2-15 builds this.
1.5 Promotion (after the slice)
Promotion moves files from a playground prefix into a book folder. Only Melanie does it, in Obsidian
today. A Console verb library.promote {from, to} comes later (C2-19). It is human only and moves every
file in one plumbing commit, by the steps of the ruling after r11 with several paths, and pushes.
2. What exists and how Case 2 uses it
2.1 The business file and project record repos
-
The business file accepts only
rootandrepoper project (packages/business/src/business.mjs:186) and requiresroot(:187). C2-04 adds thekind: "library"entry from 1.1, which has noroot. Several places assume every project has a root. Each must skip a library project, with its own test (B11):ensureCheckoutsclones every entry's root (packages/seat/src/checkout.mjs:310,:319). The launcher passes it every project (packages/cli/src/launcher.mjs:353).projectFilePath(entry.root)(packages/business/src/project.mjs:9-10) is called atpackages/business/src/cli.mjs:72,:78,:118,packages/business/src/resolve.mjs:44andpackages/business/src/instance-settings.mjs:142.projectRepo's overlap loop callsabsolute(project.root)(packages/project/src/paths.mjs:41-44).
With those skips, agents get no clone of her repo. C2-04's launcher test proves it.
-
Mirror location:
projectRepo(paths.mjs:34-48) puts a project repo in<dataRoot>/projects/<business>/<project>and refuses symlinked ancestors. The bare mirror uses the same path rule. -
Git:
git()passes onlygitEnv(), the ceiling directory and the identity (git.mjs:10-12).SAFEsetsprotocol.allow=never(git.mjs:8), andinspectGitrefuses anyremote.*key (git.mjs:44). The mirror keeps both rules. The URL goes on the command line from the business file, and fetch uses its own command builder (7.2). -
Bus writes (C2-02) don't use
commitFile(index.mjs:195-234), which commits in a working tree. The mirror has none (D7). C2-02 writes plumbing commits on the bare mirror by the steps of the ruling after r11, under the library lock and after M12.commitFileandwriteHumanFilestay as they are.
2.2 ProseMirror editor (packages/webui/editor)
- It edits Markdown (
model.mjs:17-18). Blocks it can't draw, front matter included, are kept byte for byte (model.mjs:3-11,:220-231). - The Console opens the visual editor only if
roundTrip(text) === text, otherwise Source (packages/webui/src/public/bus.js:919-920). The commonmark rules (model.mjs:119-120) have no wikilink rule. I didn't measure whether the serializer escapes[[. C2-11a's first test does. - Load and save:
GET/POST /api/project-file(bus.js:907,:974), thenpackages/webui/src/serve.mjs:545-547and:591-593, thenproject.fileandproject.file.save(packages/webui/src/cli.mjs:75-76), thenpackages/bus/src/broker.mjs:1434-1458. The allowlist is indocumentPath(paths.mjs:14-15) andDOC_PATH(bus.js:885). Case 2 adds library verbs next to these and leavesdocumentPathas it is.
2.3 Roles (roles/*.json, packages/business/src/role.mjs)
validateRoleDocument(role.mjs:138) checks a role file. Its keys are closed (:19-20),toolsmust be non-empty and drawn fromTOOLS(:23-28,packages/business/src/vocabulary.mjs:92-97), andmessaginglimits who it talks to (:55-66). A role with nomessagingkey may message anyone (:50-51).- Tools reach the harness through
packages/business/src/resolve.mjs:60-64, thenpackages/harness/src/bundle.mjs:247-256(policy.json), thengate.mjs:130-138, which refuses any built-in not in the policy. - The harness's own web tools are on only when the organization's search source is
harness-builtinand the role grantsweb(bundle.mjs:28-35,gate.mjs:28-33). Those fetches never reach the broker (5.2).
2.4 Tool gate and typed tools (packages/harness/src)
decide()(gate.mjs:130-167) allows a typed tool by name (:134), allows a built-in only if the policy holds it (:137-138), and keeps file paths inside the workspace (:146-156). The Claude Code hook turns a refusal into exit 2 (claude-gate.mjs:13).typedTools(tools.mjs:91-261) builds the typed tools per session from held actions.write_recordexists only forrecord.writeholders (:211-220), andsend_messageformessage.sendholders (:95-102).raise_decisiongoes to every agent (:221), with free-textquestionand optiontext.- Case 2 adds the lookup tools (section 3) when the business has a library project, adds the two write
tools for a creative session holding
library.write, and dropsraise_decisionand the other non-creative tools for creative definitions (5.1 M3, M6).
2.5 Threads, messages, conversations and Discord
thread.createpicks the single instance whose definition isassistantand refuses more than one (broker.mjs:2331-2334). It stores that instance on the thread (:2336-2347). The same rule is in#threaded(:290-291),vocabulary.mjs:131, and a second copy inlauncher.mjs:135and:236-238. This is code, not schema.thread.sendstores one message tot.instanceand routes it (broker.mjs:2374-2399). Triggers require every threaded message to be to or from the thread's instance (packages/bus/schema.sql:296-299) and deliver only to the thread's holder (:300-306).threadscan't be updated (:310). Thread names are unique per project (:249).- The runner sends the model's final text as a
message.sendRESULT to the sender (packages/harness/src/runner.mjs:571-574ataca16404c). It sends(the session gave no answer)for an empty turn andThe turn failed: <reason>for a failed one (:572). A refused send is logged andmessage.readis skipped (:577-583), so the message stays unread and the answer never reaches her (webby-0151 N2). - Sessions are per thread:
threadStore(dataRoot, business, thread)(packages/seat/src/session.mjs:59-64) and the Claude session id file under it (adapters/claude/adapter.sh:47-53). One thread per agent means one session per agent. - The Console's conversation view proxies
/api/conversationsand/api/conversationto the control board (packages/webui/src/serve.mjs:559-569), which answers fromreader.catalogue()andreader.open()(packages/control-board/src/serve.mjs:210-219). The reader lists session files (packages/conversation/src/reader.mjs:225-240) and emits the model's raw text blocks (packages/conversation/src/events.mjs:58,:82). A refused answer stays in the session file, so this view would show it (5.1 M7). - The Discord connector (
packages/discord) binds one seat's conversation to listed channels. It builds its own engine inpackages/discord/src/cli.mjs:210-212fromengine-pi.mjs. Each turn callsengine.prompt(connector.mjs:381) and posts the result throughdeliver(:386-387,rest.createMessageat:106). The broker never sees that text. The README names the engine as the module to swap (packages/discord/README.md:13-14). Authorization is the binding'suserslist (connector.mjs:84,:443).
3. Agent lookup tools
Broker verbs with typed tools. Every agent session in a business with a library project gets them. Each reads the mirror at its current served commit (7.2) and returns that commit.
| Tool | Arguments | Returns |
|---|---|---|
library_find |
universe (required), query, type?, book?, scope? (canon default, playground or supporting) |
up to 50 {path, type, name, appears_in, scope, universe, book} |
library_get |
path |
{path, commit, blob, scope, frontmatter, lines: [[n, text]], links_out, links_in} |
library_search |
universe, text, book?, scope? |
up to 50 {path, line, excerpt, scope} |
library_manuscript |
universe, book, scope? (canon default), chapter?, from?, to? |
{path, commit, scope, lines}, one chapter file, at most 400 lines per call |
library_universe |
universe |
its books, type folders, and for a hybrid the sources, constraints and notes |
- Universe isolation is in code.
universeis required, and results never leave it, except for a hybrid's declared sources minus exclusions. - Every result carries
scopefrom the path (D2). Playground is opt-in. Unmapped files never appear. library_get'slinks_outandlinks_indrop targets in another universe and unmapped targets, as the other tools do (N1). A hybrid keeps links into its declared sources.- Replies stay under
REPLY_CAP(tools.mjs:271-280) by paging. They are never cut. - The index (types, names, links and backlinks) is built per commit and cached in memory by commit id. Her repo has 203 Markdown files today. I estimate a rebuild well under a second, and C2-05a's gate measures it on a 3,000-file fixture.
Writes. library_doc_write {universe, path, base, content} and library_doc_delete {universe, path, base} call the agent verbs of the ruling after r11. Only a creative session holding library.write gets
them (M3). base is the blob that library_get returned, or null to create. Each returns {commit, path} or a refusal code, and a successful write also stores the notice.
4. Creative roles, Bean and Chet
Six new role definitions. Each is a version 2 role file plus a .md contract.
| Role | Purpose | tools |
|---|---|---|
companion |
Her general writing partner. Backs Bean and Chet | read, web |
ideas |
Brainstorming, mostly in the playground | read |
research |
Real-world facts from the web | read, web |
proofreader |
Spelling, grammar, punctuation | read |
fact-checker |
Text against canon and her research notes | read |
continuity |
Consistency across books, timeline and her tracker | read |
All six have messaging: {sendTo: ["human"], receiveFrom: ["human"]}, so they can't relay through each
other. network is none, except api-only for companion and research. C2-08 landed them with
authority: {withinRole: ["message.send"], crossRole: []} and credentials: []. C2-08b changes both,
which is a role change Mos authorized in mos-1309 item 5: withinRole becomes ["message.send", "library.write"], and credentials becomes [{"service": "gitea"}], the definition credential the
grant path needs (business.mjs:132-167). Each creative instance also gets the identity and publish
fields that path reads. roles/chief-of-staff.json gets the same credential and no library.write, so
the chief can hold the read grant the mirror fetches with.
Bean and Chet. Jason's ruling (Q180): "She will want to keep those agent names and personalities." They
are two business role instances, bean and chet, both with definition companion. The personas are
Melanie's (mos-1033), so only the human changes one, and no organization read or log holds the text.
Where the persona lives (B2). Instance entries take inline vars only
(packages/business/src/business.mjs:89). Inline vars go wherever the business file goes. The
instances read returns every instance's vars whole (packages/business/src/instance-settings.mjs:58-63),
and the tester may call it (TESTER_ORGANIZATION_VERBS, broker.mjs:103). Every organization write logs
old and new values to businesses/.history/<id>/changes.jsonl (organization.mjs:70-80, :204). So the
text stays out of the business file. C2-08 declares two agent-layer keys in REGISTRY
(packages/business/src/vars.mjs:102):
persona.name, a string of at most 40 characters, the name she sees ("Bean", "Chet").persona.sha256, 64 lowercase hex characters, the digest of the instance's persona file.
The persona file is <configDir>/personas/<business>/<instance>.md, outside businesses/. It is UTF-8,
at most 8,000 characters, mode 0600, in 0700 directories owned by <user>. The bundle reads it at launch
with O_NOFOLLOW. It refuses a file that isn't regular, isn't owned by the bus's uid or has any group or
other bit, the same check organization.mjs:141 makes on changes.jsonl. It refuses persona-mismatch
when the file's sha256 isn't the var's. So the instances read, the Console's view of it and
changes.jsonl show persona.sha256 and never the text. No verb returns the file, and agent file tools
stay inside the workspace (packages/harness/src/gate.mjs:146-156). A persona var or file is refused on
a non-creative definition.
Where else the text sits (N3). The persona's own session gets it in its prompt, which is the point, and two copies come with that:
prompt.mdin each run's bundle, mode 0600 in a 0700 directory (bundle.mjs:241-244,:265), kept until the run is pruned. Only<user>can read it. The C2-13 precheck checks the run root's mode.- The harness's argv. Both adapters read
prompt.mdinto a variable and pass the text as--system-prompt(adapters/claude/adapter.sh:79,:82,adapters/pi/adapter.sh:106,:121). Any local user on the host can read that in/proc/<pid>/cmdline. C2-08 makes both adapters pass the file instead. Pi 0.85.1, the pinned version (package.json:8), reads--system-promptas a file when the value is an existing path (dist/core/resource-loader.js:17-30). It uses the path string itself as the prompt when the file doesn't exist (:21,:30, no warning) or can't be read (:24-27). So the adapter checks[ -r ]on the file first, which covers both, and refuses otherwise. Claude Code 2.1.304's--helpnames--system-prompt-file. I read the help but didn't run it, and the repo doesn't pin the claude version. C2-08's adapter tests check that argv holds the path and not a marker from the prompt. When some creative instance's harness is claude-code, C2-13 runs one marker prompt through the host's claude with--system-prompt-fileand checks that it replaces the default prompt rather than adding to it. With the r9 model path, Bean and Chet run pi. If the host's claude lacks the flag, the argv copy is a residual for Jason, stated with that version.
Modes, from code. The writer creates the business file's candidate and changes.jsonl with mode 0600
(organization.mjs:61, :139) and its .history directories with 0700 (:39). It refuses a log with any
group or other bit (:141). C2-08 tests those modes after an instances.set write, and the C2-13
precheck checks the business file, .history and the persona files on <host>.
Who can change a persona or a creative instance (B3, and r3's B1). The rule is one check, used on every
path below: a write that touches a CREATIVE_DEFINITIONS instance is accepted only when its actor is
the business file's human. The broker sets the actor to the human's id for a human session, to
tester for the tester (broker.mjs:1272), and to <definition>.<instance> for a role session such as
the chief of staff (:1278). Otherwise it refuses organization-creative-human-only. The five paths
today, and what C2-08 does to each:
limits.set is the human's verb and organization.set is the same write for a role (broker.mjs:1248-1251).
Both take ORGANIZATION_KEYS (:145) and reach the same set() (:1284, launch-settings.mjs:150-169),
so the first three paths below are reached by both verbs, and the check keys on the actor, never the verb.
- The
assistantskey, which the chief of staff holds (broker.mjs:145,:1266-1271), targets only definitionassistant(packages/business/src/launch-settings.mjs:140,:186,:191). Those three lines stayTHREAD_DEFINITION, notTHREAD_DEFINITIONS. Test:assistants: {bean: ...}from the chief of staff refusesorganization-not-assistant, and the file is unchanged. - The
instanceskey sets themodelvar of any instance (launch-settings.mjs:170-177). It now refuses a creative instance withorganization-creative-instance, for every actor. Test: the chief of staff'sorganization.setand the human'slimits.setwithinstances: {bean: ...}both refuse. She changes Bean's model withinstances.setorroutes.set. - The
modelskey replaceslaunch.modelswhole (launch-settings.mjs:169), which can change the model behind a key Bean or Chet names without touching the instance. A write that changes or removes alaunch.modelsentry named by a creative instance'smodelorfallback.modelnow refusesorganization-creative-human-onlyunless the actor is the human (N1, mos-1051 item 2). Test: the chief of staff'sorganization.setchange to Bean's key refuses with the file unchanged, its change to an unnamed key works, and the human'slimits.setchange to Bean's key works. The last case shows the check keys on the actor, not the verb. Mutant: dropping the check makes the first case fail. routes.setreplaces an instance's harness, model and thinking vars (launch-settings.mjs:240-277). It is human only, but the tester may call it too (broker.mjs:103,:1258).setRouteschecks no definition before its write at:277. It now refuses a creative instance unless the human calls it. Test: the tester'sroutes.setonbeanrefuses with the file unchanged, and the human's works.instances.setis human only, and the tester may call it too (broker.mjs:103,:1263-1265). Today it refuses anyEND_PROTECTEDdefinition (instance-settings.mjs:23,:109), which would make every creative add or change a hand edit. C2-08 adds one branch. An add or change of aCREATIVE_DEFINITIONSinstance is accepted from the human and refused from the tester withorganization-creative-human-only. That is how her agents' models change, how a new persona digest is set after the file is replaced, and how a specialist is added later (10 step 7). Tests: the human addsideasand changesbean's model, the tester's add and change refuse, andinstances.setstill refuses anassistantinstance.- The tester is off on her business.
bindTesterrefuses unlessbus.testeris exactly true (broker.mjs:542-545), and the C2-13 precheck refuses a library business with it on. If it were on, the tester would see digests only, and each of the paths above would refuse it.
C2-08 runs every tester case through the broker with a bound tester session, and every chief of staff
case with a role session, so a change to how the broker sets the actor (:1272, :1278) fails a test.
The human id can't be tester (broker.mjs:245), and no instance may be named tester
(packages/business/src/instance-settings.mjs:86).
The persona block (B4). prompt() puts the contract first (packages/harness/src/bundle.mjs:177), then
"## This session" (:179) and the session lines. The persona block goes between the two:
<contract>
## Persona
Your name here is <persona.name>. The persona below sets your voice and manner. The role contract above
wins on any conflict, and the broker checks every reply whatever the persona says.
<persona-file instance="bean" sha256="<64 hex>">
<persona text>
</persona-file>
## This session
It is fenced the way skills are (SKILL_END, bundle.mjs:107, :170). The bundle refuses
persona-invalid for text that holds </persona-file> or has a line starting # or ## , so the text
can't close its fence or open a section of its own. C2-08's snapshot pins contract, persona, session, in
that order. The order is a hint to the model. The enforcement is in code, M12 on writes and the
citation guard in 5.2, and neither reads the persona, so no persona text can lift a guard.
What changes for her. In Hermes, Bean and Chet answer in free text and may write story text into her files when she asks. In Mosaic they still answer in free text, of any length, and they never write into her story files. Whatever they write lands in a supporting folder she marked, as a commit she sees and can revert. That follows from Jason's "NEVER" as Q223 reads it. She should hear it before the move, not discover it (10 step 7).
The five specialists stay available as further instances. Whether the slice starts with Bean and Chet
only or with all seven is her choice through Jason (10 step 7). Each running agent session measured 300
to 390 MB on dragon-lin (mos-1000), and a thread's session stops after 900 idle seconds
(THREAD_IDLE_SECONDS, business.mjs:20), so memory depends on how many she talks to at once.
read is there because role.mjs:24 requires a non-empty tool list. The workspace has no clone of her
repo (2.1), so read reaches nothing of hers. All library reads go through the lookup tools.
New constants in vocabulary.mjs, each changed only by a reviewed commit:
THREAD_DEFINITIONS = ["assistant", "companion", "ideas", "research", "proofreader", "fact-checker", "continuity"]. This replaces the singleTHREAD_DEFINITIONwherever it means "may hold a thread". The definition the chief of staff may set staysassistant(tools.mjs:186), and so do the threeassistantslines inlaunch-settings.mjs(B3 above).END_PROTECTED(vocabulary.mjs:135) becomesTHREAD_DEFINITIONS(N2). An instance of one is then ended only by the chief of staff or a decision the human resolved (broker.mjs:148-150, Q169), not by another agent. r2 said only the human, which was wrong. Ending a run changes no persona, and a thread's session stops after 900 idle seconds anyway. This matches TARGET.md:66 ("hire/fire authority for agents as needed; unless user forbids per agent"). If Melanie wants it forbidden for an agent, the request comes through Jason (mos-1038).CREATIVE_DEFINITIONS, the six above.CREATIVE_TOOLS = ["read", "grep", "find", "ls", "web"].validateRoleDocumentrefuses a creative definition whosetoolsinclude anything else, whose authority holds anything butmessage.sendand, after C2-08b,library.write, that has nomessagingkey, or whosemessagingnames anything buthuman(N6).- C2-08 also landed
CREATIVE_KINDS. C2-08b removes it, since replies have no item kinds now.
5. Guardrails enforced in code
5.1 Agents never write story
| Mechanism | What it refuses | Test |
|---|---|---|
M1 role validator (role.mjs, CREATIVE_TOOLS) |
A creative role file with write, edit or bash, any authority but message.send and library.write, or a missing or wider messaging |
C2-08: each rejected variant refuses with its message |
M2 tool gate (gate.mjs:137-138, unchanged) |
Write, Edit or Bash from a creative session |
C2-08: decide() with each creative policy refuses all three, on pi and claude-code |
M3 typed tool set (packages/harness/src/tools.mjs) |
The tool list is exact: the five lookups, send_message, library_doc_write and library_doc_delete, plus web_search and web_fetch for companion and research (mos-1308, ruling after r11). No raise_decision, read_spec, list_agents, inbox, trail, write_record or tracker read |
C2-05b: a snapshot of tool names per creative role (row 224, six or eight). C2-02 adds the write tools to it: eight names, or ten for companion and research |
| M4 human verbs and agent verbs | library.file, library.file.save, library.sync, library.status, library.limits.set and library.revert from an agent session refuse human-required, as at broker.mjs:1434-1435. library.doc.write and library.doc.delete refuse every caller but a creative session holding library.write and a live grant |
C2-04, C2-03a, C2-06 and C2-02: each human verb from an agent session refuses, and each agent verb from the human, the tester, the chief of staff and a creative session without a grant refuses |
| M5 citation guard (5.2) | A cite that fails a check in 5.2, whenever a reply carries one. More than citationsPerReply citations. A reply with none when requireCitation is true. Both keys are replyLimits settings read at check time (Q222, Q226), and a missing or invalid one uses its default. An unreadable business file resolves requireCitation to true |
C2-06: each check refused once and passed once. citationsPerReply refused one past the limit and accepted at it. requireCitation true refuses an uncited reply and false accepts it. A changed value takes effect on the next reply. Each invalid value uses the default, "true", 1, null, [true], {}, 0, 1001, 2.5, true for the count and "5" included. With the business file unreadable, a reply with 21 citations refuses, an uncited reply refuses citation-missing, and messages is unchanged in both |
| M6 no free-text decision channel | decision.raise from a creative definition refuses decision-not-allowed in the broker. The typed tool is also absent (M3) |
C2-08: a creative session's raise refuses and no decision row appears. An assistant's raise still works |
| M7 no raw session view | On a business with a library project, the conversation reader leaves creative sessions out of catalogue(), and open() refuses them with conversation-not-listed |
C2-10a: a creative session file under the root isn't listed and won't open by id. An assistant session still lists |
| M8 no raw Discord engine | On a business with a library project, discord check and discord run refuse a binding whose engine isn't mosaic-bus (discord-engine-not-allowed), and a mosaic-bus binding with tools |
C2-10a: both refuse on a library business. A pi binding on a business without a library still checks clean |
| M9 no agent write to the mapping | An organization write whose projects differ from the file's refuses organization-projects-protected, in organizationWriter.write |
C2-04: write called directly with a changed playground prefix, and again with a changed hybrid entry, refuses, and the file and changes.jsonl are unchanged |
| M10 personas and creative instances are the human's | Any write by an actor other than the business file's human that touches a creative instance: the chief of staff's assistants, instances and models keys through organization.set, and the tester's instances.set and routes.set. A persona file that fails its digest, mode or fence check. A harness argv that holds the prompt text |
C2-08: each path refuses with its code (4) and the file is unchanged. The human's instances.set and routes.set on bean work, and so does her limits.set change to Bean's launch.models key. Tester cases through a bound tester session. A mutant without the models check fails. No instances read, changes.jsonl line or adapter argv holds a marker string from a fixture persona |
| M11 reply settings are the human's | library.limits.set from an agent session (human-required) or the tester (tester-refused). Any organization write whose replyLimits differs from the on-disk file's refuses organization-reply-limits-human-only in organizationWriter.write, on document and on written, unless the actor is the on-disk human and not an agent actor shape |
C2-06: the human's library.limits.set works. The chief of staff's organization.set, the tester's instances.set and a direct write with a changed replyLimits refuse. A direct write that changes both human, to its own actor, and replyLimits refuses. With a dotted human id equal to a role's actor, that role's write refuses. library.limits.set with an unknown key refuses invalid-request. In each refusal the file and changes.jsonl are unchanged. A mutant without the writer check fails, and so does one that compares document.human |
| M12 agents write supporting docs only | supportingPath refuses library-write-story-path on library.doc.write and library.doc.delete for every path class the ruling after r11 lists, before any git call. A write commits only by the plumbing steps, pushes fast-forward only and carries the Mosaic-Agent trailer | C2-01d: one refusal per path class against a fixture tree with a symlink, a gitlink and a case variant. C2-02: each refusal through the broker leaves the remote ref, the served ref and the mirror's refs unchanged. One write and one delete land, each with one parent, the seat's identity and the trailer. A moved tip with an unchanged blob re-applies and a changed blob refuses stale-base. A mutant without the supporting check fails |
Residual. The ruling after r11 states it. A supporting doc can hold any text, and the seat token's reach at Gitea is wider than M12. Chat has no length or kind limit now (Q223), so the 360-character residual of Q219 is gone with the caps. What lands in her story files stays hers alone. M2, M4 and M12 make an agent write there impossible through the hub.
5.2 Agents always refer to the source material
The citation guard runs in the broker. It covers every message from an agent session to human in a
business with a library project, from the runner's RESULT and from send_message alike. It doesn't look
at the sender's definition, so a misconfigured role can't skip it. Citation checks need git, so they run
before the insert, outside the SQL transaction, as #requestProject does (broker.mjs:1378-1430).
Every channel to Melanie shows stored messages only (D8), so this one check covers the Console, Discord
and any later channel.
The reply body is free text, or a JSON object when the reply carries citations:
{"text": "Mara's eyes are grey in chapter 3 but green on her character page.",
"citations": [{"path": "<book>/<chapters>/03.md", "commit": "<40 hex>", "lines": [41, 41], "quote": "her grey eyes", "as": "canon"},
{"path": "<book>/<characters>/Mara.md", "commit": "<40 hex>", "lines": [9, 9], "quote": "green eyes", "as": "canon"}]}
A body that parses as a JSON object must fit this schema: text a string, citations a list of library
cites {path, commit, lines, quote, as?} or web cites {url, quote}, and no other keys. Any other body
is a reply whose text is the whole body and which carries no citation. as is optional and its one value
is "canon", which says the agent states the cited line as fact about her world.
| Check | Refusal code |
|---|---|
| A JSON object body fits the schema | reply-invalid |
requireCitation is true and the reply carries no citation |
citation-missing |
The reply carries at most citationsPerReply citations |
citations-over-limit |
The path is canon, playground or supporting (not unmapped) and indexed, it exists at commit, commit is reachable from the served commit, the lines are in range, and quote occurs in those lines after whitespace normalization |
citation-invalid |
A cite with "as": "canon" names a canon path |
playground-cited-as-canon |
| All library cites in a reply are in one universe, or a hybrid plus its sources | citation-cross-universe |
A url cite is allowed only for research and companion, only for a URL this session fetched through the broker, and its quote must occur in the kept text of that fetch |
citation-unfetched |
The guard doesn't read what the text says, only its citations (Q223). The check result stored with the
message names each library cite's scope and whether the file's last commit at commit carries a
Mosaic-Agent trailer, so the Console can label it agent-written (7.4).
Fetched text (B7). The web trail keeps the URL (broker.mjs:1521) and a summary of status, bytes and
redirects (packages/bus/src/web.mjs:46), not the page. Case 2 adds a page store in the broker's memory,
keyed by session: the text of the last 20 broker fetches, each capped at 200 KB, dropped when the session
ends. It doesn't survive a bus restart. A cite made before a restart then fails citation-unfetched, the
runner retries, and the agent fetches again. Fetches through the harness's own web tools never reach the
broker, so webPlan (bundle.mjs:28-35) refuses the harness-builtin source for a creative definition
(web-source-not-allowed).
Runner texts. The runner's two fixed texts at runner.mjs:572 (aca16404c) are plain text, which the
guard accepts while requireCitation is false. In a library business the runner sends (the session gave no answer) and (the turn failed), with the reason only in the run log, and the guard accepts
exactly those two bodies, and the two retry texts below, when requireCitation is true. An agent that
sends one of them gains nothing it couldn't say anyway.
Runner retry (C2-06, which takes in C2-07). When message.send refuses with a code from the table, the
runner starts one new turn in the same session with the code and reason. If that reply also refuses, it
sends (no answer passed the citation check). Today a refused send is logged and message.read is
skipped, so she gets no answer (runner.mjs:577-583 at aca16404c). A send that refuses
request-too-large at the client (packages/bus/src/client.mjs:19-20), before the broker, gets the same
one retry, and the new turn names the 64 KiB limit. If that reply is still too long, the runner sends
(the answer was too long to send). If it refuses with a code from the table, the runner sends the
citation text. Only one retry runs per answer (webby-0151 R1).
The unslop note and the clip. The runner appends unslop.note() to every RESULT body, and clip cuts a
body over 32,000 characters (runner.mjs:130-131, :572-573 at aca16404c). Either one turns a JSON body
into plain text and drops its citations, and either one breaks the fixed texts. In a library business
C2-06 changes both. A body that parses as a JSON object gets the note added to the end of its text and
is serialized again, and is never clipped. A body too long for the 64 KiB request limit gets the retry
above. A fixed text goes out with no note, and the note waits for the next
reply, as it does after a failed send. A plain body keeps today's clip and note.
Tests (C2-06): one passing and one refused case per row of the table, through both the runner's RESULT
path and send_message, and once with a non-creative sender in a library business (N8). Plain text
passes with requireCitation false and refuses with it true. The fixture is a repo the test builds in
her layout's shape, with spaces and capitals in folder names, a .canvas file, a supporting folder and
an unmapped top-level note. Each refusal must leave the messages count unchanged. A runner test with a
fake harness that answers with a bad cite and then a valid reply stores one message. One that refuses
twice stores the retry's fixed text. With requireCitation true, an empty and a failed turn store their
fixed texts. With a flagged unslop event, a cited reply is stored with its citations whole and the note
at the end of text, a fixed text is stored with no note, and the next reply carries it. A cited reply
over 32,000 characters isn't cut. A cited reply over 64 KiB, then a valid one, stores the second. Two
replies over 64 KiB store (the answer was too long to send), and with requireCitation true the guard
accepts it. With the business file unreadable, an uncited reply refuses citation-missing and
messages is unchanged.
5.3 Tool use is guarded
- Built-ins: M1 and M2. The gate is unchanged. The test proves creative policies hit it.
- Harness web tools:
webPlanrefusesharness-builtinfor creative definitions. C2-08 tests it. - Typed tools: M3 and M6. The broker re-checks every call (
tools.mjs:6-7). The lookup verbs refuse a session in a business without a library (library-not-configured) and a missinguniverse. - Messaging:
#checkMessagingonmessage.send(broker.mjs:1867) enforcessendTo: ["human"]. C2-08 tests that a creative instance sending to another instance is refused. researchweb use goes through the broker's web tools (tools.mjs:197-206), which check the grant on every call (broker.mjs:1516).- Discord: the relay has no tools of its own, and M8 refuses one that does.
- Writes: M12 and the grant check on
library.doc.writeandlibrary.doc.delete. The broker re-checks every call, so a session whose grant was revoked getslibrary-write-no-grantfrom a tool it still holds.
6. One chat with N agents and @tag addressing
D4: the chat is a project. Each creative instance holds one thread in it. Each thread still has exactly
one instance, so schema.sql:296-306 hold as written and no migration is needed. Webby checked this
against the schema in webby-0001.
C2-09a, in the slice:
#threaded(broker.mjs:290-291) and the launcher (launcher.mjs:135,:236-238) readTHREAD_DEFINITIONS. A run starts per thread when that thread has waiting messages, as the assistant's does now.thread.createtakesinstance, which must be aTHREAD_DEFINITIONSinstance. With noinstanceand exactly one such instance, today's behaviour holds (broker.mjs:2331-2334). With more than one, it still refusesthread-instance-ambiguous.- The Console's new-thread control lists the thread instances by
persona.nameor instance name and passes the one she picks. A thread with Bean and a thread with Chet sit side by side, much as her Hermes channels do today (mos-1000).
C2-09b, after the slice:
room.open {project}. Human only. It creates a thread for each thread instance in the project, named after the instance. A same-name thread that belongs to the same instance is reused. One that belongs to another instance makesroom.openrefuseroom-thread-name-takenand create nothing, because names are unique per project (schema.sql:249).room.send {project, body}. Human only. Leading@nametokens, before the first other word, pick the recipients. Names resolve through#named(broker.mjs:642-650) or instance keys. With no tag, every member gets the message. An unknown tag refusesunknown-addresseeand inserts nothing. The broker inserts one message per target thread in one transaction, asthread.senddoes (:2383-2397).room.read {project, after?}returns the merged timeline of the project's threads.- A Discord channel bound to a room uses
room.send, so @tags work in Discord too. - An agent sees only its own thread. Showing agents each other's replies is C2-16.
Tests. C2-09a: a creative instance's thread starts a run, thread.create with instance binds it, and
two thread instances with no instance still refuse. C2-09b: @continuity x makes exactly one message,
in continuity's thread. No tag makes N. @nobody x refuses with the message count unchanged. A tag after
the first word is plain text. room.open against a same-name thread on another instance refuses.
7. Obsidian, Console, Discord and sync
7.1 Obsidian
She has it working already. ~/src is her vault, the books repo is a folder in it, and obsidian-git
pushes it to git.mosaicstack.dev (mos-1000). The hub changes nothing on her machine. It commits to her
repo only an agent's supporting doc write, her revert and her own Console save, and obsidian-git pulls
each on its next pull. The setup sheet (C2-12, rewritten by C2-12b) tells her which folders the agents
read, that a folder named in playground is her playground, that a folder named in supporting is where
agents may write, and how to add a graph colour group for those paths in her own vault settings if she
wants one. The graph view and backlinks stay Obsidian's own. The sheet also says two
things she might not expect. Moving a note into a book folder makes it canon, so agents may cite it as
fact (N2). And a short link to a name that also exists in another of her repos can point somewhere else
for the agents than in Obsidian. A full-path link avoids that (N4, 1.3).
7.2 Sync (bus side)
One function, syncCommand(kind, url), builds every fetch and push. Its argv is SAFE
(git.mjs:7-9), then -c protocol.https.allow=always, -c http.followRedirects=false and
-c credential.helper=, then -C <mirror>, then the command with the URL as an argument. Its env is
gitEnv() plus LC_ALL=C and GIT_ASKPASS naming a helper script outside the mirror, which reads the
slot file. The helper answers only the exact Username and Password prompts for the configured host and
user and exits 1 on anything else (ruling after r11). The URL carries no userinfo. The token is never in the URL, the mirror's
config, argv, logs or replies (CONSTITUTION gate 13). The URL must be https://. Tests reach a local bare
repo only through a module parameter that replaces syncCommand. No business file field or bus verb can
reach that parameter.
The slice (C2-03a) is fetch only. Under the library lock, library.sync runs:
- Fetch the configured branch into
refs/remotes/library/<branch>. - If it fast-forwards from the served commit, move the served ref
refs/heads/servedto it and rebuild the index and the R-rule problems. - If it doesn't fast-forward, keep serving the old commit and record
library-diverged. A force-push to her repo would be unusual, so the bus waits for a humanlibrary.sync --accept <commit>instead of guessing. - If the fetch fails, keep serving and record the error and its time.
The sync state (served commit, problems, last fetch, last error) is a JSON file inside the mirror,
written atomically, so it survives a bus restart. The human verb library.status reads it.
library.sync runs on a 60-second timer in the bus, and before any lookup that finds the last fetch
more than 60 seconds old.
C2-03b adds the push for C2-02's commits, the one race retry, library-push-failed, and
library-remote-invalid refusals of writes (B9). It also changes whose token the helper answers with: the
chief of staff's grant token and Gitea user for the fetch, in place of slot library-gitea, the writing
seat's own grant token for its write, and slot library-human for her save and revert (ruling after
r11). The fetch falls back to slot library-gitea only while the events table holds no gitea grant
event for the chief, and nothing else ever reads that slot. There is no unpushed state.
Tests (C2-03a): syncCommand argv equals SAFE plus the fixed sequence above, with no other
protocol.* key. Its env holds LC_ALL=C and GIT_ASKPASS pointing outside the mirror. The helper
prints the user for the username prompt and the slot file's content for the password prompt, and
refuses a wrong host, a wrong port and another user. A 302 to another host fails the fetch. A search of argv, the mirror's config and the run log finds no token. Against
a bare repo: fast-forward, R-problems recorded without holding the head, library-diverged kept until
--accept, a failed fetch kept serving. A mutant that sets protocol.allow=always fails the argv test.
7.3 Console WYSIWYG (slice rows that don't gate 10-27)
- Library pages (C2-11b) show a tree of universes, books, type folders, manuscript, playground and
supporting folders, the last two each in their own section and colour. They open files in the existing
editor through
library.file. Each save is one commit throughlibrary.file.save {path, base, content}(ruling after r11, F41). A file that moved refusesstale-base, and the page keeps her text and offers a reload. - Wikilinks render as links in the editor (C2-11a). Clicking one opens the target.
- Promote (C2-19) and crossover (C2-15) buttons come later.
7.4 Console chat (C2-10b)
- The chat view renders a reply as its text, then its citations as path, lines and quote, each with a
scope badge (canon, playground, supporting or web). A cite whose file's last commit carries a
Mosaic-Agenttrailer gets an agent-written label (Mos point 3). It marks a cite "changed since" when the file's last commit is newer than the cited one. - A
library-writenotice shows a fixed line, " wrote " or " deleted ", with the commit and a Revert button that callslibrary.revert. - A status line reads
library.status: the served commit and its age,library-diverged, a fetch error, and the count of R-problems with a link to the list. renderReply(body)is one pure function inpackages/library, shared with the Discord relay. It replaces C2-01'srenderItems.
7.5 User and agent sync
- Agents read the served commit through the broker. They never have a private copy (2.1).
- Every reply cites a commit, and the Console marks stale cites.
- Her push is visible to agents within about 60 seconds.
- Agents write only supporting docs, each a commit with a notice she can revert. Her Console saves are
commits too. Obsidian sees both on its next pull. A race with her Obsidian push refuses
stale-baseand never merges (D6).
7.6 Discord relay (C2-18, after the slice, optional)
Jason's Q180 ruling takes Discord out of the slice. Hermes keeps her Discord bots running until Bean and Chet are live in Mosaic. If she later wants Bean and Chet reachable from Discord, the connector stays and its engine changes, as below.
- A new engine module,
engine-bus.mjs, next toengine-pi.mjs. Binding fieldengine.provider: "mosaic-bus", plus a map from each channel id to one thread id.prompt(text)callsthread.sendon the bound thread with her message as the body. It then waits for the next message in that thread from the instance tohuman, up to the binding's turn timeout, and returnsrenderReply(body)astext. The connector's loop, journal, delivery and authorization stay as they are (connector.mjs:381-387). - The relay posts only bodies the broker stored, and those passed the guard (D8). It never sees a model's output. A timeout posts the fixed line "No answer yet. It will appear here when it arrives", and a later poll posts the stored reply.
- A rendered reply can pass Discord's 2,000-character limit, since replies have no length cap now. The relay splits at paragraph boundaries, and a paragraph over the limit at line and then character boundaries, each part with its own nonce.
- On each change of
library.status, the relay posts one line to her channels: diverged, a fetch error, a new R-problem count, or "in sync again". - M8 refuses any other engine on a library business, and a
mosaic-busbinding that setstools. - It is a new Discord application and bot. It never uses Hermes's two bots or their token slots.
- Human authority.
thread.sendis human only, and the broker accepts the human through the human CLI's path-bound proof today. A Discord message from a listed user becoming human input is a new way through a security boundary. It needs its own Jason ruling before C2-18 is built.
Tests (C2-18), against a fake bus and fake REST, with no Discord server:
- A posted Discord message equals
renderReplyof the stored body, byte for byte. - A refused agent reply (nothing stored) makes the relay post nothing but the fixed notice the runner then stores.
- A pi-engine binding and a relay binding with
toolsboth refuse on a library business. - A long reply splits at paragraph boundaries, and no part goes over 2,000 characters.
- Each
library.statuschange posts exactly one line.
8. Row list
Owners follow Q177 and Q178. Opus 5.5 takes hard design and builds. Haiku 5.5 at high effort takes mid
work as Sage-dispatched subagents (lead decision 129). Codex gpt-6.1-sol (Rocko) takes parallel builds.
gpt-6-luna takes low-level work. An independent reviewer (Filbert, Dewey or Webby) reviews every row.
Each gate is its package's changed test files run singly plus the full gate before landing. No test may
touch ~/.mosaic-dev, ~/.config/mosaic-dev, live units, slot files, her repo or any Discord server.
Chains follow Q177 merge-chaining. S marks a slice row. C2-11a and C2-11b are slice rows that don't gate
10-27 (Q195).
| Row | S | Scope | After | Owner | Test gate |
|---|---|---|---|---|---|
| C2-01 | S | packages/library: mapping schema, scope rule, front matter reader, link resolver (vaultPrefix, relative, shortest path), R1-R7 reporting, renderItems, and library survey |
none | Opus (Gosalyn) | one case per R-rule and per resolver step, a link into another repo unresolved, appears_in wikilinks indexed. renderItems snapshot per kind and notice. The survey prints names, counts, key sets and link-form counts, and a marker string in a fixture body never appears in its output |
| C2-01c | S | Playgrounds at three levels (ruling after r11, mos-1233): kind values, draft entries, the prefix refusals, classify checking prefixes first, book: null |
C2-01 | Opus | one test per level and per new refusal, including a prefix that holds its own book and one that holds another entry's book, and the readers named in the ruling given a draft and a null book. mapping.test.mjs:16-20 moves to the new codes |
| C2-04 | S | Business file kind: "library" entry, skips at each consumer in 2.1, M9, mirror creation at the projectRepo path, human verbs library.file (read) and library.status |
C2-01 | Sol (Herb, sage-1482) | business file tests. One test per consumer skip. A launcher test that the workspace has no clone. M9 test. Each verb from an agent session refuses (M4) |
| C2-03a | S | Fetch-only sync (7.2): syncCommand, askpass helper, served ref, library-diverged, --accept, sync state file, timer, library.sync |
C2-04 | Opus | the C2-03a tests in 7.2 |
| C2-05a | S | Lookup index and queries: find, get, search, manuscript with scope, universe, isolation | C2-01 | Haiku high | isolation, scope labels on every tool, unmapped files never returned, paging under REPLY_CAP, rebuild time on a 3,000-file fixture |
| C2-01d | S | supporting lists and their prefix rules, library-supporting-other-universe, the supporting scope ahead of playground and canon, the editor field, and supportingPath (M12) |
C2-01c | Opus | one test per prefix rule and refusal, a folder in both lists, scope order, every M12 path class, editor refused when not two non-empty strings |
| C2-05b | S | Broker lookup verbs and typed tools. Sync before a read older than 60 s. Row 224, built to the earlier brief and landing as built (sage-2833) | C2-05a, C2-03a | Sol (Rocko) | broker verb tests. Typed tool name snapshot per creative role without the write tools (M3) |
| C2-05c | S | scope with supporting in the lookup verbs and tools, blob in library_get (sage-2833) |
C2-05b, C2-01d | Sage picks | a supporting lookup in find, search and get, canon by default leaves supporting files out, blob equals the served blob id, the M3 snapshot unchanged |
| C2-06 | S | Citation guard (5.2) in packages/library, the broker's pre-check on agent-to-human message.send, the page store, replyLimits with requireCitation and citationsPerReply and M11 (Q222, Q226), the runner texts, and the runner retry that was C2-07 |
C2-05a, C2-04 | Opus | one pass and one refusal per check in M5, M11 and 5.2, through the RESULT path and send_message, and with a non-creative sender. messages count unchanged on each refusal. The runner tests in 5.2 |
| C2-07 | Folded into C2-06 (ruling after r11) | ||||
| C2-08 | S | Six role files and contracts, the new constants, M1, M6, the webPlan refusal, gate tests (M2), messaging test. persona.name and persona.sha256 in REGISTRY, the persona file read and checks, and the fenced persona block between the contract and "## This session" in prompt(). M10: the organization.set refusals, the models check, the routes.set and instances.set creative branches (4). Both adapters pass the prompt as a file, not its text (4) |
none | Opus | role tests, gate tests, a creative raise refused, harness-builtin refused for research and companion, scripts/test-config.sh. Persona vars refused at other layers, malformed and on a non-creative definition. The persona file refused when missing, a symlink, another uid's, group or other readable, over 8,000 characters, off its digest, holding </persona-file> or a # /## line. A prompt snapshot pinning contract, persona, session. The M10 tests, tester cases through a bound tester session, the models mutant. Adapter tests: argv holds the prompt path and no marker from the prompt, and pi's adapter refuses a missing and an unreadable prompt file. M10's actor case: the human's limits.set change to Bean's launch.models key works next to the chief's refusal. Business file, .history and changes.jsonl modes after a write |
| C2-09a | S | #threaded and launcher read THREAD_DEFINITIONS. thread.create takes instance. The Console new-thread control picks it |
C2-08 | Sol (Rocko) | the C2-09a tests in section 6 |
| C2-10a | S | M7 in the conversation reader. M8 in discord check and discord run |
C2-08 | Haiku high | the M7 reader test, the M8 tests |
| C2-10b | S | Console chat: the reply renderer through renderReply, which replaces renderItems, the agent-written label, the notice line with Revert, the status line |
C2-06, C2-09a, C2-03a, C2-02 | Haiku high | webui tests for text and citations, scope badges, the agent-written label, the notice line and its Revert call, "changed since" and the status line |
| C2-12 | S | The setup sheet for Melanie (7.1, including N2 and N4, and what changes for Bean and Chet in 4) and the survey run sheet for <host> |
C2-01 | Luna | doc only, reviewed against the survey's real flags |
| C2-12b | S | Setup sheet rewrite for Q223 and Q226: free-text chat, supporting folders and how to mark one, agent writes as commits with notices and Revert, her Console saves, the editor name and email, the settings that replace the caps, and the residual |
C2-12 | Luna | doc only, reviewed against the ruling after r11 |
| C2-20 | S | Persona import (9.1): a script that reads only the persona definitions of the two Hermes profiles, confirms profile to name, and writes each persona file (4) with its persona.name and persona.sha256 for the business file. Prints names, byte counts and sha256 only. A --dry-run mode, run before Jason's go, prints only counts of files, lines and lines starting # or ## |
C2-08 | Opus (Gosalyn), run under Jason's go | fixture Hermes profile trees: the right fields are taken, memory and session files are never opened (a read trace on the fixture), a profile with no persona field refuses, a persona that would fail C2-08's fence check refuses, output holds no persona text, each written file is 0600 and its digest matches, --dry-run writes nothing and prints the right counts for a fixture with two heading lines and no marker string from it |
| C2-13a | S | scripts/case2-unit.sh, which renders the bus and Console system units from their existing templates and which root runs from a root-owned checkout in 10 step 2c, and scripts/case2-precheck.sh with the host checks. No library dependency, so row 195 chains on it (sage-1480) |
none | Opus (Gosalyn) | the C2-13a brief's render and precheck cases: token rules, @PATH@ from --node, the four hardening lines, the Console's capability lines and privileged port, one WantedBy=multi-user.target on each unit (Q189), copy-id pairing, the serve flag set, the key file and the unprivileged port start. Scratch unit directory and scratch data root |
| C2-13b | S | Install script for <host> as <user>: bootstrap, the stack copy, business file with the mapping, bean and chet with their persona vars and files, any specialists she picks, her human id, token slot path. The install writes no unit. Adds the library checks to the precheck. Jason or his delegate runs it (10) |
C2-13a, C2-04, C2-03a, C2-20 | Opus (Gosalyn), Jason's go | precheck library checks: the business file, .history, changes.jsonl and persona files have the modes in 4 and each persona digest matches, no Discord binding exists, and after step 4 --library-status reads the bus's recorded sync state and lists R-problems (10 step 4a). Tested against a scratch data root and a local bare repo |
| C2-08b | S | Role changes, authorized in mos-1309 item 5: library.write in the creative authority and in checkCreative, the gitea credential on the six creative definitions and the chief of staff, instance identity and publish, CREATIVE_KINDS removed |
C2-08 | Opus | role tests: a creative role with library.write validates, one with any other added action refuses, the chief holds the credential and not library.write. scripts/test-config.sh |
| C2-21 | S | Grant executor for kind: "library" (ruling after r11, F40): the creative raise check narrowed to library projects (mos-1163), collaborator in step 4, the level check in step 5, the rendered block, revoke. The install enrolls the chief of staff |
C2-08b, C2-13b | Opus | first, the two Gitea facts proven on a scratch Gitea 1.27.1. Then executor tests: write for a creative definition, read for the chief and any other, a wider permission or admin refuses gitea-reach-wider, revoke deletes the token, a non-library project still forks, a creative raise naming a non-library project refuses gitea-not-declared |
| C2-03b | S | Push and credentials (7.2): the push form of syncCommand, the race retry once, library-push-failed, library-remote-invalid on writes, the helper answering with a seat's grant token, the chief's for the fetch, and slot library-human |
C2-03a | Opus | push tests against a bare repo. The helper refuses another seat's user, and no token reaches argv, config or a log |
| C2-02 | S | Writes (ruling after r11): library.doc.write, library.doc.delete, their typed tools, the plumbing commit and trailer, the notice, library.revert, the trailer label in the check result, and library.file.save |
C2-01d, C2-03b, C2-04, C2-05c, C2-08b | Opus | the M12 broker tests, write, delete and revert against a bare repo, the grant refusals, the notice body, her save's author, the M3 snapshot with the write tools |
| C2-14 | S | End-to-end acceptance on a scratch data root and a bare repo in her layout: sync, ask a creative agent in its thread, a reply whose cites verify, a playground cite as canon refused, a force-push held as diverged, an agent's supporting doc write and its notice, a story path write refused, a revert, her Console save as one commit, the Console shows the reply | slice rows | Opus | one script, which the reviewer also runs |
| C2-19 | library.promote, multi-path commit |
C2-02, C2-03b | Sol (Rocko) | promote in one commit, human only | |
| C2-09b | room.open, room.send with @tags, room.read |
C2-09a | Sol (Rocko) | the C2-09b tests in section 6 | |
| C2-18 | Discord relay (7.6), optional, after its own Jason ruling | C2-09a, C2-06, C2-03a | Opus | the C2-18 tests in 7.6 | |
| C2-10c | Console room view: merged timeline, @name completion | C2-09b, C2-10b | Haiku high | webui tests, @tag parse parity with the broker | |
| C2-11a | S | Editor wikilink node. First measure whether [[ round-trips today |
none | Sol (Rocko) | roundTrip tests: plain, labelled, heading, in a list, next to front matter |
| C2-11b | S | Console library pages: tree with playground and supporting sections, open, one commit per save through library.file.save, stale-base keeps her text |
C2-02, C2-11a | Haiku high | webui tests: a save makes one commit with her editor as author, a moved file shows stale-base and keeps her text, an unmapped path offers no save |
| C2-15 | Hybrid universes in the business file, exclude and pin |
C2-05a | later | ||
| C2-16 | Agents see each other's replies in a room | C2-09b | later | ||
| C2-17 | Dropped: there is no unpushed state (ruling after r11) |
Parallel waves:
- Landed: C2-01, C2-01c, C2-03a, C2-04, C2-08, C2-12, C2-13a, C2-20. C2-13b is in progress.
- Next, side by side: C2-01d, C2-03b, C2-05a, C2-08b, C2-09a, C2-10a, C2-11a, C2-12b.
- Then: C2-05b (row 224, built), C2-05c, C2-06, C2-21 and C2-02. Then C2-10b and C2-11b.
- Last: C2-14. Rooms, the relay and promotion follow when a seat is free.
Dependencies on other tracks: row 190 doesn't apply, since nothing else runs as <user>. SSO (Q175,
#412) gates the Console from her machine, and the Console is now her channel, so the slice needs SSO
landed or Jason's ruling on an interim sign-in (10 step 5). T1 row 7 owns
compaction count and clear context (GAP-MAP C2.12, C2.13). Until then, a new thread is a fresh context
(session.mjs:59-64).
9. Where it runs, Bean and Chet's move, and what can't be ready by 2026-10-27
Where it runs (Jason, Q181, relayed in mos-1031). <host> is her VM 10.1.1.54, W-Melanie-OC, VM 1012
on pve. <user> is mosaic, a system user with no sudo, no supplementary groups and no keys, home
/srv/mosaic at mode 0700. It runs as two system units, mosaic-bus@<business>.service and mosaic-webui.service, both with ProtectHome, so nothing it needs may
live under /home. Its config directory, <configDir>, is /srv/mosaic/.config/mosaic-dev, and the token
slot and persona files live under it. ops-01's plan, section 3, set this layout. That plan stopped
with #421, and Q183 keeps a mosaic user without sudo on each VM. Section 10 step 2a creates it. The rest of the design
still names <host> and <user>, so a later move changes only the install inputs. The bus, data root,
mirror and runners live under <user>. It needs outbound HTTPS to git.mosaicstack.dev and the model
providers. It needs nothing from her user, because her writing reaches it through the Gitea remote she
already uses.
Model path (Q186, mos-1090). For 10-27, bean and chet both run harness pi with provider zai on
glm-5.3, through one key for the hub (10 step 2d). Pi's provider comes from the instance's session
(packages/plugins/harness/pi/index.mjs:57), and pi-ai reads a zai key from ZAI_API_KEY
(env-api-keys.js:89, with glm-5.3 in dist/providers/data/zai.json). ENV_ALLOW doesn't pass that name today
(packages/seat/src/session.mjs:83-88), so row A adds it. A pi launch has no credential check
(pi/index.mjs:42, packages/cli/src/launcher.mjs:299), so a missing key shows up only at the first
turn. The C2-13 precheck catches that before the start. The creative roles' web search goes through the
broker's search source, not the harness, so pi having no web tool of its own (pi/index.mjs:44-45)
changes nothing here (5.2). After 10-27 Chet moves to his own ChatGPT/Codex login. That needs a
per-instance pi agent dir, which is row B (Vikunja #428), and it starts after a live check that two
clients on one login don't revoke each other's refresh tokens. Running any pi role waits on #383.
Memory. From mos-1000, her two Hermes services use about 1.8 GB together, and each Mosaic agent session measured 300 to 390 MB on dragon-lin. I estimated at least 2 GB more than the 3.8 GiB her guest saw, and 4 GB more with specialists running at the same time. ops-01 is setting VM 1012 to 16 GiB fixed with 6 cores at 2026-10-21T09:00Z (mos-1031, mos-1038), which covers both. C2-13's precheck still prints free memory before the start.
Isolation on that host. <user> without sudo keeps the hub from touching her files. The reverse is the
B7 residual from r2, and it has two parts.
- The factory's paths to .54: pve root, ssh as
melanie, and the root her passwordless sudo gives through those paths (mos-1000, jason-master-restartprojects/mosaic-mvp/MELANIE-HOST-RECON.md:27-39). Jason accepted them (Q182, Q183), and FED R8 records his acceptance instead of refusing to start. - The Hermes processes. They run as
melanieand could use her sudo to read/srv/mosaic. Jason's acceptance doesn't cover this part. It is Mos's residual (mos-1064). Q181 keeps Hermes running, and it ends when #423 step 5 removes her sudo once Bean and Chet are live.
The in-VM tightening is Vikunja #423. Only its step 1, the localadmin account, gates the install
(10 step 2).
9.1 Moving Bean and Chet (C2-20)
- What the row reads: the persona definition of each of the two Hermes profiles, default and chet, and
nothing else. Not the memory store (188 facts), not
state.dbor any session or message, not the four per-channel prompts, not the bot token slots. It lists the per-channel prompts by name and size only, so Jason can say whether they belong to the personality. - Profile to name. I expect default to be Bean and chet to be Chet. The row confirms it from each profile's own configured name, and prints the pairing for Jason before anything is written. If a profile names neither, it stops and asks.
- How it reads. A script, run on
<host>by whoever Jason names, because the persona files sit in her home directory and<user>can't read them. It writes each persona text to a 0600 staging file, and a second step run as<user>installs it as<configDir>/personas/<business>/<instance>.md(4). It printspersona.nameandpersona.sha256for the business file entries ofbeanandchet, and otherwise names, byte counts and sha256 only. The text never enters the repository, the business file orchanges.jsonl. Outside the persona file, it sits only in Bean's or Chet's own run bundles, 0600 until prune, and in the harness argv if the host's claude lacks--system-prompt-file(4). - Dry run first (N2, mos-1051 item 3). Before Jason's go to import, the row runs a dry pass that reads the
same two persona definitions and prints counts only: files, lines, and lines starting
#or##. No text and no line numbers. It also counts lines that name a Hermes tool or channel, from a fixed list in the script: the tool names in the hermes-agent checkout at df5700ebe, read from its code and not her profiles, plusHermesandDiscord. That prints one count per persona file, never the line, its number or the word. A nonzero count changes nothing in the import. It tells Jason a persona refers to things Bean and Chet won't have in Mosaic. If any count of heading lines is nonzero, the fence rule in 4 would refuse a real persona, so the rule changes, in a reviewed C2-08 follow-up, before the import runs. The text is never rewritten to fit the rule unless Melanie says so through Jason. - Hermes keeps running. The row changes nothing in the Hermes checkout, its profiles, its services or its Discord bots. Bean and Chet in Mosaic are new agents with the same persona, not a move of the Hermes ones. She uses both until she is happy, and stopping Hermes is her choice through Jason, after Bean and Chet are live (10 step 9).
- Memory and history stay in Hermes unless she chooses otherwise through Jason. Bean and Chet in Mosaic start without her 188 facts. Their knowledge of her work comes from the lookup tools.
9.2 The slice and the date
The smallest slice that lets her start is the S rows: C2-01, C2-01c, C2-01d, C2-02, C2-03a, C2-03b, C2-04, C2-05a, C2-05b, C2-05c, C2-06, C2-08, C2-08b, C2-09a, C2-10a, C2-10b, C2-12, C2-12b, C2-13a, C2-13b, C2-14, C2-20 and C2-21. C2-11a and C2-11b are slice rows too, and they don't gate 10-27 (Q195). With it she keeps writing in Obsidian exactly as now. In the Console she opens a thread with Bean, Chet or a specialist and reads each answer as free text whose citations the broker checked. Each agent write shows as a notice she can revert. A status line tells her when the hub falls behind her repo. Hermes keeps running beside it.
Not ready by 10-27: promotion (C2-19), rooms with @tags (C2-09b, C2-10c), the Discord relay (C2-18), C2-15 and C2-16. The editor may land after 10-27 without holding it. The slice makes 10-27 only if section 10 steps 1-7 are done by about 10-24. The Console from her machine needs SSO (#412) or an interim sign-in ruling, and that is the step most likely to be late.
9.3 The Console's forgery limit on .54
Mos asked this in mos-1097 item 2, for Jason. Decision 95 accepted the local-forgery limit only on dragon-lin under Jason's user, so on .54 it is Jason's call. Everything below comes from the code and this design. Nobody has looked at the live host.
Who runs as mosaic, the Console's uid.
- The bus host,
mosaic-bus@<business>.service, and everything it starts. - The Console,
mosaic-webui.service, and thehuman-cli.mjschildren it starts for each bus call (packages/webui/src/bus.mjs:2). - Bean's and Chet's sessions, and any specialist's. The bus starts each one inside bwrap, and the
runner, the harness and every shell the harness opens run inside it (
packages/seat/src/sandbox.mjs:1-4). Inside, the session is stillmosaic(packages/harness/README.md, "Limits", "Same UID"). - The admin steps Jason or his delegate runs as
sudo -u mosaic -Hfromlocaladmin: the install, the precheck,library surveyand the C2-20 import (10 steps 2b, 6 and 7). Nothing else.mosaichas no login shell, no key and no sudo (10 step 2a).
What a session can reach from inside bwrap.
- The Console's port, yes. bwrap doesn't confine the network. A session shares the host's network
namespace and reaches every service on
127.0.0.1and every abstract Unix socket (packages/harness/README.md, "Limits", "Network and abstract sockets aren't confined"). - The proof file, no. The sandbox holds only the binds
sandbox.mjs:6-22lists, and C2-13a requires the proof file under<home>/.config/mosaic-dev/secrets/, which is not one of them. The session's/procis private and in its own PID namespace (sandbox.mjs:16,:67), so it can't read the Console's memory or environment either. - The bus's human socket, no. The sandbox binds the broker socket, the launch socket and the git socket
only (
sandbox.mjs:97-108). The bus and Console code opens no abstract socket (a search ofpackages/bus/src,packages/webui/srcandpackages/cli/srcfor one finds none).
What reaching the port gives a session. Every write in preview mode needs the public Host, and the proof
check comes before every auth-header read (packages/webui/src/serve.mjs:495-503). A request with the
public Host and a missing or wrong proof gets 401. A request with a loopback Host is read-only, since
every write route requires named (serve.mjs:532-546), and the proof header with a loopback Host gets
403. So with the proof on and no tester listener, a session that reaches the port can read what the
Console shows on its loopback path and can't send as Melanie. Without the proof it could, because it can
set the public Host and the auth header itself. That is why C2-13a makes the proof pair required and
refuses --tester-port.
A gap I found while checking this: a port squat. When the Console isn't running, a session can bind
its port, if the port is 1024 or above. The proxy then forwards Melanie's browser to the session's
listener, with her sign-in header and the proof header on every request. The session gets the proof
value, and from then on it can send as her to the real Console. It can also show her a fake Console.
The Console is down after a crash for RestartSec=15, during each re-pin, and for a moment at boot
before its unit starts. Q189 starts it at boot, so a reboot no longer leaves it down until a human
starts it, as r11's B1 did. Bean and Chet run model-driven shells, so I treat it as reachable. The same reach exists wherever
sessions share a host and a network namespace with a Console on a port above 1023. I haven't looked at
the live install and make no claim about it. The fix in C2-13a is a Console port below 1024. Only a
process with CAP_NET_BIND_SERVICE in the host's network namespace can bind it. The Console unit gets
that capability through AmbientCapabilities= and CapabilityBoundingSet=. A session holds
capabilities only in its own user namespace, which doesn't own the host's network namespace, so it can't
bind the port. The precheck checks that net.ipv4.ip_unprivileged_port_start is above the port. The
Console's own --port check takes 0..65535 (packages/webui/src/cli.mjs:27), so serve needs no change.
Step 5's forward must point at that port. The default board origin, 127.0.0.1:7331, has a smaller
version of the same gap. Case 2 runs no control board, so a session could listen there and the Console
would show its answers in the board panel. That is content on her screen, not a send as her. I left it
as a named residual and didn't add a flag for it.
The narrowest form by 10-27, with C2-13a's flags (proof on, no tester listener, port below 1024): a send as Melanie needs the proof value. These can read the proof file:
mosaicoutside bwrap: the bus host, the Console and the admin steps above. All run reviewed code from the copy, or a command Jason's go names. No model-driven process runs there.- root on .54. That is
localadmin, the factory's paths through pve (Jason accepted those in Q182 and Q183), andmelaniethrough her passwordless sudo. Her Hermes processes could use that sudo, as section 9 says for the key file. Bean's and Chet's sessions reach the port and get 401 or 403. Melanie's own uid without sudo can't read/srv/mosaic(mode 0700).
What closes the rest.
- #423 step 5 removes
melanie's sudo once Bean and Chet are live. That ends the Hermes part. - A separate Console uid (#385) splits the Console from the bus, so the bus host no longer runs as the uid that owns the proof.
- The containerized install puts the Console in its own container (I4) and the sessions in theirs (I5).
Then no session reaches the Console at all (
docs/plans/2026-10-18_containerized-install-design.md:584-586), and the squat goes with it. That design says it doesn't close the limit for a deliberate process on the host (:587-588), so root andmosaicoutside a container stay. - Root on .54 stays after all of these. That part is Jason's acceptance, as for the key file.
10. Jason-gated steps for C2-13a and C2-13b
The Gitea host. Her books repo is already on git.mosaicstack.dev (mos-1000). On this host that name
resolves to 10.1.1.222, and STATE.md lists its certificate among the edge certificates, so I treat it as
reachable from the internet. Her writing is already there by her own choice. The hub adds no new remote.
It adds no shared bot account. Each writing seat gets its own Gitea account through the grant path, a
collaborator on that one repo with write, and the chief of staff one with read (ruling after r11).
Her own token signs her Console saves.
- The host is set (Q181): 10.1.1.54 as
mosaicunder/srv/mosaic, 16 GiB and 6 cores. Jason confirmed Q4 (Q183), so #421 is closed and the install doesn't wait on it.- Root steps on
<host>create themosaicuser and its home, under step 2's go (2a). C2-13's precheck refuses if<user>has sudo or any supplementary group. - FED R8 records Jason's acceptance of the factory's paths to .54. Gizmoduck's r4.10 of the federation design writes that wording, and it is in progress.
- The in-VM tightening on .54 is Vikunja #423. Only its step 1, which creates
localadmin, gates the install, through step 2. Its step 5 removesmelanie's sudo once Bean and Chet are live, and that ends Mos's residual (9).
- Root steps on
- The install on
<host>. Jason's go covers all three parts. 2a and 2c are root steps, owned by ops-01 (mos-1064). The path is ssh as[email protected]with NOPASSWD sudo, which #423 step 1 creates. Neither usesmelanie's sudo or pve root. They are acts on Melanie's machine, so ops-01 doesn't run them without the go.- 2a, ops-01 via
localadmin:useraddmosaicas a system user with no sudo, no supplementary groups and no keys. Create/srv/mosaicat 0700, owned bymosaic. - 2b: the install as
<user>, by him or his delegate, run assudo -u mosaic -Hfromlocaladmin.mosaichas no keys and no login shell, and nobody adds a key to make 2b work.scripts/case2-install.shrunsscripts/bootstrap.shand makes the stack copy, whosenpm ciinstalls the pinned harness. It runs norelease.sh packageoractivate: both need Docker, and the hub runs no containers (webby-0085 B1, sage-2189). The model path is set (9, Q186). Chet's ChatGPT/Codex login is not part of 10-27. It is row B (#428). - 2c, ops-01 via
localadmin: root makes a checkout of the reviewed commit outside/srv/mosaic, owned by root, and checks that its HEAD is the commit Jason's go names. From that checkout it runsscripts/case2-unit.sh, which renders two system units from the checkout's existing templates:[email protected]frompackages/cli/systemd/mosaic-bus.service.in, enabled asmosaic-bus@<business>.service, andmosaic-webui.servicefrompackages/webui/systemd/mosaic-webui.service.in. Inputs:--user,--home,--business,--copy,--nodeand the Console's serve flags. In both,@REPO@is the copy<home>/.local/share/mosaic-stack/copies/<id>.%hbecomes the literal home, because a system unit's%his root's home.@PATH@starts with the directory of--node. The render addsUser=,Group=and four hardening lines, and the Console unit also getsCAP_NET_BIND_SERVICEfor its port below 1024 (9.3). Each unit'sWantedBy=becomesmulti-user.target, so both start at boot (Q189). The C2-13a brief lists every line and rule. It checksUser=,Group=,ExecStart=, the bus unit'sEnvironmentFile=and the hardening lines against fixed values, and the copy id is the one part that may change between renders. It writes both under/etc/systemd/system/asroot:root0644. It never usessystemctl link, and it refuses a symlink at either unit path. On a re-install it refuses an installed unit that differs from the render. Root runs nothing from/srv/mosaicand reads no unit thatmosaicwrote. Then it enables both units and starts the bus and then the Console. They stop in the reverse order. - 2d, Jason: the hub's own key on the existing Z.ai account, with a spend cap if the account offers
one. Model use is spend, so the key and the cap are his. Whose Z.ai account it is hasn't been said,
and that is his input. The key goes in
/srv/mosaic/.config/mosaic-dev/secrets/claude-session.envasZAI_API_KEY=, owned bymosaic, mode 0600, the file the bus unit'sEnvironmentFile=line loads (the same line aspackages/cli/systemd/mosaic-bus.service.in:24). The name says claude, but it is the one file decision 119 andpackages/cli/README.mddocument, so I kept it. He places it, or says who may, per the Q173 secrets rule. Agents never print it. Until #423 step 5,melanie's sudo can read it, and so can her Hermes processes (9).
- 2a, ops-01 via
- No bot account. The install enrolls the chief of staff, which raises the gitea grants for
booksthrough the existing step, and the executor makes each seat a collaborator on her repo (C2-21). The seats reach the books repo only, not bean-brain and not her other three repos. Whether each grant also needs his Approve is Q31, asked of him, and this design adds no step for it. The collaborators change her repo's access list, which is hers, so step 7 tells her. - Her own Gitea token for her Console saves, in slot
library-humanunder<configDir>/secrets/<business>/, which is/srv/mosaic/.config/mosaic-dev/secrets/<business>/formosaic, mode 0600. He puts it there, or says who may, per the Q173 secrets rule. Agents never print it. If the install runs before C2-03b lands, the C2-03a fetch still reads slotlibrary-gitea, which then needs a read token as r11 said. After C2-03b the fetch reads that slot only while the bus has no gitea grant event for the chief (7.2). Step 4b removes it. 4a.scripts/case2-precheck.sh --business <business> --library-statusas<user>, once the bus is up and the fetch token is in place. It reads the bus's recorded sync state only and syncs nothing. It refuses when the mirror is missing, whenlastErroris set, or whenlastFetchis null. It prints each R-problem as aproblemline and then a count, never text (sage-2189). The bus syncs every 60 seconds, so the first check after step 4 may have to wait for one sync. 4b. Once C2-21 has run andlibrary.statusshows the fetch on the chief's grant, Jason deletes slotlibrary-giteaand revokes its token in Gitea. The fetch never reads the slot again, since a gitea grant event for the chief then exists and events are never deleted (webby-0151 R3, webby-0156 R4). - Her Console sign-in from her machine: her Authentik user and
subonce SSO lands (#412 step 3 is the model), or his ruling on an interim. The proxy forwards to the Console's port below 1024 and sets the proof header. The proof file goes under<configDir>/secrets/, mode 0600, owned bymosaic, and agents never print it (9.3). He places it, or says who may, per the Q173 secrets rule. Whether he accepts the forgery limit on .54 in the form 9.3 names is his decision, and Mos takes it to him. - The C2-20 persona import: who runs it on
<host>, his check of the printed profile-to-name pairing, and whether the per-channel prompts count as personality. - Running
library surveyon<host>after step 4, which prints names and counts, never text. Then contacting Melanie: who tells her what changes for Bean and Chet (4), sends the setup sheet (C2-12b), tells her the agents become collaborators on her repo, and asks her to confirm the mapping, the universes, the playground and supporting folders, hereditorname and email, and which specialists to start with, and when. A specialist she wants later is added by the human throughinstances.set(4), with no hand edit and no new row. - The residual in 5.1. Closed. Q219 set lower caps, and Q223 then removed the caps with the item format. What remains is the residual the ruling after r11 states, a supporting doc that holds any text, and C2-12b tells her about it.
- The instance start. Stopping Hermes comes later, and only when she chooses it.
- The bus and the Console are already running from step 2c, and both come back after a boot on their own (Q189, which reverses mos-1097 item 1). Nobody starts the Console by hand here. With a port below 1024, no session can take the port while it is down (9.3).
- One session starts under the bus unit before she is told the hub is up. The first turn of
beanreaches a running session, and its run record shows the runner started. The bwrap precheck runs inmosaic's shell, outside the unit's hardening, so it can't show this. Asandbox-unavailablerefusal here means a unit line or the host's AppArmor rule for unprivileged user namespaces stops bwrap. The fix is a reviewed re-render, never a hand edit of the installed unit. Running a pi role waits on #383, so this check does too. - .54 runs Ubuntu 24.04 (MELANIE-HOST-RECON.md:7), the release where row 195 found
kernel.apparmor_restrict_unprivileged_userns=1and no bwrap profile on .53. If the precheck's bwrap check refuses on .54, the fix is row 195's root step 2a': bubblewrap and Ubuntu'sbwrap-userns-restrictprofile from the pinnedapparmor-profilesdeb (darkwing/row195 at0075be358,agents/darkwing/work/queue-195/DESIGN.mdsection 1, mos-1091 Q2 4b). On .54 it would run under step 2's go, as a 2a item for ops-01.