docs(readme): round-2 review fixes (00aa4a4c / 09bf3e77)

- exit 5: a busy or missing target cancels the job; a superseded attempt
  leaves the record to its new owner, unchanged
- install.sh copies a legacy .env to .mam.env and keeps the original
- restore-off fallback: without tomllib the file keeps only that key
  (restore off, other settings lost, warning); restore on only when no
  file can be written
- PoC: no token only when none is passed; tests: every test gets a temp
  registry and log dir, most also the sandbox HOME and mock herdr

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-13 11:32:01 +09:00
1 parent 423abc7844
commit ae2813773c
2 files changed
+10 -10

No files matched your search

+5 -5
View File
@@ -35,7 +35,7 @@ bash deploy/install.sh
- herdr 와 Python 3.9+ 가 설치돼 있는지, 시스템 `python3` 에 PyYAML 이 있는지 확인합니다. - herdr 와 Python 3.9+ 가 설치돼 있는지, 시스템 `python3` 에 PyYAML 이 있는지 확인합니다.
- `.agents/` 와 `.mam.env.example` 을 워크스페이스에 복사합니다. - `.agents/` 와 `.mam.env.example` 을 워크스페이스에 복사합니다.
- `.venv` 를 만들고 `paho-mqtt`·PyYAML 을 설치합니다. - `.venv` 를 만들고 `paho-mqtt`·PyYAML 을 설치합니다.
- `MQTT_BROKER=localhost` 로 된 초기 `.mam.env` 를 씁니다. 이미 있는 `.mam.env` 는 그대로 두고, 예전 `.env` 는 `.mam.env` 로 이름을 바꿉니다. - `MQTT_BROKER=localhost` 로 된 초기 `.mam.env` 를 씁니다. 이미 있는 `.mam.env` 는 그대로 둡니다. 예전 `.env` 만 있으면 그것을 `.mam.env` 로 복사하고 원본은 남깁니다.
- `.venv`, `.mam/`, `.mam.env` 를 `.gitignore` 에 추가합니다. - `.venv`, `.mam/`, `.mam.env` 를 `.gitignore` 에 추가합니다.
`.mam.env` 의 `MQTT_BROKER` 를 직접 운영하는 브로커로 바꾸십시오(사설 브로커는 [MESSAGING.md](MESSAGING.md) 참고). `MQTT_BROKER` 가 아예 설정되지 않으면 코드는 공개 브로커 `broker.hivemq.com` 을 씁니다. 시험용으로는 괜찮지만 실제 작업용은 아닙니다. `deploy/generate-env.sh` 는 `.mam.env.example` 로 `.mam.env` 를 다시 만듭니다. 업데이트와 제거는 `deploy/update.sh`, `deploy/remove.sh` 입니다. `deploy/install_mam.sh` 는 로컬 클론에서 다른 디렉터리로 복사해 설치하는 별도 설치기입니다. `.mam.env` 의 `MQTT_BROKER` 를 직접 운영하는 브로커로 바꾸십시오(사설 브로커는 [MESSAGING.md](MESSAGING.md) 참고). `MQTT_BROKER` 가 아예 설정되지 않으면 코드는 공개 브로커 `broker.hivemq.com` 을 씁니다. 시험용으로는 괜찮지만 실제 작업용은 아닙니다. `deploy/generate-env.sh` 는 `.mam.env.example` 로 `.mam.env` 를 다시 만듭니다. 업데이트와 제거는 `deploy/update.sh`, `deploy/remove.sh` 입니다. `deploy/install_mam.sh` 는 로컬 클론에서 다른 디렉터리로 복사해 설치하는 별도 설치기입니다.
@@ -89,7 +89,7 @@ MAM 은 각자 레지스트리를 가진 두 계층으로 이루어집니다.
- **워크스페이스 전용 서버.** `create` 는 기본으로 워크스페이스 이름(slug)을 딴 herdr 서버에서 에이전트를 돌립니다. 이 서버는 기본 herdr 와 분리된 자기 소켓을 씁니다. `--herdr-session <name>` 또는 `HERDR_SESSION_NAME` 으로 다른 이름을 고를 수 있습니다. 서버 이름은 세션 행에 기록되고, 이후 모든 명령이 그 서버로 갑니다. - **워크스페이스 전용 서버.** `create` 는 기본으로 워크스페이스 이름(slug)을 딴 herdr 서버에서 에이전트를 돌립니다. 이 서버는 기본 herdr 와 분리된 자기 소켓을 씁니다. `--herdr-session <name>` 또는 `HERDR_SESSION_NAME` 으로 다른 이름을 고를 수 있습니다. 서버 이름은 세션 행에 기록되고, 이후 모든 명령이 그 서버로 갑니다.
- **shim.** `lib.sh` 를 source 하면 `<workspace>/.mam/shim/herdr` 를 쓰고 `PATH` 맨 앞에 둡니다. shim 은 스크립트가 쓰는 tmux 식 명령(`new-session`, `has-session` …)을 herdr 의 에이전트 API 위에 제공합니다. 이름 있는 서버면 호출마다 `--session <server>` 를 붙이고, 그 서버가 돌고 있지 않으면 헤드리스로 시작합니다. 단 `MAM_NO_AUTOSTART=1` 이면 시작하지 않습니다. `status` 와 `monitor` 가 이 값을 설정하므로, 조회만으로 서버가 뜨는 일은 없습니다. `default` 서버 호출은 바꾸지 않고 사용자의 herdr 로 그대로 갑니다. - **shim.** `lib.sh` 를 source 하면 `<workspace>/.mam/shim/herdr` 를 쓰고 `PATH` 맨 앞에 둡니다. shim 은 스크립트가 쓰는 tmux 식 명령(`new-session`, `has-session` …)을 herdr 의 에이전트 API 위에 제공합니다. 이름 있는 서버면 호출마다 `--session <server>` 를 붙이고, 그 서버가 돌고 있지 않으면 헤드리스로 시작합니다. 단 `MAM_NO_AUTOSTART=1` 이면 시작하지 않습니다. `status` 와 `monitor` 가 이 값을 설정하므로, 조회만으로 서버가 뜨는 일은 없습니다. `default` 서버 호출은 바꾸지 않고 사용자의 herdr 로 그대로 갑니다.
- **예상치 못한 재실행 없음.** MAM 이 시작한 서버는 보통 사용자의 herdr 설정에 `resume_agents_on_restore = false` 를 더해 씁니다. 그래서 herdr 가 재시작돼도 저장된 에이전트를 권한 플래그 없이 다시 띄우지 않습니다. 이 설정을 만들 수 없으면 부트스트랩이 경고를 남기고, 서버는 restore 가 켜진 채 시작합니다. - **예상치 못한 재실행 없음.** MAM 이 시작한 서버는 보통 사용자의 herdr 설정에 `resume_agents_on_restore = false` 를 더해 씁니다. 그래서 herdr 가 재시작돼도 저장된 에이전트를 권한 플래그 없이 다시 띄우지 않습니다. `tomllib` 이 없는 Python(macOS 기본 3.9 등)에서는 이 파일에 그 키만 들어가므로, restore 는 꺼지지만 사용자의 다른 herdr 설정은 반영되지 않습니다. 이때 부트스트랩이 경고합니다. 설정 파일을 아예 쓰지 못한 경우에만 서버가 restore 가 켜진 채 시작하며, 이때도 경고를 남깁니다.
- **pane 배치는 한 번에 하나.** pane 배치는 서버별 잠금(`lib_py/pane_lock.py`, macOS·Linux 에서 `flock`)을 잡고 합니다. 한 폴더에서 동시에 실행한 `create` 도 각자 자기 pane 을 받습니다. - **pane 배치는 한 번에 하나.** pane 배치는 서버별 잠금(`lib_py/pane_lock.py`, macOS·Linux 에서 `flock`)을 잡고 합니다. 한 폴더에서 동시에 실행한 `create` 도 각자 자기 pane 을 받습니다.
### 🛡️ 레지스트리 쓰기와 동시성 ### 🛡️ 레지스트리 쓰기와 동시성
@@ -117,7 +117,7 @@ MAM 은 각자 레지스트리를 가진 두 계층으로 이루어집니다.
| `1` | 실패, 또는 T 안에 `started` 없음 | | `1` | 실패, 또는 T 안에 `started` 없음 |
| `2` | K×N 동안 이벤트 없음(`deadline_timeout`) 또는 예산 소진(`wall_clock_timeout`) | | `2` | K×N 동안 이벤트 없음(`deadline_timeout`) 또는 예산 소진(`wall_clock_timeout`) |
| `3` | 브로커를 쓸 수 없고, 디스크에도 종결 상태가 없음 | | `3` | 브로커를 쓸 수 없고, 디스크에도 종결 상태가 없음 |
| `5` | 전달 안 됨(`cancelled`): 세션이 바쁨, 대상을 찾지 못함, 또는 기다리는 동안 레코드를 다른 쪽이 가져감 | | `5` | 이번 시도는 프롬프트를 전달하지 않음: 세션이 바쁘거나 대상을 찾지 못함(잡은 `cancelled`), 또는 기다리는 동안 다른 시도가 레코드를 가져감(레코드는 새 소유자에게 그대로 둠) |
--- ---
@@ -157,7 +157,7 @@ sequenceDiagram
## 🔐 보안 ## 🔐 보안
- **서명된 이벤트**: 토큰을 직접 넘기거나 브로커가 TLS 또는 사용자 이름을 쓰면, 잡은 고유 토큰(`auth_token`)을 받습니다. 그때 모든 이벤트와 retained 상태 메시지에 그 토큰으로 만든 HMAC-SHA256 서명이 붙고, 구독자는 검증에 실패한 메시지를 버립니다. 이벤트와 상태 메시지에는 토큰이 아니라 서명만 실립니다. TLS 도 사용자 이름도 없는 브로커(PoC 구성)에서는 잡에 토큰이 없어 서명을 검사하지 않습니다. - **서명된 이벤트**: 토큰을 직접 넘기거나 브로커가 TLS 또는 사용자 이름을 쓰면, 잡은 고유 토큰(`auth_token`)을 받습니다. 그때 모든 이벤트와 retained 상태 메시지에 그 토큰으로 만든 HMAC-SHA256 서명이 붙고, 구독자는 검증에 실패한 메시지를 버립니다. 이벤트와 상태 메시지에는 토큰이 아니라 서명만 실립니다. 토큰을 넘기지 않았고 브로커에 TLS 도 사용자 이름도 없으면(PoC 구성), 잡에 토큰이 없어 서명을 검사하지 않습니다.
- **원격 디스패치에는 토큰이 실린다**: `remote:<name>` 디스패치 메시지에는 잡의 토큰이 들어갑니다(`MAM_DISPATCH_SECRET` 으로 서명될 뿐 암호화되지는 않음). 접근이 통제된 사설 브로커에서만 쓰십시오. - **원격 디스패치에는 토큰이 실린다**: `remote:<name>` 디스패치 메시지에는 잡의 토큰이 들어갑니다(`MAM_DISPATCH_SECRET` 으로 서명될 뿐 암호화되지는 않음). 접근이 통제된 사설 브로커에서만 쓰십시오.
- **재전송과 순서**: 이벤트마다 증가하는 `seq` 가 붙습니다. 구독자는 지금까지 본 것보다 큰 번호만 받고, 빈 번호는 `seq_gap` 으로 기록합니다. 타임스탬프는 참고용입니다. - **재전송과 순서**: 이벤트마다 증가하는 `seq` 가 붙습니다. 구독자는 지금까지 본 것보다 큰 번호만 받고, 빈 번호는 `seq_gap` 으로 기록합니다. 타임스탬프는 참고용입니다.
- **자격 증명**: 브로커 자격 증명은 `.mam.env` 에만 있고, 잡 레코드에는 없습니다. - **자격 증명**: 브로커 자격 증명은 `.mam.env` 에만 있고, 잡 레코드에는 없습니다.
@@ -172,7 +172,7 @@ sequenceDiagram
- 테스트는 약 1,900 개이고, 전체 실행에 한 시간 넘게 걸립니다. - 테스트는 약 1,900 개이고, 전체 실행에 한 시간 넘게 걸립니다.
- 프로세스 안에서 자체 MQTT 브로커를 띄우므로 `.mam.env` 가 필요 없습니다. 테스트용으로 `.mam.env` 를 export 하지 마십시오. - 프로세스 안에서 자체 MQTT 브로커를 띄우므로 `.mam.env` 가 필요 없습니다. 테스트용으로 `.mam.env` 를 export 하지 마십시오.
- 테스트는 임시 HOME·레지스트리·감사 로그 디렉터리와 mock herdr 를 쓰는 샌드박스에서 돕니다. - 모든 테스트는 임시 잡 레지스트리와 감사 로그 디렉터리를 씁니다. 대부분의 테스트는 임시 HOME 과 mock herdr 를 설정하는 샌드박스 픽스처도 씁니다.
- 실제 herdr 를 다루는 테스트는 일회용 herdr 서버를 직접 띄웁니다. - 실제 herdr 를 다루는 테스트는 일회용 herdr 서버를 직접 띄웁니다.
- 실행이 끝나면 세션 단위 가드가 실제 제어 평면과 사용자의 에이전트 홈(`~/.claude/projects`, `~/.codex`, `~/.gemini` …)을 확인하고, 테스트가 남긴 항목이 있으면 실행을 실패시킵니다. - 실행이 끝나면 세션 단위 가드가 실제 제어 평면과 사용자의 에이전트 홈(`~/.claude/projects`, `~/.codex`, `~/.gemini` …)을 확인하고, 테스트가 남긴 항목이 있으면 실행을 실패시킵니다.
- 일부 테스트는 플랫폼에 따라 다릅니다. Python 3.11+(`tomllib`), Linux(`/proc`), `nats-docker` 서브모듈이 필요한 테스트는 그 밖의 환경에서 건너뜁니다. - 일부 테스트는 플랫폼에 따라 다릅니다. Python 3.11+(`tomllib`), Linux(`/proc`), `nats-docker` 서브모듈이 필요한 테스트는 그 밖의 환경에서 건너뜁니다.
+5 -5
View File
@@ -35,7 +35,7 @@ bash deploy/install.sh
- checks that herdr and Python 3.9+ are installed, and that the system `python3` has PyYAML; - checks that herdr and Python 3.9+ are installed, and that the system `python3` has PyYAML;
- copies `.agents/` and `.mam.env.example` into the workspace; - copies `.agents/` and `.mam.env.example` into the workspace;
- creates `.venv` and installs `paho-mqtt` and PyYAML into it; - creates `.venv` and installs `paho-mqtt` and PyYAML into it;
- writes a starter `.mam.env` with `MQTT_BROKER=localhost`. An existing `.mam.env` is kept, and a legacy `.env` is renamed to `.mam.env`; - writes a starter `.mam.env` with `MQTT_BROKER=localhost`. An existing `.mam.env` is kept. If only a legacy `.env` exists, it is copied to `.mam.env` and the original stays;
- adds `.venv`, `.mam/` and `.mam.env` to `.gitignore`. - adds `.venv`, `.mam/` and `.mam.env` to `.gitignore`.
Point `MQTT_BROKER` in `.mam.env` at a broker you run (see [MESSAGING.md](MESSAGING.md) for a private broker). If `MQTT_BROKER` is not set at all, the code falls back to the public `broker.hivemq.com`. That is fine for a trial, but not for real work. `deploy/generate-env.sh` recreates `.mam.env` from `.mam.env.example`. `deploy/update.sh` and `deploy/remove.sh` update and uninstall. `deploy/install_mam.sh` is a separate installer that copies from a local clone into another directory. Point `MQTT_BROKER` in `.mam.env` at a broker you run (see [MESSAGING.md](MESSAGING.md) for a private broker). If `MQTT_BROKER` is not set at all, the code falls back to the public `broker.hivemq.com`. That is fine for a trial, but not for real work. `deploy/generate-env.sh` recreates `.mam.env` from `.mam.env.example`. `deploy/update.sh` and `deploy/remove.sh` update and uninstall. `deploy/install_mam.sh` is a separate installer that copies from a local clone into another directory.
@@ -89,7 +89,7 @@ MAM has two layers, each with its own registry:
- **A dedicated server per workspace.** By default `create` runs the agents on a herdr server named after the workspace (its slug), with its own socket, separate from your default herdr. `--herdr-session <name>` or `HERDR_SESSION_NAME` picks another name. The server name is recorded on the session row, and every later command routes to it. - **A dedicated server per workspace.** By default `create` runs the agents on a herdr server named after the workspace (its slug), with its own socket, separate from your default herdr. `--herdr-session <name>` or `HERDR_SESSION_NAME` picks another name. The server name is recorded on the session row, and every later command routes to it.
- **The shim.** Sourcing `lib.sh` writes `<workspace>/.mam/shim/herdr` and puts it first on `PATH`. The shim provides the tmux-style commands the scripts use (`new-session`, `has-session`, …) on top of herdr's agent API. For a named server it adds `--session <server>` to each call, and it starts that server headless if it is not running. It does not start one when `MAM_NO_AUTOSTART=1`, which `status` and `monitor` set, so looking never starts a server. Calls for the `default` server go to your ambient herdr unchanged. - **The shim.** Sourcing `lib.sh` writes `<workspace>/.mam/shim/herdr` and puts it first on `PATH`. The shim provides the tmux-style commands the scripts use (`new-session`, `has-session`, …) on top of herdr's agent API. For a named server it adds `--session <server>` to each call, and it starts that server headless if it is not running. It does not start one when `MAM_NO_AUTOSTART=1`, which `status` and `monitor` set, so looking never starts a server. Calls for the `default` server go to your ambient herdr unchanged.
- **No surprise relaunches.** A server MAM starts normally uses your herdr config plus `resume_agents_on_restore = false`. A herdr restart therefore does not relaunch saved agents without their permission flags. If that config cannot be built, the bootstrap prints a warning and the server starts with restore on. - **No surprise relaunches.** A server MAM starts normally uses your herdr config plus `resume_agents_on_restore = false`. A herdr restart therefore does not relaunch saved agents without their permission flags. With a Python that has no `tomllib` (such as macOS's 3.9), the file holds only that key, so restore stays off but your other herdr settings are not carried over; the bootstrap warns about this. Only if no config file can be written at all does the server start with restore on, again with a warning.
- **One pane claim at a time.** Pane placement holds a per-server lock (`lib_py/pane_lock.py`, `flock` on macOS and Linux). Concurrent `create` calls in one folder each get their own pane. - **One pane claim at a time.** Pane placement holds a per-server lock (`lib_py/pane_lock.py`, `flock` on macOS and Linux). Concurrent `create` calls in one folder each get their own pane.
### 🛡️ Registry writes and concurrency ### 🛡️ Registry writes and concurrency
@@ -117,7 +117,7 @@ The full specification is in [MESSAGING.md §2.5](MESSAGING.md).
| `1` | failure, or no `started` within T | | `1` | failure, or no `started` within T |
| `2` | no event for K×N (`deadline_timeout`) or budget spent (`wall_clock_timeout`) | | `2` | no event for K×N (`deadline_timeout`) or budget spent (`wall_clock_timeout`) |
| `3` | broker unavailable, and no terminal status on disk | | `3` | broker unavailable, and no terminal status on disk |
| `5` | not delivered (`cancelled`): the session was busy, the target was not found, or the record was taken over while waiting | | `5` | this attempt did not deliver the prompt: the session was busy or the target was not found (the job is `cancelled`), or another attempt took the record over while this one waited (the record is left to its new owner, unchanged) |
--- ---
@@ -157,7 +157,7 @@ sequenceDiagram
## 🔐 Security ## 🔐 Security
- **Signed events**: a job gets its own token (`auth_token`) when you pass one, or automatically when the broker uses TLS or a username. Each event and each retained state message then carries an HMAC-SHA256 signature made with it, and the subscriber drops anything that fails verification. Events and state messages carry only the signature, not the token. On a broker with neither TLS nor a username (the PoC setup), a job has no token and signatures are not checked. - **Signed events**: a job gets its own token (`auth_token`) when you pass one, or automatically when the broker uses TLS or a username. Each event and each retained state message then carries an HMAC-SHA256 signature made with it, and the subscriber drops anything that fails verification. Events and state messages carry only the signature, not the token. If no token is passed and the broker has neither TLS nor a username (the PoC setup), a job has no token and signatures are not checked.
- **Remote dispatch carries the token**: the `remote:<name>` dispatch message includes the job's token (signed with `MAM_DISPATCH_SECRET`, not encrypted). Use it only on an access-controlled private broker. - **Remote dispatch carries the token**: the `remote:<name>` dispatch message includes the job's token (signed with `MAM_DISPATCH_SECRET`, not encrypted). Use it only on an access-controlled private broker.
- **Replay and ordering**: each event carries an increasing `seq`. The subscriber accepts only sequence numbers higher than any it has seen and records gaps as `seq_gap`. Timestamps are informational only. - **Replay and ordering**: each event carries an increasing `seq`. The subscriber accepts only sequence numbers higher than any it has seen and records gaps as `seq_gap`. Timestamps are informational only.
- **Credentials**: broker credentials live only in `.mam.env`, never in a job record. - **Credentials**: broker credentials live only in `.mam.env`, never in a job record.
@@ -172,7 +172,7 @@ sequenceDiagram
- The suite has about 1,900 tests, and a full run takes over an hour. - The suite has about 1,900 tests, and a full run takes over an hour.
- It runs its own in-process MQTT broker, so it never needs `.mam.env`. Don't export `.mam.env` for tests. - It runs its own in-process MQTT broker, so it never needs `.mam.env`. Don't export `.mam.env` for tests.
- Tests run in sandboxes, with a temporary HOME, registry and audit-log directory, and a mock herdr: - Every test gets a temporary job registry and audit-log directory. Most tests also use the sandbox fixtures, which set a temporary HOME and a mock herdr:
- the tests that drive a real herdr start their own throwaway herdr server; - the tests that drive a real herdr start their own throwaway herdr server;
- after the run, session-level guards check the real control plane and your agent homes (`~/.claude/projects`, `~/.codex`, `~/.gemini`, …) and fail the run if a test left entries there. - after the run, session-level guards check the real control plane and your agent homes (`~/.claude/projects`, `~/.codex`, `~/.gemini`, …) and fail the run if a test left entries there.
- Some tests are platform-specific. A few need Python 3.11+ (`tomllib`), Linux (`/proc`) or the `nats-docker` submodule; elsewhere they are skipped. - Some tests are platform-specific. A few need Python 3.11+ (`tomllib`), Linux (`/proc`) or the `nats-docker` submodule; elsewhere they are skipped.