@agfpd/iapeer 0.4.109 → 0.4.111

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/docs/04-lifecycle-and-daemon.md +11 -0
  3. package/docs/06-storage.md +2 -1
  4. package/docs/07-install-and-update.md +3 -3
  5. package/docs/10-messaging.md +12 -0
  6. package/docs/12-cli-reference.md +1 -1
  7. package/docs/13-architecture-and-contract.md +44 -0
  8. package/docs/15-fleet-api.md +2 -2
  9. package/docs/18-origin-guard.md +2 -2
  10. package/docs/ru/04-/320/266/320/270/320/267/320/275/320/265/320/275/320/275/321/213/320/271-/321/206/320/270/320/272/320/273-/320/270-/320/264/320/265/320/274/320/276/320/275.md +11 -0
  11. package/docs/ru/06-/321/205/321/200/320/260/320/275/320/265/320/275/320/270/320/265.md +2 -1
  12. package/docs/ru/07-/321/203/321/201/321/202/320/260/320/275/320/276/320/262/320/272/320/260-/320/270-/320/276/320/261/320/275/320/276/320/262/320/273/320/265/320/275/320/270/320/265.md +3 -3
  13. package/docs/ru/10-/320/276/320/261/320/274/320/265/320/275-/321/201/320/276/320/276/320/261/321/211/320/265/320/275/320/270/321/217/320/274/320/270.md +12 -0
  14. package/docs/ru/12-cli-reference.md +1 -1
  15. package/docs/ru/13-/320/260/321/200/321/205/320/270/321/202/320/265/320/272/321/202/321/203/321/200/320/260-/320/270-/320/272/320/276/320/275/321/202/321/200/320/260/320/272/321/202.md +46 -0
  16. package/docs/ru/15-fleet-api.md +2 -2
  17. package/docs/ru/18-origin-guard.md +2 -2
  18. package/package.json +4 -2
  19. package/scripts/proof-install-signature.ts +189 -0
  20. package/src/cli/index.ts +25 -3
  21. package/src/daemon/fleet.ts +8 -1
  22. package/src/daemon/index.ts +1 -1
  23. package/src/install/index.ts +41 -16
  24. package/src/install/signature.ts +94 -0
  25. package/src/install/signing.ts +7 -7
  26. package/src/launch/adapters/claude.ts +7 -0
  27. package/src/launch/launchdRun.ts +1 -1
  28. package/src/launch/ptyHost.ts +1 -1
  29. package/src/lifecycle/index.ts +1 -1
  30. package/src/runtime/deploy.ts +21 -0
  31. package/src/runtime/signature.ts +35 -0
  32. package/src/runtime/update.ts +11 -4
  33. package/src/supervisor/daemon.ts +12 -1
  34. package/src/supervisor/fixtures/owned-stdio.ts +57 -0
  35. package/src/supervisor/processTree.ts +71 -0
  36. package/src/transport/attachments.ts +163 -0
  37. package/src/transport/index.ts +28 -4
  38. package/src/update/index.ts +21 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ### Fixed
14
+
15
+ - **Hosted hard teardown reclaims PTY-owned subprocesses, not only the runtime root.**
16
+ `self-fresh`, stop and reap snapshot ancestry before killing the runtime, including stdio MCP
17
+ launchers in separate process groups. Start-token and controlling-terminal checks exclude reused
18
+ PIDs, unrelated processes and detached/shared daemon branches. An isolated real-Codex proof and
19
+ hermetic regressions cover old-tree exit, fresh MCP calls and sentinel survival. Already-orphaned
20
+ processes and shared-backend thread shutdown remain outside this bounded cleanup.
21
+
22
+ - **macOS executable signatures are checked before activation.** Runtime packages
23
+ must strictly verify compiled and copied staged binaries, repair invalid trusted
24
+ build artifacts with ad-hoc signing, and re-verify before binary/manifest publication.
25
+ A lightweight `@agfpd/iapeer/install-signature` helper implements the gate. Foundation
26
+ install and rollback now sign/verify the staged inode before replacing the binary;
27
+ runtime install/update also verify installed launchers before provisioning/restart,
28
+ even when the package/version is already current. Foundation update also verifies
29
+ its installed binary before activation, including same-version stale-daemon healing.
30
+ Valid signatures are preserved;
31
+ failed repair never licenses activation of invalid code.
32
+
33
+ - **IAP attachments now transfer ownership instead of forwarding fragile source paths.**
34
+ Every `send_to_peer`, `iapeer send`, and Fleet API send copies regular-file sources
35
+ into the target peer's durable, content-addressed foundation inbox before an
36
+ envelope can be accepted. The recipient can read or forward the copy after the
37
+ sender deletes `/tmp` or otherwise removes the source. Copies live for the
38
+ recipient peer's lifetime and `iapeer remove` purges its inbox.
39
+
13
40
  ## [0.4.25] - 2026-06-22
14
41
 
15
42
  ### Fixed
@@ -98,4 +98,15 @@ So `stop`, `start`, `new` refuse to touch foreign launchd peers — for those th
98
98
  - **`iapeer interrupt`** — interrupt the current turn (Escape) without losing context.
99
99
  - **`iapeer self-fresh` / `self-done`** — an agent's self-calls: mark itself for a fresh restart, or quietly finish (for ephemeral workers).
100
100
 
