@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 +1 -1
- package/src/approval/hook.ts +29 -9
package/package.json
CHANGED
package/src/approval/hook.ts
CHANGED
|
@@ -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
|
-
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
|
|
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:
|
|
131
|
-
...(d.
|
|
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
|
}
|