@secure-ai/guard 0.1.0 → 0.2.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 CHANGED
@@ -133,7 +133,7 @@ cause in a process already carrying too many.
133
133
  ## Also available over MCP
134
134
 
135
135
  Agents that speak MCP can reach the same controls as tools at
136
- `https://api.secureai.one/mcp` — `inspect_action`, `check_policy`,
136
+ `https://secureai.one/mcp` — `inspect_action`, `check_policy`,
137
137
  `recent_activity`, plus `redact` and `restore`.
138
138
 
139
139
  Full reference: <https://secureai.one/developers>
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
@@ -82,7 +82,20 @@ export class SecureAIError extends Error {
82
82
  this.code = code;
83
83
  }
84
84
  }
85
- const DEFAULT_BASE = "https://api.secureai.one";
85
+ /**
86
+ * Where the client talks to, when nobody says otherwise.
87
+ *
88
+ * This was an api. subdomain in 0.2.0, and that host does not
89
+ * exist -- it was never created. Nothing caught it: the tests
90
+ * pass a baseUrl, the typecheck has no opinion about a string, and the
91
+ * package published cleanly. The first person to install the SDK and follow
92
+ * the README would have got ENOTFOUND on their first call.
93
+ *
94
+ * secureai.one/v1 is the address the docs have always given and it is
95
+ * proxied to the Worker, so this is the same endpoint the curl examples hit.
96
+ * Keep the two in step: docs/api.md in the web repo is the other half.
97
+ */
98
+ const DEFAULT_BASE = "https://secureai.one";
86
99
  export class SecureAI {
87
100
  apiKey;
88
101
  baseUrl;
@@ -231,6 +244,31 @@ export class SecureAI {
231
244
  * check would make protection opt-in at every site, which is the thing this
232
245
  * exists to stop.
233
246
  */
247
+ /**
248
+ * What to hand the wrapped function.
249
+ *
250
+ * The API omits `input` only on a block, so on any other decision it is
251
+ * there. The question is what to do if it is not, and the answer is not
252
+ * "send what the caller had".
253
+ *
254
+ * On a redact, the caller's input is the one thing that must not go: it
255
+ * still contains the values the decision just said to replace. Falling
256
+ * back to it turns a missing field into the product doing the exact
257
+ * opposite of its purpose, silently, with the audit trail recording a
258
+ * redaction that did not happen. So a redact with nothing to send throws.
259
+ *
260
+ * On an allow, nothing was rewritten and the caller's own input is the
261
+ * correct thing to pass, so the fallback is right there and stays.
262
+ */
263
+ sendable(tool, verdict, original) {
264
+ if (verdict.input !== undefined)
265
+ return verdict.input;
266
+ if (verdict.decision === "redact") {
267
+ throw new SecureAIError(`Secure AI decided to redact ${tool} but returned nothing to send. ` +
268
+ `The original was not sent: it still holds the values that decision was about.`, 502, "missing_rewritten_input");
269
+ }
270
+ return original;
271
+ }
234
272
  guard(tool, fn, opts) {
235
273
  return async (input) => {
236
274
  let verdict;
@@ -280,11 +318,9 @@ export class SecureAI {
280
318
  });
281
319
  if (after.decision === "block")
282
320
  throw new ActionBlocked(tool, after);
283
- return await fn((after.input ?? input));
321
+ return await fn(this.sendable(tool, after, input));
284
322
  }
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));
323
+ return await fn(this.sendable(tool, verdict, input));
288
324
  };
289
325
  }
290
326
  /**
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.1",
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",