@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 +1 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +65 -4
- package/package.json +1 -1
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://
|
|
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
|
|
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
|
-
|
|
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
|
|
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") {
|