@agfpd/iapeer 0.4.72 → 0.4.74
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/package.json
CHANGED
|
@@ -277,11 +277,15 @@ export const claudeAdapter: RuntimeAdapter = {
|
|
|
277
277
|
*
|
|
278
278
|
* - '--dangerously-skip-permissions' claude-start.sh:318, spawner.ts:776 —
|
|
279
279
|
* headless peer has no interactive owner to grant per-tool permission.
|
|
280
|
-
* - '--disallowedTools',
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
280
|
+
* - '--disallowedTools', <list> claude-start.sh:313/316, spawner.ts:777 —
|
|
281
|
+
* AskUserQuestion would render in a TUI no headless peer owner watches; the
|
|
282
|
+
* question goes "into the void", so it is disallowed by DEFAULT. The value is
|
|
283
|
+
* an OPT-IN per-peer override via spec.disallowedTools (launch.env
|
|
284
|
+
* PEER_DISALLOWED_TOOLS, merged in buildLaunchInvocation): UNDEFINED (the
|
|
285
|
+
* fleet default) → the literal 'AskUserQuestion'; an EXPLICIT empty string →
|
|
286
|
+
* the flag is OMITTED (allow all — e.g. to re-enable AskUserQuestion for a
|
|
287
|
+
* throwaway peer); a non-empty value → that verbatim list. Only an explicitly
|
|
288
|
+
* set env key changes behavior — the whole existing fleet is byte-identical.
|
|
285
289
|
* - '--add-dir','/' grants the file/edit/bash tools access OUTSIDE cwd (the
|
|
286
290
|
* CLI equivalent of settings `permissions.additionalDirectories`). A peer
|
|
287
291
|
* reads/writes its memory vault (often an iCloud Obsidian dir) and ~/.iapeer,
|
|
@@ -314,11 +318,14 @@ export const claudeAdapter: RuntimeAdapter = {
|
|
|
314
318
|
// active as a fail-SAFE backstop, and the separately-installed PreToolUse hook is
|
|
315
319
|
// the primary interceptor (it fires regardless of mode — verified live 2.1.201).
|
|
316
320
|
const gated = spec.approvalMode === 'gated'
|
|
321
|
+
// Opt-in disallowedTools (docs/17 — real-modal acceptance / general per-peer control). UNDEFINED →
|
|
322
|
+
// the hardcoded default 'AskUserQuestion' (fleet unchanged); explicit '' → omit the flag (allow all);
|
|
323
|
+
// a value → that verbatim list. A blank/whitespace value collapses to "omit" (the re-enable case).
|
|
324
|
+
const disallowed = (spec.disallowedTools === undefined ? 'AskUserQuestion' : spec.disallowedTools).trim()
|
|
317
325
|
return [
|
|
318
326
|
cfg.claudeBin,
|
|
319
327
|
...(gated ? ['--permission-mode', 'default'] : ['--dangerously-skip-permissions']),
|
|
320
|
-
'--disallowedTools',
|
|
321
|
-
'AskUserQuestion',
|
|
328
|
+
...(disallowed ? ['--disallowedTools', disallowed] : []),
|
|
322
329
|
'--add-dir',
|
|
323
330
|
'/',
|
|
324
331
|
...(spec.systemPromptFile ? ['--system-prompt-file', spec.systemPromptFile] : []),
|
|
@@ -23,6 +23,23 @@ export interface NumberedModal {
|
|
|
23
23
|
|
|
24
24
|
const CAP_LINES = 24
|
|
25
25
|
const CAP_BYTES = 8192
|
|
26
|
+
// How far above the footer to scan for the modal's options — the real AskUserQuestion box (question +
|
|
27
|
+
// pill + N options each with a DESCRIPTION line + an inter-option separator) is a dozen-plus rows.
|
|
28
|
+
const MODAL_MAX_LINES = 40
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Is `line` a claude/codex interactive-SELECT footer — the stable signature of a live numbered picker?
|
|
32
|
+
* AskUserQuestion: "Enter to select · ↑/↓ to navigate · Esc to cancel"; a resume/boot select: "Enter to
|
|
33
|
+
* confirm · Esc to cancel"; codex: "Press enter to confirm or esc to go back". Anchoring on this footer
|
|
34
|
+
* (boris's guidance — устойчивее чем «❯ внизу») is layout-robust: it survives the cursor sitting on ANY
|
|
35
|
+
* option and options being interleaved with description lines. The command-approval breaker's footer
|
|
36
|
+
* ("Esc to cancel · Tab to amend · ctrl+e to explain") deliberately does NOT match (no select/navigate
|
|
37
|
+
* hint) — it is a KNOWN class handled by its own taxonomy, and the structural guards below (a live
|
|
38
|
+
* glyph-selected option + ≥2 options + nothing below the footer) keep this from firing on prose.
|
|
39
|
+
*/
|
|
40
|
+
export function isSelectFooter(line: string): boolean {
|
|
41
|
+
return /esc to (?:cancel|go back)/i.test(line) && /(?:enter to |to navigate|to select|to confirm|press enter)/i.test(line)
|
|
42
|
+
}
|
|
26
43
|
// Unicode box-drawing block (U+2500–U+257F) — modal borders/separators; stripped for PARSING only
|
|
27
44
|
// (the verbatim `content` keeps them).
|
|
28
45
|
const BOX_DRAWING = /[─-╿]/g
|
|
@@ -65,37 +82,42 @@ function capBlock(block: string[]): string {
|
|
|
65
82
|
*/
|
|
66
83
|
export function detectNumberedModal(pane: string, glyph: string): NumberedModal | null {
|
|
67
84
|
const lines = pane.split(/\r?\n/)
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
// 1. FOOTER anchor — the stable signature of a live interactive select (boris: устойчивее «❯ внизу»).
|
|
86
|
+
// The LAST footer line is THIS modal's (a scrolled-off older one sits above). Anchoring here is
|
|
87
|
+
// robust to the cursor sitting on ANY option and to options interleaved with DESCRIPTION lines /
|
|
88
|
+
// separators (the real AskUserQuestion layout — verified against a live 2.1.202 render).
|
|
89
|
+
let footerIdx = -1
|
|
90
|
+
for (let i = 0; i < lines.length; i++) if (isSelectFooter(lines[i])) footerIdx = i
|
|
91
|
+
if (footerIdx < 0) return null
|
|
92
|
+
// 2. COMPOSER-REPLACED guard — a LIVE blocking modal has NOTHING below its footer (the composer is
|
|
93
|
+
// gone). A mere QUOTE of a modal pasted into chat has the ready composer glyph rendered BELOW it, so
|
|
94
|
+
// ANY select glyph after the footer means "not a live block" → reject (the В40 quote class).
|
|
95
|
+
for (let i = footerIdx + 1; i < lines.length; i++) if (lines[i].includes(glyph)) return null
|
|
96
|
+
// 3. OPTIONS — scan a bounded WINDOW above the footer (NOT a contiguous run: the real modal interleaves
|
|
97
|
+
// each option with a wrapped description line and even an inter-option separator, and a blank can sit
|
|
98
|
+
// between the last option and the footer). Require a glyph-SELECTED option (a live cursor is on one)
|
|
99
|
+
// and ≥2 distinct numbered options. option1 = the "1." row CLOSEST to the footer (the modal's own —
|
|
100
|
+
// a stray transcript "1." further up loses to it).
|
|
101
|
+
const windowTop = Math.max(0, footerIdx - MODAL_MAX_LINES)
|
|
74
102
|
const selectedRe = new RegExp(`${escapeForRegex(glyph)}\\s*\\d+[.)]\\s`)
|
|
75
|
-
if (!selectedRe.test(lines[selected])) return null
|
|
76
|
-
// The select glyph sits only on the SELECTED option; siblings render WITHOUT it (above and below). Grow
|
|
77
|
-
// the contiguous OPTION RUN in BOTH directions from the selected row while each line parses as a
|
|
78
|
-
// numbered option (an interleaved blank / non-option line ends the run). This captures every option,
|
|
79
|
-
// not just the ones above the cursor.
|
|
80
|
-
let optTop = selected
|
|
81
|
-
let optBottom = selected
|
|
82
|
-
while (optTop - 1 >= 0 && parseOption(lines[optTop - 1], glyph)) optTop--
|
|
83
|
-
while (optBottom + 1 < lines.length && parseOption(lines[optBottom + 1], glyph)) optBottom++
|
|
84
|
-
// need ≥2 DISTINCT numbered options (a real select, not a lone printed `N.` line).
|
|
85
103
|
const nums = new Set<number>()
|
|
86
104
|
let option1 = ''
|
|
87
|
-
|
|
105
|
+
let hasSelected = false
|
|
106
|
+
let firstOptIdx = -1
|
|
107
|
+
for (let i = windowTop; i < footerIdx; i++) {
|
|
108
|
+
if (selectedRe.test(lines[i])) hasSelected = true
|
|
88
109
|
const opt = parseOption(lines[i], glyph)
|
|
89
110
|
if (!opt) continue
|
|
111
|
+
if (firstOptIdx < 0) firstOptIdx = i
|
|
90
112
|
nums.add(opt.num)
|
|
91
|
-
if (opt.num === 1
|
|
113
|
+
if (opt.num === 1) option1 = opt.label // last (closest-to-footer) wins
|
|
92
114
|
}
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
const block = lines.slice(
|
|
115
|
+
if (!hasSelected) return null // no live cursor on an option → a printed list, not a live select
|
|
116
|
+
if (nums.size < 2) return null // ≥2 options → a real select
|
|
117
|
+
// 4. Verbatim block shown to the human: from a couple of lines above the first option (the question) to
|
|
118
|
+
// the footer, blanks trimmed + capped.
|
|
119
|
+
const cTop = Math.max(windowTop, (firstOptIdx < 0 ? footerIdx : firstOptIdx) - 2)
|
|
120
|
+
const block = lines.slice(cTop, footerIdx + 1)
|
|
99
121
|
while (block.length && block[0].trim() === '') block.shift()
|
|
100
122
|
while (block.length && block[block.length - 1].trim() === '') block.pop()
|
|
101
123
|
return { content: capBlock(block), option1: option1 || '(option 1)' }
|
package/src/launch/invocation.ts
CHANGED
|
@@ -34,10 +34,15 @@ export function buildLaunchInvocation(
|
|
|
34
34
|
): LaunchInvocation {
|
|
35
35
|
const env = cfg.env ?? process.env
|
|
36
36
|
const launchEnv = readLaunchEnv(spec.cwd, spec.runtime)
|
|
37
|
-
const specWithArgs: LaunchSpec =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
const specWithArgs: LaunchSpec = {
|
|
38
|
+
...spec,
|
|
39
|
+
...(launchEnv.startArgs.length > 0 ? { extraArgs: [...(spec.extraArgs ?? []), ...launchEnv.startArgs] } : {}),
|
|
40
|
+
// launch.env PEER_DISALLOWED_TOOLS opt-in — an explicit spec.disallowedTools (caller override) wins;
|
|
41
|
+
// else the launch.env value (undefined = key absent → adapter default, fleet unchanged).
|
|
42
|
+
...(spec.disallowedTools === undefined && launchEnv.disallowedTools !== undefined
|
|
43
|
+
? { disallowedTools: launchEnv.disallowedTools }
|
|
44
|
+
: {}),
|
|
45
|
+
}
|
|
41
46
|
const argv = adapter.buildArgv(specWithArgs, cfg)
|
|
42
47
|
const childEnv: NodeJS.ProcessEnv = {
|
|
43
48
|
...env,
|
package/src/launch/types.ts
CHANGED
|
@@ -159,6 +159,11 @@ export interface LaunchSpec {
|
|
|
159
159
|
resumeRef?: string
|
|
160
160
|
/** Free-form extra CLI args (PEER_START_ARGS). */
|
|
161
161
|
extraArgs?: string[]
|
|
162
|
+
/** claude `--disallowedTools` value (opt-in per-peer override via launch.env `PEER_DISALLOWED_TOOLS`).
|
|
163
|
+
* UNDEFINED (the default for the whole fleet) → the adapter's hardcoded default (`AskUserQuestion`);
|
|
164
|
+
* an EXPLICIT empty string → the flag is OMITTED (allow all — e.g. re-enable AskUserQuestion); a
|
|
165
|
+
* non-empty value → that verbatim list. Only claude's buildArgv consumes it (codex has no such flag). */
|
|
166
|
+
disallowedTools?: string
|
|
162
167
|
/** Peer intelligence (artificial/natural/absent). Used to enforce an adapter's
|
|
163
168
|
* intelligence gate at launch (telegram allows natural|absent). Optional: a doctrine-
|
|
164
169
|
* less throwaway may omit it, but an adapter that declares allowedIntelligences
|
package/src/storage/index.ts
CHANGED
|
@@ -215,6 +215,11 @@ export function peerLaunchEnvPath(cwd: string, runtime: Runtime): string {
|
|
|
215
215
|
export interface LaunchEnv {
|
|
216
216
|
/** Tokens from PEER_START_ARGS, appended AFTER the adapter's base argv flags. */
|
|
217
217
|
startArgs: string[]
|
|
218
|
+
/** claude `--disallowedTools` override from PEER_DISALLOWED_TOOLS. UNDEFINED = the key was ABSENT
|
|
219
|
+
* (→ the adapter default is used, the whole fleet unchanged); a string (incl. '') = the key was
|
|
220
|
+
* PRESENT (opt-in: '' omits the flag, a value sets it). The absent-vs-empty distinction is the
|
|
221
|
+
* opt-in gate — only an explicitly-present key changes behavior. */
|
|
222
|
+
disallowedTools?: string
|
|
218
223
|
/** Other KEY=VALUE assignments — extra child-process env for the peer session. */
|
|
219
224
|
env: Record<string, string>
|
|
220
225
|
}
|
|
@@ -255,8 +260,15 @@ export function readLaunchEnv(cwd: string, runtime: Runtime): LaunchEnv {
|
|
|
255
260
|
}
|
|
256
261
|
const startArgsRaw = (env.PEER_START_ARGS ?? '').trim()
|
|
257
262
|
delete env.PEER_START_ARGS // consumed as argv, not propagated as a child env var
|
|
263
|
+
// PEER_DISALLOWED_TOOLS — a LAUNCH DIRECTIVE (claude --disallowedTools), not a child env var. Preserve
|
|
264
|
+
// the ABSENT (undefined → default) vs PRESENT-EMPTY ('' → omit the flag) distinction: an unset key must
|
|
265
|
+
// leave the whole fleet on the hardcoded default. Consumed out of the child env like PEER_START_ARGS.
|
|
266
|
+
const hasDisallowed = Object.prototype.hasOwnProperty.call(env, 'PEER_DISALLOWED_TOOLS')
|
|
267
|
+
const disallowedTools = hasDisallowed ? env.PEER_DISALLOWED_TOOLS : undefined
|
|
268
|
+
delete env.PEER_DISALLOWED_TOOLS
|
|
258
269
|
return {
|
|
259
270
|
startArgs: startArgsRaw ? startArgsRaw.split(/\s+/) : [],
|
|
271
|
+
...(disallowedTools !== undefined ? { disallowedTools } : {}),
|
|
260
272
|
env,
|
|
261
273
|
}
|
|
262
274
|
}
|