@secure-ai/guard 0.2.0 → 0.2.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
@@ -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
@@ -250,7 +250,7 @@ export declare class SecureAI {
250
250
  * What to hand the wrapped function.
251
251
  *
252
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
253
+ * there. The question is what to do when it is not, and the answer is not
254
254
  * "send what the caller had".
255
255
  *
256
256
  * On a redact, the caller's input is the one thing that must not go: it
@@ -261,6 +261,25 @@ export declare class SecureAI {
261
261
  *
262
262
  * On an allow, nothing was rewritten and the caller's own input is the
263
263
  * correct thing to pass, so the fallback is right there and stays.
264
+ *
265
+ * ── Why this asks which decisions may send, rather than which may not ──
266
+ *
267
+ * Two decisions mean "this may go": allow and redact. Every other value,
268
+ * present or future, means it may not. Asked the other way round — refuse
269
+ * on block, refuse on approve, send otherwise — a decision added to the
270
+ * policy later is sent by a client that has never heard of it, and the
271
+ * agent is told the check passed.
272
+ *
273
+ * That is not hypothetical here. It is the bug this library already had:
274
+ * "approve" was added, `guard` compared against "block" and nothing else,
275
+ * and an action a policy said must wait for a person went immediately.
276
+ * The Python comment on that branch still records it. The same shape cost
277
+ * ten fixes across the Worker, the dashboard and both clients.
278
+ *
279
+ * Reachable today? No. The Worker returns 409 rather than a second
280
+ * "approve" when an approval cannot be redeemed, so the re-check after a
281
+ * yes comes back allow, redact, or an error. This is the client refusing
282
+ * to depend on that, across a version boundary it does not control.
264
283
  */
265
284
  private sendable;
266
285
  guard<A, R>(tool: string, fn: (input: A) => Promise<R> | R, opts?: {
package/dist/index.js CHANGED
@@ -82,7 +82,44 @@ 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";
99
+ /**
100
+ * An approval off the wire, with the one field the wait loop depends on
101
+ * made a number.
102
+ *
103
+ * waitForApproval stops when now is past expiresAt. Handed a body without
104
+ * one, `Date.now() >= undefined` is a NaN comparison, which is false —
105
+ * so the loop never stops. Not an error, not a hang anybody can see from
106
+ * the outside: a guarded call polling a metered API every two seconds for
107
+ * as long as the process lives.
108
+ *
109
+ * The Python client already coerced this, defaulting a missing expiry to 0
110
+ * and so treating it as already expired. This is the same rule: anything
111
+ * that is not a finite number means expired, which refuses rather than
112
+ * waits. Today's Worker always sends one — buildApproval sets it from a
113
+ * bounded TTL — and the client should not be the reason a change there
114
+ * becomes an infinite loop in somebody's agent.
115
+ */
116
+ function toApproval(raw) {
117
+ const expiresAt = Number(raw?.expiresAt);
118
+ return {
119
+ ...raw,
120
+ expiresAt: Number.isFinite(expiresAt) ? expiresAt : 0,
121
+ };
122
+ }
86
123
  export class SecureAI {
87
124
  apiKey;
88
125
  baseUrl;
@@ -146,7 +183,7 @@ export class SecureAI {
146
183
  /** One held action. */
147
184
  async approval(id) {
148
185
  const body = await this.request("GET", `/v1/approvals/${encodeURIComponent(id)}`);
149
- return body.approval;
186
+ return toApproval(body.approval);
150
187
  }
151
188
  /** Everything waiting, for a reviewer's screen. */
152
189
  async approvals(opts) {
@@ -157,7 +194,7 @@ export class SecureAI {
157
194
  q.set("limit", String(opts.limit));
158
195
  const qs = q.toString();
159
196
  const body = await this.request("GET", `/v1/approvals${qs ? `?${qs}` : ""}`);
160
- return body.approvals;
197
+ return (body.approvals ?? []).map(toApproval);
161
198
  }
162
199
  /**
163
200
  * Wait for a person to decide.
@@ -235,7 +272,7 @@ export class SecureAI {
235
272
  * What to hand the wrapped function.
236
273
  *
237
274
  * 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
275
+ * there. The question is what to do when it is not, and the answer is not
239
276
  * "send what the caller had".
240
277
  *
241
278
  * On a redact, the caller's input is the one thing that must not go: it
@@ -246,8 +283,32 @@ export class SecureAI {
246
283
  *
247
284
  * On an allow, nothing was rewritten and the caller's own input is the
248
285
  * correct thing to pass, so the fallback is right there and stays.
286
+ *
287
+ * ── Why this asks which decisions may send, rather than which may not ──
288
+ *
289
+ * Two decisions mean "this may go": allow and redact. Every other value,
290
+ * present or future, means it may not. Asked the other way round — refuse
291
+ * on block, refuse on approve, send otherwise — a decision added to the
292
+ * policy later is sent by a client that has never heard of it, and the
293
+ * agent is told the check passed.
294
+ *
295
+ * That is not hypothetical here. It is the bug this library already had:
296
+ * "approve" was added, `guard` compared against "block" and nothing else,
297
+ * and an action a policy said must wait for a person went immediately.
298
+ * The Python comment on that branch still records it. The same shape cost
299
+ * ten fixes across the Worker, the dashboard and both clients.
300
+ *
301
+ * Reachable today? No. The Worker returns 409 rather than a second
302
+ * "approve" when an approval cannot be redeemed, so the re-check after a
303
+ * yes comes back allow, redact, or an error. This is the client refusing
304
+ * to depend on that, across a version boundary it does not control.
249
305
  */
250
306
  sendable(tool, verdict, original) {
307
+ if (verdict.decision !== "allow" && verdict.decision !== "redact") {
308
+ throw new SecureAIError(`Secure AI answered "${verdict.decision}" for ${tool}, which this ` +
309
+ `version does not know how to send safely. Nothing was sent. ` +
310
+ `Upgrade @secure-ai/guard.`, 502, "unknown_decision");
311
+ }
251
312
  if (verdict.input !== undefined)
252
313
  return verdict.input;
253
314
  if (verdict.decision === "redact") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@secure-ai/guard",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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",