@odingard/guardian-openai-agents 0.1.0 → 0.1.2

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 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: true` makes the
145
- adapter propose the record-only entry itself on first use (this needs a
146
- grant that allows it; production usually does not).
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
- #installIds = /* @__PURE__ */ new Set();
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 installIds = this.#installIds;
75
- const noteInstall = (result) => {
74
+ const setupIds = this.#setupIds;
75
+ const noteSetup = (result) => {
76
76
  const id = result?.requestId;
77
- if (typeof id === "string" && id.length > 0) installIds.add(id);
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") noteInstall(result);
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 (installIds.has(id)) noteInstall(result);
90
+ if (setupIds.has(id)) noteSetup(result);
89
91
  return result;
90
92
  });
91
- const executeInstall = decide(
93
+ const executeSetup = decide(
92
94
  (input) => client.execute(input)
93
95
  );
94
96
  const execute = (input) => {
95
- if (installIds.has(input.requestId)) {
96
- return executeInstall(input);
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,13 +225,20 @@ 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
- const output = await runThroughGuardian(ctx, toolName, input, () => {
227
- gate.commit();
228
- return userExecute(input, runContext, details);
229
- });
233
+ const output = await runThroughGuardian(
234
+ ctx,
235
+ toolName,
236
+ input,
237
+ (approved) => {
238
+ gate.commit();
239
+ return userExecute(approved, runContext, details);
240
+ }
241
+ );
230
242
  gate.settle();
231
243
  return output;
232
244
  } catch (err) {
@@ -242,7 +254,8 @@ function buildTool(shared, toolOptions) {
242
254
  }
243
255
  throw err;
244
256
  }
245
- if (err instanceof GuardianDenied) {
257
+ const guardianDidNotAnswer = err instanceof GuardianDenied && err.reasonCode === "SERVICE_UNAVAILABLE" && !startedBeforeError;
258
+ if (err instanceof GuardianDenied && !guardianDidNotAnswer) {
246
259
  if (onDeny === "result" && !hasOutputSchema) {
247
260
  return denyMessage(err);
248
261
  }
@@ -299,12 +312,22 @@ function buildTool(shared, toolOptions) {
299
312
  }
300
313
  function guardianTool(toolOptions, options) {
301
314
  return buildTool(
302
- { options, ensured: /* @__PURE__ */ new Set() },
315
+ {
316
+ options,
317
+ ensured: /* @__PURE__ */ new Set(),
318
+ installed: /* @__PURE__ */ new Set(),
319
+ heldSetup: /* @__PURE__ */ new Map()
320
+ },
303
321
  toolOptions
304
322
  );
305
323
  }
306
324
  function createGuardianTool(options) {
307
- const shared = { options, ensured: /* @__PURE__ */ new Set() };
325
+ const shared = {
326
+ options,
327
+ ensured: /* @__PURE__ */ new Set(),
328
+ installed: /* @__PURE__ */ new Set(),
329
+ heldSetup: /* @__PURE__ */ new Map()
330
+ };
308
331
  return (toolOptions) => buildTool(shared, toolOptions);
309
332
  }
310
333
  function findApprovalPending(err) {
@@ -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 install does not commit the gate:
25
- * the deadline and the abort signal still apply to the tool call after it.
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.0",
3
+ "version": "0.1.2",
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.8",
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.8",
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",