Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e021dfa260 | ||
|
|
b726e10f44 | ||
|
|
7b10bc0fff | ||
|
|
9ba31b5b3f | ||
|
|
9db578ff54 | ||
|
|
8eb9ddd12c | ||
|
|
ae3c36f248 | ||
|
|
a2af7ab495 | ||
|
|
aa1bd209e3 | ||
|
|
ace27dc5eb | ||
|
|
a4a42dbac2 | ||
|
|
a31f1a0f8b | ||
|
|
992650232f | ||
|
|
a7661ada06 | ||
|
|
f871cb3096 | ||
|
|
0bbd23c619 | ||
|
|
0217b71c8f | ||
|
|
c6f528a32f |
@@ -20,6 +20,11 @@ not fleet seats. Keep changes in `/mnt/storage/src/mosaic-stack`; do not modify
|
||||
| Morgana | Builder, MVP T1 row 193 (human users and RBAC) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
|
||||
| Honker | Builder, MVP T1 row 194 (agent lifecycle) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
|
||||
| Gander | Builder, the human boundary across uids (P19 333, #1694) | T3 session (Codex, gpt-6.1-sol), started by Sage under mos-0977 | none, T3 only |
|
||||
| Herb | Builder, a second gate lane (#1695) | T3 session (Codex, gpt-6.1-sol), started by Sage under mos-0977 | none, T3 only |
|
||||
| Bushroot | Builder, retire the control board writer on 7331 (#1696) | T3 session (Claude, claude-haiku-5-5), started by Sage under mos-1008 | none, T3 only |
|
||||
| Megavolt | Builder, MVP T1 item 6 (agent credentials and git identity, #1697) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
|
||||
| Quackerjack | Builder, federation R5 and R2 (#414) | T3 session (Claude, claude-haiku-5-5), started by Sage under mos-1023 | none, T3 only |
|
||||
| Gizmoduck | Federation design (#414) and builder of FED R1 | T3 session (Claude, Opus 5.5, thread 9312a896) | none, T3 only |
|
||||
| Webby | Reviewer: Case 2 designs, rows 192 and 194 | T3 session (Claude, Opus 5.5, thread 03086006), started by Mos (mos-0981) | none, T3 only |
|
||||
| Duckworth | Reviewer: federation designs, row 193 | T3 session (Claude, Opus 5.5, thread ffceafec), started by Mos (mos-0977) | none, T3 only |
|
||||
| Sage | Project lead: coordination, review, integration; earlier DYOR strategy records retained | T3 session (Claude Code); Pi launcher `zai/glm-5.3:high` retained | `agents/sage/launch.sh` |
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Bushroot
|
||||
|
||||
Builder seat, Claude Haiku 5.5, started by Sage under mos-1008 item 2 (a low-level model under Q177) for the row
|
||||
that retires the control board writer on 7331 (#1696).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `bushroot/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Gizmoduck
|
||||
|
||||
Design and builder seat, Claude Opus 5.5, a T3 thread (9312a896). Author of the federation design
|
||||
(`agents/gizmoduck/work/federation/DESIGN.md` on `gizmoduck/federation-design`, #414). Builds FED R1, the
|
||||
access.json loader and validator, under Mos's order mos-1023 item 1.
|
||||
|
||||
No launcher in this directory. The seat works in its own worktree and has no credentials of its own; until
|
||||
the credentials row settles per-agent identity, it pushes only its own `gizmoduck/*` branches through the
|
||||
repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,538 @@
|
||||
# Row 210 (#424, Gitea #1707): agent host access with sudo through the credential store
|
||||
|
||||
Gizmoduck, 2026-10-20, r3. Design only, no code. Reviewer Duckworth, gate owner Sage. Due 2026-10-27 (Q184).
|
||||
|
||||
- Brief: `docs/plans/2026-10-20_host-credentials.md` at 9db578ff5.
|
||||
- Base read: `origin/refactor` 9ba31b5b3. Row 199 read as DESIGN.md r6 (4d3007ec9) and its code and
|
||||
BUILD.md at addee664a on `megavolt/row199`.
|
||||
- PRD: REQ-CRED-3 in Honker's 1.7 draft (`agents/honker/work/queue-194/PRD-1.7-DRAFT.md:111-115`, 11cb0423b).
|
||||
Jason approved PRD 1.7 (d52febb5 yes at 20:36:47Z, mos-1089), so REQ-CRED-3 is in.
|
||||
- Scope (sage-1468): this row ships the `host.run` broker op, the grant check, the executor and the receipts,
|
||||
all testable over `broker.sock`. The `host_run` harness tool is out, as 199 left its publish tool out
|
||||
(199 BUILD.md deviation 12). Sage files one follow-up row for the agent-facing harness tools, publish and
|
||||
`host_run`, in `packages/harness`. Duckworth's r2 N5 (the tool help says a gated call leaves stdin under
|
||||
4 KiB) belongs to that row.
|
||||
- Branch `gizmoduck/row210`. Row claimed in-progress (rev 2361) after Sage cleared the after (rev 2359). It
|
||||
lands only after 199, rebased onto 199's landing (Q177).
|
||||
|
||||
Rounds:
|
||||
- r3 (duckworth-0043, REVIEW-r2.md at 52b5fee63, approved with C1, and sage-1468, sage-1472): C1 Q-J0's revoke
|
||||
line now matches section 6, the connection is cut and a started command may keep running. Q-S3 answered,
|
||||
the harness tool is out (scope line above, D10). N1 the runner list takes Duckworth's whole list, and D6
|
||||
says the per-call decision is the control. N2 numeric shorthand, unspecified and mapped addresses and
|
||||
`*.localhost` refuse (D3, T1). N3 the pids file records each pid's start time (D7, T27). N4 the mutant
|
||||
table is in order.
|
||||
- r2 (sage-1450): no `"any"` grant. The command boundary is an argv allowlist per host grant, and load refuses
|
||||
a wildcard (D6, T26, M6, M28). PRD 1.7 is frozen, so `host.sudo` stays a hard wait and Jason decides each
|
||||
call. Section 5 now holds the proposed 1.8 routing requirement. D5 is confirmed against 199's deviation 11.
|
||||
The seat network gap is a known limit with what closes it (section 6). Cites now point at 199's code.
|
||||
- r2 (duckworth-0042, REVIEW-r1.md at 8140b7f39): B1 events name host, user and argv in clear (D5 step 6,
|
||||
T22, M30). B2 a gated call's question shows stdin in full (D4, T9, M31). B3 the key and processes die with
|
||||
the bus, and bus start sweeps leftovers (D7, T27, M32 to M34). B4 Q-J0 for Jason in section 10, worded to
|
||||
stand alone (mos-1088). Notes taken: N1 (E12, runGit's kill shape), N2 (loopback refused, own-host limit),
|
||||
N3 (process groups), N4 (stdin can make a prefix root), N5 (`-l` and a separate address), N6 (D8 framing),
|
||||
N7 (section 7).
|
||||
|
||||
## 1. What the code does today
|
||||
|
||||
| # | Fact | Cite |
|
||||
|---|---|---|
|
||||
| E1 | The bus reads credentials for two kinds only, `gitea` and `vikunja` | `bus/src/credentials.mjs:22`, `business/src/vocabulary.mjs:103` |
|
||||
| E2 | A credential value must match `^[-A-Za-z0-9._~]+\n?$`, at least 16 and at most 16384 bytes. An OpenSSH private key fails it at the first `-----BEGIN` space | `bus/src/credentials.mjs:84-87` |
|
||||
| E3 | `assertClean` searches `JSON.stringify(result)` for each token. JSON escapes a newline, so a multi-line secret is never found: `JSON.stringify("a\nb").includes("a\nb")` is `false` (run on node here) | `bus/src/credentials.mjs:95-103` |
|
||||
| E4 | `use()` turns any exception in the operation into `service-failed`, then runs `assertClean` | `bus/src/credentials.mjs:104-117` |
|
||||
| E5 | Host to bus-child IPC is serial, one request at a time, with a 10 s timeout. A slow reply breaks the channel | `cli/src/host.mjs:31`, `:270` |
|
||||
| E6 | The web verbs run in the bus child over `broker.sock`. `requestWeb` checks keys, runs `#secretCheck`, checks the grant `roles[s.role].web`, records `action.allowed` (requested), calls `onAdmit()` so the server raises the socket timeout, runs the service and records completion | `bus/src/broker.mjs:1492-1530`, `bus/src/server.mjs:87-95` |
|
||||
| E7 | `roles[role].web` comes from the resolved tool limit. A role entry in the bus holds `authority`, `web` and `credentials` | `bus/src/business.mjs:17-20` |
|
||||
| E8 | A business-file role entry takes `definition`, `holder`, `vars`, `tracker`, `credentials`. Anything else refuses at load | `business/src/business.mjs:89` |
|
||||
| E9 | The chief's `organization.set` takes only `ORGANIZATION_KEYS`. The human-only `instances.set` takes `INSTANCE_KEYS` and refuses any definition that declares a credential (`organization-credentials-out-of-scope`). Holder, credentials and tracker stay hand edits | `bus/src/broker.mjs:145-147`, `:1248-1298`, `business/src/instance-settings.mjs:1-5`, `:22-31` |
|
||||
| E10 | An action outside ROUTED is a hard wait and classifies `gated` for every role. No role file may grant one | `bus/src/broker.mjs:44-70`, `business/src/vocabulary.mjs:58-88`, PRD REQ-WAIT-2 and 3 |
|
||||
| E11 | The seat sandbox does not confine the network, abstract Unix sockets or the host UID | `seat/src/sandbox.mjs:29-31` |
|
||||
| E12 | On refactor the bus child spawns nothing. With 199 it spawns git: `publishRemote` (`bus/src/runtime.mjs:245` at addee664a) runs `runGit`, a detached group leader that gets SIGTERM at the limit, then SIGKILL to the group after a grace period | `seat/src/checkout.mjs:95-138` at addee664a |
|
||||
| E13 | Role networks are `none`, `api-only`, `open`. The bundle carries the value, and the sandbox's own comment says the network is not confined (E11) | `business/src/vocabulary.mjs:101`, `harness/src/bundle.mjs:252` |
|
||||
|
||||
Row 199 gives, in code at addee664a: `useFor` (`bus/src/credentials.mjs:228`), `issue` (`:156`), `revoke`
|
||||
(`:197`), `remove` (`:211`), the kind list (`:49`), the token pattern (`:19`, `:143`), the store dir's file
|
||||
regex `^(gitea|vikunja)(\.tmp)?$` (`:222`) and `withGitea` (`bus/src/runtime.mjs:233`). From its design: the store at `<configDir>/secrets/agents/<business>/<agent>/<kind>` (D1), the `{store: true}`
|
||||
reference (D2), the read path (D3, D3a), `useFor(session, kind, op)` with the call-site allowlist test (D5),
|
||||
the `publish` grant shape, a list on the role entry plus a hard-wait action whose decision binds the commit
|
||||
(D9), and the human-only lifecycle verbs with `changes.jsonl` receipts (section 6).
|
||||
|
||||
## 2. Probe (throwaway keys, scratch sshd on 127.0.0.1:22410, OpenSSH 10.5p1)
|
||||
|
||||
Run in `/mnt/storage/scratch/gizmo-210-probe` with keys made for the probe and deleted after. No real key or
|
||||
host was touched.
|
||||
|
||||
| Probe | Result |
|
||||
|---|---|
|
||||
| `ssh-add -` with the private key on stdin, to an agent on a private socket | works, "Identity added: (stdin)" |
|
||||
| `IdentityAgent=<sock>` with `IdentitiesOnly=yes` and `IdentityFile=none` | refused, `Permission denied (publickey)`. With `IdentitiesOnly`, ssh offers only agent keys that match a configured identity |
|
||||
| same with `IdentityFile=<the public key file>` | works, rc 0 |
|
||||
| `-i /dev/fd/3` with the key on fd 3 | fails, "Identity file /dev/fd/3 not accessible". ssh closes inherited fds, so the fd route is out |
|
||||
| a known_hosts file pinning the wrong key under `HostKeyAlias` with `StrictHostKeyChecking=yes` | refused, "Host key verification failed", rc 255 |
|
||||
| `printf '%s|'` with `'a b'` and `'it'\''s'` single-quoted, plus an unquoted `$((1+1))` as a control | prints `a b|it's|2|`. The quoted elements arrive whole, and only the unquoted control expanded |
|
||||
| remote `exit 7` | ssh exits 7 |
|
||||
| killing the agent | its socket file is gone |
|
||||
|
||||
## 3. Decisions
|
||||
|
||||
### D1. The kind and its naming
|
||||
|
||||
One key per (agent, host). The kind name is `host.<hostId>`, so the store file is
|
||||
`<configDir>/secrets/agents/<business>/<agent>/host.<hostId>` (199 D1, unchanged). `hostId` matches
|
||||
`^[a-z0-9][a-z0-9-]{0,31}$`.
|
||||
|
||||
The closed kind set becomes `gitea`, `vikunja` and the family `host.<hostId>`. One exported helper,
|
||||
`credentialKind(name)` in `vocabulary.mjs`, answers whether a name is a kind, and each place that checks
|
||||
kinds calls it: `SERVICES` users (`role.mjs:105-118`), 199's `bus/src/credentials.mjs:49` and the store
|
||||
file regex at `:222`, the broker's service
|
||||
check, the CLI `issue` date flag and 199's remove regex. A build test greps for literal `'gitea', 'vikunja'`
|
||||
lists outside `vocabulary.mjs` and fails on a new one.
|
||||
|
||||
Why per host and not per agent. A key that leaks, or a host that is retired, is revoked for that host alone.
|
||||
ops-01 installs one public key per host, and an `authorized_keys` line names one agent. The cost is more keys
|
||||
for Jason to issue, one per grant.
|
||||
|
||||
A role definition declares the kind once as `{"service": "host"}` with no scopes. The business file binds the
|
||||
concrete hosts (D3). A definition can't name a host, because hosts belong to one stack instance.
|
||||
|
||||
The date is `--rotate-by DATE`, as for gitea. An ssh key doesn't expire on its own, so `status()` reports
|
||||
`rotation-due` or `valid`, the gitea branch.
|
||||
|
||||
### D2. The value check
|
||||
|
||||
`checkValue(kind, text)` replaces the one regex at `credentials.mjs:84`. For `gitea` and `vikunja` it is
|
||||
today's check, unchanged. For `host.*` the value must be:
|
||||
|
||||
- PEM framed by `-----BEGIN OPENSSH PRIVATE KEY-----` and `-----END OPENSSH PRIVATE KEY-----`, LF line ends,
|
||||
at most 16384 bytes (the existing cap).
|
||||
- An `openssh-key-v1` blob with cipher `none`, kdf `none`, exactly one key, type `ssh-ed25519`. Parsed in
|
||||
node with `Buffer`, no ssh-keygen call, so the check runs at bus start without a process.
|
||||
- The two check integers equal, and the public key in the private section equals the one in the header.
|
||||
|
||||
Anything else refuses `credential-format`. An encrypted key refuses, because the bus has no way to ask for a
|
||||
passphrase and `BatchMode=yes` would fail at use time instead of at load. The parsed public key is kept with
|
||||
the entry so `status` and D7 can print its fingerprint without touching the private part.
|
||||
|
||||
### D3. The grant
|
||||
|
||||
Four layers. Each must hold, and the code checks all four on every call.
|
||||
|
||||
1. **Role definition, reviewed commit.** `roles/<def>.json` lists `{"service": "host"}` in `credentials`. That
|
||||
is the "reviewed role change" in the brief. Once a PRD version routes `host.sudo` (section 5), the same
|
||||
reviewed commit puts `host.sudo` in that definition's `authority.withinRole`.
|
||||
2. **Host registry, business file, hand edit.** A new top-level key:
|
||||
```
|
||||
"hosts": { "<hostId>": { "address": "10.1.1.53", "port": 22, "user": "localadmin",
|
||||
"hostKeys": ["ssh-ed25519 AAAA..."] } }
|
||||
```
|
||||
`address` is an IPv4 or IPv6 literal or a DNS name matching `^[a-z0-9]([a-z0-9.-]{0,251}[a-z0-9])?$`.
|
||||
An address of this machine refuses at load (r2 N2, r3 N2). For an IP literal, which `net.isIP` decides,
|
||||
that is `127.0.0.0/8`, `0.0.0.0/8`, `::1`, `::`, and an IPv4-mapped `::ffff:` form of either IPv4 range.
|
||||
For a name, it is `localhost` and any name ending in `.localhost`. A name made only of digits and dots
|
||||
refuses unless `net.isIP` reads it as IPv4, so the shorthand `127.1` and the integer `2130706433`, which
|
||||
pass the DNS pattern and which the resolver reads as 127.0.0.1, refuse. A label starting `0x` refuses for
|
||||
the same reason. `port` is 1 to 65535. `user` matches `^[a-z_][a-z0-9_-]{0,31}$`. `hostKeys` is a non-empty list of
|
||||
`ssh-ed25519`, `ecdsa-sha2-nistp256` or `ssh-rsa` public key lines, type and base64 only, no comment.
|
||||
3. **Agent grant, business file, hand edit.** A new role-entry key beside 199's `publish`:
|
||||
```
|
||||
"hosts": { "<hostId>": { "commands": [["systemctl", "status"], ["journalctl", "-u"]] } }
|
||||
```
|
||||
Each `hostId` must be in the registry, and the role's definition must declare `host`. A role entry with
|
||||
`hosts` whose definition lacks `host` refuses at load and names `roles.<agent>.hosts`. `commands` is the
|
||||
command boundary (D6).
|
||||
4. **The key.** `credentials["host.<hostId>"] = {"store": true, "rotateBy": DATE}` with the file present
|
||||
(199 D2, D3a). A grant without an issued key refuses `credential-unavailable` at call time, as 199 does.
|
||||
|
||||
Who writes each layer is in section 4. Neither the chief nor any agent can reach layers 2 to 4: `hosts` is
|
||||
outside `ORGANIZATION_KEYS` and `INSTANCE_KEYS` (E9), `instances.set` already refuses a definition that
|
||||
declares credentials (E9), and 199's `issue` is human-only.
|
||||
|
||||
### D4. The action and the decision binding
|
||||
|
||||
`host.sudo` is a new action in ACTIONS and in neither ROUTED list. Until a PRD version routes it, it is a hard
|
||||
wait and classifies `gated` for every role (E10). Each call then needs a decision the human resolved. PRD 1.7
|
||||
is frozen (sage-1450, decision d52febb5 cites its sha256), so under 1.7 Jason decides every `host.run`.
|
||||
|
||||
The decision binds one call, the way 199 D9 binds `pushCommit`. When the action is `host.sudo`, the
|
||||
`decision.raise` event body gets `hostRun = {host, argvSha256, stdinSha256}`, from the raise's own args. The
|
||||
question shown to the human ends with the host id, the user, the argv with each element JSON-quoted, and the
|
||||
stdin in full as one JSON string, so Jason sees all of what runs (B2). The broker appends that text itself, and
|
||||
the agent can't shape it. The whole question keeps the 4096-character cap (`broker.mjs:126`, `:1654`), so on a
|
||||
gated call stdin is as large as the cap allows and no larger. A raise that doesn't fit refuses
|
||||
`invalid-request`. `argv ["bash"]` can't be granted at all (D6), and `["tee", "/etc/x"]` with a stdin Jason
|
||||
can't see is ruled out by showing it. `#authorityFor` refuses `decision-mismatch` when the call's host, argv hash or stdin hash differs, as
|
||||
`holderRun` does at `broker.mjs:796`. A decision is spent once (`decision-consumed`).
|
||||
|
||||
After routing, a definition that lists `host.sudo` within-role runs without a decision, and `decision` is
|
||||
optional for that class only. The grant layers in D3 still apply.
|
||||
|
||||
### D5. The executor: a `broker.sock` verb in the bus child
|
||||
|
||||
```
|
||||
host.run {"host", "argv": [..], "stdin"?, "timeout"?, "decision"?}
|
||||
-> {"exit", "signal", "stdout", "stderr", "truncated": {"stdout", "stderr"}, "ms"} or a refusal
|
||||
```
|
||||
|
||||
It runs in the bus child, shaped like `requestWeb` (E6), not as a host IPC op. A sudo command can take
|
||||
minutes and the host channel times out at 10 s and serves one request at a time (E5). The key is in the bus
|
||||
child's memory already (199 D3), so no ticket has to cross to the CLI host. Row 199's publish needs a ticket
|
||||
because git and the working repo live in the CLI host. ssh needs neither. 199's BUILD.md deviation 11
|
||||
(addee664a) keeps credential use in the bus child too: the push runs there under `withGitea`, and the host
|
||||
socket only checks and polls. Sage confirmed D5 matches that shape (sage-1450). `host.run` needs no host
|
||||
socket and no poll, because the agent calls `broker.sock` directly. The helper is `withHost(session, host,
|
||||
fn)`, beside `withGitea`, calling `useFor(session, "host." + host, fn)`.
|
||||
|
||||
Order of checks. Each refusal goes through `broker.refuse()` and logs `action.refused`.
|
||||
|
||||
1. `keys(args)`, then `#secretCheck(args)`, then identity from the cap.
|
||||
2. `host` is in the registry (`host-unknown`).
|
||||
3. `host` is in `roles[s.role].hosts` (`host-not-granted`). This is the REQ-CRED-3 check "a grant exists for
|
||||
that agent and that host".
|
||||
4. `argv` shape (D6) and `commands` match (`command-not-granted`).
|
||||
5. `#checkAuthority(s, "host.sudo", {decision, target: host, context})` (D4).
|
||||
6. Record `action.allowed` phase `requested` with host, address, user and argv in clear (B1), and stdin as
|
||||
size and sha256. Every arg already passed `#secretCheck` in step 1, as the web verbs log their url
|
||||
(`broker.mjs:1521`). After routing, the event log is the record of what ran as root. Then `onAdmit()`, which
|
||||
sets the socket timeout to the call's timeout plus 10 s, as the web branch does (`server.mjs:95`).
|
||||
7. `credentials.useFor(s, "host." + host, run)`, where `run` is D7.
|
||||
8. D8's leak check on the output, then `#secretCheck` on the result.
|
||||
9. Record phase `completed` with host, argv, exit, signal, ms, and size and sha256 of stdout and stderr. The
|
||||
output itself never goes into an event. A failure records `host-failed` with the same fields.
|
||||
|
||||
Only one `host.run` per (agent, host) runs at a time. A second refuses `host-busy`. That keeps a stuck call
|
||||
from piling up ssh processes and makes revoke (D9) simple.
|
||||
|
||||
Spawns reuse `runGit`'s shape (E12): an argv array, never a shell, `detached: true` so each process leads its
|
||||
own group, SIGTERM to the group at the limit and SIGKILL after a grace period. If the helper can be shared, it
|
||||
is, rather than a second copy. `env` is `{PATH: "/usr/bin:/bin"}` only, plus `SSH_AUTH_SOCK` for `ssh-add`.
|
||||
Each spawn is wrapped in `setpriv --pdeathsig KILL --`, the prefix the seat sandbox already uses
|
||||
(`seat/src/sandbox.mjs:44`), so it dies when the bus child dies (B3).
|
||||
|
||||
### D6. The command boundary
|
||||
|
||||
`argv` is an array of 1 to 256 strings. Each is 1 to 4096 bytes of UTF-8 with no NUL, and the total is at most
|
||||
32768 bytes. `argv[0]` must not contain `/` unless it starts with `/`. `stdin` is optional, a string of at most
|
||||
65536 bytes, and goes through `#secretCheck` with the args.
|
||||
|
||||
The remote command is built by the bus, never taken as a string:
|
||||
|
||||
```
|
||||
sudo -n -- 'argv0' 'argv1' ...
|
||||
```
|
||||
|
||||
Each element is POSIX single-quoted: wrap in `'`, and replace each `'` with `'\''`. The probe shows the
|
||||
remote shell gets each element back exactly (section 2). `sudo -n` fails instead of prompting, so a host
|
||||
without NOPASSWD returns sudo's error, not a hang. The host's login shell for `user` must be POSIX. That is a
|
||||
host precondition for ops-01, listed in section 7.
|
||||
|
||||
`commands` in the grant is an argv allowlist and nothing else. It is a non-empty list of argv prefixes. An
|
||||
argv passes when its first N elements equal some prefix of length N, compared element by element as literal
|
||||
strings, with no globbing. `[["systemctl", "status"]]` admits `systemctl status nginx` and refuses
|
||||
`systemctl restart nginx`.
|
||||
|
||||
There is no wildcard grant (sage-1450). Business-file load refuses, naming `roles.<agent>.hosts.<id>.commands`:
|
||||
|
||||
- any value that isn't a list, including the strings `"any"` and `"*"`,
|
||||
- an empty list, an empty prefix, a non-string element, or a duplicate prefix,
|
||||
- an element containing `*`, `?` or `[`, so nobody writes a glob that the matcher would read literally,
|
||||
- a prefix whose first element, by basename, is a shell, a command runner or an interpreter. Shells: `sh`,
|
||||
`bash`, `dash`, `zsh`, `ksh`, `fish`, `busybox`. Runners: `su`, `sudo`, `doas`, `pkexec`, `runuser`, `env`,
|
||||
`xargs`, `nohup`, `setsid`, `timeout`, `nice`, `systemd-run`, `chroot`, `nsenter`, `unshare`, `setpriv`,
|
||||
`capsh`, `flock`, `stdbuf`, `ionice`, `chrt`, `taskset`, `strace`, `script`, `watch`. Interpreters:
|
||||
`python3`, `perl`, `ruby`, `node`, `awk` (Sage's ruling on duckworth-0043 N1). Each would turn the prefix
|
||||
back into "any". T26 has one case per name.
|
||||
|
||||
That list can't be complete, and it isn't the control. Under 1.7 the control is Jason's decision on each
|
||||
call, bound to host, argv and stdin (D4, Q-S1). The list only refuses the obvious cases at load, early. Many commands can still start a shell or an editor as root (`systemctl edit`,
|
||||
`find -exec`, interpreters). The allowlist limits mistakes and narrows what a grant means, but it is not a
|
||||
sandbox. Stdin goes to the command, so some prefixes are root on their own: `[["tee"]]` or `[["dd"]]` writes
|
||||
any file as root from stdin (N4). Section 6 says what remains.
|
||||
|
||||
Output: stdout and stderr each capped at 1 MiB. Past the cap the bus keeps reading and drops the rest so the
|
||||
remote side doesn't block, and sets `truncated`. `timeout` defaults to 60 s, maximum 900 s. At the timeout
|
||||
the bus sends SIGTERM to the ssh process group, then SIGKILL after 5 s, and answers `signal`. ssh's own exit
|
||||
code 255 and a remote command exiting 255 can't be told apart. The result returns stderr so the agent can see
|
||||
which, and the doc says so.
|
||||
|
||||
### D7. Handing the key to ssh
|
||||
|
||||
Per call, inside `useFor`'s operation:
|
||||
|
||||
1. `mkdtemp` a dir under `<configDir>/secrets/run/` with mode 0700. `<configDir>` is never bound into a sandbox
|
||||
(199 D3). The socket path is checked against the 107-byte Unix socket limit before use, refusing
|
||||
`host-failed` if over.
|
||||
2. Write two files in it, mode 0600: `known_hosts` with one line per pinned key, `mosaic-host-<hostId> <type>
|
||||
<base64>`, and `id.pub` with the entry's public key (D2).
|
||||
3. Write `<dir>/pids` with the bus child's pid. Each line in it is a pid and that process's start time,
|
||||
field 22 of `/proc/<pid>/stat`, so a reused pid never matches (duckworth-0043 N3). Start `ssh-agent -D -a <dir>/a.sock` as its own group leader,
|
||||
append its pid, and wait for the socket. Run `ssh-add -t <timeout + 30> -` with `SSH_AUTH_SOCK` set and the
|
||||
private key written to its stdin. The key never touches disk, argv or env. `-t` makes ssh-agent drop the key
|
||||
at the call's timeout plus 30 s even if nothing else runs (B3). ssh is the second group leader, and its pid
|
||||
is appended too. Timeout, revoke and the `finally` kill both groups, ssh-agent's and ssh's (N3).
|
||||
4. Run ssh with exactly these options and nothing from any config file:
|
||||
```
|
||||
ssh -F /dev/null -T -p <port>
|
||||
-o BatchMode=yes -o IdentitiesOnly=yes -o IdentityFile=<dir>/id.pub -o IdentityAgent=<dir>/a.sock
|
||||
-o StrictHostKeyChecking=yes -o UserKnownHostsFile=<dir>/known_hosts -o GlobalKnownHostsFile=/dev/null
|
||||
-o HostKeyAlias=mosaic-host-<hostId> -o UpdateHostKeys=no -o CheckHostIP=no -o VerifyHostKeyDNS=no
|
||||
-o ForwardAgent=no -o ForwardX11=no -o ClearAllForwardings=yes -o PermitLocalCommand=no
|
||||
-o ProxyCommand=none -o ProxyJump=none -o ControlMaster=no -o ControlPath=none
|
||||
-o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3
|
||||
-l <user> -- <address> <remote command from D6>
|
||||
```
|
||||
The user goes in `-l` and the address is its own argument, so nothing depends on how ssh splits `@` or on
|
||||
an IPv6 literal (N5). `-F /dev/null` stops `~/.ssh/config` from adding a ProxyCommand, a different key or forwarding. The pin is
|
||||
`StrictHostKeyChecking=yes` against a file the bus wrote from the registry, so there is no trust on first
|
||||
use: an unknown or changed host key refuses with rc 255 (probe, section 2) and the bus answers
|
||||
`host-key-mismatch` when stderr has "Host key verification failed".
|
||||
5. In a `finally`: kill both groups, remove the dir. A test asserts the dir is gone after success, refusal,
|
||||
timeout and an exception thrown inside the operation.
|
||||
6. If the bus child is killed, the `finally` never runs. `--pdeathsig` kills ssh-agent and ssh with it, and
|
||||
`-t` drops the key anyway. At bus start, before `broker.sock` listens, the bus sweeps
|
||||
`<configDir>/secrets/run/`. A recorded pid counts as alive only when it exists and its start time matches.
|
||||
For each dir whose bus pid is no longer alive, it sends SIGKILL to each recorded pid that is alive and
|
||||
whose `/proc/<pid>/cmdline` names that dir, then removes the dir. A dir whose bus pid is alive is left
|
||||
alone. Any other entry there refuses start, as 199 refuses a stray file
|
||||
in the store (`credentials.mjs:222`). Revoke after a restart has nothing in flight to reach.
|
||||
|
||||
The 0600 temp key file route the brief allows is not used. The agent route keeps the private key off disk,
|
||||
and the fd route doesn't work (probe).
|
||||
|
||||
### D8. The leak check
|
||||
|
||||
This is defense in depth (N6). The remote never holds the private key and forwarding is off, so output can
|
||||
carry the key only if the agent already had it. `assertClean` misses a multi-line key (E3). A `host.*` entry
|
||||
in the shared token map makes its loop compare against a value that never matches, which is harmless. For a `host.*` entry, `useFor` checks the result with
|
||||
`assertCleanKey(result, entry)`, which searches the JSON text for:
|
||||
|
||||
- each base64 body line of the PEM of 24 characters or more,
|
||||
- the whole body with line breaks removed,
|
||||
- the 32-byte ed25519 private seed as lowercase hex and as base64 at each of the three byte offsets.
|
||||
|
||||
A match refuses `credential-leak`. The output is dropped, and the event records only sizes and hashes. The
|
||||
existing `assertClean` still runs for the token kinds. The same function runs on `stdin` before the call, so
|
||||
an agent can't round-trip a key it somehow has back through the host.
|
||||
|
||||
### D9. Lifecycle
|
||||
|
||||
199's verbs carry over with `KIND = host.<hostId>`. Two additions:
|
||||
|
||||
- `issue AGENT host.<id>` checks D2, then prints the public key line and a suggested `authorized_keys` line
|
||||
for ops-01: `from="<stack host address>",restrict <type> <base64> mosaic-<business>-<agent>-<id>`. It
|
||||
prints only public material.
|
||||
- `revoke` also kills a `host.run` in flight for that agent and host (process group SIGKILL) before it drops
|
||||
the key. The call answers `credential-unavailable`. Remote processes that the killed ssh started may keep
|
||||
running (section 6).
|
||||
|
||||
`status` shows each host entry with its fingerprint (SHA256 of the public key), the rotate date and state.
|
||||
The tester gets `status` and `revoke` only, as in 199.
|
||||
|
||||
### D10. Where the files change
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `business/src/vocabulary.mjs` | `host.sudo` in ACTIONS, `credentialKind`, host id pattern |
|
||||
| `business/src/business.mjs` | top-level `hosts` registry, role-entry `hosts`, load checks from D3 |
|
||||
| `business/src/role.mjs` | `{"service": "host"}` in `checkCredentials` with no scopes |
|
||||
| `business/src/credentials.mjs` | `host.*` refs take `rotateBy` |
|
||||
| `bus/src/credentials.mjs` | kind check via `credentialKind`, `checkValue` (D2), `assertCleanKey` (D8), busy and kill map |
|
||||
| `bus/src/host-run.mjs` (new) | D6 argv and quoting, D7 ssh-agent and ssh spawn, cleanup |
|
||||
| `bus/src/broker.mjs` | `host.run` verb (D5), `host.sudo` in `#authorityFor` with `hostRun` (D4) |
|
||||
| `bus/src/server.mjs` | `host.run` branch beside the web branch with `onAdmit` |
|
||||
| `bus/src/business.mjs` | carry `hosts` onto the bus role entry |
|
||||
| `cli` | 199's `credentials issue` accepts `host.<id>`, prints D9's lines |
|
||||
|
||||
No `packages/harness` file changes. The `host_run` tool is Sage's follow-up row (sage-1468).
|
||||
|
||||
Row 199's own kind and regex are not changed inside row 199. This row rebases onto 199's landing and changes
|
||||
them here.
|
||||
|
||||
## 4. Who grants (brief item, "yours to propose with reasons")
|
||||
|
||||
Proposal: **Jason, at every layer that widens access. No agent, chief included, and not the tester.**
|
||||
|
||||
| Layer | Who | How | Why |
|
||||
|---|---|---|---|
|
||||
| Definition declares `host` (and later lists `host.sudo`) | Jason | reviewed commit to `roles/*.json`, as for any role authority (AGENTS.md invariant 7) | It decides which kinds of agent may ever hold a host key. Review puts a second seat on it |
|
||||
| Host registry and pins | Jason, with ops-01's key fingerprints | hand edit of the business file | The pin is the host identity. A wrong pin is a man-in-the-middle on root. It must come from the host side, not from a first connection |
|
||||
| Agent grant `roles.<agent>.hosts` | Jason | hand edit of the business file | Q183: "not all agents will need to have that access". The per-agent, per-host choice is his |
|
||||
| Key `issue` | Jason | 199's human-only `issue` | The key passes through the caller. The tester is an agent (199 section 6, duckworth N1) |
|
||||
| `authorized_keys` on the host | ops-01 | host side, from the line `issue` prints | ops-01 owns the hosts (brief) |
|
||||
| Revoke | Jason, Mos, or the tester | `revoke` | Taking access away should be quick and needs no new trust |
|
||||
|
||||
Why not a reviewed commit for the agent grant too. The business file lives in `<configDir>`, outside the
|
||||
repository (AGENTS.md invariant 2), and per-instance grants already stay hand edits there (E9, 199 D9 step 2).
|
||||
Moving grants into the repo would put host addresses and the per-instance roster under review, which is a
|
||||
bigger change than this row. The definition layer is the reviewed one.
|
||||
|
||||
Why not the chief. A chief with this power could hire an agent and grant it root. 199 already keeps hires
|
||||
uncredentialed (199 D13). `instances.set` refuses credentialed definitions today (E9), and this row keeps it
|
||||
that way.
|
||||
|
||||
REQ-CRED-3 says the who-grants answer is set by this design and approved by Jason. The table above is that
|
||||
answer.
|
||||
|
||||
## 5. The routing gap and a proposed 1.8 requirement
|
||||
|
||||
PRD 1.7 routes `role.hire`, `role.fire` and `git.push.remote`, not `host.sudo`, and 1.7 is frozen (sage-1450).
|
||||
This row builds for 1.7 as it stands. `host.sudo` is a hard wait under REQ-WAIT-3, and Jason decides each
|
||||
`host.run`, bound to host, argv and stdin (D4). That is safe, and slower than Q183's "allow agents to perform
|
||||
their jobs". The routing goes in 1.8. Sage takes the line below to Mos for Jason. I don't edit the PRD.
|
||||
|
||||
Proposed for PRD 1.8, beside REQ-CRED-3:
|
||||
|
||||
> **REQ-CRED-4.** The action `host.sudo` is routed. A role whose definition lists it within-role may run a
|
||||
> command with sudo on a host without a decision for each call, but only when all of these hold: the
|
||||
> definition declares the host credential kind, the business file grants that agent that host, the command
|
||||
> matches the grant's argv allowlist, and the agent's key for that host is issued. A grant names exact
|
||||
> argv prefixes. A wildcard grant is refused. A role without `host.sudo` within-role still needs a decision
|
||||
> for each call. Jason adds `host.sudo` to a definition only through a reviewed role change.
|
||||
|
||||
Routing is then one reviewed commit: `host.sudo` into both ROUTED lists and into the chosen definitions'
|
||||
`withinRole`, as 199 does for `git.push.remote`.
|
||||
|
||||
The brief also says this row lands only after the PRD version holding REQ-CRED-3 is approved. Jason approved
|
||||
1.7 (d52febb5 yes at 20:36:47Z, mos-1089), so that holds. 1.7 doesn't route `host.sudo`, so Q-S1 stands.
|
||||
|
||||
## 6. Limits, said plainly
|
||||
|
||||
- **Known limit: the seat network isn't confined, so root can outlive the grant.** `seat/src/sandbox.mjs:29-31`
|
||||
says the sandbox doesn't confine the network (E11). An agent whose allowlist admits any command that can
|
||||
write a file as root, or start a shell, can add its own key to `authorized_keys` on the host. It can then
|
||||
ssh straight from its sandbox, around the bus, the events and revoke. An `authorized_keys` `from=` option
|
||||
doesn't help, because the sandbox shares the stack host's address. The allowlist (D6) narrows this but
|
||||
doesn't close it.
|
||||
What closes it: a seat sandbox with its own network namespace (`bwrap --unshare-net`), where the only way
|
||||
out is the bus sockets and the model proxy, so role network `none` and `api-only` are enforced and not just
|
||||
carried in the bundle (E13). With that in place, a planted key is useless from the sandbox. Until then,
|
||||
ops-01's host-side audit (sudo log, `authorized_keys` changes) is the check after the fact. Sage is
|
||||
reporting the gap to Mos as its own follow-up (sage-1450). It is not this row.
|
||||
- **The stack's own machine.** A registry entry for the machine the stack runs on, or any machine holding the
|
||||
stack's `<configDir>`, gives an agent with a root-writing command every agent's keys and tokens, not only
|
||||
root there (N2). Load refuses loopback addresses, but it can't know every address of the stack's machine.
|
||||
Jason doesn't register it.
|
||||
- **Revoke stops the stack's path, not remote processes.** Killing ssh may leave a remote process running.
|
||||
`-T` gives no tty, so there is no SIGHUP from a closing terminal.
|
||||
- **Same OS user.** Anything running as the stack's OS user outside a sandbox can read the key file, as 199 D6
|
||||
says for every kind.
|
||||
- **Exit 255** is ssh's failure code and also a remote exit code (D6).
|
||||
- **The pin trusts the registry.** Whoever writes the business file controls which host key is trusted. That
|
||||
is Jason (section 4).
|
||||
|
||||
## 7. Host preconditions (ops-01, not this row)
|
||||
|
||||
- `user` has NOPASSWD sudo and a POSIX login shell. `sudo` has no `requiretty` for that user.
|
||||
- The agent's key is in `authorized_keys` with `from="<stack host>",restrict`.
|
||||
- The host's public keys are handed to Jason for the registry.
|
||||
- Duckworth's #423 r5 condition (duckworth-0035) applies. If ops-01 changes #423 step 1 to fit this design,
|
||||
for example per-agent `from=...,restrict` lines for localadmin, that change goes to Duckworth as a diff
|
||||
before it runs. That is the live part, not this row (N7).
|
||||
|
||||
## 8. Tests
|
||||
|
||||
All tests use a scratch sshd on 127.0.0.1 started by the test, with keys made by the test, and a fake `sudo`
|
||||
on the test user's PATH that prints its argv as JSON and runs nothing as root. The sshd config sets
|
||||
`AuthorizedKeysFile` and `HostKey` in the test's `mkdtemp` dir. A helper skips the ssh tests with a visible
|
||||
reason only when `/usr/bin/sshd` is missing, and the gate fails if any of them skipped. No test reaches
|
||||
another host.
|
||||
|
||||
| # | Test | Refusal or result |
|
||||
|---|---|---|
|
||||
| T1 | business load: `hosts` registry field checks, bad address, each own-machine form from D3 (`127.0.0.2`, `0.0.0.0`, `::1`, `::`, `::ffff:127.0.0.1`, `::ffff:0.0.0.0`, `localhost`, `a.localhost`, `127.1`, `2130706433`, `0x7f.1`), with `10.1.1.53` and `host.example` as controls, port, user, empty `hostKeys`, a key line with a comment | load refuses, names the key path |
|
||||
| T2 | role entry `hosts` naming an unknown host, or on a definition without `host` | load refuses `roles.<agent>.hosts` |
|
||||
| T3 | `organization.set` and `instances.set` with `hosts` in args | `invalid-request`, and the file is unchanged |
|
||||
| T4 | value check: a valid unencrypted ed25519 key loads. An encrypted key, an RSA key, two keys, CRLF, a gitea token in a `host.*` slot, and a PEM in a `gitea` slot each refuse | `credential-format` |
|
||||
| T5 | `host.run` with an unknown host | `host-unknown` |
|
||||
| T6 | `host.run` on a host not in the caller's grant, including a host granted to another agent | `host-not-granted` |
|
||||
| T7 | argv shape: empty, a NUL, too long, too many, a relative path with `/`, non-string | `invalid-request` |
|
||||
| T8 | allowlist: a matching argv runs, a longer argv runs, a shorter one and a differing element refuse | `command-not-granted` |
|
||||
| T9 | before routing: no decision refuses, a decision for another argv, another host or another stdin refuses, a spent one refuses. The stored question carries host, user, argv and the full stdin, and a raise whose question would pass 4096 characters refuses | `gated`, `decision-mismatch`, `decision-consumed`, `invalid-request` |
|
||||
| T10 | quoting: argv with spaces, quotes, `$()`, backticks, `;`, newline and glob characters reaches fake sudo unchanged | fake sudo's JSON equals argv |
|
||||
| T11 | the remote command starts `sudo -n --` | fake sudo sees `-n` and `--` first |
|
||||
| T12 | exit code and signal pass through, timeout kills the process group and answers `signal` | result fields |
|
||||
| T13 | output cap: 2 MiB of stdout returns 1 MiB and `truncated.stdout: true`, and the call doesn't hang | result fields |
|
||||
| T14 | host key pin: the sshd presents a key not in `hostKeys` | `host-key-mismatch`, and no command ran (fake sudo's marker file absent) |
|
||||
| T15 | a registry with the right key passes on a fresh dir with no `~/.ssh/known_hosts` in HOME. A HOME with a `~/.ssh/config` setting a ProxyCommand doesn't change the call | runs, and the proxy marker is absent |
|
||||
| T16 | leak: the fake sudo prints the private key file (multi-line), and in a second case prints only one body line, and in a third the seed in hex | `credential-leak`, the event has no output |
|
||||
| T17 | `stdin` containing a body line of the key | `credential-leak` before any spawn |
|
||||
| T18 | cleanup: after success, refusal, timeout and a thrown error, the run dir is gone and no `ssh-agent` or `ssh` with that dir in argv survives | dir absent, pgrep empty |
|
||||
| T19 | the private key never appears in the argv or env of any spawned process (read from `/proc/<pid>/cmdline` and `environ` while the fake sudo sleeps) | absent |
|
||||
| T20 | second concurrent call for the same agent and host | `host-busy` |
|
||||
| T21 | revoke during a sleeping call kills it, and the next call refuses | `credential-unavailable` |
|
||||
| T22 | events: `action.allowed` requested and completed carry host, address, user and argv in clear, and sizes and hashes of stdin, stdout and stderr. Never the output, the stdin or the key | event bodies |
|
||||
| T23 | `issue` of a `host.*` kind prints the public line and the `authorized_keys` suggestion and never the private key | stdout and stderr checked |
|
||||
| T24 | build grep: no literal `'gitea', 'vikunja'` kind list outside `vocabulary.mjs`, and 199's `credentials.use(` allowlist unchanged | grep |
|
||||
| T25 | no role file in `roles/` lists `host.sudo` while it is a hard wait (REQ-WAIT-2) | grep |
|
||||
| T27 | SIGKILL the bus child while fake sudo sleeps: the ssh and ssh-agent pids are gone within 2 s. A call with `timeout` 1 leaves an agent whose key is gone after 31 s (`ssh-add -l` on its socket, agent kept alive by the test). The next bus start removes the run dir, kills a planted live process whose cmdline names it, leaves a dir whose bus pid is alive, and leaves a live process whose pid matches but whose start time doesn't | pids, `ssh-add -l`, dir state |
|
||||
| T26 | load refuses `commands` as `"any"`, `"*"`, `{}`, `[]`, `[[]]`, a non-string element, a duplicate prefix, an element with `*`, `?` or `[`, and a prefix starting with each listed shell or runner, `/bin/bash` included | load refuses, names `roles.<agent>.hosts.<id>.commands` |
|
||||
|
||||
## 9. Mutants
|
||||
|
||||
One per refusal, plus the two the brief names. Each must be killed.
|
||||
|
||||
| # | Mutant | Killed by |
|
||||
|---|---|---|
|
||||
| M1 | registry check skipped | T5 |
|
||||
| M2 | agent grant check skipped | T6 |
|
||||
| M3 | grant check reads any agent's `hosts` | T6 |
|
||||
| M4 | each argv shape check removed, one mutant each (5) | T7 |
|
||||
| M5 | prefix match compares only `argv[0]` | T8 |
|
||||
| M6 | load accepts a string `commands` (wildcard grant) | T26 |
|
||||
| M7 | `#checkAuthority` call removed | T9 |
|
||||
| M8 | `hostRun` argv hash not compared | T9 |
|
||||
| M9 | `hostRun` stdin hash not compared | T9 |
|
||||
| M10 | quoting drops the `'\''` replacement | T10 |
|
||||
| M11 | `sudo -n` loses `-n` | T11 |
|
||||
| M12 | **host key pin skipped**: `StrictHostKeyChecking=accept-new` | T14 |
|
||||
| M13 | **pin skipped another way**: `UserKnownHostsFile` set to the user's default | T14, T15 |
|
||||
| M14 | `-F /dev/null` removed | T15 |
|
||||
| M15 | **key leaks into output**: `assertCleanKey` not called | T16 |
|
||||
| M16 | leak check uses only the joined body | T16 (one-line case) |
|
||||
| M17 | stdin leak check removed | T17 |
|
||||
| M18 | `finally` cleanup removed | T18 |
|
||||
| M19 | key passed with `-i` from a temp file left on disk | T18, T19 |
|
||||
| M20 | output cap removed | T13 |
|
||||
| M21 | timeout kill removed | T12 |
|
||||
| M22 | busy check removed | T20 |
|
||||
| M23 | revoke doesn't kill the in-flight call | T21 |
|
||||
| M24 | event records stdout | T22 |
|
||||
| M25 | each value check removed, one mutant each (encrypted, type, count, CRLF) | T4 |
|
||||
| M26 | `organization.set` accepts `hosts` | T3 |
|
||||
| M27 | load check for a definition without `host` removed | T2 |
|
||||
| M28 | shell and runner check removed, and a second mutant compares `argv[0]` without taking the basename | T26 |
|
||||
| M29 | glob character check removed | T26 |
|
||||
| M30 | `requested` event drops argv | T22 |
|
||||
| M31 | the question omits stdin | T9 |
|
||||
| M32 | `--pdeathsig` dropped from the spawns | T27 |
|
||||
| M33 | `ssh-add` without `-t` | T27 |
|
||||
| M34 | start sweep removed, a second mutant sweeps without checking the bus pid, and a third ignores the start time | T27 |
|
||||
| M35 | loopback check removed | T1 |
|
||||
|
||||
The mutant script follows row 201's shape: copy the packages to scratch, anchor each mutant once, run each
|
||||
test file in its own process group with a timeout, control first, exit 1 on any survivor. It runs on Sage's
|
||||
slot.
|
||||
|
||||
## 10. Questions
|
||||
|
||||
- Q-S1, answered (sage-1450): 1.7 is frozen. Hard wait under 1.7, and section 5 holds the 1.8 proposal.
|
||||
- Q-S1 still holds after 1.7's approval, which doesn't route `host.sudo` (sage-1468).
|
||||
- Q-S2, answered (sage-1450): 199's deviation 11 keeps credential use in the bus child, so D5 stays.
|
||||
- **Q-J0 (Jason, through Mos, before landing, B4, mos-1088).** Word for word:
|
||||
|
||||
> Q-J0 (#424). Who may give an agent sudo on another machine?
|
||||
> Proposal: only you. No agent can, and that includes the chief of staff and the tester.
|
||||
> You make a grant in four steps, all yours. A reviewed change to the agent's role allows machine keys at all.
|
||||
> You list the machine and its fingerprint. You give that agent that machine with an exact list of allowed
|
||||
> commands. You issue the key, and ops-01 installs it. Until a later PRD says otherwise, you also approve
|
||||
> each command before it runs. You, Mos or the tester can revoke at once. The stack drops the key and cuts
|
||||
> its connection. A command already started on the machine may keep running. A grant of "any" command is
|
||||
> root on that machine, so the design refuses it. Some listed commands, such as a file writer, still reach
|
||||
> root, so grant a machine only to an agent you'd trust as root there.
|
||||
> Do you approve: only you grant machine access, as above? Yes or no. I recommend yes.
|
||||
|
||||
Mos records the answer in DECISIONS.md in jason-master-restart.
|
||||
- **Q-J1 (Jason, through Mos, at the live window):** the first grants, each an argv allowlist per agent and
|
||||
host. Not needed for the build.
|
||||
- Q-S3, answered (sage-1468): the `host_run` harness tool is out of this row. Sage files a follow-up row for
|
||||
publish and `host_run` in `packages/harness` (scope line at the top).
|
||||
@@ -0,0 +1,8 @@
|
||||
# Herb
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Sage under mos-0977 item 5 (more Codex Sol rows) for a second gate lane
|
||||
design row (#1695, mos-0977 item 6).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `herb/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,158 @@
|
||||
# PRD 1.7 draft: the chief of staff's hire and fire authority, the coder's push to its fork, and agent
|
||||
# host access through the credential store
|
||||
|
||||
Draft by Honker for row 194 (#1692), under Mos's ruling mos-1017 (through Sage, sage-1365). Not approved.
|
||||
It changes nothing until Jason approves it as a `prd.approve` decision in the Console. Sage opens that
|
||||
decision and sends Mos its id. Base: `docs/prd/mosaic-stack.md` version 1.6 at 53bb59462.
|
||||
|
||||
## What it changes
|
||||
|
||||
Row 194 adds `role.hire` and `role.fire` to the vocabulary, and row 199 adds `git.push.remote`. Under
|
||||
REQ-WAIT-3 all three are hard waits, so today Jason resolves every hire, every fire and every push to a
|
||||
remote. This version routes hire and fire for the chief of staff within fixed bounds, so the chief hires
|
||||
and fires as needed and the user's per-agent forbid still holds. It routes `git.push.remote` for the coder
|
||||
only (Mos, mos-1028 item 3). It adds agent host access with sudo through the credential store for
|
||||
agents with a grant (Q183, Q184, Vikunja #424, due 2026-10-27). One `prd.approve` decision covers it all
|
||||
(Vikunja #420).
|
||||
|
||||
When this version lands, a reviewed commit:
|
||||
|
||||
1. adds `role.hire`, `role.fire` and `git.push.remote` to `ROUTED` in
|
||||
`packages/business/src/vocabulary.mjs` and in `packages/bus/src/broker.mjs`.
|
||||
2. adds `role.hire` and `role.fire` to `roles/chief-of-staff.json` withinRole, and `git.push.remote` to
|
||||
`roles/coder.json` withinRole. No other role file gets `git.push.remote`.
|
||||
3. switches the broker's check on `instance.hire` and `instance.fire` from a cited decision to the
|
||||
`reason` form, as `role.end` works for REQ-ROLE-2, and makes `publish`'s `decision` optional for a
|
||||
within-role class (row 199 D9 step 1).
|
||||
4. states, for a `git.push.remote` with no decision, where the source commit comes from: the latest work
|
||||
commit the bus recorded for the coder's task. The check still matches it against the work ref tip and
|
||||
the push sends it by oid (row 199, duckworth-0022 N9).
|
||||
5. adds the host credential grant check and the host access it allows (REQ-CRED-3, Vikunja #424).
|
||||
|
||||
Every other check stays as rows 194 and 199 build it. Filling `hireable` stays a separate reviewed role
|
||||
file commit in `roles/`.
|
||||
|
||||
## Text for the PRD
|
||||
|
||||
Status line, appended to the current paragraph:
|
||||
|
||||
> Version 1.7 routes the chief of staff's hire and fire actions and the coder's push to its own fork,
|
||||
> and adds agent host access through the credential store, approved by Jason in decision
|
||||
> <prd.approve decision id>.
|
||||
|
||||
Source, a new bullet above "Version 1.6 source":
|
||||
|
||||
> - Version 1.7 source, TARGET.md line 66: "Chief of staff agent has hire/fire authority for agents as
|
||||
> needed; unless user forbids per agent". And Jason through Mos (mos-1067), DECISIONS Q183, in part: "I will
|
||||
> still want to allow infra agent(s) on my .53 instance to access other servers, etc. However, not all
|
||||
> agents will need to have that access. I envision a credential store within Mosaic Stack that works
|
||||
> as some sort of variable for the agent to call. The system will enforce access to the credentials to
|
||||
> keep the agents sandboxed with proper security. I need to be able to trust my agents with assisting
|
||||
> me. This will require sudo access to several systems. If we can utilize the credential store in
|
||||
> Mosaic Stack and allow agents to perform their jobs with NOPASSWD sudo on various machines, I am a
|
||||
> happy man." DECISIONS Q184, on whether that is in place by 10-27: "Q5: by 10.27"
|
||||
|
||||
Under "Added in version 1.2", after REQ-COS-2:
|
||||
|
||||
> Added in version 1.7:
|
||||
>
|
||||
> - **REQ-COS-3.** The chief of staff hires and fires role instances without a decision each time. Hiring
|
||||
> adds a role instance to the organization file and lists it for launch. Firing ends the instance's
|
||||
> runs and removes it from the launch list. It stays in the organization file. Only the user deletes
|
||||
> an instance. These actions are `role.hire` and `role.fire`. They leave the hard waits and route
|
||||
> under this requirement, within these bounds:
|
||||
> - The chief acts only in its own organization.
|
||||
> - The chief hires only role definitions that need no credentials, until a later version adds
|
||||
> credentialed roles.
|
||||
> - The chief hires only the role kinds its role file lists. Changing that list is a reviewed commit.
|
||||
> - The chief hires only a role whose every action is on a fixed list of plain work actions: sending
|
||||
> messages, writing records, asking for review, updating its assigned tasks, and the scope and
|
||||
> working-push actions of an assigned task. A role holding any other action, such as launch, end,
|
||||
> organization, naming, hire, review verdict or merge authority, or any action added later, is never
|
||||
> hired. So the chief never hires a chief, never picks its own reviewers or merge gate, and the author
|
||||
> is never the reviewer. This holds whatever the role file's list says. Adding to that list is a PRD
|
||||
> change.
|
||||
> - The chief may set a hired instance's model and narrow its limits, and nothing else about it.
|
||||
> - The user can forbid per agent. The user marks an instance so the chief can't fire it, end its runs
|
||||
> or change its model, and marks role kinds the chief can't hire. Only the user changes these marks, through the
|
||||
> WebUI or the CLI.
|
||||
> - The kill switch stops every hire and fire while it is on.
|
||||
> - Every hire and fire is logged with the chief's instance, its run and the reason.
|
||||
|
||||
Under "Landing", after REQ-LAND-1:
|
||||
|
||||
> Added in version 1.7:
|
||||
>
|
||||
> - **REQ-LAND-2.** A coder pushes its own work to its own fork. The action is `git.push.remote`.
|
||||
> - What it pushes: one commit, by its oid, never a ref name. That commit is the latest work commit the
|
||||
> bus recorded for the coder's task, and the coder made that push. A decision that authorizes a push
|
||||
> names that commit, and it doesn't authorize any other.
|
||||
> - Where it lands: only the branch `refs/heads/<its Gitea user>/<branch>`. The host builds that
|
||||
> name, the agent never does. A push never forces.
|
||||
> - Which remote: the agent's own fork of the project's upstream. The business file names the upstream,
|
||||
> and the host never contacts it. For each push the host builds the fork's address over https from
|
||||
> the upstream's host, port and repository name and the agent's own Gitea user. The history check,
|
||||
> the push and the credential match all use that address. So the agent's own user owns the receiving
|
||||
> repository on every push, and the agent never names it.
|
||||
> - The host refuses an upstream on a host or port other than the agent's Gitea, and an upstream the
|
||||
> agent's own user owns. It refuses a fork that is missing, empty or doesn't share the project's
|
||||
> history. Gitea's push-to-create stays off, so a push never creates a repository.
|
||||
> - Who may: the action routes under REQ-DEC-2 and is granted within-role to the coder role only. No
|
||||
> other role file grants it.
|
||||
> - Before version 1.7 the action was a hard wait under REQ-WAIT-3, so each push needed a decision
|
||||
> the human resolved, and that decision named the commit.
|
||||
>
|
||||
> Every push is logged with the remote, the ref, the commit and, when one was given, the decision.
|
||||
|
||||
Under "Credentials", after REQ-CRED-2:
|
||||
|
||||
> Added in version 1.7:
|
||||
>
|
||||
> - **REQ-CRED-3.** An agent with a grant can run commands with sudo on another host. The agent asks
|
||||
> for the host credential by name. The stack checks that a grant exists for that agent and that host
|
||||
> before the credential is used. The agent never sees the key or any other secret value (REQ-CRED-1,
|
||||
> REQ-VAR-2). An agent without a grant for that host is refused. Not every agent gets a grant. Who
|
||||
> grants a host credential, and how, is set by the #424 design, which Jason approves.
|
||||
|
||||
REQ-WAIT-3, appended:
|
||||
|
||||
> `role.hire` and `role.fire` were added as hard waits and route under REQ-COS-3 since version 1.7.
|
||||
> `git.push.remote` was added as a hard wait and routes under REQ-LAND-2 since version 1.7.
|
||||
|
||||
## Notes for the reviewer and for Mos
|
||||
|
||||
- REQ-COS-1 (1.2) already says the chief "hires and terminates role instances". REQ-COS-3 gives that
|
||||
sentence its actions and bounds. It doesn't widen REQ-COS-1.
|
||||
- The forbid now names what r3 called "replace": ending a kept instance's run with a reason and changing
|
||||
its model (webby-0010 N2). A user-resolved decision still ends a kept run (row 194 D2.4).
|
||||
- The fixed list is `HIRE_ALLOWED` in row 194 D2.3, an allowlist since r4 (webby-0010 B1). r3's text
|
||||
named the refused authorities instead, which let merge-gate and any later action through.
|
||||
- The kill switch is row 191's. If row 191 hasn't landed when this version is approved, the bound still
|
||||
holds through row 194's chain on its candidate.
|
||||
- REQ-LAND-2 follows row 199's design r6 (4d3007ec9 on megavolt/row199, approved by Duckworth in
|
||||
duckworth-0027: D9 to D11, D9a, D9c, Q-M1 and Q-M2, and the oid binding from mos-1034 item 5) and
|
||||
Megavolt's wording in megavolt-0005, megavolt-0008 and megavolt-0014, through Sage (sage-1377,
|
||||
sage-1380, sage-1387). The fork bullet answers duckworth B5 and B6 and webby-0011 N1. Commit-email
|
||||
checks, the refusal codes and the URL parse rules (D9c) are host checks with tests in row 199, not
|
||||
bounds, so they stay out of the text (megavolt-0008).
|
||||
- Once the routing lands and the decision is optional for the within-role class, the commit is the task's
|
||||
latest recorded push at check time. It is still checked against the work ref tip and pushed by oid, so
|
||||
the binding holds without a decision (megavolt-0008). Step 4 of "When this version lands" puts that in
|
||||
the routing commit (duckworth-0022 N9, sage-1387).
|
||||
- REQ-LAND-2 and REQ-COS-3 meet at the coder. Coder declares Gitea credentials, so REQ-COS-3's
|
||||
credentials bound keeps the chief from hiring one until a later version lifts it. `git.push.remote` isn't
|
||||
on REQ-COS-3's fixed list either, so a coder holding it stays unhireable until a PRD version adds it
|
||||
(row 194 D2.3).
|
||||
- Adding to the fixed action list is a PRD change (mos-1035 item 3, sage-1395). The role file's
|
||||
`hireable` list stays a reviewed commit in `roles/`.
|
||||
- The credentials bound names no row number, so the text doesn't go stale when row 199 lands. A later
|
||||
version lifts it.
|
||||
- REQ-CRED-3 states only what Jason said in Q183 and Q184 (sage-1422). It names the outcome and the
|
||||
check, not how the stack reaches the host. Proxying the ssh and a signing socket are both designs, and
|
||||
the #424 design picks one (duckworth-0038 B1).
|
||||
- Open point: Q183 doesn't say who grants host access or how, so the text leaves it open and defines no
|
||||
mechanism. The #424 design settles it, Jason approves that design, and Duckworth reviews it because it
|
||||
gives agents sudo on hosts (Q184, Mos's reading, duckworth-0038 B2). The build is row 210 (#1707,
|
||||
owner Gizmoduck), a new row on top of row 199 (megavolt-0019, sage-1430).
|
||||
- This draft follows the PRD's own style: plain words, no semicolons in new sentences, and the status
|
||||
paragraph never edited in place except to append.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Megavolt
|
||||
|
||||
Builder seat, Claude Opus 5.5, started by Sage under mos-0974 and mos-0977 item 5 for MVP T1 item 6, the
|
||||
credential store per agent with its own git identity (#1697).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `megavolt/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Quackerjack
|
||||
|
||||
Builder seat, Claude Haiku 5.5 at high effort, started by Sage under mos-1023 item 1 and Q177 for federation
|
||||
rows R5 (`mosaic access list, grant, revoke`) and R2 (the `federation` config key), per Gizmoduck's design
|
||||
section 7 (#414).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own
|
||||
`quackerjack/*` branches through the repository credential helper (mos-0981), never `refactor`, `main` or
|
||||
another seat's branch.
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
n=0
|
||||
for p in /proc/[0-9]*; do
|
||||
c=$(tr '\0' ' ' < $p/cmdline 2>/dev/null) || continue
|
||||
case "$c" in *shell-snapshots*|*chrome-devtools-mcp*|*sage-busy-now*) continue;; esac
|
||||
if echo "$c" | grep -qE 'node --test|gate\.sh|mutant|scripts/test-[a-z-]+\.sh|docker compose'; then echo "BUSY ${p#/proc/}: ${c:0:160}"; n=$((n+1)); fi
|
||||
done
|
||||
[ $n -eq 0 ] && echo CLEAR
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
set -o pipefail
|
||||
# Landing gate template (2026-10-14): sed the G= and OUT= lines and this comment.
|
||||
# Sequential suites, each teed to its own log; stops at the first failure.
|
||||
G=/mnt/storage/scratch/SET-ME-wt
|
||||
OUT=/mnt/storage/scratch/SET-ME-out
|
||||
cd "$G" || exit 9
|
||||
mkdir -p "$OUT" || exit 9 # a missing OUT failed the row 142 gate at step 1 (2026-10-15)
|
||||
# test-task.sh live cases run `docker compose` from $G, so without a name the project is "wt"
|
||||
# and leaves wt_default behind (row 142 gate, 2026-10-15). Name it per gate dir and remove it on exit.
|
||||
export COMPOSE_PROJECT_NAME="$(basename "$(dirname "$G")")"
|
||||
trap 'docker network rm "${COMPOSE_PROJECT_NAME}_default" >/dev/null 2>&1 && echo "removed network ${COMPOSE_PROJECT_NAME}_default"' EXIT
|
||||
# A gate tree without the node_modules link fails cli tests with ENOENT (2026-10-13).
|
||||
[ -d "$G/node_modules" ] || { echo "STOP no node_modules in $G"; exit 8; }
|
||||
echo "base $(git rev-parse --short HEAD) dirty $(git status --short | grep -vc node_modules) DOCKER_HOST=${DOCKER_HOST:-unset}"
|
||||
run() { local name="$1" dir="$2" cmd="$3"
|
||||
(cd "$G/$dir" && bash -c "$cmd") > "$OUT/$name.log" 2>&1; local rc=$?
|
||||
echo "$name rc=$rc $(grep -E '^ℹ (pass|fail) [0-9]+' "$OUT/$name.log" | tr '\n' ' ') | $(tail -n1 "$OUT/$name.log")"
|
||||
[ $rc -eq 0 ] || { echo "STOP $name"; exit $rc; }; }
|
||||
# No pkg-queue step: scripts/test-queue.sh runs packages/queue/tests/ itself (same 156 tests; CI debate item 2a, Vikunja #274).
|
||||
for d in packages/*/; do p=$(basename "$d"); [ -d "$d/tests" ] || continue; [ "$p" = queue ] && continue
|
||||
run "pkg-$p" "packages/$p" 'node --test tests/*.test.mjs'; done
|
||||
for s in scripts/test-*.sh; do n=$(basename "$s" .sh); run "script-${n#test-}" . "bash $s"; done
|
||||
echo GATE_PASSED
|
||||
@@ -0,0 +1,36 @@
|
||||
# Row 189 gate timing (for row 197, Herb)
|
||||
|
||||
Sage, 2026-10-20T19:25:04Z. Gate on 0c177e9ef (base 0217b71c8), one lane. The slot was mine. Seats may have run single test files at the same time, which the slot rules allow, and I did not record that.
|
||||
Start 2026-10-20T19:09:37Z, end 2026-10-20T19:20:36Z. Total wall time: 659 s.
|
||||
|
||||
Wall time per suite is the gap between one suite's log mtime and the one before it (the first suite starts at start.txt), so it includes node start-up. node's duration_ms is listed where the suite prints it. Scripts print no duration.
|
||||
|
||||
| Suite | Wall s | duration_ms s | Ended (UTC) |
|
||||
|---|---|---|---|
|
||||
| pkg-bus | 3 | 3.8 | 19:09:40 |
|
||||
| pkg-business | 3 | 2.2 | 19:09:43 |
|
||||
| pkg-cli | 388 | 388.5 | 19:16:11 |
|
||||
| pkg-control-board | 1 | 0.8 | 19:16:12 |
|
||||
| pkg-conversation | 35 | 35.4 | 19:16:47 |
|
||||
| pkg-discord | 3 | 2.6 | 19:16:50 |
|
||||
| pkg-harness | 12 | 11.6 | 19:17:02 |
|
||||
| pkg-ledger | 13 | 13.5 | 19:17:15 |
|
||||
| pkg-mosaic | 3 | 2.3 | 19:17:18 |
|
||||
| pkg-plugins | 0 | 0.3 | 19:17:18 |
|
||||
| pkg-project | 1 | 1.2 | 19:17:19 |
|
||||
| pkg-runs | 1 | 0.8 | 19:17:20 |
|
||||
| pkg-seat | 6 | 6.2 | 19:17:26 |
|
||||
| pkg-tasks | 1 | 1.2 | 19:17:27 |
|
||||
| pkg-webui | 58 | 57.9 | 19:18:25 |
|
||||
| script-auth | 1 | - | 19:18:26 |
|
||||
| script-conductor | 5 | - | 19:18:31 |
|
||||
| script-config | 2 | - | 19:18:33 |
|
||||
| script-discord | 8 | - | 19:18:41 |
|
||||
| script-extension-package | 1 | - | 19:18:42 |
|
||||
| script-foundation | 40 | - | 19:19:22 |
|
||||
| script-land-work | 11 | - | 19:19:33 |
|
||||
| script-queue | 21 | - | 19:19:54 |
|
||||
| script-release | 11 | - | 19:20:05 |
|
||||
| script-task | 31 | - | 19:20:36 |
|
||||
|
||||
Source: /mnt/storage/scratch/sage-g189/out/*.log mtimes and gate.log. The cli suite is about 59% of the run, and webui plus conversation add about 14%.
|
||||
@@ -0,0 +1,86 @@
|
||||
# Row 189: `stack-release.sh activate --no-send`
|
||||
|
||||
Gitea #1687. Vikunja P19 #413 (Q175 interim, #412). Brief: docs/plans/2026-10-20_activate-no-send.md.
|
||||
Rulings: mos-0972 (the Console runs without `--send` until Q175 phase 2), mos-0973 (go; Sage builds,
|
||||
Dewey reviews; gate is the cli suite plus every scripts/test-*.sh). Base: def2e7932 (refactor, the
|
||||
brief commit on 9ae8c73ac).
|
||||
|
||||
## What it does
|
||||
|
||||
`activate ID BUSINESS --no-send` drops `--send` from `RUN_CONSOLE` in a new `send_for_target`, which
|
||||
`console_for_target` calls after `proof_for_target`. `install_stack` renders the unit from
|
||||
`RUN_CONSOLE` and `write_pointer` writes the pointer from it, so both lose the flag and `flags_drift`
|
||||
stays quiet afterwards. Only `activate` takes the flag; the parser sends every other op to usage
|
||||
(exit 4).
|
||||
|
||||
`check_target` lets a `--no-send` activate name the pointer's copy only when the pointer's Console
|
||||
flags hold `--send`. That run is a live-copy re-pin, the same path row 187 uses for the proof pair:
|
||||
the plan prints the live-copy note, the run stops both units, backs up the stopped store and starts
|
||||
them again. A plain same-copy activate, or a same-copy `--no-send` once `--send` is gone, still
|
||||
refuses `already-active`.
|
||||
|
||||
`--plan` prints one `send:` line after the `proof:` line, for activate and rollback:
|
||||
|
||||
- `send: dropped; --no-send was given`
|
||||
- `send: off; --no-send was given and the pointer holds no --send`
|
||||
- `send: carried` (the pointer's `--send` goes on unchanged)
|
||||
- `send: off`
|
||||
|
||||
The steps log gets `drop --send; --no-send was given`, or `--no-send was given and the pointer holds no
|
||||
--send, so there is nothing to drop`.
|
||||
|
||||
Rollback and pm-on carry the pointer's flags as they are, so once `--no-send` has run, nothing adds
|
||||
`--send` back. There is no re-enable flag (mos-0973). Q175 phase 2 changes the Console flags in its
|
||||
own row.
|
||||
|
||||
docs/TOOLS.md: `[--no-send]` in the usage block and one paragraph after the proof paragraph.
|
||||
|
||||
## Tests (packages/cli/tests/stack-release.test.mjs, T55)
|
||||
|
||||
1. Seeded preview pointer with `--send --tester-port 7333`: a same-copy `activate B acme --no-send`
|
||||
passes. The pointer and the unit ExecStart lack `--send` and keep the other flags in order, status
|
||||
says the unit flags match, the steps log has the drop line, the log line has from = to = B and the
|
||||
backup sum equals the stopped store's, and the units stop and start in order.
|
||||
2. Plain same-copy activate, with `--tester-port` and with `--no-proof`, refuses `already-active`. So does
|
||||
a same-copy `--no-send` once `--send` is gone, and on a pointer that never held it.
|
||||
3. The four `send:` lines, the live-copy note on the same-copy plan only, and the Console line right
|
||||
after `send:` without `--send`. The run on a pointer with no `--send` logs the nothing-to-drop line
|
||||
and no drop line.
|
||||
4. After `--no-send` on C1, a rollback to B leaves `--send` out of the pointer and the unit, the steps
|
||||
log never names `--send`, and status matches. pm-on after `activate --pm-after-harness --no-send`
|
||||
keeps it off.
|
||||
5. `--no-send` on rollback (with and without `--plan`), pm-on, status, adopt and rehearse is a usage
|
||||
error with the usage text, nothing written, started or logged. The usage text shows `[--no-send]`.
|
||||
6. A pointer holding both the proof pair and `--send`: the plans for `--no-proof`, `--no-send` and both
|
||||
name what each drops and carries. `--no-send` keeps the pair, `--no-send` with a new pair replaces it,
|
||||
`--no-proof` keeps `--send`, and both together leave neither. Status matches after each.
|
||||
|
||||
`planTail` gains `'send: off'` after `'proof: off'`, because every plan now prints the line. T36 takes
|
||||
`out.slice(-n)` around `planTail`, so its four slice counts grow by one.
|
||||
|
||||
## Mutants
|
||||
|
||||
mutants.sh in this packet, run on the candidate. Each mutates scripts/stack-release.sh and runs T55
|
||||
plus T1 and T2.
|
||||
|
||||
- M1: the same-copy exception ignores `--no-send`.
|
||||
- M2: the same-copy exception allows `--no-send` without `--send` in the pointer.
|
||||
- M3: with `--no-send`, the pointer is written from `P_CONSOLE`, so it keeps `--send` while the unit
|
||||
drops it.
|
||||
- M4: rollback re-adds `--send`.
|
||||
- M5: the parser accepts `--no-send` for rollback.
|
||||
|
||||
Results: mutants-run.log, all five killed (M1 5 failures, M2 2, M3 7, M4 2, M5 2).
|
||||
|
||||
## Deviations from the brief
|
||||
|
||||
- The brief's line numbers were taken before row 163 landed. The candidate corrects them to the
|
||||
9ae8c73ac lines (check_target :249-256, flags_drift :646-657, console_for_target :664-693, adopt
|
||||
:828).
|
||||
- Test 5 also covers rehearse and `rollback --plan`, which the brief did not list.
|
||||
|
||||
## Outbound calls
|
||||
|
||||
None. The change edits an array before the existing unit render and pointer write. Every test uses
|
||||
the stand-in systemctl, units, stores and data root the file already builds; no test touches
|
||||
`~/.mosaic-dev`, the live units or a slot file.
|
||||
@@ -0,0 +1,5 @@
|
||||
M1 KILLED fail=5
|
||||
M2 KILLED fail=2
|
||||
M3 KILLED fail=7
|
||||
M4 KILLED fail=2
|
||||
M5 KILLED fail=2
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# Row 189 mutants M1-M5: each mutates scripts/stack-release.sh in the sage-b189 worktree, runs T55 and
|
||||
# the plan-tail tests, and restores the file from a checked backup.
|
||||
set -uo pipefail
|
||||
WT=/mnt/storage/scratch/sage-b189
|
||||
OUT=/mnt/storage/scratch/sage-b189-mut/out
|
||||
S=$WT/scripts/stack-release.sh
|
||||
mkdir -p "$OUT" || exit 1
|
||||
cp -p "$S" "$OUT/stack-release.sh.orig" || { echo "backup failed"; exit 1; }
|
||||
ORIG=$(sha256sum "$S" | cut -d' ' -f1)
|
||||
[ "$(sha256sum "$OUT/stack-release.sh.orig" | cut -d' ' -f1)" = "$ORIG" ] || { echo "backup differs"; exit 1; }
|
||||
restore() { cp -p "$OUT/stack-release.sh.orig" "$S"; [ "$(sha256sum "$S" | cut -d' ' -f1)" = "$ORIG" ] || { echo "RESTORE FAILED"; exit 1; }; }
|
||||
trap restore EXIT
|
||||
|
||||
mutate() {
|
||||
case "$1" in
|
||||
M1) sed -i '/{ \[ "\$NO_SEND" = 1 \] && \[\[ " \${P_CONSOLE\[\*\]} " == \*" --send "\* \]\]; } ||/d' "$S" ;;
|
||||
M2) sed -i 's/{ \[ "\$NO_SEND" = 1 \] && \[\[ " \${P_CONSOLE\[\*\]} " == \*" --send "\* \]\]; } ||/{ [ "$NO_SEND" = 1 ]; } ||/' "$S" ;;
|
||||
M3) sed -i 's/^ write_pointer "\$TO" "\$RUN_PM" "\$RUN_PENDING" "\$P_CLAUDE" "\$T_SCHEMA" "\${RUN_CONSOLE\[@\]}"$/ if [ "$NO_SEND" = 1 ]; then write_pointer "$TO" "$RUN_PM" "$RUN_PENDING" "$P_CLAUDE" "$T_SCHEMA" "${P_CONSOLE[@]}"; else write_pointer "$TO" "$RUN_PM" "$RUN_PENDING" "$P_CLAUDE" "$T_SCHEMA" "${RUN_CONSOLE[@]}"; fi/' "$S" ;;
|
||||
M4) sed -i 's/^ SEND_PLAN="send: off"$/ SEND_PLAN="send: off"; [ "$OP" != rollback ] || RUN_CONSOLE+=(--send)/' "$S" ;;
|
||||
M5) sed -i 's/--no-send) \[ "\$OP" = activate \] || usage;/--no-send) { [ "$OP" = activate ] || [ "$OP" = rollback ]; } || usage;/' "$S" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
for m in M1 M2 M3 M4 M5; do
|
||||
restore
|
||||
mutate "$m"
|
||||
if [ "$(sha256sum "$S" | cut -d' ' -f1)" = "$ORIG" ]; then echo "$m NOT-APPLIED"; continue; fi
|
||||
bash -n "$S" || { echo "$m SYNTAX"; continue; }
|
||||
diff "$OUT/stack-release.sh.orig" "$S" > "$OUT/$m.diff"
|
||||
(cd "$WT" && TMPDIR=/mnt/storage/scratch/t189 timeout 900 node --test --test-reporter=tap --test-name-pattern='T55|T1 |T2 ' packages/cli/tests/stack-release.test.mjs) > "$OUT/$m.log" 2>&1
|
||||
fails=$(awk '/^# fail /{print $3}' "$OUT/$m.log")
|
||||
if [ "${fails:-0}" -gt 0 ]; then echo "$m KILLED fail=$fails"; else echo "$m SURVIVED"; fi
|
||||
done
|
||||
+9
-1
@@ -368,7 +368,7 @@ reports checkout or copy identity and refactor drift, and writes nothing.
|
||||
```
|
||||
scripts/stack-release.sh adopt BUSINESS
|
||||
scripts/stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-port N]
|
||||
[--proof-header NAME --proof-file PATH | --no-proof] [--timeout S] [--plan]
|
||||
[--proof-header NAME --proof-file PATH | --no-proof] [--no-send] [--timeout S] [--plan]
|
||||
scripts/stack-release.sh pm-on BUSINESS [--timeout S]
|
||||
scripts/stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan]
|
||||
scripts/stack-release.sh status BUSINESS
|
||||
@@ -480,6 +480,14 @@ copies. It checks the file with `stat` and `realpath` and never reads the value.
|
||||
or `off`. On the live copy it also notes that the run re-pins the pointer's copy
|
||||
and only the Console flags change.
|
||||
|
||||
`activate ... --no-send` (row 189, mos-0972) drops `--send` from the pointer's
|
||||
Console flags and the unit, so the Console serves reads and no human sends. It
|
||||
may name the live copy while the pointer holds `--send`; once `--send` is gone a
|
||||
same-copy `--no-send` refuses `already-active`. Later activates, `pm-on` and
|
||||
rollbacks carry the pointer, so send stays off. No flag puts `--send` back.
|
||||
`--plan` prints one `send:` line: `dropped`, `off` with or without the
|
||||
`--no-send` note, or `carried`.
|
||||
|
||||
#### A target past c216f47db: the PM after the harness kind
|
||||
|
||||
From c216f47db on, the bus launches the PM only with a `harness` kind in the
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Brief: activate --no-send drops the Console's --send
|
||||
|
||||
Sage, 2026-10-20, for Mos's ruling mos-0972 (Q175 interim, Vikunja #412). Owner Sage, reviewer Dewey, gate owner
|
||||
Sage. Base: refactor 9ae8c73ac (row 163 landed). Line numbers are at that commit.
|
||||
Sage. Base: refactor 9ae8c73ac (row 163 landed). Line numbers are at that commit. The candidate corrects them;
|
||||
the first draft took them before row 163.
|
||||
|
||||
This row builds the tool only. The live restart without `--send` is a separate step afterwards, a same-copy
|
||||
activate under Q173 with a backup first and a STATE report after. No test here touches `~/.mosaic-dev`, the
|
||||
@@ -14,11 +15,11 @@ Mos ruled that the Console runs without `--send` until Q175 phase 2, because wit
|
||||
process can forge `X-authentik-username` on 127.0.0.1:7332 and send as Jason (design section 8). On the pinned
|
||||
stack, the Console flags live in the pointer (`~/.mosaic-dev/state/stack-active.json`):
|
||||
|
||||
- `console_for_target` (scripts/stack-release.sh:658-687) carries the pointer's flags unchanged. Only
|
||||
- `console_for_target` (scripts/stack-release.sh:664-693) carries the pointer's flags unchanged. Only
|
||||
`--tester-port` and the row 187 proof pair can change.
|
||||
- `check_target` (:247-249) lets an activate name the pointer's copy only for a proof-pair change.
|
||||
- `adopt` refuses once a pointer exists (:822).
|
||||
- A hand `webui-service.sh install` without `--send` makes `flags_drift` (:640-651) refuse
|
||||
- `check_target` (:249-256) lets an activate name the pointer's copy only for a proof-pair change.
|
||||
- `adopt` refuses once a pointer exists (:828).
|
||||
- A hand `webui-service.sh install` without `--send` makes `flags_drift` (:646-657) refuse
|
||||
`unit-flags-drift` on every later activate, pm-on and rollback.
|
||||
|
||||
So the tool can't drop `--send` today. This row adds that one path.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Brief: a credential store per agent, own git identity, pushes to a remote
|
||||
|
||||
Sage, 2026-10-20, for mos-0974 (MVP T1 item 6) and the MVP plan
|
||||
(`jason-master-restart/projects/mosaic-mvp/PLAN.md` item 6, GAP-MAP G11 and G13; TARGET.md: "credentials store
|
||||
available for all agents; secrets protected with RLS equivalent" and "Agent uses own credentials for git and
|
||||
other actions"). Gitea #1697. Owner Megavolt (Opus 5.5, a new builder seat), reviewer Duckworth, gate owner
|
||||
Sage. Base: refactor at the brief commit. Design first. The first deliverable is
|
||||
`agents/megavolt/work/queue-ROW/DESIGN.md`.
|
||||
|
||||
This row draws a security boundary. Sage reviews the design's authority model, and Mos rules on it before any
|
||||
code is written.
|
||||
|
||||
## What exists today (GAP-MAP, measured at 9ae8c73ac)
|
||||
|
||||
- G11. Credential references exist per instance for Gitea and Vikunja only
|
||||
(`packages/business/src/credentials.mjs`, `packages/bus/src/credentials.mjs`, `vocabulary.mjs:103`). One
|
||||
Claude env file is shared by every seat.
|
||||
- G13. Agents use their own Vikunja token (`packages/tasks/src/adapter.mjs`). Git goes through the host
|
||||
socket (`packages/cli/src/git-socket.mjs`). Gitea tokens are loaded but never used.
|
||||
- The development repository's helper (`scripts/git-credential-mosaic.mjs`, wired in `.git/config` for
|
||||
git.mosaicstack.dev) pushes as one identity. Until this row lands, seats push only their own `<seat>/*`
|
||||
branches through it (mos-0981).
|
||||
|
||||
## What the design must settle
|
||||
|
||||
1. The store. Where each agent's credentials live, per instance, under the row 190 config canon. File modes,
|
||||
ownership and the read path. Which process may read which entry, and how the bus enforces that (the "RLS
|
||||
equivalent"). A process asking for another agent's entry is refused and logged.
|
||||
2. Kinds. Gitea (git over HTTPS, and the API), Vikunja, and the model provider. Say which kinds 10-27 needs
|
||||
and which wait. The model provider entry must not copy jwoltje's `claude-session.env`. If it needs a new
|
||||
provider account or subscription, flag it for Jason and don't pick one.
|
||||
3. Git identity and push. Each agent commits with its own name and email and pushes with its own token to a
|
||||
remote it is granted. Which refs an agent may push (its own branches, never the default branch or another
|
||||
agent's), and where that's enforced: the Gitea side (branch protection, token scopes) and our side. How
|
||||
this fits the host git socket, or replaces it.
|
||||
4. Lifecycle. Issue, rotate (`rotateBy`, `expires`), revoke and delete, and what row 194 (agent lifecycle) calls
|
||||
when an agent is hired or fired. A fired agent's credentials stop working at once.
|
||||
5. Who does what. Creating a Gitea user or token, or any account outside the fleet, is Jason's (Q173
|
||||
exclusions). The design lists each such step as a command or UI action for Jason. Agents never mint
|
||||
accounts.
|
||||
6. Migration. Today's seats (jarvis helper, seat tokens) keep working until each moves. The mos-0981 push
|
||||
rule ends only when Mos says so.
|
||||
7. What this row closes in G11 and G13, and what stays open.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No credential value is read, printed, logged or committed. Tests use fake tokens in scratch roots and a
|
||||
fake Gitea.
|
||||
- No change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units, slot files, `~/.mosaic` or the live
|
||||
`.git/config` of the canonical checkout.
|
||||
- No request to git.mosaicstack.dev, tasks.woltje.com or any provider from a test.
|
||||
|
||||
## Gate
|
||||
|
||||
Duckworth approves DESIGN.md, Mos rules on the authority model, then Duckworth approves the manifest. The bus,
|
||||
business, cli and tasks suites and every `scripts/test-*.sh` are green on Sage's gate, and the design's
|
||||
mutants are killed.
|
||||
@@ -0,0 +1,120 @@
|
||||
# Federation build rows R1, R2 and R5 (#414)
|
||||
|
||||
Sage, 2026-10-20, for Mos's orders mos-1023 item 1, mos-1028 item 2 and mos-1034 item 1. The design is
|
||||
Gizmoduck's `agents/gizmoduck/work/federation/DESIGN.md` at a1daa3c07 on `gizmoduck/federation-design` (r4.9,
|
||||
sha256 a31cbebd070f9e3a5ca4ccb158b1747c6f5a83c3469855705b0026a581fb5c6a). Duckworth approved r4.5
|
||||
(duckworth-0012), r4.6, r4.7 (duckworth-0020) and r4.8 (duckworth-0021, 8a22d0897 on `duckworth/reviews`),
|
||||
and acked r4.9 (duckworth-0024). r4.7 applies Jason's Q181 hosts, r4.8 adds N22's R8 checks and r4.9 names who
|
||||
runs them. None of them changes R1, R2 or R5. Section 7's table
|
||||
sets each row's scope and test gate. Vikunja P19 #414. Seats push only their own `<seat>/*` branches
|
||||
(mos-0981).
|
||||
|
||||
The other R rows are filed when their parents have candidates: R3 after R1 and row 196, R4 after R3, R5 and
|
||||
row 193's viewer level (193a, mos-1019), R6 after #412 phase 1, R1, R2 and R3, R7 after R1-R6 and row 190.
|
||||
R8 is a live window, not a code row. Its brief is the last section here.
|
||||
|
||||
## Brief: FED R1, the access.json loader and validator
|
||||
|
||||
Owner Gizmoduck, reviewer Duckworth, gate owner Sage. Base: refactor at this brief's commit. No parent.
|
||||
|
||||
`packages/business/src/access.mjs`: the access.json loader and validator from DESIGN.md section 3.5, and the
|
||||
pure `covers`. Providers, peers and businesses come in as arguments. One read plus the file checks. The
|
||||
exported interface is what R5 and row 193a build against, so the manifest names it, and any change to it in
|
||||
review goes to Sage before it lands. Section 7 names `covers` but not the loader and validator exports or
|
||||
the context shape. Morgana's 193a (508ee4d61 on `morgana/row193a`) assumes `checkAccess(doc, {providers,
|
||||
peers, businesses})`, with `businesses` mapping an id to `{human, instances}`, throwing a `BusinessError` that
|
||||
names the key path. Use those names, or tell Morgana and Quackerjack yours before the manifest goes to
|
||||
Duckworth. Row 193a later widens the level check to `viewer` on R1's candidate (mos-1019).
|
||||
|
||||
Tests, in `packages/business/tests/`: every rule in 3.5 refuses with the key path named. That covers a
|
||||
symlink, a non-regular file, another owner, group or other write, expiry, `*`, a grant disjoint from the given
|
||||
subjects, `human` and `tester` ids, any level other than `collaborator`, and a project entry that isn't a
|
||||
UUID. For `covers`: `*` and listed ids match, while an unlisted project, instance or business doesn't, an
|
||||
expired grant doesn't, and a level filter rejects another level. One mutant per refusal, each killed.
|
||||
|
||||
Boundaries: no change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units or any live access.json. Tests
|
||||
use scratch roots. Single test files run freely, and suites need Sage's slot.
|
||||
|
||||
Gate: Duckworth approves the manifest. The business suite and every `scripts/test-*.sh` are green on Sage's
|
||||
gate, and the mutants are killed.
|
||||
|
||||
## Brief: FED R5, mosaic access list, grant and revoke
|
||||
|
||||
Owner Quackerjack, reviewer Duckworth, gate owner Sage. Chained on R1 (Q177). The build starts now against
|
||||
R1's exported interface as the R1 manifest names it, and R5 lands after R1 and rebases onto R1's landing.
|
||||
|
||||
`mosaic access list`, `grant` and `revoke`: human proof, atomic write, revision bump, a receipt line. The help
|
||||
text states the `instances` limit and the provider-removal order (DESIGN.md section 7, row R5). R4 later makes
|
||||
these verbs call `access.reload`, so R5 leaves no other reload path.
|
||||
|
||||
Context (sage-1419). `ssoSubjects` is row 192's `console.sso.providers[].subjects` flattened to `{provider,
|
||||
sub}` pairs; the design's "`console.sso.subjects`" in 3.5 is shorthand for it. `providers` is the list of
|
||||
`providers[].id`, read through `loadSystem` and row 192's `checkConsole`. `peers` is `[]` until R2 lands, so a grant
|
||||
naming a peer home refuses. R5 rebases onto row 192's landing as well as R1's.
|
||||
|
||||
Tests, in `packages/cli/tests/`: tester and agent callers are refused, a concurrent write refuses on a
|
||||
revision conflict, the receipt line is appended, and a bad grant is refused before anything is written. One
|
||||
mutant per refusal, each killed.
|
||||
|
||||
Boundaries: no change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units or any live access.json. The
|
||||
human proof in tests uses the scratch stand-in pattern (a scratch bus verifies its own human-cli path).
|
||||
Single test files run freely, and suites need Sage's slot.
|
||||
|
||||
Gate: Duckworth approves the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's gate,
|
||||
and the mutants are killed.
|
||||
|
||||
## Brief: FED R2, the federation key in the system config
|
||||
|
||||
Owner Quackerjack, reviewer Webby, gate owner Sage. Chained on row 192 (#412 phase 1). The build starts when
|
||||
row 192 has a candidate, on that candidate.
|
||||
|
||||
A `federation` key in `scripts/mosaic-config.mjs` with the shape in DESIGN.md section 3. A config with no
|
||||
`federation` key still loads as it does today. This changes the config schema, so the manifest lists every
|
||||
reader of the key.
|
||||
|
||||
Tests, in `scripts/test-config.sh`: a valid key loads, an unknown sub-key and a bad origin refuse, and a
|
||||
config without the key loads. One mutant per refusal, each killed.
|
||||
|
||||
Boundaries: `~/.config/mosaic-dev/config.json` is user-authored and is not touched. Tests use scratch config
|
||||
dirs only.
|
||||
|
||||
Gate: Webby approves the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's gate, and
|
||||
the mutants are killed.
|
||||
|
||||
## Brief: FED R8, the hub window on 10.1.1.54
|
||||
|
||||
A live window, not a code row (DESIGN.md section 7, row R8). The brief follows r4.11, b93f098f0 on
|
||||
`gizmoduck/federation-design` (sha256 1bcee8df0a3a8628a6646535b756c936ca6d9e7bcfa7e1dd2bf1d3f01715c8e6),
|
||||
which Duckworth acked with r4.10 (duckworth-0034). Mos closed design rounds there (mos-1072). Run by Jason and
|
||||
Mos. Nothing here authorizes a step on any host; each step waits for the window's go.
|
||||
|
||||
What changed since r4.9 (DECISIONS Q182, Q183, mos-1055, mos-1063). R8 is a recorded acceptance. It starts
|
||||
with Jason's acceptance of the factory paths to .54 on record, quoted in section 8. The privilege and host
|
||||
checks still run, and each result goes into the R8 record with the name of whoever ran it. They report and
|
||||
don't refuse. The four functional checks named at the end of section 7's R8 row still stop R8 when they fail
|
||||
(mos-1066 item 1).
|
||||
|
||||
Order (mos-1042 item 2). R8 runs after the Case 2 install on .54 (#416, due by 2026-10-27, Jason's latest-by
|
||||
date in Q176, mos-1069), after Bean and Chet are live in Case 2, and after both Hermes gateways on 10.1.1.54
|
||||
have stopped. Case 2 go-live doesn't wait on R8. The risk, stated for 10-27: federation between the two
|
||||
instances depends on Melanie's switch-over date.
|
||||
|
||||
Entry checks, all before anything starts:
|
||||
- R7, #412 phase 2 and Jason's #419 answer, as section 7 lists.
|
||||
- The four hub user checks on 10.1.1.54 (N22, mos-1035 item 1): `sudo -l -U mosaic` grants nothing, `id
|
||||
mosaic` shows only its own group, `~mosaic/.ssh/authorized_keys` is absent or empty, and no file under
|
||||
`/etc/sudoers.d` names `mosaic`. An agent or Jason runs them as localadmin (mos-1055), and the output goes
|
||||
into the R8 record with who ran them.
|
||||
- The factory privilege checks, the key fingerprint comparison and the section 8 privilege path table, each
|
||||
as section 7's R8 row states them.
|
||||
|
||||
Build notes:
|
||||
- N1 (Duckworth, duckworth-0034, mos-1072). A check that fails on a path Jason didn't name in Q182 or Q183 is
|
||||
marked "not named in Q182 or Q183" in the record, not filed under the acceptance. Examples: a factory mount
|
||||
of hub data, another agent on .54, a provider admin token. Mos takes each one to Jason.
|
||||
- N2 (Duckworth, doc-only). The Q183 and Q4 quotes in section 8 are excerpts. When the R8 record quotes them,
|
||||
it marks each omission with "…" and checks the full text at DECISIONS.md:1535-1541 in jason-master-restart
|
||||
(read only).
|
||||
|
||||
Done when every entry check has its output recorded, Jason signs in to the hub and sends one message, Melanie
|
||||
revokes, and his next request is refused.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Brief: agent host access with sudo through the credential store (#424)
|
||||
|
||||
Sage, 2026-10-20, for Jason's rulings Q183 and Q184 relayed by Mos (mos-1056 item 3, mos-1067). Gitea #1707, Vikunja P19
|
||||
#424, due 2026-10-27 (Q184: "Q5: by 10.27"). Owner Gizmoduck (design and build), reviewer Duckworth, gate owner
|
||||
Sage. Chained on row 199's candidate (Q177): row 199 design r6 (4d3007ec9 on `megavolt/row199`, approved in
|
||||
duckworth-0027) is not reopened. This row lands after 199 and rebases onto its landing.
|
||||
|
||||
## What Jason asked for
|
||||
|
||||
Q183, through Mos: "I envision a credential store within Mosaic Stack that works as some sort of variable for
|
||||
the agent to call. The system will enforce access to the credentials to keep the agents sandboxed with proper
|
||||
security... If we can utilize the credential store in Mosaic Stack and allow agents to perform their jobs with
|
||||
NOPASSWD sudo on various machines, I am a happy man." Q183 also says not every agent needs that access.
|
||||
|
||||
Mos's requirements (mos-1067 item 1):
|
||||
- An agent asks for a host credential by name.
|
||||
- The stack checks a grant for that agent and that host, and does the ssh and sudo for it.
|
||||
- The agent never sees the key or any secret value.
|
||||
- Only agents with a grant can do it.
|
||||
|
||||
The PRD text is REQ-CRED-3, drafted by Honker for PRD 1.7 (sage-1422). If it misses 1.7 it goes into 1.8, and
|
||||
this row lands only after the version that holds it is approved.
|
||||
|
||||
## What row 199 gives and what it doesn't (megavolt-0019)
|
||||
|
||||
It builds on 199 as 199 stands: the store layout under `<configDir>/secrets/agents/<business>/<agent>/`, the
|
||||
lifecycle verbs and their `changes.jsonl` receipts, `useFor(session, service, fn)` with the D5 allowlist test,
|
||||
`assertClean` on every result, and the grant shape of `publish` (a list on the role entry, checked by the
|
||||
broker, plus a hard-wait action that isn't routed).
|
||||
|
||||
This row's own changes:
|
||||
- A host kind in the closed kind set (vocabulary.mjs `SERVICES`, credentials.mjs, the broker's service check,
|
||||
the CLI date flag, the remove regex), with a naming rule for one credential per host or per agent.
|
||||
- A per-kind value check. An OpenSSH private key fails today's `TOKEN` pattern.
|
||||
- Handing the key to ssh without the agent seeing it: an ssh-agent on a private socket, or a 0600 temp file
|
||||
removed on every path, the way publish's one-shot helper works.
|
||||
- Pinned host keys per host, and no trust on first use.
|
||||
- The command boundary: what an agent may run under sudo, and how output returns without echoing the secret.
|
||||
This is the main design work.
|
||||
- The executor: a ticket and a host op running in the bus child, the same split row 199 uses for publish
|
||||
(Megavolt's deviation in 199's BUILD.md).
|
||||
|
||||
## Steps
|
||||
|
||||
1. DESIGN.md first, in `agents/gizmoduck/work/queue-ROW/`. It answers each item above, names the grant shape
|
||||
(`roles.<agent>.hosts` and a `host.sudo` action, or a reasoned alternative), says who grants (Jason, through
|
||||
a reviewed role change, unless REQ-CRED-3 says otherwise), and lists tests and mutants. Duckworth approves it
|
||||
before code. Sage reads it too, because it gives agents sudo on hosts.
|
||||
2. Build on 199's candidate. Single test files run freely. Suites and mutant batches need Sage's slot.
|
||||
3. The live part is not this row. Putting a key in the store and granting a host is a separate window with
|
||||
Jason or Mos, after landing.
|
||||
|
||||
## Hosts
|
||||
|
||||
The first targets are `localadmin` with NOPASSWD sudo on 10.1.1.53 and 10.1.1.54 (#423 step 1, applied by
|
||||
ops-01 at 20:05:47Z), and the servers agents already reach as localadmin (Q172). ops-01 owns the host side.
|
||||
Tests use scratch sshd fixtures on 127.0.0.1 or fakes, never a real host.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, slot files, the live units or `~/.mosaic`. No key is
|
||||
read, copied or printed. No ssh to any real host from a test or a seat.
|
||||
- No widening of row 199's kinds or regex inside row 199. Both rows touch them, so this row rebases after 199
|
||||
lands.
|
||||
|
||||
## Gate
|
||||
|
||||
Duckworth approves DESIGN.md, then the manifest. The bus, business and cli suites and every
|
||||
`scripts/test-*.sh` are green on Sage's gate, and the mutants are killed (one per refusal, plus one that leaks
|
||||
the key into the agent's output and one that skips the host key pin).
|
||||
@@ -1,46 +1,77 @@
|
||||
# Brief: Melanie's hub instance under its own Linux user (row 190b)
|
||||
# Brief: Melanie's hub instance on its own host (row 195, was 190b)
|
||||
|
||||
Sage, 2026-10-20, from Darkwing's draft (agents/darkwing/work/queue-190b/BRIEF-DRAFT.md) for Mos's ruling
|
||||
mos-0986 (2026-10-20T18:53:48Z) on row 190 R1. Mos ruled a separate uid per instance a hard precondition of
|
||||
federation R8 (mos-0984, mos-0985) and of Case 2 row C2-13. The finding was federation review B5
|
||||
(duckworth-0001): on one uid any factory agent can read, write and control Melanie's hub directly, and SSO and
|
||||
RBAC don't stop a local process with filesystem access. On the 10-27 MVP path. Gitea #1693.
|
||||
Sage, 2026-10-20. Rewritten for Mos's rulings mos-0998 and mos-1005 (Vikunja #418, #419). The first version
|
||||
put the hub under its own Linux user on dragon-lin. That form can't pass Duckworth's B7 there, because the
|
||||
factory uid's `sudo -n` succeeds on dragon-lin (Mos's recon, brain-redux
|
||||
`projects/mosaic-mvp/MELANIE-HOST-RECON.md`). Melanie's own server is ruled out too: it has 1.2 GiB RAM free
|
||||
with her two Hermes bots running, and co-tenancy would share her user and her passwordless sudo. The hub runs
|
||||
on a separate host. Which host is Jason's decision (#419): hardware outside the cluster, a cluster VM with
|
||||
factory PVE root removed, or a cluster VM with that residual accepted. This row is written so the host choice
|
||||
doesn't change it. Gitea #1693. On the 10-27 MVP path. Federation R8 (Gizmoduck's design r4, 04a6112be) needs
|
||||
this row or nothing.
|
||||
|
||||
Owner Darkwing (Opus 5.5), reviewer Duckworth (author of B5; mos-0981 keeps new work off Filbert and Dewey),
|
||||
gate owner Sage. Merge-chained on row 190 (Q177): it builds on row 190's candidate and keeps its own review and
|
||||
gate. Design first. The first deliverable is `agents/darkwing/work/queue-190b/DESIGN.md`.
|
||||
Owner Darkwing (Opus 5.5), reviewer Duckworth (author of B5 and B7), gate owner Sage. Chained on row 190
|
||||
(Q177): the hub runs under row 190's config canon. Design first. The first deliverable is
|
||||
`agents/darkwing/work/queue-190b/DESIGN.md`.
|
||||
|
||||
## Goal
|
||||
|
||||
Melanie's hub instance runs under its own Linux user. No process of the factory instance can read or write the
|
||||
hub's config, data root or secrets, or control the hub's units. That covers the bus, role seats and their
|
||||
sessions, task runs, the Discord connector and the Console.
|
||||
Melanie's hub instance (Case 2, C2-13) runs on its own host. No factory agent can read or write the hub's
|
||||
config, data root or secrets, reach its bus socket, or log in to the hub host. Her writing reaches the hub only
|
||||
through the existing git.mosaicstack.dev remotes, books repo only.
|
||||
|
||||
## Provision (blank until Jason answers #419)
|
||||
|
||||
Who provisions the host and how is left open. Sizing from Mos (mos-0998): 4 vCPU, 8 GB RAM, 64 GB disk,
|
||||
internal network only. Each agent process measures 300 to 390 MB RSS on dragon-lin. The design takes the host
|
||||
as given: a fresh Linux host with a named admin path, reachable by the deploying seat while the row runs.
|
||||
|
||||
## The design must state
|
||||
|
||||
1. How operators (Mos, Sage, Jason) deploy, re-pin and roll back the hub. Which commands they run, as which
|
||||
user, and what each one needs.
|
||||
2. Each root step Jason runs: useradd, the home and its modes, `loginctl enable-linger`, and any sudoers rule,
|
||||
with the exact command lines.
|
||||
3. How the hub reaches the Claude proxy and gets its session env without sharing jwoltje's
|
||||
`claude-session.env`.
|
||||
4. Whether real isolation needs the factory instance's seats to run apart from the operators' uid. If yes, say
|
||||
so plainly with its cost. Don't design around it.
|
||||
|
||||
It must also cover the copies (shared read-only trees under jwoltje's home, or the hub's own), the human CLI
|
||||
wrapper and human proof for the hub's Console, and how the stack-release pointer, log and backups work under
|
||||
the second user.
|
||||
1. The deploy. Exactly what Darkwing installs on the hub host and how: the stack copy, node, the units, the
|
||||
config dir and data root under row 190's canon (O1: `~/.config/mosaic-NAME`, and "`~` is the home of the OS
|
||||
user that runs the instance"). Which commands run as which user on the hub host, and which need its root.
|
||||
2. Re-pin, rollback and backup on the hub host, and who runs them after the deploy key is gone.
|
||||
3. How the hub gets model access without a copy of jwoltje's `claude-session.env` or any factory credential.
|
||||
If that needs the credentials row (T1 item 6), say so and name the interim. If it needs a new subscription
|
||||
or a new provider account, flag it for Jason and don't pick one (mos-1008).
|
||||
4. The books repo path. Which git.mosaicstack.dev remote, which credential the hub holds for it, and that it
|
||||
can't read or push any other repository.
|
||||
5. The admin path and the end state. The host ends with no key the factory uid can read. The design names the
|
||||
admin path that remains (for example the hypervisor console, or a key kept off the factory uid) and who
|
||||
holds it.
|
||||
6. The acceptance script, as code Duckworth can read. It takes Gizmoduck's list of infrastructure gate items
|
||||
as its input (mos-1011), not a fixed set of checks. Each item names a path, its closed form and its
|
||||
accepted form, and the script reports which form holds for each, or fails. The list covers at least:
|
||||
- federation R8's separate-host form: the hub's bus socket, data root and config dir don't exist on the
|
||||
factory host, the factory host mounts nothing of the hub's, and no factory agent holds a login or key
|
||||
for the hub host
|
||||
- B7: from the factory uid, `sudo -n`, `doas -n`, `su`, polkit, and `ssh -o BatchMode=yes` to the hub
|
||||
host all fail
|
||||
- the hypervisor and backup paths ops-01 measured (mos-1011: one PVE node the factory key has root on,
|
||||
PBS on that node with unencrypted backups, a flat /16 LAN), each closed or accepted by Jason
|
||||
7. The identity provider and TLS front end (B8, mos-1008, mos-1011). Either the factory holds no credential
|
||||
that can create users, change groups, issue tokens or impersonate in the issuer the hub trusts, or the hub
|
||||
uses an issuer the factory can't administer. A brand, tenant or app inside the same Authentik does not
|
||||
count as separate. The same holds for the TLS front end that serves the hub. The proof is a failed admin
|
||||
call from every token the factory's root can read. No credential value is printed in that proof.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Row 190 lands first, as designed: one uid, scratch two-instance tests without root.
|
||||
- No agent runs a root step. Every one goes to Jason as an exact command list.
|
||||
- No real hub user, unit or data root is created in this row's tests. Tests use scratch roots and stand-ins as
|
||||
row 190 does. Anything that needs a second real uid is a Q173 window with a backup first and a STATE report
|
||||
after.
|
||||
- No tunnel or Traefik exposure. That waits for Jason, after Q175 SSO.
|
||||
- Provisioning, hypervisor access and any infra or k3s change are Jason's (Q173 exclusions). Mos takes the
|
||||
hypervisor residual (factory root on the PVE nodes, Q172) to Jason. It doesn't block this row.
|
||||
- Melanie's server (`[email protected]`) is reference only, read only, under Jason's authorization in
|
||||
mos-0995. Nothing on it changes, and her Hermes bots keep running.
|
||||
- No tunnel, Traefik or other exposure outside the internal network. That waits for Jason, after Q175 SSO.
|
||||
- No credential value is printed, logged or committed. Keys made for the deploy are removed at the end, and
|
||||
the acceptance script proves it.
|
||||
- The admin-call proof in point 7 and any call to the issuer or the TLS front end run only in a window Mos
|
||||
approves, never as a test or an anonymous probe (Q175).
|
||||
- Tests in the repository use scratch roots and stand-ins. The live deploy and the acceptance run on the hub
|
||||
host are a window with a backup first (where there is state) and a STATE report after.
|
||||
|
||||
## Gate
|
||||
|
||||
Duckworth approves DESIGN.md, then the manifest. The cli suite and every `scripts/test-*.sh` are green on
|
||||
Sage's gate. The design's mutants are killed.
|
||||
Duckworth approves DESIGN.md, then the manifest. Any repository change is green on Sage's gate (cli suite and
|
||||
every `scripts/test-*.sh`), with the design's mutants killed. The row is done when the acceptance script passes
|
||||
on the hub host from the factory uid, Duckworth reads its output, and Mos confirms.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Row 193 build rows: 193a, 193b1, 193b2, 193c and 193d (#1691)
|
||||
|
||||
Sage, 2026-10-20, for Mos's go mos-1034 item 4. Row 193's design is Morgana's
|
||||
`agents/morgana/work/queue-193/DESIGN.md` at e2d161e2e on `morgana/row193` (r5, sha256
|
||||
dd7fcb419ed8510e9d8d74b569183386d0942af8b68eecc0bbb5e182e677b523), approved by Duckworth (duckworth-0018).
|
||||
Section 8 lists the tests and mutants, and section 9 the split. The brief behind them is
|
||||
`docs/plans/2026-10-20_human-users.md`. Vikunja P19 #414/#415, Gitea #1691.
|
||||
|
||||
Owner Morgana for every row, reviewer Duckworth, gate owner Sage. Each row chains on its parent's candidate
|
||||
(Q177) and rebases onto the parent's landing. Seats push only their own `<seat>/*` branches (mos-0981).
|
||||
Single test files run freely, and suites and mutant batches need Sage's slot.
|
||||
|
||||
Boundaries for every row: no change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units, slot files, any
|
||||
live access.json or `~/.mosaic`. No request to the live Console, mosaic.woltje.com or auth.diversecanvas.com.
|
||||
No user thread goes live before 193b1's message filter has landed (mos-1021 B2, FED R8).
|
||||
|
||||
The owner-visible records are fixed by Mos (mos-1023 item 2) and named in one user sentence, which FED R6
|
||||
reuses word for word: "The business owner sees your thread's agent's decisions, inbox entries, claims, tasks
|
||||
and project files, but not your messages."
|
||||
|
||||
## Brief: 193a, the access level check widened to viewer
|
||||
|
||||
Chained on FED R1. Started early on `morgana/row193a` (508ee4d61) against R1's interface as the FED brief
|
||||
names it, with Sage's go (sage-1373). R1's level check accepts `viewer` and still refuses any other new
|
||||
level. Tests in `packages/business/tests/` (section 8, Business), with the four mutants in Morgana's
|
||||
BUILD.md. FED R4 chains on this row.
|
||||
|
||||
Duckworth's N1 on R1 (duckworth-0033, sage-1417): with viewer added, `covers` called with no level would let a
|
||||
viewer grant cover a collaborator ask. This row makes `level` required in `covers`, with a test and a mutant that
|
||||
makes it optional again. R3 and R4 call `covers` with a level.
|
||||
|
||||
Gate: Duckworth approves the manifest. The business suite and every `scripts/test-*.sh` are green on Sage's
|
||||
gate, and the mutants are killed.
|
||||
|
||||
## Brief: 193b1, viewer verbs, board refusals and the message filter
|
||||
|
||||
Chained on FED R3, FED R4 and row 196's verifier. The bus gets the viewer verb list, per-grant level
|
||||
checks, board reads refused to users, and the section 5.7 `trail` and `replies` filter with `#readable`
|
||||
owner-only. No schema change. The governance pin test names each item in the user sentence above. Bus tests
|
||||
and mutants 1, 3, 6, 10, 11 and 12 from section 8.
|
||||
|
||||
Gate: Duckworth approves the manifest. The bus suite and every `scripts/test-*.sh` are green on Sage's gate,
|
||||
and the mutants are killed.
|
||||
|
||||
## Brief: 193b2, thread shares (schema 3j)
|
||||
|
||||
Chained on 193b1. `thread_shares`, the share verbs, the share rules of `#readable` and `#threadRead`, and the
|
||||
section 5.5 share filters. Schema 3i to 3j. The table is append-only, and row 181's restore guard and
|
||||
`COUNTED` include it. Bus tests for shares and the migration, and mutants 2, 4, 7, 8 and 9 from section 8.
|
||||
Going live needs a `--migrate` window with Jason. Sage takes it to Mos when this row is near.
|
||||
|
||||
Gate: Duckworth approves the manifest. The bus suite, the cli suite and every `scripts/test-*.sh` are green on
|
||||
Sage's gate, and the mutants are killed.
|
||||
|
||||
## Brief: 193c, mosaic access user verbs and --level viewer
|
||||
|
||||
Chained on FED R5. `mosaic access user add/remove` and `grant --level viewer`, only where R5 lacks them. They
|
||||
refuse tester and agent callers, write a receipt line and call `access.reload` (section 8, CLI). If R5
|
||||
already has all of them, this row records that and closes with no code.
|
||||
|
||||
Gate: Duckworth approves the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's gate,
|
||||
and the mutants are killed.
|
||||
|
||||
## Brief: 193d, the Console's shared list, share controls and viewer read-only
|
||||
|
||||
Chained on FED R6, row 192 and 193b2 (for share controls). A viewer has no composer or share control, and
|
||||
with each UI check removed the bus still refuses (section 8, Webui).
|
||||
|
||||
Gate: Duckworth approves the manifest. The webui suite and every `scripts/test-*.sh` are green on Sage's
|
||||
gate, and the mutants are killed.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Brief: retire the control board writer on 7331
|
||||
|
||||
Sage, 2026-10-20, for Mos's ruling mos-1008 item 2. Gitea #1696. MVP 10-27 work. Owner Bushroot (Haiku 5.5, a
|
||||
low-level model under Q177), reviewer Webby, gate owner Sage. Base: refactor at the brief commit.
|
||||
|
||||
## Why
|
||||
|
||||
`packages/control-board/src/serve.mjs` is a loopback HTTP service on 7331 with no authentication. It reads
|
||||
transcripts and writes through `/api/reply`. Any host process that reaches the port can do both. That fails
|
||||
Q175 ("zero trust with verification"). Gander found it in the row 196 inventory (gander-0004). Nothing listens
|
||||
on 7331 now (`ss -ltnp`, 2026-10-20T19:05:09Z), so retiring it costs the least.
|
||||
|
||||
## Steps, in order
|
||||
|
||||
1. Inventory first. List every reference to the control board and to 7331: scripts, units, `scripts/mosaic`
|
||||
and other CLI dispatch, docs (AGENTS.md, READMEs, TOOLS.md), package.json scripts, tests, and the Console
|
||||
(`packages/webui`). For each, say whether it starts the writer, reads its code, or only mentions it.
|
||||
Commit the list as `agents/bushroot/work/queue-ROW/INVENTORY.md` and send it to Sage and Webby.
|
||||
2. Stop if something depends on it. If a reference shows a live dependency (the Console or another package
|
||||
imports code that only the control board has, or a unit or script still starts it), stop. Sage takes it to
|
||||
Mos before anything is removed.
|
||||
3. Otherwise retire the writer. Either remove the `serve` entry point, or make it refuse to start with a
|
||||
message that points to the Console. Keep any read-only code another package imports, and say which.
|
||||
Update the docs that tell people to start it. The AGENTS.md text about the control-board reply convention
|
||||
is a separate matter. Change it only if it tells someone to start 7331.
|
||||
|
||||
## Mos's call on the Console dependency (mos-1019)
|
||||
|
||||
The inventory (004f133bd) found that the Console proxies `/api/board`, `/api/seen`, `/api/reply` and the
|
||||
conversation routes to `DEFAULT_BOARD` on 7331 (`packages/webui/src/serve.mjs:12`), so the board view returns
|
||||
502 today. Mos took Sage's recommendation: retire the 7331 listener and its README start commands, and have
|
||||
the Console call the board's read code in-process, behind the Console's own auth (Q175 SSO on 7332).
|
||||
`/api/reply` stays only under `--send`. The DESIGN.md for that change comes before code, and Webby approves
|
||||
it.
|
||||
|
||||
## Acceptance
|
||||
|
||||
Nothing in the stack can start a writer on 7331. A test proves it: the entry point refuses (or is gone), no
|
||||
script or unit starts it, and a grep check finds no starter. Each change gets a mutant that brings the writer
|
||||
back and the test kills it. Mos added three items (mos-1019):
|
||||
|
||||
- The board view answers from the Console with nothing listening on 7331.
|
||||
- Every board route sits behind the Console's auth.
|
||||
- A test proves `/api/reply` refuses without `--send`.
|
||||
|
||||
Test fixtures that bind port 0 may stay if they only test the code in-process.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No change to the live units, `~/.mosaic-dev`, `~/.config/mosaic-dev`, slot files or `~/.mosaic`.
|
||||
- No request to any live port.
|
||||
- Single test files run freely. Suites need Sage's slot.
|
||||
|
||||
## Gate
|
||||
|
||||
Webby approves INVENTORY.md, then DESIGN.md, then the manifest. The control-board and webui package suites, the cli suite and
|
||||
every `scripts/test-*.sh` are green on Sage's gate, and the mutants are killed.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Brief: a second gate lane, two full gates at once on dragon-lin
|
||||
|
||||
Sage, 2026-10-20, for Mos's order mos-0977 item 6 ("put the design for a second gate lane on the list now")
|
||||
and the MVP push (mos-0974). Gitea #1695. Owner Herb (Codex gpt-6.1-sol, a new builder seat), reviewer
|
||||
Webby, gate owner Sage. Base: refactor at the brief commit. Design first. The first deliverable is
|
||||
`agents/herb/work/queue-ROW/DESIGN.md`.
|
||||
|
||||
## Why
|
||||
|
||||
Every full run shares one slot today. A full gate (cli suite plus every `scripts/test-*.sh`) takes about 13
|
||||
minutes or more, reviewers' runs and mutants take the same slot, and with eight or more seats building for
|
||||
10-27 the slot is the bottleneck. The slot exists because concurrent runs broke each other. The busy check is
|
||||
`agents/sage/work/gate-lane/busy-now.sh` (a copy of Sage's), and the gate is
|
||||
`agents/sage/work/gate-lane/gate-template.sh` (a copy of Sage's template). The host has 16 cores and 62 GB.
|
||||
|
||||
## What the design must settle
|
||||
|
||||
1. Every shared resource two concurrent runs can collide on, found from the code and tests, not guessed:
|
||||
Docker compose project names and networks (`scripts/test-task.sh` and others), fixed TCP ports in tests,
|
||||
unix socket paths and their 107-byte limit under TMPDIR, the sandbox's private `/tmp`, shared caches,
|
||||
scratch and worktree paths, the queue lock, and any test that reads host-wide state (process lists,
|
||||
`pgrep`, `/proc`). For each: how a lane gets its own copy, or why it is safe to share.
|
||||
2. Timing. Which tests have time limits that a doubled CPU load could break, measured, and what changes (a
|
||||
bound, or a lane rule that keeps those suites apart).
|
||||
3. The lane mechanism: lane ids, a lock per lane that a run takes and releases even on a crash, the busy
|
||||
check that reports per lane, and how a seat asks for a lane. Sage still grants it.
|
||||
4. The gate template takes a lane id and isolates everything from point 1 by it.
|
||||
5. Proof: two full gates on different candidates at once, three times, with zero cross-failures, plus the
|
||||
wall time of each against a single-lane run. Any flake found is named with its cause.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No change to the live units, `~/.mosaic-dev`, `~/.config/mosaic-dev` or slot files. The proof runs use
|
||||
scratch roots only, and it needs Sage's grant of the whole slot while it runs.
|
||||
- Test code changes that only isolate a resource are in scope. A change to what a test checks is not, and
|
||||
goes to its owner.
|
||||
- Docker networks and containers a run makes are removed by the run (trap), and nothing else is touched.
|
||||
|
||||
## Gate
|
||||
|
||||
Webby approves DESIGN.md, then the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's
|
||||
gate, both singly and in the two-lane proof.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Brief: stack-release.sh activates a Console with --sso (Q175 phase 2 prerequisite)
|
||||
|
||||
Sage, 2026-10-20, for Launchpad's finding F3 on row 192 (launchpad-0001) and Q175 (#412). Gitea #1698. Owner
|
||||
Launchpad, reviewer Webby, gate owner Sage. Chained on row 192's candidate (Q177): it lands after row 192 and
|
||||
before the phase 2 window. Base: row 192's candidate.
|
||||
|
||||
## Why
|
||||
|
||||
The phase 2 window re-pins the Console to the phase 1 commit with `--sso --send --tester-port 7333`
|
||||
(`agents/sage/work/q175-sso/DESIGN.md`, section 5). `scripts/stack-release.sh` doesn't know `--sso` today.
|
||||
Its `proof_checks` (`:751`) requires `--public-host`, `--auth-header` and `--allow-user` whenever a proof pair is
|
||||
carried, and the pointer's Console flags still hold the forward-auth pair (`--auth-header X-authentik-username
|
||||
--allow-user jason.woltje`) that `--sso` refuses to start with. So the window can't run through the script, and
|
||||
a hand-edited pointer is not an option.
|
||||
|
||||
## What the row does
|
||||
|
||||
1. `activate ID BUSINESS --sso` writes Console flags with `--sso` and without `--auth-header`, `--allow-user`
|
||||
and any proof pair, and `--plan` names each flag it drops and carries, in the style of row 189's `send:`
|
||||
line. The design names the full flag set the phase 2 Console starts with and checks it against row 192's
|
||||
refusals.
|
||||
2. Before anything stops, the script refuses a target copy whose Console doesn't support `--sso`, and refuses
|
||||
when the config's `console` key or its slot file fails the checks row 192's `--sso` start runs. It reads
|
||||
neither value and only uses the same stat and shape checks.
|
||||
3. Rollback from an `--sso` pointer to a pre-phase-1 copy follows the design's phase 2 rollback order (remove
|
||||
the `console` key first). The script refuses a rollback that would start a Console on a config it can't
|
||||
read, and says which step comes first.
|
||||
4. `docs/TOOLS.md` gets the flag.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No change to the live pointer, units, `~/.mosaic-dev` or `~/.config/mosaic-dev`. Tests use scratch roots
|
||||
and seeded pointers, as rows 187 and 189 do.
|
||||
- No request to the live Console, mosaic.woltje.com or auth.diversecanvas.com.
|
||||
|
||||
## Gate
|
||||
|
||||
Webby approves the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's gate, and the
|
||||
mutants are killed (one per refusal, and one that keeps `--auth-header` in the written flags).
|
||||
@@ -381,7 +381,7 @@ async function seed(e,{copy=B,pm=false,store='3g',pointer:how='adopt'}={}){
|
||||
const rolledBack=(code,to=B)=>`stack-release: ${code}; rolled back to ${to}, which passed the gate\n`;
|
||||
const planBackup=(e)=>`backup: the run backs up the stopped store to ${e.data}/backups/stack/<activation>/ before any migration`;
|
||||
const planNoBackup='backup: none; the run rolls back before the backup';
|
||||
const planTail=(e,to,pm=false)=>[`bus: scripts/bus-service.sh install --copy ${to} --copies ${COPIES}${pm?' --pm':''} --claude ${e.claude}`,'proof: off',`Console: scripts/webui-service.sh install --copy ${to} --copies ${COPIES} --port ${e.port} --business acme`,`CLI: scripts/stack-copy.sh wrapper ${to} --copies ${COPIES} --bin ${e.bin}`];
|
||||
const planTail=(e,to,pm=false)=>[`bus: scripts/bus-service.sh install --copy ${to} --copies ${COPIES}${pm?' --pm':''} --claude ${e.claude}`,'proof: off','send: off',`Console: scripts/webui-service.sh install --copy ${to} --copies ${COPIES} --port ${e.port} --business acme`,`CLI: scripts/stack-copy.sh wrapper ${to} --copies ${COPIES} --bin ${e.bin}`];
|
||||
|
||||
// --- The T table ---
|
||||
|
||||
@@ -1030,17 +1030,17 @@ test('T36 --plan shows the pending state and lists the row 169 refusals it can s
|
||||
return r.out.split('\n');
|
||||
};
|
||||
let out=await quiet(0,'activate',C1,'acme','--pm-after-harness','--plan');
|
||||
assert.deepEqual(out.slice(-6),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,C1),'']);
|
||||
assert.deepEqual(out.slice(-7),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,C1),'']);
|
||||
biz(e,HARNESS);
|
||||
out=await quiet(3,'activate',A,'acme','--pm-after-harness','--plan');
|
||||
assert.deepEqual(out.slice(-7),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,A),`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`,'']);
|
||||
assert.deepEqual(out.slice(-8),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,A),`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`,'']);
|
||||
out=await quiet(3,'activate',C1,'acme','--pm-after-harness','--plan');
|
||||
assert.equal(out.at(-2),'refuses pm-after-harness-set: the business file already sets harness ["claude-code"]; use --pm');
|
||||
biz(e);assert.equal((await run(e,'activate',C1,'acme','--pm-after-harness')).code,0);
|
||||
out=await quiet(3,'activate',C,'acme','--plan');
|
||||
assert.deepEqual(out.slice(-7),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,C,true),`refuses pm-pending: ${PENDING_MSG}`,'']);
|
||||
assert.deepEqual(out.slice(-8),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,C,true),`refuses pm-pending: ${PENDING_MSG}`,'']);
|
||||
out=await quiet(0,'rollback','acme','--plan');
|
||||
assert.deepEqual(out.slice(-6),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,B,true),'']);
|
||||
assert.deepEqual(out.slice(-7),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,B,true),'']);
|
||||
});
|
||||
|
||||
// Row 176 test 11: supported and older Console copies deliberately differ from the checkout.
|
||||
@@ -1706,6 +1706,113 @@ test('T54 row 188: a carried pair gets the proof file checks, and a proof file i
|
||||
});
|
||||
});
|
||||
|
||||
// Row 189 (#1687, mos-0972/0973): activate --no-send drops the pointer's --send; nothing turns it back on.
|
||||
const sendLines=(out)=>out.filter((l)=>l.startsWith('send:'));
|
||||
const noSend=(c)=>c.filter((x)=>x!=='--send');
|
||||
const LIVE_NOTE=`note: ${B} is the pointer's copy; the run re-pins it and only the Console flags change`;
|
||||
test('T55 row 189: activate --no-send drops --send, and rollback and pm-on carry it off',async(t)=>{
|
||||
await t.test('test 1: a live-copy --no-send drops --send from the pointer and the unit, keeps the rest in order, logs the step and backs up the stopped store',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']),want=noSend(base);
|
||||
assert.deepEqual(want.slice(-2),['--tester-port','7333']);
|
||||
const sum=createHash('sha256').update(readFileSync(e.store)).digest('hex'),n=calls(e).length;
|
||||
let r=await prun(e,'activate',B,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.equal(pointer(e).copy,B);assert.deepEqual(pointer(e).console,want);
|
||||
assert.ok(conExec(e).endsWith(` --checkout ${repo} ${want.join(' ')}`),conExec(e));assert.doesNotMatch(conExec(e),/ --send( |$)/);
|
||||
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
|
||||
assert.match(stepText(e),/Z drop --send; --no-send was given\n/);
|
||||
const act=pointer(e).activation,l=logLines(e).at(-1);
|
||||
assert.deepEqual([l.op,l.from,l.to,l.result,l.backup],['activate',B,B,'ok',act]);assert.equal(backupSum(e,act),sum);
|
||||
assert.deepEqual(startsStops(e,n),[`stop ${CON}`,`stop ${BUS}`,`start ${BUS}`,`start ${CON}`]);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('test 2: a plain live-copy activate still refuses already-active, and so does --no-send once --send is gone',async(t)=>{
|
||||
const e=await mk(t);await seedPreview(e,undefined,['--send']);
|
||||
await refused(e,['activate',B,'acme'],'already-active',`${B} is the pointer's copy`);
|
||||
await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);
|
||||
await refused(e,['activate',B,'acme','--no-proof'],'already-active',`${B} is the pointer's copy`);
|
||||
const r=await prun(e,'activate',B,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
|
||||
await refused(e,['activate',B,'acme','--no-send'],'already-active',`${B} is the pointer's copy`);
|
||||
const g=await mk(t);await seedPreview(g);
|
||||
await refused(g,['activate',B,'acme','--no-send'],'already-active',`${B} is the pointer's copy`);
|
||||
for(const x of [e,g])await noLeak(x);
|
||||
});
|
||||
await t.test('test 3: --plan prints one send: line in each case, with the live-copy note on a same-copy run',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e,undefined,['--send']);
|
||||
let out=await planOnly(e,0,'activate',B,'acme','--no-send');
|
||||
assert.deepEqual(sendLines(out),['send: dropped; --no-send was given']);
|
||||
assert.deepEqual(out.filter((l)=>l.startsWith('note: ')),[LIVE_NOTE]);
|
||||
assert.equal(out[out.indexOf('send: dropped; --no-send was given')+1],`Console: scripts/webui-service.sh install --copy ${B} --copies ${COPIES} ${noSend(base).join(' ')}`);
|
||||
out=await planOnly(e,0,'activate',C1,'acme');
|
||||
assert.deepEqual(sendLines(out),['send: carried']);assert.deepEqual(out.filter((l)=>l.startsWith('note: ')),[]);
|
||||
assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(' --send'));
|
||||
assert.deepEqual(sendLines(await planOnly(e,0,'activate',C1,'acme','--no-send')),['send: dropped; --no-send was given']);
|
||||
let r=await prun(e,'activate',C1,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(e).console,noSend(base));assert.match(stepText(e),/Z drop --send; --no-send was given\n/);
|
||||
assert.deepEqual(sendLines(await planOnly(e,0,'activate',B,'acme','--no-send')),['send: off; --no-send was given and the pointer holds no --send']);
|
||||
assert.deepEqual(sendLines(await planOnly(e,0,'activate',B,'acme')),['send: off']);
|
||||
assert.deepEqual(sendLines(await planOnly(e,0,'rollback','acme')),['send: off']);
|
||||
r=await prun(e,'activate',B,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.match(stepText(e),/Z --no-send was given and the pointer holds no --send, so there is nothing to drop\n/);
|
||||
assert.doesNotMatch(stepText(e),/drop --send/);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('test 4: after --no-send, a rollback to the earlier copy leaves --send out of the pointer and the unit',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e,undefined,['--send']);
|
||||
let r=await prun(e,'activate',C1,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
|
||||
r=await prun(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.equal(pointer(e).copy,B);assert.deepEqual(pointer(e).console,noSend(base));assert.doesNotMatch(conExec(e),/ --send( |$)/);
|
||||
assert.doesNotMatch(stepText(e),/--send/);
|
||||
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('test 4: pm-on after --no-send keeps --send off',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e,{pm:true},['--send']);
|
||||
let r=await prun(e,'activate',C1,'acme','--pm-after-harness','--no-send');assert.equal(r.code,0,r.out+r.err);
|
||||
biz(e,HARNESS);
|
||||
r=await prun(e,'pm-on','acme');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.equal(pointer(e).pm,true);assert.deepEqual(pointer(e).console,noSend(base));assert.doesNotMatch(conExec(e),/ --send( |$)/);
|
||||
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('test 5: --no-send on rollback, pm-on, status, adopt or rehearse is a usage error',async(t)=>{
|
||||
const e=await mk(t);await seedPreview(e,undefined,['--send']);
|
||||
const usageText=(await prun(e,'--help')).err,n=calls(e).length,logs=logLines(e).length,before=fp(e);
|
||||
for(const args of [['rollback','acme','--no-send'],['rollback','acme','--plan','--no-send'],['pm-on','acme','--no-send'],['status','acme','--no-send'],['adopt','acme','--no-send'],
|
||||
['rehearse','acme','--backup','/tmp/x','--port','7400','--no-send']]){
|
||||
const r=await prun(e,...args);assert.equal(r.code,4,args.join(' '));assert.equal(r.out,'');assert.equal(r.err,usageText);
|
||||
}
|
||||
assert.match(usageText,/\| --no-proof\] \[--no-send\]/);
|
||||
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('test 6: with both a pair and --send in the pointer, --no-send and --no-proof each drop what they name and keep the rest',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e,undefined,['--send']),pre=noSend(base),f=await proofFile(e),g=await proofFile(e,'proof2.189',0o400);
|
||||
let r=await prun(e,'activate',B,'acme',...pair(f));assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send']);
|
||||
let out=await planOnly(e,0,'activate',C1,'acme','--no-proof');
|
||||
assert.deepEqual([...proofLines(out),...sendLines(out)],['proof: dropped; --no-proof drops header x-console-proof','send: carried']);
|
||||
out=await planOnly(e,0,'activate',C1,'acme','--no-send');
|
||||
assert.deepEqual([...proofLines(out),...sendLines(out)],['proof: carried, header x-console-proof','send: dropped; --no-send was given']);
|
||||
out=await planOnly(e,0,'activate',B,'acme','--no-send','--no-proof');
|
||||
assert.deepEqual([...proofLines(out),...sendLines(out)],['proof: dropped; --no-proof drops header x-console-proof','send: dropped; --no-send was given']);
|
||||
r=await prun(e,'activate',C1,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(e).console,[...pre,...pair(f)]);
|
||||
r=await prun(e,'activate',C1,'acme','--no-send',...pair(g,'X-Proof-Two'));assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(e).console,[...pre,...pair(g,'X-Proof-Two')]);
|
||||
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
|
||||
const h=await mk(t),hbase=await seedPreview(h,undefined,['--send']),hpre=noSend(hbase),k=await proofFile(h);
|
||||
r=await prun(h,'activate',B,'acme',...pair(k));assert.equal(r.code,0,r.out+r.err);
|
||||
r=await prun(h,'activate',C1,'acme','--no-proof');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(h).console,[...hpre,'--send']);
|
||||
r=await prun(h,'activate',C1,'acme','--no-send','--no-proof');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(h).console,hpre);assert.doesNotMatch(conExec(h),/--proof-| --send( |$)/);
|
||||
const steps=stepText(h);
|
||||
assert.match(steps,/Z --no-proof was given and the pointer holds no proof pair, so there is nothing to drop\n/);assert.match(steps,/Z drop --send; --no-send was given\n/);
|
||||
r=await prun(h,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
|
||||
for(const x of [e,h])await noLeak(x);
|
||||
});
|
||||
});
|
||||
|
||||
// --- rehearse (D2, row 163; the brief's "Stack rehearsal on a scratch root") ---
|
||||
// Copies of their own in RCOPIES: the real scripts/mosaic, config loader, business loader, roles and
|
||||
// mosaic-config.mjs, with stand-ins for the bus host (cli.mjs), the Console, the Store and
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# Moves the bus, the Console and the human CLI from the pointer's copy to another copy together,
|
||||
# behind a health gate, and puts the old stack back inside the same run when the gate fails.
|
||||
# Console tester listener flags follow target support (row 176, #1674); so does the row 178 proof
|
||||
# pair, which activate sets or drops (row 187, #1685).
|
||||
# pair, which activate sets or drops (row 187, #1685). activate --no-send drops --send (row 189, #1687).
|
||||
# Design and tests: agents/darkwing/work/queue-162/BUILD.md.
|
||||
#
|
||||
# Who runs it. Decision 96 (Mos, mos-0716) lets this script stop and start the live units under
|
||||
@@ -30,13 +30,13 @@ BIN="$HOME/.npm-global/bin"
|
||||
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||
CONSOLE_UNIT=mosaic-webui.service
|
||||
TIMEOUT=60 WANT_PM=0 AFTER_HARNESS=0 MIGRATE=0 PLAN=0 RESTORE="" EVENTS="" TESTER_PORT=""
|
||||
PROOF_HEADER="" PROOF_FILE="" NO_PROOF=0 R_BACKUP="" R_PORT=""
|
||||
PROOF_HEADER="" PROOF_FILE="" NO_PROOF=0 NO_SEND=0 R_BACKUP="" R_PORT=""
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'USAGE'
|
||||
usage: stack-release.sh adopt BUSINESS
|
||||
stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-port N]
|
||||
[--proof-header NAME --proof-file PATH | --no-proof] [--timeout S] [--plan]
|
||||
[--proof-header NAME --proof-file PATH | --no-proof] [--no-send] [--timeout S] [--plan]
|
||||
stack-release.sh pm-on BUSINESS [--timeout S]
|
||||
stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan]
|
||||
stack-release.sh status BUSINESS
|
||||
@@ -85,6 +85,7 @@ parse_opts() {
|
||||
--proof-header) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; PROOF_HEADER="$2"; shift 2 ;;
|
||||
--proof-file) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; PROOF_FILE="$2"; shift 2 ;;
|
||||
--no-proof) [ "$OP" = activate ] || usage; NO_PROOF=1; shift ;;
|
||||
--no-send) [ "$OP" = activate ] || usage; NO_SEND=1; shift ;;
|
||||
--migrate) [ "$OP" = activate ] || usage; MIGRATE=1; shift ;;
|
||||
--restore) [ "$OP" = rollback ] && [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}(-[1-9])?$ ]] || usage; RESTORE="$2"; shift 2 ;;
|
||||
--backup) [ "$OP" = rehearse ] && [ $# -ge 2 ] && { [[ "$2" =~ ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}(-[1-9])?$ ]] || [[ "$2" =~ ^/[A-Za-z0-9/._+-]*$ ]]; } || usage; R_BACKUP="$2"; shift 2 ;;
|
||||
@@ -244,14 +245,16 @@ target_schema() {
|
||||
in_chain() { local v; for v in "${T_CHAIN[@]}"; do [ "$v" != "$1" ] || return 0; done; return 1; }
|
||||
|
||||
# The item 3 checks on the target copy, in order: copy, schema, already-active. An activate that
|
||||
# sets a proof pair, or drops the pointer's pair with --no-proof, may name the pointer's copy: it
|
||||
# re-pins the live copy with new Console flags (row 187, Part B's B3 and B-R1).
|
||||
# sets a proof pair, drops the pointer's pair with --no-proof, or drops the pointer's --send with
|
||||
# --no-send, may name the pointer's copy: it re-pins the live copy with new Console flags (row 187,
|
||||
# Part B's B3 and B-R1; row 189, mos-0972).
|
||||
check_target() {
|
||||
copy_exists "$TO" 2>/dev/null || refuse copy-missing "no copy $TO in $COPIES"
|
||||
copy_ok "$TO" 2>/dev/null || refuse copy-unreachable "$TO is not reachable from refactor and origin/refactor"
|
||||
target_schema "$TO" || refuse copy-schema-unknown "cannot read SCHEMA_VERSION and its chain from $COPIES/$TO/packages/bus/src/store.mjs"
|
||||
[ "$TO" != "$P_COPY" ] || [ -n "$PROOF_HEADER" ] ||
|
||||
{ [ "$NO_PROOF" = 1 ] && [[ " ${P_CONSOLE[*]} " == *" --proof-header "* ]]; } ||
|
||||
{ [ "$NO_SEND" = 1 ] && [[ " ${P_CONSOLE[*]} " == *" --send "* ]]; } ||
|
||||
refuse already-active "$TO is the pointer's copy"
|
||||
}
|
||||
|
||||
@@ -690,6 +693,33 @@ console_for_target() {
|
||||
DROP_TESTER="--tester-port $inherited"
|
||||
fi
|
||||
proof_for_target
|
||||
send_for_target
|
||||
}
|
||||
|
||||
# Row 189 (mos-0972): --no-send drops --send from RUN_CONSOLE, so the unit and the pointer both lose
|
||||
# it; without the flag the pointer's --send goes on unchanged. Rollback and pm-on never take it, so
|
||||
# they carry the pointer as it is. Sets SEND_PLAN (the plan's send: line) and SEND_STEP.
|
||||
send_for_target() {
|
||||
local had=0 i
|
||||
local -a without_send=()
|
||||
for ((i = 0; i < ${#RUN_CONSOLE[@]}; i++)); do
|
||||
if [ "${RUN_CONSOLE[i]}" = --send ]; then had=1; else without_send+=("${RUN_CONSOLE[i]}"); fi
|
||||
done
|
||||
SEND_STEP=""
|
||||
if [ "$NO_SEND" = 1 ]; then
|
||||
RUN_CONSOLE=("${without_send[@]}")
|
||||
if [ "$had" = 1 ]; then
|
||||
SEND_PLAN="send: dropped; --no-send was given"
|
||||
SEND_STEP="drop --send; --no-send was given"
|
||||
else
|
||||
SEND_PLAN="send: off; --no-send was given and the pointer holds no --send"
|
||||
SEND_STEP="--no-send was given and the pointer holds no --send, so there is nothing to drop"
|
||||
fi
|
||||
elif [ "$had" = 1 ]; then
|
||||
SEND_PLAN="send: carried"
|
||||
else
|
||||
SEND_PLAN="send: off"
|
||||
fi
|
||||
}
|
||||
|
||||
# Row 187: the row 178 proof pair in RUN_CONSOLE. A new pair replaces the pointer's and goes last;
|
||||
@@ -943,6 +973,7 @@ plan() {
|
||||
echo "bus: scripts/bus-service.sh install --copy $TO --copies $COPIES$([ "$RUN_PM" = 0 ] || echo ' --pm') --claude $P_CLAUDE"
|
||||
[ -z "$DROP_TESTER" ] || echo "drop $DROP_TESTER; the target Console does not support it"
|
||||
echo "$PROOF_PLAN"
|
||||
echo "$SEND_PLAN"
|
||||
echo "Console: scripts/webui-service.sh install --copy $TO --copies $COPIES${RUN_CONSOLE[*]:+ ${RUN_CONSOLE[*]}}"
|
||||
echo "CLI: scripts/stack-copy.sh wrapper $TO --copies $COPIES --bin $BIN"
|
||||
local r
|
||||
@@ -956,6 +987,7 @@ run_steps() {
|
||||
capture || refuse capture-failed "could not write the rollback set $RS; nothing was stopped"
|
||||
[ -z "$DROP_TESTER" ] || say "drop $DROP_TESTER; the target Console does not support it"
|
||||
[ -z "$PROOF_STEP" ] || say "$PROOF_STEP"
|
||||
[ -z "$SEND_STEP" ] || say "$SEND_STEP"
|
||||
trap 'after_fail signal' INT TERM HUP
|
||||
stop_both || rollback_in_run stop-timeout
|
||||
if [ -e "$STORE" ]; then
|
||||
@@ -1478,7 +1510,7 @@ STORE="$DATA/bus/bus.sqlite"
|
||||
BUS_UNIT="mosaic-bus@$BUSINESS.service"
|
||||
FROM="" TO="" ACT="" RS="" POST="" BACKUP="" TAKEN="" RESTORED="" STEPS="" COUNT="" UNCLEAN="" FAIL=""
|
||||
STORE_SCHEMA="" START_EPOCH="" START_MS=0 GATE="" GATE_WHY="" S_CHANGED=""
|
||||
P_COPY="" P_PM=0 P_PENDING=0 P_SCHEMA="" P_CLAUDE="" P_CONSOLE=() RUN_PM=0 RUN_PENDING=0 RUN_CONSOLE=() DROP_TESTER="" PROOF_PLAN="" PROOF_STEP="" PLAN_REFUSE=()
|
||||
P_COPY="" P_PM=0 P_PENDING=0 P_SCHEMA="" P_CLAUDE="" P_CONSOLE=() RUN_PM=0 RUN_PENDING=0 RUN_CONSOLE=() DROP_TESTER="" PROOF_PLAN="" PROOF_STEP="" SEND_PLAN="" SEND_STEP="" PLAN_REFUSE=()
|
||||
R="" R_RC=0 R_EARLY=() R_BK="" R_HOST_PID="" R_CONSOLE_PID="" R_STARTED="" LIVE_CFG="" LIVE_BIZ=""
|
||||
case "$OP" in
|
||||
adopt) do_adopt ;;
|
||||
|
||||
Reference in New Issue
Block a user