ac-teams 0.0.6 → 0.0.8

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 CHANGED
@@ -4,7 +4,7 @@ Persistent agent teams for [Pi](https://github.com/earendil-works/pi): shared ta
4
4
 
5
5
  ## Install
6
6
 
7
- Requires Linux, Node 22.19 or newer, and an authenticated `pi` executable on `PATH`. Interactive teams also require `tmux` on `PATH`. Tested with Pi 1.4.0.
7
+ Requires Linux, Node 22.19 or newer, and an authenticated `pi` executable on `PATH`. Interactive teams also require `tmux` on `PATH`. Tested with Pi 1.4.0 and 1.5.0.
8
8
 
9
9
  ```sh
10
10
  pi install npm:ac-teams@0.0.4
@@ -28,13 +28,13 @@ Your Pi session is the root; its child agents are members. Ask Pi to delegate a
28
28
 
29
29
  The task summary sits above the input. The agent selector sits below the input, above your existing footer, and shows descriptive names, models, and context usage. ac-teams publishes a compact team status without replacing the footer or editor supplied by other extensions. `/teams config` toggles these displays independently.
30
30
 
31
- From an empty editor, press Down to select the team row, then Enter to open a member. A red **Needs input** label marks a member with an open question. Ctrl+Shift+Left returns to Main without stopping the member or dismissing its question. `/teams stop` stops every descendant.
31
+ From an empty editor, press Down to select the team row, then Enter to open a member. A red **Needs input** label marks a member with an open question. Ctrl+Shift+Left returns to Main without stopping the member or dismissing its question. `/teams stop` stops every member; main sessions keep running. `/teams join` and `/teams leave` add this session to another team or take it out.
32
32
 
33
33
  Defaults allow up to **10 live members** and **two levels of delegation**. Use the fewest useful agents.
34
34
 
35
35
  ## Know the boundaries
36
36
 
37
- - Members share your filesystem access. All members can read all team messages, including direct messages.
37
+ - Members share your filesystem access unless launched with [enforced access](docs/access.md), an opt-in Docker sandbox that needs Docker and a locally built member image. All members can read all team messages, including direct messages.
38
38
  - Limits cover only `team_agent`, not other delegation tools. Use separate worktrees for concurrent writers.
39
39
  - Stop retains tasks, sessions, and worktrees. Worktree deletion is explicit and requires clean, integrated work.
40
40
  - Exit confirmation needs Pi's `session_before_exit` hook, which installed Pi 1.4.0 lacks. Without it, shutdown cleans up but cannot ask first. Signals always bypass confirmation.
@@ -45,6 +45,7 @@ See the [operations guide](docs/operations.md) for recovery guarantees, storage,
45
45
 
46
46
  - [Usage](docs/usage.md): tool actions, task rules, messages, and companion tools
47
47
  - [Operations](docs/operations.md): limits, stop/resume, worktrees, and security
48
+ - [Enforced access](docs/access.md): Docker-enforced read/write grants, outbound proxy and destination policy, and setup
48
49
  - [Development](docs/development.md): source setup, verification, and publishing
49
50
  - [Implementation history](docs/implementation-plan.md): design decisions and prior art
50
51
 
package/docs/access.md ADDED
@@ -0,0 +1,103 @@
1
+ # Enforce member access
2
+
3
+ By default, members are ordinary unsandboxed Pi processes with your full user permissions. Enforced access is opt-in: ac-teams runs the member's whole Pi process, including its shell, extensions, and subprocesses, in a Docker container that sees only the directories you grant. If enforcement is unavailable, the launch fails; ac-teams never falls back to an unsandboxed member.
4
+
5
+ ## Request access
6
+
7
+ `team_agent` launch accepts `access`:
8
+
9
+ ```json
10
+ { "action": "launch", "name": "reviewer", "brief": "...", "access": { "read": ["."] } }
11
+ { "action": "launch", "name": "parser", "brief": "...", "cwd": "/repo/parser", "access": { "read": ["/repo/docs", "/repo/lib"], "write": ["."] } }
12
+ ```
13
+
14
+ - Paths are directories, absolute or relative to the member's `cwd`. Write implies read. The `cwd` must be inside a grant.
15
+ - `network.allow` limits outbound destinations: `host`, or `*.host` for its subdomains, each optionally `:port`; without a port, an entry allows ports 80 and 443. `[]` allows only the model provider. Omit `allow` for any public destination. Omitting `network` keeps the requester's network access; from an unsandboxed requester that is any public destination.
16
+ - Omitting `access` inherits the requesting member's access. From an unsandboxed requester, it launches an unsandboxed member, as before.
17
+ - Members can only narrow access: a sandboxed member cannot grant a path outside its own access, give write where it has read, allow a destination its own allowlist does not, or launch or resume an unsandboxed member. Access is fixed at launch; resume keeps it.
18
+ - Refused grants: `/`, your home directory or its parents, `/tmp`, `/run`, anything under `/proc`, `/dev`, or `/sys`, and anything containing or inside `~/.pi/agent` or `~/.pi/ac-teams`. They would expose host service sockets, credentials, transcripts, or coordinator state.
19
+ - Grants must be canonical paths with no symlinks, and cannot contain commas, which Docker's mount syntax cannot escape.
20
+ - Docker resolves mount paths when a container starts, so a sandboxed writer could swap a granted path for a symlink just before another member starts. ac-teams holds each grant open from before the container is created, and after the container starts, before any member code runs, compares every mounted grant with the directory it holds; any difference removes the container. A writer of `/repo` and a reader of `/repo/src` can therefore run together. Runtime, transcript, and socket mounts are not compared this way, so none may sit strictly inside another live member's writable grant, across all teams. This protects against sandboxed members only; your own processes, Main, and unsandboxed members can change any path and are trusted.
21
+ - Sandboxed members cannot create or clean up worktrees, send user team actions (`/teams` feedback, task requests, resume), or open other members' terminals; use Main.
22
+
23
+ Before launching, check `sandbox` in `team_agent` `list` or `limits`: `{ "available": true, "backend": "docker" }`, or `available: false` with a `reason`. Every member receives `AC_TEAMS_ACCESS`: `{"enforced":true,"read":[...],"write":[...],"network":{"allow":[...]}}` (no `allow` means any public destination), or `{"enforced":false}`. `team_agent status` reports the coordinator's authoritative `access`.
24
+
25
+ ## Write grants and untrusted output
26
+
27
+ A write grant lets the member change those live files, including Git metadata such as hooks and config, Pi project resources, and build scripts. Treat the result like any untrusted contribution: review changes before you run them, commit them, or start Git or Pi in those directories. Running changed code later outside the sandbox is your choice, not a sandbox escape.
28
+
29
+ ## What an enforced member can reach
30
+
31
+ | Resource | Enforced member |
32
+ | --- | --- |
33
+ | Granted directories | Mounted at the same paths, read-only or read-write |
34
+ | Rest of the host filesystem | Not mounted. The container's system files come from the member image |
35
+ | Pi runtime | Read-only: the ac-teams package and these entries from your Pi agent directory: `npm`, `extensions`, `themes`, `skills`, `prompts`, `AGENTS.md`, `keybindings.json`, `acstack.json`, `models-store.json`. They run as trusted code inside the container and are not secret-free by design: the member can read your installed package code and lockfile, custom extension source, and global instructions. Keep secrets out of them. Your real `auth.json`, sessions, and caches are not mounted |
36
+ | Web search settings | A copy of your `web-search.json` (including any search API keys) with the proxy set |
37
+ | Trust decisions | A filtered `trust.json` with only the entries for the member's `cwd` and its parents |
38
+ | Settings | A copy of your Pi `settings.json`, including its package list and device ID, so do not store secrets there |
39
+ | Scratch | Private `/tmp` (512 MiB), home (256 MiB), and Pi agent directory (64 MiB), discarded at exit |
40
+ | Network | Outbound HTTP and HTTPS through the parent's proxy only (below); no direct network |
41
+ | Processes | Private process namespace; host processes are invisible |
42
+ | Team | The coordinator socket for team tools, and the member's own transcript directory |
43
+ | Environment | A fixed minimal set; host variables, including credentials, are not inherited |
44
+
45
+ Any Unix socket inside a granted directory remains reachable, so grant narrow directories.
46
+
47
+ Containers run as your user with all capabilities dropped, `no-new-privileges`, a read-only root filesystem, `--network none`, no published ports, no restart policy, no Docker socket, and logging disabled. Before starting a container, ac-teams inspects its effective configuration and refuses to start it on any difference in network, ports, privileges, capabilities, security options, namespaces, devices, mounts, image volumes, user, limits, or restart policy. Docker reports bind sources unresolved, so symlink swaps are caught by re-resolving sources instead. It checks network and ports again after start.
48
+
49
+ ## Outbound access goes through the parent's proxy
50
+
51
+ Containers have no network interface except loopback. A bridge inside the container exposes a proxy owned by the root Pi process on `127.0.0.1:3128`, and members get `HTTP_PROXY`, `HTTPS_PROXY`, their lowercase forms, `NO_PROXY` for loopback, and `NODE_USE_ENV_PROXY=1`. Pi, Node, curl, and other clients that honour these variables reach the web; ac-teams also points pi-web-access and billion-context at the proxy through their own settings. Clients that ignore proxy settings have no network at all; there is no transparent capture of other protocols.
52
+
53
+ The proxy:
54
+
55
+ - Handles HTTPS through `CONNECT` and plain HTTP requests, without following redirects; each new request is checked again
56
+ - Resolves names itself and refuses a destination if any resolved address is not global unicast: loopback, private, CGNAT (including Tailscale), link-local and cloud metadata, documentation, multicast, IPv4-mapped and translated forms, and every address of this host, even public ones. It connects to the address it checked, so DNS cannot change in between
57
+ - Applies the member's `network.allow` list, if any, but always allows the selected model provider's endpoint so Pi can run
58
+ - Is reachable only from that member's container, through its own socket
59
+ - Accepts at most 256 open connections per member, so one member cannot exhaust Main's file descriptors
60
+
61
+ A destination allowlist does not keep data private: an allowed host, including the model provider, receives whatever the member sends it.
62
+
63
+ ## Model credentials
64
+
65
+ The member gets an `auth.json` with only its selected provider's current OAuth access token and expiry: no refresh token and no other providers. Main's Pi refreshes the token and rewrites the member's copy when it changes. The member can use, or leak, that token until it expires, and spend your quota with it; it cannot mint new tokens. Switching to another provider inside the member fails, because no other credential is present. A member with a `network.allow` list keeps its provider: its children and its resumes must use the same provider, so the always-allowed provider endpoint cannot widen its network access. The file is removed when the member exits; if Main's Pi is killed, it stays until that team is opened again and its orphans are cleaned up.
66
+
67
+ Supported: OAuth `anthropic` and `openai` models at their default endpoints, tested with Pi 1.4.0 and 1.5.0. Other providers, API-key credentials, or a changed credential shape fail the launch.
68
+
69
+ ## Resource limits
70
+
71
+ Each member container is capped at 2 GiB of memory (no swap) and 1024 processes. ac-teams allows at most as many enforced members at once, across all teams on the host, as fit in 75% of host memory at that cap (5 on a 15 GiB host). It counts created and running member containers after creating a new one, so concurrent launches cannot both pass. This bounds the sum of container caps; it does not limit Main, unsandboxed members, or other host processes. CPU is not capped.
72
+
73
+ A new enforced member takes about 20 seconds to start, because Pi compiles TypeScript extensions into an empty cache.
74
+
75
+ ## Set up Docker
76
+
77
+ Enforced access needs a running Docker daemon your user can use, and the member image for your Pi version. Using Docker means trusting the Docker daemon, which runs as root; membership in the `docker` group is equivalent to root access on the host.
78
+
79
+ When the image is missing, `team_agent list` reports the exact build command, for example:
80
+
81
+ ```sh
82
+ docker build -t ac-teams-member:1.5.0-<hash> --build-arg PI_VERSION=1.5.0 -f <ac-teams>/src/sandbox.Dockerfile <ac-teams>/src
83
+ ```
84
+
85
+ The image is `node:22-bookworm-slim`, pinned by digest, with bash, curl, Git, ripgrep, CA certificates, and the same Pi version as Main. Rebuild after upgrading Pi.
86
+
87
+ ## Promotion drops enforcement
88
+
89
+ Promoting an enforced member to a main session relaunches it outside the container with your user's access. The panel asks for confirmation first. Members always run with `ACSTACK_AGENT_ROLE=member`; main sessions run without it and without inherited `AC_TEAMS_*` variables.
90
+
91
+ ## Storage
92
+
93
+ - Coordinator state and settings: `~/.pi/ac-teams/teams/<team>/`, `~/.pi/ac-teams/config.json`, and `~/.pi/ac-teams/display.json`
94
+ - Generated member configuration (settings copy, provider access token, trust and web search copies, launch file): `~/.pi/ac-teams/agents/<member>/`, removed when the member exits
95
+ - Member transcripts: `<Main's session directory>/ac-teams/<team>/<member>/`, normally under `~/.pi/agent/sessions/<project>/`. Pi's `/resume` lists only top-level sessions, so member transcripts appear in `/teams`, not `/resume`
96
+
97
+ ## Known limits
98
+
99
+ - Tools outside the mounted runtime, such as the ACStack CLI at `~/.claude/acstack/bin/acstack`, need explicit read grants for their directory and any symlink target. Grant those narrow directories, never `~/.claude`, which holds credentials.
100
+ - Interactive members run in the team's tmux pane through `docker start --attach`, on the container's own terminal.
101
+ - Linux only. [Development](development.md#enforced-access-verification) records what has been verified.
102
+
103
+ [Back to README](../README.md)
@@ -0,0 +1,343 @@
1
+ # Session roles - promotion, joining, and several main sessions
2
+
3
+ A team can have several main sessions. The user can promote a member into a main session or join an existing session to a team. Identity, transcript discovery, membership, coordinator ownership, and permission authority stay separate, and ACSTACK\_AGENT\_ROLE tells hooks which processes are main.
4
+
5
+ <!-- Generated from the entry YAML and its element files; regenerate with design-doc --markdown -->
6
+
7
+ ```mermaid
8
+ flowchart TD
9
+ element_user["User<br/>actor"]
10
+ element_main-session["Main session<br/>component · cli"]
11
+ element_member["Member<br/>component"]
12
+ element_coordinator["Coordinator<br/>component"]
13
+ element_team-store["Team store<br/>store"]
14
+ element_transcripts["Pi transcripts<br/>store"]
15
+ element_herdr["Herdr<br/>external-system"]
16
+ element_read-guard["ACStack read guard<br/>external-system"]
17
+ element_user -->|"Promotes, joins, and leaves through /teams in"| element_main-session
18
+ element_user -->|"Switches between main sessions in"| element_herdr
19
+ element_main-session -->|"Hosts as owner, or connects to as a client"| element_coordinator
20
+ element_main-session -->|"Writes its conversation and team entry to"| element_transcripts
21
+ element_read-guard -->|"Finds no member role in"| element_main-session
22
+ element_coordinator -->|"Relaunches promoted members as"| element_main-session
23
+ element_coordinator -->|"Launches, stops, and resumes"| element_member
24
+ element_member -->|"Calls team tools through"| element_coordinator
25
+ element_member -->|"Writes its conversation to"| element_transcripts
26
+ element_read-guard -->|"Reads ACSTACK_AGENT_ROLE=member in"| element_member
27
+ element_coordinator -->|"Holds owner.lock and records roles in"| element_team-store
28
+ element_coordinator -->|"Moves promoted transcripts within"| element_transcripts
29
+ element_coordinator -->|"Opens main-session tabs through"| element_herdr
30
+ ```
31
+
32
+ ## Session roles: promotion, joining, and several main sessions
33
+
34
+ Status: draft for review. Implements nothing yet. Resolves the design questions in [issue #19](https://github.com/ac8318740/ac-teams/issues/19).
35
+
36
+ A team can have several **main sessions**. A main session is a Pi session the user created, joined to a team, or promoted. Every other team process is a **member**. The user can promote a member to a main session from the TUI, or join an existing session to a team with `/teams join`. ac-teams marks every member process with `ACSTACK_AGENT_ROLE=member`, so ACStack's large-read guard applies only to main sessions.
37
+
38
+ ### Review questions
39
+
40
+ 1. Does promotion by relaunch feel seamless enough, given that a promoted session moves to its own Herdr tab?
41
+ 2. Does it look right that every main session can control every member and other main sessions? That matches what an unsandboxed session can already do through its shell.
42
+ 3. Is the takeover after the owner exits (automatic, with a notice) the right default?
43
+ 4. Are the changes and tests at the end complete enough to start implementation?
44
+
45
+ ### Trade-offs to judge
46
+
47
+ - **Relaunch, not live handover.** Promotion stops the member process and reopens its transcript in a new process. The member loses its in-flight turn, but its role, sandbox, token, and environment change cleanly.
48
+ - **No coordinator daemon.** The coordinator stays inside whichever main session holds `owner.lock`. When the owner exits, members stop and the next owner resumes them automatically. The cost is a pause and at most one redone turn per busy member.
49
+ - **Wide main-session authority.** Any main session can stop work started by another. ac-teams does not try to guard against this, because an unsandboxed session can already stop processes through the shell or Herdr.
50
+
51
+ ## Definitions
52
+
53
+ These five properties are independent. A change to one never changes another.
54
+
55
+ | Property | Defined by | Changes when |
56
+ |---|---|---|
57
+ | Identity | Pi session id in the transcript header | Never |
58
+ | Discovery | Where the transcript file lives | Promotion moves it to the top-level session folder |
59
+ | Membership | A member record in the team store | Join adds one; leave clears its role |
60
+ | Coordinator ownership | Holder of `owner.lock` | The owner exits and another main session takes it |
61
+ | Authority | The record's `role`: `main` or `member` | Only promotion, join, or leave, all user actions |
62
+
63
+ ### What the coordinator does
64
+
65
+ The coordinator is ac-teams' in-process server. Today it runs inside Main. In this design, it runs inside whichever main session holds `owner.lock`. In practice it:
66
+
67
+ - **Runs member processes.** Starts each member's Pi, either headless or in tmux, optionally inside a Docker sandbox. It also stops and resumes members, and stops them all when its host session exits
68
+ - **Answers team tools.** Every `team_agent`, `team_task`, and `team_message` call from any session arrives over `coordinator.sock` and is applied here, so concurrent edits are serialized
69
+ - **Keeps team state.** Writes members, tasks, and messages to the team store
70
+ - **Delivers notifications.** Tells each session when new team messages arrive
71
+ - **Enforces rules.** Checks authority, depth and member limits, and sandbox grants, and brokers provider credentials for sandboxed members
72
+
73
+ This design keeps all of that. It adds role checks, connections from main sessions it did not launch, promotion, joining, leaving, and ownership takeover.
74
+
75
+ The team id stays the session id of the session that created the team, even after that session leaves or hands over ownership.
76
+
77
+ ### Main-agent signal
78
+
79
+ - The coordinator sets `ACSTACK_AGENT_ROLE=member` on every member process, next to `AC_TEAMS_MEMBER`. That includes the terminal-launch script, which today only passes `AC_TEAMS_*` and `HERDR_ENV`
80
+ - A hook treats the session as main when the variable is absent or `main`, and as not main for any other value
81
+ - Anything a member starts inherits the variable and counts as not main
82
+ - Promotion and every main-session launch must remove `ACSTACK_AGENT_ROLE`, every `AC_TEAMS_*` variable, and `HERDR_ENV=0`. Tests must cover this
83
+ - ACStack reads the variable on every tool call. It detects Claude Code and Codex subagents from `agent_id` in their hook payloads instead. That detection belongs to ACStack, not ac-teams
84
+ - Role grants no permission by itself
85
+
86
+ ## User flows
87
+
88
+ ### Promote a member
89
+
90
+ 1. The user opens a teammate from the agent selector or `/teams` and chooses **Promote to main**
91
+ 2. If the member is sandboxed, a confirmation states that the session will run with the user's full access. Declining cancels
92
+ 3. The coordinator stops the member, waiting for its process to exit as `stop` does today
93
+ 4. The coordinator moves the transcript from `ac-teams/<team>/<member>/` to the project's top-level session folder. It removes the member's config at `~/.pi/ac-teams/agents/<id>` and sets the member's role to `main` without a parent. The member's children stay its descendants
94
+ 5. The coordinator writes the session's team entry: a Pi custom entry holding the team id and member id, with no secrets
95
+ 6. The coordinator relaunches `pi --session <moved file>` with a clean environment:
96
+ - With Herdr, in a new tab in the workspace for the session's worktree, creating the workspace if needed. The tab gets focus and the session's name
97
+ - Without Herdr, in a tmux session on a per-main-session socket that the owner's `kill-server` cannot reach. The user's screen stays attached, and ac-teams prints the reattach command
98
+ 7. The new process reads its team entry, connects to the coordinator with a main-session token from the team store, and appears in `/resume`
99
+
100
+ ### Join an existing session to a team
101
+
102
+ 1. The user runs `/teams join` in an ordinary Pi session
103
+ 2. ac-teams lists this project's teams. The user picks one
104
+ 3. ac-teams refuses if the session already belongs to a team with members, or if its session id is already in the chosen team
105
+ 4. The coordinator adds a member record with role `main` and writes the team entry
106
+ 5. The session receives a short summary of open tasks, owners, and recent messages, with its message cursor at the latest message. It claims or creates work with the normal team tools
107
+
108
+ ### Several main sessions
109
+
110
+ - Each main session holds one connection to the coordinator. The owner hosts it in-process; the others connect over `coordinator.sock`
111
+ - Every main session sits at depth 0. Any main session can launch, stop, resume, and clean up any member, and stop or resume another main session
112
+ - Stopping a main session asks it to shut down through its connection. Resuming one relaunches its transcript, as in step 6 of promotion
113
+ - Members keep today's rule: they control only their descendants and never a main session
114
+ - Tasks and messages stay shared. Ownership and revision checks still serialize concurrent edits, because one coordinator applies them all
115
+
116
+ ### Leave a team
117
+
118
+ `/teams leave` returns the session's in-progress tasks to todo and clears their ownership. The member record keeps its history but loses its role, so its descendants stay under the other main sessions. The team entry records the departure, so a later `/resume` does not rejoin. Leaving is refused while the session is the only main session and members are still live.
119
+
120
+ ## Recovery
121
+
122
+ - **The owner exits while another main session runs.** Before stopping members, the owner records each one's state (busy, idle, or needs input) in the team store. Today, `Coordinator.open` discards that state. A connected main session then takes `owner.lock`, opens the coordinator, and runs the resume pass below. Every main session gets a notice naming the new owner
123
+ - **No main session is running when the owner exits.** Members stay stopped with their recorded state. The next main session that opens the team runs the same resume pass
124
+ - **A promoted session is resumed with no coordinator running.** The extension reads its team entry at startup and offers to take ownership. Accepting opens the coordinator and runs the resume pass. Declining runs the session without team tools
125
+
126
+ ### Resume pass
127
+
128
+ | Recorded state | Action |
129
+ |---|---|
130
+ | Busy | Resume and send: "Your process was restarted during a coordinator handover. Check your last steps and continue your current task." |
131
+ | Idle | Resume and leave idle |
132
+ | Needs input | Resume and show **Needs input** again |
133
+
134
+ Pi saves each completed message, so a busy member loses only the reply it was generating and redoes that turn. A command killed mid-run may have half finished. The message tells the member to check before it continues, which reduces that risk but does not remove it, the same as stopping a member today.
135
+ - **A main session crashes.** Its record stays. The coordinator marks it interrupted, and any main session can resume it
136
+ - **Promotion fails partway.** Each step is recorded in the team store before the next starts. On failure, the coordinator rolls back to a stopped member with its original transcript path, and the member can be retried or resumed
137
+
138
+ ## Security boundaries
139
+
140
+ - Only the user can promote, join, or leave. These actions have no agent-tool form
141
+ - Promoting a sandboxed member is an explicit escalation, approved in the confirmation step. Without approval, nothing changes
142
+ - Main-session tokens live in the private team store, which sandboxed members cannot read. A member therefore cannot impersonate a main session over the socket
143
+ - Descendant non-escalation stays as it is: a member cannot grant its descendants more access than it has
144
+ - Provider credentials for sandboxed members come from the current owner's Pi login. After a takeover, the new owner brokers them under the same OS user
145
+
146
+ ## Changes needed before implementation
147
+
148
+ - **Team store schema 4.** Add `role` to members and migrate `root` to a main record. Replace `actor === "root"` checks with role checks. Store each main session's token
149
+ - **Coordinator.**
150
+ - Accept connections from main sessions that were not launched by the coordinator
151
+ - Add promote, join, leave, and the stop and resume of main sessions
152
+ - Add ownership takeover, and recompute descendant depths after a promotion
153
+ - **Launch environment.** Set `ACSTACK_AGENT_ROLE=member` for every member, including the terminal script's variable filter. Strip it on main-session launches
154
+ - **Extension.** Read the team entry at `session_start`. Add `/teams join`, `/teams leave`, and the **Promote to main** action. Open Herdr tabs, or use the tmux fallback
155
+ - **Docs.** Update `docs/usage.md`, `docs/operations.md`, and `docs/access.md`
156
+
157
+ ## Tests needed
158
+
159
+ - A launched member has `ACSTACK_AGENT_ROLE=member`, in both RPC and terminal modes
160
+ - A promoted session's environment has no `ACSTACK_AGENT_ROLE` and no `AC_TEAMS_*` variables
161
+ - Promotion keeps the session id, moves the file, updates the stored path, and writes the team entry. Pi's session listing then includes the session
162
+ - A sandboxed promotion is refused without confirmation and runs unsandboxed after it
163
+ - Joining refuses a duplicate session id, and refuses a session that leads another team with members
164
+ - A main session can stop and resume members and other main sessions. A member cannot act on a main session
165
+ - When the owner exits, another main session takes the lock. Busy members resume with the continue message, idle members stay idle, and members that needed input show **Needs input** again
166
+ - When no main session is running, the next one to open the team runs the same resume pass
167
+ - Resuming a promoted transcript with no coordinator offers takeover, and declining leaves team tools off
168
+ - Leaving returns in-progress tasks to todo and keeps descendants manageable
169
+ - A promotion interrupted after each step rolls back to a resumable member
170
+ - Schema 3 teams migrate to schema 4 with root as a main record
171
+
172
+
173
+ ## User
174
+
175
+ *Starts main sessions and is the only party who can promote a member, join a session to a team, or approve a sandboxed member's full access.*
176
+
177
+ - Identity: `user`
178
+ - Kind: actor
179
+ - Channels: none
180
+ - Source: [user.yaml](user.yaml)
181
+
182
+
183
+
184
+ ## Main session
185
+
186
+ *A Pi session the user started, joined to a team, or promoted. Every main session controls every member and other main sessions, shares the team's tasks and messages, and belongs to one team at a time.*
187
+
188
+ - Identity: `main-session`
189
+ - Kind: component
190
+ - Channels: cli
191
+ - Source: [main-session.yaml](main-session.yaml)
192
+
193
+ ## Main-agent signal
194
+
195
+ - Requirement: ACStack's large-read guard applies only to main agents. A main agent is a session the user created or promoted, never one an agent created. The signal must reach in-process Pi hooks and shell hooks, and must change when a member is promoted.
196
+ - Design response: The coordinator sets ACSTACK\_AGENT\_ROLE=member on every member process, next to AC\_TEAMS\_MEMBER. Absent or "main" means main; any other value means not main. Children of a member inherit it. Promotion always relaunches the transcript in a new process without ACSTACK\_AGENT\_ROLE or any AC\_TEAMS\_\* variable, so a live process never needs to change its role. Main status grants no permissions.
197
+ - Applies to: coordinator, member, main-session, read-guard
198
+
199
+ ## Separate identity, discovery, membership, and authority
200
+
201
+ - Requirement: Issue \#19 forbids tying session identity or permission authority to picker visibility, storage location, or a single main session.
202
+ - Design response: Identity is the Pi session id. Discovery is where the transcript file lives. Membership and role are records in the team store. Ownership is whoever holds owner.lock. Authority comes from the member's role in the team store, which only explicit user actions change. Moving a file or taking the lock changes nothing else.
203
+ - Applies to: main-session, member, coordinator, team-store, transcripts
204
+
205
+ ## Duplicate prevention
206
+
207
+ - Requirement: A session must not appear twice in a team, run as two processes, or belong to two teams.
208
+ - Design response: The team store keys members by member id and refuses a second record with the same Pi session id. The coordinator accepts one live connection per member and keeps the existing pid and identity check before any launch or relaunch. A session in a team with members cannot join another team.
209
+ - Applies to: coordinator, team-store, main-session
210
+
211
+ ## Recovery without the original owner
212
+
213
+ - Requirement: The team must carry on when the owner exits, and a promoted session resumed later must find its team again.
214
+ - Design response: When the owner exits, another connected main session takes owner.lock and resumes members by their recorded state: busy members get a message to check their last steps and continue, idle members stay idle, and open questions reappear. A main session resumed from /resume reads its team entry, then connects to the live coordinator or offers to take ownership.
215
+ - Applies to: main-session, coordinator, team-store, member
216
+
217
+ ## Member
218
+
219
+ *An agent-launched Pi process with ACSTACK\_AGENT\_ROLE=member and AC\_TEAMS\_\* variables. It controls only its descendants, and its transcript stays under ac-teams/&\#60;team&\#62;/&\#60;member&\#62;/ until promotion.*
220
+
221
+ - Identity: `member`
222
+ - Kind: component
223
+ - Channels: none
224
+ - Source: [member.yaml](member.yaml)
225
+
226
+ ## Main-agent signal
227
+
228
+ - Requirement: ACStack's large-read guard applies only to main agents. A main agent is a session the user created or promoted, never one an agent created. The signal must reach in-process Pi hooks and shell hooks, and must change when a member is promoted.
229
+ - Design response: The coordinator sets ACSTACK\_AGENT\_ROLE=member on every member process, next to AC\_TEAMS\_MEMBER. Absent or "main" means main; any other value means not main. Children of a member inherit it. Promotion always relaunches the transcript in a new process without ACSTACK\_AGENT\_ROLE or any AC\_TEAMS\_\* variable, so a live process never needs to change its role. Main status grants no permissions.
230
+ - Applies to: coordinator, member, main-session, read-guard
231
+
232
+ ## Separate identity, discovery, membership, and authority
233
+
234
+ - Requirement: Issue \#19 forbids tying session identity or permission authority to picker visibility, storage location, or a single main session.
235
+ - Design response: Identity is the Pi session id. Discovery is where the transcript file lives. Membership and role are records in the team store. Ownership is whoever holds owner.lock. Authority comes from the member's role in the team store, which only explicit user actions change. Moving a file or taking the lock changes nothing else.
236
+ - Applies to: main-session, member, coordinator, team-store, transcripts
237
+
238
+ ## Recovery without the original owner
239
+
240
+ - Requirement: The team must carry on when the owner exits, and a promoted session resumed later must find its team again.
241
+ - Design response: When the owner exits, another connected main session takes owner.lock and resumes members by their recorded state: busy members get a message to check their last steps and continue, idle members stay idle, and open questions reappear. A main session resumed from /resume reads its team entry, then connects to the live coordinator or offers to take ownership.
242
+ - Applies to: main-session, coordinator, team-store, member
243
+
244
+ ## Coordinator
245
+
246
+ *The team's single referee, running inside whichever main session holds owner.lock. It starts and stops member processes, answers every team\_agent, team\_task, and team\_message call, delivers message notifications, checks who may do what, and runs promotion, joining, and takeover.*
247
+
248
+ - Identity: `coordinator`
249
+ - Kind: component
250
+ - Channels: none
251
+ - Source: [coordinator.yaml](coordinator.yaml)
252
+
253
+ ## Main-agent signal
254
+
255
+ - Requirement: ACStack's large-read guard applies only to main agents. A main agent is a session the user created or promoted, never one an agent created. The signal must reach in-process Pi hooks and shell hooks, and must change when a member is promoted.
256
+ - Design response: The coordinator sets ACSTACK\_AGENT\_ROLE=member on every member process, next to AC\_TEAMS\_MEMBER. Absent or "main" means main; any other value means not main. Children of a member inherit it. Promotion always relaunches the transcript in a new process without ACSTACK\_AGENT\_ROLE or any AC\_TEAMS\_\* variable, so a live process never needs to change its role. Main status grants no permissions.
257
+ - Applies to: coordinator, member, main-session, read-guard
258
+
259
+ ## Separate identity, discovery, membership, and authority
260
+
261
+ - Requirement: Issue \#19 forbids tying session identity or permission authority to picker visibility, storage location, or a single main session.
262
+ - Design response: Identity is the Pi session id. Discovery is where the transcript file lives. Membership and role are records in the team store. Ownership is whoever holds owner.lock. Authority comes from the member's role in the team store, which only explicit user actions change. Moving a file or taking the lock changes nothing else.
263
+ - Applies to: main-session, member, coordinator, team-store, transcripts
264
+
265
+ ## Duplicate prevention
266
+
267
+ - Requirement: A session must not appear twice in a team, run as two processes, or belong to two teams.
268
+ - Design response: The team store keys members by member id and refuses a second record with the same Pi session id. The coordinator accepts one live connection per member and keeps the existing pid and identity check before any launch or relaunch. A session in a team with members cannot join another team.
269
+ - Applies to: coordinator, team-store, main-session
270
+
271
+ ## Recovery without the original owner
272
+
273
+ - Requirement: The team must carry on when the owner exits, and a promoted session resumed later must find its team again.
274
+ - Design response: When the owner exits, another connected main session takes owner.lock and resumes members by their recorded state: busy members get a message to check their last steps and continue, idle members stay idle, and open questions reappear. A main session resumed from /resume reads its team entry, then connects to the live coordinator or offers to take ownership.
275
+ - Applies to: main-session, coordinator, team-store, member
276
+
277
+ ## Team store
278
+
279
+ *Private team state under \~/.pi/ac-teams/teams/&\#60;team&\#62;/: members with their role, tasks, messages, owner.lock, and each main session's connection token. Sandboxed members cannot read it.*
280
+
281
+ - Identity: `team-store`
282
+ - Kind: store
283
+ - Channels: none
284
+ - Source: [team-store.yaml](team-store.yaml)
285
+
286
+ ## Separate identity, discovery, membership, and authority
287
+
288
+ - Requirement: Issue \#19 forbids tying session identity or permission authority to picker visibility, storage location, or a single main session.
289
+ - Design response: Identity is the Pi session id. Discovery is where the transcript file lives. Membership and role are records in the team store. Ownership is whoever holds owner.lock. Authority comes from the member's role in the team store, which only explicit user actions change. Moving a file or taking the lock changes nothing else.
290
+ - Applies to: main-session, member, coordinator, team-store, transcripts
291
+
292
+ ## Duplicate prevention
293
+
294
+ - Requirement: A session must not appear twice in a team, run as two processes, or belong to two teams.
295
+ - Design response: The team store keys members by member id and refuses a second record with the same Pi session id. The coordinator accepts one live connection per member and keeps the existing pid and identity check before any launch or relaunch. A session in a team with members cannot join another team.
296
+ - Applies to: coordinator, team-store, main-session
297
+
298
+ ## Recovery without the original owner
299
+
300
+ - Requirement: The team must carry on when the owner exits, and a promoted session resumed later must find its team again.
301
+ - Design response: When the owner exits, another connected main session takes owner.lock and resumes members by their recorded state: busy members get a message to check their last steps and continue, idle members stay idle, and open questions reappear. A main session resumed from /resume reads its team entry, then connects to the live coordinator or offers to take ownership.
302
+ - Applies to: main-session, coordinator, team-store, member
303
+
304
+ ## Pi transcripts
305
+
306
+ *Standard Pi session files. Main sessions sit at the top level of the project's session folder, where /resume finds them. Members sit under ac-teams/&\#60;team&\#62;/&\#60;member&\#62;/, where only /teams finds them.*
307
+
308
+ - Identity: `transcripts`
309
+ - Kind: store
310
+ - Channels: none
311
+ - Source: [transcripts.yaml](transcripts.yaml)
312
+
313
+ ## Separate identity, discovery, membership, and authority
314
+
315
+ - Requirement: Issue \#19 forbids tying session identity or permission authority to picker visibility, storage location, or a single main session.
316
+ - Design response: Identity is the Pi session id. Discovery is where the transcript file lives. Membership and role are records in the team store. Ownership is whoever holds owner.lock. Authority comes from the member's role in the team store, which only explicit user actions change. Moving a file or taking the lock changes nothing else.
317
+ - Applies to: main-session, member, coordinator, team-store, transcripts
318
+
319
+ ## Herdr
320
+
321
+ *Optional terminal workspace manager. When present, each main session gets its own tab in the workspace for its worktree.*
322
+
323
+ - Identity: `herdr`
324
+ - Kind: external-system
325
+ - Channels: none
326
+ - Source: [herdr.yaml](herdr.yaml)
327
+
328
+
329
+
330
+ ## ACStack read guard
331
+
332
+ *ACStack's large-read hook. It blocks a large untargeted read once in main agents only, and reads ACSTACK\_AGENT\_ROLE on every call to decide.*
333
+
334
+ - Identity: `read-guard`
335
+ - Kind: external-system
336
+ - Channels: none
337
+ - Source: [read-guard.yaml](read-guard.yaml)
338
+
339
+ ## Main-agent signal
340
+
341
+ - Requirement: ACStack's large-read guard applies only to main agents. A main agent is a session the user created or promoted, never one an agent created. The signal must reach in-process Pi hooks and shell hooks, and must change when a member is promoted.
342
+ - Design response: The coordinator sets ACSTACK\_AGENT\_ROLE=member on every member process, next to AC\_TEAMS\_MEMBER. Absent or "main" means main; any other value means not main. Children of a member inherit it. Promotion always relaunches the transcript in a new process without ACSTACK\_AGENT\_ROLE or any AC\_TEAMS\_\* variable, so a live process never needs to change its role. Main status grants no permissions.
343
+ - Applies to: coordinator, member, main-session, read-guard
@@ -0,0 +1,44 @@
1
+ id: coordinator
2
+ name: Coordinator
3
+ summary: >-
4
+ The team's single referee, running inside whichever main session holds
5
+ owner.lock. It starts and stops member processes, answers every team_agent,
6
+ team_task, and team_message call, delivers message notifications, checks
7
+ who may do what, and runs promotion, joining, and takeover.
8
+ kind: component
9
+ relationships:
10
+ - id: main-uses-coordinator
11
+ from: main-session
12
+ to: coordinator
13
+ direction: one-way
14
+ label: Hosts as owner, or connects to as a client
15
+ - id: member-uses-coordinator
16
+ from: member
17
+ to: coordinator
18
+ direction: one-way
19
+ label: Calls team tools through
20
+ - id: coordinator-runs-member
21
+ from: coordinator
22
+ to: member
23
+ direction: one-way
24
+ label: Launches, stops, and resumes
25
+ - id: coordinator-launches-main
26
+ from: coordinator
27
+ to: main-session
28
+ direction: one-way
29
+ label: Relaunches promoted members as
30
+ - id: coordinator-writes-store
31
+ from: coordinator
32
+ to: team-store
33
+ direction: one-way
34
+ label: Holds owner.lock and records roles in
35
+ - id: coordinator-moves-transcript
36
+ from: coordinator
37
+ to: transcripts
38
+ direction: one-way
39
+ label: Moves promoted transcripts within
40
+ - id: coordinator-opens-tabs
41
+ from: coordinator
42
+ to: herdr
43
+ direction: one-way
44
+ label: Opens main-session tabs through
@@ -0,0 +1,70 @@
1
+ id: session-roles
2
+ name: Session roles - promotion, joining, and several main sessions
3
+ summary: >-
4
+ A team can have several main sessions. The user can promote a member into a
5
+ main session or join an existing session to a team. Identity, transcript
6
+ discovery, membership, coordinator ownership, and permission authority stay
7
+ separate, and ACSTACK_AGENT_ROLE tells hooks which processes are main.
8
+ detail: journey.md
9
+ elements:
10
+ - user.yaml
11
+ - main-session.yaml
12
+ - member.yaml
13
+ - coordinator.yaml
14
+ - team-store.yaml
15
+ - transcripts.yaml
16
+ - herdr.yaml
17
+ - read-guard.yaml
18
+ concerns:
19
+ - id: main-signal
20
+ name: Main-agent signal
21
+ elements: [coordinator, member, main-session, read-guard]
22
+ requirement: >-
23
+ ACStack's large-read guard applies only to main agents. A main agent is
24
+ a session the user created or promoted, never one an agent created. The
25
+ signal must reach in-process Pi hooks and shell hooks, and must change
26
+ when a member is promoted.
27
+ response: >-
28
+ The coordinator sets ACSTACK_AGENT_ROLE=member on every member process,
29
+ next to AC_TEAMS_MEMBER. Absent or "main" means main; any other value
30
+ means not main. Children of a member inherit it. Promotion always
31
+ relaunches the transcript in a new process without ACSTACK_AGENT_ROLE
32
+ or any AC_TEAMS_* variable, so a live process never needs to change its
33
+ role. Main status grants no permissions.
34
+ - id: separate-axes
35
+ name: Separate identity, discovery, membership, and authority
36
+ elements: [main-session, member, coordinator, team-store, transcripts]
37
+ requirement: >-
38
+ Issue #19 forbids tying session identity or permission authority to
39
+ picker visibility, storage location, or a single main session.
40
+ response: >-
41
+ Identity is the Pi session id. Discovery is where the transcript file
42
+ lives. Membership and role are records in the team store. Ownership is
43
+ whoever holds owner.lock. Authority comes from the member's role in the
44
+ team store, which only explicit user actions change. Moving a file or
45
+ taking the lock changes nothing else.
46
+ - id: duplicate-prevention
47
+ name: Duplicate prevention
48
+ elements: [coordinator, team-store, main-session]
49
+ requirement: >-
50
+ A session must not appear twice in a team, run as two processes, or
51
+ belong to two teams.
52
+ response: >-
53
+ The team store keys members by member id and refuses a second record
54
+ with the same Pi session id. The coordinator accepts one live
55
+ connection per member and keeps the existing pid and identity check
56
+ before any launch or relaunch. A session in a team with members cannot
57
+ join another team.
58
+ - id: recovery
59
+ name: Recovery without the original owner
60
+ elements: [main-session, coordinator, team-store, member]
61
+ requirement: >-
62
+ The team must carry on when the owner exits, and a promoted session
63
+ resumed later must find its team again.
64
+ response: >-
65
+ When the owner exits, another connected main session takes owner.lock
66
+ and resumes members by their recorded state: busy members get a message
67
+ to check their last steps and continue, idle members stay idle, and open
68
+ questions reappear. A main session resumed
69
+ from /resume reads its team entry, then connects to the live
70
+ coordinator or offers to take ownership.
@@ -0,0 +1,17 @@
1
+ id: herdr
2
+ name: Herdr
3
+ summary: >-
4
+ Optional terminal workspace manager. When present, each main session gets
5
+ its own tab in the workspace for its worktree.
6
+ kind: external-system
7
+ relationships:
8
+ - id: user-uses-herdr
9
+ from: user
10
+ to: herdr
11
+ direction: one-way
12
+ label: Switches between main sessions in
13
+ - id: coordinator-opens-tabs
14
+ from: coordinator
15
+ to: herdr
16
+ direction: one-way
17
+ label: Opens main-session tabs through