@secure-ai/guard 0.1.0 → 0.2.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/dist/index.d.ts CHANGED
@@ -246,6 +246,23 @@ export declare class SecureAI {
246
246
  * check would make protection opt-in at every site, which is the thing this
247
247
  * exists to stop.
248
248
  */
249
+ /**
250
+ * What to hand the wrapped function.
251
+ *
252
+ * The API omits `input` only on a block, so on any other decision it is
253
+ * there. The question is what to do if it is not, and the answer is not
254
+ * "send what the caller had".
255
+ *
256
+ * On a redact, the caller's input is the one thing that must not go: it
257
+ * still contains the values the decision just said to replace. Falling
258
+ * back to it turns a missing field into the product doing the exact
259
+ * opposite of its purpose, silently, with the audit trail recording a
260
+ * redaction that did not happen. So a redact with nothing to send throws.
261
+ *
262
+ * On an allow, nothing was rewritten and the caller's own input is the
263
+ * correct thing to pass, so the fallback is right there and stays.
264
+ */
265
+ private sendable;
249
266
  guard<A, R>(tool: string, fn: (input: A) => Promise<R> | R, opts?: {
250
267
  direction?: Direction;
251
268
  agent?: string;
package/dist/index.js CHANGED
@@ -231,6 +231,31 @@ export class SecureAI {
231
231
  * check would make protection opt-in at every site, which is the thing this
232
232
  * exists to stop.
233
233
  */
234
+ /**
235
+ * What to hand the wrapped function.
236
+ *
237
+ * The API omits `input` only on a block, so on any other decision it is
238
+ * there. The question is what to do if it is not, and the answer is not
239
+ * "send what the caller had".
240
+ *
241
+ * On a redact, the caller's input is the one thing that must not go: it
242
+ * still contains the values the decision just said to replace. Falling
243
+ * back to it turns a missing field into the product doing the exact
244
+ * opposite of its purpose, silently, with the audit trail recording a
245
+ * redaction that did not happen. So a redact with nothing to send throws.
246
+ *
247
+ * On an allow, nothing was rewritten and the caller's own input is the
248
+ * correct thing to pass, so the fallback is right there and stays.
249
+ */
250
+ sendable(tool, verdict, original) {
251
+ if (verdict.input !== undefined)
252
+ return verdict.input;
253
+ if (verdict.decision === "redact") {
254
+ throw new SecureAIError(`Secure AI decided to redact ${tool} but returned nothing to send. ` +
255
+ `The original was not sent: it still holds the values that decision was about.`, 502, "missing_rewritten_input");
256
+ }
257
+ return original;
258
+ }
234
259
  guard(tool, fn, opts) {
235
260
  return async (input) => {
236
261
  let verdict;
@@ -280,11 +305,9 @@ export class SecureAI {
280
305
  });
281
306
  if (after.decision === "block")
282
307
  throw new ActionBlocked(tool, after);
283
- return await fn((after.input ?? input));
308
+ return await fn(this.sendable(tool, after, input));
284
309
  }
285
- // input is present whenever the decision is not block; the fallback is
286
- // belt and braces against a future field being dropped.
287
- return await fn((verdict.input ?? input));
310
+ return await fn(this.sendable(tool, verdict, input));
288
311
  };
289
312
  }
290
313
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@secure-ai/guard",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Data loss prevention for AI agents. Inspect what an agent is about to do, before it does it.",
5
5
  "license": "MIT",
6
6
  "type": "module",