@agfpd/iapeer 0.4.74 → 0.4.75
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 +2 -2
- package/docs/16-tray.md +8 -3
- package/docs/ru/16-tray.md +8 -3
- package/package.json +1 -1
- package/src/launch/index.ts +20 -7
- package/src/tray/render.ts +17 -0
package/README.md
CHANGED
|
@@ -107,6 +107,8 @@ What this means for you:
|
|
|
107
107
|
- You are trusting the model's judgment, and the doctrine you give each peer, the same way you would trust a developer with a shell on your account.
|
|
108
108
|
- iapeer does **not** silently weaken your own interactive `claude` / `codex` setup. The bypass acceptance is recorded per peer, in that agent's own config; the project-MCP pre-approval is written **project-locally**, under the peer's folder — never to your global user settings.
|
|
109
109
|
|
|
110
|
+
**Want a human in the loop for a given peer?** Set its approval mode to `gated` — `iapeer approval-mode <peer> gated`. That peer launches **without** the bypass flag, and every blocking approval its runtime would raise is routed to you — a Telegram button, a click in the tray, or `iapeer approve` on the CLI — before the tool runs; your `allow`/`deny` (with a reason) flows back and the tool proceeds or is refused. The default stays `yolo` (bypass) for every peer; `gated` is opt-in, per peer. See [docs/17 — Human approval](docs/17-approval.md).
|
|
111
|
+
|
|
110
112
|
Run iapeer on a host you control, give each peer only the access it needs, and treat a peer's instructions as security-relevant. `iapeer onboard` states this at setup; it's repeated here so the trade-off is explicit before you create your first agent. Details on every first-run dialog: [docs/07 — Install & update](docs/07-install-and-update.md#first-run-dialogs-on-a-clean-machine).
|
|
111
113
|
|
|
112
114
|
## Documentation
|
|
@@ -121,8 +123,6 @@ Planned, not yet built:
|
|
|
121
123
|
- **Linux support** — iapeer runs on macOS today; Linux is planned.
|
|
122
124
|
- **More agent runtimes** — `opencode` and `antigravity`, two further CLI runtimes alongside Claude Code and Codex.
|
|
123
125
|
- **Discord runtime** — a human-presence runtime for Discord, alongside Telegram.
|
|
124
|
-
- **Permissions gate** — finer control over what an agent is allowed to do.
|
|
125
|
-
- **Approve permissions over Telegram** — approve an agent's permission requests from the chat; today permissions are auto-allowed by flags.
|
|
126
126
|
|
|
127
127
|
## License
|
|
128
128
|
|
package/docs/16-tray.md
CHANGED
|
@@ -24,11 +24,16 @@ activation that also installs SwiftBar.app when it is absent.
|
|
|
24
24
|
daemon any other way; the built-in TUI keeps its direct read path for that case, see
|
|
25
25
|
[15](15-fleet-api.md)).
|
|
26
26
|
- **Dropdown:** a host header (`iapeer <version> · up <uptime>`) and the memory/voice
|
|
27
|
-
provider heartbeats, then one row per peer
|
|
27
|
+
provider heartbeats, then one row per peer, in **two groups**: the `🔒` **launchd block**
|
|
28
|
+
(the human channel + service infra — non-clickable status rows) is pulled to the **top as
|
|
29
|
+
one contiguous block, before the first working agent**, regardless of live/asleep — so the
|
|
30
|
+
bar separates "service, don't touch" from the working agents at a glance. Working agents
|
|
31
|
+
follow. Within **each** group the order is live → asleep → stopped, then alphabetical.
|
|
28
32
|
- `<peer> <runtime><glyph> … <age>` with the same glyph semantics as the CLI —
|
|
29
33
|
`●` live · `○` asleep · `✕` stopped — green when any runtime is live. Badges: `👤`
|
|
30
|
-
a human is attached, `🔒` launchd-managed (H4 read-only),
|
|
31
|
-
|
|
34
|
+
a human is attached, `🔒` launchd-managed (H4 read-only), `🛡` the peer runs in **gated**
|
|
35
|
+
human-approval mode (docs/17; `yolo` is the fleet default and carries no badge — a bare
|
|
36
|
+
row reads as yolo), `⏳N` an ephemeral peer's queue depth.
|
|
32
37
|
- **Click an agent peer → Terminal.app running `iapeer attach <peer>`.** The row itself is
|
|
33
38
|
the attach action (no submenu, so a click attaches rather than merely expanding). The
|
|
34
39
|
handoff goes through `iapeer tray attach-term`, which writes a per-peer `.command`
|
package/docs/ru/16-tray.md
CHANGED
|
@@ -22,11 +22,16 @@ SwiftBar.app, если его нет.
|
|
|
22
22
|
покажет сломанный демон — встроенный TUI держит прямой путь чтения для этого случая, см.
|
|
23
23
|
[15](15-fleet-api.md)).
|
|
24
24
|
- **Выпадашка:** шапка хоста (`iapeer <версия> · up <аптайм>`) и хартбиты провайдеров памяти
|
|
25
|
-
и голоса, затем строка на
|
|
25
|
+
и голоса, затем строка на пира, **двумя группами**: блок `🔒` **под launchd** (человеческий
|
|
26
|
+
канал + служебная инфра — некликабельные статус-строки) вынесен **наверх единым непрерывным
|
|
27
|
+
блоком, перед первым рабочим агентом**, независимо от live/asleep — так бар с одного взгляда
|
|
28
|
+
отделяет «служебное, не трогать» от рабочих агентов. Рабочие агенты идут следом. Внутри
|
|
29
|
+
**каждой** группы порядок: живой → спит → остановлен, затем по алфавиту.
|
|
26
30
|
- `<peer> <рантайм><глиф> … <возраст>` с той же семантикой глифов, что в CLI —
|
|
27
31
|
`●` живой · `○` спит · `✕` остановлен — зелёный, если хоть один рантайм живой. Бейджи:
|
|
28
|
-
`👤` подключён человек, `🔒` под launchd (H4 read-only),
|
|
29
|
-
|
|
32
|
+
`👤` подключён человек, `🔒` под launchd (H4 read-only), `🛡` пир в режиме **gated**
|
|
33
|
+
human-approval (docs/17; `yolo` — дефолт флота, без бейджа: голая строка читается как yolo),
|
|
34
|
+
`⏳N` глубина очереди эфемерного пира.
|
|
30
35
|
- **Клик по агент-пиру → Terminal.app с `iapeer attach <peer>`.** Строка сама и есть действие
|
|
31
36
|
attach (без подменю — клик аттачит, а не раскрывает). Передача идёт через `iapeer tray
|
|
32
37
|
attach-term`: пишет per-peer `.command`-лаунчер и `open`'ит его — `open <файл>.command`
|
package/package.json
CHANGED
package/src/launch/index.ts
CHANGED
|
@@ -122,6 +122,24 @@ export function exitLogPath(exitLogDir: string): string {
|
|
|
122
122
|
// launch — bring up ONE session (runtime-agnostic via the adapter)
|
|
123
123
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* The intelligence gate — launch step (0), extracted as a PURE predicate so it is
|
|
127
|
+
* unit-testable in isolation (no bring-up, never touches the pty host). A channel
|
|
128
|
+
* adapter that declares `allowedIntelligences` (telegram/voicetalk → ['natural',
|
|
129
|
+
* 'absent']) refuses a peer whose nature is NOT in the set — and refuses too when
|
|
130
|
+
* the nature is unknown (cannot confirm). Returns the fail-loud reason string, or
|
|
131
|
+
* `null` when the gate PASSES (the peer may proceed to bring-up).
|
|
132
|
+
*/
|
|
133
|
+
export function intelligenceGateReason(adapter: RuntimeAdapter, spec: LaunchSpec): string | null {
|
|
134
|
+
if (adapter.allowedIntelligences && (spec.intelligence == null || !adapter.allowedIntelligences.includes(spec.intelligence))) {
|
|
135
|
+
return (
|
|
136
|
+
`runtime "${spec.runtime}" requires intelligence ∈ {${adapter.allowedIntelligences.join(', ')}}, ` +
|
|
137
|
+
`peer "${spec.personality}" is ${spec.intelligence ?? 'unknown'} — refusing to launch`
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
|
|
125
143
|
export const launch: LaunchFn = async (
|
|
126
144
|
spec: LaunchSpec,
|
|
127
145
|
adapter: RuntimeAdapter,
|
|
@@ -137,13 +155,8 @@ export const launch: LaunchFn = async (
|
|
|
137
155
|
// A channel runtime carries a human (natural) OR a faceless service bot (absent), never an
|
|
138
156
|
// LLM agent (artificial). Ports the persistent-peer FATAL human-channel guard, relaxed to
|
|
139
157
|
// first-class faceless service bots (docs/Рантайм-адаптеры).
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
identity,
|
|
143
|
-
`runtime "${spec.runtime}" requires intelligence ∈ {${adapter.allowedIntelligences.join(', ')}}, ` +
|
|
144
|
-
`peer "${spec.personality}" is ${spec.intelligence ?? 'unknown'} — refusing to launch`,
|
|
145
|
-
)
|
|
146
|
-
}
|
|
158
|
+
const gateReason = intelligenceGateReason(adapter, spec)
|
|
159
|
+
if (gateReason) return fail(identity, gateReason)
|
|
147
160
|
|
|
148
161
|
// (0.5) Ensure the socket's PARENT dir exists before `tmux new-session -S <sock>` —
|
|
149
162
|
// tmux does NOT create it and fails SILENTLY (the session "dies immediately")
|
package/src/tray/render.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface TrayPeer {
|
|
|
43
43
|
last_active_ms?: number
|
|
44
44
|
attached?: boolean
|
|
45
45
|
launchd_managed?: boolean
|
|
46
|
+
/** Human-approval mode (docs/17). ABSENT ⇒ `yolo` (client obligation — a pre-approval daemon omits it). */
|
|
47
|
+
approval_mode?: 'yolo' | 'gated'
|
|
46
48
|
wake_policy?: 'warm' | 'ephemeral'
|
|
47
49
|
queue_depth?: number
|
|
48
50
|
}
|
|
@@ -133,6 +135,12 @@ function aggregate(p: TrayPeer): 'live' | 'asleep' | 'stopped' {
|
|
|
133
135
|
|
|
134
136
|
const RANK: Record<'live' | 'asleep' | 'stopped', number> = { live: 0, asleep: 1, stopped: 2 }
|
|
135
137
|
|
|
138
|
+
// 🔒 launchd-managed peers (the human channel + service infra — arthur/timer/watcher) are
|
|
139
|
+
// NON-clickable status rows. They form ONE contiguous block at the very TOP, before the first
|
|
140
|
+
// working agent, regardless of live/asleep — so the bar separates "service, don't touch" from the
|
|
141
|
+
// working agents at a glance (owner UX). `attachable` (= !launchd_managed) is the same predicate.
|
|
142
|
+
const lockRank = (p: TrayPeer): number => (p.launchd_managed ? 0 : 1)
|
|
143
|
+
|
|
136
144
|
// ── renderers ─────────────────────────────────────────────────────────────────
|
|
137
145
|
|
|
138
146
|
const MANAGE_COMMANDS: Array<{ cmd: string; label: string }> = [
|
|
@@ -150,6 +158,10 @@ export function renderSwiftBar(snapshot: TraySnapshot, opts: RenderOptions): str
|
|
|
150
158
|
const now = opts.now ?? Date.now()
|
|
151
159
|
const bin = opts.binPath
|
|
152
160
|
const peers = [...snapshot.peers].sort((a, b) => {
|
|
161
|
+
// primary: the 🔒 lock block first (service/human infra), working agents below
|
|
162
|
+
const l = lockRank(a) - lockRank(b)
|
|
163
|
+
if (l !== 0) return l
|
|
164
|
+
// secondary (within EACH block): the existing live→asleep→stopped, then alpha
|
|
153
165
|
const r = RANK[aggregate(a)] - RANK[aggregate(b)]
|
|
154
166
|
return r !== 0 ? r : a.personality.localeCompare(b.personality)
|
|
155
167
|
})
|
|
@@ -241,9 +253,14 @@ function attachable(p: TrayPeer): boolean {
|
|
|
241
253
|
function renderPeerRow(p: TrayPeer, bin: string, now: number, pendingCount = 0): string {
|
|
242
254
|
const agg = aggregate(p)
|
|
243
255
|
const detail = p.runtimes.map(r => `${r.runtime}${GLYPH[r.status]}`).join(' ')
|
|
256
|
+
// approval mode (docs/17): the whole fleet defaults to `yolo`, so showing it on every row is
|
|
257
|
+
// noise — mark ONLY the exception, `gated` (this peer's blocking approvals go to a human), with a
|
|
258
|
+
// 🛡 shield. No badge ⇒ yolo (the default; also how a pre-approval snapshot with the field absent
|
|
259
|
+
// renders). So the row reads the mode: 🛡 = gated, bare = yolo.
|
|
244
260
|
const badges =
|
|
245
261
|
(p.attached ? ' 👤' : '') +
|
|
246
262
|
(p.launchd_managed ? ' 🔒' : '') +
|
|
263
|
+
(p.approval_mode === 'gated' ? ' 🛡' : '') +
|
|
247
264
|
(p.wake_policy === 'ephemeral' && (p.queue_depth ?? 0) > 0 ? ` ⏳${p.queue_depth}` : '')
|
|
248
265
|
const ageStr = p.last_active_ms ? ` ${fmtAge(p.last_active_ms, now)}` : ''
|
|
249
266
|
// HIGHLIGHT a peer with a pending approval: a ⚠ prefix + a red count badge + the row painted red
|