101
+ ### Process ownership at teardown
102
+
103
+ Closing a hosted session is a **hard teardown**, not the MCP client's graceful stdin/EOF shutdown.
104
+ Before killing the runtime, the supervisor snapshots its descendants and kills those still attached
105
+ to its controlling PTY, including stdio MCP launchers in separate process groups. Each PID's start
106
+ token and terminal are re-checked before signalling; unrelated processes and detached daemon branches
107
+ are excluded. This also applies to `self-fresh`: finish work and persist the handoff before calling it.
108
+ Processes already orphaned before the snapshot, a concurrent fork/exit race, an unreadable process
109
+ table, and shared-backend threads outside the PTY are not covered by this cleanup. A shared runtime
110
+ daemon needs its own per-thread shutdown protocol; ancestry alone does not authorize killing it.
111
+
101
112
  Behavior by runtime class — [08 — Peer management](08-peer-management.md); ephemeral workers — [10 — Messaging](10-messaging.md).
@@ -26,6 +26,7 @@ The same plugin can store data at both levels: shared in `~/.iapeer/`, peer-boun
26
26
  │ └── .iapeer/ (peer-level layout — see below)
27
27
  ├── state/
28
28
  │ ├── iapeer/ daemon state (router.sock, router.json …)
29
+ │ │ └── attachments/ recipient-owned IAP attachment copies
29
30
  │ └── <plugin>/ a plugin's mutable state
30
31
  ├── logs/
31
32
  │ ├── iapeer/ daemon logs: lifecycle.log, delivery.log, exits.log
@@ -53,7 +54,7 @@ The same plugin can store data at both levels: shared in `~/.iapeer/`, peer-boun
53
54
  A few nodes worth highlighting:
54
55
 
55
56
  - **`peers-profiles.json`** — the registry. Written only through a single writer under the file lock `peers-profiles.lock`; direct editing is forbidden at the core level.
56
- - **`state/iapeer/`** — where the daemon lives: its local socket `router.sock` and config `router.json`.
57
+ - **`state/iapeer/`** — where the daemon lives: its local socket `router.sock`, config `router.json`, and `attachments/<recipient>/<sha256>/<basename>` inboxes. Attachment inboxes are durable recipient state (not cache); peer removal cleans that recipient's inbox.
57
58
  - **`logs/<name>/`** — an infrastructure peer's logs are placed by peer name at the shared level (not in the peer's folder), because it's the output of a launchd service.
58
59
  - **`runtimes/<runtime>/`** — dual ownership: the root belongs to the runtime itself (Telegram keeps bot tokens and poll state here), while the `plugins/<plugin>/` subfolder is the plugins' space, holding their config under this runtime.
59
60
  - **`docs/<package>/`** — on-host reference docs, one folder per ecosystem package (see below).
@@ -60,7 +60,7 @@ On a fresh macOS host the first install → onboard → peer launch surfaces a h
60
60
  **Three things you do (these cannot be automated):**
61
61
 
62
62
  1. **Log in to the runtimes — before onboard.** Sign in to Claude Code and Codex first. Their login screen is a browser OAuth flow that no headless peer can drive, so iapeer deliberately does **not** auto-answer it; a runtime that is installed but not signed in makes a peer fail its first wake loudly. `iapeer onboard` warns when it finds an unauthenticated runtime.
63
- 2. **Login keychain password — once, during install.** The install code-signs the binary with a stable local identity so Full Disk Access survives updates; macOS asks for your login keychain password the one time that identity is created. It is expected (the installer announces it). If declined, the binary still works — TCC prompts would just recur on updates.
63
+ 2. **Login keychain password — once, during install.** The install code-signs the staged binary with a stable local identity so Full Disk Access survives updates; macOS asks for your login keychain password the one time that identity is created. It is expected (the installer announces it). If declined, install may use a verified ad-hoc signature (TCC prompts may recur). Signature verification itself is mandatory: an unrepairable build aborts before replacing the installed binary.
64
64
  3. **Full Disk Access — a manual grant, no prompt.** macOS TCC cannot be set by any flag, env var, or script. Without it, a peer reading or writing a TCC-protected path (an iCloud Obsidian vault, Desktop, Documents, Downloads) silently fails with EPERM — no prompt, no hang. Grant it in **System Settings → Privacy & Security → Full Disk Access** (`~/.iapeer` itself is under `$HOME` and needs nothing); onboard prints the reminder.
65
65
 
