@ory/codex 0.14.0 → 1.0.1

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.
@@ -1,26 +1,10 @@
1
- import { OryAgentClient, ensureUserAuthenticated, ensureAgentIdentity } from "@ory/argus";
1
+ import { ensureAgentIdentity, ensureSubAgentIdentity, ensureUserAuthenticated } from "@ory/argus";
2
+ import type { OryAgentClient } from "@ory/argus";
2
3
  import type { CodexHookInput, CodexHookOutput } from "./types.js";
3
4
  export interface HandleHookEventDeps {
4
- /** Test injection point for the user login flow. */
5
5
  userLogin?: typeof ensureUserAuthenticated;
6
- /** Test injection point for the agent identity gate. */
7
6
  agentGate?: typeof ensureAgentIdentity;
7
+ subAgentGate?: typeof ensureSubAgentIdentity;
8
8
  }
9
- /**
10
- * Route a Codex hook event to the appropriate Ory integration.
11
- *
12
- * Codex events: SessionStart, PreToolUse, PostToolUse, PermissionRequest,
13
- * UserPromptSubmit, Stop. Hooks are declared by the plugin (`hooks` in
14
- * plugin.json → hooks.json) and must be trusted by the user before they run.
15
- */
16
9
  export declare function handleHookEvent(input: CodexHookInput, client: OryAgentClient, deps?: HandleHookEventDeps): Promise<CodexHookOutput>;
17
- /**
18
- * Whether a hook output carries anything Codex needs to see on stdout.
19
- *
20
- * The hook entry point handles `decision === "block"` (exit code 2 + stdout)
21
- * before consulting this. Everything else with at least one populated field
22
- * must be written to stdout — most importantly `hookSpecificOutput`, which
23
- * carries the PermissionRequest allow/deny decision. Empty outputs stay
24
- * silent to keep stdout clean for the harness.
25
- */
26
10
  export declare function shouldEmitOutput(output: CodexHookOutput): boolean;
package/dist/handlers.js CHANGED
@@ -3,14 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.handleHookEvent = handleHookEvent;
4
4
  exports.shouldEmitOutput = shouldEmitOutput;
5
5
  const argus_1 = require("@ory/argus");
