mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-08 08:11:17 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12df562ceb | ||
|
|
16121548c2 | ||
|
|
e5baf5989b | ||
|
|
8ca69bf8a8 | ||
|
|
0957554c2d | ||
|
|
91929c6521 | ||
|
|
821ddac44e |
No files matched your search
+225
@@ -8,6 +8,231 @@ deprecations, and behavioral changes. Breaking changes are called out explicitly
|
||||
|
||||
---
|
||||
|
||||
## [0.7.8]
|
||||
|
||||
OpenRig 0.7.8 lets a person answer a runner seat's permission requests, with a
|
||||
conformance check and an example runner to build against. It adds a maintained
|
||||
way to run a Claude worker in an NVIDIA OpenShell sandbox, and includes 0.7.7's
|
||||
fixes plus repairs across seats, tmux, Slack and the TUI.
|
||||
|
||||
### Before you upgrade
|
||||
|
||||
- **Restart the daemon after installing:**
|
||||
```bash
|
||||
npm install -g @openrig/cli@0.7.8
|
||||
rig daemon stop && rig daemon start
|
||||
```
|
||||
Four migrations (142 to 145) store a Claude seat's MCP opt-out, seat activity
|
||||
grants, OpenShell environment bindings and the person chosen to answer a
|
||||
seat's permission requests.
|
||||
([#2177](https://github.com/mvschwarz/openrig/pull/2177), [#2295](https://github.com/mvschwarz/openrig/pull/2295), [#2296](https://github.com/mvschwarz/openrig/pull/2296), [#2301](https://github.com/mvschwarz/openrig/pull/2301))
|
||||
- **The bundled openrig-core plugin advances from 0.1.23 to 0.1.24.** One line
|
||||
of its seat-continuity skill changed. Managed refresh preserves replaced files
|
||||
under `$OPENRIG_HOME/backups/managed-refresh/<id>/`; reapply local edits you
|
||||
still need from that copy. ([#2287](https://github.com/mvschwarz/openrig/pull/2287))
|
||||
|
||||
### Permissions for runner seats
|
||||
|
||||
- **A runner seat's permission requests now reach a person, and the default is
|
||||
permit.** The runner contract's first version refused every request. Section 7
|
||||
of `docs/reference/harness-runner-contract.md` now covers who is asked:
|
||||
- **With a default person,** "They get an ordinary ask in the Feed and on
|
||||
Slack. No setup is needed."
|
||||
- **To choose someone else,** run
|
||||
`rig seat set-permissions-to <seat> --to <person> --reason "…"`. `--clear`
|
||||
returns to the default.
|
||||
- **When no one can be asked,** the request is "Permitted at once, with the
|
||||
agent's own allow option."
|
||||
- **Refusal is opt-in,** with `--deny`.
|
||||
|
||||
"A person's no is never a grant." An unanswered request "stays pending and
|
||||
visible" until the agent's turn ends: "It's never timed out into a grant."
|
||||
([#2301](https://github.com/mvschwarz/openrig/pull/2301))
|
||||
- **A runner conformance check:**
|
||||
`node docs/reference/runner-conformance/check-runner.mjs --runner '<runner command with {agent}>' --name <harness>`.
|
||||
It covers the answered path, and needs Node and tmux but no daemon, model,
|
||||
credentials or network.
|
||||
([#2303](https://github.com/mvschwarz/openrig/pull/2303), [#2306](https://github.com/mvschwarz/openrig/pull/2306))
|
||||
- **An example runner for DeepSeek Harness (`dsh`),** with its preset, is under
|
||||
`docs/reference/community-harness-examples/dsh/`. OpenRig doesn't install it.
|
||||
([#2305](https://github.com/mvschwarz/openrig/pull/2305))
|
||||
- **A seat activity grant** lets a runner outside the host's hook setup report
|
||||
activity for one seat and launch, without the install-wide hook token. ([#2295](https://github.com/mvschwarz/openrig/pull/2295))
|
||||
|
||||
### Claude workers in OpenShell sandboxes
|
||||
|
||||
- **`rig environment setup` installs a maintained reference** that binds a seat
|
||||
to one NVIDIA OpenShell sandbox (`rig environment create` or `attach`, then
|
||||
`rig up`).
|
||||
- **Context and results:** the seat's guidance, skills, plugins and startup
|
||||
files go into the sandbox, and verified JSON results come back through
|
||||
queue work.
|
||||
- **Stop and delete:** stopping the agent, stopping compute and deleting the
|
||||
sandbox are separate actions.
|
||||
- **Restore:** a recreated sandbox resumes the same conversation.
|
||||
|
||||
The steps are in `daemon/assets/openshell/README.md`.
|
||||
([#2296](https://github.com/mvschwarz/openrig/pull/2296), [#2300](https://github.com/mvschwarz/openrig/pull/2300))
|
||||
|
||||
### Seats, delivery and the TUI
|
||||
|
||||
- **Seats:**
|
||||
- an isolated tmux server per OpenRig home
|
||||
(`rig config set terminal.tmux_server isolated`);
|
||||
- `claude_mcp: none` for a Claude member;
|
||||
- resuming with a new token file;
|
||||
- first starts on `rig launch --seats`;
|
||||
- an accurate account record and kept effort after `rig provider switch`;
|
||||
- skill copies, activity hooks and startup declarations kept across
|
||||
`rig down`, resumes and exports;
|
||||
- clearer stops for handover's `fork:<seat>`, a missing conversation,
|
||||
`claude` not found, Codex restart refusals and failed unpins.
|
||||
([#1260](https://github.com/mvschwarz/openrig/pull/1260), [#2177](https://github.com/mvschwarz/openrig/pull/2177), [#2269](https://github.com/mvschwarz/openrig/pull/2269), [#2342](https://github.com/mvschwarz/openrig/pull/2342), [#2344](https://github.com/mvschwarz/openrig/pull/2344), [#2337](https://github.com/mvschwarz/openrig/pull/2337), [#2338](https://github.com/mvschwarz/openrig/pull/2338), [#2190](https://github.com/mvschwarz/openrig/pull/2190), [#2297](https://github.com/mvschwarz/openrig/pull/2297), [#2287](https://github.com/mvschwarz/openrig/pull/2287), [#2255](https://github.com/mvschwarz/openrig/pull/2255), [#2282](https://github.com/mvschwarz/openrig/pull/2282), [#2330](https://github.com/mvschwarz/openrig/pull/2330), [#2332](https://github.com/mvschwarz/openrig/pull/2332))
|
||||
- **tmux:**
|
||||
- with `OPENRIG_TMUX_CONTROL=1`, no crash on tmux 3.3a to 3.7c when the
|
||||
control client ends while another leaves;
|
||||
- a submit key that a read-only client blocks is pasted instead.
|
||||
([#2317](https://github.com/mvschwarz/openrig/pull/2317), [#2325](https://github.com/mvschwarz/openrig/pull/2325), [#2345](https://github.com/mvschwarz/openrig/pull/2345))
|
||||
- **Wakes:**
|
||||
- `OPENRIG_WAKE_SUSPEND` matches destinations with a colon;
|
||||
- prompt escalations stay deliverable without a reachable person;
|
||||
- reminders stop at a full quota;
|
||||
- `human@kernel` raises no false finding;
|
||||
- OpenRig's own Codex hooks are trusted when no choice is recorded;
|
||||
- refusals name their fix.
|
||||
([#2353](https://github.com/mvschwarz/openrig/pull/2353), [#2343](https://github.com/mvschwarz/openrig/pull/2343), [#2281](https://github.com/mvschwarz/openrig/pull/2281), [#2286](https://github.com/mvschwarz/openrig/pull/2286), [#2294](https://github.com/mvschwarz/openrig/pull/2294), [#2290](https://github.com/mvschwarz/openrig/pull/2290), [#2284](https://github.com/mvschwarz/openrig/pull/2284), [#2280](https://github.com/mvschwarz/openrig/pull/2280), [#2273](https://github.com/mvschwarz/openrig/pull/2273), [#2202](https://github.com/mvschwarz/openrig/pull/2202))
|
||||
- **Slack and usage:**
|
||||
- typed replies to parked decisions are recorded as answers;
|
||||
- row bodies render Markdown;
|
||||
- resolved decisions stop showing as live asks;
|
||||
- OpenRouter charge retries aren't delayed.
|
||||
([#2293](https://github.com/mvschwarz/openrig/pull/2293), [#2299](https://github.com/mvschwarz/openrig/pull/2299), [#2316](https://github.com/mvschwarz/openrig/pull/2316), [#2349](https://github.com/mvschwarz/openrig/pull/2349), [#2314](https://github.com/mvschwarz/openrig/pull/2314))
|
||||
- **Hosts and setup:**
|
||||
- `rig host edit` and `rig host remove`;
|
||||
- a warning when another OS user's daemon holds the port;
|
||||
- `rig daemon status` shows a pending start.
|
||||
([#2310](https://github.com/mvschwarz/openrig/pull/2310), [#2346](https://github.com/mvschwarz/openrig/pull/2346), [#2302](https://github.com/mvschwarz/openrig/pull/2302), [#2348](https://github.com/mvschwarz/openrig/pull/2348))
|
||||
- **The TUI and views:**
|
||||
- agent pages show health and recent work;
|
||||
- pod, spec and instance-table views read correctly;
|
||||
- `rig terminal open mission:<id> --follow --provider herdr` follows a
|
||||
mission.
|
||||
([#2263](https://github.com/mvschwarz/openrig/pull/2263), [#2264](https://github.com/mvschwarz/openrig/pull/2264), [#2265](https://github.com/mvschwarz/openrig/pull/2265), [#2266](https://github.com/mvschwarz/openrig/pull/2266), [#2327](https://github.com/mvschwarz/openrig/pull/2327), [#2260](https://github.com/mvschwarz/openrig/pull/2260), [#2289](https://github.com/mvschwarz/openrig/pull/2289))
|
||||
- **Also:**
|
||||
- MCP text is cut between whole characters;
|
||||
- an ACP cancel survives a failed note;
|
||||
- long transcript lines read faster;
|
||||
- there are Pi and kernel dashboard recovery guides.
|
||||
([#2268](https://github.com/mvschwarz/openrig/pull/2268), [#2262](https://github.com/mvschwarz/openrig/pull/2262), [#2274](https://github.com/mvschwarz/openrig/pull/2274), [#2186](https://github.com/mvschwarz/openrig/pull/2186), [#2275](https://github.com/mvschwarz/openrig/pull/2275))
|
||||
|
||||
### Behaviour that changed
|
||||
|
||||
- **A runner following section 7 gets answers or permits, not refusals.**
|
||||
`--deny` keeps a seat refusing.
|
||||
- **On tmux older than 3.7c,** `OPENRIG_TMUX_CONTROL=1` uses spawned clients
|
||||
instead of a control client.
|
||||
|
||||
### Known limits
|
||||
|
||||
- **No harness has run end to end under the contract yet.** The dsh example and
|
||||
the conformance check were checked against stand-ins.
|
||||
- **The answer owner is a registered person.** "A colleague's answer is
|
||||
followed on this host. If the ask is handed to another host, the request
|
||||
waits, shown as needs input, until the agent's turn ends."
|
||||
- **`allow_always` from a permit:** with no one to ask, "If the agent offers
|
||||
only `allow_always`, the harness then remembers a permission nobody chose."
|
||||
- **"Permits and answers aren't recorded beyond the runner's pane and the ask
|
||||
itself."**
|
||||
- **Coming as follow-ups:** recording a permit made when no one can be asked,
|
||||
and passing a person's typed words that aren't a plain yes or no to the agent.
|
||||
- **The dsh example keeps one hold per seat:** "if dsh asks twice in one turn,
|
||||
settling one ask releases the hold while the other may still wait on a
|
||||
person."
|
||||
- **The OpenShell reference requires:**
|
||||
- NVIDIA OpenShell 0.1.6;
|
||||
- a Linux image;
|
||||
- an API-backed provider using `ANTHROPIC_API_KEY`. Subscription or OAuth
|
||||
credentials aren't supported.
|
||||
|
||||
"Provider registration and image construction can incur costs." The worker's
|
||||
activity reads unknown, from pane sampling, never idle.
|
||||
- **"Never grant the guest access to the daemon's main API port (normally
|
||||
7433)."** The guest-only listener is what confines a guest's reach. Its grant
|
||||
only authorizes requests for its own seat and launch.
|
||||
- **The 0.7.7 and 0.7.6 limits below still apply.**
|
||||
|
||||
### Credits
|
||||
|
||||
Thanks to @Aschenbath, @dajiaohuang, @Vaishnavi220506, @Nikhila-Dhevi-R,
|
||||
@Nikhi00718, @oodadoudou, @DeryFerd, @ch-davidriott, @vanducng, @Ethereal49,
|
||||
@AdityaxDeore and @Mydudeclaps for merged contributions. Thanks to @salbang,
|
||||
@ziadeh, @davey64, @bpamiri and @oneandonlydean for reports carried into this
|
||||
release, and to @bpamiri for dsh-seat, whose fake agent the conformance check's
|
||||
stand-in follows.
|
||||
|
||||
## [0.7.7]
|
||||
|
||||
**Not yet published.** 0.7.7 is a separate reliability patch for 0.7.6. Its three
|
||||
fixes are also in 0.7.8.
|
||||
|
||||
OpenRig 0.7.7 is 0.7.6 with three reliability fixes for managed Claude seats: an
|
||||
idle prompt under Claude's "Update installed" line is recognized again, managed
|
||||
compaction no longer types into a message you haven't sent, and a Claude seat
|
||||
launched through a wrapper script can be rebuilt again.
|
||||
|
||||
### Before you upgrade
|
||||
|
||||
- **Restart the daemon after installing** so the fixes take effect:
|
||||
```bash
|
||||
npm install -g @openrig/cli@0.7.7
|
||||
rig daemon stop && rig daemon start
|
||||
```
|
||||
There are no database migrations. The bundled openrig-core plugin stays at
|
||||
0.1.23 with unchanged files.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **A Claude seat under the "Update installed" line reads idle again.** After
|
||||
Claude installs an update, it prints "✔ Update installed · /restart to apply"
|
||||
(or "Restart to apply") below its mode bar. OpenRig didn't recognize that line,
|
||||
so a seat at an empty prompt read unknown instead of idle, and managed
|
||||
compaction waited. Both spellings are now recognized; lines that only look
|
||||
similar still read unknown. ([#2311](https://github.com/mvschwarz/openrig/pull/2311))
|
||||
- **Managed compaction no longer types into a draft.** With the optional typing
|
||||
guard off (the default), compaction's preparation prompt could be pasted onto
|
||||
a message someone had typed but not sent. Now the preparation prompt,
|
||||
`/compact` and the restore prompts check the input box first: typed or
|
||||
unreadable input holds the step with a reason. A held restore prompt goes ahead
|
||||
on its own once the box is empty; a manual `rig compact` says why it stopped,
|
||||
to be retried once the box is empty. An empty box or Claude's own suggestion
|
||||
goes ahead.
|
||||
([#2347](https://github.com/mvschwarz/openrig/pull/2347))
|
||||
- **A Claude seat launched through a wrapper script can be rebuilt again.** On
|
||||
0.7.6, `rig seat handover --source rebuild` on a seat whose `claude` is a `#!`
|
||||
script that starts the real Claude without `exec` stopped after 30 seconds with
|
||||
"Waiting to confirm the new Claude process in this pane", though the new Claude
|
||||
was running. The readiness check now also accepts the script's own direct child
|
||||
when it names this launch's conversation; every other check is unchanged.
|
||||
([#2341](https://github.com/mvschwarz/openrig/issues/2341), [#2354](https://github.com/mvschwarz/openrig/pull/2354))
|
||||
- **A failed handover no longer moves the seat's resume target** to the new,
|
||||
unprimed conversation while the seat keeps its previous occupant.
|
||||
([#2354](https://github.com/mvschwarz/openrig/pull/2354))
|
||||
|
||||
### Known limits
|
||||
|
||||
- **Other reasons managed compaction can wait are unchanged.**
|
||||
- **The draft check reads the input box just before pasting.** Typing that starts
|
||||
in the same moment can still be appended to.
|
||||
- **A Claude seat whose current occupant generation was never registered,** such as
|
||||
an adopted Claude carrying another generation, or a managed launch whose tenure
|
||||
record failed to write, keeps its first conversation token, but later hooks don't
|
||||
move it.
|
||||
- **The 0.7.6 known limits below still apply.**
|
||||
|
||||
### Thanks
|
||||
|
||||
- @oneandonlydean for the report and the evidence in #2341.
|
||||
|
||||
## [0.7.6]
|
||||
|
||||
OpenRig 0.7.6 lets one terminal workspace follow the current owners of a slice's
|
||||
|
||||
@@ -26,9 +26,9 @@ use:
|
||||
|
||||
```sh
|
||||
# Claude Code
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.5/skills/rigs --skill rigs --agent claude-code
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.8/skills/rigs --skill rigs --agent claude-code
|
||||
# Codex
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.5/skills/rigs --skill rigs --agent codex
|
||||
npx skills add https://github.com/mvschwarz/openrig/tree/v0.7.8/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,11 +68,11 @@ 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.7.5` 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.7.8` 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.7.5/scripts/install.sh | sh -s -- --dry-run
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.5/scripts/install.sh | sh
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.8/scripts/install.sh | sh -s -- --dry-run
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.8/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.
|
||||
|
||||
@@ -49,7 +49,8 @@ COPY ${OPENRIG_TARBALL} /tmp/openrig.tgz
|
||||
RUN set -eux; \
|
||||
npm install -g /tmp/openrig.tgz; \
|
||||
rm -f /tmp/openrig.tgz; \
|
||||
rig --version
|
||||
rig --version; \
|
||||
rig --help
|
||||
|
||||
# --- layer 4: stub runtime assets (zero-token by construction; staged by the build verb into the
|
||||
# build context as ./stub-assets/, hashed into the manifest via deriveStubAssetsHash) ---
|
||||
|
||||
@@ -16,7 +16,7 @@ This draft was published with **OpenRig 0.7.6** (tag [`v0.7.6`](https://github.c
|
||||
| Community launcher presets, RigSpec members and AgentSpec role/startup declarations | Implemented interfaces | They compose configuration. A launcher must implement its declared effects; successful parsing does not prove delivery or native behavior. |
|
||||
| `runner: true` with `launch_result: true` | Released in 0.7.6 | The declaration is retained at launch, and Core uses the runner identity/activity path. It does not supply a runner executable. |
|
||||
| `passes_model: true` | Released in 0.7.6 | Passes the declared model through `OPENRIG_MODEL`. It does not observe or verify the effective native model. Cursor and Hermes examples leave it off pending native qualification. |
|
||||
| The dsh/ACP input, cancellation and refusal-first permission recipe | Proposed author implementation | No end-to-end dsh integration is established. Native reopen capabilities and the tools-off case remain implementer questions. |
|
||||
| The dsh/ACP input, cancellation and answered permission recipe (a person answers, and the default is permit) | Proposed author implementation | No end-to-end dsh integration is established. Native reopen capabilities and the tools-off case remain implementer questions. |
|
||||
| Exact native resume via an author launcher | Existing interface; integration-specific evidence | A requested conversation must reopen exactly or fail honestly. One provider/harness result does not establish portability to another. |
|
||||
| Environment preparation, projection, artifact export and owned-resource cleanup | Measured in a private wrapper; generic seam proposed | The platform chapter makes the responsibilities reusable. Core does not provide a generic platform driver or `DeploySpec` loader at this baseline. |
|
||||
| `HarnessSpec`, `DeploySpec`, generic lifecycle fields and wake-on-demand declarations | Proposed vocabulary or unsupported configuration | They are not new accepted YAML keys. Use existing composition and an explicit author implementation. |
|
||||
|
||||
@@ -20,7 +20,7 @@ it never reads an unsupported capability as idle, working, ready or verified.
|
||||
| Resume | when declared | `resume: true` with one of the two above (see [Reopening a conversation](#reopening-a-conversation-resume-true)). Without it, a resume says `Resume is unsupported: community harness "<name>" is launch-only`, and a restore stops and asks. |
|
||||
| Activity | partly | Read from the screen only; there are no activity hooks. Work and attention the screen shows are kept, but a screen that looks idle reads `unknown`, never idle, because a quiet prompt can be an approval or a pause. A [runner preset](#runner-presets-runner-true)'s own reports decide it instead. |
|
||||
| Attention | partly | Read from the screen: a permission question or numbered choice that OpenRig recognizes reads as needing attention, and `rig send` won't type into it. A prompt it doesn't recognize reads `unknown`. |
|
||||
| Approval | no | Approvals stay in the harness's own pane. OpenRig relays none, so a person answers them there. |
|
||||
| Approval | no | Approvals stay in the harness's own pane. OpenRig relays none, so a person answers them there. A [runner preset](#runner-presets-runner-true) is the exception: its runner submits the agent's permission requests to OpenRig, which asks a person, and the default is permit (section 7 of the [harness and runner contract](harness-runner-contract.md)). |
|
||||
| Cancel | no | OpenRig has no verb that cancels a running turn. `rig seat stop` and `rig down` end the seat's tmux session; anything the harness runs outside its pane, such as a gateway, is its own, and the preset's `limits` should say what keeps running. Restarting in place and switching harness are refused. |
|
||||
| Transport | yes | The seat is a tmux pane like any other, so `rig capture`, `rig send` and `--host` work as they do for any seat. |
|
||||
|
||||
|
||||
@@ -191,11 +191,11 @@ 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.7.5/scripts/install.sh | sh -s -- --dry-run
|
||||
curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.8/scripts/install.sh | sh -s -- --dry-run
|
||||
```
|
||||
|
||||
The same command without `--dry-run`,
|
||||
`curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.5/scripts/install.sh | sh`, installs the
|
||||
`curl -fsSL https://raw.githubusercontent.com/mvschwarz/openrig/v0.7.8/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
|
||||
|
||||
@@ -183,11 +183,11 @@ existing built-in runtimes or require every possible runner protocol to use ACP.
|
||||
- take answers typed into the pane;
|
||||
- route an approval to another seat.
|
||||
|
||||
So a job that needed an edit or a command approved couldn't finish. The answered path below, added in 0.7.7,
|
||||
So a job that needed an edit or a command approved couldn't finish. The answered path below, added in 0.7.8,
|
||||
replaces this as the default.
|
||||
- **Unknown requests from the agent:** answer with JSON-RPC `-32601` and carry on.
|
||||
|
||||
### The answered path (0.7.7)
|
||||
### The answered path (0.7.8)
|
||||
|
||||
**Only requests the agent actually makes reach anyone.** The harness's own permission mode decides that, and a harness
|
||||
that runs without asking gains no approval step. Ordinary conversation and `rig send` are unchanged.
|
||||
@@ -286,7 +286,7 @@ permission nobody chose. That covers:
|
||||
times first);
|
||||
- a refusal the table marks "permits", and any status or code this section doesn't list.
|
||||
|
||||
**Limits in 0.7.7:**
|
||||
**Limits in 0.7.8:**
|
||||
- **The answer owner is a registered person.** Choosing a seat (another agent) to answer is a later change.
|
||||
- **A colleague's answer is followed on this host.** If the ask is handed to another host, the request waits, shown as
|
||||
needs input, until the agent's turn ends.
|
||||
@@ -371,7 +371,7 @@ permission nobody chose. That covers:
|
||||
2. **For the dsh maintainer:** can dsh's ACP profile run with tools off? A first journey that needs no permission
|
||||
answers is the simplest to check.
|
||||
3. **For OpenRig:** should a later version route permission requests to a chosen seat, as `rig acp run` already does
|
||||
with queue tasks? 0.7.7 routes them to the default person, or to a person chosen for the seat (section 7). A chosen
|
||||
with queue tasks? 0.7.8 routes them to the default person, or to a person chosen for the seat (section 7). A chosen
|
||||
seat is still open.
|
||||
4. **For OpenRig:** once a second ACP harness exists, should one shared ACP runner live in OpenRig rather than one per
|
||||
harness?
|
||||
@@ -17,7 +17,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.7.5`. If OpenRig is already installed, use the tag
|
||||
For a new install, use the latest release's tag, for example `v0.7.8`. 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 [What you need](#what-you-need)). Then type `/rigs` in Claude Code, or `$rigs` in Codex, and say what
|
||||
you want built.
|
||||
|
||||
@@ -71,5 +71,5 @@ In an installed OpenRig, the check is at `$(npm root -g)/@openrig/cli/daemon/doc
|
||||
- **The model handoff:** `OPENRIG_MODEL` is mapped to your harness's own setting, with no wire this check can see.
|
||||
- **A resume for a preset without `resume`:** OpenRig doesn't pass `OPENRIG_RESUME_REF` to it.
|
||||
- **Restarting the agent after a failed handshake:** the contract doesn't rule on that.
|
||||
- **The chosen person's side of an ask:** who may answer, the Feed and Slack, and refusing another person's click or typed
|
||||
words are OpenRig's side, and its own tests cover them. Routing an ask to a seat is a later change.
|
||||
- **The chosen person's side of an ask:** who may answer, the Feed and Slack are OpenRig's side, and its own tests
|
||||
cover them. Routing an ask to a seat is a later change.
|
||||
+256
-178
@@ -1,192 +1,270 @@
|
||||
# What changed in OpenRig 0.7.1
|
||||
# What changed in OpenRig 0.7.8
|
||||
|
||||
This note is for an agent on an install that was just upgraded to 0.7.1. It lists only what behaves differently from
|
||||
0.7.0 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.
|
||||
This note is for an agent on an install that was just upgraded to 0.7.8. OpenRig
|
||||
0.7.8 lets a community harness's runner have its agent's permission requests
|
||||
answered by a person, adds a maintained OpenShell path for Claude workers, and
|
||||
includes 0.7.7's three reliability fixes. If you upgraded from 0.7.5 or earlier,
|
||||
also read "What changed in OpenRig 0.7.6" below. For the full capability map,
|
||||
read `rig context get onboarding-width/public-what-you-can-do.md`. After the
|
||||
upgraded daemon starts, `rig context get reference/whats-new.md` describes this
|
||||
version. Without a running daemon, read the packaged
|
||||
`daemon/docs/reference/whats-new.md`; `$OPENRIG_HOME/reference/whats-new.md` can
|
||||
still be the previous copy until startup.
|
||||
|
||||
## After the upgrade
|
||||
|
||||
- **Restart the daemon** (`rig daemon stop`, then `rig daemon start`). Eight migrations run on the first start (124, 125,
|
||||
127, 128, 129, 130, 131 and 135). Nine settings are added: `policies.claude_compaction.preparation_root` (unset),
|
||||
`skills.router_line.enabled` (off), `decisions.enabled` (false), `decisions.consumers`, `decisions.template_dir`,
|
||||
`retention.stream_days` (30), `retention.stream_max_items` (20000), `provider.selected` (empty) and
|
||||
`daemon.db_conflict` (`warn`). None is removed.
|
||||
- **Stream retention is on.** Stream items emitted after the upgrade are deleted after 30 days, or beyond the newest
|
||||
20,000, unless a record names them (a classification, duplicate link, attempt or disposition). History from before the
|
||||
upgrade is kept. Raise `retention.stream_days` or `retention.stream_max_items` to keep more.
|
||||
- **The bundled openrig-core plugin goes from 0.1.17 to 0.1.18.** Startup replaces the installed plugin's changed files:
|
||||
its Claude hook definitions, three hook scripts and the pre-compaction hook (so hooks start on a seat whose PATH has
|
||||
no Node), and the `openrig-user`, `messaging-the-human` and `forming-an-openrig-mental-model` skills. As in
|
||||
0.7.0, it first copies each file it replaces under `$OPENRIG_HOME/backups/managed-refresh/<id>/`, edited or not, and
|
||||
logs `[plugin-vendor] preserved previous contents of <file> at <copy> before managed refresh`. A copy doesn't mean
|
||||
anyone edited the file; if someone did, reapply the change from the copy.
|
||||
- **Claude seats need Claude Code 2.1.280 or newer to see their team skills,** which now arrive as a per-seat plugin
|
||||
through `CLAUDE_CODE_PLUGIN_DIRS` (below). OpenRig doesn't check the version. A running seat gets the new layout at its
|
||||
next launch, restore or handover.
|
||||
- **If a worktree's `.claude/settings.local.json` says `"defaultMode": "acceptEdits"` and the person didn't choose it,**
|
||||
an earlier OpenRig wrote it when it added or freshly launched a managed Claude seat. 0.7.1 no longer writes it but
|
||||
doesn't repair it; ask the person which mode they want and set it once.
|
||||
- **Restart the daemon** to load the new code: `rig daemon stop`, then
|
||||
`rig daemon start`.
|
||||
- **Migrations 142 to 145** store a Claude seat's MCP opt-out, seat activity
|
||||
grants, OpenShell environment bindings and a seat's chosen permission person.
|
||||
- **The bundled openrig-core plugin goes from 0.1.23 to 0.1.24.** One skill line
|
||||
changed. Managed refresh preserves replaced files under
|
||||
`$OPENRIG_HOME/backups/managed-refresh/<id>/`; reapply local edits you still
|
||||
need from the preserved copy.
|
||||
|
||||
## New things you can do
|
||||
## Runner seats: a person answers permission requests
|
||||
|
||||
- **Choose how much automatic input reaches a seat:** `rig seat set-typing-guard <seat> --mode <mode> --reason "…"`, where
|
||||
`<mode>` adds `openrig-urgent` (OpenRig's own notices and anything urgent) and `urgent-only` between `off` and `hold`.
|
||||
Mark a message urgent with `rig send --urgent`, or create the row at priority `urgent` or `critical`. Held input stays
|
||||
readable (`rig seat held-messages`) and is never replayed.
|
||||
- **Choose how far OpenRig and peers may act on a seat:** `rig seat set-terminal-policy <seat> --preset
|
||||
no-peer-interventions|help-within-instructions|full-control --reason "…"`, with `--helper <addresses…>`,
|
||||
`--rig-default` (seats added later), and exceptions such as `--peer-compaction deny` or `--openrig-model allow`.
|
||||
Managed compaction and saved model or effort changes are checked; a denial is `terminal_policy_refused`. Other
|
||||
interventions are guidance only.
|
||||
- **Read a seat's planned permissions:** `rig seat status <seat>` shows what its next fresh managed launch will pass and
|
||||
where each part comes from, and its terminal policy. `rig setup --router-line yes|no` decides whether OpenRig adds its
|
||||
one line to shared guidance files.
|
||||
- **Let a person choose whether every ask reaches Slack:** `rig gateway human set <id> slack-subscription
|
||||
every-ask|urgent-only|unset`. Every ask is on the TUI's Feed either way. Under `urgent-only`, only an ask
|
||||
created or handed off with `--alert` posts. An ask marked `--alert` stands out in Slack (an interrupt with a mention)
|
||||
under either setting, so use it only for something only they can do, or an approval whose wait costs now.
|
||||
- **Give one Claude member its own setup** in the rig spec: `agent_file`, `append_system_prompt_file`, `plugin_dirs` and
|
||||
`advisor` (an Advisor model). They're copied into that seat's storage, never the project.
|
||||
- **See a seat's own skills:** `rig skill loadout --seat <session|self>`.
|
||||
- **Reach the team from an assistant without a shell:** the MCP server has 34 tools, including `rig_whoami`,
|
||||
`rig_restore_status`, `rig_team`, `rig_telemetry_events`, following a handoff, reading a finished item's artifact and
|
||||
deciding inside a person's decision room. `rig mcp serve --http` serves them over HTTP; `rig mcp token add <name>
|
||||
--seat <session>` gives each caller its own seat.
|
||||
- **Hand a row's task to an outside agent:** `rig acp run <row>` for an ACP agent (Hermes, OpenClaw).
|
||||
`docs/reference/a2a-agents.md` covers A2A agents.
|
||||
- **Run a community harness that names, reports or reopens its conversations:** preset fields `launch_result`, `resume`,
|
||||
`names_conversation` and `startup_assisted`, with examples for Cursor, Hermes and OpenClaw under
|
||||
`docs/reference/community-harness-examples/`.
|
||||
- **Change one edge on a running rig:** `rig edge add <rig> <from> <to> --kind <kind>` and `rig edge remove <rig>
|
||||
<edge-id>` (`--plan`, `--json`). A `delegates_to` change says when it moves who work routes to.
|
||||
- **Run one spec as several rigs:** `rig up <spec> --name <rig>`.
|
||||
- **Retune a watchdog in place:** `rig watchdog update <jobId> [--interval-seconds n] [--message text] [--spec path]`
|
||||
keeps its id and attachments. The context watch takes `--threshold-tokens` or `--threshold-percent`.
|
||||
- **Put restore maps where seats may write:** `rig config set policies.claude_compaction.preparation_root <abs path>`.
|
||||
- **Pick a Herdr session:** `rig terminal open <view> --provider herdr --session <name>`.
|
||||
- **Close a row with its artifact:** `rig queue update <id> --state done --evidence-ref <path>`.
|
||||
- **File and read subscription move reports:** `rig provider moves report` and `rig provider moves`. `--host <id>` and
|
||||
`--codex-home <dir>` on `rig auth status|list|switch` and `rig seat restart` act on another machine's Codex home.
|
||||
- **Route stream observations with typed decisions (opt-in, off by default):** once `decisions.enabled` is true,
|
||||
`decisions.consumers` includes `stream` and the intake seat has `OPENROUTER_API_KEY`, `rig project wake --decide`
|
||||
gives each eligible stream observation one bounded typed decision during that seat's wake: create a row for a supplied
|
||||
owner, join supplied work, or record a duplicate or no action. Anything uncertain, refused or failed goes to the
|
||||
intake seat as its own disposition, and the model isn't asked again. `rig project decisions status` shows the state
|
||||
without making a call. Without `--decide`, or with decisions off, nothing changes; turning decisions off no longer
|
||||
strands an accepted create or join whose effect hadn't landed.
|
||||
- **Read the stream as a subscriber:** `rig stream subscribe <seat>` (`--tags`, `--type`; re-subscribing keeps the
|
||||
place), then `rig stream read` returns the next bounded page for the calling seat (at most 20 items and 64 KiB, with
|
||||
`more`, `unread` and any `gap`). `rig stream subscriptions` lists readers; `rig stream unsubscribe <seat>` stops one.
|
||||
`rig stream show <id>` says what became of an observation.
|
||||
- **Keep the kernel to the harnesses you chose:** `rig setup --providers <list>` is now saved as `provider.selected`, and
|
||||
the kernel starts seats only for those.
|
||||
- **Stop a second daemon on a live database:** `rig config set daemon.db_conflict stop` refuses a start whose database a
|
||||
matching live daemon already holds (the default `warn` only warns). `rig daemon start --allow-db-conflict` allows one
|
||||
deliberate start.
|
||||
Section 7 of `docs/reference/harness-runner-contract.md` is the rule. Read it
|
||||
before writing or changing a runner.
|
||||
- **Who is asked:**
|
||||
- **the default person** (`rig gateway default`, or the only registered
|
||||
person) gets an ordinary ask in the Feed and on Slack;
|
||||
- **a chosen person,** after
|
||||
`rig seat set-permissions-to <seat> --to <person> --reason "…"`. `--clear`
|
||||
returns to the default;
|
||||
- **no one:** when no one can be asked, the request is "Permitted at once,
|
||||
with the agent's own allow option";
|
||||
- **refusal** is opt-in, with `--deny`. `rig seat status` shows the choice.
|
||||
- **What a person's answer does:** a click gives that option, a plain yes gives
|
||||
the allow, and a plain no gives the reject. "A person's no is never a grant."
|
||||
An unanswered request stays pending until the agent's turn ends.
|
||||
- **The runner's protocol:**
|
||||
- `POST /api/activity/permission-requests` submits the request;
|
||||
- `GET …/<askId>?waitSeconds=25` reads the answer (25 at most);
|
||||
- `POST …/<askId>/withdraw` withdraws it at the turn's end.
|
||||
|
||||
## Behaviour that changed
|
||||
Deliver an answer once, only to the live request.
|
||||
- **Check a runner** with
|
||||
`node daemon/docs/reference/runner-conformance/check-runner.mjs --runner '<command with {agent}>' --name <harness>`
|
||||
from the installed package. It needs Node and tmux, and no daemon, model or
|
||||
network.
|
||||
- **The worked example** is the dsh runner under
|
||||
`daemon/docs/reference/community-harness-examples/dsh/`.
|
||||
- **A seat activity grant** lets a runner outside the host's hook setup report
|
||||
activity for one seat and launch, without the install-wide hook token.
|
||||
|
||||
- **Team skills live in each seat's own storage** (`$OPENRIG_HOME/state/seats/<rig id>/<logical seat>/<runtime>/`), not
|
||||
in the project's `.claude/skills` or `.agents/skills`. Claude seats get them as a generated plugin (`openrig:<id>`),
|
||||
Codex seats as their own catalog. If a link sits in that path, the seat starts without them and the result names
|
||||
what wasn't delivered.
|
||||
- **Urgent wakes are labelled:** an urgent or critical row's wake starts with `Urgent (marked by …):` at every seat. A
|
||||
seat on `hold` gets one held notice per wake instead of re-nudges, and lifecycle work is refused at the middle levels as
|
||||
at `hold`.
|
||||
- **A daemon restart picks up a managed compaction where it stopped.** An owed restore step resumes, a `/compact` that
|
||||
may have been sent is never sent again, and `rig compact --state` shows anything stranded or owed.
|
||||
- **A Claude seat whose startup turn is refused** (an expired login, say) holds the rest of its startup context as owed.
|
||||
A startup input sent before the refusal is noticed can still be submitted (see Known issues in the
|
||||
[0.7.1 release notes](https://github.com/mvschwarz/openrig/releases/tag/v0.7.1)).
|
||||
- **`rig send` refuses with `target_needs_input`** when a Claude seat's hook has just reported it running and its screen
|
||||
shows an MCP enablement menu, a question or a permission prompt.
|
||||
- **`rig down` asks before stopping the caller's own rig.** Without a terminal it exits 1, stops nothing and prints the
|
||||
`--yes` command.
|
||||
- **`rig up --plan` doesn't start a stopped daemon:** it exits 1 with `daemon_not_running` and names `rig daemon start`
|
||||
and `--no-kernel`.
|
||||
- **`rig daemon start`** waits up to 20 seconds for a slow daemon, and starts with a warning when only a secondary
|
||||
listener, such as Tailscale's, can't be verified.
|
||||
- **A seat joined with `rig attach` as an outside app** (Claude Desktop) stays attached through `rig down` and restore
|
||||
(`left_attached`). To replace it, `rig unclaim <name>`, then launch.
|
||||
- **New seats get the daemon's `OPENRIG_DB` and `TMUX_TMPDIR`.**
|
||||
- **A rebuilt Codex seat keeps its observed permissions,** and a read-only Codex profile launches without OpenRig's
|
||||
writable roots.
|
||||
- **A seat's own role** goes only to that seat, never into a shared `CLAUDE.md` or `AGENTS.md`, and `rig down` returns
|
||||
guidance files byte for byte.
|
||||
- **Workflow actions** (`project`, `route`, `resume`, `abort`) record the calling agent; a request with neither identity
|
||||
nor actor gets the shared `actor_required` error.
|
||||
- **`rig provider moves` is back** for subscription move reports. It switches nothing.
|
||||
- **`--dangerously-interact` into a seat's input box stops first** when the box already holds typed or pasted text: it
|
||||
shows that text (apart from suggestions) and types nothing. Clear the box and retry, or pass `--type-without-checking`,
|
||||
which clears nothing. Keys for a recognized native menu are sent as before.
|
||||
- **A terminal seat gets exactly the text sent to it,** without the From/To lines (the result says `envelopeOmitted`).
|
||||
A queue wake to a terminal seat types nothing and fails `failed:terminal-seat`; the row stays queued.
|
||||
- **A daemon restart that can't see the seats' tmux server** no longer detaches seats whose recorded process still
|
||||
runs, so it doesn't relaunch them beside themselves.
|
||||
- **Provider credentials and the activity-hook token stay off tmux, shell and `env` command lines** at launch and
|
||||
handover. A launch whose staging fails stops instead of falling back.
|
||||
- **An answer retried from Slack or Steering** after an interrupted close reuses the first reply and finishes the
|
||||
request once.
|
||||
- **One bad row no longer stops the stuck sweep** from flagging the others; **a malformed unrelated diagnosis row** no
|
||||
longer stops health diagnoses from listing.
|
||||
## OpenShell workers
|
||||
|
||||
## Output that changed shape
|
||||
`rig environment setup` installs a reference that binds a seat to one NVIDIA
|
||||
OpenShell sandbox. Then use `rig environment create` (or `attach`), `rig up`, and
|
||||
`rig environment export` for verified JSON results. Stopping the agent, stopping
|
||||
compute and deleting an owned sandbox are separate actions. A recreated sandbox
|
||||
can resume the same conversation from a checkpoint. Follow
|
||||
`daemon/assets/openshell/README.md` in the installed package.
|
||||
|
||||
- **`rig ps --nodes`** separates every cell with one space (the default row is 5 columns wider), adds an `ATTN` column
|
||||
(`yes`, `no` or `?`) before `REASON`, and marks a `running` seat whose tmux couldn't be reached as unconfirmed.
|
||||
- **`rig queue show --json`** no longer has `lastNudgeInputBox`, and send results no longer describe the input box.
|
||||
- **`rig queue show` and `rig queue whoami`** print their human notices on stderr, so stdout is the JSON.
|
||||
- **A claimed row whose owner waits on a person** reads pickup `needs-input`, with the reason.
|
||||
- **`rig down --json`** adds `leftRunning`; `rig reconcile-session` says whether resume metadata was recorded.
|
||||
- **`rig compact-plan`** labels its thresholds as its own and says whether automatic compaction is on.
|
||||
- **A send held for a draft** in the seat's input box reports `inputPhase: "not_reached"`: it was never typed.
|
||||
- **A stream body over 16 KiB** is stored truncated, ending `[truncated by OpenRig: kept N of M bytes]`, and reads mark it
|
||||
`bodyTruncated`. `rig stream list --after <sortKey>` from a pruned item returns 410 `stream_cursor_expired`, naming
|
||||
`--since` to resume, instead of an empty list.
|
||||
**What it needs:**
|
||||
- NVIDIA OpenShell 0.1.6;
|
||||
- a Linux image;
|
||||
- an API-backed provider using `ANTHROPIC_API_KEY`. Subscription or OAuth
|
||||
credentials aren't supported.
|
||||
|
||||
## What to stop doing
|
||||
"Provider registration and image construction can incur costs", so name them to
|
||||
the person before you start. "Never grant the guest access to the daemon's main
|
||||
API port (normally 7433)."
|
||||
|
||||
- **Don't read `lastNudgeInputBox`.** It's gone.
|
||||
- **Don't parse `rig ps --nodes` by column position.** Use `--json`, with `--fields` to pick fields.
|
||||
- **Don't look for a team seat's skills in the project folders.** Use `rig skill loadout --seat`.
|
||||
- **Don't stop and re-register a watchdog to change it.** Use `rig watchdog update`.
|
||||
- **Don't take a rig down to fix one edge.** Use `rig edge add` or `remove`.
|
||||
- **Don't print `OPENRIG_ACTIVITY_*` values** while looking for identity. Use `rig whoami`.
|
||||
- **Don't use `--dangerously-interact` to type over a half-written input box.** It stops and shows the text; choose
|
||||
explicitly, or use `rig send --urgent` where the seat's interruption level allows it.
|
||||
## Other changes to act on
|
||||
|
||||
## Known gaps in 0.7.1
|
||||
- **Mission following:** `rig terminal open mission:<id> --follow --provider herdr`
|
||||
follows a mission's current owners, with the same Herdr path as slices.
|
||||
- **Launch choices:**
|
||||
- `claude_mcp: none` on a Claude member launches it without MCP servers;
|
||||
- `rig config set terminal.tmux_server isolated` gives this OpenRig home its
|
||||
own persistent tmux server.
|
||||
- **Hosts:** `rig host edit` and `rig host remove` correct or retire a host
|
||||
entry. Use them instead of editing `hosts.yaml` by hand.
|
||||
- **Another user's daemon:** when `rig doctor` warns that the port is held by
|
||||
another OS user's daemon, follow its fix (`rig config set daemon.port <free-port>`),
|
||||
or set `OPENRIG_URL` to share that daemon on purpose.
|
||||
- **tmux control mode:** with `OPENRIG_TMUX_CONTROL=1`, OpenRig starts a control
|
||||
client only on tmux 3.7c or newer, and uses spawned clients otherwise.
|
||||
- **Read-only tmux clients:** a submit key refused because of one goes through
|
||||
the paste route.
|
||||
- **Slack:** a typed reply to a decision parked on its person is recorded as the
|
||||
answer. Read the person's words; don't treat the close as approval.
|
||||
- **The TUI:** agent pages now show health findings and recently finished items,
|
||||
and an agent spec's "not loaded in this view" means it wasn't read, not that
|
||||
nothing runs it.
|
||||
|
||||
- **A new Claude seat's startup text can stay in its input box unsent.** If a fresh Claude seat sits idle with text at
|
||||
its prompt, press Enter in its pane, or ask the person to.
|
||||
- **Claude seats on a macOS native install can read as needing attention after a restore.** Check the pane before
|
||||
acting on it.
|
||||
- **A Slack channel-mapping change needs a daemon restart.** Don't use `rig slack disable` then `enable` for it: enabling
|
||||
marks any alerts waiting to post to Slack as already seen.
|
||||
- **Ubuntu 22.04's tmux 3.2a:** the query known to crash its server is gone, but a narrower race remains. Avoid 3.2a
|
||||
(`tmux -V`).
|
||||
- **`rig daemon start` can take 20 to 40 seconds** before its listener warning, longer than the TUI's 30-second "restore
|
||||
everything" wait. On a slow host, start the daemon yourself first.
|
||||
- **A launch that warns "Scoped tmux environment unavailable; retaining the previous environment"** didn't give that seat
|
||||
the clean environment. Check `env` in the seat before relying on OpenRig's variables.
|
||||
- **Going back to 0.7.0 with a Slack subscription set:** 0.7.0 can't read a person file that holds one, and its people
|
||||
registry stops loading. Save the choice first (`rig gateway human show <id> --json > <id>.json`), then run
|
||||
`rig gateway human set <id> slack-subscription unset` (`urgent-only` becomes every-ask, as on 0.7.0); set it again after
|
||||
returning to 0.7.1. A person file still holding a `subscriptions:` entry fails on 0.7.0 too. Don't restore an older
|
||||
gateway folder or database instead: that can lose newer people, bindings and delivery records.
|
||||
- **A Herdr tile over SSH** doesn't come back by itself after a Herdr restart. Reopen it with `rig terminal open <view>
|
||||
--provider herdr`.
|
||||
- **Delegated decisions:** the ask is created before the decision, so without quiet hours it can post to the person
|
||||
first. Pair a decision room with quiet hours, or decide before asking.
|
||||
- **`restore-check` still treats a missing startup file as rig-wide.** Trust `rig up --existing`'s per-seat result.
|
||||
- **Managed Claude launch requires `/usr/bin/env` support for `-0` and `-u`** (option support checked on GNU coreutils
|
||||
and macOS 15; BusyBox and older macOS unverified). Without the required option support, launch stops instead of
|
||||
falling back to credential-bearing command-line arguments.
|
||||
- **In draft-aware mode, an agent's multi-line message to a Claude seat can remain unsubmitted when the paste changes
|
||||
Claude's footer hint.** OpenRig withholds Enter, leaving the text in the composer and later sends waiting. Observed on
|
||||
Claude 2.1.309 with both 0.7.0 and the 0.7.1 pre-cut build; #14 remains open.
|
||||
- **A Claude seat signed in with a long-lived token (`claude setup-token`) gets no usage reading.** The reader accepts
|
||||
only claude.ai subscription logins and reports `not_subscription_login`.
|
||||
## Limits to keep in view
|
||||
|
||||
- **No harness has run end to end under the runner contract yet.** The dsh
|
||||
example and the conformance check were checked against stand-ins.
|
||||
- **The answer owner is a registered person,** and a colleague's answer is
|
||||
followed on this host only. A request handed to another host waits as needs
|
||||
input until the turn ends.
|
||||
- **Permits and answers aren't recorded** beyond the runner's pane and the ask.
|
||||
If the agent offers only `allow_always`, a permit makes its harness remember a
|
||||
permission nobody chose.
|
||||
- **Coming as follow-ups:** recording permits made when no one can be asked, and
|
||||
passing typed words that aren't a plain yes or no to the agent.
|
||||
- **The dsh example keeps one hold per seat:** two asks in one turn can leave one
|
||||
waiting after the hold is released.
|
||||
- **An OpenShell worker's activity reads unknown,** sampled from the pane, never
|
||||
idle.
|
||||
- **The 0.7.7 and 0.7.6 limits below still apply.**
|
||||
|
||||
---
|
||||
|
||||
## What changed in OpenRig 0.7.7
|
||||
|
||||
The changes from 0.7.6 to 0.7.7, a reliability patch for 0.7.6 that isn't
|
||||
published yet. All three fixes are in 0.7.8.
|
||||
|
||||
### The fixes
|
||||
|
||||
- A Claude seat at an empty prompt reads idle again when Claude prints
|
||||
"✔ Update installed · /restart to apply" (or "Restart to apply") below its mode
|
||||
bar after installing an update. Before, that line hid the mode bar, so the seat
|
||||
read unknown and managed compaction waited for an idle prompt it could not see.
|
||||
Both spellings are recognized; lines that only look similar still read unknown.
|
||||
([#2311](https://github.com/mvschwarz/openrig/pull/2311))
|
||||
- Managed compaction checks the input box before it pastes its preparation
|
||||
prompt, `/compact` or a restore prompt, even with the typing guard off. Typed,
|
||||
unreadable or changed input holds that step. A manual `rig compact` then says it
|
||||
stopped to preserve the input box; read `rig compact <seat> --state` and retry
|
||||
once the box is empty. A held restore prompt waits and goes ahead on its own once
|
||||
the box is empty. An empty box or a high-confidence suggestion goes ahead.
|
||||
Ordinary `rig send` and the typing-guard defaults are unchanged.
|
||||
([#2347](https://github.com/mvschwarz/openrig/pull/2347))
|
||||
- A Claude seat whose `claude` is a `#!` wrapper that starts the real Claude without
|
||||
`exec` can be rebuilt again (`rig seat handover --source rebuild`): readiness also
|
||||
accepts the wrapper's direct child when it names this launch's conversation token,
|
||||
and refuses every other process as before. A rebuild that fails no longer leaves
|
||||
the seat's resume target on the new, unprimed conversation.
|
||||
([#2341](https://github.com/mvschwarz/openrig/issues/2341), [#2354](https://github.com/mvschwarz/openrig/pull/2354))
|
||||
|
||||
### Limits to keep in view
|
||||
|
||||
- Other reasons managed compaction can wait are unchanged.
|
||||
- The input-box check is a read just before the paste, not a lock: typing that
|
||||
starts in the same moment can still be appended to.
|
||||
- If a seat's current occupant generation was never registered (an adopted Claude
|
||||
with another generation, or a managed launch whose tenure record failed to write),
|
||||
hooks don't move its resume token after the first one.
|
||||
- The 0.7.6 limits below still apply.
|
||||
|
||||
---
|
||||
|
||||
## What changed in OpenRig 0.7.6
|
||||
|
||||
The changes from 0.7.5 to 0.7.6, for an install upgrading from 0.7.5 or earlier.
|
||||
|
||||
### After the upgrade
|
||||
|
||||
- Restart the daemon (`rig daemon stop`, then `rig daemon start`) to load the new
|
||||
code. Two database migrations record account-home selection and whether a
|
||||
community harness launch declared a runner. Both use prefix 141, with distinct
|
||||
full filenames.
|
||||
- The bundled openrig-core plugin goes from 0.1.22 to 0.1.23. Its Claude activity
|
||||
hook clears permission marks after tool batches. Managed refresh preserves
|
||||
replaced files under `$OPENRIG_HOME/backups/managed-refresh/<id>/`; reapply
|
||||
local edits you still need from the preserved copy.
|
||||
|
||||
### Follow a slice's work in one workspace
|
||||
|
||||
Use `rig terminal open slice:<name-or-id> --follow --provider herdr`. Membership
|
||||
comes from pending, in-progress and blocked queue rows tagged with the exact
|
||||
slice. Associate a row with `rig queue create --slice <name-or-id>`; a handoff
|
||||
inherits its tags unless replaced. A mission-only tag does not associate every
|
||||
slice. One owner appears once, with all its obligations listed.
|
||||
|
||||
When a slice's folder name repeats across missions, tag its work with the slice's
|
||||
unique ID (`rig queue create --slice <id>`) or add `--mission <mission>`. A row
|
||||
tagged only with the shared name cannot be placed, so the view reads incomplete
|
||||
until it is.
|
||||
|
||||
New slice views default to four automatic tiles (`--max-tiles` overrides it);
|
||||
qitem views keep eight. A known empty slice stays following and can repopulate.
|
||||
An incomplete read keeps the prior viewers. Missing or ambiguous scope identities
|
||||
are errors, never an empty team. Pins, drafts, unknown input and focus retain
|
||||
former viewers. Closing a viewer does not stop its agent.
|
||||
|
||||
An owner's join refused before any pane split can join on a later refresh;
|
||||
hold notices keep the original seat, phase and reason. A created viewer whose
|
||||
attach has not yet run can continue without replacing its pane. A viewer whose
|
||||
attach already ran and then exited back to a shell is not restarted
|
||||
automatically, so the whole slice view can hold until that tile is resolved.
|
||||
No tested workaround is offered for that case.
|
||||
|
||||
Slice following targets the Linux Herdr 0.9.5 / protocol 22 path. Mission
|
||||
following is not included. See
|
||||
`reference/terminal-sessions.md` for controls, bounds and refresh behavior.
|
||||
Configured Herdr session endpoints now carry through viewer/follow operations.
|
||||
The TUI's terminal action opens the agent, pod or rig currently being viewed.
|
||||
|
||||
### Changes agents should act on
|
||||
|
||||
- **A readiness deadline does not end a still-starting daemon.** A nonzero
|
||||
pending result names the child PID, launch endpoint and log. The child stays
|
||||
running and the startup reservation stays in place; readiness is not
|
||||
confirmed. Follow the returned pending guidance before attempting recovery.
|
||||
- **Read exited state honestly.** A verified exited process in a retained pane
|
||||
reads `pane_exited`, instead of running and ready. Its outstanding work uses
|
||||
the existing recovery-finding route. Unknown observations still mean unknown.
|
||||
- **Use the same member settings when expanding.** `rig expand` now carries raw
|
||||
members to the canonical rig validator, keeping env, effort and startup
|
||||
settings. Restores keep the model saved with `rig seat set-model`.
|
||||
- **Account-home moves start a fresh conversation with a handover.**
|
||||
`rig provider switch --seat <seat> --to-account <name> --account-home <dir>`
|
||||
selects an existing home; preview with `--dry-run`. `--clear-account-home`
|
||||
returns to configured defaults, not historical credentials. Check the moved
|
||||
seat's account and conversation.
|
||||
- **Claude API-key switching resumes the same conversation but interrupts work.**
|
||||
Use `rig provider switch --seat <seat> --to-account api --api-key-file <abs>`;
|
||||
return with `--to-account plan --token-file <abs>`. The API-key path remains
|
||||
unqualified. Do not combine it with account-home selectors. Credential files
|
||||
stay private; budgets remain the caller's responsibility.
|
||||
- **A new Claude member can name its subscription token in the spec:**
|
||||
`claude_token: {label: name, file: /abs/token}`, beside its own
|
||||
`env.CLAUDE_CONFIG_DIR`. This stores a file reference, not the token. Validation
|
||||
does not read the file; a missing or unreadable token uses the existing launch
|
||||
fallback notice, so verify the account actually used.
|
||||
- **Git over SSH uses the daemon's SSH agent** in managed seats. Make sure that
|
||||
agent has the intended key (`ssh-add -l`).
|
||||
- **Slack button answers resume decisions parked on the person asked.** Set
|
||||
`rig slack setup --reactions off` if acknowledgement emoji should not wake
|
||||
agents. The default remains `route`; manifest regeneration preserves it.
|
||||
Replies posted outside a deleted Slack thread are tracked where Slack put them.
|
||||
- **Queue delivery keeps its original intent.** A wake refused at a selection
|
||||
prompt retries after it clears. Cross-host `queue create --verify` reads the
|
||||
destination's receipt. An unreadable receipt stays unconfirmed; do not replay
|
||||
a create just because verification could not establish delivery.
|
||||
- **Preset authors can opt into `runner: true` and `passes_model: true`.** The
|
||||
runner also requires `launch_result: true` and reports activity through the
|
||||
existing hook route. Model input arrives as `OPENRIG_MODEL`. Passed means the
|
||||
launch command was sent, not that the harness applied that model; the observed
|
||||
model remains unknown. Cursor and Hermes examples leave model passing off
|
||||
pending native qualification.
|
||||
Start with the draft [integration author contracts](author-contracts.md) for
|
||||
harness, runner and platform duties, with implemented interfaces distinguished from proposals.
|
||||
- **Update viewer API handling.** `/api/terminal/viewer/ensure` returns 400 for
|
||||
`view_required` and `unknown_provider` where it previously returned 200 with
|
||||
an error body. Slice not-found is 404; ambiguous/unavailable is 409. Continue
|
||||
reading the body code. Unsupported follow kinds keep `follow_view_required`
|
||||
in a 200 `ok:false` response.
|
||||
|
||||
### Limits to keep in view
|
||||
|
||||
- The reported [Codex hook-refresh refusal (#2200)](https://github.com/mvschwarz/openrig/issues/2200)
|
||||
remains unresolved. A verified Node-launcher fix is included, but that does
|
||||
not establish that this report is fixed. If
|
||||
`rig seat restart <seat> --keep-conversation` refuses, nothing is stopped:
|
||||
leave the seat running and report the refusal with CLI and daemon versions.
|
||||
A refused restart has not refreshed hooks; review any Codex hook prompt.
|
||||
- Retained messages are not delivered. Protected Codex pastes and unreadable
|
||||
composers can still retain work; inspect the explanation, use a larger pane
|
||||
or shorten the message. Turning the typing guard off removes protection.
|
||||
- Inventory now avoids redundant reads, but no general speed or stability
|
||||
improvement is claimed without comparable measurements.
|
||||
- Internet-facing hosted members remain experimental, unsupported, off by
|
||||
default and at your own risk. Slice following does not require them.
|
||||
Generated
+6
-90
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openrig",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openrig",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.8",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"packages/daemon",
|
||||
@@ -1136,7 +1136,6 @@
|
||||
"version": "1.32.2",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.2.tgz",
|
||||
"integrity": "sha512-SWsVtH/0vULSQStoZ6xxH2K09Li8fXYTMC64ZeogCdpZ3XSi1eCPOByV1OpuJxuI3OcmHhosDWYyii07tpPsLw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.9 || ^2.0.5",
|
||||
@@ -2902,7 +2901,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "^3.0.0",
|
||||
@@ -3125,7 +3123,6 @@
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.4.0.tgz",
|
||||
"integrity": "sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
@@ -3150,7 +3147,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
|
||||
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -3164,7 +3160,6 @@
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
||||
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
@@ -3226,7 +3221,6 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
@@ -3246,7 +3240,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -3260,7 +3253,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
@@ -3405,7 +3397,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.1.tgz",
|
||||
"integrity": "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -3419,7 +3410,6 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@@ -3436,7 +3426,6 @@
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@@ -3452,7 +3441,6 @@
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.6.0"
|
||||
@@ -3462,7 +3450,6 @@
|
||||
"version": "2.8.6",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
|
||||
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
@@ -3480,7 +3467,6 @@
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
@@ -3664,7 +3650,6 @@
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -3699,7 +3684,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
@@ -3745,7 +3729,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
@@ -3760,7 +3743,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
@@ -3773,7 +3755,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
@@ -3796,7 +3777,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -3806,7 +3786,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -3823,7 +3802,6 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
@@ -3887,7 +3865,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
@@ -3904,7 +3881,6 @@
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@@ -3914,7 +3890,6 @@
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
|
||||
"integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventsource-parser": "^3.0.1"
|
||||
@@ -3927,7 +3902,6 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz",
|
||||
"integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -3947,7 +3921,6 @@
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.3.0.tgz",
|
||||
"integrity": "sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
@@ -3991,7 +3964,6 @@
|
||||
"version": "8.7.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.1.tgz",
|
||||
"integrity": "sha512-2FXbqkygil3mZiXzEtM0IjE/UBmBNot0J1GEVrJT4JbrvwOSoEtqtO+wHbiMQELWe1eNvHoYRg2dkjOid+6JkA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
@@ -4011,7 +3983,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
|
||||
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -4113,7 +4084,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
@@ -4135,7 +4105,6 @@
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@@ -4158,7 +4127,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
@@ -4213,7 +4181,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
@@ -4247,7 +4214,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
@@ -4286,7 +4252,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -4308,7 +4273,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -4355,7 +4319,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
@@ -4427,14 +4390,12 @@
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.7.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.5.tgz",
|
||||
"integrity": "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
@@ -4444,7 +4405,6 @@
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
@@ -4518,7 +4478,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isbot": {
|
||||
@@ -4534,7 +4493,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
@@ -4550,7 +4508,6 @@
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz",
|
||||
"integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
@@ -4626,7 +4583,6 @@
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
|
||||
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/json5": {
|
||||
@@ -4723,7 +4679,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -4733,7 +4688,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
|
||||
"integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
@@ -4747,7 +4701,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -4794,7 +4747,6 @@
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@@ -4804,7 +4756,6 @@
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "^1.54.0"
|
||||
@@ -4852,7 +4803,6 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mz": {
|
||||
@@ -4888,7 +4838,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz",
|
||||
"integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^2.1.0"
|
||||
@@ -4905,7 +4854,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
|
||||
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -4968,7 +4916,6 @@
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -4981,7 +4928,6 @@
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.2.tgz",
|
||||
"integrity": "sha512-ATWUv387i+nLD49EiDoOft5scf32Vsdz3ktrQJG9bi7jRFmaXTssvyvj1EmeKVVgvesbqUtvQQvaS2TpTukAZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
@@ -4998,7 +4944,6 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
@@ -5021,7 +4966,6 @@
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
@@ -5031,7 +4975,6 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -5047,7 +4990,6 @@
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -5111,7 +5053,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
|
||||
"integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
@@ -5293,7 +5234,6 @@
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
|
||||
"integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
@@ -5321,7 +5261,6 @@
|
||||
"version": "6.16.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz",
|
||||
"integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.1",
|
||||
@@ -5358,7 +5297,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
|
||||
"integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@@ -5372,7 +5310,6 @@
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
|
||||
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
@@ -5388,7 +5325,6 @@
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
||||
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
@@ -5654,7 +5590,6 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
@@ -5701,7 +5636,6 @@
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
@@ -5737,7 +5671,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
@@ -5785,7 +5718,6 @@
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
|
||||
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "^2.0.0",
|
||||
@@ -5805,14 +5737,12 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
@@ -5825,7 +5755,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -5835,7 +5764,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
||||
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -5855,7 +5783,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -5872,7 +5799,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
@@ -5891,7 +5817,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
@@ -5946,7 +5871,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
@@ -6246,7 +6170,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
@@ -6314,7 +6237,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
|
||||
"integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^2.0.0",
|
||||
@@ -6333,7 +6255,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
|
||||
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -6377,7 +6298,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
@@ -6475,7 +6395,6 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
@@ -7201,7 +7120,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
@@ -7234,7 +7152,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
@@ -7310,7 +7227,6 @@
|
||||
"version": "3.25.2",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
|
||||
"integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.28 || ^4"
|
||||
@@ -7346,13 +7262,14 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@openrig/cli",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.8",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.13.0",
|
||||
"@hono/node-ws": "^1.3.1",
|
||||
"@modelcontextprotocol/node": "^2.1.2",
|
||||
"@modelcontextprotocol/sdk": "^1.32.2",
|
||||
"@modelcontextprotocol/server": "^2.4.0",
|
||||
"ajv": "^8.18.0",
|
||||
"ajv-formats": "^3.0.1",
|
||||
@@ -7373,7 +7290,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@modelcontextprotocol/client": "^2.4.0",
|
||||
"@modelcontextprotocol/sdk": "^1.32.2",
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^3.0.0"
|
||||
@@ -7384,7 +7300,7 @@
|
||||
},
|
||||
"packages/daemon": {
|
||||
"name": "@openrig/daemon",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.8",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.13.0",
|
||||
@@ -7426,7 +7342,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@openrig/ui",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.8",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openrig",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.8",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openrig/cli",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.8",
|
||||
"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": [
|
||||
@@ -48,6 +48,7 @@
|
||||
"@hono/node-server": "^1.13.0",
|
||||
"@hono/node-ws": "^1.3.1",
|
||||
"@modelcontextprotocol/node": "^2.1.2",
|
||||
"@modelcontextprotocol/sdk": "^1.32.2",
|
||||
"@modelcontextprotocol/server": "^2.4.0",
|
||||
"ajv": "^8.18.0",
|
||||
"ajv-formats": "^3.0.1",
|
||||
@@ -64,7 +65,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@modelcontextprotocol/client": "^2.4.0",
|
||||
"@modelcontextprotocol/sdk": "^1.32.2",
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^3.0.0"
|
||||
|
||||
@@ -110,7 +110,7 @@ export function environmentCommand(override?: EnvironmentCommandDeps): Command {
|
||||
.requiredOption("--seat <address>").option("--node <id>", "Existing node ID; otherwise claimed by the exact named seat on its first launch")
|
||||
.requiredOption("--gateway <ref>").requiredOption("--workspace <ref>").requiredOption("--name <name>")
|
||||
.requiredOption("--image <digest>").requiredOption("--provider <ref>").requiredOption("--policy <path>")
|
||||
.requiredOption("--guest-base-url <origin>", "Guest-reachable daemon origin for the single P6 activity grant")
|
||||
.requiredOption("--guest-base-url <origin>", "Guest-reachable guest-only activity listener origin (not the main daemon API)")
|
||||
.option("--operation <id>", "Stable effect identity (default verb plus binding ID)").option("--json");
|
||||
if (verb === "attach") sub.requiredOption("--resource-id <id>", "Immutable live resource ID").option("--fingerprint <value>", "Expected host-key fingerprint");
|
||||
sub.action(action(async (binding: string, opts: Record<string, string>) => {
|
||||
@@ -160,10 +160,11 @@ export function environmentCommand(override?: EnvironmentCommandDeps): Command {
|
||||
.option("--json").description("Select finite completion for one task, generation and required verified export; persistent defaults stay unchanged")
|
||||
.action(action(async (id: string, opts: { request: string }) => report(checked(await (await client()).post<Record<string, any>>(`/api/environments/${encodeURIComponent(id)}/completion`, JSON.parse(readFileSync(opts.request, "utf8")))))));
|
||||
cmd.command("effect").argument("<binding>").requiredOption("--effect <name>", "close-viewer, stop-native, stop-compute or delete-owned")
|
||||
.requiredOption("--generation <id>", "Selected execution generation").requiredOption("--operation <id>", "Stable effect identity; retries reconcile without another provider mutation")
|
||||
.option("--generation <id>", "Selected execution generation; omit only for delete-owned before any generation exists")
|
||||
.option("--resource-id <id>", "Exact immutable compute ID; required for delete-owned without a generation").requiredOption("--operation <id>", "Stable effect identity; retries reconcile without another provider mutation")
|
||||
.option("--engine-socket <path>", "Owned local rootless Podman socket, required to capture dependent identities before delete-owned").option("--json")
|
||||
.description("Execute one selected effect and return its separate observed outcome; attached native/storage/compute are preserved")
|
||||
.action(action(async (id: string, opts: { effect: string; generation: string; operation: string; engineSocket?: string }) => report(checked(await (await client()).post<Record<string, any>>(`/api/environments/${encodeURIComponent(id)}/effect`, { effect: opts.effect, generation: opts.generation, operationId: opts.operation, engineSocket: opts.engineSocket }, { timeoutMs: 120_000 })))));
|
||||
cmd.addHelpText("after", "\nStart with rig environment setup --help. Create/attach are explicit effects; a 202 is unconfirmed. The first native start remains rig up with the installed openshell-claude preset. Checkpoint before stop, select effects explicitly, and recreate compute before exact normal rig restore. No unknown effect permits fresh-native fallback.\n");
|
||||
.action(action(async (id: string, opts: { effect: string; generation?: string; resourceId?: string; operation: string; engineSocket?: string }) => report(checked(await (await client()).post<Record<string, any>>(`/api/environments/${encodeURIComponent(id)}/effect`, { effect: opts.effect, generation: opts.generation ?? null, ...(opts.resourceId ? { resourceId: opts.resourceId } : {}), operationId: opts.operation, engineSocket: opts.engineSocket }, { timeoutMs: 120_000 })))));
|
||||
cmd.addHelpText("after", "\nStart with rig environment setup --help. Run from your managed seat, or set OPENRIG_SESSION_NAME to the acting member@rig in a plain shell for existing transport attribution (not authentication). --guest-base-url names the separate guest-only activity listener, reachable from the guest; never the main API. Create/attach are explicit effects; a 202 is unconfirmed. The first native start remains rig up with the installed openshell-claude preset. Checkpoint before stop, select effects explicitly, and recreate compute before exact normal rig restore. No unknown effect permits fresh-native fallback.\n");
|
||||
return cmd;
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export const USAGE_ACROSS_HOSTS_REFRESH_TIMEOUT_MS = 75_000;
|
||||
type Peer = { logicalId: string; sessionName: string | null; home?: "same" | "different" | "unknown"; effect: string; step?: string };
|
||||
type SeatView = { home?: string; activity?: string; peers: Peer[] };
|
||||
type SwitchReport = { credentialKind?: "api-key"; home?: string; account: string; tokenFile?: string; conversation?: string | null; loginChanged: boolean; undo: string | null; peers: Peer[];
|
||||
restart?: { recoveryCommand?: string; remainingStep?: string }; next?: string; warnings?: string[]; tokenApplied?: boolean };
|
||||
restart?: { recoveryCommand?: string; remainingStep?: string }; next?: string; warnings?: string[]; tokenApplied?: boolean; effort?: string };
|
||||
|
||||
// A switch waits as long as `rig seat restart` does: the seat's in-place restart runs inside it.
|
||||
const SWITCH_TIMEOUT_MS = 120_000;
|
||||
@@ -735,6 +735,7 @@ CLAUDE_CONFIG_DIR and are not isolated by an account home; check them before rel
|
||||
console.log(`Claude ${seat.credentialKind === "api-key" ? "API key" : "token"}: ${seat.tokenFile ? `"${seat.account}", read from ${seat.tokenFile}` : "the config dir's stored login"}${seat.loginChanged
|
||||
? seat.tokenApplied === false ? " (reference saved; the token didn't take effect)" : " (set)" : ""}`);
|
||||
for (const warning of seat.warnings ?? []) console.log(`warning: ${warning}`);
|
||||
if (seat.effort) console.log(`effort: ${seat.effort}`);
|
||||
if (seat.conversation) console.log(`conversation: ${seat.conversation}`);
|
||||
if (seat.undo) console.log(`to switch back: ${seat.undo}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { environmentCommand } from "../src/commands/environment.js";
|
||||
import { STATE_FILE, type LifecycleDeps } from "../src/daemon-lifecycle.js";
|
||||
import type { EnvironmentCommandDeps } from "../src/commands/environment.js";
|
||||
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
it.each([undefined, "generation-one"])("sends the explicit cleanup selection with generation %s", async generation => {
|
||||
const lifecycleDeps: LifecycleDeps = {
|
||||
spawn: vi.fn(), fetch: vi.fn(async () => ({ ok: true })), kill: vi.fn(() => true),
|
||||
readFile: vi.fn((path: string) => path === STATE_FILE ? JSON.stringify({ pid: 123, port: 7433, db: "test.sqlite", startedAt: "2026-04-20T00:00:00Z" }) : null),
|
||||
writeFile: vi.fn(), removeFile: vi.fn(), exists: vi.fn((path: string) => path === STATE_FILE),
|
||||
mkdirp: vi.fn(), openForAppend: vi.fn(() => 3), isProcessAlive: vi.fn(() => true),
|
||||
};
|
||||
const post = vi.fn(async () => ({ status: 200, data: { state: "confirmed" } }));
|
||||
const deps: EnvironmentCommandDeps = { lifecycleDeps, clientFactory: () => ({ post }) as never };
|
||||
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
await environmentCommand(deps).parseAsync(["node", "rig", "effect", "binding-one", "--effect", "delete-owned", "--operation", "delete-one",
|
||||
...(generation ? ["--generation", generation] : ["--resource-id", "compute-one"]), "--engine-socket", "/owned/engine.sock", "--json"]);
|
||||
expect(post).toHaveBeenCalledWith("/api/environments/binding-one/effect", {
|
||||
effect: "delete-owned", generation: generation ?? null, ...(generation ? {} : { resourceId: "compute-one" }), operationId: "delete-one", engineSocket: "/owned/engine.sock",
|
||||
}, { timeoutMs: 120_000 });
|
||||
expect(log).toHaveBeenCalled();
|
||||
});
|
||||
@@ -392,4 +392,11 @@ describe("rig provider switch --seat tells the person what follows", () => {
|
||||
expect(after.logs).toContain("recovery: rig launch rig dev.a --snapshot-id snap-1");
|
||||
expect(after.logs).toContain("If the pane shows no running Claude, stop the seat first.");
|
||||
});
|
||||
|
||||
it("a Claude seat's switch prints which effort its relaunch used, and why", async () => {
|
||||
const effort = "kept the running effort high: the seat has no effort choice of its own.";
|
||||
const kept = switchDeps(async () => ({ status: 200, data: { outcome: "rebind_in_progress", message: "Relaunched dev-a@rig in its own pane.",
|
||||
seat: { account: "work", tokenFile: "/tokens/work", conversation: "conv-1", loginChanged: true, tokenApplied: true, effort, undo: null, peers: [] } } }));
|
||||
expect((await run(["switch", "--seat", "dev-a@rig", "--to-account", "work", "--token-file", "/tokens/work"], kept)).logs).toContain(`effort: ${effort}`);
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,8 @@ The image recipe is an input recipe, not a published prebuilt image or a reprodu
|
||||
|
||||
## Install the reference and work connection
|
||||
|
||||
Run the environment commands from your managed OpenRig seat. In a plain shell, set `OPENRIG_SESSION_NAME` to the actual acting `member@rig` address, for example `export OPENRIG_SESSION_NAME=operator@kernel` when operating as that member. The CLI uses this existing convention to stamp `X-OpenRig-Session`; it attributes requests within the daemon's trusted transport, and is not authentication. The work and activity credentials below are separate authorities, not replacements for this actor attribution.
|
||||
|
||||
Start an MCP HTTP listener using `rig mcp serve --http --help` and the normal member-grant file on this daemon host. Select a guest-reachable address and explicit port. Setup adds one non-owner, one-seat, one-rig work grant in that existing file and stores its bearer credential in a private managed file. It prints only the file reference. Existing files/presets with different bytes are preserved.
|
||||
|
||||
```sh
|
||||
@@ -15,7 +17,7 @@ rig environment setup --directory ./openshell-example --binding worker-one \
|
||||
--guest-base-url http://host.openshell.internal:7782 --json
|
||||
```
|
||||
|
||||
Enable the daemon's separate guest activity listener with `OPENRIG_GUEST_ACTIVITY_PORT=7782` and a guest-reachable `OPENRIG_GUEST_ACTIVITY_HOST` in the daemon launch environment. It is disabled by default; this is an explicit additional listener, not a change to the main API's binding. The listener serves only `POST /api/activity/hooks`, using the seat/generation grant. It exposes no queue, config, event-stream or parked-seat reads and refuses the install-wide token. Use the actual guest-reachable MCP and guest-activity ports. Setup generates `policy.yaml` for those two selected host/port pairs, without blanket egress. Never grant the guest access to the daemon's main API port (normally 7433). OpenShell's REST policy applies to a host and port: the dedicated listener confines the surface, while its activity grant and the separate MCP work grant authorize their respective requests. The activity rule names `/usr/bin/node`, matching the selected plugin hook interpreter. Qualify that path in your image. The separate read-only `platform.claude.com` rule is for the pinned native startup check and carries no inference credential.
|
||||
Enable the daemon's separate guest activity listener with `OPENRIG_GUEST_ACTIVITY_PORT=7782` and a guest-reachable `OPENRIG_GUEST_ACTIVITY_HOST` in the daemon launch environment. It is disabled by default; this is an explicit additional listener, not a change to the main API's binding. The bind host must be an address on the daemon host that the guest can actually reach; a listener bound only to `127.0.0.1` is not reachable through a host alias that maps to a different interface. Set `OPENRIG_GUEST_ACTIVITY_HOST` to that selected host address and use its guest-visible name in `--guest-base-url`. With rootless networking, verify the host alias's route rather than assuming it reaches loopback. Bind the MCP listener to its separately selected guest-reachable address as well. The listener serves only `POST /api/activity/hooks` and the three permission-request routes (`POST /api/activity/permission-requests`, `GET /api/activity/permission-requests/<askId>` and `POST /api/activity/permission-requests/<askId>/withdraw`), using the seat/generation grant. It exposes no queue, config, event-stream or parked-seat reads and refuses the install-wide token. Use the actual guest-reachable MCP and guest-activity ports. Setup generates `policy.yaml` for those two selected host/port pairs, without blanket egress. Never grant the guest access to the daemon's main API port (normally 7433). OpenShell's REST policy applies to a host and port: the dedicated listener confines the surface, while its activity grant and the separate MCP work grant authorize their respective requests. The activity rule names `/usr/bin/node`, matching the selected plugin hook interpreter. Qualify that path in your image. The separate read-only `platform.claude.com` rule is for the pinned native startup check and carries no inference credential.
|
||||
|
||||
Import `claude-code-provider.yaml` using OpenShell's provider-profile commands, then create a provider with its `ANTHROPIC_API_KEY` environment lookup. See `openshell provider --help`; keep the master credential in provider custody, never an argument or source file. `create` passes that provider reference with `--no-auto-providers`. The guest helper saves only the provider-injected reference in a private file and uses Claude's `apiKeyHelper`. It does not copy an owner authentication home.
|
||||
|
||||
@@ -82,6 +84,15 @@ rig environment effect worker-one --effect stop-compute --generation YOUR_OCCUPA
|
||||
rig environment effect worker-one --effect delete-owned --generation YOUR_OCCUPANT_GENERATION --operation delete-one --engine-socket /run/user/YOUR_UID/podman/podman.sock --json
|
||||
```
|
||||
|
||||
If create succeeded but projection failed before any execution generation was established, inspect the binding and select its exact immutable resource ID instead of inventing a generation:
|
||||
|
||||
```sh
|
||||
rig environment inspect worker-one --json
|
||||
rig environment effect worker-one --effect delete-owned --resource-id YOUR_IMMUTABLE_RESOURCE_ID --operation delete-unstarted-one --engine-socket /run/user/YOUR_UID/podman/podman.sock --json
|
||||
```
|
||||
|
||||
Omitting `--generation` is supported only for `delete-owned` with an explicit `--resource-id` while the binding still has no generation. A newly established generation or changed resource refuses this selection; inspect again and select the actual generation. Attached resources remain preserved.
|
||||
|
||||
Use the actual local rootless Podman socket on the provider host. The observer is read-only; it never removes engine resources itself. Before deletion it records exact workload/supervisor container identities and their attributable workspace/control/channel volumes. After the single vendor request, a complete gateway listing must establish the immutable resource's absence, and independent queries on that same engine/socket must establish those components absent. Other engine resources are preserved. Transport/auth errors, changed sockets, incomplete lists or a remaining volume stay unconfirmed. A vendor Accepted/Completed response alone does not establish engine cleanup. Per-sandbox secret cleanup is not an observed component class here.
|
||||
|
||||
Attached consumers may close only their own viewer. Their external native process, storage and compute remain preserved. `rig seat stop` and `rig down` retain their existing pane semantics and label community panes accordingly; neither stands in for these explicit resource receipts. A pending/unconfirmed effect retains its original operation. Repeating that operation observes its result without blindly repeating stop/delete. A reused name with another immutable ID refuses deletion.
|
||||
|
||||
@@ -55,9 +55,11 @@ def safe(root, raw):
|
||||
def regular(root, raw, limit=LIMIT):
|
||||
path = safe(root, raw)
|
||||
parts = path.relative_to(root).parts
|
||||
directory = os.open("/", os.O_RDONLY | os.O_DIRECTORY)
|
||||
directory = None
|
||||
try:
|
||||
for part in (*Path(root).parts[1:], *parts[:-1]):
|
||||
# Ancestors need traversal, not a directory-read grant. Anchor reads at the selected root.
|
||||
directory = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
||||
for part in parts[:-1]:
|
||||
child = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=directory)
|
||||
os.close(directory)
|
||||
directory = child
|
||||
@@ -79,7 +81,8 @@ def regular(root, raw, limit=LIMIT):
|
||||
except OSError:
|
||||
raise Refused("environment_file_invalid")
|
||||
finally:
|
||||
os.close(directory)
|
||||
if directory is not None:
|
||||
os.close(directory)
|
||||
|
||||
|
||||
def atomic(root, raw, data):
|
||||
|
||||
@@ -2,6 +2,7 @@ version: "0.2"
|
||||
name: openshell-example
|
||||
pods:
|
||||
- id: team
|
||||
label: Team
|
||||
members:
|
||||
- id: coordinator
|
||||
agent_ref: local:agents/coordinator
|
||||
@@ -13,7 +14,7 @@ pods:
|
||||
runtime: openshell-claude
|
||||
profile: default
|
||||
cwd: .
|
||||
edges:
|
||||
- kind: delegates_to
|
||||
from: team.coordinator
|
||||
to: team.worker
|
||||
edges:
|
||||
- kind: delegates_to
|
||||
from: coordinator
|
||||
to: worker
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openrig/daemon",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.8",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -106,11 +106,12 @@ export class OpenShellDriver implements EnvironmentDriver {
|
||||
if (beforeDispatch && !beforeDispatch()) throw new EnvironmentError("environment_selection_changed", "The selected stop is no longer eligible; no command was dispatched");
|
||||
await this.command([...this.prefix(binding.placement), "stop", binding.placement.name]);
|
||||
}
|
||||
async deleteEnvironment(binding: EnvironmentBinding, _operationId: string): Promise<void> {
|
||||
async deleteEnvironment(binding: EnvironmentBinding, _operationId: string, beforeDispatch?: () => boolean): Promise<void> {
|
||||
const actual = await this.inspect(binding);
|
||||
if (!binding.resource || actual.id !== binding.resource.id || actual.name !== binding.resource.name || actual.workspace !== binding.resource.workspace
|
||||
|| binding.resource.fingerprint && actual.fingerprint !== binding.resource.fingerprint)
|
||||
throw new EnvironmentError("environment_identity_changed", "The selected compute changed before deletion");
|
||||
if (beforeDispatch && !beforeDispatch()) throw new EnvironmentError("environment_selection_changed", "The selected delete is no longer eligible; no command was dispatched");
|
||||
await this.command([...this.prefix(binding.placement), "delete", binding.placement.name]);
|
||||
}
|
||||
connectArgs(binding: EnvironmentBinding): string[] { return [...this.prefix(binding.placement), "connect", binding.placement.name]; }
|
||||
|
||||
@@ -12,7 +12,7 @@ export type EnvironmentLifecycleDriver = EnvironmentDriver & {
|
||||
nativeIdentity?(binding: EnvironmentBinding): Promise<NativeObservation>;
|
||||
stopNative?(binding: EnvironmentBinding, native: NativeObservation, operationId: string, beforeDispatch?: () => boolean): Promise<void>;
|
||||
stopCompute?(binding: EnvironmentBinding, operationId: string, beforeDispatch?: () => boolean): Promise<void>;
|
||||
deleteEnvironment?(binding: EnvironmentBinding, operationId: string): Promise<void>;
|
||||
deleteEnvironment?(binding: EnvironmentBinding, operationId: string, beforeDispatch?: () => boolean): Promise<void>;
|
||||
captureCleanup?(binding: EnvironmentBinding, socket: string): Promise<PodmanCleanupSelection>;
|
||||
cleanupState?(binding: EnvironmentBinding, selection?: PodmanCleanupSelection): Promise<DependentObservation>;
|
||||
checkpointFiles?(binding: EnvironmentBinding, native: NativeObservation): Promise<Array<{ path: string; sha256: string; bytes: number }>>;
|
||||
@@ -22,7 +22,7 @@ export type EnvironmentCheckpoint = {
|
||||
nativeType: "community:openshell-claude"; nativeId: string; predecessorResourceId: string;
|
||||
destinationCwd: string; nativeState: string; artifacts: Array<{ path: string; source: string; sha256: string; bytes: number; taskId: string }>; files: Array<{ path: string; source: string; sha256: string; bytes: number }>;
|
||||
};
|
||||
export type EffectReceipt = { version: 1; operationId: string; bindingId: string; generation: string; resourceId: string;
|
||||
export type EffectReceipt = { version: 1; operationId: string; bindingId: string; generation: string | null; resourceId: string;
|
||||
effect: EnvironmentEffect; state: "confirmed" | "pending" | "unconfirmed"; remaining?: DependentObservation; detail?: string };
|
||||
type Operation = { binding_id: string; kind: string; request_json: string; state: string; receipt_json: string | null; effect_id: string | null };
|
||||
const stable = (v: unknown): string => JSON.stringify(v, (_key, x) => x && typeof x === "object" && !Array.isArray(x)
|
||||
@@ -49,6 +49,14 @@ export class EnvironmentLifecycle {
|
||||
if (binding.generation !== generation) refuse("environment_generation_changed", "The selected execution generation changed");
|
||||
return binding;
|
||||
}
|
||||
private cleanupSelection(id: string, resourceId: unknown): EnvironmentBinding {
|
||||
identifier(resourceId, "resourceId");
|
||||
const binding = this.platform.get(id);
|
||||
if (!binding?.resource) refuse("environment_binding_missing", "A confirmed compute binding is required");
|
||||
if (binding.generation !== null) refuse("environment_generation_changed", "An execution generation now exists; select it explicitly");
|
||||
if (binding.resource.id !== resourceId) refuse("environment_identity_changed", "The selected cleanup resource changed");
|
||||
return binding;
|
||||
}
|
||||
private operation(id: string): Operation | undefined { return this.platform.db.prepare("SELECT * FROM environment_operations WHERE operation_id = ?").get(id) as Operation | undefined; }
|
||||
private intent(binding: EnvironmentBinding, id: string, kind: string, request: unknown): Operation {
|
||||
identifier(id, "operationId");
|
||||
@@ -73,9 +81,11 @@ export class EnvironmentLifecycle {
|
||||
if (!native || native.nativeType !== "community:openshell-claude" || !/^[0-9a-f]{8}-[0-9a-f-]{27}$/.test(native.nativeId)) refuse("environment_native_unconfirmed", "The selected compute has no confirmed exact native identity");
|
||||
return native;
|
||||
}
|
||||
async effect(id: string, input: { operationId: string; generation: string; effect: EnvironmentEffect; engineSocket?: string; requestedBy?: string }, beforeDispatch?: () => boolean): Promise<EffectReceipt> {
|
||||
async effect(id: string, input: { operationId: string; generation: string | null; resourceId?: string; effect: EnvironmentEffect; engineSocket?: string; requestedBy?: string }, beforeDispatch?: () => boolean): Promise<EffectReceipt> {
|
||||
if (!["close-viewer", "stop-native", "stop-compute", "delete-owned"].includes(input.effect)) refuse("environment_effect_unsupported", "Select close-viewer, stop-native, stop-compute or delete-owned", 400);
|
||||
const binding = this.selected(id, input.generation);
|
||||
const select = () => input.effect === "delete-owned" && input.generation === null
|
||||
? this.cleanupSelection(id, input.resourceId) : this.selected(id, identifier(input.generation, "generation"));
|
||||
const binding = select();
|
||||
if (input.effect !== "close-viewer" && binding.ownership !== "owned") refuse("environment_attached_preserved", "Attached native, storage and compute are preserved; close only this consumer's viewer");
|
||||
const request = { ...input, resourceId: binding.resource!.id }, op = this.intent(binding, input.operationId, "effect", request);
|
||||
if (op.receipt_json) return JSON.parse(op.receipt_json);
|
||||
@@ -119,8 +129,9 @@ export class EnvironmentLifecycle {
|
||||
const found = listed.resources.find(row => row.id === binding.resource!.id);
|
||||
if (found) {
|
||||
this.platform.assertResource(binding, found);
|
||||
this.selected(id, input.generation);
|
||||
if (this.claim(input.operationId)) { try { await this.driver.deleteEnvironment(binding, op.effect_id!); } catch { /* observe original effect, never repeat */ } }
|
||||
select();
|
||||
if (!await dispatch(allowed => this.driver.deleteEnvironment!(binding, op.effect_id!, allowed)))
|
||||
return result("pending", { detail: "The selection changed before dispatch; no delete was sent" });
|
||||
listed = await this.driver.list(binding.placement);
|
||||
}
|
||||
if (!listed.complete || listed.resources.some(row => row.id === binding.resource!.id || row.name === binding.resource!.name)) return result("pending", { detail: "Exact provider absence is not confirmed" });
|
||||
|
||||
@@ -127,15 +127,12 @@ async function switchHeld(input: ProviderSwitchInput, relaunch: ClaudeSeatRelaun
|
||||
if (!seat.pane) {
|
||||
return { outcome: "failed_safely", reasons: ["no_bound_pane"], message: `${input.seat} has no bound tmux pane to relaunch it in. Nothing changed.` };
|
||||
}
|
||||
// The relaunch runs the seat's saved effort. A process running with another --effort (launched by hand, or from an
|
||||
// older setting) would come back changed with no one choosing it, so stop and show the difference. A relaunch effort
|
||||
// that is the seat's own explicit choice is the change `rig seat set-effort` asked for, and goes ahead.
|
||||
const effort = await relaunch.effortChange?.(nodeId, seat.pane);
|
||||
if (effort?.running && effort.running !== effort.relaunch && !effort.relaunchIsSeatChoice) {
|
||||
return { outcome: "failed_safely", reasons: ["effort_would_change"],
|
||||
message: `${input.seat} runs with effort ${effort.running}, but relaunching it would use ${effort.relaunch ?? "the runtime default (no --effort)"}. `
|
||||
+ `Nothing changed. To keep ${effort.running}, run rig seat set-effort ${input.seat} ${effort.running}, then switch again.` };
|
||||
}
|
||||
// A switch is how a person recovers a seat from a limit or an outage, so its relaunch keeps a running process's own
|
||||
// --effort (launched by hand, or from an older setting) in place of the seat's saved effort. The seat's explicit
|
||||
// choice (`rig seat set-effort`) is the change a managed relaunch applies, so it goes ahead. A kept effort isn't saved
|
||||
// as the seat's choice.
|
||||
const effort = relaunch.effortChange ? await relaunch.effortChange(nodeId, seat.pane) : undefined;
|
||||
const keepEffort = effort?.running && effort.running !== effort.relaunch && !effort.relaunchIsSeatChoice ? effort.running : undefined;
|
||||
const snapshot = relaunch.snapshot(rigId, nodeId);
|
||||
if (!snapshot.resumes) {
|
||||
return { outcome: "failed_safely", reasons: ["conversation_not_resumable"],
|
||||
@@ -145,7 +142,7 @@ async function switchHeld(input: ProviderSwitchInput, relaunch: ClaudeSeatRelaun
|
||||
touching();
|
||||
relaunch.setTokenRef(nodeId, checked.ref);
|
||||
let relaunched: Awaited<ReturnType<ClaudeSeatRelaunch["relaunchInPane"]>>;
|
||||
try { relaunched = await relaunch.relaunchInPane(rigId, logicalId, snapshot.snapshotId); }
|
||||
try { relaunched = await relaunch.relaunchInPane(rigId, logicalId, snapshot.snapshotId, keepEffort ? { effort: keepEffort } : undefined); }
|
||||
catch (err) { relaunched = { ok: false, effect: "unknown", code: "relaunch_threw", message: err instanceof Error ? err.message : String(err) }; }
|
||||
if (!relaunched.ok && relaunched.effect === "none") {
|
||||
relaunch.setTokenRef(nodeId, before);
|
||||
@@ -153,6 +150,8 @@ async function switchHeld(input: ProviderSwitchInput, relaunch: ClaudeSeatRelaun
|
||||
message: `${input.seat} wasn't relaunched (${relaunched.code}: ${relaunched.message}) and wasn't touched, so it's as it was before the switch, with its token reference as it was.` };
|
||||
}
|
||||
report.loginChanged = true;
|
||||
const effortLine = effortNote(input.seat, effort, keepEffort, relaunched);
|
||||
if (effortLine) report.effort = effortLine;
|
||||
if (!relaunched.ok) {
|
||||
const recoveryCommand = `rig launch ${rigName} ${logicalId} --snapshot-id ${snapshot.snapshotId}`;
|
||||
report.restart = { outcome: relaunched.code, message: relaunched.message, recoveryCommand,
|
||||
@@ -187,3 +186,48 @@ async function switchHeld(input: ProviderSwitchInput, relaunch: ClaudeSeatRelaun
|
||||
? `Relaunched ${input.seat} in its own pane without its own token, resuming its conversation. It uses ${checked.instead}, which Claude Code ranks above its config dir's stored login.`
|
||||
: `Relaunched ${input.seat} in its own pane on its config dir's stored login, resuming its conversation. Work in flight when it stopped didn't carry over.` };
|
||||
}
|
||||
|
||||
/** Which effort a switch's relaunch used, and why, from the selection restore's launch reported, when that isn't simply the
|
||||
* effort already running. Without that report it says so, rather than naming the effort read before the relaunch. When
|
||||
* the relaunch didn't report resuming, it's worded as asked, not done, with the rule the recovery command (a plain
|
||||
* restore) follows; the effort read before the relaunch can be stale by then. Nothing when no reader is wired. */
|
||||
function effortNote(seat: string, effort: Awaited<ReturnType<NonNullable<ClaudeSeatRelaunch["effortChange"]>>> | undefined,
|
||||
keepEffort: string | undefined, relaunched: Awaited<ReturnType<ClaudeSeatRelaunch["relaunchInPane"]>>): string | undefined {
|
||||
if (effort === undefined) return undefined;
|
||||
const running = effort === null ? null : effort.running;
|
||||
const named = (value: string | null) => value ?? "the runtime default (no --effort)";
|
||||
if (!relaunched.ok) {
|
||||
const recovery = "The recovery command (a plain restore) launches the seat's explicit effort choice at that time, else the snapshot's effort";
|
||||
if (keepEffort) {
|
||||
return `the relaunch was asked to keep the running effort ${keepEffort}, but it didn't report resuming. ${recovery}; `
|
||||
+ `to keep ${keepEffort}, run rig seat set-effort ${seat} ${keepEffort} first.`;
|
||||
}
|
||||
if (effort === null) {
|
||||
return `the running effort couldn't be read, and the relaunch didn't report resuming. ${recovery}; rig seat set-effort ${seat} <level> chooses one first.`;
|
||||
}
|
||||
if (effort.running === effort.relaunch && !effort.relaunchIsSeatChoice) return undefined;
|
||||
return `the relaunch didn't report resuming. ${recovery}.`;
|
||||
}
|
||||
const selected = relaunched.effort;
|
||||
if (!selected) {
|
||||
return `the relaunch didn't report which effort it launched; rig seat effort ${seat} shows what launched.`;
|
||||
}
|
||||
if (selected.source === "kept") {
|
||||
return `kept the running effort ${named(selected.effort)}: the seat has no effort choice of its own, so the relaunch kept what it ran with. `
|
||||
+ `Its saved effort, ${named(effort === null ? null : effort.relaunch)}, is unchanged; rig seat set-effort ${seat} <level> chooses one.`;
|
||||
}
|
||||
if (effort === null) {
|
||||
return selected.source === "seat_choice"
|
||||
? `the running effort couldn't be read; the relaunch used the seat's chosen effort ${named(selected.effort)} (rig seat set-effort).`
|
||||
: `the running effort couldn't be read, so the relaunch used the seat's saved effort, ${named(selected.effort)}.`;
|
||||
}
|
||||
if (selected.effort === running) return undefined;
|
||||
if (selected.source === "seat_choice") {
|
||||
return keepEffort
|
||||
? `the seat's chosen effort ${named(selected.effort)}, saved during the switch, in place of the running ${keepEffort} the relaunch was asked to keep.`
|
||||
: `the seat's chosen effort ${named(selected.effort)} (rig seat set-effort), in place of the running ${running ?? "process's runtime default (no --effort flag)"}.`;
|
||||
}
|
||||
return running === null
|
||||
? `the running process had no --effort flag, so the relaunch used the seat's saved effort ${named(selected.effort)}.`
|
||||
: `the relaunch used the seat's saved effort ${named(selected.effort)}, in place of the running ${running}.`;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import type { RestartInPlaceResult } from "../seat-lifecycle-service.js";
|
||||
import type { PeerReport } from "../seat-restart-in-place.js";
|
||||
import type { ClaudeSeatTokenRef } from "../claude-seat-token.js";
|
||||
import type { CredentialPrecedence } from "../claude-credential-precedence.js";
|
||||
import type { LaunchEffortSelection } from "../seat-effort.js";
|
||||
|
||||
export interface ProviderPrecheckInput {
|
||||
seat: string;
|
||||
@@ -69,9 +70,13 @@ export interface ClaudeSeatRelaunch {
|
||||
* whether that one is the seat's own explicit choice (`rig seat set-effort`). null when the running process can't be read. */
|
||||
effortChange?(nodeId: string, pane: string): Promise<{ running: string | null; relaunch: string | null; relaunchIsSeatChoice: boolean } | null>;
|
||||
/** Relaunch only this seat from that snapshot in its own pane: its process ends in place, the pane and its scrollback stay,
|
||||
* and restore resumes the conversation there. `effect: "none"` when it was refused before anything stopped. */
|
||||
relaunchInPane(rigId: string, logicalId: string, snapshotId: string): Promise<{ ok: true; status: "resumed"; warnings: string[] }
|
||||
| { ok: false; effect: "none" | "unknown"; code: string; message: string }>;
|
||||
* and restore resumes the conversation there. `effect: "none"` when it was refused before anything stopped. `effort`:
|
||||
* launch with it in place of the seat's saved effort (the running process's own, kept by a switch), unless the seat
|
||||
* has an explicit choice when restore's launch reads it; it isn't saved as the seat's choice. The result's `effort` is
|
||||
* what that launch selected and why, on every relaunch that reached the selection. */
|
||||
relaunchInPane(rigId: string, logicalId: string, snapshotId: string, opts?: { effort?: string }): Promise<
|
||||
| { ok: true; status: "resumed"; warnings: string[]; effort?: LaunchEffortSelection }
|
||||
| { ok: false; effect: "none" | "unknown"; code: string; message: string; effort?: LaunchEffortSelection }>;
|
||||
}
|
||||
|
||||
/** What would outrank a Claude seat's own token at its next launch (`ClaudeManagedLaunch.credentialPrecedence`). */
|
||||
@@ -98,6 +103,8 @@ export interface SeatSwitchReport {
|
||||
restart?: { outcome: string; message: string; recoveryCommand?: string; remainingStep?: string };
|
||||
/** Claude: what the relaunch reported, such as a token it couldn't apply. */
|
||||
warnings?: string[];
|
||||
/** Claude: which effort the relaunch used, and why, when that isn't simply the effort already running. */
|
||||
effort?: string;
|
||||
/** Claude: false when the relaunch reported starting without the seat's token. True means no such fallback was reported;
|
||||
* the seat's /status after a tool turn is the evidence. */
|
||||
tokenApplied?: boolean;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { leftAttachedReason } from "./restore-plan-preview.js";
|
||||
import { assessSnapshotCurrentOccupants, stoppedSnapshotReason } from "./rehydrate-eligibility.js";
|
||||
import { nonInterruptiveNotice, nonInterruptiveSummary } from "../adapters/non-interruptive.js";
|
||||
import { planModelAliasNotice } from "../adapters/yolo-mode.js";
|
||||
import { effortPrecedence, readSeatEffort } from "./seat-effort.js";
|
||||
import { effortPrecedence, readSeatEffort, type LaunchEffortSelection } from "./seat-effort.js";
|
||||
import { accessSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
import { ensureSeatSkillPlugin, hasSeatSkillRecords, rehydrateSeatSkills, seatSkillPaths, seatSkillPathsForNode, seatCatalogKeptNote, seatSkillReplay, writeSeatSkillCatalog, type SeatSkillRehydration, writeSeatSkillRouter, seatRouterKeptNote } from "./seat-skill-storage.js";
|
||||
@@ -617,12 +617,17 @@ export class RestoreOrchestrator {
|
||||
}
|
||||
|
||||
/** `keepPane`: the seat is running and is relaunched in its own pane (its process ended in place, the pane and scrollback
|
||||
* kept) instead of a new session; nothing checks whether it is live first, since that is the point. */
|
||||
* kept) instead of a new session; nothing checks whether it is live first, since that is the point. `keepEffort`, with
|
||||
* `keepPane`: launch with that effort (the running process's own, kept by a provider switch) in place of the seat's
|
||||
* saved one, unless the seat has an explicit choice when the launch reads it; it isn't saved as the seat's choice.
|
||||
* `onLaunchEffort`, with `keepPane`, hears the effort the launch selected and why, whether or not one was kept. */
|
||||
async launchSingleNode(rigId: string, logicalId: string, opts?: {
|
||||
adapters?: Record<string, import("./runtime-adapter.js").RuntimeAdapter>;
|
||||
fsOps?: { exists(path: string): boolean };
|
||||
snapshotId?: string;
|
||||
keepPane?: boolean;
|
||||
keepEffort?: string;
|
||||
onLaunchEffort?: (selected: LaunchEffortSelection) => void;
|
||||
selectedHistory?: SelectedRestoreHistory;
|
||||
}): Promise<NarrowLaunchResult> {
|
||||
return this.launchNodeTargets(rigId, [logicalId], { ...opts, namedAgent: true, nonTargetMode: "unchanged" });
|
||||
@@ -634,6 +639,8 @@ export class RestoreOrchestrator {
|
||||
holdReason?: string;
|
||||
snapshotId?: string;
|
||||
keepPane?: boolean;
|
||||
keepEffort?: string;
|
||||
onLaunchEffort?: (selected: LaunchEffortSelection) => void;
|
||||
selectedHistory?: SelectedRestoreHistory;
|
||||
namedAgent?: boolean;
|
||||
nonTargetMode: "unchanged" | "detach_and_hold";
|
||||
@@ -689,7 +696,8 @@ export class RestoreOrchestrator {
|
||||
}
|
||||
if (opts.keepPane) {
|
||||
launched.push(await this.restoreNodeWithCompensation({ node }, rigId, snapshot.id, snapshot.data,
|
||||
{ adapters: opts.adapters, fsOps: opts.fsOps, keepPane: true, resumePeriodicCarry }, subsetWarnings));
|
||||
{ adapters: opts.adapters, fsOps: opts.fsOps, keepPane: true, resumePeriodicCarry,
|
||||
...(opts.keepEffort ? { keepEffort: opts.keepEffort } : {}), ...(opts.onLaunchEffort ? { onLaunchEffort: opts.onLaunchEffort } : {}) }, subsetWarnings));
|
||||
continue;
|
||||
}
|
||||
// An external CLI's claim has no tmux session to probe; restoreNodeWithCompensation leaves it attached.
|
||||
@@ -1071,7 +1079,7 @@ export class RestoreOrchestrator {
|
||||
rigId: string,
|
||||
snapshotId: string,
|
||||
data: SnapshotData,
|
||||
opts?: { adapters?: Record<string, import("./runtime-adapter.js").RuntimeAdapter>; fsOps?: { exists(path: string): boolean }; freshLogicalIds?: string[]; onSessionLaunched?: (nodeId: string, sessionId: string) => void; automaticReplacements?: AutomaticSeatReplacement[]; keepPane?: boolean; selectedHistory?: SelectedRestoreHistory; resumePeriodicCarry?: boolean },
|
||||
opts?: { adapters?: Record<string, import("./runtime-adapter.js").RuntimeAdapter>; fsOps?: { exists(path: string): boolean }; freshLogicalIds?: string[]; onSessionLaunched?: (nodeId: string, sessionId: string) => void; automaticReplacements?: AutomaticSeatReplacement[]; keepPane?: boolean; keepEffort?: string; onLaunchEffort?: (selected: LaunchEffortSelection) => void; selectedHistory?: SelectedRestoreHistory; resumePeriodicCarry?: boolean },
|
||||
warnings?: string[],
|
||||
preserveStoppedCurrent = false,
|
||||
): Promise<RestoreNodeResult> {
|
||||
@@ -1590,7 +1598,7 @@ export class RestoreOrchestrator {
|
||||
data: SnapshotData,
|
||||
sessionName: string,
|
||||
launchResult?: { ok: true; sessionName: string; session: import("./types.js").Session; binding: import("./types.js").Binding },
|
||||
opts?: { adapters?: Record<string, import("./runtime-adapter.js").RuntimeAdapter>; fsOps?: { exists(path: string): boolean }; freshLogicalIds?: string[]; selectedHistory?: SelectedRestoreHistory; claudeExecutable?: string; keepPane?: boolean },
|
||||
opts?: { adapters?: Record<string, import("./runtime-adapter.js").RuntimeAdapter>; fsOps?: { exists(path: string): boolean }; freshLogicalIds?: string[]; selectedHistory?: SelectedRestoreHistory; claudeExecutable?: string; keepPane?: boolean; keepEffort?: string; onLaunchEffort?: (selected: LaunchEffortSelection) => void },
|
||||
warnings?: string[],
|
||||
priorState?: { binding: import("./types.js").Binding | null; sessions: { id: string; status: string }[] },
|
||||
seatRehydration?: SeatSkillRehydration,
|
||||
@@ -1611,9 +1619,14 @@ export class RestoreOrchestrator {
|
||||
? "SELECT id FROM nodes WHERE id = ? AND rig_id = ? AND logical_id = ?"
|
||||
: "SELECT id FROM nodes WHERE rig_id = ? AND logical_id = ?").get(...(selected ? [node.id, rigId, node.logicalId] : [rigId, node.logicalId])) as
|
||||
{ id: string } | undefined;
|
||||
const decided = effortPrecedence.restoreLaunchEffort({ snapshot: node, live: readSeatEffort(this.db, liveRow?.id ?? node.id) });
|
||||
const live = readSeatEffort(this.db, liveRow?.id ?? node.id);
|
||||
// A provider switch's relaunch keeps the effort its seat was running with, unless the seat has an explicit choice
|
||||
// by now: read here, at launch, the latest choice wins. A kept effort isn't saved as the seat's choice.
|
||||
const kept = opts?.keepEffort && !live?.choice ? opts.keepEffort : undefined;
|
||||
const decided: { effort: string | null; warning?: string } = kept ? { effort: kept } : effortPrecedence.restoreLaunchEffort({ snapshot: node, live });
|
||||
if (decided.warning) warnings?.push(`Restore: ${node.logicalId}: ${decided.warning}`);
|
||||
restoreEffort = decided;
|
||||
opts?.onLaunchEffort?.({ effort: decided.effort, source: kept !== undefined ? "kept" : live?.choice ? "seat_choice" : "saved" });
|
||||
}
|
||||
return restoreEffort.effort;
|
||||
};
|
||||
|
||||
@@ -30,6 +30,13 @@ export function resolveSeatEffort(node: { effort?: string | null; effortChoice?:
|
||||
|
||||
/** The seat's resolution from its node row. null: no such node. A database older than the
|
||||
* effort_choice column resolves from nodes.effort alone, as launches did before it existed. */
|
||||
/** The effort a launch selected, and why: the effort a provider switch asked it to keep, the seat's explicit choice,
|
||||
* or its saved resolution (the snapshot's authored effort, or the runtime default when `effort` is null). */
|
||||
export interface LaunchEffortSelection {
|
||||
effort: string | null;
|
||||
source: "kept" | "seat_choice" | "saved";
|
||||
}
|
||||
|
||||
export function readSeatEffort(db: Database.Database, nodeId: string): EffortResolution | null {
|
||||
try {
|
||||
const row = db.prepare("SELECT effort, effort_choice FROM nodes WHERE id = ?").get(nodeId) as
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { SessionRegistry } from "../domain/session-registry.js";
|
||||
import { resolveActiveSnapshotSession } from "../domain/active-occupant.js";
|
||||
import { readClaudeSeatTokenRef } from "../domain/claude-seat-token.js";
|
||||
import { processArgv, verifyClaudePaneRuntime, type NativeProcessLister } from "../domain/native-process-lineage.js";
|
||||
import { emittedEffortFromArgv, readSeatEffort } from "../domain/seat-effort.js";
|
||||
import { emittedEffortFromArgv, readSeatEffort, type LaunchEffortSelection } from "../domain/seat-effort.js";
|
||||
import type { ProviderKind } from "../domain/provider/provider-types.js";
|
||||
import { checkMoveReport, credentialRefusal, findCredential, MOVE_REPORT_TAG, readMoveReport, renderMoveReport, type MoveReport } from "../domain/provider/subscription-move-report.js";
|
||||
import { QueueRepositoryError, type QueueRepository } from "../domain/queue-repository.js";
|
||||
@@ -117,19 +117,24 @@ function claudeSeatOf(c: Context): ClaudeSeatRelaunch {
|
||||
const guard = (c.get("tmuxAdapter" as never) as TmuxAdapter | undefined)?.deliveryGuard;
|
||||
return guard ? guard.lifecycle([nodeId], fn) : fn();
|
||||
},
|
||||
relaunchInPane: async (rigId, logicalId, snapshotId) => {
|
||||
relaunchInPane: async (rigId, logicalId, snapshotId, relaunchOpts) => {
|
||||
const restore = c.get("restoreOrchestrator" as never) as RestoreOrchestrator | undefined;
|
||||
if (!restore) return { ok: false, effect: "none", code: "restore_unavailable", message: "This daemon has no restore orchestrator." };
|
||||
const adapters = (c.get("runtimeAdapters" as never) as Record<string, import("../domain/runtime-adapter.js").RuntimeAdapter> | undefined) ?? {};
|
||||
const result = await restore.launchSingleNode(rigId, logicalId, { snapshotId, adapters, fsOps: { exists: (p: string) => fs.existsSync(p) }, keepPane: true });
|
||||
// The effort restore's launch selected, and why, heard on every relaunch.
|
||||
const heard: { effort?: LaunchEffortSelection } = {};
|
||||
const result = await restore.launchSingleNode(rigId, logicalId, { snapshotId, adapters, fsOps: { exists: (p: string) => fs.existsSync(p) }, keepPane: true,
|
||||
onLaunchEffort: (selected: LaunchEffortSelection) => { heard.effort = selected; },
|
||||
...(relaunchOpts?.effort ? { keepEffort: relaunchOpts.effort } : {}) });
|
||||
const effort = heard.effort ? { effort: heard.effort } : {};
|
||||
// A refusal of the whole launch (no usable snapshot, no such seat) comes before anything is touched.
|
||||
if (!result.ok) return { ok: false, effect: "none", code: result.code ?? "launch_failed", message: result.message ?? "The seat wasn't launched from the snapshot." };
|
||||
const node = result.launched?.find((n) => n.logicalId === logicalId);
|
||||
if (node?.status === "resumed") return { ok: true, status: "resumed", warnings: result.warnings ?? [] };
|
||||
if (node?.status === "resumed") return { ok: true, status: "resumed", warnings: result.warnings ?? [], ...effort };
|
||||
// Restore stops for a decision, or leaves an attached CLI alone, before it touches the seat; a launch that failed
|
||||
// before its takeover stopped anything left it as this attempt found it too.
|
||||
const untouched = node?.nothingStopped === true || (!node?.launchStarted && (node?.status === "awaiting-decision" || node?.status === "left_attached"));
|
||||
return { ok: false, effect: untouched ? "none" : "unknown", code: node?.status ?? "not_launched", message: node?.error ?? "The seat wasn't launched from the snapshot." };
|
||||
return { ok: false, effect: untouched ? "none" : "unknown", code: node?.status ?? "not_launched", message: node?.error ?? "The seat wasn't launched from the snapshot.", ...effort };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { createFullTestDb, createTestApp } from "./helpers/test-app.js";
|
||||
|
||||
const cleanup: Array<() => void> = [];
|
||||
afterEach(() => { for (const close of cleanup.splice(0).reverse()) close(); });
|
||||
async function fixture(attached = false, automatic = true) {
|
||||
async function fixture(attached = false, automatic = true, beforeGeneration = false) {
|
||||
const root = realpathSync(mkdtempSync(join(tmpdir(), "environment-lifecycle-"))); cleanup.push(() => rmSync(root, { recursive: true, force: true }));
|
||||
const db = createFullTestDb(); cleanup.push(() => db.close());
|
||||
const resource = { id: "compute-one", name: "worker-one", workspace: "workspace", phase: "Ready", operationId: "create-one" };
|
||||
@@ -40,16 +40,82 @@ async function fixture(attached = false, automatic = true) {
|
||||
const rig = f.rigRepo.createRig("lifecycle"), node = f.rigRepo.addNode(rig.id, "worker", { runtime: "openshell-claude", cwd: root });
|
||||
const request = async (path: string, body?: unknown) => { const res = await f.app.request(`/api/environments${path}`, { method: body ? "POST" : "GET", headers: { "content-type": "application/json", "x-openrig-session": "coordinator@lifecycle" }, ...(body ? { body: JSON.stringify(body) } : {}) }); return { status: res.status, body: await res.json() as Record<string, any> }; };
|
||||
const policy = join(root, "policy.yaml"); writeFileSync(policy, "version: 1\n");
|
||||
const input = { version: 1, bindingId: "binding-one", operationId: "create-one", nodeId: node.id, seat: "worker@lifecycle", baseRuntime: "claude-code", guestRoot: "/sandbox/openrig", guestBaseUrl: "http://guest.test:7433", placement: { gateway: "gateway", workspace: "workspace", name: "worker-one", image: `image@sha256:${"a".repeat(64)}`, provider: "provider", policy } };
|
||||
const input = { version: 1, bindingId: "binding-one", operationId: "create-one", nodeId: beforeGeneration ? undefined : node.id, seat: "worker@lifecycle", baseRuntime: "claude-code", guestRoot: "/sandbox/openrig", guestBaseUrl: "http://guest.test:7433", placement: { gateway: "gateway", workspace: "workspace", name: "worker-one", image: `image@sha256:${"a".repeat(64)}`, provider: "provider", policy } };
|
||||
const created = await request(attached ? "/attach" : "/create", { ...input, ...(attached ? { resourceId: resource.id } : {}) }); expect(created.status, JSON.stringify(created.body)).toBeLessThan(300);
|
||||
if (!beforeGeneration) {
|
||||
db.prepare("UPDATE environment_bindings SET generation='generation-one' WHERE binding_id='binding-one'").run();
|
||||
const receipt = { version: 1, bindingId: "binding-one", nodeId: node.id, resourceId: resource.id, ...native, activityGrantId: "grant-one" };
|
||||
db.prepare("INSERT INTO environment_operations(operation_id,binding_id,kind,request_json,state,receipt_json) VALUES ('launch-generation-one','binding-one','launch',?,'confirmed',?)").run(JSON.stringify(receipt), JSON.stringify(receipt));
|
||||
}
|
||||
const effect = (name: string, operationId = "effect-one") => request("/binding-one/effect", { operationId, generation: "generation-one", effect: name });
|
||||
const lifecycle = new EnvironmentLifecycle(new EnvironmentPlatform(db, driver), queue, join(root, "retained"));
|
||||
return { root, db, driver, queue, lifecycle, request, effect, resource, native, files, history, capture: () => f.snapshotCapture.captureSnapshot(rig.id, "manual"), disappear: () => { present = false; }, counts: () => ({ stopCalls, deleteCalls, creates }), loseStop: () => { loseStop = true; }, unavailable: () => { unavailable = true; }, dirtyCleanup: () => { dependents.volumes = true; } };
|
||||
}
|
||||
describe("owned and attached environment effects", () => {
|
||||
async function failedProjection(attached = false) {
|
||||
const f = await fixture(attached, true, true), platform = new EnvironmentPlatform(f.db, f.driver);
|
||||
vi.spyOn(f.driver, "read").mockRejectedValueOnce(new Error("guest root read denied"));
|
||||
const source = join(f.root, "policy.yaml");
|
||||
await expect(platform.project("binding-one", { operationId: "projection-failed", generation: "not-established", files: [
|
||||
{ path: "work/role.md", source, sha256: sha256(readFileSync(source)), required: true },
|
||||
] })).rejects.toThrow("guest root read denied");
|
||||
expect(platform.get("binding-one")).toMatchObject({ generation: null, resource: { id: f.resource.id } });
|
||||
expect(platform.get("binding-one")!.nodeId).toBeUndefined();
|
||||
return { ...f, cleanup: () => f.request("/binding-one/effect", { operationId: "cleanup-failed-create", effect: "delete-owned", generation: null, resourceId: f.resource.id }) };
|
||||
}
|
||||
it("cleans the exact owned create after projection fails before node and generation binding", async () => {
|
||||
const f = await failedProjection(), result = await f.cleanup();
|
||||
expect(result.status, JSON.stringify(result.body)).toBe(200);
|
||||
expect(result.body).toMatchObject({ state: "confirmed", generation: null, resourceId: "compute-one", effect: "delete-owned" });
|
||||
expect(f.counts()).toMatchObject({ creates: 1, deleteCalls: 1, stopCalls: 0 });
|
||||
expect((await f.cleanup()).body).toEqual(result.body); expect(f.counts().deleteCalls).toBe(1);
|
||||
});
|
||||
it("reconciles uncertain pre-generation cleanup without another provider delete", async () => {
|
||||
const f = await failedProjection();
|
||||
let observe = false;
|
||||
vi.spyOn(f.driver, "deleteEnvironment").mockImplementationOnce(async () => { f.disappear(); throw new Error("lost delete reply"); });
|
||||
const original = f.driver.cleanupState;
|
||||
vi.spyOn(f.driver, "cleanupState").mockImplementation(async () => { if (!observe) throw new Error("engine unavailable"); return original(); });
|
||||
expect((await f.cleanup()).body.state).toBe("unconfirmed");
|
||||
observe = true;
|
||||
expect((await f.cleanup()).body.state).toBe("confirmed");
|
||||
expect(f.driver.deleteEnvironment).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("preserves an attached resource after projection fails before a generation exists", async () => {
|
||||
const f = await failedProjection(true), result = await f.cleanup();
|
||||
expect(result.status).toBe(409); expect(result.body.code).toBe("environment_attached_preserved");
|
||||
expect(f.counts()).toMatchObject({ creates: 0, deleteCalls: 0, stopCalls: 0 });
|
||||
});
|
||||
|
||||
it("requires the exact resource for pre-generation cleanup and never infers a native generation", async () => {
|
||||
const f = await failedProjection();
|
||||
for (const body of [
|
||||
{ effect: "delete-owned", generation: null },
|
||||
{ effect: "delete-owned", generation: null, resourceId: "another-resource" },
|
||||
{ effect: "stop-compute", generation: null, resourceId: f.resource.id },
|
||||
]) {
|
||||
const result = await f.request("/binding-one/effect", { operationId: "bad-selection", ...body });
|
||||
expect(result.status).toBeGreaterThanOrEqual(400);
|
||||
}
|
||||
expect(f.counts()).toMatchObject({ deleteCalls: 0, stopCalls: 0 });
|
||||
});
|
||||
it("refuses generation-free cleanup once a generation exists", async () => {
|
||||
const f = await fixture();
|
||||
const result = await f.request("/binding-one/effect", { operationId: "bad-selection", effect: "delete-owned", generation: null, resourceId: f.resource.id });
|
||||
expect(result.status).toBe(409); expect(result.body.code).toBe("environment_generation_changed");
|
||||
expect(f.counts().deleteCalls).toBe(0);
|
||||
});
|
||||
it("does not dispatch pre-generation deletion if projection establishes a generation during observation", async () => {
|
||||
const f = await failedProjection(), list = f.driver.list;
|
||||
vi.spyOn(f.driver, "list").mockImplementationOnce(async () => {
|
||||
const result = await list();
|
||||
f.db.prepare("UPDATE environment_bindings SET generation='new-generation' WHERE binding_id='binding-one'").run();
|
||||
return result;
|
||||
});
|
||||
const result = await f.cleanup();
|
||||
expect(result.status).toBe(409); expect(result.body.code).toBe("environment_generation_changed");
|
||||
expect(f.counts().deleteCalls).toBe(0);
|
||||
});
|
||||
it("stops only the selected native and reconciles a lost acknowledgement without a second signal", async () => {
|
||||
const f = await fixture(); f.loseStop();
|
||||
const first = await f.effect("stop-native"); expect(first.body.state).toBe("confirmed");
|
||||
|
||||
@@ -738,6 +738,55 @@ describe("RestoreOrchestrator.launchNodeSubset", () => {
|
||||
expect(db.prepare("SELECT status FROM sessions WHERE id = ?").get(live.id)).toEqual({ status: "superseded" });
|
||||
});
|
||||
|
||||
it("launches a kept running effort in place of the saved one, without saving it as the seat's choice", async () => {
|
||||
const { rigId, nodeIds } = seedPodAwareRig();
|
||||
db.prepare("UPDATE nodes SET effort = 'low' WHERE id = ?").run(nodeIds[0]);
|
||||
seedSnapshot(rigId, nodeIds, { [nodeIds[0]!]: emptyStartupContext("claude-code") });
|
||||
liveDriver(rigId, nodeIds[0]!);
|
||||
const adapter = readyAdapter("claude-code");
|
||||
const onLaunchEffort = vi.fn();
|
||||
const result = await orchestrator.launchSingleNode(rigId, "dev.driver", { adapters: { "claude-code": adapter }, keepPane: true, keepEffort: "high", onLaunchEffort });
|
||||
expect(result.launched).toEqual([expect.objectContaining({ logicalId: "dev.driver", status: "fresh-primed" })]);
|
||||
expect(adapter.launchHarness).toHaveBeenCalledWith(expect.objectContaining({ effort: "high" }), expect.anything());
|
||||
expect(onLaunchEffort).toHaveBeenCalledWith({ effort: "high", source: "kept" });
|
||||
expect(db.prepare("SELECT effort, effort_choice FROM nodes WHERE id = ?").get(nodeIds[0])).toEqual({ effort: "low", effort_choice: null });
|
||||
});
|
||||
|
||||
it("an explicit effort choice saved after the switch read the seat, during the relaunch's own awaits, wins over the kept effort, and says so", async () => {
|
||||
const { rigId, nodeIds } = seedPodAwareRig();
|
||||
db.prepare("UPDATE nodes SET effort = 'low' WHERE id = ?").run(nodeIds[0]);
|
||||
seedSnapshot(rigId, nodeIds, { [nodeIds[0]!]: emptyStartupContext("claude-code") });
|
||||
liveDriver(rigId, nodeIds[0]!);
|
||||
// The choice lands while the relaunch is respawning the pane: after the switch read "no choice", before the launch.
|
||||
(tmux as any).respawnPane = vi.fn(async () => {
|
||||
db.prepare("UPDATE nodes SET effort_choice = 'medium' WHERE id = ?").run(nodeIds[0]);
|
||||
return { ok: true as const };
|
||||
});
|
||||
const adapter = readyAdapter("claude-code");
|
||||
const onLaunchEffort = vi.fn();
|
||||
const result = await orchestrator.launchSingleNode(rigId, "dev.driver", { adapters: { "claude-code": adapter }, keepPane: true, keepEffort: "high", onLaunchEffort });
|
||||
expect(result.launched).toEqual([expect.objectContaining({ logicalId: "dev.driver", status: "fresh-primed" })]);
|
||||
expect(adapter.launchHarness).toHaveBeenCalledWith(expect.objectContaining({ effort: "medium" }), expect.anything());
|
||||
expect(onLaunchEffort).toHaveBeenCalledWith({ effort: "medium", source: "seat_choice" });
|
||||
expect(db.prepare("SELECT effort, effort_choice FROM nodes WHERE id = ?").get(nodeIds[0])).toEqual({ effort: "low", effort_choice: "medium" });
|
||||
});
|
||||
|
||||
it("reports the effort a relaunch without a kept effort selected: a newer explicit choice saved during its awaits", async () => {
|
||||
const { rigId, nodeIds } = seedPodAwareRig();
|
||||
db.prepare("UPDATE nodes SET effort = 'low', effort_choice = 'low' WHERE id = ?").run(nodeIds[0]);
|
||||
seedSnapshot(rigId, nodeIds, { [nodeIds[0]!]: emptyStartupContext("claude-code") });
|
||||
liveDriver(rigId, nodeIds[0]!);
|
||||
(tmux as any).respawnPane = vi.fn(async () => {
|
||||
db.prepare("UPDATE nodes SET effort_choice = 'medium' WHERE id = ?").run(nodeIds[0]);
|
||||
return { ok: true as const };
|
||||
});
|
||||
const adapter = readyAdapter("claude-code");
|
||||
const onLaunchEffort = vi.fn();
|
||||
await orchestrator.launchSingleNode(rigId, "dev.driver", { adapters: { "claude-code": adapter }, keepPane: true, onLaunchEffort });
|
||||
expect(adapter.launchHarness).toHaveBeenCalledWith(expect.objectContaining({ effort: "medium" }), expect.anything());
|
||||
expect(onLaunchEffort).toHaveBeenCalledWith({ effort: "medium", source: "seat_choice" });
|
||||
});
|
||||
|
||||
it("a pane that isn't in the seat's session is left alone, and the seat's records are put back", async () => {
|
||||
const { rigId, nodeIds } = seedPodAwareRig();
|
||||
seedSnapshot(rigId, nodeIds, { [nodeIds[0]!]: emptyStartupContext("claude-code") });
|
||||
|
||||
@@ -77,3 +77,15 @@ it("rechecks dispatch permission after the driver's own identity await", async (
|
||||
await entered; eligible = false; release(); await refused;
|
||||
expect(command.mock.calls.some(([args]) => args.includes("stop"))).toBe(false);
|
||||
});
|
||||
|
||||
it("rechecks cleanup selection after delete's identity await", async () => {
|
||||
let eligible = true;
|
||||
const command = vi.fn(async (args: string[]) => {
|
||||
if (args.includes("get")) { eligible = false; return JSON.stringify({ id: "immutable", name: "worker-one", workspace: "workspace", phase: "Ready" }); }
|
||||
return "";
|
||||
});
|
||||
const binding = { placement: { gateway: "gateway", workspace: "workspace", name: "worker-one" }, resource: { id: "immutable", name: "worker-one", workspace: "workspace" } } as EnvironmentBinding;
|
||||
await expect(new OpenShellDriver(command).deleteEnvironment(binding, "cleanup-one", () => eligible))
|
||||
.rejects.toMatchObject({ code: "environment_selection_changed" });
|
||||
expect(command.mock.calls.some(([args]) => args.includes("delete"))).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { RigSpecCodec } from "../src/domain/rigspec-codec.js";
|
||||
import { RigSpecSchema } from "../src/domain/rigspec-schema.js";
|
||||
import { validateRigSpecImport } from "../src/domain/spec-validation-service.js";
|
||||
import { createFullTestDb, createTestApp } from "./helpers/test-app.js";
|
||||
|
||||
const sourceRef = resolve(import.meta.dirname, "../assets/openshell/rig.yaml");
|
||||
const yaml = readFileSync(sourceRef, "utf8");
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
it("the shipped OpenShell example parses and validates with its edge inside the pod", () => {
|
||||
expect(validateRigSpecImport(yaml)).toMatchObject({ valid: true, errors: [] });
|
||||
const spec = RigSpecSchema.normalize(RigSpecCodec.parse(yaml) as Record<string, unknown>);
|
||||
expect(spec.pods[0]!.edges).toContainEqual({ kind: "delegates_to", from: "coordinator", to: "worker" });
|
||||
expect(spec.edges ?? []).toEqual([]);
|
||||
});
|
||||
it("the ordinary up plan classifies the shipped example without launching seats", async () => {
|
||||
const db = createFullTestDb();
|
||||
try {
|
||||
const f = createTestApp(db, { upRouterFsOps: {
|
||||
exists: (path: string) => path === sourceRef,
|
||||
readFile: () => yaml,
|
||||
readHead: (_path: string, bytes: number) => Buffer.from(yaml).subarray(0, bytes),
|
||||
} });
|
||||
const plan = vi.spyOn(f.bootstrapOrchestrator, "bootstrap").mockResolvedValue({ runId: "plan", status: "planned", stages: [], errors: [], warnings: [] } as never);
|
||||
const result = await f.app.request("/api/up", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ sourceRef, plan: true }) });
|
||||
expect(result.status, await result.text()).toBe(200);
|
||||
expect(plan).toHaveBeenCalledWith(expect.objectContaining({ mode: "plan", sourceRef }));
|
||||
expect(f.tmuxAdapter.createSession).not.toHaveBeenCalled();
|
||||
} finally { db.close(); }
|
||||
});
|
||||
@@ -58,3 +58,33 @@ it("the shipped resume latch refuses missing exact history before provider prepa
|
||||
writeFileSync(join(root,"control","launch.pending.json"),JSON.stringify({version:1,bindingId:"binding-one",generation:"generation-two",nativeId:id,resumeToken:id,nativeType:"community:openshell-claude",files:[],checkpoint:{nativeId:id,nativeType:"community:openshell-claude",files:[]}}));
|
||||
expect(await command(root,"--prepare")).toMatchObject({ok:false,code:"environment_checkpoint_invalid"});
|
||||
});
|
||||
|
||||
// Model the measured filesystem grant: the selected root is readable, its ancestors are not.
|
||||
it("reads inside the granted workspace when opening every ancestor is denied", async () => {
|
||||
const root = realpathSync(mkdtempSync(join(tmpdir(), "openshell-granted-root-"))); roots.push(root);
|
||||
mkdirSync(join(root, "work")); const file = join(root, "work", "role.md"); writeFileSync(file, "selected context");
|
||||
const script = `import importlib.util,os,sys
|
||||
from pathlib import Path
|
||||
spec=importlib.util.spec_from_file_location("guest",sys.argv[1]); g=importlib.util.module_from_spec(spec); spec.loader.exec_module(g)
|
||||
original=os.open
|
||||
ancestors={str(p) for p in Path(sys.argv[2]).parents}
|
||||
def granted(path, flags, *args, **kwargs):
|
||||
if str(path) in ancestors: raise PermissionError(13,"ancestor not granted",str(path))
|
||||
return original(path,flags,*args,**kwargs)
|
||||
os.open=granted
|
||||
try:
|
||||
print(g.regular(sys.argv[2],sys.argv[3]).decode())
|
||||
except Exception as error: print(type(error).__name__+":"+str(error))
|
||||
`;
|
||||
const result = await run("python3", ["-c", script, helper, root, file], { timeout: 5000 });
|
||||
expect(result.stdout.trim()).toBe("selected context");
|
||||
});
|
||||
|
||||
it("preserves rejection of a symlink in the selected root's ancestors", async () => {
|
||||
const root = realpathSync(mkdtempSync(join(tmpdir(), "openshell-root-link-"))); roots.push(root);
|
||||
mkdirSync(join(root, "real")); mkdirSync(join(root, "real", "workspace"));
|
||||
writeFileSync(join(root, "real", "workspace", "role.md"), "preserved");
|
||||
symlinkSync(join(root, "real"), join(root, "link"));
|
||||
expect(await command(join(root, "link", "workspace"), "--read", "role.md"))
|
||||
.toMatchObject({ ok: false, code: "environment_root_invalid" });
|
||||
});
|
||||
@@ -196,6 +196,17 @@ describe("provider routes", () => {
|
||||
expect(leased).toEqual([[node.id]]);
|
||||
expect(await lent.relaunchInPane(rig.id, "dev.a", "snap-1")).toEqual({ ok: true, status: "resumed", warnings: ["dev-a@fixture-rig: a launch warning"] });
|
||||
expect(launchSingleNode).toHaveBeenCalledWith(rig.id, "dev.a", expect.objectContaining({ snapshotId: "snap-1", keepPane: true }));
|
||||
expect(launchSingleNode).not.toHaveBeenCalledWith(rig.id, "dev.a", expect.objectContaining({ keepEffort: expect.anything() }));
|
||||
// The launch's selection is heard on every relaunch, with or without a kept effort.
|
||||
expect(launchSingleNode).toHaveBeenLastCalledWith(rig.id, "dev.a", expect.objectContaining({ onLaunchEffort: expect.any(Function) }));
|
||||
launchSingleNode.mockImplementationOnce((async (_rigId: string, _logicalId: string, o: { onLaunchEffort?: (e: { effort: string | null; source: string }) => void }) => {
|
||||
o.onLaunchEffort?.({ effort: "medium", source: "seat_choice" });
|
||||
return launched;
|
||||
}) as never);
|
||||
expect(await lent.relaunchInPane(rig.id, "dev.a", "snap-1", { effort: "high" }))
|
||||
.toMatchObject({ ok: true, status: "resumed", effort: { effort: "medium", source: "seat_choice" } });
|
||||
expect(launchSingleNode).toHaveBeenLastCalledWith(rig.id, "dev.a", expect.objectContaining({ snapshotId: "snap-1", keepPane: true, keepEffort: "high",
|
||||
onLaunchEffort: expect.any(Function) }));
|
||||
expect(tmux.killSession).not.toHaveBeenCalled();
|
||||
// Restore's stops before it touches the seat leave it unchanged; anything else after the launch began is unknown.
|
||||
launched = { ok: true, launched: [{ logicalId: "dev.a", status: "awaiting-decision", error: "decide first" }] };
|
||||
|
||||
@@ -39,6 +39,9 @@ function harness(opts: {
|
||||
relaunch?: Relaunched | (() => Promise<Relaunched>);
|
||||
exclusive?: (fn: () => Promise<unknown>) => Promise<unknown>;
|
||||
effort?: Awaited<ReturnType<NonNullable<ClaudeSeatRelaunch["effortChange"]>>>;
|
||||
// What restore's launch-effort seam selected and why (default: the kept effort when one was asked for, else what the
|
||||
// seat's effort read before the relaunch implies).
|
||||
selected?: { effort: string | null; source: "kept" | "seat_choice" | "saved" } | null;
|
||||
} = {}) {
|
||||
const calls: string[] = [];
|
||||
let ref: ClaudeSeatTokenRef | null = opts.ref ?? null;
|
||||
@@ -59,9 +62,15 @@ function harness(opts: {
|
||||
return run;
|
||||
},
|
||||
...(opts.effort === undefined ? {} : { effortChange: async (nodeId: string, pane: string) => { calls.push(`effort ${nodeId} ${pane}`); return opts.effort!; } }),
|
||||
relaunchInPane: async (rigId, logicalId, snapshotId) => {
|
||||
calls.push(`relaunch ${rigId} ${logicalId} ${snapshotId} with ${ref ? ref.label : "no reference"}`);
|
||||
return typeof opts.relaunch === "function" ? opts.relaunch() : opts.relaunch ?? RESUMED;
|
||||
relaunchInPane: async (rigId, logicalId, snapshotId, o) => {
|
||||
calls.push(`relaunch ${rigId} ${logicalId} ${snapshotId} with ${ref ? ref.label : "no reference"}${o?.effort ? ` keeping effort ${o.effort}` : ""}`);
|
||||
const done = typeof opts.relaunch === "function" ? await opts.relaunch() : opts.relaunch ?? RESUMED;
|
||||
const read = opts.effort;
|
||||
const selected = opts.selected !== undefined ? opts.selected
|
||||
: o?.effort ? { effort: o.effort, source: "kept" as const }
|
||||
: read ? { effort: read.relaunch, source: read.relaunchIsSeatChoice ? "seat_choice" as const : "saved" as const }
|
||||
: read === null ? { effort: null, source: "saved" as const } : null;
|
||||
return done.ok && selected ? { ...done, effort: selected } : done;
|
||||
},
|
||||
};
|
||||
const claudePrecedence = async (nodeId: string, o?: { storedLogin?: boolean }) => {
|
||||
@@ -75,33 +84,120 @@ function harness(opts: {
|
||||
const to = (account: string, file: string | null) => ({ seat: SEAT, toAccount: account, tokenFile: file, forceUnsafe: false });
|
||||
|
||||
describe("rig provider switch --seat moves a Claude seat to its own subscription token on the same conversation, in its own pane", () => {
|
||||
// The relaunch runs the seat's saved effort. A process running with another one would come back changed without anyone
|
||||
// choosing it, so the switch stops before touching the seat and shows the difference, unless the relaunch's effort is the
|
||||
// seat's own explicit choice (`rig seat set-effort`), which a managed relaunch is meant to apply.
|
||||
it("stops before touching a seat whose running --effort its relaunch would drop, and shows the difference", async () => {
|
||||
// A switch is how a person recovers a seat from a limit or an outage, so it keeps what the seat was running with. A
|
||||
// process running with its own --effort keeps it through the relaunch, unless the seat has an explicit effort choice
|
||||
// (`rig seat set-effort`), which the relaunch applies. The kept value is never saved as the seat's choice, and the
|
||||
// result says which effort the relaunch used and why.
|
||||
it("keeps the running --effort through the relaunch when the seat has no effort choice of its own, and says so", async () => {
|
||||
const h = harness({ effort: { running: "high", relaunch: null, relaunchIsSeatChoice: false } });
|
||||
const result = await h.svc.switchAccount(to("work", tokenFile()), h.seatRestart, h.claudeSeat);
|
||||
expect(result).toMatchObject({ outcome: "failed_safely", reasons: ["effort_would_change"] });
|
||||
expect(result.message).toContain("effort high");
|
||||
expect(result.message).toContain("the runtime default");
|
||||
expect(result.message).toContain(`rig seat set-effort ${SEAT} high`);
|
||||
expect(result.outcome).toBe("rebind_in_progress");
|
||||
expect(h.calls).toContain("effort node-a %7");
|
||||
expect(h.calls.some((call) => call.startsWith("setTokenRef") || call.startsWith("snapshot") || call.startsWith("relaunch"))).toBe(false);
|
||||
expect(h.ref()).toBeNull();
|
||||
expect(h.calls).toContain("relaunch rig-1 dev.a snap-1 with work keeping effort high");
|
||||
expect(result.seat?.effort).toContain("kept the running effort high");
|
||||
expect(result.seat?.effort).toContain("the runtime default");
|
||||
expect(h.ref()).toMatchObject({ label: "work" });
|
||||
});
|
||||
|
||||
it("relaunches when the seat's explicit effort choice is what changes, when the running process has no flag, or when it can't be read", async () => {
|
||||
for (const effort of [
|
||||
{ running: "high", relaunch: "low", relaunchIsSeatChoice: true },
|
||||
{ running: null, relaunch: "high", relaunchIsSeatChoice: false },
|
||||
{ running: "high", relaunch: "high", relaunchIsSeatChoice: false },
|
||||
null,
|
||||
]) {
|
||||
const h = harness({ effort });
|
||||
const result = await h.svc.switchAccount(to("work", tokenFile()), h.seatRestart, h.claudeSeat);
|
||||
expect(result.outcome).toBe("rebind_in_progress");
|
||||
expect(h.calls.some((call) => call.startsWith("relaunch"))).toBe(true);
|
||||
}
|
||||
it("says a kept effort was asked for, not done, when the relaunch didn't report resuming, and how the recovery keeps it", async () => {
|
||||
const h = harness({ effort: { running: "high", relaunch: "low", relaunchIsSeatChoice: false },
|
||||
relaunch: { ok: false, effect: "unknown", code: "failed", message: "the seat didn't come back" } });
|
||||
const result = await h.svc.switchAccount(to("work", tokenFile()), h.seatRestart, h.claudeSeat);
|
||||
expect(result.outcome).toBe("unknown");
|
||||
expect(h.calls).toContain("relaunch rig-1 dev.a snap-1 with work keeping effort high");
|
||||
expect(result.seat?.restart?.recoveryCommand).toBe("rig launch rig dev.a --snapshot-id snap-1");
|
||||
expect(result.seat?.effort).not.toContain("kept the running effort");
|
||||
expect(result.seat?.effort).toContain("asked to keep the running effort high");
|
||||
expect(result.seat?.effort).toContain("explicit effort choice at that time, else the snapshot's effort");
|
||||
expect(result.seat?.effort).not.toContain("saved effort low");
|
||||
expect(result.seat?.effort).toContain(`rig seat set-effort ${SEAT} high`);
|
||||
});
|
||||
|
||||
it("reports the seat's chosen effort when a choice saved during the switch won at restore's launch-effort seam", async () => {
|
||||
const h = harness({ effort: { running: "high", relaunch: "low", relaunchIsSeatChoice: false }, selected: { effort: "medium", source: "seat_choice" } });
|
||||
const result = await h.svc.switchAccount(to("work", tokenFile()), h.seatRestart, h.claudeSeat);
|
||||
expect(result.outcome).toBe("rebind_in_progress");
|
||||
expect(h.calls).toContain("relaunch rig-1 dev.a snap-1 with work keeping effort high");
|
||||
expect(result.seat?.effort).not.toContain("kept the running effort");
|
||||
expect(result.seat?.effort).toContain("the seat's chosen effort medium");
|
||||
expect(result.seat?.effort).toContain("high");
|
||||
});
|
||||
|
||||
it("words the chosen and unreadable efforts as intended, not applied, when the relaunch didn't report resuming", async () => {
|
||||
const unknown = { ok: false as const, effect: "unknown" as const, code: "failed", message: "the seat didn't come back" };
|
||||
const chosen = harness({ effort: { running: "high", relaunch: "low", relaunchIsSeatChoice: true }, relaunch: unknown });
|
||||
const c = await chosen.svc.switchAccount(to("work", tokenFile()), chosen.seatRestart, chosen.claudeSeat);
|
||||
expect(c.outcome).toBe("unknown");
|
||||
expect(c.seat?.effort).toContain("didn't report resuming");
|
||||
expect(c.seat?.effort).toContain("explicit effort choice at that time");
|
||||
expect(c.seat?.effort).not.toContain("low");
|
||||
const unread = harness({ effort: null, relaunch: unknown });
|
||||
const u = await unread.svc.switchAccount(to("work", tokenFile()), unread.seatRestart, unread.claudeSeat);
|
||||
expect(u.outcome).toBe("unknown");
|
||||
expect(u.seat?.effort).toContain("couldn't be read");
|
||||
expect(u.seat?.effort).toContain("didn't report resuming");
|
||||
expect(u.seat?.effort).not.toContain("kept");
|
||||
});
|
||||
|
||||
it("keeps a running --effort that differs from the seat's saved, authored one", async () => {
|
||||
const h = harness({ effort: { running: "high", relaunch: "low", relaunchIsSeatChoice: false } });
|
||||
const result = await h.svc.switchAccount(to("work", tokenFile()), h.seatRestart, h.claudeSeat);
|
||||
expect(result.outcome).toBe("rebind_in_progress");
|
||||
expect(h.calls).toContain("relaunch rig-1 dev.a snap-1 with work keeping effort high");
|
||||
expect(result.seat?.effort).toContain("kept the running effort high");
|
||||
expect(result.seat?.effort).toContain("low");
|
||||
});
|
||||
|
||||
it("applies the seat's explicit effort choice, and says so", async () => {
|
||||
const h = harness({ effort: { running: "high", relaunch: "low", relaunchIsSeatChoice: true } });
|
||||
const result = await h.svc.switchAccount(to("work", tokenFile()), h.seatRestart, h.claudeSeat);
|
||||
expect(result.outcome).toBe("rebind_in_progress");
|
||||
expect(h.calls).toContain("relaunch rig-1 dev.a snap-1 with work");
|
||||
expect(result.seat?.effort).toContain("the seat's chosen effort low");
|
||||
expect(result.seat?.effort).not.toContain("kept");
|
||||
});
|
||||
|
||||
it("reports the explicit choice the launch actually used when another choice was saved during the switch", async () => {
|
||||
const h = harness({ effort: { running: "high", relaunch: "low", relaunchIsSeatChoice: true }, selected: { effort: "medium", source: "seat_choice" } });
|
||||
const result = await h.svc.switchAccount(to("work", tokenFile()), h.seatRestart, h.claudeSeat);
|
||||
expect(result.outcome).toBe("rebind_in_progress");
|
||||
expect(h.calls).toContain("relaunch rig-1 dev.a snap-1 with work");
|
||||
expect(result.seat?.effort).toContain("the seat's chosen effort medium");
|
||||
expect(result.seat?.effort).not.toContain("low");
|
||||
});
|
||||
|
||||
it("says which effort launched wasn't reported, rather than naming the effort read before the relaunch", async () => {
|
||||
const h = harness({ effort: { running: "high", relaunch: "low", relaunchIsSeatChoice: true }, selected: null });
|
||||
const result = await h.svc.switchAccount(to("work", tokenFile()), h.seatRestart, h.claudeSeat);
|
||||
expect(result.outcome).toBe("rebind_in_progress");
|
||||
expect(result.seat?.effort).toContain("didn't report which effort it launched");
|
||||
expect(result.seat?.effort).not.toContain("low");
|
||||
});
|
||||
|
||||
it("relaunches with the saved effort when the running effort can't be read, and says it wasn't read, never that it was kept", async () => {
|
||||
const h = harness({ effort: null });
|
||||
const result = await h.svc.switchAccount(to("work", tokenFile()), h.seatRestart, h.claudeSeat);
|
||||
expect(result.outcome).toBe("rebind_in_progress");
|
||||
expect(h.calls).toContain("relaunch rig-1 dev.a snap-1 with work");
|
||||
expect(result.seat?.effort).toContain("couldn't be read");
|
||||
expect(result.seat?.effort).not.toContain("kept");
|
||||
});
|
||||
|
||||
it("relaunches with the saved effort when the running process has no --effort flag, and says so", async () => {
|
||||
const h = harness({ effort: { running: null, relaunch: "high", relaunchIsSeatChoice: false } });
|
||||
const result = await h.svc.switchAccount(to("work", tokenFile()), h.seatRestart, h.claudeSeat);
|
||||
expect(result.outcome).toBe("rebind_in_progress");
|
||||
expect(h.calls).toContain("relaunch rig-1 dev.a snap-1 with work");
|
||||
expect(result.seat?.effort).toContain("no --effort flag");
|
||||
expect(result.seat?.effort).toContain("high");
|
||||
});
|
||||
|
||||
it("says nothing about effort when the relaunch uses the effort already running", async () => {
|
||||
const h = harness({ effort: { running: "high", relaunch: "high", relaunchIsSeatChoice: false } });
|
||||
const result = await h.svc.switchAccount(to("work", tokenFile()), h.seatRestart, h.claudeSeat);
|
||||
expect(result.outcome).toBe("rebind_in_progress");
|
||||
expect(h.calls).toContain("relaunch rig-1 dev.a snap-1 with work");
|
||||
expect(result.seat?.effort).toBeUndefined();
|
||||
});
|
||||
|
||||
it("switches to an API-key reference and back without changing the resumed conversation", async () => {
|
||||
@@ -178,8 +274,11 @@ describe("rig provider switch --seat moves a Claude seat to its own subscription
|
||||
});
|
||||
|
||||
it("a relaunch that ended the seat's process but didn't resume reads unknown, with the snapshot's recovery and the way back, and isn't retried", async () => {
|
||||
const h = harness({ relaunch: { ok: false, effect: "unknown", code: "failed", message: "The pane couldn't be respawned." } });
|
||||
const h = harness({ relaunch: { ok: false, effect: "unknown", code: "failed", message: "The pane couldn't be respawned." },
|
||||
effort: { running: "high", relaunch: "low", relaunchIsSeatChoice: false } });
|
||||
const r = await h.svc.switchAccount(to("work", tokenFile()), h.seatRestart, h.claudeSeat);
|
||||
expect(r.seat?.effort).toContain("asked to keep the running effort high");
|
||||
expect(r.seat?.effort).not.toContain("kept the running effort");
|
||||
expect(r).toMatchObject({ outcome: "unknown", reasons: ["relaunch_failed"], message: expect.stringContaining("may have been ended"), seat: {
|
||||
loginChanged: true, undo: `rig provider switch --seat ${SEAT} --stored-login`,
|
||||
restart: { recoveryCommand: "rig launch rig dev.a --snapshot-id snap-1", remainingStep: expect.stringContaining(`rig seat stop ${SEAT}`) } } });
|
||||
@@ -188,9 +287,12 @@ describe("rig provider switch --seat moves a Claude seat to its own subscription
|
||||
});
|
||||
|
||||
it("a relaunch that throws reads unknown, with the snapshot's recovery, instead of rejecting the switch", async () => {
|
||||
const h = harness({ relaunch: async () => { throw new Error("transport failure after the launch request"); } });
|
||||
const h = harness({ relaunch: async () => { throw new Error("transport failure after the launch request"); },
|
||||
effort: { running: "high", relaunch: "low", relaunchIsSeatChoice: false } });
|
||||
const r = await h.svc.switchAccount(to("work", tokenFile()), h.seatRestart, h.claudeSeat);
|
||||
expect(r).toMatchObject({ outcome: "unknown", reasons: ["relaunch_threw"], seat: { restart: { recoveryCommand: "rig launch rig dev.a --snapshot-id snap-1" } } });
|
||||
expect(r.seat?.effort).toContain("asked to keep the running effort high");
|
||||
expect(r.seat?.effort).toContain(`rig seat set-effort ${SEAT} high`);
|
||||
});
|
||||
|
||||
it("a token the relaunch couldn't apply is reported: the reference is saved, the token didn't take effect, and the warning is carried", async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openrig/ui",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.8",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { isBuiltin } from "node:module";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
import ts from "typescript";
|
||||
|
||||
const root = fileURLToPath(new URL("../", import.meta.url));
|
||||
|
||||
function runtimeImports(text, file) {
|
||||
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
||||
const imports = [];
|
||||
const visit = (node) => {
|
||||
let literal;
|
||||
if (ts.isImportDeclaration(node)) {
|
||||
const clause = node.importClause;
|
||||
if (clause?.isTypeOnly) return;
|
||||
const bindings = clause?.namedBindings;
|
||||
if (!clause?.name && bindings && ts.isNamedImports(bindings)
|
||||
&& bindings.elements.length && bindings.elements.every((e) => e.isTypeOnly)) return;
|
||||
literal = node.moduleSpecifier;
|
||||
} else if (ts.isExportDeclaration(node) && !node.isTypeOnly) {
|
||||
if (node.exportClause && ts.isNamedExports(node.exportClause)
|
||||
&& node.exportClause.elements.every((e) => e.isTypeOnly)) return;
|
||||
literal = node.moduleSpecifier;
|
||||
} else if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword
|
||||
|| (ts.isIdentifier(node.expression) && node.expression.text === "require"))) {
|
||||
literal = node.arguments[0];
|
||||
}
|
||||
if (literal && ts.isStringLiteralLike(literal)) imports.push(literal.text);
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(source);
|
||||
return imports;
|
||||
}
|
||||
|
||||
test("runtime import audit includes lazy loads and re-exports, excludes types and prose", () => {
|
||||
assert.deepEqual(runtimeImports(`
|
||||
import type { A } from "types-only";
|
||||
import { type B } from "inline-types-only";
|
||||
export type { C } from "exported-types";
|
||||
const prose = 'import("not-an-import")';
|
||||
import "side-effect";
|
||||
export { value } from "re-export";
|
||||
const lazy = () => import("lazy");
|
||||
const cjs = require("required");
|
||||
`, "control.ts"), ["side-effect", "re-export", "lazy", "required"]);
|
||||
});
|
||||
|
||||
test("the CLI entry points declare every reachable external runtime dependency", () => {
|
||||
const manifest = JSON.parse(readFileSync(resolve(root, "packages/cli/package.json"), "utf8"));
|
||||
const pending = ["bin-wrapper.ts", "index.ts"].map((f) => resolve(root, "packages/cli/src", f));
|
||||
const visited = new Set();
|
||||
const missing = new Set();
|
||||
while (pending.length) {
|
||||
const file = pending.pop();
|
||||
if (visited.has(file)) continue;
|
||||
visited.add(file);
|
||||
for (const specifier of runtimeImports(readFileSync(file, "utf8"), file)) {
|
||||
if (specifier.startsWith(".")) {
|
||||
pending.push(resolve(dirname(file), specifier.replace(/\.js$/, ".ts")));
|
||||
continue;
|
||||
}
|
||||
if (isBuiltin(specifier)) continue;
|
||||
const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
|
||||
// build-package rewrites these to the daemon files shipped inside the CLI.
|
||||
if (name === "@openrig/daemon") continue;
|
||||
if (!Object.hasOwn(manifest.dependencies, name)) missing.add(`${name} (${file.slice(root.length)})`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual([...missing].sort(), [], "Runtime imports must survive a production-only install");
|
||||
});
|
||||
Reference in new issue
Block a user