@odingard/guardian-openai-agents 0.1.0 → 0.1.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 +13 -4
- package/dist/index.js +32 -14
- package/dist-dts/gate.d.ts +3 -2
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ the agent.
|
|
|
108
108
|
instead.
|
|
109
109
|
- Guardian error, network failure, timeout or a cancelled call — your
|
|
110
110
|
`execute` never runs, and a late answer from Guardian is ignored. The model
|
|
111
|
-
is told `Guardian could not check refund_order, so it did not run.`, and
|
|
111
|
+
is told `Guardian could not check refund_order, so it did not run. Try again later.`, and
|
|
112
112
|
`onGuardianError` is called. `decisionTimeoutMs` (default 30000) bounds the
|
|
113
113
|
wait for Guardian's decision; the Agents SDK's own `timeoutMs` and run
|
|
114
114
|
cancellation stop the wait too, but only while Guardian is still deciding.
|
|
@@ -137,13 +137,22 @@ even with `onAsk: "result"`.
|
|
|
137
137
|
In-process mode is decision-gated: `execute` runs after `ALLOW` and before
|
|
138
138
|
the capability is redeemed, so a revocation at redemption lands after the
|
|
139
139
|
effect. Use `mode: "remote"` where Guardian itself must hold the effect.
|
|
140
|
+
In remote mode, a call Guardian performed whose receipt status is not
|
|
141
|
+
`sent-verified` (for example the upstream answered with an error) is a
|
|
142
|
+
`GuardianExecutionFailed`, and the model is told it did not complete; it is
|
|
143
|
+
never reported as an `ALLOW` result.
|
|
140
144
|
`modes` sets the mode per tool name. The Agents SDK renames tools: every
|
|
141
145
|
character that is not a letter or a digit becomes `_` (`refund-order`
|
|
142
146
|
becomes `refund_order`), and Guardian sees the new name. A `modes` entry
|
|
143
147
|
under the name you wrote is applied to the new name; two entries for the
|
|
144
|
-
same tool that disagree are refused when the tool is built. `installRecordTransport
|
|
145
|
-
adapter propose the record-only entry
|
|
146
|
-
grant that allows it; production usually
|
|
148
|
+
same tool that disagree are refused when the tool is built. `installRecordTransport`
|
|
149
|
+
is off by default. `true` makes the adapter propose the record-only entry
|
|
150
|
+
itself on first use (this needs a grant that allows it; production usually
|
|
151
|
+
does not), and it only works for a tool that has no entry yet: an "already
|
|
152
|
+
registered" answer is refused (`RECORD_TRANSPORT_UNVERIFIED`) and nothing
|
|
153
|
+
runs, including after a restart for an entry the adapter created itself,
|
|
154
|
+
because which entries it registered is kept in memory only. Once the entry
|
|
155
|
+
exists, use it with the option off.
|
|
147
156
|
|
|
148
157
|
Setting the Agents SDK's own `needsApproval` on a tool adds a local approval
|
|
149
158
|
step before Guardian is asked. It never replaces Guardian's decision.
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,7 @@ var CallGate = class {
|
|
|
35
35
|
#signal;
|
|
36
36
|
#onAbort;
|
|
37
37
|
#rejecters = /* @__PURE__ */ new Set();
|
|
38
|
-
#
|
|
38
|
+
#setupIds = /* @__PURE__ */ new Set();
|
|
39
39
|
constructor(args) {
|
|
40
40
|
const { client, toolName, timeoutMs, signal } = args;
|
|
41
41
|
this.#timer = setTimeout(
|
|
@@ -71,29 +71,31 @@ var CallGate = class {
|
|
|
71
71
|
);
|
|
72
72
|
});
|
|
73
73
|
};
|
|
74
|
-
const
|
|
75
|
-
const
|
|
74
|
+
const setupIds = this.#setupIds;
|
|
75
|
+
const noteSetup = (result) => {
|
|
76
76
|
const id = result?.requestId;
|
|
77
|
-
if (typeof id === "string" && id.length > 0)
|
|
77
|
+
if (typeof id === "string" && id.length > 0) setupIds.add(id);
|
|
78
78
|
};
|
|
79
79
|
const request = decide(
|
|
80
80
|
async (i) => {
|
|
81
81
|
const result = await client.request(i);
|
|
82
|
-
if (i.action === "mcp.install")
|
|
82
|
+
if (i.action === "mcp.install" || i.action === "mcp.enable") {
|
|
83
|
+
noteSetup(result);
|
|
84
|
+
}
|
|
83
85
|
return result;
|
|
84
86
|
}
|
|
85
87
|
);
|
|
86
88
|
const claim = decide(async (id) => {
|
|
87
89
|
const result = await client.claim(id);
|
|
88
|
-
if (
|
|
90
|
+
if (setupIds.has(id)) noteSetup(result);
|
|
89
91
|
return result;
|
|
90
92
|
});
|
|
91
|
-
const
|
|
93
|
+
const executeSetup = decide(
|
|
92
94
|
(input) => client.execute(input)
|
|
93
95
|
);
|
|
94
96
|
const execute = (input) => {
|
|
95
|
-
if (
|
|
96
|
-
return
|
|
97
|
+
if (setupIds.has(input.requestId)) {
|
|
98
|
+
return executeSetup(input);
|
|
97
99
|
}
|
|
98
100
|
this.assertOpen();
|
|
99
101
|
this.commit();
|
|
@@ -171,12 +173,15 @@ function denyMessage(err) {
|
|
|
171
173
|
return `Guardian denied ${err.toolName} (${err.reasonCode}). It did not run. Do not retry the same call.`;
|
|
172
174
|
}
|
|
173
175
|
function unavailableMessage(toolName) {
|
|
174
|
-
return `Guardian could not check ${toolName}, so it did not run.`;
|
|
176
|
+
return `Guardian could not check ${toolName}, so it did not run. Try again later.`;
|
|
175
177
|
}
|
|
176
178
|
function executionFailedMessage(err) {
|
|
177
179
|
if (err.localEffectRan) {
|
|
178
180
|
return `${err.toolName} ran, but Guardian could not record it (${err.reasonCode}). Do not call it again; tell the user it ran without a receipt.`;
|
|
179
181
|
}
|
|
182
|
+
if (err.outcomeUnknown) {
|
|
183
|
+
return `Guardian could not confirm that ${err.toolName} completed (${err.reasonCode}, requestId ${err.requestId}). It may have run. Do not call it again; tell the user so they can check.`;
|
|
184
|
+
}
|
|
180
185
|
return `Guardian could not complete ${err.toolName} (${err.reasonCode}). Do not call it again; tell the user so they can check.`;
|
|
181
186
|
}
|
|
182
187
|
function remapModes(modes, writtenName, builtName) {
|
|
@@ -220,7 +225,9 @@ function buildTool(shared, toolOptions) {
|
|
|
220
225
|
});
|
|
221
226
|
const ctx = {
|
|
222
227
|
options: { ...coreOptions, client: gate.client },
|
|
223
|
-
ensured: shared.ensured
|
|
228
|
+
ensured: shared.ensured,
|
|
229
|
+
installed: shared.installed,
|
|
230
|
+
heldSetup: shared.heldSetup
|
|
224
231
|
};
|
|
225
232
|
try {
|
|
226
233
|
const output = await runThroughGuardian(ctx, toolName, input, () => {
|
|
@@ -242,7 +249,8 @@ function buildTool(shared, toolOptions) {
|
|
|
242
249
|
}
|
|
243
250
|
throw err;
|
|
244
251
|
}
|
|
245
|
-
|
|
252
|
+
const guardianDidNotAnswer = err instanceof GuardianDenied && err.reasonCode === "SERVICE_UNAVAILABLE" && !startedBeforeError;
|
|
253
|
+
if (err instanceof GuardianDenied && !guardianDidNotAnswer) {
|
|
246
254
|
if (onDeny === "result" && !hasOutputSchema) {
|
|
247
255
|
return denyMessage(err);
|
|
248
256
|
}
|
|
@@ -299,12 +307,22 @@ function buildTool(shared, toolOptions) {
|
|
|
299
307
|
}
|
|
300
308
|
function guardianTool(toolOptions, options) {
|
|
301
309
|
return buildTool(
|
|
302
|
-
{
|
|
310
|
+
{
|
|
311
|
+
options,
|
|
312
|
+
ensured: /* @__PURE__ */ new Set(),
|
|
313
|
+
installed: /* @__PURE__ */ new Set(),
|
|
314
|
+
heldSetup: /* @__PURE__ */ new Map()
|
|
315
|
+
},
|
|
303
316
|
toolOptions
|
|
304
317
|
);
|
|
305
318
|
}
|
|
306
319
|
function createGuardianTool(options) {
|
|
307
|
-
const shared = {
|
|
320
|
+
const shared = {
|
|
321
|
+
options,
|
|
322
|
+
ensured: /* @__PURE__ */ new Set(),
|
|
323
|
+
installed: /* @__PURE__ */ new Set(),
|
|
324
|
+
heldSetup: /* @__PURE__ */ new Map()
|
|
325
|
+
};
|
|
308
326
|
return (toolOptions) => buildTool(shared, toolOptions);
|
|
309
327
|
}
|
|
310
328
|
function findApprovalPending(err) {
|
package/dist-dts/gate.d.ts
CHANGED
|
@@ -21,8 +21,9 @@ export declare class GuardianUnavailable extends Error {
|
|
|
21
21
|
*
|
|
22
22
|
* Once the tool starts (or Guardian is asked to execute the tool call) the
|
|
23
23
|
* gate is committed and stops interfering, so a started action is always
|
|
24
|
-
* recorded. Executing a record-transport
|
|
25
|
-
*
|
|
24
|
+
* recorded. Executing a record-transport setup step (mcp.install or
|
|
25
|
+
* mcp.enable) does not commit the gate: the deadline and the abort signal
|
|
26
|
+
* still apply to the tool call after it.
|
|
26
27
|
* Once the call has settled (for example an ASK handed back to the app),
|
|
27
28
|
* the gate opens so `resume()` can claim the approval later.
|
|
28
29
|
*/
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@odingard/guardian-openai-agents",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "OpenAI Agents SDK adapter — route an agent's tool calls through Guardian for a deterministic ALLOW/ASK/DENY decision before anything executes.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
+
"author": "Odingard Security LLC",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"exports": {
|
|
8
9
|
".": {
|
|
@@ -26,11 +27,11 @@
|
|
|
26
27
|
"prepublishOnly": "npm run build && npm test"
|
|
27
28
|
},
|
|
28
29
|
"peerDependencies": {
|
|
29
|
-
"@odingard/guardian-sdk": "^0.3.
|
|
30
|
+
"@odingard/guardian-sdk": "^0.3.9",
|
|
30
31
|
"@openai/agents": ">=0.21.0 <0.22.0"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
|
-
"@odingard/guardian-sdk": "^0.3.
|
|
34
|
+
"@odingard/guardian-sdk": "^0.3.9",
|
|
34
35
|
"@openai/agents": "0.21.0",
|
|
35
36
|
"@types/node": "^22.20.5",
|
|
36
37
|
"esbuild": "^0.28.2",
|