@join-pantheon/cli 0.30.0 → 0.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -35
- package/dist/agents.js +41 -17
- package/dist/claude-confine.js +37 -21
- package/dist/cli.js +414 -38
- package/dist/client.js +258 -9
- package/dist/command-state.js +25 -2
- package/dist/config.js +79 -7
- package/dist/continued.js +42 -10
- package/dist/doctor.js +93 -15
- package/dist/file-lock.js +197 -0
- package/dist/home-guard.js +372 -17
- package/dist/identity.js +77 -1
- package/dist/mcp-supervisor.js +5 -2
- package/dist/mcp.js +78 -30
- package/dist/nonces.js +130 -0
- package/dist/notifier.js +35 -0
- package/dist/notify.js +9 -3
- package/dist/pending.js +113 -8
- package/dist/pins.js +161 -0
- package/dist/profile.js +19 -1
- package/dist/protocol.js +40 -1
- package/dist/quickstart.js +94 -13
- package/dist/reads.js +8 -1
- package/dist/relay.js +208 -35
- package/dist/repo-find.js +127 -0
- package/dist/repo-route.js +70 -0
- package/dist/roster-file.js +21 -2
- package/dist/run-failure.js +16 -0
- package/dist/service.js +7 -1
- package/dist/setup-guide.js +70 -29
- package/dist/tray.js +55 -4
- package/dist/untrusted.js +24 -5
- package/dist/upgrade.js +9 -5
- package/dist/verify-act.js +102 -0
- package/dist/worker-fleet.js +30 -9
- package/dist/worker-policy.js +96 -6
- package/dist/worker.js +1059 -93
- package/helper/macos/menubar.swift +245 -105
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,13 +76,13 @@ That's it — one command each sets up the **whole machine**: a relay and space
|
|
|
76
76
|
(them), pantheon wired into whichever agents each of you already has (**user scope** — every project,
|
|
77
77
|
every Conductor window, nothing to commit), the wake-up hook so an agent finishes what a peer is
|
|
78
78
|
waiting for, the **worker** so the machine answers the team even while its human is away (on by
|
|
79
|
-
default when run inside a git repo; `--no-worker` opts out
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
want, which decisions are yours alone (so agents stop and ask you), the
|
|
85
|
-
branch. `pantheon setup` re-runs it any time. An agent setting you up runs `pantheon setup --questions`,
|
|
79
|
+
default when run inside a git repo; `--no-worker` opts out), native decision alerts, and the menu bar. Each command
|
|
80
|
+
ends with a "You're set" summary, then a short guided setup on a terminal: two questions — your
|
|
81
|
+
name and your role — and one screen of defaults to keep or change: your teammates can hand work to
|
|
82
|
+
your machine while you are away, that work runs your tests and commits to its own branch so it
|
|
83
|
+
arrives ready to review, whichever agent you use (Claude Code, Codex, Cursor or Copilot) answers
|
|
84
|
+
them, which alerts you want, which decisions are yours alone (so agents stop and ask you), the
|
|
85
|
+
worker's playbook and its base branch. `pantheon setup` re-runs it any time. An agent setting you up runs `pantheon setup --questions`,
|
|
86
86
|
asks you in chat, and passes your answers back as flags — to `pantheon setup --yes`, or straight to
|
|
87
87
|
`pantheon start` / `pantheon join`, which apply them the moment the team exists — so nobody has to
|
|
88
88
|
open a terminal. Restart your agent once and ask it *"check pantheon_status"*.
|
|
@@ -171,8 +171,32 @@ pantheon profile set --space mobile --human-required pricing # authority that
|
|
|
171
171
|
|
|
172
172
|
The relay checks too: a session whose checkout is not one of the team's repos is refused, and a
|
|
173
173
|
`DELIVERED` must name a commit in one of them, so work from the wrong repo cannot land in a team.
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
Details and the scenario list: `docs/MULTI-SPACE.md`.
|
|
175
|
+
|
|
176
|
+
**Several repos (a frontend, a backend, a mobile app) — set up once.**
|
|
177
|
+
- `pantheon use <team> --here` in the folder that holds them binds every repo inside it, and
|
|
178
|
+
registers each with the team, in one go.
|
|
179
|
+
- `pantheon repo find` (and `join`, on its own) looks for the team's repos on this machine — in the
|
|
180
|
+
usual code folders, in a folder you name, and, if you say yes, in the folders your editors and
|
|
181
|
+
agents have opened. It reads only folder names and git remote addresses; nothing leaves the machine.
|
|
182
|
+
- A clone of one of the team's repos, wherever it lives, is that team's the first time an agent is
|
|
183
|
+
opened in it — no binding needed.
|
|
184
|
+
- The relay knows each repo by a fingerprint and its everyday name (`web`), never its address.
|
|
185
|
+
|
|
186
|
+
**One worker, every repo.** The worker serves every repo and folder bound to its team, one run at a
|
|
187
|
+
time, and runs each request where it belongs: in the repo the request names (`the api repo`,
|
|
188
|
+
`` `mobile` ``, `acme/web`), else where the goal's work was delivered, else the repo the asker sent
|
|
189
|
+
it from, else the most recently active one — with the team's other repos here attached read-only
|
|
190
|
+
(clean checkouts, never your working folder) so it can look across them. A request about a team repo
|
|
191
|
+
this machine does not have runs nothing: the asker is told it waits, and it is never answered from
|
|
192
|
+
another repo's code. `pantheon spaces` and `doctor` say what the worker serves and what is not here.
|
|
193
|
+
|
|
194
|
+
**No git at all (a PM, ops, a founder's docs).** `start` or `join` from a plain folder binds it
|
|
195
|
+
(never your home folder). The worker serves it too: a teammate's question is answered from that
|
|
196
|
+
folder, which the run can read but never change; anything it writes goes to a drafts folder of its
|
|
197
|
+
own, and you are told. Answering from a folder unattended needs Claude Code on that machine (the one
|
|
198
|
+
agent that can be opened a folder to read and held off editing it); with another agent, those requests
|
|
199
|
+
wait for your live session, and the teammate is told.
|
|
176
200
|
|
|
177
201
|
### If something is wrong
|
|
178
202
|
|
|
@@ -347,20 +371,23 @@ domain is a conflict, not a committee — routing blocks until one of them drops
|
|
|
347
371
|
it or delegates. Decide domain ownership out loud before publishing; shared
|
|
348
372
|
decisions belong to whoever answers for them, with delegation covering absence.
|
|
349
373
|
|
|
350
|
-
When a teammate
|
|
374
|
+
When a teammate asks this machine something and no session of yours picks it up, the
|
|
351
375
|
worker spawns a headless agent run to handle it. Verified end to end: an `ASK` from an idle
|
|
352
376
|
peer's machine was answered in **31 seconds with no human involved**, and:
|
|
353
377
|
|
|
354
378
|
- the run happens in a **dedicated git worktree on a `pantheon/<goal>` branch** — your checkout and
|
|
355
379
|
your uncommitted work are never touched, and nothing is merged
|
|
356
|
-
- it runs
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
380
|
+
- in a repo it runs your tests, commits and pushes to its own branch (setup's default; `pantheon setup
|
|
381
|
+
--commands no` keeps runs to edits), so work arrives ready to review; a person on this machine can
|
|
382
|
+
also grant commands later (`pantheon policy grant run-commands`, or answering the worker's own
|
|
383
|
+
request from `pantheon approvals` or the menu bar)
|
|
384
|
+
- teammates the team lets wake workers (the relay's per-member `may_wake`, which the invites you send
|
|
385
|
+
include) can hand this machine work — wake "anyone", the default. To approve each teammate on this
|
|
386
|
+
machine first, choose "only people I approve" (`pantheon setup --wake approved`); then
|
|
387
|
+
`pantheon wake allow <who>` (`wake deny`, `wake list`) keeps that list, on this machine only, and the
|
|
388
|
+
first time an unapproved teammate's request needs this machine the menu bar shows "Let <them> start
|
|
389
|
+
work here? Always / Just this once / No" (or `pantheon approvals`). The same buttons answer the
|
|
390
|
+
worker's request for commands; granting it runs the request that needed them.
|
|
364
391
|
- upgrading keeps what you had said: teammates you admitted with `--may-wake` are carried over; so is
|
|
365
392
|
everyone the team lets wake if you had chosen "anyone" (or never chose, on a machine set up before
|
|
366
393
|
0.30, when that was the default); a joiner who was never asked keeps the team's founder. The worker
|
|
@@ -420,12 +447,21 @@ With `auto` (the default) the order is: the agent a persona pins, then the one y
|
|
|
420
447
|
then PATH order — and a candidate that is not signed in is skipped and named in the run log, so a
|
|
421
448
|
run never fails on an expired session when another agent could have answered. If nothing is
|
|
422
449
|
signed in, the first on PATH runs and the alert says exactly that, with the sign-in command.
|
|
423
|
-
`pantheon doctor` shows the same choice (`worker agent`).
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
450
|
+
`pantheon doctor` shows the same choice (`worker agent`).
|
|
451
|
+
|
|
452
|
+
**Whichever agent you use answers your teammates while you are away** — Claude Code, Codex, Cursor
|
|
453
|
+
or Copilot. Claude Code runs inside its own OS sandbox (macOS and Linux, 2.1.83+), so `auto`
|
|
454
|
+
prefers it when it is installed; Codex, Cursor and Copilot answer too, so a team on any of them is
|
|
455
|
+
never kept waiting. To have Codex, Cursor and Copilot leave requests for you instead,
|
|
456
|
+
`pantheon setup --unsandboxed no` (or `pantheon policy revoke unsandboxed-runs`; a woken run can never
|
|
457
|
+
change it). Kiro answers in your live sessions; for unattended runs the worker uses another installed
|
|
458
|
+
agent. Setup turns commands on for a worker that serves a repo (`pantheon setup --commands no` keeps
|
|
459
|
+
runs to edits). Each backend is driven the way it actually works, with the same commands rule
|
|
460
|
+
spelled in its own dialect:
|
|
461
|
+
|
|
462
|
+
| backend | bash gated (`--commands no`) | with commands (setup's default in a repo, `run-commands` or `--allow-bash`) |
|
|
427
463
|
|---|---|---|
|
|
428
|
-
| claude-code | `acceptEdits`, no shell, reads held to the worktree, `~/.ssh` `~/.aws` `~/.config/gh` `~/.pantheon` and similar denied; the repo's own `.claude` settings never load (`--setting-sources user`) | the same file rules, plus Claude Code's OS sandbox (macOS/Linux): commands may write only the worktree, its git data and temp dirs, may not read the secret paths, and a run whose sandbox cannot start does not start. Commands: git subcommands (with its run-a-program options denied), the repo's scripts via npm/pnpm/yarn/bun, make, cargo, go, pytest, mvn/gradle, dotnet, plus the committed `.pantheon/commands` on the base branch (one per line; shells, wrappers, package runners like `npx`, and inline code are refused — use `./node_modules/.bin/vitest`). The run names any command it needed that was refused. On Windows, or a Claude Code
|
|
464
|
+
| claude-code | `acceptEdits`, no shell, reads held to the worktree, `~/.ssh` `~/.aws` `~/.config/gh` `~/.pantheon` and similar denied; the repo's own `.claude` settings never load (`--setting-sources user`) | the same file rules, plus Claude Code's OS sandbox (macOS/Linux): commands may write only the worktree, its git data and temp dirs, may not read the secret paths, and a run whose sandbox cannot start does not start. Commands: git subcommands (with its run-a-program options denied), the repo's scripts via npm/pnpm/yarn/bun, make, cargo, go, pytest, mvn/gradle, dotnet, plus the committed `.pantheon/commands` on the base branch (one per line; shells, wrappers, package runners like `npx`, and inline code are refused — use `./node_modules/.bin/vitest`). The run names any command it needed that was refused. On Windows, or a Claude Code older than 2.1.83, commands run from that same list without the OS sandbox (`pantheon setup --unsandboxed no` keeps those runs to edits) |
|
|
429
465
|
| codex | `--sandbox read-only` (codex has no edits-without-shell mode; reads are not restricted) | `--approve-for-me` (Codex's workspace-write sandbox, its auto-reviewer approves; any command) |
|
|
430
466
|
| cursor | proposes edits, never applies (`-p` without `--force`) | `--force` (any command, no allowlist, no sandbox) |
|
|
431
467
|
| copilot | `--allow-tool=pantheon-worker --allow-tool=write` (edits without shell, Copilot's `write` kind) | `--allow-all-tools` (any command, no allowlist, no sandbox) |
|
|
@@ -434,14 +470,17 @@ actually works, with the same "no shell unless you said so" rule spelled in its
|
|
|
434
470
|
**What the command grant really means.** A run allowed to run commands can edit the repo's own
|
|
435
471
|
scripts and then run them through a listed command (`npm test` runs whatever `package.json` says), so
|
|
436
472
|
the command list alone never contains it. Where Claude Code's sandbox runs (macOS, Linux) that
|
|
437
|
-
sandbox is the boundary. Everywhere else, a command run can
|
|
473
|
+
sandbox is the boundary. Everywhere else, a command run can read what you can — and, outside
|
|
474
|
+
Codex's own write sandbox, change it. After every run
|
|
438
475
|
Pantheon checks for the plain ways it could have widened what the NEXT run may do — a grant or wake
|
|
439
476
|
approval it added, a loosened wake setting, a changed relay address, a queued menu-bar action —
|
|
440
477
|
takes them back and tells you. That is a check after the fact, not a boundary: a run that can do
|
|
441
478
|
what you can could change something it does not look at. Pantheon also refuses to send an act that
|
|
442
479
|
holds this machine's key, a team token, or text in the shape of a common secret (a private key, a
|
|
443
480
|
cloud or API token) — a guard against a pasted secret, not against one a run takes care to disguise.
|
|
444
|
-
|
|
481
|
+
With Claude Code, what keeps your secrets from a run is that it cannot read them: the denies and the
|
|
482
|
+
sandbox above. With other agents, grant commands only where you would be comfortable with the run
|
|
483
|
+
having your access.
|
|
445
484
|
|
|
446
485
|
Claude Code, Cursor and Codex are verified with **real live woken runs** — an idle machine answering a
|
|
447
486
|
peer's ASK end to end. Codex quirks the tooling handles for
|
|
@@ -571,7 +610,8 @@ The rules that make this safe to commit into a shared repo:
|
|
|
571
610
|
|
|
572
611
|
## Guardrails
|
|
573
612
|
|
|
574
|
-
All enforced in the relay,
|
|
613
|
+
All enforced in code — by the relay, or by each machine where the relay is not to be trusted —
|
|
614
|
+
never in a prompt: a prompt can be argued out of its instructions.
|
|
575
615
|
|
|
576
616
|
| Guard | Default | Why |
|
|
577
617
|
|---|---|---|
|
|
@@ -587,9 +627,10 @@ All enforced in the relay, not in a prompt — a prompt can be argued out of its
|
|
|
587
627
|
| Membership | explicit allowlist, ed25519 | the token is not a credential; a name needs its key |
|
|
588
628
|
| Invites | single-use, expiring | a leaked link is inert once redeemed or timed out |
|
|
589
629
|
| Claims | one session per obligation | parallel Conductor workspaces cannot duplicate work |
|
|
590
|
-
| Wake rights | `pantheon wake allow`, per machine; `--may-wake`, per member for the team |
|
|
630
|
+
| Wake rights | `pantheon wake allow`, per machine; `--may-wake`, per member for the team | under "anyone" (the default): every peer the team lets wake; under "approved": only those you also approved here. Checked by the worker before a run starts, and by the relay at its claim |
|
|
591
631
|
| Run budget | 12/hour, 1 concurrent | a peer cannot drain your tokens |
|
|
592
632
|
| Personas | tighten-only | a persona file — even one a teammate committed — can lower caps and turn bash off, never the reverse |
|
|
633
|
+
| Signed acts | every act and goal, ed25519, since 0.31 | checked on each receiving machine against the key it pinned for the sender: the relay cannot forge, alter, re-file or replay a teammate's request without that machine noticing, and a worker runs only requests that check out. A teammate's changed key is held back until you confirm it (`pantheon trust`) |
|
|
593
634
|
| Worktree hygiene | no secrets on disk | credentials reach a woken run through its environment only; run scratch files are git-excluded |
|
|
594
635
|
| Verifiable delivery | branch + commit required | "I wrote the file" is not checkable from another machine |
|
|
595
636
|
| Durable state | goals + obligations on disk | a relay restart used to discard every open goal |
|
|
@@ -619,8 +660,9 @@ pantheon mcp the stdio MCP server — your agent runs this
|
|
|
619
660
|
```
|
|
620
661
|
|
|
621
662
|
`pantheon help --all` lists everything. `pantheon watch` is how a human stays oriented without
|
|
622
|
-
being in the loop; every act is also appended to the relay's ledger, so
|
|
623
|
-
replayable
|
|
663
|
+
being in the loop; every act is also appended to the relay's ledger, so a goal's history is
|
|
664
|
+
replayable while it is open — closed goals are compacted once the space needs room, so export what
|
|
665
|
+
you need to keep with `pantheon audit --json`.
|
|
624
666
|
|
|
625
667
|
## What this still is not
|
|
626
668
|
|
|
@@ -631,18 +673,55 @@ replayable.
|
|
|
631
673
|
the next real gap: `DELIVERED` is now verifiable, but *conformance to the contract* is not.
|
|
632
674
|
- **Nothing fetches the delivered commit for you.** The relay requires a ref and the receiving
|
|
633
675
|
delegate is told to check it, but there is no `pantheon fetch` helper yet.
|
|
634
|
-
- **The space token
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
676
|
+
- **The space token still reads a team's history until someone is removed.** Joining takes an
|
|
677
|
+
admitted key, but in a team that has never removed anyone the token alone can read its history
|
|
678
|
+
over HTTP (older clients need that); after any removal, reads need an admitted key. Share invite
|
|
679
|
+
links, not the token.
|
|
680
|
+
- **Commands on a woken run are on by default in a repo.** A run works in its own worktree and
|
|
681
|
+
branch, inside your agent's sandbox where it has one (see above). `pantheon setup --commands no`
|
|
682
|
+
keeps runs to edits, and `pantheon setup --wake approved` lets you approve each teammate first.
|
|
683
|
+
|
|
684
|
+
## Is Pantheon safe for my team?
|
|
685
|
+
|
|
686
|
+
Short answer: your teammates' agents can hand your machine work while you are away — it runs in its
|
|
687
|
+
own copy of the repo and is delivered on a branch for your review — and whoever runs the relay
|
|
688
|
+
cannot fake or alter a teammate's request. What that means in practice:
|
|
689
|
+
|
|
690
|
+
- **Pantheon never uploads your code, credentials or repo.** The relay passes messages between
|
|
691
|
+
agents and has no filesystem or shell access to anyone's computer; what leaves your machine is
|
|
692
|
+
what an agent writes into a message.
|
|
693
|
+
- **The relay can read the messages** (they are not end-to-end encrypted), so treat a team space
|
|
694
|
+
like a shared team channel. It **cannot write one in a teammate's name or change one**: with 0.31
|
|
695
|
+
on the relay and on your machine, every message is signed by its sender and checked against the
|
|
696
|
+
key your machine keeps for them, and one that does not check out is marked NOT VERIFIED and never
|
|
697
|
+
starts work. (The first time your machine meets a teammate it takes their key from the member
|
|
698
|
+
list; a changed key later is held back until you confirm it.) It can still delay or withhold messages.
|
|
699
|
+
- **Your teammates can hand your machine work while you are away.** `pantheon start` in a repo turns
|
|
700
|
+
the worker on (`--no-worker` leaves it off). Each run works in its own copy of the repo, runs your
|
|
701
|
+
tests, and delivers on its own branch for your review; runs are capped at 12 an hour, and
|
|
702
|
+
`pantheon panic` pauses all of it at once. `pantheon wake list` shows who can hand you work, and
|
|
703
|
+
`pantheon setup --wake approved` lets you approve each teammate first.
|
|
704
|
+
- **You can see and change all of it:** `pantheon policy` (what the worker may do, including whether
|
|
705
|
+
Pantheon updates itself), `pantheon trust` (the teammates' keys this machine checks messages
|
|
706
|
+
against), `pantheon audit`, `pantheon doctor`.
|
|
707
|
+
|
|
708
|
+
For a team of five, the defaults are the setup: everyone's agent answers the team while they are
|
|
709
|
+
away, work arrives tested on its own branch, and decisions you reserved still come to you. Prefer to
|
|
710
|
+
review every change by hand on one machine? `pantheon setup --commands no` there. When Pantheon says
|
|
711
|
+
a teammate's key changed, check it with them before `pantheon trust`. Claude Code runs inside its
|
|
712
|
+
OS sandbox; Codex answers inside its own workspace sandbox, and Cursor and Copilot run commands
|
|
713
|
+
without asking, as you (`pantheon setup --unsandboxed no` has them leave requests for you). The
|
|
714
|
+
full model, and how to report a problem: [joinpantheon.network/security](https://www.joinpantheon.network/security).
|
|
638
715
|
|
|
639
716
|
## Privacy, data, and support
|
|
640
717
|
|
|
641
718
|
The hosted relay at `relay.joinpantheon.network` holds only what coordination needs: your
|
|
642
719
|
verified email, username, and device public keys; the IPs and timestamps of your sessions and
|
|
643
720
|
an audit trail of identity and membership events; and, per space, the acts your delegates
|
|
644
|
-
exchanged plus open goals and obligations.
|
|
645
|
-
|
|
721
|
+
exchanged plus open goals and obligations. Pantheon never uploads your code, credentials or repo,
|
|
722
|
+
and the relay has no filesystem or shell access by design: what leaves your machine is what an
|
|
723
|
+
agent writes into a message (and Pantheon refuses to send its own keys, team tokens, or text in the
|
|
724
|
+
shape of a common secret).
|
|
646
725
|
Whoever operates the relay can read those acts — the operator dashboard shows each space's
|
|
647
726
|
message history — so treat a space like a shared team channel, not a private notebook.
|
|
648
727
|
|
package/dist/agents.js
CHANGED
|
@@ -6,7 +6,7 @@ import { join, dirname, isAbsolute } from "node:path";
|
|
|
6
6
|
import { launcher, kiroDir } from "./install.js";
|
|
7
7
|
import { agentSpawn, which } from "./which.js";
|
|
8
8
|
import { PANTHEON_TOOL_NAMES, PANTHEON_TOOLS } from "./tools.js";
|
|
9
|
-
import { claudeRunSettings, NO_CLAUDE_FEATURES, runTmpDir, sandboxCacheEnv } from "./claude-confine.js";
|
|
9
|
+
import { claudeRunSettings, NO_CLAUDE_FEATURES, runTmpDir, sandboxCacheEnv, versionAtLeast } from "./claude-confine.js";
|
|
10
10
|
export function launchArgs(agent, prompt, name) {
|
|
11
11
|
if (agent === "claude-code")
|
|
12
12
|
return ["--name", name, prompt];
|
|
@@ -125,7 +125,16 @@ export const SIGN_IN_CHECK = {
|
|
|
125
125
|
};
|
|
126
126
|
export const NEEDS_SIGN_IN_BEFORE_SPAWN = new Set(["kiro"]);
|
|
127
127
|
export const NOT_FOR_UNATTENDED_RUNS = new Set(["kiro"]);
|
|
128
|
-
export const KIRO_UNATTENDED_REFUSAL = "Kiro
|
|
128
|
+
export const KIRO_UNATTENDED_REFUSAL = "Kiro answers in your live sessions; for runs while you are away the worker uses another installed agent (Claude Code, Codex, Cursor or Copilot)";
|
|
129
|
+
export const SANDBOXED_FOR_UNATTENDED = new Set(["claude-code"]);
|
|
130
|
+
export const unsandboxedRefusal = (label) => `${label} is set to leave teammates' requests for you on this machine, so this one waits for your next session. ` +
|
|
131
|
+
`To have ${label} answer them while you're away: pantheon policy grant unsandboxed-runs`;
|
|
132
|
+
export function agentsNeedingUnsandboxed(available = detectAvailable()) {
|
|
133
|
+
if (available.some((a) => SANDBOXED_FOR_UNATTENDED.has(a.backend.id)))
|
|
134
|
+
return [];
|
|
135
|
+
return [...new Set(available.filter((a) => !NOT_FOR_UNATTENDED_RUNS.has(a.backend.id)).map((a) => a.backend.label))];
|
|
136
|
+
}
|
|
137
|
+
export const claudeSandboxable = (f, os = process.platform) => os !== "win32" && !!f.settings && versionAtLeast(f.version, "2.1.83");
|
|
129
138
|
export function kiroSignedIn(binary, run = runStatus, env = process.env, home = homedir(), os = platform()) {
|
|
130
139
|
if (env.KIRO_API_KEY)
|
|
131
140
|
return { ok: true, detail: "signed in (KIRO_API_KEY in the environment)" };
|
|
@@ -221,6 +230,14 @@ export function chooseAgent(preferred, opts = {}) {
|
|
|
221
230
|
skipped.push({ backend: c.backend, why: KIRO_UNATTENDED_REFUSAL });
|
|
222
231
|
if (usable.length)
|
|
223
232
|
order.splice(0, order.length, ...usable);
|
|
233
|
+
if (!opts.unsandboxedOk) {
|
|
234
|
+
const boxed = order.filter((c) => SANDBOXED_FOR_UNATTENDED.has(c.backend.id));
|
|
235
|
+
for (const c of order)
|
|
236
|
+
if (!SANDBOXED_FOR_UNATTENDED.has(c.backend.id) && boxed.length)
|
|
237
|
+
skipped.push({ backend: c.backend, why: unsandboxedRefusal(c.backend.label) });
|
|
238
|
+
if (boxed.length)
|
|
239
|
+
order.splice(0, order.length, ...boxed);
|
|
240
|
+
}
|
|
224
241
|
for (const c of order) {
|
|
225
242
|
const s = state(c.backend, c.binary);
|
|
226
243
|
if (!s || s.ok || s.unknown)
|
|
@@ -230,9 +247,15 @@ export function chooseAgent(preferred, opts = {}) {
|
|
|
230
247
|
const first = order.find((o) => !NEEDS_SIGN_IN_BEFORE_SPAWN.has(o.backend.id)) ?? order[0];
|
|
231
248
|
return { backend: first.backend, binary: first.binary, signIn: state(first.backend, first.binary), because: `${first.because} (none signed in)`, skipped };
|
|
232
249
|
}
|
|
233
|
-
export function unattendedRefusal(c) {
|
|
250
|
+
export function unattendedRefusal(c, o = {}) {
|
|
234
251
|
if (NOT_FOR_UNATTENDED_RUNS.has(c.backend.id))
|
|
235
252
|
return KIRO_UNATTENDED_REFUSAL;
|
|
253
|
+
if (!SANDBOXED_FOR_UNATTENDED.has(c.backend.id) && !o.unsandboxedOk) {
|
|
254
|
+
return c.because === "pinned"
|
|
255
|
+
? `woken runs here are pinned to ${c.backend.label} (a persona's agent: line, or the worker's --agent), which is set to leave teammates' requests for you, so this one waits for your next session. ` +
|
|
256
|
+
`To have ${c.backend.label} answer them: pantheon policy grant unsandboxed-runs — or unpin it so Claude Code takes them`
|
|
257
|
+
: unsandboxedRefusal(c.backend.label);
|
|
258
|
+
}
|
|
236
259
|
if (!NEEDS_SIGN_IN_BEFORE_SPAWN.has(c.backend.id) || c.signIn?.ok)
|
|
237
260
|
return null;
|
|
238
261
|
return `${c.backend.label} is not signed in (${c.signIn?.detail ?? "not checked"}), and signed out it opens a browser to log in, so it is not started while you are away — ${SIGN_IN_CHECK[c.backend.id].fix}`;
|
|
@@ -244,7 +267,8 @@ export function describeChoice(c) {
|
|
|
244
267
|
export function skippedSummary(c) {
|
|
245
268
|
if (!c.skipped.length)
|
|
246
269
|
return "";
|
|
247
|
-
const why = [...new Set(c.skipped.map((s) => (s.why === KIRO_UNATTENDED_REFUSAL ? `${s.backend.label} cannot run unattended yet`
|
|
270
|
+
const why = [...new Set(c.skipped.map((s) => (s.why === KIRO_UNATTENDED_REFUSAL ? `${s.backend.label} cannot run unattended yet`
|
|
271
|
+
: s.why === unsandboxedRefusal(s.backend.label) ? `${s.backend.label} has no sandbox` : "not signed in")))];
|
|
248
272
|
return `${c.skipped.length} skipped (${why.join("; ")})`;
|
|
249
273
|
}
|
|
250
274
|
export function resolveAgent(preferred) {
|
|
@@ -370,19 +394,15 @@ export function decodeCommandsFile(b) {
|
|
|
370
394
|
return text.includes("\0") ? null : text;
|
|
371
395
|
}
|
|
372
396
|
export const REPO_RULES_ARGV_MAX = 3_000;
|
|
373
|
-
export const COMMAND_GRANT_SCOPE = "a
|
|
374
|
-
"
|
|
375
|
-
"
|
|
376
|
-
"
|
|
377
|
-
"
|
|
378
|
-
"
|
|
379
|
-
"
|
|
380
|
-
"
|
|
381
|
-
"
|
|
382
|
-
"On a Claude Code older than 2.1.83 (it cannot refuse to run unsandboxed), or on Windows, it has only the list — no boundary; if Pantheon cannot tell what the installed Claude Code supports, that run gets no commands. " +
|
|
383
|
-
"Codex: any command, inside Codex's own workspace-write sandbox, approved by Codex's auto-reviewer. " +
|
|
384
|
-
"Cursor, Copilot and Kiro: any command, as you, with no sandbox — they take no allowlist from Pantheon, so such a run can read and change anything you can, " +
|
|
385
|
-
"Pantheon's own files included (a changed policy or config file is put back after the run, and you are told).";
|
|
397
|
+
export const COMMAND_GRANT_SCOPE = "a run with commands works in its own worktree on a pantheon/* branch: it runs your tests and builds, commits, pushes its own branch for your review, and never merges. " +
|
|
398
|
+
"Claude Code (macOS and Linux) runs those commands inside its OS sandbox: they write only the run's worktree, its git data and the temp dirs, stay clear of your keys and credentials " +
|
|
399
|
+
"(~/.ssh, ~/.aws, ~/.config/gh, ~/.gnupg, ~/.netrc, ~/.docker/config.json, ~/.kube, ~/.git-credentials, ~/.pantheon), and use git, the repo's scripts through npm/pnpm/yarn/bun, " +
|
|
400
|
+
"make, cargo, go, pytest, mvn/gradle and dotnet, plus whatever the committed .pantheon/commands on the base branch lists. " +
|
|
401
|
+
"Codex runs them in its own workspace-write sandbox, approved by its auto-reviewer. " +
|
|
402
|
+
"Cursor and Copilot run any command without asking, as you and with no sandbox; Claude Code on Windows or before 2.1.83 runs its listed commands the same way, without its sandbox. " +
|
|
403
|
+
"After every run Pantheon checks its own settings and puts back any change a run made to them; " +
|
|
404
|
+
"pantheon setup --unsandboxed no leaves those requests for you instead. Kiro answers in your live sessions. " +
|
|
405
|
+
"Turn commands off any time: pantheon policy revoke run-commands.";
|
|
386
406
|
export function headlessSpawn(opts) {
|
|
387
407
|
const l = launcher();
|
|
388
408
|
const identity = {
|
|
@@ -428,6 +448,7 @@ export function headlessSpawn(opts) {
|
|
|
428
448
|
const runTmp = runTmpDir(opts.runHome);
|
|
429
449
|
const c = claudeRunSettings({
|
|
430
450
|
allowBash: opts.allowBash, worktree: opts.worktree, runHome: opts.runHome, pantheonHome: opts.claude.pantheonHome,
|
|
451
|
+
...(opts.readOnlyDirs?.length ? { readOnlyDirs: opts.readOnlyDirs } : {}),
|
|
431
452
|
runTmp, bashRules, version: f.version, ...(opts.claude.home ? { home: opts.claude.home } : {}),
|
|
432
453
|
...(opts.os ? { os: opts.os } : {}), ...(opts.claude.userSettings ? { userSettings: opts.claude.userSettings } : {}),
|
|
433
454
|
});
|
|
@@ -435,6 +456,9 @@ export function headlessSpawn(opts) {
|
|
|
435
456
|
extra.push("--permission-prompts", "none");
|
|
436
457
|
if (f.settingSources)
|
|
437
458
|
extra.push("--setting-sources", "user");
|
|
459
|
+
if (f.addDir && f.settings)
|
|
460
|
+
for (const d of c.readOnly)
|
|
461
|
+
extra.push("--add-dir", d);
|
|
438
462
|
if (f.settings) {
|
|
439
463
|
const file = join(opts.runHome, "claude-settings.json");
|
|
440
464
|
writeFiles.push({ path: file, contents: JSON.stringify(c.settings, null, 2) });
|
package/dist/claude-confine.js
CHANGED
|
@@ -12,6 +12,7 @@ export function parseClaudeHelp(help, version = null) {
|
|
|
12
12
|
settings: /(^|\s)--settings\s/m.test(help),
|
|
13
13
|
settingSources: /(^|\s)--setting-sources\b/m.test(help),
|
|
14
14
|
disallowedTools: /(^|\s)--disallowed-?tools\b/im.test(help),
|
|
15
|
+
addDir: /(^|\s)--add-dir\b/m.test(help),
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
export function versionAtLeast(a, b) {
|
|
@@ -125,14 +126,15 @@ function userExcludedCommands(file) {
|
|
|
125
126
|
return [];
|
|
126
127
|
}
|
|
127
128
|
}
|
|
129
|
+
const SECRET_FILE_GLOBS = [".env", ".env.*", "*.pem", "*.key", "*.p12", "*.pfx", "id_rsa*", "id_ed25519*", ".npmrc", ".netrc", "credentials*", "*.keystore", "service-account*.json"];
|
|
128
130
|
const PANTHEON_HOME_ENTRIES = ["config.json", "config.json.bak", "config.json.lock", "worker-policy.json", "profile.json", "continued.json", "bin", "ops", "run-homes", "logs", "mcp", "data"];
|
|
129
|
-
function aroundWorktree(top,
|
|
131
|
+
function aroundWorktree(top, keeps, named) {
|
|
132
|
+
const keep = (typeof keeps === "string" ? [keeps] : [...keeps]).map((k) => resolve(k));
|
|
130
133
|
const out = [];
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
break;
|
|
134
|
+
const walk = (level, isTop) => {
|
|
135
|
+
if (keep.includes(resolve(level)))
|
|
136
|
+
return;
|
|
137
|
+
const nexts = new Set(keep.map((k) => relative(level, k).split(sep)[0]).filter((n) => !!n && n !== ".." && !isAbsolute(n)));
|
|
136
138
|
let names = [];
|
|
137
139
|
try {
|
|
138
140
|
names = readdirSync(level);
|
|
@@ -144,11 +146,13 @@ function aroundWorktree(top, worktree, named) {
|
|
|
144
146
|
catch {
|
|
145
147
|
return !n.includes(".");
|
|
146
148
|
} };
|
|
147
|
-
for (const n of new Set([...names, ...(
|
|
148
|
-
if (n
|
|
149
|
+
for (const n of new Set([...names, ...(isTop ? named : [])]))
|
|
150
|
+
if (!nexts.has(n))
|
|
149
151
|
out.push({ abs: join(level, n), dir: isDir(n) });
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
for (const n of nexts)
|
|
153
|
+
walk(join(level, n), false);
|
|
154
|
+
};
|
|
155
|
+
walk(top, true);
|
|
152
156
|
return out;
|
|
153
157
|
}
|
|
154
158
|
export function claudeRunSettings(i) {
|
|
@@ -163,16 +167,25 @@ export function claudeRunSettings(i) {
|
|
|
163
167
|
];
|
|
164
168
|
const seen = new Set();
|
|
165
169
|
const denied = [];
|
|
170
|
+
const secretTargets = targets.filter((t) => t.abs !== resolve(i.pantheonHome));
|
|
171
|
+
const readOnly = (i.readOnlyDirs ?? []).map((d) => resolve(d)).filter((d) => {
|
|
172
|
+
const bad = secretTargets.find((t) => inside(d, t.abs) || inside(t.abs, d));
|
|
173
|
+
if (bad)
|
|
174
|
+
notes.push(`${d} is not opened to the run: it holds or is inside ${bad.abs}`);
|
|
175
|
+
return !bad;
|
|
176
|
+
});
|
|
177
|
+
const keeps = [worktree, ...readOnly];
|
|
166
178
|
for (const t of targets) {
|
|
167
179
|
if (seen.has(t.abs))
|
|
168
180
|
continue;
|
|
169
181
|
seen.add(t.abs);
|
|
170
|
-
|
|
182
|
+
const kept = keeps.filter((k) => inside(k, t.abs));
|
|
183
|
+
if (kept.length) {
|
|
171
184
|
if (worktree === t.abs) {
|
|
172
185
|
notes.push(`${t.abs} is this run's worktree, so it is not denied to the run`);
|
|
173
186
|
continue;
|
|
174
187
|
}
|
|
175
|
-
for (const d of aroundWorktree(t.abs,
|
|
188
|
+
for (const d of aroundWorktree(t.abs, kept, t.abs === resolve(i.pantheonHome) ? PANTHEON_HOME_ENTRIES : []))
|
|
176
189
|
if (!seen.has(d.abs)) {
|
|
177
190
|
seen.add(d.abs);
|
|
178
191
|
denied.push(d);
|
|
@@ -181,10 +194,13 @@ export function claudeRunSettings(i) {
|
|
|
181
194
|
}
|
|
182
195
|
denied.push(t);
|
|
183
196
|
}
|
|
184
|
-
const fileRules =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
197
|
+
const fileRules = [
|
|
198
|
+
...denied.flatMap((t) => {
|
|
199
|
+
const p = `${ruleSpelling(t.abs, home, os)}${t.dir ? "/**" : ""}`;
|
|
200
|
+
return [`Read(${p})`, `Edit(${p})`];
|
|
201
|
+
}),
|
|
202
|
+
...readOnly.flatMap((d) => [`Edit(${ruleSpelling(d, home, os)}/**)`, ...SECRET_FILE_GLOBS.map((g) => `Read(${ruleSpelling(d, home, os)}/**/${g})`)]),
|
|
203
|
+
];
|
|
188
204
|
if (!i.allowBash) {
|
|
189
205
|
const permissions = { deny: ["Bash", "PowerShell", ...fileRules] };
|
|
190
206
|
if (versionAtLeast(i.version, "2.1.257"))
|
|
@@ -192,7 +208,7 @@ export function claudeRunSettings(i) {
|
|
|
192
208
|
else
|
|
193
209
|
notes.push(`reads outside the worktree are not blocked wholesale (needs Claude Code 2.1.257+, this is ${i.version ?? "an unknown version"}) — only the secret paths are denied`);
|
|
194
210
|
notes.push("edits only: no shell, file tools held to the worktree, secrets and Pantheon's files denied");
|
|
195
|
-
return { settings: { permissions }, notes, sandboxed: false };
|
|
211
|
+
return { settings: { permissions }, notes, sandboxed: false, readOnly };
|
|
196
212
|
}
|
|
197
213
|
const excluded = userExcludedCommands(i.userSettings);
|
|
198
214
|
if (excluded.length)
|
|
@@ -203,7 +219,7 @@ export function claudeRunSettings(i) {
|
|
|
203
219
|
};
|
|
204
220
|
if (os === "win32") {
|
|
205
221
|
notes.push("no OS sandbox on Windows (Claude Code has none there): the command list is the only line, and it is not a boundary; commands run through Git Bash only (the PowerShell tool is denied), so without Git Bash this run has no shell");
|
|
206
|
-
return { settings: { permissions }, notes, sandboxed: false };
|
|
222
|
+
return { settings: { permissions }, notes, sandboxed: false, readOnly };
|
|
207
223
|
}
|
|
208
224
|
const failsClosed = versionAtLeast(i.version, "2.1.83");
|
|
209
225
|
const hooks = join(resolve(i.runHome), "git-hooks");
|
|
@@ -214,7 +230,7 @@ export function claudeRunSettings(i) {
|
|
|
214
230
|
allowUnsandboxedCommands: false,
|
|
215
231
|
filesystem: {
|
|
216
232
|
allowWrite: [...new Set([resolve(i.runTmp), resolve(tmpdir())])],
|
|
217
|
-
denyWrite:
|
|
233
|
+
denyWrite: readOnly.map((d) => sandboxSpelling(d, home)),
|
|
218
234
|
denyRead: denied.map((t) => sandboxSpelling(t.abs, home)),
|
|
219
235
|
allowRead: [hooks],
|
|
220
236
|
},
|
|
@@ -222,10 +238,10 @@ export function claudeRunSettings(i) {
|
|
|
222
238
|
};
|
|
223
239
|
if (!failsClosed) {
|
|
224
240
|
notes.push(`this Claude Code (${i.version ?? "version unknown"}) cannot refuse to run a command when its sandbox does not start — update it to 2.1.83 or later; until then the command list is the only line, and it is not a boundary`);
|
|
225
|
-
return { settings: { permissions, sandbox }, notes, sandboxed: false };
|
|
241
|
+
return { settings: { permissions, sandbox }, notes, sandboxed: false, readOnly };
|
|
226
242
|
}
|
|
227
243
|
notes.push(`sandboxed: commands write only the worktree, its repo's git data and the temp dirs; the secret paths and Pantheon's files are unreadable; network allowed`);
|
|
228
|
-
return { settings: { permissions, sandbox }, notes, sandboxed: true };
|
|
244
|
+
return { settings: { permissions, sandbox }, notes, sandboxed: true, readOnly };
|
|
229
245
|
}
|
|
230
246
|
export function runTmpDir(runHome) {
|
|
231
247
|
const saltFile = join(runHome, ".tmp-salt");
|