@ory/codex 0.13.9 → 1.0.0
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 -64
- package/dist/cli/main.d.ts +1 -1
- package/dist/cli/main.js +33 -11
- package/dist/cli/setup.d.ts +2 -1
- package/dist/cli/setup.js +94 -39
- package/dist/handlers.d.ts +3 -19
- package/dist/handlers.js +158 -346
- 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.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,145 +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
|
-
attributes: { model: input.model },
|
|
61
|
-
});
|
|
62
|
-
const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
|
|
63
|
-
const decision = await userGate(client, {
|
|
64
|
-
binName: "ory-codex",
|
|
65
|
-
harness: "codex",
|
|
66
|
-
allowBlock: true,
|
|
67
|
-
});
|
|
68
|
-
// Resolve the agent identity (machine credentials). Never blocks;
|
|
69
|
-
// attaches the agent's bearer token to outgoing Ory API calls.
|
|
70
|
-
const agentGate = deps.agentGate ?? argus_1.ensureAgentIdentity;
|
|
71
|
-
await agentGate(client, { projectUrl: (0, argus_1.resolveConfig)().projectUrl, harness: "codex" });
|
|
72
|
-
// Record the user→agent delegation so the audit trail captures that
|
|
73
|
-
// this user authorized this agent for this session. Written at most once
|
|
74
|
-
// per install. Best-effort: requires both principals to be populated, and
|
|
75
|
-
// any failure is logged and swallowed (fail-open — delegation tracking is
|
|
76
|
-
// for audit, not enforcement).
|
|
77
|
-
await (0, argus_1.writeUserDelegatesAgent)(client);
|
|
78
|
-
if (!decision.proceed) {
|
|
79
|
-
return { decision: "block", reason: decision.reason };
|
|
80
|
-
}
|
|
81
|
-
if (decision.mode !== "disabled") {
|
|
82
|
-
return {};
|
|
83
|
-
}
|
|
84
|
-
const resolved = (0, argus_1.resolveConfig)();
|
|
85
|
-
if (resolved.auditOnly) {
|
|
86
|
-
client.logger.info("config.audit_only", {
|
|
87
|
-
message: "Audit-only mode enabled. Auth and permission checks are disabled.",
|
|
88
|
-
});
|
|
89
|
-
return {};
|
|
90
|
-
}
|
|
91
|
-
if (!resolved.projectUrl) {
|
|
92
|
-
client.logger.warn("config.not_configured", {
|
|
93
|
-
message: "Ory plugin is not configured. Auth and permission checks are disabled. " +
|
|
94
|
-
"Run 'npx ory-codex configure' to connect to an Ory project.",
|
|
95
|
-
});
|
|
96
|
-
return {};
|
|
97
|
-
}
|
|
98
|
-
// Try session token first, then OAuth2 token
|
|
99
|
-
const sessionToken = process.env.ORY_SESSION_TOKEN;
|
|
100
|
-
const oauth2Token = process.env.ORY_OAUTH2_TOKEN;
|
|
101
|
-
if (sessionToken) {
|
|
102
|
-
await verifySessionToken(sessionToken, client);
|
|
103
|
-
return {};
|
|
104
|
-
}
|
|
105
|
-
if (oauth2Token) {
|
|
106
|
-
await verifyOAuth2Token(oauth2Token, client);
|
|
107
|
-
return {};
|
|
108
|
-
}
|
|
109
|
-
client.logger.warn("session.no_credentials", {
|
|
110
|
-
message: "Neither ORY_SESSION_TOKEN nor ORY_OAUTH2_TOKEN is set. " +
|
|
111
|
-
"Skipping authentication.",
|
|
112
|
-
});
|
|
45
|
+
await startSession(input, client, deps);
|
|
113
46
|
return {};
|
|
114
47
|
}
|
|
115
|
-
async function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
catch (err) {
|
|
125
|
-
const oryErr = err;
|
|
126
|
-
client.logger.warn("session.verify_failed", {
|
|
127
|
-
code: oryErr.code,
|
|
128
|
-
message: oryErr.message,
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
async function verifyOAuth2Token(token, client) {
|
|
48
|
+
async function startSession(input, client, deps) {
|
|
49
|
+
await (0, argus_1.sessionStart)(client, {
|
|
50
|
+
harness: HARNESS,
|
|
51
|
+
binName: "ory-codex",
|
|
52
|
+
userLogin: deps.userLogin,
|
|
53
|
+
agentGate: deps.agentGate,
|
|
54
|
+
activityAttributes: { model: input.model },
|
|
55
|
+
});
|
|
133
56
|
try {
|
|
134
|
-
|
|
135
|
-
if (!tokenInfo.active) {
|
|
136
|
-
client.logger.warn("oauth2.token_inactive", {
|
|
137
|
-
message: "Ory OAuth2 token is not active. Obtain a new token to enable auth checks.",
|
|
138
|
-
});
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
client.logger.info("oauth2.session_authenticated", {
|
|
142
|
-
clientId: tokenInfo.clientId,
|
|
143
|
-
subject: tokenInfo.subject,
|
|
144
|
-
scope: tokenInfo.scope,
|
|
145
|
-
});
|
|
57
|
+
(0, session_state_js_1.markCodexSessionStarted)(input.session_id);
|
|
146
58
|
}
|
|
147
59
|
catch (err) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
code: oryErr.code,
|
|
151
|
-
message: oryErr.message,
|
|
60
|
+
client.logger.warn("session_start.marker_write_failed", {
|
|
61
|
+
message: err instanceof Error ? err.message : String(err),
|
|
152
62
|
});
|
|
153
63
|
}
|
|
154
64
|
}
|
|
155
|
-
// ─── PreToolUse ─────────────────────────────────────────────────────
|
|
156
65
|
async function handlePreToolUse(input, client) {
|
|
157
|
-
// No default. Codex always sets `tool_name` to the canonical value
|
|
158
|
-
// (`Bash`, `apply_patch`, or `mcp__<server>__<tool>`). Defaulting to
|
|
159
|
-
// `"Bash"` here would silently mislabel apply_patch and MCP calls.
|
|
160
66
|
const toolName = input.tool_name;
|
|
161
67
|
if (!toolName) {
|
|
162
68
|
client.logger.warn("pre_tool_use.missing_tool_name", {
|
|
@@ -170,112 +76,21 @@ async function handlePreToolUse(input, client) {
|
|
|
170
76
|
toolName,
|
|
171
77
|
toolInput: input.tool_input,
|
|
172
78
|
});
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const mcpTool = (0, argus_1.parseMcpToolGeneric)(toolName);
|
|
184
|
-
try {
|
|
185
|
-
if (mcpTool) {
|
|
186
|
-
const mcpResult = await (0, argus_1.checkMcpPermission)(client, mcpTool, {
|
|
187
|
-
subject,
|
|
188
|
-
spanAttributes: { toolName },
|
|
189
|
-
});
|
|
190
|
-
const mcpAttrs = {
|
|
191
|
-
toolName,
|
|
192
|
-
mcpServer: mcpTool.serverName,
|
|
193
|
-
mcpTool: mcpTool.toolName,
|
|
194
|
-
...inputSummary,
|
|
195
|
-
};
|
|
196
|
-
const decision = (0, argus_1.applyPermissionMode)(client, mcpResult.allowed, {
|
|
197
|
-
object: mcpTool.serverName,
|
|
198
|
-
relation: "use",
|
|
199
|
-
subjectId,
|
|
200
|
-
...("subjectSet" in subject ? { subjectSet: subject.subjectSet } : {}),
|
|
201
|
-
spanAttributes: mcpAttrs,
|
|
202
|
-
});
|
|
203
|
-
const decisionAttrs = decision.spanAttributes;
|
|
204
|
-
if (decision.kind === "allow") {
|
|
205
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
206
|
-
attributes: { ...mcpAttrs, ...decisionAttrs },
|
|
207
|
-
});
|
|
208
|
-
return {};
|
|
209
|
-
}
|
|
210
|
-
if (decision.kind === "observe") {
|
|
211
|
-
client.tracer.record("tool.block", "denied", {
|
|
212
|
-
attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
|
|
213
|
-
});
|
|
214
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
215
|
-
attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, observed: true },
|
|
216
|
-
});
|
|
217
|
-
return {};
|
|
218
|
-
}
|
|
219
|
-
client.tracer.record("tool.block", "denied", {
|
|
220
|
-
attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
|
|
221
|
-
});
|
|
222
|
-
return {
|
|
223
|
-
decision: "block",
|
|
224
|
-
reason: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, mcp: mcpTool }),
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
const namespace = resolveNamespace();
|
|
228
|
-
const outcome = await (0, argus_1.gateToolCall)(client, {
|
|
229
|
-
harness: "codex",
|
|
230
|
-
toolName,
|
|
231
|
-
check: { namespace, object: toolName, relation: "use", ...subject },
|
|
232
|
-
spanAttributes: { toolName },
|
|
233
|
-
});
|
|
234
|
-
// Interactive tools (operator-extensible via ORY_INTERACTIVE_TOOLS):
|
|
235
|
-
// pass through with the user.interaction span already recorded.
|
|
236
|
-
if (outcome.kind === "interactive") {
|
|
237
|
-
return {};
|
|
238
|
-
}
|
|
239
|
-
const decision = outcome;
|
|
240
|
-
if (decision.kind === "fail_open") {
|
|
241
|
-
return handlePermissionError(decision.error, toolName, client);
|
|
242
|
-
}
|
|
243
|
-
const attrs = { toolName, ...inputSummary };
|
|
244
|
-
const decisionAttrs = decision.spanAttributes;
|
|
245
|
-
if (decision.kind === "allow") {
|
|
246
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
247
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: true },
|
|
248
|
-
});
|
|
249
|
-
return {};
|
|
250
|
-
}
|
|
251
|
-
if (decision.kind === "observe") {
|
|
252
|
-
client.tracer.record("tool.block", "denied", {
|
|
253
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
|
|
254
|
-
});
|
|
255
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
256
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: false, observed: true },
|
|
257
|
-
});
|
|
258
|
-
return {};
|
|
259
|
-
}
|
|
260
|
-
client.tracer.record("tool.block", "denied", {
|
|
261
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
|
|
262
|
-
});
|
|
263
|
-
return {
|
|
264
|
-
decision: "block",
|
|
265
|
-
reason: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }),
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
catch (err) {
|
|
269
|
-
return handlePermissionError(err, toolName, client);
|
|
270
|
-
}
|
|
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
|
+
: {};
|
|
271
89
|
}
|
|
272
|
-
|
|
273
|
-
async function handlePostToolUse(input, client) {
|
|
90
|
+
async function handlePostToolUse(input, client, deps) {
|
|
274
91
|
const toolName = input.tool_name;
|
|
275
92
|
if (!toolName) {
|
|
276
|
-
client.logger.warn("post_tool_use.missing_tool_name", {
|
|
277
|
-
sessionId: input.session_id,
|
|
278
|
-
});
|
|
93
|
+
client.logger.warn("post_tool_use.missing_tool_name", { sessionId: input.session_id });
|
|
279
94
|
return {};
|
|
280
95
|
}
|
|
281
96
|
client.logger.info("lifecycle.post_tool_use", {
|
|
@@ -284,24 +99,85 @@ async function handlePostToolUse(input, client) {
|
|
|
284
99
|
turnId: input.turn_id,
|
|
285
100
|
toolUseId: input.tool_use_id,
|
|
286
101
|
});
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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 },
|
|
295
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
|
+
}
|
|
296
171
|
return {};
|
|
297
172
|
}
|
|
298
|
-
|
|
173
|
+
function isCollaborationSpawn(toolName) {
|
|
174
|
+
const normalized = toolName.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
175
|
+
return normalized === "collaborationspawnagent" || normalized === "spawnagent";
|
|
176
|
+
}
|
|
299
177
|
async function handlePermissionRequest(input, client) {
|
|
300
178
|
const toolName = input.tool_name;
|
|
301
179
|
if (!toolName) {
|
|
302
|
-
client.logger.warn("permission_request.missing_tool_name", {
|
|
303
|
-
sessionId: input.session_id,
|
|
304
|
-
});
|
|
180
|
+
client.logger.warn("permission_request.missing_tool_name", { sessionId: input.session_id });
|
|
305
181
|
return {};
|
|
306
182
|
}
|
|
307
183
|
client.logger.info("lifecycle.permission_request", {
|
|
@@ -309,71 +185,31 @@ async function handlePermissionRequest(input, client) {
|
|
|
309
185
|
toolName,
|
|
310
186
|
turnId: input.turn_id,
|
|
311
187
|
});
|
|
312
|
-
|
|
313
|
-
|
|
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");
|
|
314
198
|
}
|
|
315
|
-
|
|
316
|
-
if ((0, argus_1.isInteractiveTool)("codex", toolName)) {
|
|
317
|
-
client.tracer.record("user.interaction", "ok", {
|
|
318
|
-
attributes: { harness: "codex", toolName, kind: "permission-request" },
|
|
319
|
-
});
|
|
199
|
+
if (result.kind === "interactive") {
|
|
320
200
|
return permissionRequestFallback("Interactive tool — deferring to user");
|
|
321
201
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const mcpTool = (0, argus_1.parseMcpToolGeneric)(toolName);
|
|
325
|
-
try {
|
|
326
|
-
if (mcpTool) {
|
|
327
|
-
const mcpResult = await (0, argus_1.checkMcpPermission)(client, mcpTool, {
|
|
328
|
-
subject,
|
|
329
|
-
spanAttributes: { toolName },
|
|
330
|
-
});
|
|
331
|
-
const decision = (0, argus_1.applyPermissionMode)(client, mcpResult.allowed, {
|
|
332
|
-
object: mcpTool.serverName,
|
|
333
|
-
relation: "use",
|
|
334
|
-
subjectId,
|
|
335
|
-
...("subjectSet" in subject ? { subjectSet: subject.subjectSet } : {}),
|
|
336
|
-
spanAttributes: { toolName, mcpServer: mcpTool.serverName, mcpTool: mcpTool.toolName },
|
|
337
|
-
});
|
|
338
|
-
if (decision.kind === "deny") {
|
|
339
|
-
return permissionRequestDecision("deny", (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, mcp: mcpTool }));
|
|
340
|
-
}
|
|
341
|
-
return permissionRequestDecision("allow", decision.kind === "observe"
|
|
342
|
-
? "Ory observe mode — denied by policy but allowed through"
|
|
343
|
-
: "Ory MCP server permission granted");
|
|
344
|
-
}
|
|
345
|
-
const namespace = resolveNamespace();
|
|
346
|
-
const outcome = await (0, argus_1.gateToolCall)(client, {
|
|
347
|
-
harness: "codex",
|
|
348
|
-
toolName,
|
|
349
|
-
check: { namespace, object: toolName, relation: "use", ...subject },
|
|
350
|
-
spanAttributes: { toolName },
|
|
351
|
-
});
|
|
352
|
-
if (outcome.kind === "interactive") {
|
|
353
|
-
return permissionRequestFallback("Interactive tool — deferring to user");
|
|
354
|
-
}
|
|
355
|
-
const decision = outcome;
|
|
356
|
-
if (decision.kind === "fail_open") {
|
|
357
|
-
const code = decision.error.code;
|
|
358
|
-
const reason = code === "network_error" || code === "rate_limited"
|
|
359
|
-
? `Ory unavailable (${code}), falling back to user prompt`
|
|
360
|
-
: `Ory permission check failed: ${decision.error.message}`;
|
|
361
|
-
return permissionRequestFallback(reason);
|
|
362
|
-
}
|
|
363
|
-
if (decision.kind === "deny") {
|
|
364
|
-
return permissionRequestDecision("deny", (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }));
|
|
365
|
-
}
|
|
366
|
-
return permissionRequestDecision("allow", decision.kind === "observe"
|
|
367
|
-
? "Ory observe mode — denied by policy but allowed through"
|
|
368
|
-
: "Ory permission granted");
|
|
202
|
+
if (result.kind === "fail_open") {
|
|
203
|
+
return permissionRequestFallback(permissionFailureReason(result.decision));
|
|
369
204
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const reason = oryErr.code === "network_error" || oryErr.code === "rate_limited"
|
|
373
|
-
? `Ory unavailable (${oryErr.code}), falling back to user prompt`
|
|
374
|
-
: `Ory permission check failed: ${oryErr.message}`;
|
|
375
|
-
return permissionRequestFallback(reason);
|
|
205
|
+
if (result.kind === "deny") {
|
|
206
|
+
return permissionRequestDecision("deny", result.denialMessage ?? "Ory permission denied");
|
|
376
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");
|
|
377
213
|
}
|
|
378
214
|
function permissionRequestDecision(behavior, message) {
|
|
379
215
|
return {
|
|
@@ -384,36 +220,48 @@ function permissionRequestDecision(behavior, message) {
|
|
|
384
220
|
};
|
|
385
221
|
}
|
|
386
222
|
function permissionRequestFallback(message) {
|
|
387
|
-
return {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
};
|
|
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}`;
|
|
393
232
|
}
|
|
394
|
-
|
|
395
|
-
|
|
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
|
+
}
|
|
396
248
|
client.logger.info("lifecycle.user_prompt_submit", {
|
|
397
249
|
sessionId: input.session_id,
|
|
398
250
|
turnId: input.turn_id,
|
|
399
251
|
promptLen: input.prompt?.length,
|
|
400
252
|
});
|
|
401
|
-
client.
|
|
402
|
-
attributes: {
|
|
403
|
-
turnId: input.turn_id,
|
|
404
|
-
promptLen: input.prompt?.length,
|
|
405
|
-
},
|
|
253
|
+
client.logger.activity("user.prompt", "ok", {
|
|
254
|
+
attributes: { turnId: input.turn_id, promptLen: input.prompt?.length },
|
|
406
255
|
});
|
|
407
256
|
return {};
|
|
408
257
|
}
|
|
409
|
-
// ─── Stop ───────────────────────────────────────────────────────────
|
|
410
258
|
async function handleStop(input, client) {
|
|
411
259
|
client.logger.info("lifecycle.stop", {
|
|
412
260
|
sessionId: input.session_id,
|
|
413
261
|
turnId: input.turn_id,
|
|
414
262
|
stopHookActive: input.stop_hook_active,
|
|
415
263
|
});
|
|
416
|
-
client.
|
|
264
|
+
client.logger.activity("turn.stop", "ok", {
|
|
417
265
|
attributes: {
|
|
418
266
|
turnId: input.turn_id,
|
|
419
267
|
stopHookActive: input.stop_hook_active,
|
|
@@ -422,42 +270,6 @@ async function handleStop(input, client) {
|
|
|
422
270
|
});
|
|
423
271
|
return {};
|
|
424
272
|
}
|
|
425
|
-
// ─── Helpers ────────────────────────────────────────────────────────
|
|
426
|
-
/**
|
|
427
|
-
* Whether a hook output carries anything Codex needs to see on stdout.
|
|
428
|
-
*
|
|
429
|
-
* The hook entry point handles `decision === "block"` (exit code 2 + stdout)
|
|
430
|
-
* before consulting this. Everything else with at least one populated field
|
|
431
|
-
* must be written to stdout — most importantly `hookSpecificOutput`, which
|
|
432
|
-
* carries the PermissionRequest allow/deny decision. Empty outputs stay
|
|
433
|
-
* silent to keep stdout clean for the harness.
|
|
434
|
-
*/
|
|
435
273
|
function shouldEmitOutput(output) {
|
|
436
274
|
return Object.values(output).some((value) => value !== undefined);
|
|
437
275
|
}
|
|
438
|
-
function resolveNamespace() {
|
|
439
|
-
return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
|
|
440
|
-
}
|
|
441
|
-
function handlePermissionError(oryErr, toolName, client) {
|
|
442
|
-
if (oryErr.code === "network_error") {
|
|
443
|
-
client.logger.warn("permission.network_error", {
|
|
444
|
-
toolName,
|
|
445
|
-
message: "Ory unreachable, failing open",
|
|
446
|
-
});
|
|
447
|
-
return {};
|
|
448
|
-
}
|
|
449
|
-
if (oryErr.code === "rate_limited") {
|
|
450
|
-
client.logger.warn("permission.rate_limited", {
|
|
451
|
-
toolName,
|
|
452
|
-
message: "Ory rate limited, failing open",
|
|
453
|
-
});
|
|
454
|
-
return {};
|
|
455
|
-
}
|
|
456
|
-
// For other errors, also fail open but log prominently
|
|
457
|
-
client.logger.error("permission.check.error", {
|
|
458
|
-
toolName,
|
|
459
|
-
code: oryErr.code,
|
|
460
|
-
message: oryErr.message,
|
|
461
|
-
});
|
|
462
|
-
return {};
|
|
463
|
-
}
|