mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-08 17:31:56 -10:00
Compare commits
9
Commits
main
...
release/0.6.14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df1c1517f0 | ||
|
|
0bd0fe794c | ||
|
|
c6a23864fa | ||
|
|
9e35f97951 | ||
|
|
e0ebc377b7 | ||
|
|
3124373190 | ||
|
|
4feadb17dd | ||
|
|
050cc6b8bc | ||
|
|
834599c1a7 |
No files matched your search
+321
@@ -8,6 +8,327 @@ deprecations, and behavioral changes. Breaking changes are called out explicitly
|
||||
|
||||
---
|
||||
|
||||
## [0.6.14]
|
||||
|
||||
0.6.14 is the last planned release of the 0.6 line: a small set of stability
|
||||
fixes on top of 0.6.13. A seat launched by an older OpenRig that refuses every
|
||||
send for a missing tmux pane can now be repaired without losing its session, a
|
||||
manual compaction keeps waiting while its preparation sits at an approval, `rig
|
||||
ask --wake` refuses runtimes it can't wake, refocus finds notes without starting
|
||||
the CLI once per ancestor, the docs open with a getting-started page for people,
|
||||
an optional Mac check, off by default, can stop one account's `rig` commands
|
||||
from reaching another account's daemon, and the inventory read behind `rig ps`
|
||||
and TUI refreshes avoids a full event-log scan. It is built from the commit
|
||||
tagged `v0.6.14`. The changes since 0.6.13:
|
||||
|
||||
### Before you upgrade
|
||||
|
||||
- Restart the daemon after upgrading the CLI (`rig daemon stop`, then `rig
|
||||
daemon start`). No migration runs. One setting is added,
|
||||
`transport.local_user_guard`, off by default, and `/healthz` gains an `euid`
|
||||
field ([#1566](https://github.com/mvschwarz/openrig/pull/1566)). From 0.6.12
|
||||
or earlier, 0.6.13's migration `117` runs first (and from 0.6.11 or earlier,
|
||||
0.6.12's twelve before it).
|
||||
- Expect the bundled openrig-core plugin to go from 0.1.14 to 0.1.15, carrying
|
||||
refocus's own notes lookup
|
||||
([#1563](https://github.com/mvschwarz/openrig/pull/1563)).
|
||||
- Expect `rig ask --wake` to refuse a Pi, custom or unrecorded runtime before
|
||||
any wake starts. The CLI and the daemon both need 0.6.14 to catch every case
|
||||
([#1561](https://github.com/mvschwarz/openrig/pull/1561)).
|
||||
- Expect a manual `rig compact` to wait longer: its preparation has 180 seconds
|
||||
(was 120), and the request times out after 240 seconds (was 180)
|
||||
([#1562](https://github.com/mvschwarz/openrig/pull/1562)).
|
||||
|
||||
### Highlights
|
||||
|
||||
- Repair a seat that refuses every send because an older OpenRig never
|
||||
recorded its tmux pane: `rig reconcile-session <seat> --repair-missing-pane
|
||||
--no-launch` fills only the missing pane and keeps the seat's session, resume
|
||||
token and history. It never adopts and records no event. It refuses, writing
|
||||
nothing, when the binding already has a pane, the session has more than one
|
||||
pane or window, the pane changes between its two looks, a seat operation is
|
||||
running, or typing protection is on; an older daemon refuses the request
|
||||
rather than adopting ([#1564](https://github.com/mvschwarz/openrig/pull/1564)).
|
||||
- Keep a manual `rig compact` waiting when the seat's preparation stops at an
|
||||
ordinary approval. OpenRig never answers the approval; someone has to, inside
|
||||
the deadline ([#1562](https://github.com/mvschwarz/openrig/pull/1562)).
|
||||
- Refuse `rig ask --wake` for a runtime it can't wake, instead of silently
|
||||
trying a Claude wake ([#1561](https://github.com/mvschwarz/openrig/pull/1561)).
|
||||
- Resolve each ancestor's notes file inside refocus's trace instead of starting
|
||||
`rig scope resolve-notes` once per ancestor, which could use up the trace's
|
||||
time on a loaded machine
|
||||
([#1563](https://github.com/mvschwarz/openrig/pull/1563)).
|
||||
- Read fresh-launch fences through the existing node and type index in the
|
||||
fleet inventory read behind `rig ps` and each `rig tui` refresh. The query
|
||||
added in 0.6.13 scanned the whole event log, which on a long-lived install
|
||||
was slow whenever the database's pages weren't cached; it returns the same
|
||||
rows ([#1617](https://github.com/mvschwarz/openrig/pull/1617)).
|
||||
|
||||
### Off by default
|
||||
|
||||
- Add an optional Mac check, `transport.local_user_guard` (or
|
||||
`OPENRIG_TRANSPORT_LOCAL_USER_GUARD`), that makes the CLI refuse a local daemon
|
||||
another account runs when the daemon's `/healthz` report and two `ps` readings
|
||||
agree. Missing or disagreeing evidence, and an older daemon, keep today's
|
||||
behaviour; it isn't authentication. `/healthz` gains `euid` beside `pid`
|
||||
whether or not the check is on
|
||||
([#1566](https://github.com/mvschwarz/openrig/pull/1566)).
|
||||
|
||||
### Guidance and docs
|
||||
|
||||
- Add a getting-started page for people, `docs/reference/quickstart.md`, with a
|
||||
glossary; make `getting-started.md` the setup reference for the agent doing
|
||||
the setup; lead the README's install section with the `rigs` skill; and show
|
||||
what OpenRig changes on your machine before you install
|
||||
([#1565](https://github.com/mvschwarz/openrig/pull/1565)).
|
||||
- Drop three finished items from the roadmap
|
||||
([#1560](https://github.com/mvschwarz/openrig/pull/1560)).
|
||||
- Name the missing-pane repair where people meet the problem. The send refusal
|
||||
"Managed pane identity unavailable or changed; no input written." now names
|
||||
`rig reconcile-session <seat> --repair-missing-pane --no-launch`. The
|
||||
compaction refusal for a protected seat with no recorded pane says both fixes
|
||||
refuse while typing protection is on, how to turn it off and back on with the
|
||||
person's agreement, and that plain reconcile adopts into a new session row.
|
||||
The as-built CLI reference and the `openrig-user` skill list
|
||||
`--repair-missing-pane`, `rig config --help` lists
|
||||
`transport.local_user_guard`, and `rig ask --help`'s exit codes cover the
|
||||
`--wake` refusals. Text and help only
|
||||
([#1594](https://github.com/mvschwarz/openrig/pull/1594)).
|
||||
|
||||
### Dependency and install-script changes
|
||||
|
||||
- The README's skill and install-script commands, getting-started and the
|
||||
quickstart's example tag pin `v0.6.14`. No dependency changes.
|
||||
|
||||
### Known limitations
|
||||
|
||||
- The compaction and refocus fixes are tested without a native compaction or a
|
||||
loaded machine. Confirm a compaction by its restore request or the drop in
|
||||
context, and check that refocus showed the current work
|
||||
([#1562](https://github.com/mvschwarz/openrig/pull/1562),
|
||||
[#1563](https://github.com/mvschwarz/openrig/pull/1563)).
|
||||
- The pane repair refuses while typing protection is on; the send and
|
||||
compaction refusals say so, and protection goes off only if the person
|
||||
agrees. It covers a running launched seat whose pane was never recorded, and
|
||||
refuses others. A delivery held from before the repair can still refuse
|
||||
([#1564](https://github.com/mvschwarz/openrig/pull/1564),
|
||||
[#1594](https://github.com/mvschwarz/openrig/pull/1594)).
|
||||
- `rig context get reference/quickstart.md` doesn't resolve: the page ships
|
||||
under the package's `docs/reference/` but isn't in the reference context pack
|
||||
([#1565](https://github.com/mvschwarz/openrig/pull/1565)).
|
||||
- On a Mac with more than one user account, a second user's `rig` commands can
|
||||
reach the first user's daemon on its default port unless
|
||||
`transport.local_user_guard` is on. The check trusts the daemon's own report,
|
||||
identity can change between the check and the connection, and direct HTTP
|
||||
clients aren't covered ([#1566](https://github.com/mvschwarz/openrig/pull/1566)).
|
||||
- 0.6.13's other limitations still apply.
|
||||
|
||||
## [0.6.13]
|
||||
|
||||
0.6.13 is the quick follow-up to 0.6.12: fixes found by running teams on it
|
||||
every day, plus documentation. A Claude seat handed over in place waits for the
|
||||
new Claude before its packet is typed, Slack recovers missed messages in mapped
|
||||
channels, recent threads and relayed channels, Pi seats honour their reasoning
|
||||
effort, an OMP seat can be retried after a provider hiccup, and an existing seat
|
||||
can move to a new working folder. It is built from the commit tagged `v0.6.13`.
|
||||
The changes since 0.6.12:
|
||||
|
||||
### Before you upgrade
|
||||
|
||||
- Restart the daemon after upgrading the CLI (`rig daemon stop`, then `rig
|
||||
daemon start`). Migration `117` runs on every upgrade: it adds an empty,
|
||||
Codex-only `provider_account_readings` table, which fills only with
|
||||
`provider.usage_windows` on (off by default) and then keeps each resolved
|
||||
Codex account's readings across a restart
|
||||
([#1535](https://github.com/mvschwarz/openrig/pull/1535)). There is no
|
||||
migration `116`. From 0.6.11 or earlier, 0.6.12's twelve migrations run first.
|
||||
- Expect the bundled openrig-core plugin to go from 0.1.13 to 0.1.14: generated
|
||||
Claude hook commands call the daemon's Node by its absolute path, and existing
|
||||
bare-`node` entries refresh automatically
|
||||
([#1545](https://github.com/mvschwarz/openrig/pull/1545)).
|
||||
- Expect a rig name containing a dot to be refused before anything launches,
|
||||
including when you add a member to an existing one or retry its first start,
|
||||
because tmux would rename the session
|
||||
([#1500](https://github.com/mvschwarz/openrig/pull/1500)).
|
||||
- Expect a Pi seat's `effort` to reach Pi as `--thinking` from its next managed
|
||||
launch, restore or handover
|
||||
([#1528](https://github.com/mvschwarz/openrig/pull/1528)); managed Claude
|
||||
seats to receive `EDITOR`, `VISUAL`, `DISPLAY`, `XAUTHORITY` and an absolute
|
||||
`WAYLAND_DISPLAY` from their next launch
|
||||
([#1546](https://github.com/mvschwarz/openrig/pull/1546)); and the shipped
|
||||
agent specs' role file to count as the seat's role after a fresh launch
|
||||
([#1522](https://github.com/mvschwarz/openrig/pull/1522)).
|
||||
- Expect `rig restore-check` to report caveats where it was green for hooks,
|
||||
still exiting 0 ([#1537](https://github.com/mvschwarz/openrig/pull/1537)), and
|
||||
Slack recovery to scan threads inside its seven-day window once from their
|
||||
root on the first start
|
||||
([#1536](https://github.com/mvschwarz/openrig/pull/1536)).
|
||||
- Expect new output, with nothing removed: a model line per seat after `rig ps
|
||||
--nodes`'s table ([#1493](https://github.com/mvschwarz/openrig/pull/1493)), an
|
||||
input-box note on `rig capture`'s stderr
|
||||
([#1490](https://github.com/mvschwarz/openrig/pull/1490)), the
|
||||
`caller_cancelled` send refusal
|
||||
([#1513](https://github.com/mvschwarz/openrig/pull/1513)), and JSON fields
|
||||
`modelObservation`, `inputBox`, `signIn`, the Codex restart report's gate
|
||||
fields and `/api/attention` update items
|
||||
([#1508](https://github.com/mvschwarz/openrig/pull/1508),
|
||||
[#1502](https://github.com/mvschwarz/openrig/pull/1502),
|
||||
[#1054](https://github.com/mvschwarz/openrig/pull/1054)).
|
||||
|
||||
### Highlights
|
||||
|
||||
- Check a fresh in-place Claude handover's ready result against the current
|
||||
screen, so the packet waits for the successor instead of reaching a shell
|
||||
([#1553](https://github.com/mvschwarz/openrig/pull/1553), part of
|
||||
[#1544](https://github.com/mvschwarz/openrig/issues/1544)).
|
||||
- Recover Slack messages in the channel map's channels, replies in tracked
|
||||
threads whose root was posted or which closed in the last seven days, and
|
||||
channels a hub relays to a machine without its own connection; recovered
|
||||
replies land late and never answer a decision, and a rate limit holds its
|
||||
method across a restart
|
||||
([#1536](https://github.com/mvschwarz/openrig/pull/1536),
|
||||
[#739](https://github.com/mvschwarz/openrig/issues/739)).
|
||||
- Send a Pi seat's `effort` as `--thinking`, with `rig seat set-effort` and `rig
|
||||
seat effort` for Pi ([#1528](https://github.com/mvschwarz/openrig/pull/1528));
|
||||
let an OMP seat take an ordinary retry after a typed transient provider
|
||||
failure ([#1550](https://github.com/mvschwarz/openrig/pull/1550)).
|
||||
- Add `rig seat set-cwd <seat> --cwd <absolute-path> --reason <text>` to save an
|
||||
existing seat's working folder for its next fresh launch or rebuild handover
|
||||
([#1498](https://github.com/mvschwarz/openrig/pull/1498), finishing
|
||||
[#1154](https://github.com/mvschwarz/openrig/pull/1154)).
|
||||
- Show the model each seat was last seen using in `rig ps --nodes`, `rig roster
|
||||
show` and `rig roster find`
|
||||
([#1493](https://github.com/mvschwarz/openrig/pull/1493)).
|
||||
- Type nothing later for a send whose caller went away before input started
|
||||
([#1513](https://github.com/mvschwarz/openrig/pull/1513)); read Claude Code's
|
||||
MCP approval menu ([#1549](https://github.com/mvschwarz/openrig/pull/1549)),
|
||||
Codex's current folder-trust panel
|
||||
([#1494](https://github.com/mvschwarz/openrig/pull/1494)) and a Codex pane's
|
||||
old menu above an empty composer
|
||||
([#1548](https://github.com/mvschwarz/openrig/pull/1548)) correctly.
|
||||
- Read Linux processes from `/proc` on a worker thread instead of forking `ps`
|
||||
([#1385](https://github.com/mvschwarz/openrig/pull/1385)).
|
||||
|
||||
### Seats, launch and restore
|
||||
|
||||
- Prove a Claude launched through a wrapper script by its executable path
|
||||
([#1540](https://github.com/mvschwarz/openrig/pull/1540)); clear a failed
|
||||
restore's warning after a ready fresh launch
|
||||
([#1527](https://github.com/mvschwarz/openrig/pull/1527)); read a complete
|
||||
idle screen as idle through terminal repaints
|
||||
([#1521](https://github.com/mvschwarz/openrig/pull/1521)).
|
||||
- Name the Codex startup screen that stopped `rig seat restart
|
||||
--keep-conversation`
|
||||
([#1502](https://github.com/mvschwarz/openrig/pull/1502)); print the Pi/OMP
|
||||
READY line once per ready transition
|
||||
([#1401](https://github.com/mvschwarz/openrig/pull/1401)); and report a
|
||||
guidance block's real file in the node-detail API
|
||||
([#1523](https://github.com/mvschwarz/openrig/pull/1523)).
|
||||
|
||||
### Sends, wakes and queue
|
||||
|
||||
- Send one message for a seat's parked rows that fall due in the same pass
|
||||
([#1507](https://github.com/mvschwarz/openrig/pull/1507)); route a stuck-item
|
||||
finding on a person's item to the agent that closed it
|
||||
([#1495](https://github.com/mvschwarz/openrig/pull/1495), finishing
|
||||
[#1177](https://github.com/mvschwarz/openrig/pull/1177)); and trim spaces in
|
||||
`rig queue list --state`
|
||||
([#1501](https://github.com/mvschwarz/openrig/pull/1501)).
|
||||
|
||||
### Status, TUI and web UI
|
||||
|
||||
- Show a sign-in fact on each provider account row
|
||||
([#1508](https://github.com/mvschwarz/openrig/pull/1508)); show open updates
|
||||
for a person in the TUI's Feed
|
||||
([#1054](https://github.com/mvschwarz/openrig/pull/1054)); open a seat's
|
||||
terminal in the Herdr the shared TUI is viewed from, on Linux
|
||||
([#1356](https://github.com/mvschwarz/openrig/pull/1356)); and show an aborted
|
||||
workflow as aborted in the web UI
|
||||
([#1276](https://github.com/mvschwarz/openrig/pull/1276)).
|
||||
- Keep a Codex account's last reading, its reset and its last observed
|
||||
exhaustion across a daemon restart, shown as history rather than a current
|
||||
reading, and show an expectation, never a reading, when a window's reset has
|
||||
passed ([#1535](https://github.com/mvschwarz/openrig/pull/1535)).
|
||||
|
||||
### Workflows, commands and messages
|
||||
|
||||
- Clear a redriven workflow's old completion time
|
||||
([#1517](https://github.com/mvschwarz/openrig/pull/1517)); accept a rig name
|
||||
in `rig grow` and `rig remove`
|
||||
([#1407](https://github.com/mvschwarz/openrig/pull/1407)); give working cmux
|
||||
remedies in `rig doctor`
|
||||
([#1514](https://github.com/mvschwarz/openrig/pull/1514)) and real remedies
|
||||
for an edited projected file
|
||||
([#1512](https://github.com/mvschwarz/openrig/pull/1512)); and print the
|
||||
daemon's message from `rig provider`
|
||||
([#1510](https://github.com/mvschwarz/openrig/pull/1510)).
|
||||
|
||||
### Off by default
|
||||
|
||||
- Keep the refocus work packet off for both runtimes, and bound its fields when
|
||||
`OPENRIG_REFOCUS_WORK_PACKET=1` turns it on
|
||||
([#1542](https://github.com/mvschwarz/openrig/pull/1542)).
|
||||
|
||||
### Guidance and docs
|
||||
|
||||
- Correct the README's machine effects
|
||||
([#1510](https://github.com/mvschwarz/openrig/pull/1510)); say one working
|
||||
account is enough and list what starting OpenRig writes
|
||||
([#1539](https://github.com/mvschwarz/openrig/pull/1539)); give a standard Mac
|
||||
user a user-local Node.js route and say what the registry's checks decide
|
||||
([#1557](https://github.com/mvschwarz/openrig/pull/1557)); explain an adopted
|
||||
seat's restart refusal
|
||||
([#1509](https://github.com/mvschwarz/openrig/pull/1509)) and comparing token
|
||||
use across machines ([#1505](https://github.com/mvschwarz/openrig/pull/1505));
|
||||
and list the community-built Rig HQ viewer
|
||||
([#1515](https://github.com/mvschwarz/openrig/pull/1515)).
|
||||
|
||||
### Dependency and install-script changes
|
||||
|
||||
- End the one-line installer with `INSTALLED, SETUP NOT COMPLETE` when `rig
|
||||
setup` isn't ready, keeping setup's exit status
|
||||
([#1547](https://github.com/mvschwarz/openrig/pull/1547)).
|
||||
|
||||
### Tests and repository checks
|
||||
|
||||
- Tie managed `/compact` and parked refocus to their hook effects in tests
|
||||
([#1538](https://github.com/mvschwarz/openrig/pull/1538)); remove a test
|
||||
fixture's calendar expiry
|
||||
([#1559](https://github.com/mvschwarz/openrig/pull/1559)); and carry 0.6.12's
|
||||
versions and release text onto development main
|
||||
([#1558](https://github.com/mvschwarz/openrig/pull/1558)).
|
||||
|
||||
### Known limitations
|
||||
|
||||
- A fresh in-place Claude handover can still report ready early when the screen,
|
||||
pane or process can't be read or proven
|
||||
([#1553](https://github.com/mvschwarz/openrig/pull/1553)).
|
||||
- Slack recovery doesn't catch up older open threads, doesn't prune its thread
|
||||
coverage file yet, and one Slack app across machines hasn't run on two real
|
||||
machines ([#1536](https://github.com/mvschwarz/openrig/pull/1536),
|
||||
[#1436](https://github.com/mvschwarz/openrig/pull/1436)).
|
||||
- Managed compaction's 600-character budget rests on one comparison, nothing
|
||||
confirms compaction ran, two hook runs for one seat aren't serialized, and the
|
||||
plugin's manifest hooks still call bare `node`
|
||||
([#1483](https://github.com/mvschwarz/openrig/pull/1483),
|
||||
[#1344](https://github.com/mvschwarz/openrig/pull/1344),
|
||||
[#1475](https://github.com/mvschwarz/openrig/pull/1475),
|
||||
[#1545](https://github.com/mvschwarz/openrig/pull/1545)).
|
||||
- `rig seat set-cwd` checks the new folder once, and background work can use it
|
||||
before you switch ([#1498](https://github.com/mvschwarz/openrig/pull/1498)).
|
||||
- `rig policy current` and plain `rig ps` don't show a policy stored with
|
||||
`--seat` ([#1285](https://github.com/mvschwarz/openrig/pull/1285)); `rig up
|
||||
--env` isn't forwarded for flat-node specs or v1 bundles
|
||||
([#1294](https://github.com/mvschwarz/openrig/pull/1294)); the draft-aware
|
||||
guard can delay other work for a seat
|
||||
([#1481](https://github.com/mvschwarz/openrig/pull/1481)).
|
||||
- On a Mac with more than one user account, a second user's `rig` commands can
|
||||
reach the first user's daemon on its default port; this release doesn't change
|
||||
that.
|
||||
- Managed compaction, refocus after a compaction and snapshot restore behave as
|
||||
in 0.6.12, and 0.6.12's other carried limitations still apply
|
||||
([#1483](https://github.com/mvschwarz/openrig/pull/1483)).
|
||||
|
||||
## [0.6.12]
|
||||
|
||||
0.6.12 is a fix release about recovery and everyday operation, most of it from
|
||||
|
||||
@@ -26,9 +26,9 @@ use:
|
||||
|
||||
```sh
|
||||
# Claude Code
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.6.12/skills/rigs --skill rigs --agent claude-code
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.6.14/skills/rigs --skill rigs --agent claude-code
|
||||
# Codex
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.6.12/skills/rigs --skill rigs --agent codex
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.6.14/skills/rigs --skill rigs --agent codex
|
||||
```
|
||||
|
||||
Then type `/rigs` in Claude Code, or `$rigs` in Codex, and say what you want built. The skill has your agent tell
|
||||
@@ -68,14 +68,14 @@ Requires Node.js 22 or 24 and tmux, on macOS or Linux, and one working Claude Co
|
||||
|
||||
### Install it yourself
|
||||
|
||||
This runs OpenRig's install script from the `v0.6.12` release. It checks Node.js and npm, installs the latest published `@openrig/cli` with `npm install -g`, runs the Node.js and SQLite check, then runs `rig setup --dry-run` and `rig setup`. The first line prints that plan and changes nothing:
|
||||
This runs OpenRig's install script from the `v0.6.14` release. It checks Node.js and npm, installs the latest published `@openrig/cli` with `npm install -g`, runs the Node.js and SQLite check, then runs `rig setup --dry-run` and `rig setup`. The first line prints that plan and changes nothing:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.6.12/scripts/install.sh | sh -s -- --dry-run
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.6.12/scripts/install.sh | sh
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.6.14/scripts/install.sh | sh -s -- --dry-run
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.6.14/scripts/install.sh | sh
|
||||
```
|
||||
|
||||
If a provider isn't signed in yet, the last step reports `FAILED [4/4]`, and `rig setup` lists the sign-in under "Some steps need attention". If the only remaining failures are provider sign-ins, the install steps finished; sign in to each selected provider as below and continue.
|
||||
If `rig setup` doesn't finish ready, for example because a provider isn't signed in yet, the script ends with `INSTALLED, SETUP NOT COMPLETE`: the CLI is installed, and `rig setup` lists what's left under "Some steps need attention". If the only items left are provider sign-ins, sign in to each selected provider as below and continue; run `rig setup` again rather than reinstalling the CLI.
|
||||
|
||||
**Or step by step:**
|
||||
|
||||
|
||||
@@ -532,6 +532,7 @@ the sections that were not read are `null`.
|
||||
Wake seat targets must include the rig: `member@rig` or `member@rig@N` (1 = newest tenure).
|
||||
Values without `@` go unchanged to the runtime as a session ID or title; a bare member
|
||||
name is not resolved within `<rig>`.
|
||||
Only Claude and Codex tenures can be woken.
|
||||
|
||||
### chatroom
|
||||
|
||||
@@ -781,7 +782,7 @@ name is not resolved within `<rig>`.
|
||||
|
||||
| Invocation | Aliases | Declared options |
|
||||
|---|---|---|
|
||||
| `rig reconcile-session <session>` | — | `--rig <rigId>`<br>`--node <logicalId>`<br>`--no-launch`<br>`--json` |
|
||||
| `rig reconcile-session <session>` | — | `--rig <rigId>`<br>`--node <logicalId>`<br>`--no-launch`<br>`--repair-missing-pane`<br>`--json` |
|
||||
|
||||
### env
|
||||
|
||||
|
||||
@@ -177,19 +177,20 @@ You can also read [`skills/rigs/SKILL.md`](../../skills/rigs/SKILL.md) directly.
|
||||
script will do and changes nothing:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.6.12/scripts/install.sh | sh -s -- --dry-run
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.6.14/scripts/install.sh | sh -s -- --dry-run
|
||||
```
|
||||
|
||||
The same command without `--dry-run`,
|
||||
`curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.6.12/scripts/install.sh | sh`, installs the
|
||||
`curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.6.14/scripts/install.sh | sh`, installs the
|
||||
latest published `@openrig/cli` with `npm install -g`,
|
||||
runs the Node.js and SQLite check, then `rig setup --dry-run` and `rig setup`. `rig setup` checks both Claude Code and
|
||||
Codex and may install a missing one, as described below. A failed step prints
|
||||
`FAILED [n/4] <command or check> (exit <code>)`. Where a provider isn't signed in yet, step 4 ends that way and
|
||||
`rig setup` lists each sign-in under "Some steps need attention". If the only remaining failures are provider
|
||||
sign-ins, the install steps finished: sign in to each selected provider as below, then continue at
|
||||
[Start the kernel](#start-the-kernel-and-check-its-state). To install only what the selected providers need, go step
|
||||
by step instead:
|
||||
Codex and may install a missing one, as described below. A failed step among the first three prints
|
||||
`FAILED [n/4] <command or check> (exit <code>)`. When `rig setup` (step 4) doesn't finish ready, for example because a
|
||||
provider isn't signed in yet, the script ends with `INSTALLED, SETUP NOT COMPLETE (exit <code>)`: the CLI is
|
||||
installed, and `rig setup` lists what's left under "Some steps need attention". If the only items left are provider
|
||||
sign-ins, sign in to each selected provider as below, then continue at
|
||||
[Start the kernel](#start-the-kernel-and-check-its-state); run `rig setup` again rather than reinstalling the CLI. To
|
||||
install only what the selected providers need, go step by step instead:
|
||||
|
||||
### Choose your providers
|
||||
|
||||
@@ -1040,6 +1041,37 @@ same-seat stop/resume path after checking its retained policy; do not erase the
|
||||
rig or start a duplicate to reset permissions. A resume can reapply the stored
|
||||
launch mode, so verify the native mode again before continuing work.
|
||||
|
||||
## Optional Mac local-user check
|
||||
|
||||
On a Mac shared by multiple OS accounts, `transport.local_user_guard` can reduce
|
||||
accidental CLI routing to another account's local daemon. It is **off by default**.
|
||||
Enable it with `rig config set transport.local_user_guard true`, or set
|
||||
`OPENRIG_TRANSPORT_LOCAL_USER_GUARD=true` in the CLI's environment. Set it to
|
||||
`false` to turn it off; no daemon restart is needed for the CLI check.
|
||||
|
||||
When enabled, automatic local CLI data requests compare the daemon's reported
|
||||
`/healthz` PID and effective UID (`euid`) with two `ps` UID/start observations and
|
||||
a second health response, with at most one second of observation per check.
|
||||
Only stable agreement on a foreign effective UID stops the request. This does
|
||||
not require visibility into another user's socket descriptors. It does not
|
||||
stop the daemon, choose another port or retry. Diagnostic health/status checks
|
||||
remain available. Older daemons without `euid` remain unknown and are permitted.
|
||||
`sudo rig …` uses the caller's **effective** UID, so an enabled check against a
|
||||
user-owned daemon refuses when its reported/observed UID differs from root.
|
||||
|
||||
Explicit `OPENRIG_URL`/`RIGGED_URL` targets and registered remote hosts retain
|
||||
their selected routing, including tunnels. An internally resolved local URL is
|
||||
still checked. Default UI and TUI openings are checked before launch; this does
|
||||
not police later browser or TUI connections.
|
||||
|
||||
Missing, disagreeing, restricted or timed-out evidence **continues today's
|
||||
behavior**. This is a partial accidental-routing check, not authenticated user
|
||||
isolation: it trusts the daemon's PID/UID report and corroborates that process;
|
||||
it does not establish the socket's owner. Identity can change between observation
|
||||
and connection, and equal UIDs do not distinguish separate instances belonging
|
||||
to one account. The check
|
||||
does not add authentication to the daemon or protect direct HTTP clients.
|
||||
|
||||
## Custom settings and precedence
|
||||
|
||||
- **OpenRig:** member `permission_policy` overrides rig `permission_policy`.
|
||||
|
||||
@@ -41,7 +41,7 @@ npx skills add https://github.com/mvschwarz/openrig/tree/<release-tag>/skills/ri
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/<release-tag>/skills/rigs --skill rigs --agent codex
|
||||
```
|
||||
|
||||
For a new install, use the latest release's tag, for example `v0.6.12`. If OpenRig is already installed, use the tag
|
||||
For a new install, use the latest release's tag, for example `v0.6.14`. If OpenRig is already installed, use the tag
|
||||
that matches `rig --version`, or upgrade first. If the command fails with `npx: command not found`, Node.js isn't
|
||||
installed or isn't on your PATH yet (see above). Then type `/rigs` in Claude Code, or `$rigs` in Codex, and say what
|
||||
you want built.
|
||||
|
||||
+63
-165
@@ -1,189 +1,87 @@
|
||||
# What changed in OpenRig 0.6.12
|
||||
# What changed in OpenRig 0.6.14
|
||||
|
||||
This note is for an agent on an install that was just upgraded to 0.6.12. It lists only what behaves differently from
|
||||
0.6.11 and what to do about it. For everything OpenRig can do, read the capability map:
|
||||
This note is for an agent on an install that was just upgraded to 0.6.14. It lists only what behaves differently from
|
||||
0.6.13 and what to do about it. For everything OpenRig can do, read the capability map:
|
||||
`rig context get onboarding-width/public-what-you-can-do.md`. Each release replaces this file, so
|
||||
`rig context get reference/whats-new.md` describes the installed version once the upgraded daemon is running. Without
|
||||
a running daemon, read `$OPENRIG_HOME/reference/whats-new.md`. The daemon refreshes that copy when it starts, so right
|
||||
after an upgrade it can still hold the previous version's note.
|
||||
after an upgrade it can still hold the previous version's note. Coming from 0.6.12 or earlier, also read 0.6.13's
|
||||
release notes: its changes and migration `117` arrive with this upgrade.
|
||||
|
||||
## After the upgrade
|
||||
|
||||
- **Restart the daemon** (`rig daemon stop`, then `rig daemon start`). Twelve migrations run on the first start,
|
||||
whatever you use: `100`, `104`, `105`, `107` to `115`. They add columns and tables for the typing guard's modes, a
|
||||
seat's saved effort, Claude's cost estimate, Pi usage, mode and posture categories, workflow scope, seat environment
|
||||
values, the Codex `--no-daemon` record and `sessions.agent_app`; build two indexes for recent queue activity (about
|
||||
10 to 12 MB on a long-running install); and rebuild the Slack thread map. There are no migrations `102` or `106`.
|
||||
- **Claude seats now run the openrig-core plugin's hooks.** The bundled plugin goes from 0.1.9 to 0.1.13. A Claude
|
||||
seat whose plugins include `openrig-core` (every shipped agent spec selects it) starts with `CLAUDE_CODE_PLUGIN_DIRS`
|
||||
pointing at OpenRig's copy, so the activity relay, the compaction save and restore hooks and refocus actually run. It
|
||||
needs Claude Code 2.1.280 or later, which nothing checks. A running seat picks it up at its next launch, restore or
|
||||
handover. The refocus hook may take up to 60 seconds, so a hung refocus can hold a prompt that long.
|
||||
- **Six new settings, all off except one:** `output.whoami_peers` (8, used only while `output.compact` is on),
|
||||
`bundle.services_enabled`, `provider.subscription_moves`, `usage.pi_cost_facts`, `usage.pi_openrouter_charge` and
|
||||
`transport.verify_input_box`. See "Off by default" below.
|
||||
- **Restart the daemon** (`rig daemon stop`, then `rig daemon start`). No migration runs. One setting is added,
|
||||
`transport.local_user_guard`, off by default (see below).
|
||||
- **The bundled openrig-core plugin goes from 0.1.14 to 0.1.15.** Installed copies are replaced and carry refocus's own
|
||||
notes lookup.
|
||||
|
||||
## Output that changed shape
|
||||
|
||||
- **Exit codes:** eleven client-fixable `rig queue update` refusals now answer HTTP 400 and exit 1 instead of 500 and
|
||||
exit 2 (`blocker_not_found`, `blocker_not_live` and `blocker_malformed` still exit 2). `rig ask` exits 1 for an
|
||||
unknown or ambiguous rig. `rig compact` exits 1 (was 2) for `pane_unknown_protected`, and its route answers 409 (was
|
||||
502). Over the shared transport, a write that throws without a code makes `rig send` exit 1 with HTTP 409 instead of 2
|
||||
with 500, and a guarded send whose text was written returns `sent: true` with `inputPhase: "written"`.
|
||||
- **`rig compact` and `rig compact --state` add a `compact` result:** `submitted_unconfirmed` (shown as "submitted,
|
||||
unconfirmed"), `uncertain` or `not_sent`. "Submitted" means `/compact` and Enter reached the seat, not that compaction
|
||||
ran.
|
||||
- **`rig capture --lines N --json`** adds `activity: "unknown"` and `activityNote` when lines were cut.
|
||||
- **`rig start --last --json`** carries `keptDown` (always) and `warnings` (`seat_status_unreadable`).
|
||||
- **`rig queue show <id> --full`** on a closed row adds `closingTransition`; plain `rig queue show` on such a row reads
|
||||
`readView.complete: false` and lists it in `omittedFields`. `rig queue show` returns `resolution` when a close stored
|
||||
one.
|
||||
- **After `rig down`,** `rig ps`, the TUI and the rig summary read the rig `stopped`, not recoverable, and
|
||||
`hasLiveAgents` can read `false`.
|
||||
- **`rig seat status`** reads `Typing guard: on (hold)` for hold, and its JSON's `desired` and `effective` mean hold only;
|
||||
`desiredMode` and `effectiveMode` give the mode.
|
||||
- **`rig usage top`** measures tokens and rates within one conversation and one window, so figures can drop and more
|
||||
seats read `insufficient_samples`.
|
||||
- **The execution view** binds a row to a mission by its exact `mission:` tag first; `q1_lanes[].waiting` and
|
||||
`folded.merge_report` are new.
|
||||
- **Smaller:** `rig create --json` carries `detail` on failure; `rig status --json` is new; `rig mcp serve` lists 23 tools
|
||||
(five queue tools added); a JSON body of `null` gets 400 `invalid_json`; `GET /api/events` takes `?replay=`; a Slack
|
||||
seat post's sender line ends `on <host id>`; `rig ps --nodes` `resumeCommand` can carry `--no-daemon`.
|
||||
- **`rig ask --wake`** on a seat whose runtime isn't Claude or Codex prints `Wake runtime '<runtime>' … is not
|
||||
supported. Supported runtimes are claude and codex.` A `member@rig` target exits 2 (the daemon refuses it); a raw-id
|
||||
`--runtime` value exits 1 (the CLI refuses it). No wake starts.
|
||||
- **`/healthz`** gains `euid`, the daemon's effective user ID, beside `pid`, whether or not the Mac check is on.
|
||||
- **`rig compact`** (manual) waits up to 240 seconds for its result, up from 180, because preparation now has 180
|
||||
seconds, up from 120.
|
||||
|
||||
## Seats and restore
|
||||
|
||||
- **`rig start --last` leaves down a rig stopped on purpose** (`rig down` or `rig seat stop`) and prints how to bring it
|
||||
back: `rig start --rigs <rig>`, or `rig up '<name>' --existing`.
|
||||
- **An ordinary terminal seat relaunches fresh on restore** and re-runs its startup actions; don't answer `--fresh` for
|
||||
it. A missing required startup file for such a seat blocks the whole restore before anything launches.
|
||||
- **Restore stops a seat whose current runtime differs from its snapshot's,** before launching it; `--fresh` doesn't
|
||||
bypass it. Restore from a snapshot taken with the seat's current runtime.
|
||||
- **A slow Claude restore ends `attention_required`, not failed:** "Claude resume did not finish within Ns; Claude may
|
||||
still be starting, so the launch was kept". Look at the pane, and once Claude is up run `rig seat clear-attention`.
|
||||
Each such seat can add up to about the readiness timeout, one after another, so `rig up` can pass its 120-second
|
||||
wait while the daemon keeps working: check `rig ps` before running it again.
|
||||
- **"Returned to a shell" now means the process tree shows an idle shell.** "The pane shows a shell that is not a proven
|
||||
idle shell; a launch script may still be starting …" means the seat may still be coming up. A Pi or OMP seat at a
|
||||
bare shell reads `attention_required`.
|
||||
- **A Codex fork whose new conversation ID arrives late** stops at `attention_required` with its startup context kept;
|
||||
let it finish starting, then run `rig seat continue <seat>`. A handover to a Codex fork can complete with `Fork id:
|
||||
PENDING`.
|
||||
- **A seat whose startup failed for a recoverable reason** becomes ready after a successful send or capture.
|
||||
- **`rig launch` takes a rig's name as well as its ID;** a name two active rigs share stops with `rig_ambiguous`.
|
||||
- **`rig add`, single-seat `rig launch` and `rig launch --seats` wait up to 120 seconds.** A timeout still isn't a
|
||||
rollback.
|
||||
- **A runtime `--version` check gives up after 30 seconds.** `rig bootstrap` blocks on a runtime that's installed but
|
||||
fails its check, reported apart from one that's missing; `rig up`'s preflight fails a Claude, Codex or OMP check that
|
||||
doesn't answer (a warning for Pi).
|
||||
- **Claude login detection:** only a current refusal reads `login_required`, not the "Not logged in" footer alone.
|
||||
- **Model pins:** `fable` now names `claude-fable-5-1`, a trailing `[1m]` is set aside, and pinned Pi seats are checked
|
||||
(a `:high`-style thinking suffix is accepted).
|
||||
- **A custom permission policy saved with CRLF line endings** now takes effect at the next launch or restore, instead
|
||||
of the advisory floor.
|
||||
- **`rig scope mission create`** writes `status: planned`; set it `active` to count it in `rig proof show --project`.
|
||||
With `--workspace <project>`, missions go under the project's declared `missions.root`.
|
||||
|
||||
## Sends, wakes and the queue
|
||||
|
||||
- **A send to a pane in tmux copy or view mode** leaves the mode and delivers; `pane_in_mode` means the pane couldn't
|
||||
leave its mode. Leave it in that pane, then send again.
|
||||
- **`rig send --dangerously-interact` types exactly what you give it.** Enter is added only for a complete answer
|
||||
staged in a recognised Claude Code or Codex input box; anywhere else, put CR in the payload. With `--verify` it reads
|
||||
unchecked.
|
||||
- **A Codex seat reads "needs you" only when a real prompt is visible;** a pending permission request reads
|
||||
`permission_request_pending` first, and an unreadable pane gets `permission_request_unverified` after a grace.
|
||||
Codex's `»` prompt is recognised.
|
||||
- **Managed `/compact` is short enough to run and is sent only to an idle seat.** Confirm compaction by effect: the
|
||||
restore request, or the drop in context.
|
||||
- **A held-back (`retained`) wake doesn't use up a one-shot park timer;** it's tried again at the interval. You don't
|
||||
need to re-arm after one.
|
||||
- **A retired watchdog doesn't deliver a late wake.** A row parked on its owner's own repeating reminder reads
|
||||
`servedByOwnReminder` in `rig parked` and draws no extra parked-owner wake.
|
||||
- **`rig queue update <id> --summary "<text>"`** alone corrects a row's subject. Close with **`--resolution
|
||||
"<outcome>"`** to store the outcome on the row.
|
||||
- **`rig remove` and `rig shrink`** keep a seat that gets work while stopping (409, `retained: true`) unless
|
||||
`--fallback` takes it. A stale `rig unclaim` is refused with `session_not_current`.
|
||||
- **A seat that refuses every send with "Managed pane identity unavailable or changed; no input written."** because an
|
||||
older OpenRig never recorded its tmux pane can be repaired in place: `rig reconcile-session <seat>
|
||||
--repair-missing-pane --no-launch`. It fills only the missing pane and keeps the session, resume token and occupant
|
||||
generation; the result names the same session, with continuity unverified. It refuses, writing nothing, with
|
||||
`repair_not_applicable` (the binding already has a pane), `pane_ambiguous` (more than one pane or window),
|
||||
`pane_missing` (no PID), `guard_target_changed`, `guard_operation_in_progress`, `typing_guard_enabled` or
|
||||
`repair_timed_out`. Against an older daemon it reads `repair_missing_pane_unsupported` and never adopts. It records no
|
||||
event.
|
||||
- **A manual `rig compact` whose preparation stops at an ordinary approval** keeps waiting within its deadline instead
|
||||
of giving up. OpenRig never answers the approval: the person, or an agent allowed to, answers it.
|
||||
- **Refocus resolves each ancestor's notes file itself** (`NOTES.md`, then `MISSION_NOTES.md`) instead of starting `rig
|
||||
scope resolve-notes` per ancestor. Its output, budgets and settings are unchanged.
|
||||
|
||||
## New things you can do
|
||||
|
||||
- **Set one seat's reasoning effort:** `rig seat set-effort <seat> --effort <level> --reason "<why>"` (or `--clear`); it
|
||||
applies at the next managed launch, not to the running process. Check it with `rig seat effort <seat>`.
|
||||
- **Change a seat's stored permission policy:** `rig policy apply <policy> --seat <seat> --reason "<why>"`. `rig policy
|
||||
current` and plain `rig ps` don't show it; `rig seat status` does.
|
||||
- **Give seats environment values:** `env:` on the rig or a member in `rig.yaml`, or `rig up <spec> --env KEY=VALUE` for
|
||||
one launch. Never put secrets there.
|
||||
- **Scope a workflow run:** `--project`, `--mission`, `--slice` and `--tags` on `rig workflow instantiate` and `run`;
|
||||
write a target as `member@{rig}` to follow the bound rig.
|
||||
- **Read cost:** `rig usage cost [--seat <session>] [--json]`. A missing figure is never $0, and Claude's figure is an
|
||||
estimate, not an invoice.
|
||||
- **Hand work over without a shell:** `rig mcp serve` with `OPENRIG_SESSION_NAME` set gives `rig_queue_create`,
|
||||
`rig_queue_show`, `rig_queue_list`, `rig_queue_claim` and `rig_queue_update`. Nothing wakes that agent.
|
||||
- **`rig status --json`:** treat `daemon.reachable: null` as unconfirmed, not down.
|
||||
- OpenRig 0.6.12 adds `rig community-harness list` for installed community-harness presets, showing their maintainer
|
||||
and declared limits. These harnesses are not tested by OpenRig. Support is launch-only: no resume or fork, no
|
||||
resource projection or startup-file delivery, and identity remains unconfirmed. A quiet screen is not proof that the
|
||||
seat is idle. (#1392, #1518)
|
||||
- **Guidance:** the `rigs` skill offers to join the team before the first operator message, and offers Codex users one
|
||||
removable rules file; `delegating-work` says to brief a peer by the model `rig ps --nodes --json` shows; the operator
|
||||
reads `$OPENRIG_HOME/reference/keeping-a-watch.md` when asked to watch or remind, including a low-allowance warning.
|
||||
- User-visible text says "harness" for the agent software around a model, and "community harness" for one an outside
|
||||
contributor maintains; `docs/reference/topology-naming.md` defines the terms. (#1506)
|
||||
- **Repair a missing pane without adopting:** `rig reconcile-session <seat> --repair-missing-pane --no-launch`.
|
||||
- **Point a person at a page written for them:** `docs/reference/quickstart.md` ("Getting started") is the first-use
|
||||
page, with a glossary. `docs/reference/getting-started.md` is now titled "OpenRig setup reference" and opens with what
|
||||
the agent doing the setup needs; its sections and anchors are unchanged, so `rig context get
|
||||
reference/getting-started.md#…` lookups still work.
|
||||
|
||||
## Off by default
|
||||
|
||||
- **`output.compact`** now also shortens `rig queue show` and `rig queue transitions` (`--full` for everything) and
|
||||
bounds `rig whoami --json` peers to `output.whoami_peers` (`--all-peers` lists all).
|
||||
- **`provider.usage_windows`** now also shows Claude's windows, each seat's configured account and, with
|
||||
`--all-hosts` or `--hosts`, every registered machine's windows. A Claude account's current availability always reads
|
||||
`unknown`: don't tell a person an account has room from a seat's reading.
|
||||
- **`usage.pi_cost_facts`** and **`usage.pi_openrouter_charge`** take effect when a Pi seat next launches or resumes, not
|
||||
at once; what was recorded stays visible after they're turned off.
|
||||
- **`provider.subscription_moves`** enables `rig provider moves`. The `openrig-user` skill's section for it mentions apps
|
||||
that don't ship in this release.
|
||||
- **`bundle.services_enabled`**, **`transport.verify_input_box`**, the per-seat draft-aware typing guard (`rig seat
|
||||
set-typing-guard <seat> --mode draft-aware`), `rig bundle create --include-login-files`, `OPENRIG_TMUX_CONTROL=1`
|
||||
and `OPENRIG_REFOCUS_BUDGET_MS` (4500 to 52000) each act only when chosen.
|
||||
- **The daemon runs tmux without `/bin/sh`** on Linux and macOS; `OPENRIG_TMUX_SHELL=1` at daemon start keeps the old
|
||||
path.
|
||||
- **A Mac local-user check.** On a Mac shared by several accounts, `rig config set transport.local_user_guard true` (or
|
||||
`OPENRIG_TRANSPORT_LOCAL_USER_GUARD=true` in the CLI's environment) makes the CLI refuse a local daemon another account
|
||||
runs. It compares the PID and effective UID the daemon reports in `/healthz` with two `ps` readings and a second
|
||||
health response, and refuses only when they agree on another account; the refusal names `rig daemon start --port
|
||||
<unused-port>`. Missing, disagreeing or timed-out evidence, and an older daemon without `euid`, keep today's
|
||||
behaviour. No daemon restart is needed. Explicit `OPENRIG_URL` targets and registered remote hosts keep their routing,
|
||||
later browser or TUI connections aren't checked, and with it on `sudo rig …` against a user-owned daemon refuses. It
|
||||
isn't authentication: it trusts the daemon's own report. Details: `rig context get
|
||||
reference/getting-started.md#optional-mac-local-user-check`.
|
||||
|
||||
## What to stop doing
|
||||
|
||||
- **Stop looking up a rig's ID before `rig launch`;** the name works.
|
||||
- **Stop expecting `rig start --last` to bring back a rig taken down on purpose;** use `rig start --rigs <rig>` or `rig
|
||||
up '<name>' --existing`.
|
||||
- **Stop counting on `--dangerously-interact` to press Enter;** put CR in the payload.
|
||||
- **Stop taking the preparation prompt's delivery as the compaction's result;** read the `compact` result and confirm by
|
||||
effect.
|
||||
- **Stop answering `--fresh` for an ordinary terminal seat on every restore.**
|
||||
- **Stop branching on exit 2 for a fixable `rig queue update` refusal;** it's exit 1.
|
||||
- **Stop recreating a seat, or editing the database, to change its permission policy;** use `rig policy apply --seat`.
|
||||
- **Don't use plain `rig reconcile-session` to fix a launched seat's missing pane.** It adopts the seat into a new
|
||||
session with no resume token; use `--repair-missing-pane --no-launch`.
|
||||
- **Don't abandon a manual `rig compact` because preparation hit an approval.** Answer it, or ask the person to, within
|
||||
the 180 seconds.
|
||||
- **Don't try `rig ask --wake` on a Pi or custom seat.** Use `rig ask` without `--wake` to search what it said, or send
|
||||
to the live seat.
|
||||
|
||||
## Known gaps in 0.6.12
|
||||
## Known gaps in 0.6.14
|
||||
|
||||
- **Managed compaction's 600-character budget** rests on one comparison on Claude Code 2.1.282, and nothing confirms
|
||||
compaction ran.
|
||||
- **The plugin hooks haven't run through this path in a real session,** and two hook runs for one seat aren't
|
||||
serialized: a post-compaction record can be lost or a refocus delivered twice.
|
||||
- **One Slack app across machines was simulated in one process,** never run on two real machines. Relayed events are
|
||||
trusted from the calling daemon, and `rig slack verify` on the hub doesn't check relayed channels.
|
||||
- **`env:` values aren't secret,** and members added with `rig grow` can't carry a member-level `env:`.
|
||||
- **`rig up --env` is not forwarded for flat-node specs or v1 bundles,** although the launch may succeed. Do not rely on
|
||||
the flag for those formats in this release.
|
||||
- **The draft-aware guard waits for idle inside the seat's delivery lease,** which can delay other work for that seat.
|
||||
- **With `bundle.services_enabled` on, `rig up <file>.rigbundle` installs into the current folder** when no `--target` is
|
||||
given.
|
||||
- **`rig seat restart --keep-conversation` supports Linux only,** and hasn't been re-run on 0.6.12.
|
||||
- **The rig status API can call a running rig `blocked`** because of one seat that isn't running: it folds a restore
|
||||
forecast into current status.
|
||||
- **A Codex seat can keep reading as at its usage limit** in a constructed screen case not seen in a real session.
|
||||
- **After `rig scope slice create` says it couldn't verify membership, the slice was kept.** Check `mission.yaml` before
|
||||
creating it again.
|
||||
- **A Claude Code seat whose permission request was approved automatically can still read "needs input";** the Codex
|
||||
case is fixed.
|
||||
- **Codex team seats aren't asked before lifecycle commands yet.**
|
||||
- **The send gate trusts the most recently recorded hook;** `rig capture` shows the real screen.
|
||||
- **A launched team still writes into its working folder;** the operator says what changes before you agree.
|
||||
- **After a restart, `rig status` shows `Kernel: skipped` whatever the kernel seats' state.** Read them with
|
||||
`rig ps --nodes --rig kernel --full`.
|
||||
- **`rig tui --shared` still attaches on the default tmux server,** so use `rig tui` when the daemon runs on a non-default
|
||||
tmux socket.
|
||||
- **The compaction and refocus fixes** are tested without a native compaction or a loaded machine: confirm a
|
||||
compaction by its restore request or the drop in context, and check that a restored seat is really working.
|
||||
- **The pane repair refuses while typing protection is on.** The compaction refusal for a protected seat whose pane
|
||||
isn't recorded still suggests plain `rig reconcile-session <seat> --no-launch`, which adopts. For a running launched
|
||||
seat whose pane was never recorded, use `rig reconcile-session <seat> --repair-missing-pane --no-launch` instead;
|
||||
it needs typing protection off, so turn protection off only if the person agrees. A delivery held from before the
|
||||
repair can still refuse.
|
||||
- **`rig context get reference/quickstart.md` doesn't resolve:** the page isn't in the reference context pack. Read it
|
||||
from the package's `docs/reference/` or the repository.
|
||||
- **On a Mac with more than one user account,** a second user's `rig` commands can reach the first user's daemon on
|
||||
its default port (7433) unless `transport.local_user_guard` is on. The check trusts the daemon's own report, identity
|
||||
can change between the check and the connection, and direct HTTP clients aren't covered.
|
||||
- **0.6.13's other known gaps still apply:** a fresh in-place Claude handover can report ready early when the screen,
|
||||
pane or process can't be read or proven; one Slack app across machines hasn't run on two real machines, and Slack
|
||||
recovery doesn't catch up older open threads; `rig seat set-cwd` checks the new folder once.
|
||||
Generated
+5
-5
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openrig",
|
||||
"version": "0.6.12",
|
||||
"version": "0.6.14",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openrig",
|
||||
"version": "0.6.12",
|
||||
"version": "0.6.14",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"packages/daemon",
|
||||
@@ -7068,7 +7068,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@openrig/cli",
|
||||
"version": "0.6.12",
|
||||
"version": "0.6.14",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -7103,7 +7103,7 @@
|
||||
},
|
||||
"packages/daemon": {
|
||||
"name": "@openrig/daemon",
|
||||
"version": "0.6.12",
|
||||
"version": "0.6.14",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.13.0",
|
||||
@@ -7145,7 +7145,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@openrig/ui",
|
||||
"version": "0.6.12",
|
||||
"version": "0.6.14",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openrig",
|
||||
"version": "0.6.12",
|
||||
"version": "0.6.14",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openrig/cli",
|
||||
"version": "0.6.12",
|
||||
"version": "0.6.14",
|
||||
"type": "module",
|
||||
"description": "Build your own network of agents from Claude Code, Codex and Pi: persistent teams with roles, shared context and owned work.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import { assessLocalDaemonOwner, ForeignLocalDaemonError, type LocalDaemonOwner } from "./local-daemon-owner.js";
|
||||
export { ForeignLocalDaemonError } from "./local-daemon-owner.js";
|
||||
import path from "node:path";
|
||||
import { ConfigStore } from "./config-store.js";
|
||||
import { getOpenRigHome, readOpenRigEnv } from "./openrig-compat.js";
|
||||
@@ -131,6 +133,7 @@ interface DaemonRequestOptions {
|
||||
}
|
||||
|
||||
interface DaemonClientOptions {
|
||||
ownerProbe?: (baseUrl: string, timeoutMs?: number) => Promise<LocalDaemonOwner>;
|
||||
fetchImpl?: typeof fetch;
|
||||
timeoutMs?: number;
|
||||
}
|
||||
@@ -155,6 +158,7 @@ export class DaemonClient {
|
||||
readonly baseUrl: string;
|
||||
private fetchImpl: typeof fetch = fetch;
|
||||
private timeoutMs = 5_000;
|
||||
private ownerProbe: (baseUrl: string, timeoutMs?: number) => Promise<LocalDaemonOwner>;
|
||||
/** Set by remoteDaemonClient; absent origin is labelled explicitly on remote requests. */
|
||||
originSelfHostId?: string;
|
||||
remoteTarget = false;
|
||||
@@ -213,9 +217,27 @@ export class DaemonClient {
|
||||
}
|
||||
|
||||
this.fetchImpl = options?.fetchImpl ?? fetch;
|
||||
this.ownerProbe = options?.ownerProbe ?? ((url, timeoutMs) => assessLocalDaemonOwner(url, { fetchImpl: this.fetchImpl, timeoutMs }));
|
||||
this.timeoutMs = options?.timeoutMs ?? 5_000;
|
||||
}
|
||||
|
||||
/** Shared with raw CLI data readers. Constructor URLs alone do not establish explicit intent. */
|
||||
localOwnerCheckEnabled(): boolean {
|
||||
if (this.remoteTarget) return false;
|
||||
const explicit = readOpenRigEnv("OPENRIG_URL", "RIGGED_URL");
|
||||
if (explicit && explicit.replace(/\/+$/, "") === this.baseUrl.replace(/\/+$/, "")) return false;
|
||||
let enabled = false;
|
||||
try { enabled = new ConfigStore().resolveWithSource("transport.local_user_guard").value === true; }
|
||||
catch { /* Unreadable owner evidence/config is not a new default-on refusal. */ }
|
||||
return enabled;
|
||||
}
|
||||
|
||||
async assertLocalOwner(timeoutMs = 1000): Promise<void> {
|
||||
if (!this.localOwnerCheckEnabled()) return;
|
||||
const owner = await this.ownerProbe(this.baseUrl, timeoutMs);
|
||||
if (owner.state === "foreign") throw new ForeignLocalDaemonError(this.baseUrl, owner);
|
||||
}
|
||||
|
||||
async get<T = unknown>(path: string, options?: DaemonRequestOptions): Promise<DaemonResponse<T>> {
|
||||
return this.requestJson<T>(path, { method: "GET" }, options);
|
||||
}
|
||||
@@ -266,6 +288,8 @@ export class DaemonClient {
|
||||
}
|
||||
|
||||
private async fetch(path: string, init: RequestInit, options?: DaemonRequestOptions, consumeResponse?: (response: Response) => Promise<void>): Promise<Response> {
|
||||
// Diagnostic health/status remains reachable even for a known foreign listener.
|
||||
if (path.split("?")[0] !== "/healthz") await this.assertLocalOwner();
|
||||
const timeoutMs = options?.timeoutMs ?? this.timeoutMs;
|
||||
if (options?.signal) init = { ...init, signal: options.signal };
|
||||
if (options?.headers) {
|
||||
|
||||
@@ -118,8 +118,11 @@ Examples:
|
||||
|
||||
Exit codes:
|
||||
0 Success
|
||||
1 Daemon not running, or the rig is unknown or ambiguous
|
||||
2 Failed to fetch data from daemon (or wake timed out)`);
|
||||
1 Daemon not running, the rig is unknown or ambiguous, or --runtime or
|
||||
--wake-timeout is invalid
|
||||
2 Failed to fetch data from daemon; the --wake seat has no such tenure, or its
|
||||
tenure isn't Claude or Codex or has no resume token; or the wake failed or
|
||||
timed out`);
|
||||
|
||||
const getDeps = (): AskCommandDeps => (depsOverride as AskCommandDeps | undefined) ?? {
|
||||
lifecycleDeps: realDeps(),
|
||||
|
||||
@@ -185,6 +185,7 @@ export function chatroomCommand(depsOverride?: StatusDeps): Command {
|
||||
// Direct SSE watch
|
||||
const url = `${client.baseUrl}/api/rigs/${encodeURIComponent(rigId)}/chat/watch`;
|
||||
try {
|
||||
await client.assertLocalOwner();
|
||||
const res = await fetch(url, {
|
||||
headers: { Accept: "text/event-stream" },
|
||||
});
|
||||
|
||||
@@ -110,6 +110,7 @@ Keys:
|
||||
usage.pi_cost_facts Pi seats report model-priced cost from their persisted session entries to rig usage cost (default off)
|
||||
usage.pi_openrouter_charge with usage.pi_cost_facts on, Pi seats look up OpenRouter's actual charge for their requests, shown beside the estimate (default off; changes take effect when a Pi seat next launches or resumes; earlier outcomes stay visible)
|
||||
transport.verify_input_box with --verify, a send to a Claude Code or Codex seat (also queue wakes) reads verified only when the seat was proven idle before the paste and its input box showed the exact text before Enter and was empty after it, on the same pane process; a busy or unknown seat, and anything else, reads unconfirmed, not failed. Reporting only (default off)
|
||||
transport.local_user_guard on a Mac, the CLI refuses a local daemon whose reported user, checked with ps, isn't yours; an explicit OPENRIG_URL skips the check. A mitigation for reaching the wrong daemon, not authentication (default off)
|
||||
|
||||
Precedence: CLI flag > environment variable > config file > default`)
|
||||
.action((opts: { json?: boolean; withSource?: boolean }) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import path from "node:path";
|
||||
import net from "node:net";
|
||||
import { execSync } from "node:child_process";
|
||||
import { resolveDaemonPath } from "../daemon-lifecycle.js";
|
||||
import { formatDaemonHostForUrl } from "../client.js";
|
||||
import { DaemonClient, ForeignLocalDaemonError, formatDaemonHostForUrl } from "../client.js";
|
||||
import { ConfigStore } from "../config-store.js";
|
||||
import { buildWritableHomeCheck } from "../system-preflight.js";
|
||||
import {
|
||||
@@ -313,7 +313,12 @@ async function buildSpecConformanceCheck(deps: DoctorDeps): Promise<DoctorCheck>
|
||||
return { name, status: "fail", message: `Rig spec at ${deps.specPath} declares no name or no pods.` };
|
||||
}
|
||||
|
||||
const liveIds = deps.fetchLiveLogicalIds ? await deps.fetchLiveLogicalIds(rigName) : null;
|
||||
let liveIds: string[] | null;
|
||||
try { liveIds = deps.fetchLiveLogicalIds ? await deps.fetchLiveLogicalIds(rigName) : null; }
|
||||
catch (error) {
|
||||
if (error instanceof ForeignLocalDaemonError) return { name, status: "fail", message: error.message };
|
||||
throw error;
|
||||
}
|
||||
if (liveIds === null) {
|
||||
return {
|
||||
name,
|
||||
@@ -389,6 +394,26 @@ function readTmuxMouseMode(deps: DoctorDeps): "on" | "off" | null {
|
||||
}
|
||||
}
|
||||
|
||||
/** Raw topology reads retain their existing headers; the owner policy is shared with the client. */
|
||||
export async function readLiveLogicalIds(base: string, rigName: string, ownerClient = new DaemonClient(base)): Promise<string[] | null> {
|
||||
try {
|
||||
await ownerClient.assertLocalOwner();
|
||||
const rigsRes = await fetch(`${base}/api/rigs`);
|
||||
if (!rigsRes.ok) return null;
|
||||
const rigs = (await rigsRes.json()) as Array<{ id?: string; name?: string }>;
|
||||
const rig = rigs.find((r) => r.name === rigName);
|
||||
if (!rig?.id) return null;
|
||||
await ownerClient.assertLocalOwner();
|
||||
const nodesRes = await fetch(`${base}/api/rigs/${rig.id}/nodes`);
|
||||
if (!nodesRes.ok) return null;
|
||||
const nodes = (await nodesRes.json()) as Array<{ logicalId?: string | null }>;
|
||||
return nodes.map((n) => n.logicalId ?? "");
|
||||
} catch (error) {
|
||||
if (error instanceof ForeignLocalDaemonError) throw error;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function doctorCommand(depsOverride?: DoctorDeps): Command {
|
||||
const cmd = new Command("doctor")
|
||||
.description("Check installation, provider authentication, writable paths, port/daemon and conditional cmux control")
|
||||
@@ -408,22 +433,15 @@ export function doctorCommand(depsOverride?: DoctorDeps): Command {
|
||||
mkdirp: (dirPath: string) => mkdirSync(dirPath, { recursive: true }),
|
||||
checkWritable: (dirPath: string) => accessSync(dirPath, constants.W_OK),
|
||||
specPath: opts.spec ? path.resolve(opts.spec) : undefined,
|
||||
// Returns null on ANY failure to read the live side. The conformance check treats null as
|
||||
// Returns null when the live side is unavailable; a known foreign owner is reported. The conformance check treats null as
|
||||
// "unknown" and skips — an unreachable daemon must never read as a matching topology.
|
||||
fetchLiveLogicalIds: async (rigName: string): Promise<string[] | null> => {
|
||||
try {
|
||||
const cfg = new ConfigStore().resolve();
|
||||
const base = `http://${formatDaemonHostForUrl(cfg.daemon.host ?? "127.0.0.1")}:${cfg.daemon.port ?? DEFAULT_PORT}`;
|
||||
const rigsRes = await fetch(`${base}/api/rigs`);
|
||||
if (!rigsRes.ok) return null;
|
||||
const rigs = (await rigsRes.json()) as Array<{ id?: string; name?: string }>;
|
||||
const rig = rigs.find((r) => r.name === rigName);
|
||||
if (!rig?.id) return null;
|
||||
const nodesRes = await fetch(`${base}/api/rigs/${rig.id}/nodes`);
|
||||
if (!nodesRes.ok) return null;
|
||||
const nodes = (await nodesRes.json()) as Array<{ logicalId?: string | null }>;
|
||||
return nodes.map((n) => n.logicalId ?? "");
|
||||
} catch {
|
||||
return await readLiveLogicalIds(base, rigName);
|
||||
} catch (error) {
|
||||
if (error instanceof ForeignLocalDaemonError) throw error;
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DaemonClient, ForeignLocalDaemonError } from "../client.js";
|
||||
import { Command } from "commander";
|
||||
import { getDaemonStatus, getDaemonUrl, type LifecycleDeps , daemonStatusGuard} from "../daemon-lifecycle.js";
|
||||
import { readOpenRigEnv } from "../openrig-compat.js";
|
||||
@@ -55,13 +56,19 @@ export function uiCommand(depsOverride?: UiDeps): Command {
|
||||
// Ask the running daemon, not local config: the setting takes effect when the daemon starts.
|
||||
const probeUi = deps.probeUi ?? ((target: string) => fetch(target, { signal: AbortSignal.timeout(3000) }));
|
||||
try {
|
||||
await new DaemonClient(url).assertLocalOwner();
|
||||
const res = await probeUi(url);
|
||||
if (res.status === 404 && res.headers.get("x-openrig-web-ui") === "off") {
|
||||
console.error(UI_DISABLED_GUIDANCE);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (error instanceof ForeignLocalDaemonError) {
|
||||
console.error(error.message);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
// No answer: open the URL as before; the browser shows whatever the daemon serves.
|
||||
}
|
||||
console.log(url);
|
||||
|
||||
@@ -187,9 +187,10 @@ export interface RiggedConfig {
|
||||
piOpenRouterCharge: boolean;
|
||||
};
|
||||
// A verified Claude Code or Codex send reports on input-box evidence (default off). Read by the daemon at
|
||||
// each such send; the CLI key is the lockstep twin.
|
||||
// each such send; the CLI key is the lockstep twin. localUserGuard is the optional CLI-side Mac check.
|
||||
transport: {
|
||||
verifyInputBox: boolean;
|
||||
localUserGuard: boolean;
|
||||
};
|
||||
// OPR.0.4.6.FS-1 W2 — queue-retention maintenance knobs (twin of the daemon
|
||||
// settings-store): enabled + the four bounded numeric tunables.
|
||||
@@ -360,6 +361,7 @@ const DEFAULTS = {
|
||||
// Input-box evidence for verified sends OFF until the switch is on.
|
||||
transport: {
|
||||
verifyInputBox: false,
|
||||
localUserGuard: false,
|
||||
},
|
||||
// OPR.0.4.6.FS-1 W2 — retention defaults (twin of daemon getDefaultValue).
|
||||
retention: {
|
||||
@@ -486,6 +488,7 @@ export const VALID_KEYS = [
|
||||
"usage.pi_cost_facts",
|
||||
"usage.pi_openrouter_charge",
|
||||
"transport.verify_input_box",
|
||||
"transport.local_user_guard",
|
||||
// OPR.0.4.6.FS-1 W2 — queue-retention maintenance knobs; CLI-settable twin,
|
||||
// lockstep with the daemon settings-store SETTINGS_VALID_KEYS.
|
||||
"retention.enabled",
|
||||
@@ -589,6 +592,7 @@ export const ENV_MAP: Record<ValidKey, { primary: string; legacy?: string }> = {
|
||||
"provider.subscription_moves": { primary: "OPENRIG_PROVIDER_SUBSCRIPTION_MOVES" },
|
||||
"usage.pi_cost_facts": { primary: "OPENRIG_USAGE_PI_COST_FACTS" },
|
||||
"usage.pi_openrouter_charge": { primary: "OPENRIG_USAGE_PI_OPENROUTER_CHARGE" },
|
||||
"transport.local_user_guard": { primary: "OPENRIG_TRANSPORT_LOCAL_USER_GUARD" },
|
||||
"transport.verify_input_box": { primary: "OPENRIG_TRANSPORT_VERIFY_INPUT_BOX" },
|
||||
// OPR.0.4.6.FS-1 W2 — retention knobs; net-new keys, OPENRIG_* primary only.
|
||||
"retention.enabled": { primary: "OPENRIG_RETENTION_ENABLED" },
|
||||
@@ -676,6 +680,7 @@ const KEY_TO_PATH: Record<ValidKey, string[]> = {
|
||||
"provider.subscription_moves": ["provider", "subscriptionMoves"],
|
||||
"usage.pi_cost_facts": ["usage", "piCostFacts"],
|
||||
"usage.pi_openrouter_charge": ["usage", "piOpenRouterCharge"],
|
||||
"transport.local_user_guard": ["transport", "localUserGuard"],
|
||||
"transport.verify_input_box": ["transport", "verifyInputBox"],
|
||||
"retention.enabled": ["retention", "enabled"],
|
||||
"retention.transitions_days": ["retention", "transitionsDays"],
|
||||
@@ -1226,6 +1231,7 @@ export class ConfigStore {
|
||||
piOpenRouterCharge: v("usage.pi_openrouter_charge") as boolean,
|
||||
},
|
||||
transport: {
|
||||
localUserGuard: v("transport.local_user_guard") as boolean,
|
||||
verifyInputBox: v("transport.verify_input_box") as boolean,
|
||||
},
|
||||
retention: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import path from "node:path";
|
||||
import { existsSync } from "node:fs";
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
import { connectionErrorCode, formatDaemonHostForUrl } from "./client.js";
|
||||
import { DaemonClient, ForeignLocalDaemonError, connectionErrorCode, formatDaemonHostForUrl } from "./client.js";
|
||||
import { blockedConnectionGuidance, isBlockedConnectionCode } from "./daemon-reachability.js";
|
||||
import type { DaemonStartLock } from "./daemon-start-lock.js";
|
||||
import { DAEMON_STOP_WAIT_MS, DAEMON_SHUTDOWN_RECEIPT, type DaemonShutdownReceipt } from "@openrig/daemon/daemon-shutdown";
|
||||
@@ -1256,11 +1256,16 @@ export async function waitForKernelReady(
|
||||
timeoutMs: number,
|
||||
pollIntervalMs = 500,
|
||||
): Promise<KernelReadyResult> {
|
||||
const ownerClient = new DaemonClient(baseUrl);
|
||||
// Resolve the opt-in before starting the wait. Disabled polls retain main's request/deadline flow.
|
||||
const checkOwner = ownerClient.localOwnerCheckEnabled();
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let last: KernelReadyResult = { ok: false, kernelState: null, variant: null, detail: null };
|
||||
while (Date.now() < deadline) {
|
||||
const remainingMs = Math.max(1, deadline - Date.now());
|
||||
try {
|
||||
if (checkOwner) await ownerClient.assertLocalOwner(Math.min(1000, Math.max(1, deadline - Date.now())));
|
||||
// Once admitted, keep the final request even if an unknown owner check consumed the remainder.
|
||||
const remainingMs = Math.max(1, deadline - Date.now());
|
||||
const res = await fetch(`${baseUrl}/api/kernel/status`, {
|
||||
signal: AbortSignal.timeout(remainingMs),
|
||||
});
|
||||
@@ -1288,7 +1293,8 @@ export async function waitForKernelReady(
|
||||
return last;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (error instanceof ForeignLocalDaemonError) return { ...last, detail: error.message };
|
||||
// Transient fetch failure; keep polling until the deadline.
|
||||
}
|
||||
const sleepMs = Math.min(pollIntervalMs, Math.max(0, deadline - Date.now()));
|
||||
|
||||
@@ -202,10 +202,12 @@ function diagnosticVerdict(diagnostic: FrontDoorPermissionDiagnostic): string {
|
||||
async function defaultLaunchTui(): Promise<number> {
|
||||
const entry = resolveTuiPath(import.meta.dirname);
|
||||
if (!entry) throw new Error("mission-control TUI is not installed (no tui/dist/main.js next to this CLI)");
|
||||
const client = new DaemonClient();
|
||||
await client.assertLocalOwner();
|
||||
return await new Promise<number>((resolve, reject) => {
|
||||
const sharedKernel = envValue(process.env, "OPENRIG_SESSION_NAME", "RIGGED_SESSION_NAME") === "operator-human@kernel";
|
||||
const child = spawn(process.execPath, [entry, ...(sharedKernel ? ["--instance", "kernel"] : [])], { stdio: "inherit", env: { ...process.env,
|
||||
OPENRIG_URL: new DaemonClient().baseUrl,
|
||||
OPENRIG_URL: client.baseUrl,
|
||||
OPENRIG_TUI_CLI_ENTRY: path.join(import.meta.dirname, "bin-wrapper.js"),
|
||||
OPENRIG_TUI_CLI_IDENTITY: `${BUILD_INFO.semver ?? "version unstamped"} · ${BUILD_INFO.commit ?? "commit unstamped"}${BUILD_INFO.dirty === true ? " · dirty" : ""}` } });
|
||||
child.on("error", reject);
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
export type LocalDaemonOwner =
|
||||
| { state: "same-user" | "foreign"; pid: number; uid: number; callerUid: number }
|
||||
| { state: "unknown"; reason: string };
|
||||
|
||||
interface OwnerProbeDeps {
|
||||
platform?: string;
|
||||
callerUid?: number;
|
||||
fetchImpl?: typeof fetch;
|
||||
run?: (file: string, args: string[], timeoutMs: number) => Promise<string>;
|
||||
timeoutMs?: number;
|
||||
}
|
||||
const exec = promisify(execFile);
|
||||
const unknown = (reason: string): LocalDaemonOwner => ({ state: "unknown", reason });
|
||||
const integer = (s: string): number | undefined => /^\d+$/.test(s) && Number.isSafeInteger(Number(s)) ? Number(s) : undefined;
|
||||
|
||||
/** Keep deliberate remote routing out of the local process namespace. */
|
||||
function isLoopbackEndpoint(baseUrl: string): boolean {
|
||||
try {
|
||||
const url = new URL(baseUrl);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
||||
const host = url.hostname.replace(/^\[|\]$/g, "");
|
||||
return host === "localhost" || host === "::1" || /^127(?:\.\d{1,3}){3}$/.test(host);
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
function processIdentity(raw: string): { uid: number; start: string } | undefined {
|
||||
// LC_ALL=C: ps lstart is a stable process-start discriminator, not a PID-only check.
|
||||
const match = raw.trim().match(/^(\d+)\s+([A-Z][a-z]{2}\s+[A-Z][a-z]{2}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4})$/);
|
||||
if (!match?.[1] || !match[2] || !Number.isFinite(Date.parse(match[2]))) return;
|
||||
const uid = integer(match[1]);
|
||||
return uid !== undefined ? { uid, start: match[2].replace(/\s+/g, " ") } : undefined;
|
||||
}
|
||||
|
||||
/** Optional accidental-routing mitigation, not authentication or same-user instance isolation.
|
||||
* Trusts the daemon's reported PID/euid, corroborated by ps; it does not prove socket ownership.
|
||||
* Every request observes afresh. Repeated observations cannot make the following TCP request atomic. */
|
||||
export async function assessLocalDaemonOwner(baseUrl: string, deps: OwnerProbeDeps = {}): Promise<LocalDaemonOwner> {
|
||||
const callerUid = deps.callerUid ?? process.geteuid?.();
|
||||
if ((deps.platform ?? process.platform) !== "darwin" || !isLoopbackEndpoint(baseUrl) || callerUid === undefined) return unknown("not_local_mac");
|
||||
const timeoutMs = deps.timeoutMs ?? 1000;
|
||||
const deadline = performance.now() + timeoutMs;
|
||||
const controller = new AbortController();
|
||||
const run = deps.run ?? (async (file: string, args: string[], bound: number) => {
|
||||
const result = await exec(file, args, { timeout: bound, killSignal: "SIGKILL", maxBuffer: 64 * 1024, encoding: "utf8", env: { ...process.env, LC_ALL: "C" } });
|
||||
return result.stdout;
|
||||
});
|
||||
const observe = async (file: string, args: string[]) => {
|
||||
const remaining = Math.floor(deadline - performance.now());
|
||||
if (remaining <= 0 || controller.signal.aborted) throw new Error("owner observation expired");
|
||||
return run(file, args, remaining);
|
||||
};
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
try {
|
||||
return await Promise.race([
|
||||
(async (): Promise<LocalDaemonOwner> => {
|
||||
const healthIdentity = async (): Promise<{ pid: number; uid: number } | undefined> => {
|
||||
if (controller.signal.aborted || performance.now() >= deadline) return;
|
||||
const health = await (deps.fetchImpl ?? fetch)(`${baseUrl.replace(/\/+$/, "")}/healthz`, { signal: controller.signal, redirect: "error" });
|
||||
if (!health.ok) return;
|
||||
const body = await health.json() as { pid?: unknown; euid?: unknown } | null;
|
||||
if (!Number.isSafeInteger(body?.pid) || (body?.pid as number) <= 0
|
||||
|| !Number.isSafeInteger(body?.euid) || (body?.euid as number) < 0) return;
|
||||
return { pid: body!.pid as number, uid: body!.euid as number };
|
||||
};
|
||||
const reported = await healthIdentity();
|
||||
if (!reported) return unknown("health_identity_missing");
|
||||
const processes = () => observe("/bin/ps", ["-o", "uid=,lstart=", "-p", String(reported.pid)]);
|
||||
const before = processIdentity(await processes());
|
||||
if (!before || before.uid !== reported.uid) return unknown("process_unconfirmed");
|
||||
const second = await healthIdentity();
|
||||
if (!second || second.pid !== reported.pid || second.uid !== reported.uid) return unknown("reported_identity_changed");
|
||||
const after = processIdentity(await processes());
|
||||
if (!after || after.uid !== before.uid || after.start !== before.start) return unknown("process_changed");
|
||||
if (controller.signal.aborted || performance.now() >= deadline) return unknown("timeout");
|
||||
return { state: reported.uid === callerUid ? "same-user" : "foreign", ...reported, callerUid };
|
||||
})(),
|
||||
new Promise<LocalDaemonOwner>(resolve => { timer = setTimeout(() => { controller.abort(); resolve(unknown("timeout")); }, timeoutMs); }),
|
||||
]);
|
||||
} catch { return unknown("observation_unavailable"); }
|
||||
finally { clearTimeout(timer); controller.abort(); }
|
||||
}
|
||||
|
||||
export class ForeignLocalDaemonError extends Error {
|
||||
readonly code = "foreign_local_daemon";
|
||||
constructor(baseUrl: string, owner: Extract<LocalDaemonOwner, { state: "same-user" | "foreign" }>) {
|
||||
super(`The optional local user guard found a different OS user at ${baseUrl} (reported PID ${owner.pid}, effective UID ${owner.uid}, corroborated by ps; caller effective UID ${owner.callerUid}). No business request was sent after this owner check. Start your own daemon with \`rig daemon start --port <unused-port>\`, or explicitly select an intended shared/remote endpoint with OPENRIG_URL.`);
|
||||
this.name = "ForeignLocalDaemonError";
|
||||
}
|
||||
}
|
||||
@@ -183,6 +183,7 @@ describe("ConfigStore — extended namespaces (User Settings v0)", () => {
|
||||
"usage.pi_cost_facts",
|
||||
"usage.pi_openrouter_charge",
|
||||
"transport.verify_input_box",
|
||||
"transport.local_user_guard",
|
||||
// OPR.0.4.6.FS-1 W2 — queue-retention maintenance knobs.
|
||||
"retention.enabled",
|
||||
"retention.transitions_days",
|
||||
@@ -260,14 +261,25 @@ describe("ConfigStore — extended namespaces (User Settings v0)", () => {
|
||||
expect(setting).toMatchObject({ value: 3, source: "default", defaultValue: 3 });
|
||||
});
|
||||
|
||||
it("local user guard is opt-in and resolves the file/env twins", () => {
|
||||
const store = new ConfigStore(configPath);
|
||||
expect(store.resolveWithSource("transport.local_user_guard")).toMatchObject({ value: false, source: "default" });
|
||||
store.set("transport.local_user_guard", "true");
|
||||
expect(store.resolve().transport.localUserGuard).toBe(true);
|
||||
try {
|
||||
process.env.OPENRIG_TRANSPORT_LOCAL_USER_GUARD = "false";
|
||||
expect(store.resolveWithSource("transport.local_user_guard")).toMatchObject({ value: false, source: "env" });
|
||||
} finally { delete process.env.OPENRIG_TRANSPORT_LOCAL_USER_GUARD; }
|
||||
});
|
||||
|
||||
it("transport.verify_input_box defaults off, resolves its env twin, and appears in the resolved config", () => {
|
||||
const store = new ConfigStore(configPath);
|
||||
expect(store.resolveWithSource("transport.verify_input_box")).toMatchObject({ value: false, source: "default", defaultValue: false });
|
||||
expect(store.resolve().transport).toEqual({ verifyInputBox: false });
|
||||
expect(store.resolve().transport).toEqual({ verifyInputBox: false, localUserGuard: false });
|
||||
try {
|
||||
process.env.OPENRIG_TRANSPORT_VERIFY_INPUT_BOX = "1";
|
||||
expect(store.resolveWithSource("transport.verify_input_box")).toMatchObject({ value: true, source: "env" });
|
||||
expect(store.resolve().transport).toEqual({ verifyInputBox: true });
|
||||
expect(store.resolve().transport).toEqual({ verifyInputBox: true, localUserGuard: false });
|
||||
} finally {
|
||||
delete process.env.OPENRIG_TRANSPORT_VERIFY_INPUT_BOX;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,50 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import http from "node:http";
|
||||
import { DaemonClient } from "../src/client.js";
|
||||
import { ConfigStore } from "../src/config-store.js";
|
||||
import { waitForKernelReady } from "../src/daemon-lifecycle.js";
|
||||
|
||||
describe("waitForKernelReady deadline bounds", () => {
|
||||
it("guard off keeps the admitted final request after the clock crosses its deadline", async () => {
|
||||
vi.stubEnv("OPENRIG_URL", ""); vi.stubEnv("RIGGED_URL", "");
|
||||
vi.stubEnv("OPENRIG_TRANSPORT_LOCAL_USER_GUARD", "false");
|
||||
vi.spyOn(Date, "now").mockReturnValueOnce(0).mockReturnValueOnce(1).mockReturnValue(2);
|
||||
const request = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({ kernel_state: "ready" })));
|
||||
try {
|
||||
expect(await waitForKernelReady("http://127.0.0.1:43219", 2, 1)).toMatchObject({ ok: true });
|
||||
expect(request).toHaveBeenCalledTimes(1);
|
||||
} finally { vi.restoreAllMocks(); vi.unstubAllEnvs(); }
|
||||
});
|
||||
|
||||
it("guard off resolves configuration before starting the readiness window", async () => {
|
||||
vi.stubEnv("OPENRIG_URL", ""); vi.stubEnv("RIGGED_URL", "");
|
||||
vi.stubEnv("OPENRIG_TRANSPORT_LOCAL_USER_GUARD", "false");
|
||||
let now = 0;
|
||||
vi.spyOn(Date, "now").mockImplementation(() => now);
|
||||
const setting = vi.spyOn(ConfigStore.prototype, "resolveWithSource").mockImplementation(() => {
|
||||
now = 1000; // Synchronous configuration I/O may take the entire former window.
|
||||
return { value: false } as never;
|
||||
});
|
||||
const request = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({ kernel_state: "ready" })));
|
||||
try {
|
||||
expect(await waitForKernelReady("http://127.0.0.1:43219", 5, 1)).toMatchObject({ ok: true });
|
||||
expect(setting).toHaveBeenCalledTimes(1); expect(request).toHaveBeenCalledTimes(1);
|
||||
} finally { vi.restoreAllMocks(); vi.unstubAllEnvs(); }
|
||||
});
|
||||
|
||||
it("unknown enabled ownership keeps the admitted final request after its check used the remainder", async () => {
|
||||
vi.stubEnv("OPENRIG_URL", ""); vi.stubEnv("RIGGED_URL", "");
|
||||
vi.stubEnv("OPENRIG_TRANSPORT_LOCAL_USER_GUARD", "true");
|
||||
let now = 0;
|
||||
vi.spyOn(Date, "now").mockImplementation(() => now);
|
||||
vi.spyOn(DaemonClient.prototype, "assertLocalOwner").mockImplementation(async () => { now = 1000; });
|
||||
const request = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({ kernel_state: "ready" })));
|
||||
try {
|
||||
expect(await waitForKernelReady("http://127.0.0.1:43219", 5, 1)).toMatchObject({ ok: true });
|
||||
expect(request).toHaveBeenCalledTimes(1);
|
||||
} finally { vi.restoreAllMocks(); vi.unstubAllEnvs(); }
|
||||
});
|
||||
|
||||
it("bounds slow headers to the wait deadline", async () => {
|
||||
const server = http.createServer((_req, res) => {
|
||||
// Delay sending headers beyond the client wait deadline
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import http from "node:http";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { assessLocalDaemonOwner } from "../src/local-daemon-owner.js";
|
||||
import { allowFetchTarget } from "./fetch-guard.js";
|
||||
|
||||
const processRow = (uid = 502, start = "Tue Oct 20 03:40:00 2026") => `${uid} ${start}\n`;
|
||||
function deps(outputs = [processRow(), processRow()]) {
|
||||
return {
|
||||
platform: "darwin", callerUid: 501,
|
||||
fetchImpl: vi.fn(async () => new Response(JSON.stringify({ pid: 42, euid: 502 }))) as unknown as typeof fetch,
|
||||
run: vi.fn(async (file: string) => {
|
||||
// Model unavailable foreign descriptors for the previous assessor, not ps output fed to lsof.
|
||||
if (file === "/usr/sbin/lsof") return "";
|
||||
const next = outputs.shift();
|
||||
if (next === undefined) throw new Error("fixture exhausted");
|
||||
return next;
|
||||
}),
|
||||
};
|
||||
}
|
||||
describe("bounded Mac reported/process effective-UID agreement", () => {
|
||||
it("corroborates reported foreign euid with repeated process UID/start without descriptor inspection", async () => {
|
||||
const d = deps();
|
||||
expect(await assessLocalDaemonOwner("http://127.0.0.1:7433", d)).toMatchObject({ state: "foreign", pid: 42, uid: 502, callerUid: 501 });
|
||||
expect(d.run.mock.calls.map(c => c[0])).toEqual(["/bin/ps", "/bin/ps"]);
|
||||
expect(d.fetchImpl).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it("same effective UID permits use but does not assert instance identity", async () => {
|
||||
const d = deps(); d.callerUid = 502;
|
||||
expect(await assessLocalDaemonOwner("http://127.0.0.1:7433", d)).toMatchObject({ state: "same-user" });
|
||||
});
|
||||
it.each([
|
||||
["PID reuse", [processRow(), processRow(502, "Tue Oct 20 03:41:00 2026")]],
|
||||
["reported/process UID disagreement", [processRow(501)]],
|
||||
["changed process UID", [processRow(), processRow(501)]],
|
||||
["missing process", [""]],
|
||||
["ambiguous process rows", [processRow() + processRow(503)]],
|
||||
["malformed process", ["502 nonsense"]],
|
||||
])("%s remains unknown", async (_name, rows) => {
|
||||
expect(await assessLocalDaemonOwner("http://127.0.0.1:7433", deps([...rows]))).toMatchObject({ state: "unknown" });
|
||||
});
|
||||
it.each([{ pid: 43, euid: 502 }, { pid: 42, euid: 503 }, {}])("changed health identity %j remains unknown", async changed => {
|
||||
const d = deps();
|
||||
d.fetchImpl = vi.fn().mockResolvedValueOnce(new Response(JSON.stringify({ pid: 42, euid: 502 })))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify(changed)));
|
||||
expect(await assessLocalDaemonOwner("http://127.0.0.1:7433", d)).toMatchObject({ state: "unknown" });
|
||||
});
|
||||
it.each(["http://localhost:7433", "http://[::1]:7433"])("checks a loopback target %s", async url => {
|
||||
expect(await assessLocalDaemonOwner(url, deps())).toMatchObject({ state: "foreign" });
|
||||
});
|
||||
it.each(["http://remote.example:7433", "http://192.0.2.1:7433", "not a URL"])("never inspects a remote/unparseable target %s", async url => {
|
||||
const d = deps();
|
||||
expect(await assessLocalDaemonOwner(url, d)).toMatchObject({ state: "unknown" });
|
||||
expect(d.fetchImpl).not.toHaveBeenCalled(); expect(d.run).not.toHaveBeenCalled();
|
||||
});
|
||||
it.each([null, {}, { pid: 42 }, { pid: "42", euid: 502 }, { pid: -1, euid: 502 },
|
||||
{ pid: 42, euid: "502" }, { pid: 42, euid: -1 }, { pid: 42, euid: 1.5 }])("old or malformed health %j remains unknown", async body => {
|
||||
const d = deps(); d.fetchImpl = vi.fn(async () => new Response(JSON.stringify(body))) as unknown as typeof fetch;
|
||||
expect(await assessLocalDaemonOwner("http://127.0.0.1:7433", d)).toMatchObject({ state: "unknown" });
|
||||
expect(d.run).not.toHaveBeenCalled();
|
||||
});
|
||||
it.each([{ caller: 0, daemon: 502 }, { caller: 501, daemon: 0 }])("effective UID zero is valid on either side: %j", async ({ caller, daemon }) => {
|
||||
const d = deps([processRow(daemon), processRow(daemon)]); d.callerUid = caller;
|
||||
d.fetchImpl = vi.fn(async () => new Response(JSON.stringify({ pid: 42, euid: daemon }))) as unknown as typeof fetch;
|
||||
expect(await assessLocalDaemonOwner("http://127.0.0.1:7433", d)).toMatchObject({ state: "foreign", callerUid: caller, uid: daemon });
|
||||
});
|
||||
it("unavailable tools/permissions remain unknown without a privileged fallback", async () => {
|
||||
const d = deps(); d.run.mockRejectedValue(new Error("EPERM"));
|
||||
expect(await assessLocalDaemonOwner("http://127.0.0.1:7433", d)).toMatchObject({ state: "unknown" });
|
||||
expect(d.run).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("bounds a non-cooperative health fetch and does not start OS work after expiry", async () => {
|
||||
const d = deps(); d.fetchImpl = (() => new Promise(() => {})) as typeof fetch;
|
||||
const start = performance.now();
|
||||
expect(await assessLocalDaemonOwner("http://127.0.0.1:7433", { ...d, timeoutMs: 30 })).toMatchObject({ state: "unknown" });
|
||||
expect(performance.now() - start).toBeLessThan(500); expect(d.run).not.toHaveBeenCalled();
|
||||
});
|
||||
it.skipIf(process.platform !== "darwin" || process.env.OPENRIG_TEST_MAC_OWNER_TOOLS !== "1")("qualifies unprivileged Mac ps and an owned reporting listener", async () => {
|
||||
const server = http.createServer((_req, res) => res.end(JSON.stringify({ pid: process.pid, euid: process.geteuid!() })));
|
||||
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve));
|
||||
const url = `http://127.0.0.1:${(server.address() as { port: number }).port}`;
|
||||
allowFetchTarget(url);
|
||||
try {
|
||||
const started = performance.now();
|
||||
expect(await assessLocalDaemonOwner(url)).toMatchObject({ state: "same-user", pid: process.pid, uid: process.geteuid!() });
|
||||
const elapsedMs = performance.now() - started;
|
||||
// PID1 belongs to root on the hosted Mac: observe UID/start only, without sudo or descriptor access.
|
||||
const other = await promisify(execFile)("/bin/ps", ["-o", "uid=,lstart=", "-p", "1"], { timeout: 1000, encoding: "utf8", env: { ...process.env, LC_ALL: "C" } });
|
||||
expect(other.stdout.trim()).toMatch(/^0\s+/); expect(process.geteuid!()).not.toBe(0);
|
||||
console.log(JSON.stringify({ qualification: "owned reported identity and foreign-UID ps visibility", pid: process.pid, uid: process.geteuid!(), foreignPid: 1, foreignUid: 0, elapsedMs }));
|
||||
expect(elapsedMs).toBeLessThan(1500);
|
||||
} finally { await new Promise<void>(resolve => server.close(() => resolve())); }
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { spawn } from "node:child_process";
|
||||
import { uiCommand } from "../src/commands/ui.js";
|
||||
import { openMissionControl } from "../src/front-door.js";
|
||||
import { Command } from "commander";
|
||||
import { DaemonClient, ForeignLocalDaemonError } from "../src/client.js";
|
||||
import { readLiveLogicalIds } from "../src/commands/doctor.js";
|
||||
import { chatroomCommand } from "../src/commands/chatroom.js";
|
||||
import { waitForKernelReady, type LifecycleDeps } from "../src/daemon-lifecycle.js";
|
||||
|
||||
vi.mock("node:child_process", async () => ({
|
||||
...await vi.importActual<typeof import("node:child_process")>("node:child_process"),
|
||||
spawn: vi.fn(() => { throw new Error("unexpected TUI spawn"); }),
|
||||
}));
|
||||
|
||||
const foreign = { state: "foreign" as const, pid: 42, uid: 502, callerUid: 501 };
|
||||
const base = "http://127.0.0.1:43219";
|
||||
beforeEach(() => {
|
||||
vi.stubEnv("OPENRIG_URL", ""); vi.stubEnv("RIGGED_URL", "");
|
||||
vi.stubEnv("OPENRIG_SESSION_NAME", ""); vi.stubEnv("RIGGED_SESSION_NAME", "");
|
||||
vi.stubEnv("OPENRIG_TRANSPORT_LOCAL_USER_GUARD", "true");
|
||||
});
|
||||
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllEnvs(); process.exitCode = undefined; });
|
||||
describe("raw CLI daemon readers use the shared owner check", () => {
|
||||
it("doctor cannot fetch topology on positive foreign ownership", async () => {
|
||||
const requests = vi.spyOn(globalThis, "fetch");
|
||||
const instance = new DaemonClient(base, { ownerProbe: async () => foreign });
|
||||
await expect(readLiveLogicalIds(base, "fixture", instance)).rejects.toThrow(/different OS user/);
|
||||
expect(requests).not.toHaveBeenCalled();
|
||||
});
|
||||
it("doctor unknown evidence keeps the real two-read topology result", async () => {
|
||||
const requests = vi.spyOn(globalThis, "fetch")
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify([{ id: "rig-id", name: "fixture" }])))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify([{ logicalId: "dev.worker" }])));
|
||||
const probe = vi.fn(async () => ({ state: "unknown" as const, reason: "old_health" }));
|
||||
expect(await readLiveLogicalIds(base, "fixture", new DaemonClient(base, { ownerProbe: probe }))).toEqual(["dev.worker"]);
|
||||
expect(requests).toHaveBeenCalledTimes(2); expect(probe).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it("SSE rechecks after resolving the rig, so a changed listener receives no stream GET", async () => {
|
||||
const requests = vi.spyOn(globalThis, "fetch");
|
||||
const probe = vi.fn().mockResolvedValueOnce({ state: "same-user", pid: 42, uid: 501, callerUid: 501 }).mockResolvedValue(foreign);
|
||||
const instance = new DaemonClient(base, {
|
||||
ownerProbe: probe,
|
||||
fetchImpl: async () => new Response(JSON.stringify([{ id: "rig-id", name: "fixture" }])),
|
||||
});
|
||||
const deps = {
|
||||
lifecycleDeps: { exists: () => false, readFile: () => null, fetch: async () => ({ ok: true }) } as unknown as LifecycleDeps,
|
||||
clientFactory: () => instance,
|
||||
};
|
||||
const err = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
await new Command().addCommand(chatroomCommand(deps)).parseAsync(["node", "rig", "chatroom", "watch", "fixture"]);
|
||||
expect(process.exitCode).toBe(1); expect(err.mock.calls.flat().join(" ")).toMatch(/different OS user/);
|
||||
expect(probe).toHaveBeenCalledTimes(2); expect(requests).not.toHaveBeenCalled();
|
||||
});
|
||||
it("default UI opening refuses before its page probe or browser effect", async () => {
|
||||
vi.spyOn(DaemonClient.prototype, "assertLocalOwner").mockRejectedValue(new ForeignLocalDaemonError(base, foreign));
|
||||
const probeUi = vi.fn(); const exec = vi.fn();
|
||||
const err = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
await new Command().addCommand(uiCommand({
|
||||
lifecycleDeps: { exists: () => false, readFile: () => null, fetch: async () => ({ ok: true }) } as unknown as LifecycleDeps,
|
||||
probeUi, exec,
|
||||
})).parseAsync(["node", "rig", "ui", "open"]);
|
||||
expect(probeUi).not.toHaveBeenCalled(); expect(exec).not.toHaveBeenCalled();
|
||||
expect(process.exitCode).toBe(1); expect(err.mock.calls.flat().join(" ")).toMatch(/different OS user/);
|
||||
});
|
||||
it("default TUI opening refuses before spawning its child", async () => {
|
||||
vi.spyOn(DaemonClient.prototype, "assertLocalOwner").mockRejectedValue(new ForeignLocalDaemonError(base, foreign));
|
||||
const err = vi.fn(); const exit = vi.fn();
|
||||
await openMissionControl({ probeDaemon: async () => true, err, exit });
|
||||
expect(spawn).not.toHaveBeenCalled(); expect(exit).toHaveBeenCalledWith(1);
|
||||
expect(err.mock.calls.flat().join(" ")).toMatch(/different OS user/);
|
||||
});
|
||||
it("kernel polling reports the foreign owner without a business fetch or retry", async () => {
|
||||
const check = vi.spyOn(DaemonClient.prototype, "assertLocalOwner").mockRejectedValue(new ForeignLocalDaemonError(base, foreign));
|
||||
const requests = vi.spyOn(globalThis, "fetch");
|
||||
expect(await waitForKernelReady(base, 1000, 1)).toMatchObject({ ok: false, detail: expect.stringMatching(/different OS user/) });
|
||||
expect(check).toHaveBeenCalledTimes(1); expect(requests).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import http from "node:http";
|
||||
import { Command } from "commander";
|
||||
import { DaemonClient, remoteDaemonClient } from "../src/client.js";
|
||||
import { queueCommand } from "../src/commands/queue.js";
|
||||
import { sendCommand } from "../src/commands/send.js";
|
||||
import type { LifecycleDeps } from "../src/daemon-lifecycle.js";
|
||||
import { allowFetchTarget } from "./fetch-guard.js";
|
||||
|
||||
const foreign = { state: "foreign" as const, pid: 42, uid: 502, callerUid: 501 };
|
||||
let server: http.Server;
|
||||
let base: string;
|
||||
let business: string[];
|
||||
beforeEach(async () => {
|
||||
vi.stubEnv("OPENRIG_URL", ""); vi.stubEnv("RIGGED_URL", "");
|
||||
vi.stubEnv("OPENRIG_SESSION_NAME", ""); vi.stubEnv("RIGGED_SESSION_NAME", "");
|
||||
vi.stubEnv("OPENRIG_TRANSPORT_LOCAL_USER_GUARD", "true");
|
||||
business = [];
|
||||
server = http.createServer((req, res) => {
|
||||
if (req.url !== "/healthz") business.push(`${req.method} ${req.url}`);
|
||||
res.setHeader("content-type", "application/json");
|
||||
res.end(JSON.stringify(req.url === "/healthz" ? { pid: 42 } : []));
|
||||
});
|
||||
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve));
|
||||
base = `http://127.0.0.1:${(server.address() as { port: number }).port}`;
|
||||
allowFetchTarget(base);
|
||||
});
|
||||
afterEach(async () => {
|
||||
await new Promise<void>(resolve => server.close(() => resolve()));
|
||||
vi.unstubAllEnvs(); vi.restoreAllMocks(); process.exitCode = undefined;
|
||||
});
|
||||
function client(state = foreign) {
|
||||
// A constructor URL can be an internal status result, not explicit user intent.
|
||||
const options = { ownerProbe: vi.fn(async () => state) };
|
||||
return { instance: new DaemonClient(base, options), probe: options.ownerProbe };
|
||||
}
|
||||
function lifecycle(): LifecycleDeps {
|
||||
return {
|
||||
exists: () => false, readFile: () => null, isProcessAlive: () => true,
|
||||
fetch: async () => ({ ok: true, json: async () => ({ pid: 42 }) }),
|
||||
} as unknown as LifecycleDeps;
|
||||
}
|
||||
describe("optional local user guard at actual CLI consumers", () => {
|
||||
it("foreign owner blocks a business read and write without calling either route", async () => {
|
||||
const { instance, probe } = client();
|
||||
await expect(instance.get("/api/queue")).rejects.toThrow(/different OS user/);
|
||||
await expect(instance.post("/api/transport/send", { text: "fixture" })).rejects.toThrow(/different OS user/);
|
||||
expect(probe).toHaveBeenCalledTimes(2);
|
||||
expect(business).toEqual([]);
|
||||
});
|
||||
it("real queue list with a status-derived client sends no business GET", async () => {
|
||||
// Select the private port through the existing local configuration precedence.
|
||||
vi.stubEnv("OPENRIG_PORT", new URL(base).port);
|
||||
const { instance } = client();
|
||||
const cmd = new Command().addCommand(queueCommand({ lifecycleDeps: lifecycle(), clientFactory: () => instance }));
|
||||
await expect(cmd.parseAsync(["node", "rig", "queue", "list", "--json"])).rejects.toThrow(/different OS user/);
|
||||
expect(business).toEqual([]);
|
||||
});
|
||||
it("real send with a status-derived client sends no transport POST", async () => {
|
||||
vi.stubEnv("OPENRIG_PORT", new URL(base).port);
|
||||
const { instance } = client();
|
||||
const cmd = new Command().addCommand(sendCommand({ lifecycleDeps: lifecycle(), clientFactory: () => instance }));
|
||||
await expect(cmd.parseAsync(["node", "rig", "send", "seat@fixture", "harmless"])).rejects.toThrow(/different OS user/);
|
||||
expect(business).toEqual([]);
|
||||
});
|
||||
it.each(["same-user", "unknown"] as const)("real queue and send preserve %s/no-state/custom-port routing", async state => {
|
||||
vi.stubEnv("OPENRIG_PORT", new URL(base).port);
|
||||
vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
const instance = new DaemonClient(base, { ownerProbe: async () => state === "same-user"
|
||||
? { state, pid: 42, uid: 501, callerUid: 501 } : { state, reason: "old_or_missing_health_pid" } });
|
||||
const deps = { lifecycleDeps: lifecycle(), clientFactory: () => instance };
|
||||
await new Command().addCommand(queueCommand(deps)).parseAsync(["node", "rig", "queue", "list", "--json"]);
|
||||
await new Command().addCommand(sendCommand(deps)).parseAsync(["node", "rig", "send", "seat@fixture", "harmless"]);
|
||||
expect(business).toHaveLength(2);
|
||||
expect(business[0]).toMatch(/^GET \/api\/queue\/list\?/);
|
||||
expect(business[1]).toBe("POST /api/transport/send");
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
});
|
||||
it("real send retains its custom endpoint after health timeout and unknown ownership", async () => {
|
||||
vi.stubEnv("OPENRIG_PORT", new URL(base).port);
|
||||
vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
const options = { ownerProbe: async () => ({ state: "unknown" as const, reason: "timeout" }) };
|
||||
const deps = lifecycle(); deps.fetch = async () => { throw new Error("fixture health timeout"); };
|
||||
const selected: string[] = [];
|
||||
await new Command().addCommand(sendCommand({ lifecycleDeps: deps, clientFactory: url => {
|
||||
selected.push(url); return new DaemonClient(url, options);
|
||||
} })).parseAsync(["node", "rig", "send", "seat@fixture", "harmless"]);
|
||||
expect(selected).toEqual([base]);
|
||||
expect(business).toEqual(["POST /api/transport/send"]);
|
||||
});
|
||||
it("defaults off and performs no owner observation", async () => {
|
||||
vi.stubEnv("OPENRIG_TRANSPORT_LOCAL_USER_GUARD", "false");
|
||||
const { instance, probe } = client();
|
||||
await instance.get("/api/queue");
|
||||
expect(probe).not.toHaveBeenCalled();
|
||||
expect(business).toEqual(["GET /api/queue"]);
|
||||
});
|
||||
it.each(["same-user", "unknown"] as const)("%s retains the selected endpoint and allows GET and POST", async state => {
|
||||
const options = { ownerProbe: async () => ({ state, reason: "fixture" }) };
|
||||
const instance = new DaemonClient(base, options);
|
||||
await instance.get("/api/queue"); await instance.post("/api/transport/send", {});
|
||||
expect(business).toEqual(["GET /api/queue", "POST /api/transport/send"]);
|
||||
});
|
||||
it("checks again after an earlier same-user response", async () => {
|
||||
const probe = vi.fn().mockResolvedValueOnce({ state: "same-user" }).mockResolvedValue(foreign);
|
||||
const instance = new DaemonClient(base, { ownerProbe: probe });
|
||||
await instance.get("/api/queue");
|
||||
await expect(instance.post("/api/transport/send", {})).rejects.toThrow(/different OS user/);
|
||||
expect(business).toEqual(["GET /api/queue"]);
|
||||
});
|
||||
it.each(["OPENRIG_URL", "RIGGED_URL"])("preserves explicit %s intent", async key => {
|
||||
vi.stubEnv(key, base);
|
||||
const { instance, probe } = client();
|
||||
await instance.get("/api/queue");
|
||||
expect(probe).not.toHaveBeenCalled(); expect(business).toHaveLength(1);
|
||||
});
|
||||
it("an environment target does not authorize a different internal endpoint", async () => {
|
||||
vi.stubEnv("OPENRIG_URL", "http://127.0.0.1:1");
|
||||
await expect(client().instance.get("/api/queue")).rejects.toThrow(/different OS user/);
|
||||
expect(business).toEqual([]);
|
||||
});
|
||||
it("preserves registered remote/tunnel intent and diagnostic health reachability", async () => {
|
||||
const { instance, probe } = client();
|
||||
await instance.get("/healthz");
|
||||
expect(probe).not.toHaveBeenCalled();
|
||||
await remoteDaemonClient(() => instance, base).get("/api/queue");
|
||||
expect(probe).not.toHaveBeenCalled(); expect(business).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -384,11 +384,19 @@ rig reconcile-session <session>
|
||||
rig reconcile-session <session> --rig <rigId> --node <logicalId>
|
||||
rig reconcile-session <session> --no-launch
|
||||
rig reconcile-session <session> --json
|
||||
rig reconcile-session <session> --repair-missing-pane --no-launch
|
||||
```
|
||||
|
||||
This is a no-launch, no-input adopt. `--rig`/`--node` disambiguate when the
|
||||
canonical session name does not uniquely resolve. `--no-launch` is accepted
|
||||
for explicitness (it is the only mode this command has).
|
||||
This is a no-launch, no-input adopt: it records a new claimed session row for
|
||||
the seat (any running row is superseded); it re-captures the resume token if
|
||||
it can, and conversation continuity stays unverified. `--rig`/`--node`
|
||||
disambiguate when the canonical session name does not uniquely resolve.
|
||||
`--no-launch` is accepted for explicitness.
|
||||
|
||||
`--repair-missing-pane --no-launch` is different: for a running launched seat
|
||||
whose tmux pane was never recorded, it fills in the pane and keeps the
|
||||
session, resume token and generation. It never adopts. Both forms refuse
|
||||
while the seat's typing protection is on.
|
||||
|
||||
### Five-term restore status vocabulary
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ not know exists.** So the point of what follows is not skill. It is recognition:
|
||||
lands, something rhymes, and you go check instead of building.
|
||||
|
||||
Eighty-eight top-level verbs ship (capability canon refreshed through
|
||||
`capability-delta-v0.6.12`). This marker describes the pack's teaching, not publication or live
|
||||
`capability-delta-v0.6.14`). This marker describes the pack's teaching, not publication or live
|
||||
adoption. Read this once for shape, and let it make you suspicious that a thing already exists.
|
||||
Model-divergence proclamations are live product (trust them; pins use canonical
|
||||
model IDs).
|
||||
@@ -32,6 +32,11 @@ one command away. Your memory of the fleet is a claim about the past.
|
||||
filters to running and **hides detached, exited and attention states** — which are exactly the
|
||||
cells you were scanning for. A seat whose tmux session is confirmed gone reads `detached`
|
||||
(activity reason `session_missing`), and `rig ps --nodes` says to restore or relaunch it.
|
||||
`rig ps --nodes` also shows the model each seat was last seen using (`modelObservation`, with its
|
||||
source and time), or unknown; it never shows the configured model in its place. A Claude seat at
|
||||
Claude Code's MCP-server approval menu reads as needing input (`mcp_gate`), a Codex seat at its
|
||||
folder-trust panel reads `trust_gate`, and an OMP seat whose turn ended with a typed transient
|
||||
provider failure reads idle and takes an ordinary retry.
|
||||
**`rig ps --resources`** shows this host's load and transcript capture cost.
|
||||
- **`rig ps --all-hosts --nodes -A`** — the same across every registered machine. Rollup-only
|
||||
unless you ask for nodes.
|
||||
@@ -74,7 +79,7 @@ one command away. Your memory of the fleet is a claim about the past.
|
||||
- **`rig roster list` / `show` / `find`** — who across your rigs and hosts is recommended for a
|
||||
kind of work, from JSON rosters in `<workspace.root>/rosters`. Read-only: it dispatches nothing
|
||||
and does not claim anyone is available. The built-in starter and factory leads publish their
|
||||
team's roster at first start.
|
||||
team's roster at first start. `show` and `find` include each seat's last observed model.
|
||||
- **`rig telemetry events` / `transitions` / `tenures <node-id>`** — past events, queue
|
||||
transitions and a seat's occupants, one bounded page at a time with gaps reported. Reach for it
|
||||
before querying the database yourself.
|
||||
@@ -98,7 +103,9 @@ unseen — which makes messages the one delivery channel that never gets skipped
|
||||
that seat aren't held behind it; N is a deadline from the start of the wait.
|
||||
Over `--host` to an http-registered host, a send that may have reached the remote reads
|
||||
`remote-outcome-unknown` ("Delivery UNCONFIRMED"): check the target with `rig capture <session>
|
||||
--host <id>` before any resend. Only a host that was never reached reads as unreachable.
|
||||
--host <id>` before any resend. Only a host that was never reached reads as unreachable. A send
|
||||
whose caller goes away (Ctrl-C, or its own timeout) before input starts is refused with
|
||||
`caller_cancelled` and never types later.
|
||||
- **`rig send --raw`** — send exact text or keystrokes without the From/To envelope;
|
||||
the interactive-prompt guard still applies. To deliberately drive an interactive prompt,
|
||||
**`--dangerously-interact --reason "<why>"`** is the explicit override and implies raw text.
|
||||
@@ -116,7 +123,9 @@ unseen — which makes messages the one delivery channel that never gets skipped
|
||||
every seat in one call. `--lines N` is the last N lines (stderr says how many earlier lines were
|
||||
cut); `--history-plus-pane` gives the older N history lines plus the whole pane. A short capture
|
||||
doesn't prove a seat is idle: a capture that cut lines says activity is unknown (`activity:
|
||||
"unknown"` in `--json`), so read `rig ps --nodes` for it.
|
||||
"unknown"` in `--json`), so read `rig ps --nodes` for it. For a Claude Code or Codex seat, stderr
|
||||
notes whether the input box's text looks typed or like a faint harness suggestion (`inputBox` in
|
||||
`--json`); it's a heuristic, and `unknown` means unknown.
|
||||
- **`rig transcript <seat>`** — what has been *said* in a seat, and **seat-scoped rather than
|
||||
session-scoped**: one file spanning every agent generation that has occupied it, so it reaches
|
||||
back through handovers. Capped (`transcripts.lines`, default 1000 — raise it for depth), and
|
||||
@@ -126,7 +135,8 @@ unseen — which makes messages the one delivery channel that never gets skipped
|
||||
read something, do not ask it. Look.**
|
||||
- **`rig ask`** — search what was said or decided, across *every generation that ever sat in a
|
||||
seat*. It reaches back past a handover that erased your predecessor's context, and costs no
|
||||
runtime tokens.
|
||||
runtime tokens. `--wake` asks a Claude or Codex seat itself; any other runtime (Pi, custom, or
|
||||
none recorded) is refused before a wake starts.
|
||||
- **`rig chatroom wait`** — block until a peer actually says something, instead of polling
|
||||
`capture` in a loop. Also `history`, `watch`, `topic` for a named thread that stays retrievable.
|
||||
Its `--timeout` is in seconds and ignores a suffix, so `2m` means 2 seconds. Poll timeouts,
|
||||
@@ -170,7 +180,10 @@ unseen — which makes messages the one delivery channel that never gets skipped
|
||||
- **`rig slack`** — inspect and manage one connector implementation. Use registered-human
|
||||
readiness for the delivery decision and `queue create --verify` for its receipt; connector
|
||||
setup or verification is not an outbound human message. After a connection gap the daemon
|
||||
catches up on missed channel messages and marks them late; `rig slack status` shows the
|
||||
catches up on missed messages in the channels this machine owns (its default channel, its
|
||||
channel map's, and on a relayed machine each channel the hub has relayed to it) and on replies
|
||||
in tracked threads whose root or close is within seven days; recovered replies are marked late
|
||||
and never answer a decision. `rig slack status` shows the
|
||||
connection, the catch-up state, and the count of retained retry records for inbound messages,
|
||||
reactions and click answers (kept across restarts, not a distinct-message count). A retained alert whose row closed before its retry isn't
|
||||
posted; the row says why. Its `Delivery:` line says whether events are confirmed arriving;
|
||||
@@ -389,7 +402,8 @@ not have to tear one down to change it, and you rarely have to start from nothin
|
||||
- **`rig up <source>`** — make a whole rig exist and run: from a spec you wrote, a shipped starter
|
||||
by name, a bundle someone handed you, a GitHub folder link (pinned to one commit), or a stopped
|
||||
rig. `rig up` with a pod-aware spec on a stopped team's name replaces that team (the earlier one
|
||||
is archived), so check `rig ps --nodes` first. Launch-created `AGENTS.md` or `CLAUDE` files now show in `git status`.
|
||||
is archived), so check `rig ps --nodes` first. A rig name containing a dot is refused before
|
||||
launch, because tmux would rename its sessions. Launch-created `AGENTS.md` or `CLAUDE` files now show in `git status`.
|
||||
`rig up <rig> --existing` puts back the OpenRig guidance blocks `rig down` removed before a
|
||||
resumed seat starts. A seat still missing one makes the rig `partially_restored` (its node lists
|
||||
`guidanceGaps`) and `rig up` exits 1. A seat that wasn't running at the snapshot, but whose
|
||||
@@ -432,7 +446,7 @@ not have to tear one down to change it, and you rarely have to start from nothin
|
||||
which conversation Codex is on now. If the conversation was changed inside the running Codex,
|
||||
the restart may refuse or resume the earlier one, so look at the pane (`rig capture <seat>`) first.
|
||||
- **`rig add`** / **`rig expand`** — graft one more member, or a whole new pod, onto a rig that is
|
||||
already running. `rig add` takes the rig's name or ID; a name two active rigs share is refused
|
||||
already running. `rig add`, `rig grow` and `rig remove` take the rig's name or ID; a name two active rigs share is refused
|
||||
with `rig_ambiguous` and the candidates. `rig grow --new-pod` and `rig expand` are one ingress; choose by input shape.
|
||||
- **`rig remove`** / **`rig shrink`** — take a seat, or an entire pod, out of a running topology.
|
||||
If it holds active work, pass `--fallback <live-seat>` to reroute before mutation; without a
|
||||
@@ -447,7 +461,12 @@ not have to tear one down to change it, and you rarely have to start from nothin
|
||||
to a node. **`rig adopt`** does the whole thing at once: create the structure and bind the
|
||||
running sessions into it.
|
||||
- **`rig reconcile-session`** — you resumed a seat by hand and the daemon still shows it down; make
|
||||
the system see the live process **without launching anything**.
|
||||
the system see the live process **without launching anything**. A running seat that refuses every
|
||||
send because its tmux pane was never recorded (one launched by an older OpenRig) takes
|
||||
`rig reconcile-session <seat> --repair-missing-pane --no-launch`: it fills only the missing pane,
|
||||
keeps the session and resume token, never adopts, and refuses while typing protection is on.
|
||||
Plain reconcile adopts the seat into a new session without its resume token; use the repair even
|
||||
where a protected seat's compaction refusal still suggests plain reconcile.
|
||||
- **`rig unclaim` / `rig release`** — stop managing one adopted session, or all of them,
|
||||
**without killing the process or the agent inside it**. Handing something back to its human is a
|
||||
first-class move, not an abandonment.
|
||||
@@ -474,6 +493,7 @@ version, from checking the environment to a useful report to the OpenRig team.
|
||||
Stop distinguishes process/listener exit from a clean asynchronous drain: inspect the matching
|
||||
`daemon-shutdown.json` and log when completion is incomplete or unverified. No target is a
|
||||
distinct no-op, and a timeout never proves down. Use command help for exact limits and recovery.
|
||||
`/healthz` reports the daemon's effective UID as `euid`, beside `pid`.
|
||||
- **`rig doctor`** — is the *installation* wired up correctly, or are you chasing a bug that is
|
||||
really a broken install. It checks Claude and Codex installs, and a login or configured provider
|
||||
credential, the way setup does; it fails for a harness the person doesn't use, which they can
|
||||
@@ -566,7 +586,7 @@ version, from checking the environment to a useful report to the OpenRig team.
|
||||
reasoning effort for its next launch, restore, handover or in-place Codex restart without
|
||||
touching its spec or its peers; nothing restarts. **`rig seat effort <seat>`** reports what was
|
||||
requested, what's saved, what the running process was launched with and what the session
|
||||
recorded (NOT VERIFIED until the seat takes a turn).
|
||||
recorded (NOT VERIFIED until the seat takes a turn). It works for Pi seats too, sent as `--thinking`.
|
||||
- **`rig seat set-typing-guard <seat> --mode off|draft-aware|hold --reason <text>`** — choose
|
||||
ordinary automatic delivery, immediate retention when input is busy/unreadable, or holding all automatic
|
||||
input. `--enabled true|false` remains compatible: true selects hold, false selects off.
|
||||
@@ -577,7 +597,9 @@ version, from checking the environment to a useful report to the OpenRig team.
|
||||
- **`rig compact-plan`** → **`rig compact`** — who is near the context wall, then act on it.
|
||||
**Ordering matters: running `compact` without the plan is guessing which seat needed it.**
|
||||
Managed compaction waits for the seat's restore map before compacting; `rig compact --state`,
|
||||
`--cancel` and `--skip-map` inspect, stop or skip that wait. The `claude-compaction-restore`
|
||||
`--cancel` and `--skip-map` inspect, stop or skip that wait. If preparation stops at an ordinary
|
||||
approval, a manual `rig compact` keeps waiting for someone to answer it, within its 180-second
|
||||
budget; OpenRig never answers it, and the request times out after 240 seconds. The `claude-compaction-restore`
|
||||
skill covers writing and reading the map; `claude-compact-in-place` no longer ships. After a
|
||||
managed compaction, refocus waits for the restore request, which names the `refocusing` skill
|
||||
(the daemon installs it globally); three failed refocus attempts in a row for one occupant post
|
||||
@@ -587,6 +609,8 @@ version, from checking the environment to a useful report to the OpenRig team.
|
||||
`submitted, unconfirmed` (it reached the seat, not proof compaction ran), `uncertain` (not
|
||||
replayed) or `not sent`, so confirm by effect. Claude seats that select `openrig-core` load its
|
||||
hooks; a seat can give a slow refocus more time with `OPENRIG_REFOCUS_BUDGET_MS` (4500 to 52000).
|
||||
The refocus work packet stays off by default for Claude Code and Codex;
|
||||
`OPENRIG_REFOCUS_WORK_PACKET=1` turns it on, with each field bounded.
|
||||
- **Know what compaction costs before you reach for it.** On some runtimes what comes back has
|
||||
enough context left to believe it knows everything and not enough to actually know anything — and
|
||||
**the compacted agent is the only one who knows it happened**, while every other seat keeps
|
||||
@@ -825,7 +849,10 @@ your circumstances is configuration, and the ones that are not, another agent ca
|
||||
- **`rig config get` / `reset` / `init-workspace`** — read one value, change a system-wide setting
|
||||
and put it back exactly as it was, or additively lay down the canonical project scaffold on a
|
||||
box that has none. Instance startup uses the same additive initializer for the surrounding
|
||||
`state`, `context`, `skills`, `topology`, and operational roots.
|
||||
`state`, `context`, `skills`, `topology`, and operational roots. With `transport.local_user_guard`
|
||||
on (off by default; `rig config set transport.local_user_guard true`), a Mac's local `rig` requests
|
||||
stop only when the daemon's reported, `ps`-corroborated user differs from yours; unknown evidence
|
||||
lets the call through. It isn't authentication or isolation.
|
||||
- **`rig policy permissions list` / `show` / `current` / `apply`** — what permission policy is
|
||||
actually in effect: discovers custom policy specs, validates refs (malformed can never read as
|
||||
valid OR absent), shows what would apply. The older `rig policy list|show|current|apply` forms
|
||||
@@ -894,7 +921,12 @@ your circumstances is configuration, and the ones that are not, another agent ca
|
||||
windows, listed once per allowance and never added up; `--refresh` (optionally `--provider
|
||||
codex|claude`) reads the logins now. A Claude account's current availability reads `unknown`,
|
||||
because nothing links a seat's reading to the login it used: read each window as that seat's
|
||||
reading, and don't tell a person an account has room from it. With `provider.subscription_moves`
|
||||
reading, and don't tell a person an account has room from it. Each account row also carries a
|
||||
sign-in fact (`usable`, `needs sign-in` or `unknown`); a signed-in Claude login never reads
|
||||
better than `unknown`. A Codex account's last reading, its reset and its last observed exhaustion
|
||||
survive a daemon restart as history (`retained: "before_restart"`), never a current reading; a
|
||||
window whose reset has passed shows an `expectation`, which is planning, not a reading. Run `rig provider status --refresh`
|
||||
before telling anyone an account has room. With `provider.subscription_moves`
|
||||
on (off by default), `rig provider moves` records and lists moves of a Codex home's seats between
|
||||
subscriptions.
|
||||
- **Codex configuration fragments** — a `codex_config_fragment` must open with a table header and
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openrig/daemon",
|
||||
"version": "0.6.12",
|
||||
"version": "0.6.14",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -446,7 +446,7 @@ export class TmuxAdapter {
|
||||
throw error;
|
||||
}
|
||||
const pane = fresh ? created.pane : bound.pane;
|
||||
if (!pane || panes.length !== 1 || panes[0]!.id !== pane) throw new Error("Managed pane identity unavailable or changed; no input written.");
|
||||
if (!pane || panes.length !== 1 || panes[0]!.id !== pane) throw new Error("Managed pane identity unavailable or changed; no input written. If the seat's pane was never recorded, rig reconcile-session <seat> --repair-missing-pane --no-launch fills it (with typing protection off).");
|
||||
if (inputKind === "control" && guard.needsInputCheck(identity)) await guard.beforeInput(identity);
|
||||
if (inputKind === "respawn" && guard.needsInputCheck(identity)) {
|
||||
if (!guard.ownsLifecycle(bound.nodeId) || !await this.isPaneDead(pane)) {
|
||||
|
||||
@@ -37,11 +37,12 @@ export function readFreshOccupantRelations(db: Database.Database, rigId: string)
|
||||
export function readReadyFreshLaunchFences(db: Database.Database, rigId?: string): Map<string, number> {
|
||||
// Indexed: from each node's current tenure into that node's fresh-launch events
|
||||
// (idx_events_node_type_seq), not every event of the type. `rig ps` runs this on every call.
|
||||
// CROSS JOIN keeps events the inner table: left to choose, SQLite scans every event first.
|
||||
const events = db.prepare(`
|
||||
SELECT e.seq AS seq, e.payload AS payload, t.node_id AS nodeId, t.generation_uuid AS generation
|
||||
FROM occupant_tenures t
|
||||
JOIN nodes n ON n.id = t.node_id
|
||||
JOIN events e ON e.node_id = t.node_id AND e.type = 'seat.fresh_launched'
|
||||
CROSS JOIN events e ON e.node_id = t.node_id AND e.type = 'seat.fresh_launched'
|
||||
WHERE t.generation_ordinal =
|
||||
(SELECT MAX(t2.generation_ordinal) FROM occupant_tenures t2 WHERE t2.node_id = t.node_id)${rigId ? " AND n.rig_id = ?" : ""}
|
||||
ORDER BY e.seq
|
||||
|
||||
@@ -195,6 +195,7 @@ export const SETTINGS_VALID_KEYS = [
|
||||
"usage.pi_openrouter_charge",
|
||||
// A verified Claude Code or Codex send reports on input-box evidence; daemon-read, twin of the CLI key.
|
||||
"transport.verify_input_box",
|
||||
"transport.local_user_guard",
|
||||
// OPR.0.4.6.FS-1 W2 — queue-retention maintenance knobs (arch D3; closed-set,
|
||||
// arch-safe defaults BAKED in getDefaultValue, bounded validation in
|
||||
// KEY_CONSTRAINTS). CLI-settable twin: lockstep with cli/src/config-store.ts
|
||||
@@ -305,6 +306,7 @@ const ENV_MAP: Record<SettingsValidKey, { primary: string; legacy?: string }> =
|
||||
"provider.subscription_moves": { primary: "OPENRIG_PROVIDER_SUBSCRIPTION_MOVES" },
|
||||
"usage.pi_cost_facts": { primary: "OPENRIG_USAGE_PI_COST_FACTS" },
|
||||
"usage.pi_openrouter_charge": { primary: "OPENRIG_USAGE_PI_OPENROUTER_CHARGE" },
|
||||
"transport.local_user_guard": { primary: "OPENRIG_TRANSPORT_LOCAL_USER_GUARD" },
|
||||
"transport.verify_input_box": { primary: "OPENRIG_TRANSPORT_VERIFY_INPUT_BOX" },
|
||||
// OPR.0.4.6.FS-1 W2 — retention knobs; net-new keys, OPENRIG_* primary only.
|
||||
"retention.enabled": { primary: "OPENRIG_RETENTION_ENABLED" },
|
||||
@@ -391,6 +393,7 @@ const KEY_TO_PATH: Record<SettingsValidKey, string[]> = {
|
||||
"provider.subscription_moves": ["provider", "subscriptionMoves"],
|
||||
"usage.pi_cost_facts": ["usage", "piCostFacts"],
|
||||
"usage.pi_openrouter_charge": ["usage", "piOpenRouterCharge"],
|
||||
"transport.local_user_guard": ["transport", "localUserGuard"],
|
||||
"transport.verify_input_box": ["transport", "verifyInputBox"],
|
||||
"retention.enabled": ["retention", "enabled"],
|
||||
"retention.transitions_days": ["retention", "transitionsDays"],
|
||||
@@ -675,6 +678,7 @@ function getDefaultValue(key: SettingsValidKey, workspaceRoot: string): string |
|
||||
case "usage.pi_cost_facts": return false;
|
||||
case "usage.pi_openrouter_charge": return false;
|
||||
// Input-box evidence for verified Claude Code and Codex sends; off until the switch is on.
|
||||
case "transport.local_user_guard": return false;
|
||||
case "transport.verify_input_box": return false;
|
||||
// OPR.0.4.6.FS-1 W2 — retention defaults (arch D3 safe values: archive
|
||||
// terminal+aged transitions >30d; prune watchdog_history >14d keep-50/job;
|
||||
|
||||
@@ -173,7 +173,7 @@ function manualReasonMessage(sessionName: string, reason: string): string {
|
||||
case "preparation_incomplete":
|
||||
return `Preparation (restore map and idle wait) did not finish in time for '${sessionName}'; managed compaction is disarmed. Inspect rig compact ${sessionName} --state, retry explicitly, or use --skip-map once.`;
|
||||
case "pane_unknown_protected":
|
||||
return `Refused: '${sessionName}' uses draft-aware input protection, and its tmux pane is not recorded. Input to a protected seat needs a known pane, so no preparation or /compact was sent. For a confirmed live managed seat with one intended pane, run rig reconcile-session ${shellQuote(sessionName)} --no-launch, then check its projection-drift report for unresolved pane observations before explicitly retrying rig compact ${shellQuote(sessionName)}. Reconciliation records a new claimed session row, superseding any running row for the seat; conversation continuity stays unverified. If the intended pane cannot be resolved, use rig context get help for operator recovery. Keep input protection enabled; reconciliation does not restart this attempt.`;
|
||||
return `Refused: '${sessionName}' uses draft-aware input protection, and its tmux pane is not recorded. Input to a protected seat needs a known pane, so no preparation or /compact was sent. Both fixes refuse while protection is on, so with the person's agreement turn it off first (rig seat set-typing-guard ${shellQuote(sessionName)} --mode off --reason "<why>") and back on afterwards (--mode draft-aware). To keep the session and resume token, run rig reconcile-session ${shellQuote(sessionName)} --repair-missing-pane --no-launch. Otherwise rig reconcile-session ${shellQuote(sessionName)} --no-launch adopts: a new session row (any running row for the seat is superseded); it re-captures the resume token if it can, and conversation continuity stays unverified. Then explicitly retry rig compact ${shellQuote(sessionName)}; reconciliation does not restart this attempt. If the intended pane cannot be resolved, use rig context get help for operator recovery.`;
|
||||
case "preparation_cancelled":
|
||||
return `Preparation for '${sessionName}' was cancelled; no further compact is armed.`;
|
||||
case "disabled":
|
||||
|
||||
@@ -666,12 +666,14 @@ export function createApp(deps: AppDeps): Hono {
|
||||
|
||||
app.get("/healthz", (c) => {
|
||||
// OPR.0.4.3.21 — enrich the health surface with event-loop wedge evidence
|
||||
// when the monitor is wired. Absent monitor keeps the exact legacy body so
|
||||
// existing probes / tests that assert `{ status: "ok" }` are unchanged.
|
||||
// when the monitor is wired. Process identity is additive with or without the monitor.
|
||||
// OPR.0.4.4.11 FR-7 — a STAMPED build additionally carries
|
||||
// {semver, commit, dirty, builtAt}; dev runs add NOTHING (stampFields is
|
||||
// {} without a stamp — no invented identity, legacy bodies preserved).
|
||||
const stamp = stampFields();
|
||||
// Self-report for the optional CLI effective-user check, not authentication.
|
||||
const euid = process.geteuid?.();
|
||||
const processIdentity = { pid: process.pid, ...(euid === undefined ? {} : { euid }) };
|
||||
// 51-09 increment 3 (arch ruling 2e1b737f; local always-suffix rendering since
|
||||
// superseded by the 2026-08-27 root invariant) — expose the daemon's boot-reconciled
|
||||
// self-host id so the CLI edge can detect cross-host targets, self-resolve a
|
||||
@@ -706,12 +708,12 @@ export function createApp(deps: AppDeps): Hono {
|
||||
// floor (escalation view + daemon-health); same additive contract.
|
||||
const wakeLadder = deps.wakeLadderStatus ? { wakeLadder: deps.wakeLadderStatus.snapshot() } : {};
|
||||
if (!monitor) {
|
||||
return c.json({ status: "ok", pid: process.pid, ...stamp, ...selfHost, ...slowOperations, ...bind, ...stuckSweep, ...wakeLadder });
|
||||
return c.json({ status: "ok", ...processIdentity, ...stamp, ...selfHost, ...slowOperations, ...bind, ...stuckSweep, ...wakeLadder });
|
||||
}
|
||||
const eventLoop = monitor.snapshot();
|
||||
return c.json({
|
||||
status: "ok",
|
||||
pid: process.pid,
|
||||
...processIdentity,
|
||||
...stamp,
|
||||
...selfHost,
|
||||
eventLoop,
|
||||
|
||||
@@ -23,7 +23,7 @@ describe("OPR.0.4.3.21 — /healthz enrichment", () => {
|
||||
const { app } = createTestApp(db);
|
||||
const res = await app.request("/healthz");
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({ status: "ok", pid: process.pid });
|
||||
expect(await res.json()).toEqual({ status: "ok", pid: process.pid, euid: process.geteuid?.() });
|
||||
db.close();
|
||||
});
|
||||
|
||||
@@ -38,11 +38,13 @@ describe("OPR.0.4.3.21 — /healthz enrichment", () => {
|
||||
const body = (await res.json()) as {
|
||||
status: string;
|
||||
pid: number;
|
||||
euid: number;
|
||||
eventLoop: { lagMeanMs: number; lagP99Ms: number; utilization: number; lastTickAgeMs: number; healthy: boolean };
|
||||
routeTimings: Record<string, unknown>;
|
||||
};
|
||||
expect(body.status).toBe("ok");
|
||||
expect(body.pid).toBe(process.pid);
|
||||
expect(body.euid).toBe(process.geteuid?.());
|
||||
expect(typeof body.eventLoop.lagMeanMs).toBe("number");
|
||||
expect(typeof body.eventLoop.lastTickAgeMs).toBe("number");
|
||||
expect(typeof body.eventLoop.healthy).toBe("boolean");
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type Database from "better-sqlite3";
|
||||
import { createDb } from "../src/db/connection.js";
|
||||
import { migrate } from "../src/db/migrate.js";
|
||||
import { ALL_MIGRATIONS } from "../src/db/all-migrations.js";
|
||||
import { getNodeInventory, getNodeInventoryForRigs } from "../src/domain/node-inventory.js";
|
||||
|
||||
// Without ANALYZE statistics SQLite's join order here does not depend on table size, so a
|
||||
// small fixture shows the plan a long-lived install's daemon runs on every inventory read.
|
||||
const databases: Database.Database[] = [];
|
||||
afterEach(() => { for (const db of databases.splice(0)) db.close(); });
|
||||
|
||||
function seeded(): Database.Database {
|
||||
const db = createDb(); databases.push(db);
|
||||
migrate(db, ALL_MIGRATIONS);
|
||||
db.prepare("INSERT INTO rigs (id, name) VALUES ('rig-1', 'rig-one')").run();
|
||||
db.prepare("INSERT INTO nodes (id, rig_id, logical_id, runtime) VALUES ('node-1', 'rig-1', 'dev.impl', 'claude-code')").run();
|
||||
db.prepare("INSERT INTO occupant_tenures (id, node_id, generation_ordinal, generation_uuid, kind) VALUES ('tenure-1', 'node-1', 1, 'gen-1', 'fresh')").run();
|
||||
db.prepare("INSERT INTO sessions (id, node_id, session_name, status, startup_status) VALUES ('sess-1', 'node-1', 'dev-impl@rig-one', 'running', 'ready')").run();
|
||||
const event = db.prepare("INSERT INTO events (rig_id, node_id, type, payload) VALUES ('rig-1', ?, ?, ?)");
|
||||
event.run(null, "restore.completed", JSON.stringify({ result: { nodes: [{ nodeId: "node-1", status: "resumed" }] } }));
|
||||
event.run("node-1", "seat.fresh_launched", JSON.stringify({ nodeId: "node-1", sessionId: "sess-1", newGeneration: "gen-1", status: "ready" }));
|
||||
for (let i = 0; i < 200; i++) event.run("node-1", "agent.activity", JSON.stringify({ i }));
|
||||
return db;
|
||||
}
|
||||
|
||||
/** The fresh-launch reads the inventory actually prepares, with the plan SQLite runs for each. */
|
||||
function fencePlans(db: Database.Database): string[][] {
|
||||
const original = db.prepare;
|
||||
const statements = new Set<string>();
|
||||
db.prepare = function(sql: string) {
|
||||
if (sql.includes("'seat.fresh_launched'")) statements.add(sql);
|
||||
return original.call(this, sql);
|
||||
} as typeof db.prepare;
|
||||
try {
|
||||
getNodeInventoryForRigs(db, new Set(["rig-1"]));
|
||||
getNodeInventory(db, "rig-1");
|
||||
} finally { db.prepare = original; }
|
||||
return [...statements].map((sql) =>
|
||||
(db.prepare(`EXPLAIN QUERY PLAN ${sql}`).all(...Array((sql.match(/\?/g) ?? []).length).fill("rig-1")) as Array<{ detail: string }>)
|
||||
.map((row) => row.detail));
|
||||
}
|
||||
|
||||
describe("ready fresh-launch fences", () => {
|
||||
it("read each current tenure's fresh launches through idx_events_node_type_seq, never the whole events log", () => {
|
||||
const plans = fencePlans(seeded());
|
||||
expect(plans).toHaveLength(2); // the fleet and the selected-rig read
|
||||
for (const plan of plans) {
|
||||
expect(plan).toContain("SEARCH e USING INDEX idx_events_node_type_seq (node_id=? AND type=?)");
|
||||
expect(plan).not.toContain("SCAN e");
|
||||
}
|
||||
});
|
||||
|
||||
it("still fence a restore that started before the ready fresh launch", () => {
|
||||
const db = seeded();
|
||||
expect(getNodeInventory(db, "rig-1")[0]?.restoreOutcome).toBe("n-a");
|
||||
expect(getNodeInventoryForRigs(db, new Set(["rig-1"])).get("rig-1")).toEqual(getNodeInventory(db, "rig-1"));
|
||||
// A restore after the launch describes the current occupant and still shows.
|
||||
db.prepare("INSERT INTO events (rig_id, node_id, type, payload) VALUES ('rig-1', NULL, 'restore.completed', ?)")
|
||||
.run(JSON.stringify({ result: { nodes: [{ nodeId: "node-1", status: "resumed" }] } }));
|
||||
expect(getNodeInventory(db, "rig-1")[0]?.restoreOutcome).toBe("resumed");
|
||||
});
|
||||
});
|
||||
@@ -132,13 +132,13 @@ function createAppWithUiDist(db: ReturnType<typeof createFullTestDb>, uiDistDir:
|
||||
}
|
||||
|
||||
describe("Hono server (production app)", () => {
|
||||
it("GET /healthz returns 200 with status ok", async () => {
|
||||
it("GET /healthz reports its process effective UID beside pid", async () => {
|
||||
const db = createFullTestDb();
|
||||
const { app } = createTestApp(db);
|
||||
const res = await app.request("/healthz");
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body).toEqual({ status: "ok", pid: process.pid });
|
||||
expect(body).toEqual({ status: "ok", pid: process.pid, euid: process.geteuid?.() });
|
||||
db.close();
|
||||
});
|
||||
|
||||
|
||||
@@ -117,9 +117,10 @@ describe("config routes (User Settings v0)", () => {
|
||||
// + 1 usage.pi_openrouter_charge (slice 02 K4 OpenRouter charge beside the Pi estimate; default off).
|
||||
// + 1 transport.verify_input_box (input-box evidence for verified Claude Code and Codex sends; default off).
|
||||
// + 1 bundle.services_enabled (service archive packing, preview and target installs; default off).
|
||||
expect(body.settings["transport.local_user_guard"]).toMatchObject({ value: false, source: "default", defaultValue: false });
|
||||
expect(body.settings["bundle.services_enabled"]).toMatchObject({ value: false, source: "default", defaultValue: false });
|
||||
// + 1 provider.subscription_moves (slice 18 subscription move record; default off).
|
||||
expect(Object.keys(body.settings).length).toBe(81);
|
||||
expect(Object.keys(body.settings).length).toBe(82);
|
||||
expect(body.settings["provider.subscription_moves"]).toMatchObject({ value: false, source: "default", defaultValue: false });
|
||||
expect(body.settings["launch.non_interruptive"]).toMatchObject({ value: false, source: "default" });
|
||||
expect(body.settings["runtime.readiness_timeout_seconds"]).toMatchObject({ value: 30, source: "default" });
|
||||
|
||||
@@ -165,6 +165,7 @@ describe("SettingsStore (User Settings v0)", () => {
|
||||
"usage.pi_cost_facts",
|
||||
"usage.pi_openrouter_charge",
|
||||
"transport.verify_input_box",
|
||||
"transport.local_user_guard",
|
||||
// OPR.0.4.6.FS-1 W2 — queue-retention maintenance knobs.
|
||||
"retention.enabled",
|
||||
"retention.transitions_days",
|
||||
@@ -377,6 +378,17 @@ describe("SettingsStore (User Settings v0)", () => {
|
||||
expect(store.resolveOne("bundle.services_enabled")).toMatchObject({ value: false, source: "default" });
|
||||
});
|
||||
|
||||
it("local user guard is opt-in and resolves the file/env twins", () => {
|
||||
const store = new SettingsStore(configPath);
|
||||
expect(store.resolveOne("transport.local_user_guard")).toMatchObject({ value: false, source: "default" });
|
||||
store.set("transport.local_user_guard", "true");
|
||||
expect(store.resolveOne("transport.local_user_guard")).toMatchObject({ value: true, source: "file" });
|
||||
try {
|
||||
process.env.OPENRIG_TRANSPORT_LOCAL_USER_GUARD = "false";
|
||||
expect(store.resolveOne("transport.local_user_guard")).toMatchObject({ value: false, source: "env" });
|
||||
} finally { delete process.env.OPENRIG_TRANSPORT_LOCAL_USER_GUARD; }
|
||||
});
|
||||
|
||||
it("transport.verify_input_box is off by default and resolves file, then env", () => {
|
||||
const store = new SettingsStore(configPath);
|
||||
expect(store.resolveOne("transport.verify_input_box"))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openrig/ui",
|
||||
"version": "0.6.12",
|
||||
"version": "0.6.14",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"openrig-operating-model/templates/SOP.md": "1eaf0daf4f9bbf0715e6a5aaf0f29f030af5fe8b8a10232488ee97c507afa6dc",
|
||||
"openrig-operating-model/templates/SPEC.md": "f126536daa59cea3f1cfb0898d83c7cdf99f6392f6f155ea64fe55187dd0f5f1",
|
||||
"openrig-skills/SKILL.md": "c30ca27054b4253db09dc45b7d3e029d8d81f9019f2a1b487d6ca0ca2b99bbf5",
|
||||
"openrig-user/SKILL.md": "fe40d7c0f3418e426f8b91acacc81f845461043be0ddd8cdf211390c2ad66420",
|
||||
"openrig-user/SKILL.md": "8091a8ddda9c53a49bf7ebe0b7d78dda797efc43aa3cf4bfad09b58e92e27d7d",
|
||||
"orienting-to-an-inherited-seat/CHANGELOG.md": "4d0970e75e5c15b00a48d6a7150376d646257777264fd1b9a191165a8d5a6e84",
|
||||
"orienting-to-an-inherited-seat/SKILL.md": "252fed402f19027056bbba241df3305be88de3175b67dc8fe051143bb0d4412b",
|
||||
"queue-handoff/SKILL.md": "5ff22b30a632b9832dcefa6a107866ef06fc3991f8d7d9f367394eb88261be1c",
|
||||
|
||||
@@ -47,6 +47,7 @@ for (const key of [
|
||||
"PROVIDER_USAGE_WINDOWS",
|
||||
// A seat with input-box verification on must not change what the transport suites verify.
|
||||
"TRANSPORT_VERIFY_INPUT_BOX",
|
||||
"TRANSPORT_LOCAL_USER_GUARD",
|
||||
]) {
|
||||
delete process.env[`OPENRIG_${key}`];
|
||||
delete process.env[`RIGGED_${key}`];
|
||||
|
||||
Reference in new issue
Block a user