@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.
- package/README.md +72 -67
- package/dist/cli/main.d.ts +1 -1
- package/dist/cli/main.js +33 -18
- package/dist/cli/setup.d.ts +2 -1
- package/dist/cli/setup.js +93 -38
- package/dist/handlers.d.ts +3 -19
- package/dist/handlers.js +163 -279
- package/dist/hook.js +41 -16
- package/dist/session-state.d.ts +3 -0
- package/dist/session-state.js +61 -0
- package/dist/types.d.ts +1 -0
- package/marketplace/plugins/ory-codex/.codex-plugin/plugin.json +1 -1
- package/package.json +9 -8
package/dist/handlers.d.ts
CHANGED
|
@@ -1,26 +1,10 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
|
21
|
+
return handleSessionStart(input, client, deps);
|
|
30
22
|
case "PreToolUse":
|
|
31
|
-
return
|
|
23
|
+
return handlePreToolUse(input, client);
|
|
32
24
|
case "PostToolUse":
|
|
33
|
-
return
|
|
25
|
+
return handlePostToolUse(input, client, deps);
|
|
34
26
|
case "PermissionRequest":
|
|
35
|
-
return
|
|
27
|
+
return handlePermissionRequest(input, client);
|
|
36
28
|
case "UserPromptSubmit":
|
|
37
|
-
return
|
|
29
|
+
return handleUserPromptSubmit(input, client, deps);
|
|
38
30
|
case "Stop":
|
|
39
|
-
return
|
|
31
|
+
return handleStop(input, client);
|
|
40
32
|
default:
|
|
41
33
|
client.logger.debug("hook.passthrough", { event });
|
|
42
|
-
client.
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
52
|
+
userLogin: deps.userLogin,
|
|
53
|
+
agentGate: deps.agentGate,
|
|
54
|
+
activityAttributes: { model: input.model },
|
|
70
55
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
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
|
-
|
|
299
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
323
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
50
|
+
function onData(chunk) {
|
|
31
51
|
chunks.push(chunk);
|
|
32
|
-
|
|
52
|
+
if (idleTimer)
|
|
53
|
+
clearTimeout(idleTimer);
|
|
33
54
|
idleTimer = setTimeout(done, 100);
|
|
34
|
-
}
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
|
63
|
-
process.
|
|
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
|
|
69
|
-
process.
|
|
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
|
-
|
|
97
|
+
await flushEvents();
|
|
98
|
+
process.exitCode = 0; // Fail open
|
|
74
99
|
});
|