@agfpd/iapeer 0.4.68 → 0.4.70
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/docs/15-fleet-api.md +10 -2
- package/docs/16-tray.md +18 -4
- package/docs/17-approval.md +128 -0
- package/docs/README.md +1 -0
- package/docs/ru/15-fleet-api.md +7 -1
- package/docs/ru/16-tray.md +14 -2
- package/docs/ru/17-approval.md +128 -0
- package/docs/ru/README.md +1 -0
- package/package.json +1 -1
- package/src/approval/brokerClient.ts +92 -0
- package/src/approval/cli.ts +118 -0
- package/src/approval/hook.ts +193 -0
- package/src/approval/install.ts +200 -0
- package/src/cli/index.ts +66 -2
- package/src/daemon/approvals.ts +202 -0
- package/src/daemon/approvalslog.ts +54 -0
- package/src/daemon/fleet.ts +116 -1
- package/src/identity/index.ts +28 -0
- package/src/identity/profileStandard.ts +3 -0
- package/src/launch/adapters/claude.ts +83 -16
- package/src/launch/adapters/codex.ts +14 -3
- package/src/launch/index.ts +1 -1
- package/src/launch/ptyHost.ts +3 -3
- package/src/launch/types.ts +27 -8
- package/src/lifecycle/index.ts +16 -1
- package/src/supervisor/approvalRoute.ts +76 -0
- package/src/supervisor/boot.ts +13 -3
- package/src/supervisor/daemon.ts +62 -6
- package/src/tray/index.ts +25 -0
- package/src/tray/render.ts +107 -6
package/src/tray/render.ts
CHANGED
|
@@ -18,8 +18,15 @@ const COLOR = {
|
|
|
18
18
|
meta: '#8b949e',
|
|
19
19
|
live: '#3fb950',
|
|
20
20
|
down: '#f85149',
|
|
21
|
+
warn: '#d29922',
|
|
21
22
|
} as const
|
|
22
23
|
|
|
24
|
+
// Approval content shown INLINE in the menu is capped (a big diff must not blow up the
|
|
25
|
+
// dropdown); the FULL verbatim content stays available via `iapeer approvals` + GET
|
|
26
|
+
// /fleet/v1/approvals/<id> (criterion #7 holds on the payload, the menu is its truncated view).
|
|
27
|
+
const APPROVAL_CONTENT_MAX_LINES = 20
|
|
28
|
+
const APPROVAL_CONTENT_MAX_CHARS = 8000
|
|
29
|
+
|
|
23
30
|
// ── docs/15 snapshot shape (only the fields the tray reads) ───────────────────
|
|
24
31
|
|
|
25
32
|
export interface TrayRuntime {
|
|
@@ -40,6 +47,16 @@ export interface TrayPeer {
|
|
|
40
47
|
queue_depth?: number
|
|
41
48
|
}
|
|
42
49
|
|
|
50
|
+
export interface TrayApproval {
|
|
51
|
+
id: string
|
|
52
|
+
personality: string
|
|
53
|
+
runtime?: string
|
|
54
|
+
kind?: string
|
|
55
|
+
tool: string
|
|
56
|
+
summary?: string
|
|
57
|
+
content?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
43
60
|
export interface TraySnapshot {
|
|
44
61
|
version?: string
|
|
45
62
|
host?: {
|
|
@@ -50,6 +67,8 @@ export interface TraySnapshot {
|
|
|
50
67
|
fda?: boolean | null
|
|
51
68
|
}
|
|
52
69
|
peers: TrayPeer[]
|
|
70
|
+
/** Pending human-approval requests (docs/15/17). ABSENT ⇒ empty queue (client obligation). */
|
|
71
|
+
approvals?: TrayApproval[]
|
|
53
72
|
}
|
|
54
73
|
|
|
55
74
|
export interface RenderOptions {
|
|
@@ -135,12 +154,28 @@ export function renderSwiftBar(snapshot: TraySnapshot, opts: RenderOptions): str
|
|
|
135
154
|
return r !== 0 ? r : a.personality.localeCompare(b.personality)
|
|
136
155
|
})
|
|
137
156
|
const liveCount = snapshot.peers.filter(p => aggregate(p) === 'live').length
|
|
157
|
+
const pending = snapshot.approvals ?? []
|
|
158
|
+
const pendingByPeer = new Map<string, number>()
|
|
159
|
+
for (const a of pending) pendingByPeer.set(a.personality, (pendingByPeer.get(a.personality) ?? 0) + 1)
|
|
138
160
|
|
|
139
161
|
const lines: string[] = []
|
|
140
|
-
// ── menu-bar title
|
|
141
|
-
|
|
162
|
+
// ── menu-bar title ──
|
|
163
|
+
// Pending approvals TAKE OVER the menu-bar as a red iOS-style badge so the owner sees an awaiting
|
|
164
|
+
// request at a glance; an empty queue is the normal antenna + live-peer count.
|
|
165
|
+
if (pending.length > 0) {
|
|
166
|
+
lines.push(renderBadge(pending.length))
|
|
167
|
+
} else {
|
|
168
|
+
lines.push(line(0, String(liveCount), { sfimage: 'antenna.radiowaves.left.and.right' }))
|
|
169
|
+
}
|
|
142
170
|
lines.push('---')
|
|
143
171
|
|
|
172
|
+
// ── pending approvals (ALWAYS-ON: whenever the queue is non-empty, at the TOP of the dropdown;
|
|
173
|
+
// each request expanded with verbatim content + Allow/Deny at depth 0 — no extra clicks) ──
|
|
174
|
+
if (pending.length > 0) {
|
|
175
|
+
lines.push(...renderApprovals(pending, bin))
|
|
176
|
+
lines.push('---')
|
|
177
|
+
}
|
|
178
|
+
|
|
144
179
|
// ── host header ──
|
|
145
180
|
const ver = snapshot.host?.version ?? snapshot.version ?? '?'
|
|
146
181
|
const up = snapshot.host?.uptimeSecs !== undefined ? ` · up ${fmtUptime(snapshot.host.uptimeSecs)}` : ''
|
|
@@ -154,7 +189,7 @@ export function renderSwiftBar(snapshot: TraySnapshot, opts: RenderOptions): str
|
|
|
154
189
|
lines.push(line(0, 'no peers registered', { color: COLOR.meta }))
|
|
155
190
|
}
|
|
156
191
|
for (const p of peers) {
|
|
157
|
-
lines.push(renderPeerRow(p, bin, now))
|
|
192
|
+
lines.push(renderPeerRow(p, bin, now, pendingByPeer.get(p.personality) ?? 0))
|
|
158
193
|
}
|
|
159
194
|
|
|
160
195
|
// ── Manage submenu (lifecycle commands per peer; kept off the rows so a row click
|
|
@@ -203,7 +238,7 @@ function attachable(p: TrayPeer): boolean {
|
|
|
203
238
|
* `Cmd-T` System-Events keystroke that silently no-ops without Accessibility permission.
|
|
204
239
|
* Infra (launchd) rows are plain status lines. Lifecycle lives in the Manage submenu.
|
|
205
240
|
*/
|
|
206
|
-
function renderPeerRow(p: TrayPeer, bin: string, now: number): string {
|
|
241
|
+
function renderPeerRow(p: TrayPeer, bin: string, now: number, pendingCount = 0): string {
|
|
207
242
|
const agg = aggregate(p)
|
|
208
243
|
const detail = p.runtimes.map(r => `${r.runtime}${GLYPH[r.status]}`).join(' ')
|
|
209
244
|
const badges =
|
|
@@ -211,8 +246,13 @@ function renderPeerRow(p: TrayPeer, bin: string, now: number): string {
|
|
|
211
246
|
(p.launchd_managed ? ' 🔒' : '') +
|
|
212
247
|
(p.wake_policy === 'ephemeral' && (p.queue_depth ?? 0) > 0 ? ` ⏳${p.queue_depth}` : '')
|
|
213
248
|
const ageStr = p.last_active_ms ? ` ${fmtAge(p.last_active_ms, now)}` : ''
|
|
214
|
-
|
|
215
|
-
|
|
249
|
+
// HIGHLIGHT a peer with a pending approval: a ⚠ prefix + a red count badge + the row painted red
|
|
250
|
+
// so the owner sees WHICH peer is waiting (owner criterion — the static-menu equivalent of a pulse;
|
|
251
|
+
// SwiftBar cannot animate). The highlight wins over the live/meta color.
|
|
252
|
+
const prefix = pendingCount > 0 ? '⚠ ' : ''
|
|
253
|
+
const pendingBadge = pendingCount > 0 ? ` 🔴${pendingCount}` : ''
|
|
254
|
+
const label = `${prefix}${p.personality} ${detail}${badges}${pendingBadge}${ageStr}`
|
|
255
|
+
const rowColor = pendingCount > 0 ? COLOR.down : agg === 'live' ? COLOR.live : COLOR.meta
|
|
216
256
|
if (!attachable(p)) return line(0, label, { color: rowColor })
|
|
217
257
|
return line(0, label, { color: rowColor, bash: bin, param1: 'tray', param2: 'attach-term', param3: p.personality, terminal: false })
|
|
218
258
|
}
|
|
@@ -242,6 +282,67 @@ function renderManage(peers: TrayPeer[], bin: string): string[] {
|
|
|
242
282
|
return out
|
|
243
283
|
}
|
|
244
284
|
|
|
285
|
+
// ── approval renderers (docs/17 — the always-on approval channel in the bar) ─────
|
|
286
|
+
|
|
287
|
+
/** Menu-bar pending badge: a red `N.circle.fill` SF Symbol (the iOS-notification look — a red circle
|
|
288
|
+
* with the number inside). SF Symbols carries numbered circles 0…50; past that, a red
|
|
289
|
+
* exclamation-circle + the count as text. `sfcolor` tints the symbol red; `color` is set too as a
|
|
290
|
+
* fallback for SwiftBar builds that read `color` for the tint. The numbered symbol carries the count,
|
|
291
|
+
* so no text is needed for ≤50. */
|
|
292
|
+
function renderBadge(count: number): string {
|
|
293
|
+
if (count <= 50) return line(0, '', { sfimage: `${count}.circle.fill`, sfcolor: COLOR.down, color: COLOR.down })
|
|
294
|
+
return line(0, String(count), { sfimage: 'exclamationmark.circle.fill', sfcolor: COLOR.down, color: COLOR.down })
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Defuse SwiftBar STRUCTURAL injection from verbatim content: ` | ` is the param separator (replace
|
|
298
|
+
* with a broken bar) and a leading `--` nests a submenu (defused by the `│ ` prefix in capContent).
|
|
299
|
+
* Display-only — the FULL verbatim content stays in `iapeer approvals` (criterion #7 on the payload;
|
|
300
|
+
* the menu is its truncated, structurally-safe view). */
|
|
301
|
+
function sanitizeMenuText(s: string): string {
|
|
302
|
+
return s.replace(/ \| /g, ' ¦ ')
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Cap the inline content (a big diff must not blow up the dropdown) and prefix each row with `│ ` so
|
|
306
|
+
* it reads as quoted content AND never begins with `--`. Full verbatim content stays in `iapeer
|
|
307
|
+
* approvals`. */
|
|
308
|
+
function capContent(content: string): string[] {
|
|
309
|
+
const clipped = content.length > APPROVAL_CONTENT_MAX_CHARS ? content.slice(0, APPROVAL_CONTENT_MAX_CHARS) : content
|
|
310
|
+
let rows = clipped.split('\n')
|
|
311
|
+
let truncated = content.length > APPROVAL_CONTENT_MAX_CHARS
|
|
312
|
+
if (rows.length > APPROVAL_CONTENT_MAX_LINES) {
|
|
313
|
+
rows = rows.slice(0, APPROVAL_CONTENT_MAX_LINES)
|
|
314
|
+
truncated = true
|
|
315
|
+
}
|
|
316
|
+
const out = rows.map(r => `│ ${sanitizeMenuText(r)}`)
|
|
317
|
+
if (truncated) out.push('│ …[truncated — full content: iapeer approvals]')
|
|
318
|
+
return out
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** The pending-approval section (top of the dropdown). Each request is FLAT (depth 0, no submenu): a
|
|
322
|
+
* header (peer · tool), the verbatim content (monospace, capped), then Allow / Deny click actions —
|
|
323
|
+
* both visible + clickable with NO extra clicks. A click runs `iapeer tray approve|deny <id>` in the
|
|
324
|
+
* background (unix-first fleet POST) → the broker resolves → the streaming plugin re-renders off the
|
|
325
|
+
* approval-resolved SSE event (no refresh=true — the stream is always fresh). */
|
|
326
|
+
function renderApprovals(approvals: TrayApproval[], bin: string): string[] {
|
|
327
|
+
const out: string[] = [
|
|
328
|
+
line(0, `${approvals.length} pending approval${approvals.length === 1 ? '' : 's'}`, {
|
|
329
|
+
sfimage: 'bell.badge.fill',
|
|
330
|
+
sfcolor: COLOR.down,
|
|
331
|
+
color: COLOR.down,
|
|
332
|
+
}),
|
|
333
|
+
]
|
|
334
|
+
for (let i = 0; i < approvals.length; i++) {
|
|
335
|
+
if (i > 0) out.push('---') // separate multiple requests
|
|
336
|
+
const a = approvals[i]!
|
|
337
|
+
out.push(line(0, `${a.personality} · ${a.tool}`, { sfimage: 'hand.raised.fill', sfcolor: COLOR.warn, color: COLOR.warn }))
|
|
338
|
+
const content = a.content ?? a.summary ?? ''
|
|
339
|
+
if (content) for (const cl of capContent(content)) out.push(line(0, cl, { font: 'Menlo', size: 12, color: COLOR.meta }))
|
|
340
|
+
out.push(line(0, 'Allow', { sfimage: 'checkmark.circle.fill', sfcolor: COLOR.live, color: COLOR.live, bash: bin, param1: 'tray', param2: 'approve', param3: a.id, terminal: false }))
|
|
341
|
+
out.push(line(0, 'Deny', { sfimage: 'xmark.circle.fill', sfcolor: COLOR.down, color: COLOR.down, bash: bin, param1: 'tray', param2: 'deny', param3: a.id, terminal: false }))
|
|
342
|
+
}
|
|
343
|
+
return out
|
|
344
|
+
}
|
|
345
|
+
|
|
245
346
|
/** The degraded output when no advertised daemon address answers (docs/15: "A
|
|
246
347
|
* dashboard that cannot reach any advertised address should render the daemon as
|
|
247
348
|
* down"). Distinct menu-bar icon + a red header; a Refresh item to retry. */
|