6
- /**
7
- * Route a Codex hook event to the appropriate Ory integration.
8
- *
9
- * Codex events: SessionStart, PreToolUse, PostToolUse, PermissionRequest,
10
- * UserPromptSubmit, Stop. Hooks are declared by the plugin (`hooks` in
11
- * plugin.json → hooks.json) and must be trusted by the user before they run.
12
- */
13
- async function handleHookEvent(input, client, deps = {}) {
6
+ const session_state_js_1 = require("./session-state.js");
7
+ const HARNESS = "codex";
8
+ const SPAWN_PARENT_FLUSH_TIMEOUT_MS = 500;
9
+ const SPAWN_IDENTITY_TIMEOUT_MS = 10_000;
10
+ function handleHookEvent(input, client, deps = {}) {
14
11
  const event = input.hook_event_name;
15
12
  client.logger.debug("hook.received", {
16
13
  event,
@@ -18,73 +15,54 @@ async function handleHookEvent(input, client, deps = {}) {
18
15
  toolName: input.tool_name,
19
16
  turnId: input.turn_id,
20
17
  });
21
- // Set trace context so all spans within this hook invocation correlate
22
- client.tracer.setContext({
23
- traceId: (0, argus_1.deriveTraceId)(input.session_id),
24
- sessionId: input.session_id,
25
- });
26
- try {
18
+ return (0, argus_1.withHookContext)(client, { sessionId: input.session_id }, async () => {
27
19
  switch (event) {
28
20
  case "SessionStart":
29
- return await handleSessionStart(input, client, deps);
21
+ return handleSessionStart(input, client, deps);
30
22
  case "PreToolUse":
31
- return await handlePreToolUse(input, client);
23
+ return handlePreToolUse(input, client);
32
24
  case "PostToolUse":
33
- return await handlePostToolUse(input, client);
25
+ return handlePostToolUse(input, client, deps);
34
26
  case "PermissionRequest":
35
- return await handlePermissionRequest(input, client);
27
+ return handlePermissionRequest(input, client);
36
28
  case "UserPromptSubmit":
37
- return await handleUserPromptSubmit(input, client);
29
+ return handleUserPromptSubmit(input, client, deps);
38
30
  case "Stop":
39
- return await handleStop(input, client);
31
+ return handleStop(input, client);
40
32
  default:
41
33
  client.logger.debug("hook.passthrough", { event });
42
- client.tracer.record("hook.passthrough", "skipped", {
43
- attributes: { event },
44
- });
34
+ client.logger.activity("hook.passthrough", "skipped", { attributes: { event } });
45
35
  return {};
46
36
  }
47
- }
48
- finally {
49
- client.tracer.clearContext();
50
- }
37
+ });
51
38
  }
52
- // ─── SessionStart ───────────────────────────────────────────────────
53
39
  async function handleSessionStart(input, client, deps) {
54
40
  client.logger.info("lifecycle.session_start", {
55
41
  sessionId: input.session_id,
56
42
  model: input.model,
57
43
  cwd: input.cwd,
58
44
  });
59
- client.tracer.record("session.start", "ok", {
60
- attributes: { model: input.model },
61
- });
62
- // The user login flow runs on every session and never blocks. It refreshes
63
- // persisted tokens / launches the PKCE login and populates the user
64
- // principal; enforcement is governed solely by permissionMode at tool-call
65
- // time.
66
- const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
67
- await userGate(client, {
45
+ await startSession(input, client, deps);
46
+ return {};
47
+ }
48
+ async function startSession(input, client, deps) {
49
+ await (0, argus_1.sessionStart)(client, {
50
+ harness: HARNESS,
68
51
  binName: "ory-codex",
69
- harness: "codex",
52
+ userLogin: deps.userLogin,
53
+ agentGate: deps.agentGate,
54
+ activityAttributes: { model: input.model },
70
55
  });
71
- // Resolve the agent identity (machine credentials). Never blocks;
72
- // attaches the agent's bearer token to outgoing Ory API calls.
73
- const agentGate = deps.agentGate ?? argus_1.ensureAgentIdentity;
74
- await agentGate(client, { projectUrl: (0, argus_1.resolveConfig)().projectUrl, harness: "codex" });
75
- // Record the user→agent delegation so the audit trail captures that
76
- // this user authorized this agent for this session. Written at most once
77
- // per install. Best-effort: requires both principals to be populated, and
78
- // any failure is logged and swallowed (fail-open — delegation tracking is
79
- // for audit, not enforcement).
80
- await (0, argus_1.writeUserDelegatesAgent)(client);
81
- return {};
56
+ try {
57
+ (0, session_state_js_1.markCodexSessionStarted)(input.session_id);
58
+ }
59
+ catch (err) {
60
+ client.logger.warn("session_start.marker_write_failed", {
61
+ message: err instanceof Error ? err.message : String(err),
62
+ });
63
+ }
82
64
  }
83
- // ─── PreToolUse ─────────────────────────────────────────────────────
84
65
  async function handlePreToolUse(input, client) {
85
- // No default. Codex always sets `tool_name` to the canonical value
86
- // (`Bash`, `apply_patch`, or `mcp__<server>__<tool>`). Defaulting to
87
- // `"Bash"` here would silently mislabel apply_patch and MCP calls.
88
66
  const toolName = input.tool_name;
89
67
  if (!toolName) {
90
68
  client.logger.warn("pre_tool_use.missing_tool_name", {
@@ -98,112 +76,21 @@ async function handlePreToolUse(input, client) {
98
76
  toolName,
99
77
  toolInput: input.tool_input,
100
78
  });
101
- const inputSummary = (0, argus_1.summarizeToolInput)(toolName, input.tool_input);
102
- // In audit-only mode, log the invocation but skip permission checks
103
- if ((0, argus_1.resolveConfig)().auditOnly) {
104
- client.tracer.record("tool.invoke", "ok", {
105
- attributes: { toolName, ...inputSummary },
106
- });
107
- return {};
108
- }
109
- const subject = (0, argus_1.resolveUserSubject)(client, `session:${input.session_id}`);
110
- const subjectId = (0, argus_1.subjectLabel)(subject);
111
- const mcpTool = (0, argus_1.parseMcpToolGeneric)(toolName);
112
- try {
113
- if (mcpTool) {
114
- const mcpResult = await (0, argus_1.checkMcpPermission)(client, mcpTool, {
115
- subject,
116
- spanAttributes: { toolName },
117
- });
118
- const mcpAttrs = {
119
- toolName,
120
- mcpServer: mcpTool.serverName,
121
- mcpTool: mcpTool.toolName,
122
- ...inputSummary,
123
- };
124
- const decision = (0, argus_1.applyPermissionMode)(client, mcpResult.allowed, {
125
- object: mcpTool.serverName,
126
- relation: "use",
127
- subjectId,
128
- ...("subjectSet" in subject ? { subjectSet: subject.subjectSet } : {}),
129
- spanAttributes: mcpAttrs,
130
- });
131
- const decisionAttrs = decision.spanAttributes;
132
- if (decision.kind === "allow") {
133
- client.tracer.record("tool.invoke", "ok", {
134
- attributes: { ...mcpAttrs, ...decisionAttrs },
135
- });
136
- return {};
137
- }
138
- if (decision.kind === "observe") {
139
- client.tracer.record("tool.block", "denied", {
140
- attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
141
- });
142
- client.tracer.record("tool.invoke", "ok", {
143
- attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, observed: true },
144
- });
145
- return {};
146
- }
147
- client.tracer.record("tool.block", "denied", {
148
- attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
149
- });
150
- return {
151
- decision: "block",
152
- reason: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, mcp: mcpTool }),
153
- };
154
- }
155
- const namespace = resolveNamespace();
156
- const outcome = await (0, argus_1.gateToolCall)(client, {
157
- harness: "codex",
158
- toolName,
159
- check: { namespace, object: toolName, relation: "use", ...subject },
160
- spanAttributes: { toolName },
161
- });
162
- // Interactive tools (operator-extensible via ORY_INTERACTIVE_TOOLS):
163
- // pass through with the user.interaction span already recorded.
164
- if (outcome.kind === "interactive") {
165
- return {};
166
- }
167
- const decision = outcome;
168
- if (decision.kind === "fail_open") {
169
- return handlePermissionError(decision.error, toolName, client);
170
- }
171
- const attrs = { toolName, ...inputSummary };
172
- const decisionAttrs = decision.spanAttributes;
173
- if (decision.kind === "allow") {
174
- client.tracer.record("tool.invoke", "ok", {
175
- attributes: { ...attrs, ...decisionAttrs, allowed: true },
176
- });
177
- return {};
178
- }
179
- if (decision.kind === "observe") {
180
- client.tracer.record("tool.block", "denied", {
181
- attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
182
- });
183
- client.tracer.record("tool.invoke", "ok", {
184
- attributes: { ...attrs, ...decisionAttrs, allowed: false, observed: true },
185
- });
186
- return {};
187
- }
188
- client.tracer.record("tool.block", "denied", {
189
- attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
190
- });
191
- return {
192
- decision: "block",
193
- reason: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }),
194
- };
195
- }
196
- catch (err) {
197
- return handlePermissionError(err, toolName, client);
198
- }
79
+ const result = await (0, argus_1.gate)(client, {
80
+ harness: HARNESS,
81
+ toolName,
82
+ toolArgs: input.tool_input,
83
+ subjectFallback: `session:${input.session_id}`,
84
+ mcpTool: (0, argus_1.parseMcpToolGeneric)(toolName) ?? undefined,
85
+ });
86
+ return result.blocked
87
+ ? { decision: "block", reason: result.denialMessage }
88
+ : {};
199
89
  }
200
- // ─── PostToolUse ────────────────────────────────────────────────────
201
- async function handlePostToolUse(input, client) {
90
+ async function handlePostToolUse(input, client, deps) {
202
91
  const toolName = input.tool_name;
203
92
  if (!toolName) {
204
- client.logger.warn("post_tool_use.missing_tool_name", {
205
- sessionId: input.session_id,
206
- });
93
+ client.logger.warn("post_tool_use.missing_tool_name", { sessionId: input.session_id });
207
94
  return {};
208
95
  }
209
96
  client.logger.info("lifecycle.post_tool_use", {
@@ -212,24 +99,85 @@ async function handlePostToolUse(input, client) {
212
99
  turnId: input.turn_id,
213
100
  toolUseId: input.tool_use_id,
214
101
  });
215
- client.tracer.record("tool.complete", "ok", {
216
- attributes: {
217
- toolName,
218
- turnId: input.turn_id,
219
- toolUseId: input.tool_use_id,
220
- ...(0, argus_1.summarizeToolInput)(toolName, input.tool_input),
221
- ...(0, argus_1.summarizeToolOutput)(toolName, input.tool_response),
222
- },
102
+ const collaborationSpawn = isCollaborationSpawn(toolName);
103
+ const spawnSignal = collaborationSpawn
104
+ ? AbortSignal.timeout(SPAWN_IDENTITY_TIMEOUT_MS)
105
+ : undefined;
106
+ if (collaborationSpawn) {
107
+ await (0, argus_1.ensureReadCredential)(client, {
108
+ harness: HARNESS,
109
+ agentGate: deps.agentGate,
110
+ signal: spawnSignal,
111
+ });
112
+ }
113
+ (0, argus_1.complete)(client, {
114
+ toolName,
115
+ input: input.tool_input,
116
+ output: input.tool_response,
117
+ extraActivityAttributes: { turnId: input.turn_id, toolUseId: input.tool_use_id },
223
118
  });
119
+ if (collaborationSpawn) {
120
+ // Keep the spawn tool's events attributed to the parent before switching
121
+ // the reporter to the newly authenticated Sub-Agent.
122
+ await client.flushEvents(SPAWN_PARENT_FLUSH_TIMEOUT_MS);
123
+ const subAgentType = "collaboration";
124
+ let subAgentKind;
125
+ let subAgentReason;
126
+ let subAgentSubject;
127
+ let subAgentToken;
128
+ const subAgentGate = deps.subAgentGate ?? argus_1.ensureSubAgentIdentity;
129
+ await (0, argus_1.registerSubagent)(client, {
130
+ harness: HARNESS,
131
+ subAgentType,
132
+ perSpawnId: input.tool_use_id,
133
+ signal: spawnSignal,
134
+ subAgentGate: async (gateClient, options) => {
135
+ const identity = await subAgentGate(gateClient, {
136
+ ...options,
137
+ emitActivity: false,
138
+ });
139
+ subAgentKind = identity.kind;
140
+ subAgentReason = identity.reason;
141
+ subAgentSubject = identity.subject;
142
+ subAgentToken = identity.token;
143
+ return identity;
144
+ },
145
+ });
146
+ if (subAgentKind === "runtime" || (subAgentSubject && subAgentToken)) {
147
+ if (subAgentSubject && subAgentToken) {
148
+ client.setAgentPrincipal({ subject: subAgentSubject, token: subAgentToken });
149
+ }
150
+ client.logger.activity("agent.auth", "ok", {
151
+ attributes: {
152
+ kind: subAgentKind === "runtime" ? "subagent_runtime" : "subagent_dynamic",
153
+ subAgentType,
154
+ ...(subAgentSubject ? { subject: subAgentSubject } : {}),
155
+ },
156
+ });
157
+ client.logger.activity("subagent.start", "ok", {
158
+ attributes: { subAgentType, agentId: input.tool_use_id },
159
+ });
160
+ }
161
+ else {
162
+ client.logger.activity("agent.auth", "skipped", {
163
+ attributes: {
164
+ kind: subAgentKind === "dynamic" ? "subagent_dynamic" : "subagent_none",
165
+ subAgentType,
166
+ reason: subAgentReason,
167
+ },
168
+ });
169
+ }
170
+ }
224
171
  return {};
225
172
  }
226
- // ─── PermissionRequest ──────────────────────────────────────────────
173
+ function isCollaborationSpawn(toolName) {
174
+ const normalized = toolName.toLowerCase().replace(/[^a-z0-9]/g, "");
175
+ return normalized === "collaborationspawnagent" || normalized === "spawnagent";
176
+ }
227
177
  async function handlePermissionRequest(input, client) {
228
178
  const toolName = input.tool_name;
229
179
  if (!toolName) {
230
- client.logger.warn("permission_request.missing_tool_name", {
231
- sessionId: input.session_id,
232
- });
180
+ client.logger.warn("permission_request.missing_tool_name", { sessionId: input.session_id });
233
181
  return {};
234
182
  }
235
183
  client.logger.info("lifecycle.permission_request", {
@@ -237,71 +185,31 @@ async function handlePermissionRequest(input, client) {
237
185
  toolName,
238
186
  turnId: input.turn_id,
239
187
  });
240
- if ((0, argus_1.resolveConfig)().auditOnly) {
241
- return permissionRequestFallback("Audit-only mode — deferring to user");
188
+ const mcpTool = (0, argus_1.parseMcpToolGeneric)(toolName);
189
+ const result = await (0, argus_1.decideTool)(client, {
190
+ harness: HARNESS,
191
+ toolName,
192
+ toolArgs: input.tool_input,
193
+ subjectFallback: `session:${input.session_id}`,
194
+ mcpTool: mcpTool ?? undefined,
195
+ });
196
+ if (result.kind === "not_connected") {
197
+ return permissionRequestFallback("Ory Agent Security not connected — deferring to user");
242
198
  }
243
- // Interactive tools: defer to the user instead of pre-answering.
244
- if ((0, argus_1.isInteractiveTool)("codex", toolName)) {
245
- client.tracer.record("user.interaction", "ok", {
246
- attributes: { harness: "codex", toolName, kind: "permission-request" },
247
- });
199
+ if (result.kind === "interactive") {
248
200
  return permissionRequestFallback("Interactive tool — deferring to user");
249
201
  }
250
- const subject = (0, argus_1.resolveUserSubject)(client, `session:${input.session_id}`);
251
- const subjectId = (0, argus_1.subjectLabel)(subject);
252
- const mcpTool = (0, argus_1.parseMcpToolGeneric)(toolName);
253
- try {
254
- if (mcpTool) {
255
- const mcpResult = await (0, argus_1.checkMcpPermission)(client, mcpTool, {
256
- subject,
257
- spanAttributes: { toolName },
258
- });
259
- const decision = (0, argus_1.applyPermissionMode)(client, mcpResult.allowed, {
260
- object: mcpTool.serverName,
261
- relation: "use",
262
- subjectId,
263
- ...("subjectSet" in subject ? { subjectSet: subject.subjectSet } : {}),
264
- spanAttributes: { toolName, mcpServer: mcpTool.serverName, mcpTool: mcpTool.toolName },
265
- });
266
- if (decision.kind === "deny") {
267
- return permissionRequestDecision("deny", (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, mcp: mcpTool }));
268
- }
269
- return permissionRequestDecision("allow", decision.kind === "observe"
270
- ? "Ory observe mode — denied by policy but allowed through"
271
- : "Ory MCP server permission granted");
272
- }
273
- const namespace = resolveNamespace();
274
- const outcome = await (0, argus_1.gateToolCall)(client, {
275
- harness: "codex",
276
- toolName,
277
- check: { namespace, object: toolName, relation: "use", ...subject },
278
- spanAttributes: { toolName },
279
- });
280
- if (outcome.kind === "interactive") {
281
- return permissionRequestFallback("Interactive tool — deferring to user");
282
- }
283
- const decision = outcome;
284
- if (decision.kind === "fail_open") {
285
- const code = decision.error.code;
286
- const reason = code === "network_error" || code === "rate_limited"
287
- ? `Ory unavailable (${code}), falling back to user prompt`
288
- : `Ory permission check failed: ${decision.error.message}`;
289
- return permissionRequestFallback(reason);
290
- }
291
- if (decision.kind === "deny") {
292
- return permissionRequestDecision("deny", (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }));
293
- }
294
- return permissionRequestDecision("allow", decision.kind === "observe"
295
- ? "Ory observe mode — denied by policy but allowed through"
296
- : "Ory permission granted");
202
+ if (result.kind === "fail_open") {
203
+ return permissionRequestFallback(permissionFailureReason(result.decision));
297
204
  }
298
- catch (err) {
299
- const oryErr = err;
300
- const reason = oryErr.code === "network_error" || oryErr.code === "rate_limited"
301
- ? `Ory unavailable (${oryErr.code}), falling back to user prompt`
302
- : `Ory permission check failed: ${oryErr.message}`;
303
- return permissionRequestFallback(reason);
205
+ if (result.kind === "deny") {
206
+ return permissionRequestDecision("deny", result.denialMessage ?? "Ory permission denied");
304
207
  }
208
+ return permissionRequestDecision("allow", result.kind === "observe"
209
+ ? "Ory observe mode — denied by policy but allowed through"
210
+ : mcpTool
211
+ ? "Ory MCP server permission granted"
212
+ : "Ory permission granted");
305
213
  }
306
214
  function permissionRequestDecision(behavior, message) {
307
215
  return {
@@ -312,36 +220,48 @@ function permissionRequestDecision(behavior, message) {
312
220
  };
313
221
  }
314
222
  function permissionRequestFallback(message) {
315
- return {
316
- hookSpecificOutput: {
317
- hookEventName: "PermissionRequest",
318
- message,
319
- },
320
- };
223
+ return { hookSpecificOutput: { hookEventName: "PermissionRequest", message } };
224
+ }
225
+ function permissionFailureReason(decision) {
226
+ if (decision.kind !== "fail_open")
227
+ return "Ory permission check failed";
228
+ const { code, message } = decision.error;
229
+ return code === "network_error" || code === "rate_limited"
230
+ ? `Ory unavailable (${code}), falling back to user prompt`
231
+ : `Ory permission check failed: ${message}`;
321
232
  }
322
- // ─── UserPromptSubmit ───────────────────────────────────────────────
323
- async function handleUserPromptSubmit(input, client) {
233
+ async function handleUserPromptSubmit(input, client, deps) {
234
+ let sessionStarted = false;
235
+ try {
236
+ sessionStarted = (0, session_state_js_1.hasCodexSessionStarted)(input.session_id);
237
+ }
238
+ catch (err) {
239
+ client.logger.warn("session_start.marker_read_failed", {
240
+ message: err instanceof Error ? err.message : String(err),
241
+ });
242
+ }
243
+ if (!sessionStarted) {
244
+ // Codex consumes SessionStart only once. If the plugin was installed or
245
+ // trusted afterward, recover before processing the next prompt.
246
+ await startSession(input, client, deps);
247
+ }
324
248
  client.logger.info("lifecycle.user_prompt_submit", {
325
249
  sessionId: input.session_id,
326
250
  turnId: input.turn_id,
327
251
  promptLen: input.prompt?.length,
328
252
  });
329
- client.tracer.record("user.prompt", "ok", {
330
- attributes: {
331
- turnId: input.turn_id,
332
- promptLen: input.prompt?.length,
333
- },
253
+ client.logger.activity("user.prompt", "ok", {
254
+ attributes: { turnId: input.turn_id, promptLen: input.prompt?.length },
334
255
  });
335
256
  return {};
336
257
  }
337
- // ─── Stop ───────────────────────────────────────────────────────────
338
258
  async function handleStop(input, client) {
339
259
  client.logger.info("lifecycle.stop", {
340
260
  sessionId: input.session_id,
341
261
  turnId: input.turn_id,
342
262
  stopHookActive: input.stop_hook_active,
343
263
  });
344
- client.tracer.record("turn.stop", "ok", {
264
+ client.logger.activity("turn.stop", "ok", {
345
265
  attributes: {
346
266
  turnId: input.turn_id,
347
267
  stopHookActive: input.stop_hook_active,
@@ -350,42 +270,6 @@ async function handleStop(input, client) {
350
270
  });
351
271
  return {};
352
272
  }
353
- // ─── Helpers ────────────────────────────────────────────────────────
354
- /**
355
- * Whether a hook output carries anything Codex needs to see on stdout.
356
- *
357
- * The hook entry point handles `decision === "block"` (exit code 2 + stdout)
358
- * before consulting this. Everything else with at least one populated field
359
- * must be written to stdout — most importantly `hookSpecificOutput`, which
360
- * carries the PermissionRequest allow/deny decision. Empty outputs stay
361
- * silent to keep stdout clean for the harness.
362
- */
363
273
  function shouldEmitOutput(output) {
364
274
  return Object.values(output).some((value) => value !== undefined);
365
275
  }
366
- function resolveNamespace() {
367
- return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
368
- }
369
- function handlePermissionError(oryErr, toolName, client) {
370
- if (oryErr.code === "network_error") {
371
- client.logger.warn("permission.network_error", {
372
- toolName,
373
- message: "Ory unreachable, failing open",
374
- });
375
- return {};
376
- }
377
- if (oryErr.code === "rate_limited") {
378
- client.logger.warn("permission.rate_limited", {
379
- toolName,
380
- message: "Ory rate limited, failing open",
381
- });
382
- return {};
383
- }
384
- // For other errors, also fail open but log prominently
385
- client.logger.error("permission.check.error", {
386
- toolName,
387
- code: oryErr.code,
388
- message: oryErr.message,
389
- });
390
- return {};
391
- }
package/dist/hook.js CHANGED
@@ -10,6 +10,16 @@
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
11
  const argus_1 = require("@ory/argus");
12
12
  const handlers_js_1 = require("./handlers.js");
13
+ const EVENT_FLUSH_TIMEOUT_MS = 750;
14
+ let client;
15
+ async function flushEvents() {
16
+ try {
17
+ await client?.flushEvents(EVENT_FLUSH_TIMEOUT_MS);
18
+ }
19
+ catch {
20
+ // The hook decision must not depend on activity delivery.
21
+ }
22
+ }
13
23
  /**
14
24
  * Read all of stdin as a string.
15
25
  *
@@ -21,29 +31,42 @@ function readStdin() {
21
31
  return new Promise((resolve, reject) => {
22
32
  const chunks = [];
23
33
  let resolved = false;
34
+ let idleTimer;
35
+ function cleanup() {
36
+ if (idleTimer)
37
+ clearTimeout(idleTimer);
38
+ process.stdin.off("data", onData);
39
+ process.stdin.off("end", done);
40
+ process.stdin.off("error", onError);
41
+ process.stdin.pause();
42
+ }
24
43
  function done() {
25
44
  if (!resolved) {
26
45
  resolved = true;
46
+ cleanup();
27
47
  resolve(Buffer.concat(chunks).toString("utf-8"));
28
48
  }
29
49
  }
30
- process.stdin.on("data", (chunk) => {
50
+ function onData(chunk) {
31
51
  chunks.push(chunk);
32
- clearTimeout(idleTimer);
52
+ if (idleTimer)
53
+ clearTimeout(idleTimer);
33
54
  idleTimer = setTimeout(done, 100);
34
- });
35
- process.stdin.on("end", done);
36
- process.stdin.on("error", (err) => {
55
+ }
56
+ function onError(err) {
37
57
  if (!resolved) {
38
58
  resolved = true;
59
+ cleanup();
39
60
  reject(err);
40
61
  }
41
- });
42
- let idleTimer;
62
+ }
63
+ process.stdin.on("data", onData);
64
+ process.stdin.on("end", done);
65
+ process.stdin.on("error", onError);
43
66
  });
44
67
  }
45
68
  async function main() {
46
- const client = argus_1.OryAgentClient.fromEnv("codex");
69
+ client = argus_1.OryAgentClient.fromEnv("codex");
47
70
  const raw = await readStdin();
48
71
  let input;
49
72
  try {
@@ -51,24 +74,26 @@ async function main() {
51
74
  }
52
75
  catch {
53
76
  client.logger.error("hook.stdin.parse_failed", { raw: raw.slice(0, 200) });
54
- await client.tracer.shutdown();
55
- process.exit(0); // Don't block on parse errors
77
+ process.exitCode = 0; // Don't block on parse errors
78
+ return;
56
79
  }
57
80
  const output = await (0, handlers_js_1.handleHookEvent)(input, client);
58
81
  if (output.decision === "block") {
59
82
  // Codex reads the block reason from stderr
60
83
  process.stderr.write(output.reason ?? "Blocked by Ory agent plugin");
61
84
  process.stdout.write(JSON.stringify(output));
62
- await client.tracer.shutdown();
63
- process.exit(2);
85
+ await flushEvents();
86
+ process.exitCode = 2;
87
+ return;
64
88
  }
65
89
  if ((0, handlers_js_1.shouldEmitOutput)(output)) {
66
90
  process.stdout.write(JSON.stringify(output));
67
91
  }
68
- await client.tracer.shutdown();
69
- process.exit(0);
92
+ await flushEvents();
93
+ process.exitCode = 0;
70
94
  }
71
- main().catch((err) => {
95
+ main().catch(async (err) => {
72
96
  process.stderr.write(`[ory-agent] fatal: ${err}\n`);
73
- process.exit(0); // Fail open
97
+ await flushEvents();
98
+ process.exitCode = 0; // Fail open
74
99
  });