66
66
  Gatekeeper does not gate the locally-built CLI run from a terminal (no quarantine attribute). Verified live on a truly-virgin Claude config (claude 2.1.183): the theme picker, folder-trust, the bypass-permissions accept (cursor stepped to "Yes"), and the project MCP-server approval all auto-clear in sequence, the peer reaches its input prompt, and a second launch on the same config is dialog-free; the Codex folder-trust / update auto-clearing likewise (an owner's clean-machine install + an isolated first-boot test).
@@ -78,7 +78,7 @@ A bare `iapeer update` updates the **whole host stack** in one command — the c
78
78
 
79
79
  **1. Core (foundation)** — and it ABORTS the cascade on a hard failure (never update runtimes onto a broken core):
80
80
 
81
- 1. Learns the target version from npm. If the installed one already matches and there's no `--force` — it does nothing.
81
+ 1. Learns the target version from npm. If the installed one already matches and there's no `--force`, it verifies the installed executable signature without repairing it; a valid, version-consistent daemon needs no restart. An invalid binary is refused, including before recovery of a stale live daemon.
82
82
  2. Downloads and builds the new binary via `npm pack`, not `npx` (the binary doesn't rebuild itself).
83
83
  3. Restarts the daemon onto the new binary.
84
84
  4. Restarts the infrastructure launchd jobs the core owns (`com.iapeer.*` — the `timer`, `watcher`, Telegram routers), moving them onto the just-replaced binary. It's the JOBS that restart, not the runtimes' package code — that's the next step. If the daemon didn't come up in step 3, this is skipped.
@@ -90,7 +90,7 @@ A bare `iapeer update` updates the **whole host stack** in one command — the c
90
90
 
91
91
  Runtimes and memory are **best-effort**: a component failure is reported and the rest still run; the command exits non-zero if anything failed. Agentic peers (Claude/Codex) pick up new core doctrine lazily on their next wake.
92
92
 
93
- `iapeer update --foundation-only` does just step 1 (the narrow case). A pinned `iapeer update <version>` is **core-only** by design — a version pin is core-specific (downgrade, or pin a proven version, deeper than one rollback step).
93
+ `iapeer update --foundation-only` does just the foundation leg, including infrastructure-job re-registration when the binary/daemon changes; it narrows package updates, not process restarts. A pinned `iapeer update <version>` has the same **core-only** scope — a version pin is core-specific (downgrade, or pin a proven version, deeper than one rollback step).
94
94
 
95
95
  > **Upgrading from before the cascade (< 0.4.2).** The cascade is driven by the *installed* binary, so the very first `iapeer update` from a pre-cascade version runs the OLD (foundation-only) logic: it upgrades the core to the cascade-capable binary but does not yet cascade runtimes + memory. Run `iapeer update` once more — now the new binary cascades the rest. This is a one-time bootstrap; from 0.4.2 onward it is a single command. (A fresh install lands on a cascade-capable binary directly, so it never needs the double run.)
96
96
 
@@ -23,6 +23,18 @@ iapeer send assistant --message "..." --topic "release" --from claude-reviewer
23
23
 
24
24
  The `--from` flag sets the sender (`<runtime>-<name>`); by default it's the identity of the current folder's peer. `--attachment` is repeatable. `--message-file`, with the body from a file or stdin, is handy for long, multi-line messages.
25
25
 
26
+ ## Attachment ownership
27
+
28
+ An attachment argument is a **source path**, not the delivered path. `routeSend` opens every source as a regular file and copies its bytes into the target peer's foundation-owned inbox before it builds the envelope:
29
+
30
+ ```text
31
+ ~/.iapeer/state/iapeer/attachments/<recipient>/<sha256>/<basename>
32
+ ```
33
+
34
+ The envelope contains these absolute inbox paths. Therefore an `ok`/`delivered`/`queued` result also means that every attachment already has a readable recipient copy; the sender may immediately delete or rename its source. A missing, unreadable, or non-regular source fails the send before delivery. Re-sending an inbox file to a third peer makes another copy in that third peer's inbox.
35
+
36
+ The copy is recipient-owned state, not cache: it is not age-GCed and lives for the recipient peer's lifetime. `iapeer remove <recipient>` removes the complete inbox. Removing the sender, cleaning `/tmp`, restarting the daemon, draining an ephemeral queue, and bridge history rotation do not remove it.
37
+
26
38
  ## The delivery path
27
39
 
28
40
  The daemon finds the recipient in the registry and selects its endpoint first. With no explicit `runtime`, the endpoint is always the recipient's `default_runtime`; liveness of other declared runtimes does not change that choice. An explicit `runtime` selects that exact surface. The daemon then looks at the selected endpoint's state:
@@ -61,7 +61,7 @@ The full list of `iapeer <command>`. General rules: `--help` / `-h` anywhere in
61
61
  | `send <target> (--message <text> \| --message-file <f\|->) [--from <id>] [--runtime <r>] [--attachment <path>]… [--topic <t>]` | Manual IAP send over the same path as `send_to_peer`. |
62
62
  | `confirm-send <id> [--runtime <rt>]` | Deliver a held send ([origin guard](18-origin-guard.md)): as addressed, or `--runtime` redirects it to the origin channel. No re-send of the message text. |
63
63
 
64
- `--message-file -` reads the body from stdin. `--attachment` is repeatable. `--from` defaults to the current folder's peer identity.
64
+ `--message-file -` reads the body from stdin. `--attachment` is repeatable and names an absolute source file; the router copies it into recipient-owned state before accepting the send. `--from` defaults to the current folder's peer identity.
65
65
 
66
66
  ## Launch and runtimes
67
67
 
@@ -13,6 +13,7 @@ The system's behavior rests on these:
13
13
  - **A single registry writer.** `peers-profiles.json` is written only through one writer under a file lock. Direct writing of the file is forbidden at the core level — this rules out races.
14
14
  - **Liveness is defined by a peer's live session.** A peer is alive if and only if it has a live session under the supervisor. This isn't a separate state flag that can desync, but an observable fact.
15
15
  - **`delivered` ⟺ landed in a live session.** Delivery success means the message was accepted by a live session (the daemon waited for confirmation). If it didn't confirm — the daemon treats the session as dead rather than lying about success.
16
+ - **Attachments are copied before routing.** Caller paths are only sources. Before an envelope can be accepted as delivered or queued, `routeSend` copies every attachment into the target peer's foundation-owned inbox and puts those copy paths into the envelope. A later delete, rename, or cleanup of the caller's source cannot break the received attachment. The copies live for the lifetime of the recipient peer and `iapeer remove <peer>` removes that inbox.
16
17
  - **The source of truth is the local profile.** The registry is a reprojectable projection of the peer profiles. On a divergence, the profile wins.
17
18
  - **The binary is installed on the host.** The daemon runs from the installed `~/.local/bin/iapeer`, not from a source working tree. An update is replacing the binary and restarting the daemon onto it.
18
19
 
@@ -82,6 +83,49 @@ Commands like `prepare`, `interface`, `bot`, `doctor` are the package's own inte
82
83
  - **`install-runtime <runtime>`** determines the npm package (from the built-in registry or the `--package` flag), installs it via `npx`, then provisions the declared peer set.
83
84
  - **`update-runtime <runtime>`** compares the manifest version with npm; when behind, it reinstalls, re-provisions, and restarts the runtime's peers with the standard `stop`/`start`.
84
85
 
86
+ ### Executable signature before activation (macOS)
87
+
88
+ A successful Bun compile or executable copy does not guarantee a valid Mach-O
89
+ signature. Each package owns this gate on **every** self-install path (compile and
90
+ copy-self), including direct `npx` installs outside the foundation:
91
+
92
+ 1. Build/copy to a sibling temporary file and set its executable permissions.
93
+ 2. Run `/usr/bin/codesign --verify --deep --strict <tmp>`. Preserve a valid signature
94
+ unchanged, including certificate-backed signatures used for stable TCC grants.
95
+ 3. For an invalid/unsigned **installer-owned build artifact**, repair with
96
+ `/usr/bin/codesign --force --sign - <tmp>` and repeat strict verification.
97
+ 4. Only after verification succeeds, atomically rename the file over the installed
98
+ binary and publish the new manifest. On failure, remove the temporary file, exit
99
+ non-zero, and leave the previous binary and manifest unchanged.
100
+
101
+ The optional lightweight helper `ensureExecutableSignature` is exported from
102
+ `@agfpd/iapeer/install-signature`; a package may implement the same contract locally.
103
+ It throws on verification/repair failure, skips shebang script launchers and non-macOS
104
+ hosts, and supports injected command runners for hermetic tests. Its repair mode is
105
+ for trusted installer artifacts, **not** a general authenticity check or a way to
106
+ trust arbitrary modified installed code. Stable certificate signing for TCC is a
107
+ separate concern; signature validity is fail-closed even if stable signing is soft.
108
+
109
+ The foundation additionally verifies installed runtime launchers before provisioning
110
+ or restart, including already-installed/already-latest paths. This secondary gate
111
+ is verify-only: it reports invalid signatures rather than re-signing live binaries.
112
+ It cannot replace the package's pre-rename gate, since launchd may reopen a binary
113
+ as soon as the package publishes it.
114
+
115
+ Foundation install and rollback apply the same staged validity gate before binary
116
+ replacement. Stable local-certificate signing happens on the staged inode; a
117
+ certificate-signing failure can fall back to a verified ad-hoc signature, but an
118
+ unrepairable executable is never activated. Repair is not guaranteed for malformed
119
+ Bun output: a strict-validation failure aborts safely and requires a working build.
120
+ The update orchestrator also verify-only checks the installed foundation binary before
121
+ daemon activation, including the same-version stale-daemon recovery path.
122
+
123
+ The shipped `scripts/proof-install-signature.ts` exercises the actual package CLI
124
+ installer and native codesign in disposable paths (requires the existing local signing
125
+ identity). It includes refusal/state preservation, verify-only runtime gates and
126
+ rollback. Update lifecycle calls are injected: this proof never restarts the host daemon
127
+ and is not a deployed-health acceptance.
128
+
85
129
  ## The capability-plugin standard
86
130
 
87
131
  A capability plugin adds an ability to a peer without being a runtime. The distinction is clear: a runtime is a peer's mode of presence, a plugin is an ability on top of a runtime.
@@ -203,10 +203,10 @@ What the headline states look like on the stream: a peer **waking** is `ev=wake`
203
203
  `POST /fleet/v1/send` — deliver a message through the same routing path as the `send_to_peer` tool (wake-on-miss, queueing, delivery log — identical semantics):
204
204
 
205
205
  ```json
206
- { "personality": "boris", "message": "…", "runtime": "claude", "topic": "…", "from": "nova" }
206
+ { "personality": "boris", "message": "…", "runtime": "claude", "topic": "…", "attachments": ["/tmp/report.pdf"], "from": "nova" }
207
207
  ```
208
208
 
209
- `from` is optional: a full `<runtime>-<personality>`, or a bare personality (its default runtime). Default: the **single natural-intelligence peer** of the registry — a GUI-originated message is the human's. With zero or several natural peers the request is refused (`400`) with an instruction to pass `from`.
209
+ `from` is optional: a full `<runtime>-<personality>`, or a bare personality (its default runtime). Default: the **single natural-intelligence peer** of the registry — a GUI-originated message is the human's. With zero or several natural peers the request is refused (`400`) with an instruction to pass `from`. `attachments`, when present, is the same array of absolute source paths as the MCP tool; the shared `routeSend` path copies them into the recipient inbox before acceptance.
210
210
 
211
211
  Not part of the *command* API, deliberately: **attach** (a terminal handoff — a client opens the system terminal with `iapeer attach <peer>`), registry/host **admin verbs** (`create`/`remove`/`update`/`uninstall` — will be added when a client actually needs them). The **human-approval broker** now lives under `/fleet/v1/approvals*` (`GET` the queue / one item, `POST /<id>/(approve|deny)`) with the `approval-request` / `approval-resolved` SSE `ev` kinds and the `approvals` snapshot field above — the full contract is **docs/17-approval**. The **notice board** lives under `/fleet/v1/notices*` (`GET` the board / one item — **read-only**: a notice is one-way and has nothing to resolve) with the `notice-raised` SSE `ev` kind and the `notices` snapshot field above — the full contract is **docs/19-notices**.
212
212
 
@@ -30,7 +30,7 @@ verbatim as og-1a2b3c4d (expires in 15 min).
30
30
  Do not re-send the message text — confirm-send delivers the held message.
31
31
  ```
32
32
 
33
- Both exits cost one shell command and **zero message regeneration**: the held send carries the full original input (message, topic, attachments).
33
+ Both exits cost one shell command and **zero message regeneration**: the held send carries the original message/topic/runtime and recipient-owned copies of every attachment. Copying happens before the hold is persisted, so deleting a caller's temporary sources during the 15-minute decision window cannot break `confirm-send`.
34
34
 
35
35
  ```bash
36
36
  iapeer confirm-send og-1a2b3c4d # deliver as addressed (intentional cross-channel)
@@ -48,7 +48,7 @@ State is on disk (it must survive daemon restarts and short-lived CLI processes)
48
48
  ```text
49
49
  ~/.iapeer/state/iapeer/origin/
50
50
  state.json # (agent → human → {rt, inboundTs, answeredTs}) — last-wins, atomic writes
51
- pending/<id>.json # held sends, verbatim; lazily GC'd past the pending TTL
51
+ pending/<id>.json # held sends; attachment fields already name recipient-inbox copies
52
52
  ```
53
53
 
54
54
  ## Scope and non-interference
@@ -98,4 +98,15 @@
98
98
  - **`iapeer interrupt`** — прервать текущий ход (Escape), не теряя контекст.
99
99
  - **`iapeer self-fresh` / `self-done`** — самовызовы агента: пометить себя на свежий перезапуск или тихо завершиться (для эфемерных воркеров).
100
100
 
101
+ ### Владение процессами при закрытии сессии
102
+
103
+ Закрытие управляемой сессии — **жёсткое завершение**, а не протокол мягкого закрытия stdin/EOF
104
+ клиентом MCP. До остановки рантайма супервизор снимает дерево его потомков и завершает тех,
105
+ кто ещё принадлежит его управляющему PTY, включая stdio MCP в отдельных группах процессов.
106
+ Перед сигналом повторно проверяются время рождения PID и терминал; чужие процессы и ветви
107
+ отсоединённых демонов исключены. Это относится и к `self-fresh`: работу и хендофф завершают до самовызова.
108
+ Уже осиротевшие до снимка процессы, гонка параллельного порождения/выхода, недоступная таблица
109
+ процессов и потоки общего бэкенда вне PTY этой очисткой не покрываются. Общему демону рантайма
110
+ нужен собственный протокол закрытия отдельного потока; одно лишь родство не разрешает убивать его.
111
+
101
112
  Поведение по классам рантаймов — [08 — Управление пирами](08-управление-пирами.md); эфемерные воркеры — [10 — Обмен сообщениями](10-обмен-сообщениями.md).
@@ -26,6 +26,7 @@
26
26
  │ └── .iapeer/ (раскладка уровня пира — см. ниже)
27
27
  ├── state/
28
28
  │ ├── iapeer/ состояние демона (router.sock, router.json …)
29
+ │ │ └── attachments/ копии IAP-вложений, принадлежащие получателям
29
30
  │ └── <плагин>/ изменяемое состояние плагина
30
31
  ├── logs/
31
32
  │ ├── iapeer/ журналы демона: lifecycle.log, delivery.log, exits.log
@@ -53,7 +54,7 @@
53
54
  Несколько узлов стоит выделить:
54
55
 
55
56
  - **`peers-profiles.json`** — реестр. Пишется только через единственного писателя под файловой блокировкой `peers-profiles.lock`; прямое редактирование запрещено на уровне ядра.
56
- - **`state/iapeer/`** — здесь живёт демон: его локальный сокет `router.sock` и конфигурация `router.json`.
57
+ - **`state/iapeer/`** — здесь живёт демон: его локальный сокет `router.sock`, конфигурация `router.json` и inbox-каталоги `attachments/<получатель>/<sha256>/<имя-файла>`. Inbox — долгоживущее состояние получателя, а не кэш; удаление пира очищает его inbox.
57
58
  - **`logs/<имя>/`** — журналы инфраструктурных пиров кладутся по имени пира на общем уровне (а не в папке пира), потому что это вывод службы под launchd.
58
59
  - **`runtimes/<рантайм>/`** — двойное владение: корень принадлежит самому рантайму (Telegram держит здесь токены ботов, состояние опроса), а подпапка `plugins/<плагин>/` — это пространство плагинов, где лежит их конфиг под этим рантаймом.
59
60
  - **`docs/<пакет>/`** — справочные доки на хосте, по одной папке на каждый пакет экосистемы (см. ниже).
@@ -60,7 +60,7 @@ iapeer status
60
60
  **Три пункта на тебе (автоматизировать нельзя):**
61
61
 
62
62
  1. **Залогинься в рантаймы — ДО onboard.** Войди в Claude Code и Codex заранее. Их экран логина — браузерный OAuth, который headless-пир не пройдёт, поэтому iapeer НАМЕРЕННО его не авто-отвечает; установленный, но не залогиненный рантайм роняет первый wake пира громко. `iapeer onboard` предупреждает, если найдёт неаутентифицированный рантайм.
63
- 2. **Пароль логин-связки — один раз, при install.** Install подписывает бинарь стабильной локальной идентичностью, чтобы Full Disk Access пережил обновления; macOS один раз спросит пароль логин-связки при создании этой идентичности. Это ожидаемо (инсталлер анонсирует). При отказе бинарь всё равно работает — просто TCC-промпты будут повторяться на апдейтах.
63
+ 2. **Пароль логин-связки — один раз, при install.** Install подписывает временный бинарь стабильной локальной идентичностью, чтобы Full Disk Access пережил обновления; macOS один раз спросит пароль логин-связки при создании этой идентичности. Это ожидаемо (инсталлер анонсирует). При отказе возможна проверенная ad-hoc-подпись (TCC-промпты могут повторяться). Проверка валидности обязательна: неисправимая сборка прерывает установку до замены установленного бинаря.
64
64
  3. **Full Disk Access — ручной грант, без промпта.** macOS TCC нельзя выставить ни флагом, ни env, ни скриптом. Без него пир, читающий/пишущий TCC-защищённый путь (iCloud Obsidian vault, Desktop, Documents, Downloads), тихо падает с EPERM — без промпта, без зависания. Выдай в **System Settings → Privacy & Security → Full Disk Access** (`~/.iapeer` сам под `$HOME` и ничего не требует); onboard напомнит.
65
65
 
66
66
  Gatekeeper не гейтит локально собранный CLI, запускаемый из терминала (нет quarantine-атрибута). Проверено живьём на по-настоящему виргин-конфиге Claude (claude 2.1.183): theme-picker, folder-trust, принятие bypass-permissions (курсор шагнут на «Yes») и аппрув проектных MCP-серверов авто-снимаются по очереди, пир доходит до строки ввода, а повторный запуск на том же конфиге — уже без диалогов; codex folder-trust / update — аналогично (clean-install владельца + изолированный first-boot тест).
@@ -78,7 +78,7 @@ iapeer update --force # переустановить, даже есл
78
78
 
79
79
  **1. Ядро (фундамент)** — и оно ПРЕРЫВАЕТ каскад при жёсткой ошибке (не обновлять рантаймы на сломанное ядро):
80
80
 
81
- 1. Узнаёт нужную версию из npm. Если установленная уже совпадает и нет `--force` — ничего не делает.
81
+ 1. Узнаёт нужную версию из npm. Если установленная уже совпадает и нет `--force`, проверяет подпись установленного бинарника без переподписания; валидный демон с актуальной версией не требует рестарта. Невалидный бинарник отклоняется, в том числе до восстановления демона со старой версией.
82
82
  2. Скачивает и собирает новый бинарь через `npm pack`, а не `npx` (бинарь не пересобирает себя сам).
83
83
  3. Перезапускает демон на новый бинарь.
84
84
  4. Перезапускает launchd-джобы инфраструктуры, которыми владеет ядро (`com.iapeer.*` — роутеры `timer`, `watcher`, Telegram), переводя их на заменённый бинарь. Перезапускаются именно ДЖОБЫ, а не пакетный код рантаймов — это следующий шаг. Если демон на шаге 3 не поднялся, шаг пропускается.
@@ -90,7 +90,7 @@ iapeer update --force # переустановить, даже есл
90
90
 
91
91
  Рантаймы и память — **best-effort**: ошибка компонента сообщается, остальные всё равно отрабатывают; код возврата ненулевой, если что-то упало. Агентские пиры (Claude/Codex) подхватывают новую доктрину ядра лениво на следующем пробуждении.
92
92
 
93
- `iapeer update --foundation-only` делает только шаг 1 (узкий случай). Запиненный `iapeer update <версия>` — **только ядро** by design: пин версии специфичен для ядра (откат глубже одного шага или фиксация проверенной версии).
93
+ `iapeer update --foundation-only` выполняет только foundation-плечо, включая перерегистрацию инфраструктурных jobs при изменении бинарника/демона: сужается обновление пакетов, а не область рестартов процессов. Запиненный `iapeer update <версия>` имеет ту же область **только ядро**: пин версии специфичен для ядра (откат глубже одного шага или фиксация проверенной версии).
94
94
 
95
95
  > **Обновление с версии до каскада (< 0.4.2).** Каскад исполняет *установленный* бинарь, поэтому самый первый `iapeer update` со старой версии работает по СТАРОЙ (foundation-only) логике: он поднимает ядро до каскад-способного бинаря, но рантаймы + память пока не каскадит. Запусти `iapeer update` ещё раз — теперь новый бинарь докаскадит остальное. Это разовый bootstrap; начиная с 0.4.2 — одна команда. (Свежая установка сразу ставит каскад-способный бинарь, поэтому двойной прогон ей не нужен.)
96
96
 
@@ -23,6 +23,18 @@ iapeer send assistant --message "..." --topic "релиз" --from claude-reviewe
23
23
 
24
24
  Флаг `--from` задаёт отправителя (`<рантайм>-<имя>`); по умолчанию — идентичность пира текущей папки. `--attachment` повторяемый. `--message-file` с телом из файла или stdin удобен для длинных и многострочных сообщений.
25
25
 
26
+ ## Владение вложениями
27
+
28
+ Аргумент вложения — **путь источника**, а не доставленный путь. До сборки конверта `routeSend` открывает каждый источник как обычный файл и копирует его байты в foundation-owned inbox целевого пира:
29
+
30
+ ```text
31
+ ~/.iapeer/state/iapeer/attachments/<получатель>/<sha256>/<имя-файла>
32
+ ```
33
+
34
+ В конверт попадают абсолютные пути этих копий. Поэтому исход `ok`/`delivered`/`queued` одновременно значит, что каждое вложение уже имеет читаемую копию получателя; отправитель может сразу удалить или переименовать исходник. Отсутствующий, нечитаемый или необычный файл валит отправку до доставки. Пересылка inbox-файла третьему пиру создаёт отдельную копию в inbox третьего пира.
35
+
36
+ Копия — состояние получателя, а не кэш: по возрасту не удаляется и живёт столько же, сколько пир-получатель. `iapeer remove <получатель>` удаляет весь inbox. Удаление отправителя, очистка `/tmp`, рестарт демона, дренаж ephemeral-очереди и ротация истории моста её не удаляют.
37
+
26
38
  ## Путь доставки
27
39
 
28
40
  Демон находит получателя в реестре и сначала выбирает поверхность доставки. Без явного `runtime` это всегда `default_runtime` адресата; живость других объявленных рантаймов выбор не меняет. Явный `runtime` выбирает ровно указанную поверхность. Затем демон смотрит на состояние выбранной поверхности:
@@ -61,7 +61,7 @@
61
61
  | `send <цель> (--message <текст> \| --message-file <f\|->) [--from <id>] [--runtime <r>] [--attachment <путь>]… [--topic <t>]` | Ручная отправка IAP по тому же пути, что `send_to_peer`. |
62
62
  | `confirm-send <id> [--runtime <rt>]` | Доставить удержанную отправку ([origin guard](18-origin-guard.md)): как адресована, либо `--runtime` перенаправляет её в канал-источник. Без повторной отправки текста. |
63
63
 
64
- `--message-file -` читает тело из stdin. `--attachment` повторяемый. `--from` по умолчанию — идентичность пира текущей папки.
64
+ `--message-file -` читает тело из stdin. `--attachment` повторяемый и задаёт абсолютный путь файла-источника; до принятия отправки роутер копирует его в состояние получателя. `--from` по умолчанию — идентичность пира текущей папки.
65
65
 
66
66
  ## Запуск и рантаймы
67
67
 
@@ -13,6 +13,7 @@
13
13
  - **Единственный писатель реестра.** `peers-profiles.json` пишется только через одного писателя под файловой блокировкой. Прямая запись файла запрещена на уровне ядра — это исключает гонки.
14
14
  - **Живость определяется live-сессией пира.** Пир жив тогда и только тогда, когда у него есть живая сессия под супервизором. Это не отдельный флаг в состоянии, который можно рассинхронизировать, а наблюдаемый факт.
15
15
  - **`доставлено` ⟺ легло в живую сессию.** Успех доставки означает, что сообщение принято живой сессией (демон дождался подтверждения). Не подтвердила — демон считает сессию мёртвой, а не лжёт об успехе.
16
+ - **Вложения копируются до маршрутизации.** Пути вызывающего — только источники. Прежде чем конверт может быть принят как delivered или queued, `routeSend` копирует каждое вложение в принадлежащий foundation inbox целевого пира и вкладывает в конверт уже пути копий. Последующие удаление, переименование или очистка исходника не ломают полученное вложение. Копии живут столько же, сколько пир-получатель; `iapeer remove <пир>` удаляет его inbox.
16
17
  - **Источник истины — локальный профиль.** Реестр — пересобираемая проекция профилей пиров. При расхождении прав профиль.
17
18
  - **Бинарь установлен на хост.** Демон работает с установленного `~/.local/bin/iapeer`, а не из рабочего дерева исходников. Обновление — это замена бинаря и перезапуск демона на него.
18
19
 
@@ -82,6 +83,51 @@
82
83
  - **`install-runtime <рантайм>`** определяет npm-пакет (по встроенному реестру или флагу `--package`), ставит его через `npx`, затем разворачивает объявленный набор пиров.
83
84
  - **`update-runtime <рантайм>`** сверяет версию манифеста с npm; при отставании переустанавливает, заново провизионирует и перезапускает пиров рантайма штатными `stop`/`start`.
84
85
 
86
+ ### Подпись исполняемого файла до активации (macOS)
87
+
88
+ Успешная компиляция Bun или копирование исполняемого файла не гарантирует валидную
89
+ Mach-O-подпись. Пакет проверяет её на **каждом** пути self-install (compile и
90
+ copy-self), включая прямую установку через `npx` вне ядра:
91
+
92
+ 1. Собрать/скопировать файл во временный соседний файл, установить права исполнения.
93
+ 2. Выполнить `/usr/bin/codesign --verify --deep --strict <tmp>`. Валидную подпись
94
+ сохранить без изменений, включая сертификатную подпись для стабильных TCC-грантов.
95
+ 3. Для невалидного/неподписанного **артефакта сборки, принадлежащего установщику**
96
+ выполнить `/usr/bin/codesign --force --sign - <tmp>` и повторить строгую проверку.
97
+ 4. Только после успешной проверки атомарно заменить установленный бинарь и записать
98
+ новый манифест. При ошибке удалить временный файл, завершиться ненулевым кодом и
99
+ оставить прежние бинарь и манифест неизменными.
100
+
101
+ Необязательный лёгкий helper `ensureExecutableSignature` экспортирован из
102
+ `@agfpd/iapeer/install-signature`; пакет может реализовать тот же контракт локально.
103
+ Он бросает исключение при неуспехе проверки/исправления, пропускает скриптовые лаунчеры с shebang
104
+ и хосты вне macOS, поддерживает внедряемый runner для герметичных тестов. Исправление
105
+ предназначено для доверенных артефактов установщика, **не** для проверки подлинности
106
+ или легитимации произвольно изменённого установленного кода. Стабильная сертификатная
107
+ подпись для TCC — отдельный вопрос; валидность подписи проверяется fail-closed даже
108
+ при мягком отказе стабильного подписания.
109
+
110
+ Ядро дополнительно проверяет установленные лаунчеры рантайма перед провижном или
111
+ рестартом, включая ветки уже установленного пакета и актуальной версии. Вторичный
112
+ гейт только проверяет: он сообщает о невалидной подписи, не переподписывая живые
113
+ бинарники. Он не заменяет проверку пакетом до rename: launchd может открыть бинарь
114
+ сразу после его публикации установщиком.
115
+
116
+ Установка и откат ядра применяют тот же гейт к staged-файлу до замены бинаря.
117
+ Стабильное подписывание локальным сертификатом выполняется на временном inode;
118
+ его отказ допускает переход к проверенной ad-hoc-подписи, но неисправимый файл
119
+ никогда не активируется. Исправление произвольно дефектного вывода Bun не
120
+ гарантируется: ошибка строгой валидации безопасно прерывает установку и требует
121
+ рабочей сборки. Оркестратор update также проверяет установленный бинарник ядра без
122
+ переподписания перед активацией демона, включая восстановление старого демона при
123
+ совпадающей версии бинарника.
124
+
125
+ Поставляемый `scripts/proof-install-signature.ts` проверяет штатный CLI-установщик
126
+ самого пакета и настоящий codesign в одноразовых каталогах (нужна существующая локальная
127
+ подписывающая идентичность). Он покрывает отказ и сохранение состояния, вторичные гейты
128
+ рантаймов и rollback. Lifecycle-вызовы update заменены изолированными зависимостями:
129
+ проверка не перезапускает демон хоста и не является приёмкой здоровья после выкатки.
130
+
85
131
  ## Стандарт capability-плагина
86
132
 
87
133
  Capability-плагин добавляет пиру способность, не будучи рантаймом. Отличие чёткое: рантайм — способ присутствия пира, плагин — способность поверх рантайма.
@@ -133,10 +133,10 @@ GET /fleet/v1/events?replay=50
133
133
  `POST /fleet/v1/send` — доставить сообщение тем же маршрутом, что инструмент `send_to_peer` (wake-on-miss, очереди, delivery-лог — семантика идентична):
134
134
 
135
135
  ```json
136
- { "personality": "boris", "message": "…", "runtime": "claude", "topic": "…", "from": "nova" }
136
+ { "personality": "boris", "message": "…", "runtime": "claude", "topic": "…", "attachments": ["/tmp/report.pdf"], "from": "nova" }
137
137
  ```
138
138
 
139
- `from` опционален: полный `<runtime>-<personality>` или голая personality (её default-рантайм). По умолчанию — **единственный natural-intelligence пир** реестра: сообщение из GUI принадлежит человеку. При нуле или нескольких natural-пирах запрос отклоняется (`400`) с указанием передать `from`.
139
+ `from` опционален: полный `<runtime>-<personality>` или голая personality (её default-рантайм). По умолчанию — **единственный natural-intelligence пир** реестра: сообщение из GUI принадлежит человеку. При нуле или нескольких natural-пирах запрос отклоняется (`400`) с указанием передать `from`. `attachments`, если передан, — тот же массив абсолютных путей источников, что и у MCP-инструмента; общий путь `routeSend` копирует их в inbox получателя до принятия.
140
140
 
141
141
  Не входит в *командный* API, намеренно: **attach** (передача терминала — клиент открывает системный терминал с `iapeer attach <peer>`), реестровые/хостовые **admin-verb'ы** (`create`/`remove`/`update`/`uninstall` — добавятся, когда появится клиент с реальной потребностью). **Брокер human-approval** теперь живёт под `/fleet/v1/approvals*` (`GET` очередь / один item, `POST /<id>/(approve|deny)`) с видами SSE `approval-request` / `approval-resolved` и полем `approvals` в снапшоте выше — полный контракт в **docs/17-approval**.
142
142
 
@@ -30,7 +30,7 @@ verbatim as og-1a2b3c4d (expires in 15 min).
30
30
  Do not re-send the message text — confirm-send delivers the held message.
31
31
  ```
32
32
 
33
- Оба выхода стоят одну shell-команду и **ноль регенерации сообщения**: удержанная отправка несёт полный исходный ввод (текст, топик, вложения).
33
+ Оба выхода стоят одну shell-команду и **ноль регенерации сообщения**: удержанная отправка несёт исходные текст/топик/рантайм и принадлежащие получателю копии каждого вложения. Копирование идёт до сохранения hold, поэтому удаление временных исходников в 15-минутном окне решения не ломает `confirm-send`.
34
34
 
35
35
  ```bash
36
36
  iapeer confirm-send og-1a2b3c4d # доставить как адресовано (кросс-канал намеренный)
@@ -48,7 +48,7 @@ iapeer confirm-send og-1a2b3c4d --runtime web # доставить то же
48
48
  ```text
49
49
  ~/.iapeer/state/iapeer/origin/
50
50
  state.json # (агент → человек → {rt, inboundTs, answeredTs}) — last-wins, атомарные записи
51
- pending/<id>.json # удержанные отправки, дословно; ленивый GC после TTL
51
+ pending/<id>.json # удержанные отправки; вложения уже указывают на копии inbox
52
52
  ```
53
53
 
54
54
  ## Границы и невмешательство
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.4.109",
3
+ "version": "0.4.111",
4
4
  "description": "Run Claude Code and Codex CLI as local background agents that message each other, wake on demand, and share one memory.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "agfpd",
@@ -34,6 +34,7 @@
34
34
  "src",
35
35
  "bin",
36
36
  "docs",
37
+ "scripts/proof-install-signature.ts",
37
38
  "CHANGELOG.md",
38
39
  "tsconfig.json",
39
40
  "!src/**/*.test.ts",
@@ -45,7 +46,8 @@
45
46
  "./storage": "./src/storage/index.ts",
46
47
  "./identity": "./src/identity/index.ts",
47
48
  "./registry": "./src/registry/index.ts",
48
- "./codec": "./src/codec/index.ts"
49
+ "./codec": "./src/codec/index.ts",
50
+ "./install-signature": "./src/install/signature.ts"
49
51
  },
50
52
  "scripts": {
51
53
  "test": "IAPEER_TEST_SANDBOX=1 bun test",