The rehearsal provider matches routes on the parsed pathname, so a
query string no longer turns authorize or token into a 404. Authorize
and token answer 403 to any method before the GET check, and only
discovery and the JWKS answer 405 to another method. R20 now sends a
POST to token and a GET to authorize with a query, and expects 403 for
both.
The upstream A2 agent-levels write test stubbed an owner session
without rootOwner. M3 sets the hook's owner flag from rootOwner, as
sso.mjs grants it, so the stub now carries rootOwner: true, as
system-types-http does. No check changes.
An installed unit must equal the render but for the copy id, so a
re-pin that adds --sso refused render-refused. case2-unit gains --check
(every render check, nothing written) and --check-new (a first install's
checks). stack-upgrade runs both checks before anything stops, and
--next-args reads root's <instance>.args.next: equal before -- but for
--copy, different after it. After the stage it moves the installed unit
files into a new <instance>.replaced.XXXXXX directory and renders a
first install. A failure moves them back unchanged and restarts the old
copy, and confirm swaps the args file. The run sheet gives the commands,
the rollback past the migration included.
A stamped rehearsal now starts an isolated provider on 127.0.0.1 port 0
with a certificate and keys made for the run. It serves discovery and a
JWKS for the live provider ids and answers 403 to authorize and token.
Only the scratch Console trusts its certificate. The gate requires each
provider ready (sso-ready); provider-discovery requires discovery and
JWKS fetches and no code or token request. The provider stops with the
run, and by itself once the run's shell is gone.
The system-path test helper asserts the log's from copy again. The
pinned-copy sign-in requests are bounded, and cleanup waits for each
Console to exit.
stack-release refuses a user-unit activate, rollback or pm-on when the
instance shows any accounts sign (stamped target, accounts.json,
console.sso, .deployed), before anything stops. Root renders the system
units with case2-unit.sh, and the op runs with --units external.
case2-unit renders a stamped copy's units only with --sso, console.sso,
an accounts.json that loads and a valid .deployed, and names each
missing step in run-sheet order before any write. Per Jason's ruling of
2026-11-10, a stamped library unit takes --sso and refuses proxy-preview
admission and its proof. The unstamped and dev paths are unchanged.
rehearse runs a stamped copy against rehearsal.invalid with scratch
slot placeholders, a copy of the live accounts.json and the copy's own
.deployed. It refuses sso-not-prepared and accounts-not-initialized
before any rehearsal directory exists, and sso-isolated before the host
starts.
The tests that turned SSO on through the user unit now pin the refusal
and reach the same end state through the system path. The copy tests
start the Console on real scratch prerequisites and pin each refusal.
The run sheet, TOOLS.md and the accounts design record the ruling and
the migration order.
Review fix round for slice A3.
- Every project's union is recomputed from the current organization file
at each load. A refused union refuses the project's own copies and every
inherited root or org agent there: no mount at launch (checkout-skipped,
project-refused), no thread and no room, each naming the project and why.
- A project exclude removes that project's mount from the excluded agent;
a narrow reaches the instance's resolved limits, so its real bundle is
narrowed. A narrow in any project narrows the instance for every launch,
since one launch mounts every project it works in.
- Two copies whose instance names collide both refuse, naming each
claimant; the view lists the claimants. An organization instance of the
same name replaces a root or project copy, at boot and after it, and the
copy is gone rather than refused.
- Rooms hold only the thread instances that work in the room's project.
- The grid finds an agent's copy by name and project from the bus view,
never by rebuilding the instance name.
- Real-boot regressions: inherited agents in a refused union, exclude and
narrow in the bundle, an org override after boot, two organizations each
running a copy, a root file change while running, and a grid fed by a
booted bus.
Each root agent an organization does not exclude, and each agent a project
defines, becomes a copy when the organization's bus boots. The root file is
read once, through loadRootAgents, at the cli boot step; the snapshot is fixed
until the next boot. A bound copy joins the organization document's roles and
goes through validateBusinessDocument and checkRoles with the organization's
own credential map. A copy whose role needs a credential service is unbound
and refuses at launch by name and service (credential-unbound); a copy the
organization refuses is copy-refused; every agent in a project whose union
refuses is project-refused, naming the project. A project agent launches
only with its own project's mounts and takes a thread only in its project
(project-agent-elsewhere). Refusal events carry the code alone. The bus's
instances read carries the copies with their running runs, and the Agents
grid shows each copy's real status and reason.
channel-proof re-pins webui bus.mjs for four STATUS entries; no proof or
transport change. packages/business gets the agent-copies.mjs re-export.
The CLI agent verbs validate the instance config through loadSystem before
any read or write, and print a fixed line for errors they do not own. The
root writer refuses a config directory with an ancestor another user can
write without the sticky bit. The project HTTP route checks agent names
with the organization ID_PATTERN and answers 400 naming the key.
The Settings level forms take their own class, so the role-instance
handlers no longer catch them, and keep both drafts in module state with
the revision each began at. The organization writer tests now reach the
file's own uid check and test the assistant protection with an assistant
in the fixture.
Root writer for <configDir>/agents.json (owner only, revision-checked, atomic, A1-validated,
receipts name agents and keys only), the project.agents writer verb on the organization
writer, `mosaic agents root` and `mosaic project agents` verbs, and owner-only Settings >
Agents edit forms on GET/POST hooks gated like /api/agents/levels.
The mark from the previous commit kept the wait off the old page, but
the wait could still start on it and be killed by the reload
("Inspected target navigated or closed", once in ten runs). navigate()
waits for the load; a query makes the same URL load again.
Review fix. dialog.close() clears `open` at once but queues the close
event that empties the dialog. A conflict read that resumed in between
still found the old form connected and reopened the dialog, and the
queued event then emptied it: an open modal with nothing in it. The
check is now the dialog's own `open` state and that it still holds the
form. A browser test closes the dialog and releases the read's body in
one task; it fails on the previous commit.
The wait after location.reload() could pass on the board that was
unloading, and the next read then found no board (seen once in ten
runs under load). A mark set before the reload goes with the old page,
so the wait now passes only on the reloaded one.
A product race, not only a test race. On task-conflict the dialog wrote
the refusal into the old form, turned its buttons back on and only then
read the current task. For the length of that read the form showed the
refusal beside the old task and a Save went out against the old digest.
The conflict test read the title in that window and failed under load.
The old form now stays disabled with "Reading the current task…" until
the read answers; the refusal and the reopened form appear together. A
read a poll overtook no longer reopens on the board's old read; the
dialog reads the task itself. If the task cannot be read the form keeps
only Cancel and takes no save. A dialog closed during the read stays
closed, and a form whose Save is disabled refuses a submit sent past it.
The requirements page drew project-request-select, project-north-star
and project-prd with fixed ids, so the same project in both panes put
each id in the document twice. mountContent takes the pane's idPrefix
(pane-a-, pane-b-) and every id and id lookup on the page follows it;
the full view passes none and keeps its ids.
The dashboard was already prefixed and the Agents page draws no ids.
A new browser test opens the same dashboard, Agents page and project
pages in both panes and checks for duplicate ids and for labels, ARIA
references and in-page links that leave their pane.
U5 draws the organization dashboard into a side-by-side pane, and the
System types section came with it: Enable and Disable buttons that wrote
to the selected organization whatever the pane showed, a heading id
repeated in both panes, and a Settings Open button that focused the
first pane's section. A pane now shows its own organization's types
with no switch and says to change them from the full view, as U5 does
for the bus restart. The heading id carries the pane's prefix, Open
focuses the section in its own dashboard, and the click handler and
the post-write repaint ignore panes.
Recheck the session after the read so a sign-out dispatches no write.
Take the enabled set and its revision from one plugins read. Let the
owner clear an inactive-only selection when nothing is enabled. Keep
role tools as the hard ceiling in the toolset design, name only
vocabulary tools in the mapping, and intersect mission and task over
the final set.
The org dashboard lists the installed system-type modules with their own
labels and descriptions, marks which are enabled, and gives the owner an
enable or disable control through plugins.set behind the owner fence, with
the revision and the transaction receipt. The project page selects from the
organization's enabled set through project.system-types.set and shows a
disabled selection as inactive, as system-types.console reports. Both routes
go through the per-org Console call path and its admission checks.
docs/plans/2026-11-10_system-type-toolsets-design.md proposes how a launch
turns a project's active system types into toolsets under the role, mission
and task ceilings. Design only; no roles/ or contract change is applied.
The rebase onto U3 brought in A1's "Available in a project" chooser. Its
change handler kept one renderer and wrote into the first table in the
document, keyed by the page's organization. The rebase resolution keys
the renderer and the choice by each grid's organization and redraws the
table in the chooser's own section. This test covers it: before that
change, pane b's choice never redrew pane b.
A pane keeps its request choice and PRD disclosure on the pane, per
organization, instead of on the project content element. A new
conversation changes the project header, which replaces that element;
the choice now survives it.
One observer per pane element, disconnected on leaving the page, so a
closed and reopened pane rewrites links for its current organization.
Project reads check the pane generation and organization before any
DOM write. Request selection and expanded PRDs are kept per pane.
A late navigation focuses its heading only when focus has not moved to
the other pane. Project pages in a pane use a compact layout with
container queries.
The Side by side page now opens two panes. Each pane shows an organization
dashboard, an agents grid or a project page, chosen from a picker that lists
every organization and project. Pane state lives in the URL and is kept per
viewer. A separator resizes the panes by pointer or arrow keys, closing a
pane goes back to one, and each pane keeps its own focus and scroll through
polling. A pane whose organization's bus is down says so while the other
pane keeps working. On a phone a two-way switch shows one pane at a time.
Panes are read-only. Conversation, room, settings, queue, docs and inbox
pages open in full view; chat.js is a single-instance page with global ids.
A conflict on a task another writer moved out of the board's server
filter now reads that task from its organization unfiltered, so the
dialog reopens on the current task. An empty result names the
organizations whose tasks were not read instead of saying there are
no tasks.
- Edit and archive require the digest the board read (expect); the route
refuses a write without it.
- The edit dialog saves against the task it showed, so a poll that reads a
newer task turns a stale save into task-conflict; the dialog reopens on the
current task.
- An answer the board cannot read, or one marked sent=unknown, is shown as an
open outcome, not as a refusal.
- The per-org cap applies after the filters and keeps the most recently
changed tasks; projects, metrics and the inbox count every task. The board
sends project, state, assignee and label filters to the server.
- A poll keeps the focus, caret and scroll the person has at the moment it
answers, not when it started.
- An org whose tasks read fails shows "tasks not read" and is not writable.
- browser.test.mjs pins tasks-board.js in the script order.
Replaces the Tasks placeholder with sample 06: every organization's
tasks read through its own bus and tagged with the org, Kanban and List
with the layout remembered per viewer, and org, project, state,
assignee and label filters. A down org keeps its U1 state.
Writes (create, move, edit, archive) go to POST /api/workspace/tasks,
behind the hook fence, owner only and --send only. Each write resolves
only the org it names and calls that org's bus with the person's store
verbs, so the bus's human check, tracker check and digest check all
apply. Refusals reach the person with what was and was not written.
A Vikunja organization's tasks are read through the tasks adapter and
shown read only; the store verbs refuse them with tracker-not-store.
The workspace nav repaints after async organization and project reads.
Under load (gate at load 14) a repaint landed between the flat-grouping
wait and the org click, and the click found no button. The click now
retries inside the wait until the page shows org grouping.
A project row's sessions, open tasks and needs-you figures come from
reads beyond the project list; when those failed in the row's
organization the cells now say not read instead of none or 0, and a
note names the organization. Dashboard per-organization figures and
Done this week count only organizations that made the tile's reads, a
dashboard with no answering organization shows no totals, and empty
lists say not read when no organization could make the read. Sessions
now names each session's organization for screen readers. The
org-archive test waits for the setup steps, which render after the
editor title.
In organization mode #/ is the cross-organization dashboard (sample 02),
#/projects lists every organization's projects with sort, filters and the
shared By org / Flat grouping (sample 04), and #/inbox lists decisions
across organizations with per-organization decide paths and terminal
commands carrying --org and the full id (sample 05). The per-organization
board moves to #/board and the organization's own project list to #/chat,
which keeps the project surface. Reads go through workspace-data.js; a
read an organization could not make says not read, never zero. Polling
repaints in place and keeps focus, caret and scroll.
open() waits only for the editor title; the setup step enables the bus run
button after its async /setup read. Under load the assert ran first and
failed (A1 final gate, 2 of 4 runs). Wait for the enabled button instead.
Two independent reviews (Sol, daybreak-blue) asked for changes.
- An organization's own assistant instance is scoped to that
organization. Only the root assistant, which no union holds, is
cross-organization.
- A root opt-out or project exclude whose name a more specific level
defines is no longer listed as unavailable.
- A malformed root agents.json refuses with "not valid JSON" only; the
parser's text, which can quote the file's bytes, is dropped.
- A refused root or project var names its key, never its value; an id
check's quote of a root value is dropped too. A value can be a secret
typed in the wrong place, and these refusals reach the Console.
- The root assistant's row shows the model the root file sets.
Tests cover each case, with a sentinel value checked absent from the
route body. The design doc records the behavior.
Phase A slice A1, no authority change. Agents are defined at three levels:
the user's root (<configDir>/agents.json), the organization (roles) and the
project (projects.<p>.agents in the organization file). A project's agents
are their union, the most specific level winning.
- organization: agent-levels.mjs loads and checks the root file (no link,
owner uid, no group or other write) and project entries (define, narrow
intersect keys only, exclude), and resolves the union. The organization
file checks project agents for every reader and leaves them out of the
loaded business, so buses see what they saw before. Old-path re-export
in packages/business.
- webui: GET /api/agents/levels, owner only, with no host path, credential
reference or var other than the model in its body. display.js leaves it
unscoped. SERVER_PROJECT_KEYS holds agents.
- Agents grid: the level band, a project picker, and per agent its level,
the file that defines it, memory, status and model, with narrowed keys,
overrides and what the project does not get.
- Design: docs/plans/2026-11-10_agent-levels-design.md. The roles/ change is
a proposal outside this branch.
- Agent activity comes from the bus agents read (the state for the run
holding the role); with no reported state a live agent shows "running",
never working or idle.
- A session's project and task is the thread its live run claims
(thread.claims), not the agent's first listed conversation.
- The dashboard reads an organization only after /api/orgs lists it as
valid; otherwise it shows "Organization unavailable" and reads nothing.
Agents reads the selected business only for a listed organization.
- A failed conversation read says "Conversation unavailable", not that
none exists.
- A reported model gets a Change link to Settings, Routing, in the
dashboard and agents tables.
Finishes the U2 WIP (37b0891a3) to samples 01, 03 and 08 of the
2026-11-09 unified workspace set:
- workspace.js: sidebar Organizations and Projects groups (collapsible,
state remembered), the org dashboard, the Agents levels.
- workspace-data.js: every read these screens make, GET no-store,
redirect manual; a refused read shows unavailable for that part only.
- shell.css: the html[data-organizations] .ws-* block from the sample.
- bus.js: org-mode sidebar order, pinned Settings, no project tree under
Projects, focus kept across repaint.
- Honest empty states where the Console has no data (KPIs, metrics, kind).
- Tests: unified-workspace-browser (10), picker/delete/switch browser tests
follow the sidebar, browser.test script pin.
GET /api/workspace on the dev-local-owner Console: one snapshot of
projects, sessions, inbox, tasks and per-org metrics across every
organization the owner belongs to. Membership reuses the org list's
admission (organizations.members); each org is read over the same
per-org options the org pages use, with one deadline per org (4000 ms)
and a state (ok, down, starting, timeout, invalid) with a time. Every
row carries its org id; decide commands carry --org. Granted users
and the tester get 403 owner-only. Polling at 10 s, no push.
mosaic console restart and mosaic org reload <id> call the Console's
F3 routes (POST /api/console/restart, POST /api/orgs/<id>/restart/bus)
so gating, proof and trail are the Console's own; both refuse inside
an agent run first.
The cross-org assistant read is designed and stopped: it needs a new
broker read verb or role authority.
The instances route now passes the owner trail note, so an organization
instance change appends the actions.jsonl line the README promises (P3).
The README claimed no agent reaches the F3 controls. On a dev local owner
Console that is false: every local request acts as the owner, which the
pinned startup warning already says. The README now states it and names
sign-in as the boundary; a local-agent peer refusal is a follow-up (P1).
app.js busDown conflict: refactor's order (org list read before
bus-status, E2E5 review P2) with F3's Restart the bus attachment on
the Bus unavailable banner.
E2E5 review P2. An open Settings page kept Done after the bus died
because only opening the organization reread bus health. display.js
now announces each fresh list read, org-create.js redraws the setup
steps when the selected org's bus state flips (choices kept, no draft
guard), and app.js reads the list before bus-status so a failed
bus-status read no longer skips it. Regression: healthy, Refresh down
shows Start, Refresh up hides it; red on 8a8edc44b.
Conflicts kept both sides: rename stage B organization paths and the
relative-launch re-exec with F3 imports, F2 onboarding script and the
north-star fence exemption with F3 console-restart.js and ORG_OWNER_WRITE,
and the /api/config fields from both.
Self-service operation from the WebUI, for a dev instance first.
- Settings > Console > Restart Console. A foreground `mosaic console
start` now supervises the webui: the child exits 75 after answering a
restart and the supervisor relaunches it with the same argv. Org buses
keep running. /api/config carries a per-process boot id, so the open
page polls until a new process answers and then reloads. A detached
Console has no supervisor and answers 409.
- Restart bus on every "Restart needed" receipt and on the bus-down
banner: POST /api/orgs/<id>/restart/bus restarts a running bus or
starts a stopped one, then shows a done receipt with the time.
- Settings > Routing: after a save, the page lists how the change
reaches each agent (next start, session end, thread idle) and offers
"Restart <agent> now" for a running one. A session is terminated with
the reason "route change" and launched again; a thread is terminated.
- Organization settings: the signed-in owner's settings, routing,
agent, plug-in and Model proxy writes now reach the selected org's
bus instead of answering 403. Org mode exists only under
--dev-local-owner. Proxy setup stays a terminal step; a dev-only note
names the two commands.
Every control needs the signed-in owner session, the page's exact
Origin and Sec-Fetch-Site: same-origin. The tester listener, granted
users, API tokens, agents and bus roles are refused before any work.
Each use is written to <dataRoot>/console/actions.jsonl (0600, no
symlinks) with who, when, what and the outcome. The Console and bus
restart trails fail closed. No change to credential handling, the
broker, the bus tenant boundary or v2 schemas.