@ory/opencode 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 +77 -63
- package/dist/cli/config.d.ts +90 -0
- package/dist/cli/config.js +267 -0
- package/dist/cli/main.js +94 -56
- package/dist/cli/setup.d.ts +3 -0
- package/dist/cli/setup.js +39 -56
- package/dist/plugin.d.ts +1 -16
- package/dist/plugin.js +90 -412
- package/package.json +9 -8
package/dist/plugin.js
CHANGED
|
@@ -2,18 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createOryPlugin = createOryPlugin;
|
|
4
4
|
const argus_1 = require("@ory/argus");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*
|
|
8
|
-
* OpenCode plugins export { server: Plugin } where Plugin receives
|
|
9
|
-
* context and returns a Hooks object. Hook functions use a mutating
|
|
10
|
-
* (input, output) => Promise<void> pattern.
|
|
11
|
-
*
|
|
12
|
-
* The `server` factory has no return-value channel for blocking, so the
|
|
13
|
-
* user login runs in advisory mode at startup — it emits the audit span,
|
|
14
|
-
* refreshes tokens, and may launch a browser flow when interactive, but
|
|
15
|
-
* cannot prevent the session from starting.
|
|
16
|
-
*/
|
|
5
|
+
const HARNESS = "opencode";
|
|
6
|
+
const SESSION_CACHE_LIMIT = 256;
|
|
17
7
|
function createOryPlugin(clientOrConfig, deps = {}) {
|
|
18
8
|
return {
|
|
19
9
|
id: "ory",
|
|
@@ -21,45 +11,66 @@ function createOryPlugin(clientOrConfig, deps = {}) {
|
|
|
21
11
|
const client = clientOrConfig instanceof argus_1.OryAgentClient
|
|
22
12
|
? clientOrConfig
|
|
23
13
|
: clientOrConfig
|
|
24
|
-
? new argus_1.OryAgentClient({
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
})
|
|
28
|
-
: argus_1.OryAgentClient.fromEnv("opencode");
|
|
29
|
-
// Log session status on startup (never blocks)
|
|
30
|
-
await verifyOnStartup(client, deps);
|
|
14
|
+
? new argus_1.OryAgentClient({ ...clientOrConfig, harness: HARNESS })
|
|
15
|
+
: argus_1.OryAgentClient.fromEnv(HARNESS);
|
|
16
|
+
const ensureSessionStarted = createSessionStarter(client, deps);
|
|
31
17
|
return {
|
|
32
18
|
config: createConfigHandler(client),
|
|
33
|
-
"chat.message": createChatMessageHandler(client),
|
|
34
|
-
"permission.ask": createPermissionHandler(client, options),
|
|
35
|
-
"tool.execute.before": createToolBeforeHandler(client),
|
|
19
|
+
"chat.message": createChatMessageHandler(client, ensureSessionStarted),
|
|
20
|
+
"permission.ask": createPermissionHandler(client, ensureSessionStarted, options),
|
|
21
|
+
"tool.execute.before": createToolBeforeHandler(client, ensureSessionStarted),
|
|
36
22
|
"tool.execute.after": createToolAfterHandler(client),
|
|
37
23
|
};
|
|
38
24
|
},
|
|
39
25
|
};
|
|
40
26
|
}
|
|
41
|
-
// ─── config ────────────────────────────────────────────────────────
|
|
42
27
|
function createConfigHandler(client) {
|
|
43
28
|
return async (_input) => {
|
|
29
|
+
const resolved = (0, argus_1.resolveConfig)();
|
|
44
30
|
client.logger.info("lifecycle.config", {
|
|
45
|
-
projectUrl:
|
|
46
|
-
|
|
31
|
+
projectUrl: resolved.projectUrl,
|
|
32
|
+
securityConnected: resolved.security.connected,
|
|
47
33
|
});
|
|
48
|
-
client.
|
|
34
|
+
client.logger.activity("config.resolve", "ok", {
|
|
49
35
|
attributes: {
|
|
50
|
-
projectUrl:
|
|
51
|
-
|
|
36
|
+
projectUrl: resolved.projectUrl,
|
|
37
|
+
securityConnected: resolved.security.connected,
|
|
52
38
|
},
|
|
53
39
|
});
|
|
54
40
|
};
|
|
55
41
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
42
|
+
function createSessionStarter(client, deps) {
|
|
43
|
+
const sessions = new Map();
|
|
44
|
+
const completed = new Set();
|
|
45
|
+
return async (sessionId) => {
|
|
46
|
+
let start = sessions.get(sessionId);
|
|
47
|
+
if (!start) {
|
|
48
|
+
start = (0, argus_1.withHookContext)(client, { sessionId }, async () => {
|
|
49
|
+
await (0, argus_1.sessionStart)(client, {
|
|
50
|
+
harness: HARNESS,
|
|
51
|
+
binName: "ory-opencode",
|
|
52
|
+
userLogin: deps.userLogin,
|
|
53
|
+
agentGate: deps.agentGate,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
sessions.set(sessionId, start);
|
|
57
|
+
start.then(() => {
|
|
58
|
+
completed.add(sessionId);
|
|
59
|
+
while (sessions.size > SESSION_CACHE_LIMIT) {
|
|
60
|
+
const oldest = completed.values().next().value;
|
|
61
|
+
if (oldest === undefined)
|
|
62
|
+
break;
|
|
63
|
+
completed.delete(oldest);
|
|
64
|
+
sessions.delete(oldest);
|
|
65
|
+
}
|
|
66
|
+
}, () => sessions.delete(sessionId));
|
|
67
|
+
}
|
|
68
|
+
await start;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function createChatMessageHandler(client, ensureSessionStarted) {
|
|
72
|
+
return (input, _output) => (0, argus_1.withHookContext)(client, { sessionId: input.sessionID }, async () => {
|
|
73
|
+
await ensureSessionStarted(input.sessionID);
|
|
63
74
|
client.logger.info("lifecycle.chat_message", {
|
|
64
75
|
sessionID: input.sessionID,
|
|
65
76
|
messageID: input.messageID,
|
|
@@ -67,7 +78,7 @@ function createChatMessageHandler(client) {
|
|
|
67
78
|
providerID: input.model?.providerID,
|
|
68
79
|
modelID: input.model?.modelID,
|
|
69
80
|
});
|
|
70
|
-
client.
|
|
81
|
+
client.logger.activity("user.prompt", "ok", {
|
|
71
82
|
attributes: {
|
|
72
83
|
messageID: input.messageID,
|
|
73
84
|
agent: input.agent,
|
|
@@ -75,399 +86,66 @@ function createChatMessageHandler(client) {
|
|
|
75
86
|
modelID: input.model?.modelID,
|
|
76
87
|
},
|
|
77
88
|
});
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
// ─── Session verification on startup ────────────────────────────────
|
|
81
|
-
async function verifyOnStartup(client, deps) {
|
|
82
|
-
// Run the user login. The OpenCode `server` factory has no block
|
|
83
|
-
// primitive, so allowBlock is false — the flow emits the user.auth
|
|
84
|
-
// audit span, refreshes tokens, and may prompt the user, but never
|
|
85
|
-
// prevents the session from starting. When ORY_USER_LOGIN is unset
|
|
86
|
-
// it's a no-op (mode === "disabled") and we fall through to legacy
|
|
87
|
-
// logging.
|
|
88
|
-
const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
|
|
89
|
-
const decision = await userGate(client, {
|
|
90
|
-
binName: "ory-opencode",
|
|
91
|
-
harness: "opencode",
|
|
92
|
-
allowBlock: false,
|
|
93
|
-
});
|
|
94
|
-
// Resolve the agent identity (machine credentials). Never blocks;
|
|
95
|
-
// attaches the agent's bearer token to outgoing Ory API calls.
|
|
96
|
-
const agentGate = deps.agentGate ?? argus_1.ensureAgentIdentity;
|
|
97
|
-
await agentGate(client, { projectUrl: (0, argus_1.resolveConfig)().projectUrl, harness: "opencode" });
|
|
98
|
-
// Record the user→agent delegation so the audit trail captures that
|
|
99
|
-
// this user authorized this agent for this session. Written at most once
|
|
100
|
-
// per install. Best-effort: requires both principals to be populated, and
|
|
101
|
-
// any failure is logged and swallowed (fail-open — delegation tracking is
|
|
102
|
-
// for audit, not enforcement).
|
|
103
|
-
await (0, argus_1.writeUserDelegatesAgent)(client);
|
|
104
|
-
if (decision.mode !== "disabled") {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
const resolved = (0, argus_1.resolveConfig)();
|
|
108
|
-
if (resolved.auditOnly) {
|
|
109
|
-
client.logger.info("config.audit_only", {
|
|
110
|
-
message: "Audit-only mode enabled. Auth and permission checks are disabled.",
|
|
111
|
-
});
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
if (!resolved.projectUrl) {
|
|
115
|
-
client.logger.warn("config.not_configured", {
|
|
116
|
-
message: "Ory plugin is not configured. Auth and permission checks are disabled. " +
|
|
117
|
-
"Run 'npx ory-opencode configure' to connect to an Ory project.",
|
|
118
|
-
});
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
const sessionToken = process.env.ORY_SESSION_TOKEN;
|
|
122
|
-
const oauth2Token = process.env.ORY_OAUTH2_TOKEN;
|
|
123
|
-
if (sessionToken) {
|
|
124
|
-
try {
|
|
125
|
-
const session = await client.verifySession(sessionToken);
|
|
126
|
-
if (!session.active) {
|
|
127
|
-
client.logger.warn("session.inactive", {
|
|
128
|
-
message: "Ory session is not active. Re-authenticate to enable auth checks.",
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
catch (err) {
|
|
133
|
-
const oryErr = err;
|
|
134
|
-
client.logger.warn("session.verify_failed", {
|
|
135
|
-
code: isOryError(err) ? oryErr.code : "unknown",
|
|
136
|
-
message: err instanceof Error ? err.message : String(err),
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (oauth2Token) {
|
|
142
|
-
try {
|
|
143
|
-
const tokenInfo = await client.introspectToken(oauth2Token);
|
|
144
|
-
if (!tokenInfo.active) {
|
|
145
|
-
client.logger.warn("oauth2.token_inactive", {
|
|
146
|
-
message: "Ory OAuth2 token is not active. Obtain a new token to enable auth checks.",
|
|
147
|
-
});
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
client.logger.info("oauth2.session_authenticated", {
|
|
151
|
-
clientId: tokenInfo.clientId,
|
|
152
|
-
subject: tokenInfo.subject,
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
catch (err) {
|
|
156
|
-
const oryErr = err;
|
|
157
|
-
client.logger.warn("oauth2.introspect_failed", {
|
|
158
|
-
code: isOryError(err) ? oryErr.code : "unknown",
|
|
159
|
-
message: err instanceof Error ? err.message : String(err),
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
client.logger.warn("session.no_credentials", {
|
|
165
|
-
message: "Neither ORY_SESSION_TOKEN nor ORY_OAUTH2_TOKEN is set. " +
|
|
166
|
-
"Skipping authentication.",
|
|
167
89
|
});
|
|
168
90
|
}
|
|
169
|
-
|
|
170
|
-
function createPermissionHandler(client, _options) {
|
|
91
|
+
function createPermissionHandler(client, ensureSessionStarted, _options) {
|
|
171
92
|
return async (input, output) => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
toolID:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// the final call. Pre-answering on Ory's behalf hides the prompt the
|
|
185
|
-
// user is meant to see.
|
|
186
|
-
if ((0, argus_1.isInteractiveTool)("opencode", toolId)) {
|
|
187
|
-
client.tracer.record("user.interaction", "ok", {
|
|
188
|
-
attributes: { harness: "opencode", toolName: toolId, kind: "permission-ask" },
|
|
93
|
+
const run = async () => {
|
|
94
|
+
if (input.sessionID)
|
|
95
|
+
await ensureSessionStarted(input.sessionID);
|
|
96
|
+
const toolName = input.toolID ?? input.type ?? "unknown";
|
|
97
|
+
client.logger.info("lifecycle.permission_ask", { toolID: toolName, title: input.title });
|
|
98
|
+
const result = await (0, argus_1.decideTool)(client, {
|
|
99
|
+
harness: HARNESS,
|
|
100
|
+
toolName,
|
|
101
|
+
subjectFallback: input.sessionID ? `session:${input.sessionID}` : undefined,
|
|
102
|
+
canBlock: true,
|
|
103
|
+
mcpTool: (0, argus_1.parseMcpToolGeneric)(toolName) ?? undefined,
|
|
104
|
+
extraActivityAttributes: { kind: "permission-ask" },
|
|
189
105
|
});
|
|
190
|
-
|
|
106
|
+
applyPermissionAskDecision(output, result.kind);
|
|
107
|
+
};
|
|
108
|
+
if (input.sessionID) {
|
|
109
|
+
await (0, argus_1.withHookContext)(client, { sessionId: input.sessionID }, run);
|
|
191
110
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// would silently match the agent's tuples instead of the user's.
|
|
195
|
-
const subject = (0, argus_1.resolveUserSubject)(client, input.sessionID ? `session:${input.sessionID}` : undefined);
|
|
196
|
-
const mcpTool = (0, argus_1.parseMcpToolGeneric)(toolId);
|
|
197
|
-
try {
|
|
198
|
-
if (mcpTool) {
|
|
199
|
-
const mcpResult = await (0, argus_1.checkMcpPermission)(client, mcpTool, {
|
|
200
|
-
subject,
|
|
201
|
-
spanAttributes: { toolName: toolId },
|
|
202
|
-
});
|
|
203
|
-
const decision = (0, argus_1.applyPermissionMode)(client, mcpResult.allowed, {
|
|
204
|
-
object: mcpTool.serverName,
|
|
205
|
-
relation: "use",
|
|
206
|
-
subjectId: (0, argus_1.subjectLabel)(subject),
|
|
207
|
-
...("subjectSet" in subject ? { subjectSet: subject.subjectSet } : {}),
|
|
208
|
-
spanAttributes: { toolName: toolId, mcpServer: mcpTool.serverName, mcpTool: mcpTool.toolName },
|
|
209
|
-
});
|
|
210
|
-
applyPermissionAskDecision(output, decision.kind);
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
const outcome = await (0, argus_1.gateToolCall)(client, {
|
|
214
|
-
harness: "opencode",
|
|
215
|
-
toolName: toolId,
|
|
216
|
-
check: { namespace: resolveNamespace(), object: toolId, relation: "use", ...subject },
|
|
217
|
-
spanAttributes: { toolName: toolId },
|
|
218
|
-
});
|
|
219
|
-
if (outcome.kind === "interactive" || outcome.kind === "audit_only") {
|
|
220
|
-
// Ory is disabled (audit-only kill switch) or this is a
|
|
221
|
-
// user-interaction primitive: leave output.status as "ask" (default)
|
|
222
|
-
// so the harness's native prompt reaches the user.
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
const decision = outcome;
|
|
226
|
-
if (decision.kind === "fail_open") {
|
|
227
|
-
const code = decision.error.code;
|
|
228
|
-
if (code === "network_error" || code === "rate_limited") {
|
|
229
|
-
client.logger.warn("permission.fallback", {
|
|
230
|
-
toolID: toolId,
|
|
231
|
-
code,
|
|
232
|
-
});
|
|
233
|
-
// Leave output.status as "ask" (default)
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
output.status = "ask";
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
applyPermissionAskDecision(output, decision.kind);
|
|
240
|
-
}
|
|
241
|
-
catch (err) {
|
|
242
|
-
if (isOryError(err) && (err.code === "network_error" || err.code === "rate_limited")) {
|
|
243
|
-
client.logger.warn("permission.fallback", {
|
|
244
|
-
toolID: toolId,
|
|
245
|
-
code: err.code,
|
|
246
|
-
});
|
|
247
|
-
// Leave output.status as "ask" (default)
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
// For other errors, also fall back to ask
|
|
251
|
-
output.status = "ask";
|
|
111
|
+
else {
|
|
112
|
+
await run();
|
|
252
113
|
}
|
|
253
114
|
};
|
|
254
115
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
116
|
+
function createToolBeforeHandler(client, ensureSessionStarted) {
|
|
117
|
+
return (input, output) => (0, argus_1.withHookContext)(client, { sessionId: input.sessionID }, async () => {
|
|
118
|
+
client.logger.info("lifecycle.tool_execute_before", input);
|
|
119
|
+
await ensureSessionStarted(input.sessionID);
|
|
120
|
+
const result = await (0, argus_1.gate)(client, {
|
|
121
|
+
harness: HARNESS,
|
|
122
|
+
toolName: input.tool,
|
|
123
|
+
toolArgs: output.args,
|
|
124
|
+
subjectFallback: `session:${input.sessionID}`,
|
|
125
|
+
canBlock: false,
|
|
126
|
+
mcpTool: (0, argus_1.parseMcpToolGeneric)(input.tool) ?? undefined,
|
|
262
127
|
});
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
traceId: (0, argus_1.deriveTraceId)(input.sessionID),
|
|
266
|
-
sessionId: input.sessionID,
|
|
267
|
-
});
|
|
268
|
-
const inputSummary = (0, argus_1.summarizeToolInput)(input.tool, output.args);
|
|
269
|
-
// In audit-only mode, log the invocation but skip permission checks
|
|
270
|
-
if ((0, argus_1.resolveConfig)().auditOnly) {
|
|
271
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
272
|
-
attributes: { toolName: input.tool, ...inputSummary },
|
|
273
|
-
});
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
const subject = (0, argus_1.resolveUserSubject)(client, input.sessionID ? `session:${input.sessionID}` : undefined);
|
|
277
|
-
const subjectId = (0, argus_1.subjectLabel)(subject);
|
|
278
|
-
const mcpTool = (0, argus_1.parseMcpToolGeneric)(input.tool);
|
|
279
|
-
try {
|
|
280
|
-
if (mcpTool) {
|
|
281
|
-
const mcpResult = await (0, argus_1.checkMcpPermission)(client, mcpTool, {
|
|
282
|
-
subject,
|
|
283
|
-
spanAttributes: { toolName: input.tool },
|
|
284
|
-
});
|
|
285
|
-
const mcpAttrs = {
|
|
286
|
-
toolName: input.tool,
|
|
287
|
-
mcpServer: mcpTool.serverName,
|
|
288
|
-
mcpTool: mcpTool.toolName,
|
|
289
|
-
...inputSummary,
|
|
290
|
-
};
|
|
291
|
-
const decision = (0, argus_1.applyPermissionMode)(client, mcpResult.allowed, {
|
|
292
|
-
object: mcpTool.serverName,
|
|
293
|
-
relation: "use",
|
|
294
|
-
subjectId,
|
|
295
|
-
...("subjectSet" in subject ? { subjectSet: subject.subjectSet } : {}),
|
|
296
|
-
spanAttributes: mcpAttrs,
|
|
297
|
-
});
|
|
298
|
-
const decisionAttrs = decision.spanAttributes;
|
|
299
|
-
if (decision.kind === "allow") {
|
|
300
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
301
|
-
attributes: { ...mcpAttrs, ...decisionAttrs },
|
|
302
|
-
});
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
if (decision.kind === "observe") {
|
|
306
|
-
client.tracer.record("tool.block", "denied", {
|
|
307
|
-
attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
|
|
308
|
-
});
|
|
309
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
310
|
-
attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, observed: true },
|
|
311
|
-
});
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
client.tracer.record("tool.block", "denied", {
|
|
315
|
-
attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
|
|
316
|
-
});
|
|
317
|
-
const message = (0, argus_1.formatDenialMessage)({
|
|
318
|
-
tool: input.tool,
|
|
319
|
-
subjectId,
|
|
320
|
-
mcp: mcpTool,
|
|
321
|
-
});
|
|
322
|
-
client.logger.warn("tool.denied", {
|
|
323
|
-
tool: input.tool,
|
|
324
|
-
subjectId,
|
|
325
|
-
message,
|
|
326
|
-
});
|
|
327
|
-
// OpenCode has no native block signal in tool.execute.before;
|
|
328
|
-
// throwing aborts execution (documented .env-protection pattern)
|
|
329
|
-
// and surfaces error.message back to the agent.
|
|
330
|
-
throw new argus_1.OryDenialError({
|
|
331
|
-
tool: input.tool,
|
|
332
|
-
subjectId,
|
|
333
|
-
mcp: mcpTool,
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
const namespace = resolveNamespace();
|
|
337
|
-
const outcome = await (0, argus_1.gateToolCall)(client, {
|
|
338
|
-
harness: "opencode",
|
|
339
|
-
toolName: input.tool,
|
|
340
|
-
check: { namespace, object: input.tool, relation: "use", ...subject },
|
|
341
|
-
spanAttributes: { toolName: input.tool },
|
|
342
|
-
});
|
|
343
|
-
// Interactive tools (operator-extensible via ORY_INTERACTIVE_TOOLS):
|
|
344
|
-
// user.interaction span is already recorded; pass through with no
|
|
345
|
-
// throw, no tool.invoke, no tool.block.
|
|
346
|
-
if (outcome.kind === "interactive") {
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
const decision = outcome;
|
|
350
|
-
if (decision.kind === "fail_open") {
|
|
351
|
-
handlePermissionError(decision.error, input.tool, client);
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
const attrs = { toolName: input.tool, ...inputSummary };
|
|
355
|
-
const decisionAttrs = decision.spanAttributes;
|
|
356
|
-
if (decision.kind === "allow") {
|
|
357
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
358
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: true },
|
|
359
|
-
});
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
if (decision.kind === "observe") {
|
|
363
|
-
client.tracer.record("tool.block", "denied", {
|
|
364
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
|
|
365
|
-
});
|
|
366
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
367
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: false, observed: true },
|
|
368
|
-
});
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
client.tracer.record("tool.block", "denied", {
|
|
372
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
|
|
373
|
-
});
|
|
374
|
-
const message = (0, argus_1.formatDenialMessage)({
|
|
375
|
-
tool: input.tool,
|
|
376
|
-
subjectId,
|
|
377
|
-
namespace,
|
|
378
|
-
});
|
|
379
|
-
client.logger.warn("tool.denied", {
|
|
380
|
-
tool: input.tool,
|
|
381
|
-
subjectId,
|
|
382
|
-
message,
|
|
383
|
-
});
|
|
384
|
-
throw new argus_1.OryDenialError({
|
|
128
|
+
if (result.kind === "deny") {
|
|
129
|
+
throw new argus_1.OryDenialError(result.denialContext ?? {
|
|
385
130
|
tool: input.tool,
|
|
386
|
-
subjectId,
|
|
387
|
-
namespace,
|
|
131
|
+
subjectId: result.subject,
|
|
132
|
+
namespace: result.namespace,
|
|
388
133
|
});
|
|
389
134
|
}
|
|
390
|
-
|
|
391
|
-
if (err instanceof argus_1.OryDenialError)
|
|
392
|
-
throw err;
|
|
393
|
-
handlePermissionError(err, input.tool, client);
|
|
394
|
-
}
|
|
395
|
-
};
|
|
135
|
+
});
|
|
396
136
|
}
|
|
397
|
-
// ─── tool.execute.after ─────────────────────────────────────────────
|
|
398
137
|
function createToolAfterHandler(client) {
|
|
399
|
-
return
|
|
400
|
-
client.logger.info("lifecycle.tool_execute_after",
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
});
|
|
405
|
-
client.tracer.setContext({
|
|
406
|
-
traceId: (0, argus_1.deriveTraceId)(input.sessionID),
|
|
407
|
-
sessionId: input.sessionID,
|
|
138
|
+
return (input, output) => (0, argus_1.withHookContext)(client, { sessionId: input.sessionID }, async () => {
|
|
139
|
+
client.logger.info("lifecycle.tool_execute_after", input);
|
|
140
|
+
(0, argus_1.complete)(client, {
|
|
141
|
+
toolName: input.tool,
|
|
142
|
+
output: { title: output.title, output: output.output },
|
|
408
143
|
});
|
|
409
|
-
|
|
410
|
-
// so input summary is unavailable here; output summary only.
|
|
411
|
-
client.tracer.record("tool.complete", "ok", {
|
|
412
|
-
attributes: {
|
|
413
|
-
toolName: input.tool,
|
|
414
|
-
...(0, argus_1.summarizeToolOutput)(input.tool, {
|
|
415
|
-
title: output.title,
|
|
416
|
-
output: output.output,
|
|
417
|
-
}),
|
|
418
|
-
},
|
|
419
|
-
});
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
// ─── Helpers ────────────────────────────────────────────────────────
|
|
423
|
-
function resolveNamespace() {
|
|
424
|
-
return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
|
|
144
|
+
});
|
|
425
145
|
}
|
|
426
|
-
/**
|
|
427
|
-
* Map an Ory permission decision onto OpenCode's `permission.ask` output.
|
|
428
|
-
*
|
|
429
|
-
* Only explicit outcomes answer the prompt: `deny` → `"deny"`,
|
|
430
|
-
* `allow` → `"allow"`. An `observe`-mode deny leaves `output.status`
|
|
431
|
-
* untouched (OpenCode's default is `"ask"`), so the harness's native
|
|
432
|
-
* user prompt still appears — observe mode must never escalate a
|
|
433
|
-
* policy-denied tool into an auto-approval. The observe deny is already
|
|
434
|
-
* recorded as a `permission.observe_deny` audit span by the gate.
|
|
435
|
-
*/
|
|
436
146
|
function applyPermissionAskDecision(output, kind) {
|
|
437
|
-
if (kind === "deny")
|
|
147
|
+
if (kind === "deny")
|
|
438
148
|
output.status = "deny";
|
|
439
|
-
|
|
440
|
-
else if (kind === "allow") {
|
|
149
|
+
else if (kind === "allow")
|
|
441
150
|
output.status = "allow";
|
|
442
|
-
}
|
|
443
|
-
// kind === "observe": leave the harness's native prompt in place.
|
|
444
|
-
}
|
|
445
|
-
function isOryError(err) {
|
|
446
|
-
return (typeof err === "object" &&
|
|
447
|
-
err !== null &&
|
|
448
|
-
"code" in err &&
|
|
449
|
-
"message" in err);
|
|
450
|
-
}
|
|
451
|
-
function handlePermissionError(err, tool, client) {
|
|
452
|
-
if (isOryError(err)) {
|
|
453
|
-
if (err.code === "network_error" || err.code === "rate_limited") {
|
|
454
|
-
client.logger.warn("permission.fallback", {
|
|
455
|
-
tool,
|
|
456
|
-
code: err.code,
|
|
457
|
-
message: "Failing open",
|
|
458
|
-
});
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
client.logger.error("permission.check.error", {
|
|
462
|
-
tool,
|
|
463
|
-
code: err.code,
|
|
464
|
-
message: err.message,
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
else {
|
|
468
|
-
client.logger.error("permission.check.error", {
|
|
469
|
-
tool,
|
|
470
|
-
error: err instanceof Error ? err.message : String(err),
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
151
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ory/opencode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Ory plugin for OpenCode: scaffolding skills, a local Ory instance, and authentication, authorization, and audit for every tool call",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://ory.com",
|
|
@@ -33,8 +33,7 @@
|
|
|
33
33
|
"agent-security",
|
|
34
34
|
"guardrails",
|
|
35
35
|
"llm",
|
|
36
|
-
"
|
|
37
|
-
"distributed-tracing",
|
|
36
|
+
"activity-auditing",
|
|
38
37
|
"observability",
|
|
39
38
|
"kratos",
|
|
40
39
|
"keto",
|
|
@@ -45,6 +44,10 @@
|
|
|
45
44
|
"registry": "https://registry.npmjs.org/",
|
|
46
45
|
"provenance": true
|
|
47
46
|
},
|
|
47
|
+
"reova": {
|
|
48
|
+
"enabled": true,
|
|
49
|
+
"endpoint": "https://telemetry.reo.dev/data"
|
|
50
|
+
},
|
|
48
51
|
"main": "dist/index.js",
|
|
49
52
|
"types": "dist/index.d.ts",
|
|
50
53
|
"exports": {
|
|
@@ -59,12 +62,11 @@
|
|
|
59
62
|
},
|
|
60
63
|
"files": [
|
|
61
64
|
"dist",
|
|
62
|
-
"!dist/dev",
|
|
63
65
|
"!dist/**/*.tsbuildinfo"
|
|
64
66
|
],
|
|
65
67
|
"dependencies": {
|
|
66
|
-
"
|
|
67
|
-
"@ory/argus": "0.
|
|
68
|
+
"reova": "^0.7.0",
|
|
69
|
+
"@ory/argus": "1.0.0"
|
|
68
70
|
},
|
|
69
71
|
"engines": {
|
|
70
72
|
"node": ">=22"
|
|
@@ -74,7 +76,6 @@
|
|
|
74
76
|
"clean": "rm -rf dist *.tsbuildinfo",
|
|
75
77
|
"test": "vitest run",
|
|
76
78
|
"test:watch": "vitest",
|
|
77
|
-
"typecheck": "tsc --noEmit"
|
|
78
|
-
"dev": "node dist/dev/launcher.js"
|
|
79
|
+
"typecheck": "tsc --noEmit"
|
|
79
80
|
}
|
|
80
81
|
}
|