@secure-ai/guard 0.2.1 → 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/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
@@ -96,6 +96,30 @@ export class SecureAIError extends Error {
96
96
  * Keep the two in step: docs/api.md in the web repo is the other half.
97
97
  */
98
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
+ }
99
123
  export class SecureAI {
100
124
  apiKey;
101
125
  baseUrl;
@@ -159,7 +183,7 @@ export class SecureAI {
159
183
  /** One held action. */
160
184
  async approval(id) {
161
185
  const body = await this.request("GET", `/v1/approvals/${encodeURIComponent(id)}`);
162
- return body.approval;
186
+ return toApproval(body.approval);
163
187
  }
164
188
  /** Everything waiting, for a reviewer's screen. */
165
189
  async approvals(opts) {
@@ -170,7 +194,7 @@ export class SecureAI {
170
194
  q.set("limit", String(opts.limit));
171
195
  const qs = q.toString();
172
196
  const body = await this.request("GET", `/v1/approvals${qs ? `?${qs}` : ""}`);
173
- return body.approvals;
197
+ return (body.approvals ?? []).map(toApproval);
174
198
  }
175
199
  /**
176
200
  * Wait for a person to decide.
@@ -248,7 +272,7 @@ export class SecureAI {
248
272
  * What to hand the wrapped function.
249
273
  *
250
274
  * 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
275
+ * there. The question is what to do when it is not, and the answer is not
252
276
  * "send what the caller had".
253
277
  *
254
278
  * On a redact, the caller's input is the one thing that must not go: it
@@ -259,8 +283,32 @@ export class SecureAI {
259
283
  *
260
284
  * On an allow, nothing was rewritten and the caller's own input is the
261
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.
262
305
  */
263
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
+ }
264
312
  if (verdict.input !== undefined)
265
313
  return verdict.input;
266
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.1",
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",