@agfpd/iapeer 0.4.76 → 0.4.77

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.4.76",
3
+ "version": "0.4.77",
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",
@@ -112,10 +112,20 @@ function throwEvent(v: unknown): never {
112
112
  throw new Error(`hook stdin has an unexpected hook_event_name "${String(v)}"`)
113
113
  }
114
114
 
115
- /** The decision JSON in the shape the given event accepts. A deny carries the reason to the
116
- * model. PermissionRequest uses `decision.behavior` (+ `message`); PreToolUse uses
117
- * `permissionDecision` (+ `permissionDecisionReason`) — both verified live 2.1.201 / codex 0.142.5. */
118
- export function formatHookDecision(d: ApprovalDecision, event: HookEvent): string {
115
+ /**
116
+ * The decision JSON in the shape the given event (and runtime) accepts. A deny carries the
117
+ * reason to the model. PermissionRequest uses `decision.behavior` (+ `message`); PreToolUse
118
+ * uses `permissionDecision` (+ `permissionDecisionReason`).
119
+ *
120
+ * codex vs claude on the PreToolUse **allow** path (VERIFIED LIVE, codex-cli 0.142.5): codex
121
+ * honors ONLY `permissionDecision:"deny"` — emitting `permissionDecision:"allow"` marks the
122
+ * hook **Failed** (fail-open, the "PreToolUse Failed"/"unsupported permissionDecision:allow"
123
+ * the operator saw). The clean allow on codex is to **abstain**: emit NOTHING (exit 0) → codex
124
+ * marks the hook **Completed** and the tool proceeds under the gated bypass base (approval is a
125
+ * DENY-or-abstain gate on codex, not a two-sided allow/deny one). claude's PreToolUse DOES honor
126
+ * `permissionDecision:"allow"`, so it keeps the affirmative form. Deny is identical on both.
127
+ */
128
+ export function formatHookDecision(d: ApprovalDecision, event: HookEvent, runtime = 'claude'): string {
119
129
  if (event === 'PermissionRequest') {
120
130
  return JSON.stringify({
121
131
  hookSpecificOutput: {
@@ -124,11 +134,19 @@ export function formatHookDecision(d: ApprovalDecision, event: HookEvent): strin
124
134
  },
125
135
  })
126
136
  }
137
+ // PreToolUse
138
+ if (d.decision === 'allow') {
139
+ // codex: abstain (empty output) — an affirmative "allow" fails the hook (verified live 0.142.5).
140
+ if (runtime === 'codex') return ''
141
+ // claude: affirmative allow is honored.
142
+ return JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' } })
143
+ }
144
+ // deny (+ reason) — honored identically on claude and codex.
127
145
  return JSON.stringify({
128
146
  hookSpecificOutput: {
129
147
  hookEventName: 'PreToolUse',
130
- permissionDecision: d.decision,
131
- ...(d.decision === 'deny' && d.reason ? { permissionDecisionReason: d.reason } : {}),
148
+ permissionDecision: 'deny',
149
+ ...(d.reason ? { permissionDecisionReason: d.reason } : {}),
132
150
  },
133
151
  })
134
152
  }
@@ -162,6 +180,9 @@ export async function runApprovalHook(stdin: string, deps: RunHookDeps = {}): Pr
162
180
  const env = deps.env ?? process.env
163
181
  const doFetch = deps.fetch ?? fetch
164
182
  const timeoutMs = deps.timeoutMs
183
+ // Runtime resolved up front so BOTH the success and the fail-safe-deny paths format for the
184
+ // right runtime (codex's allow-shape differs from claude's — see formatHookDecision).
185
+ const runtime = env.PEER_RUNTIME?.trim() || 'claude'
165
186
  let event: HookEvent
166
187
  let toolName: string
167
188
  let toolInput: Record<string, unknown> | undefined
@@ -174,18 +195,17 @@ export async function runApprovalHook(stdin: string, deps: RunHookDeps = {}): Pr
174
195
  try {
175
196
  const personality = env.PEER_PERSONALITY?.trim()
176
197
  if (!personality) throw new Error('no PEER_PERSONALITY in the hook environment')
177
- const runtime = env.PEER_RUNTIME?.trim() || 'claude'
178
198
  const { kind, content, summary } = actionContent(toolName, toolInput)
179
199
  const decision = await requestApproval(
180
200
  resolveFleetBase(env),
181
201
  { personality, runtime, kind, tool: toolName, content, summary },
182
202
  { env, fetch: doFetch, timeoutMs },
183
203
  )
184
- return { stdout: formatHookDecision(decision, event), stderr: '', exitCode: 0 }
204
+ return { stdout: formatHookDecision(decision, event, runtime), stderr: '', exitCode: 0 }
185
205
  } catch (e) {
186
206
  // Event known → event-appropriate DENY JSON (never allow-by-accident, never hang).
187
207
  return {
188
- stdout: formatHookDecision({ decision: 'deny', reason: `iapeer approval unavailable (${e instanceof Error ? e.message : String(e)}) — denied fail-safe` }, event),
208
+ stdout: formatHookDecision({ decision: 'deny', reason: `iapeer approval unavailable (${e instanceof Error ? e.message : String(e)}) — denied fail-safe` }, event, runtime),
189
209
  stderr: '',
190
210
  exitCode: 0,
191
211
  }