agentfences 0.9.0 → 0.9.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 +1 -1
- package/dist/client.js +6 -1
- package/dist/core.d.ts +6 -0
- package/dist/core.js +27 -3
- package/dist/run.d.ts +2 -0
- package/dist/run.js +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ await context({ user_id: "u_123", session_id: "s_9" }, () => research(question))
|
|
|
57
57
|
init({ apiKey: process.env.FENCES_API_KEY, endpoint: "https://your-fences-host", environment: "prod", release: "v1.4.2" });
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
The server is authoritative for limits and sees spend across processes. If it can't be reached, limits are enforced locally; pass `failClosed: true` to stop instead. A rejected key throws `FencesAuthError` when the run starts, and a key revoked during a run ends it at its next checkpoint (`fences_unreachable`); a quarantined agent throws `AgentQuarantined` before any of its code runs. A cost that isn't a finite number of zero or more throws instead of being counted. In serverless handlers, `await flush()` before returning so queued decisions and spans are sent.
|
|
60
|
+
The server is authoritative for limits and sees spend across processes. If it can't be reached, limits are enforced locally; pass `failClosed: true` to stop instead. `endpoint` must be an `https://` URL unless the server is on the same machine; `allowHttp: true` permits plain HTTP for a trusted private network (from 0.9.1). A server reply can raise a budget, but can't make a budget set in code unlimited. A rejected key throws `FencesAuthError` when the run starts, and a key revoked during a run ends it at its next checkpoint (`fences_unreachable`); a quarantined agent throws `AgentQuarantined` before any of its code runs. A cost that isn't a finite number of zero or more throws instead of being counted. In serverless handlers, `await flush()` before returning so queued decisions and spans are sent.
|
|
61
61
|
|
|
62
62
|
## Live control (from 0.4.0)
|
|
63
63
|
|
package/dist/client.js
CHANGED
|
@@ -94,8 +94,9 @@ export class FencesClient {
|
|
|
94
94
|
this.breaker.record(true);
|
|
95
95
|
return { network_error: String(e) };
|
|
96
96
|
}
|
|
97
|
+
// Redirects aren't followed: one would carry the API key (and the request) to wherever it points
|
|
97
98
|
const send = () => fetch(this.endpoint + path, {
|
|
98
|
-
method: "POST", headers: this.headers, body: json, signal: AbortSignal.timeout(this.timeoutMs),
|
|
99
|
+
method: "POST", headers: this.headers, body: json, signal: AbortSignal.timeout(this.timeoutMs), redirect: "manual",
|
|
99
100
|
});
|
|
100
101
|
let resp;
|
|
101
102
|
try {
|
|
@@ -115,6 +116,10 @@ export class FencesClient {
|
|
|
115
116
|
}
|
|
116
117
|
// A 429 on the heartbeat is its own budget, not an outage: it mustn't cut off checkpoints
|
|
117
118
|
this.breaker.record(resp.status < 500 && (resp.status !== 429 || path === "/api/heartbeat"));
|
|
119
|
+
if (resp.type === "opaqueredirect" || (resp.status >= 300 && resp.status < 400)) { // not followed: treated as no answer
|
|
120
|
+
return { network_error: `Fences answered with a redirect (HTTP ${resp.status} to ${JSON.stringify(resp.headers.get("location") ?? "?")}), ` +
|
|
121
|
+
"which isn't followed; check the endpoint" };
|
|
122
|
+
}
|
|
118
123
|
const text = await resp.text().catch(() => "");
|
|
119
124
|
let body = {}, isJson = false;
|
|
120
125
|
try {
|
package/dist/core.d.ts
CHANGED
|
@@ -55,6 +55,12 @@ export interface InitOptions {
|
|
|
55
55
|
* still send the values, which rules and approvers need; the redact hook can change or drop them.
|
|
56
56
|
*/
|
|
57
57
|
toolArguments?: ToolArguments;
|
|
58
|
+
/**
|
|
59
|
+
* endpoint must use https://, except on this machine (localhost, 127.0.0.1, ::1): over plain HTTP anyone
|
|
60
|
+
* on the network path could read the API key and rewrite the server's replies, limits included.
|
|
61
|
+
* true permits it, for a trusted private network.
|
|
62
|
+
*/
|
|
63
|
+
allowHttp?: boolean;
|
|
58
64
|
}
|
|
59
65
|
/** Call once at startup, before any governed function runs. */
|
|
60
66
|
export declare function init(options?: InitOptions): void;
|
package/dist/core.js
CHANGED
|
@@ -41,7 +41,9 @@ export function init(options = {}) {
|
|
|
41
41
|
throw new Error("apiKey is required unless localOnly is set. Use init({ localOnly: true }) for local usage.");
|
|
42
42
|
}
|
|
43
43
|
else {
|
|
44
|
-
|
|
44
|
+
const endpoint = options.endpoint ?? "http://localhost:8000";
|
|
45
|
+
checkEndpoint(endpoint, !!options.allowHttp);
|
|
46
|
+
client = new FencesClient(options.apiKey, endpoint);
|
|
45
47
|
}
|
|
46
48
|
control.configure(client, { heartbeatS: options.heartbeatS ?? 15, pauseTimeoutS: options.pauseTimeoutS ?? 3600,
|
|
47
49
|
stopGraceS: options.stopGraceS === undefined ? 30 : options.stopGraceS, onStop: options.onStop, onQuarantine: options.onQuarantine });
|
|
@@ -53,6 +55,26 @@ export function _setClient(c) {
|
|
|
53
55
|
}
|
|
54
56
|
/** @internal */
|
|
55
57
|
export const _client = () => client;
|
|
58
|
+
function checkEndpoint(endpoint, allowHttp) {
|
|
59
|
+
let url;
|
|
60
|
+
try {
|
|
61
|
+
url = new URL(endpoint);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new TypeError(`endpoint must be an https:// URL, not ${JSON.stringify(endpoint)}`);
|
|
65
|
+
}
|
|
66
|
+
if ((url.protocol !== "http:" && url.protocol !== "https:") || url.username || url.password || endpoint.includes("\\")) {
|
|
67
|
+
throw new TypeError(`endpoint must be an https:// URL with no username or backslash, not ${JSON.stringify(endpoint)}`);
|
|
68
|
+
}
|
|
69
|
+
if (url.protocol === "https:")
|
|
70
|
+
return;
|
|
71
|
+
const host = url.hostname; // parsed as fetch will: the host checked is the host connected to
|
|
72
|
+
const local = host === "localhost" || host === "127.0.0.1" || host === "[::1]";
|
|
73
|
+
if (!local && !allowHttp) {
|
|
74
|
+
throw new TypeError(`endpoint ${JSON.stringify(endpoint)} uses plain HTTP: the API key and the server's replies, limits ` +
|
|
75
|
+
"included, would cross the network unencrypted. Use https://, or pass allowHttp: true for a trusted private network.");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
56
78
|
function requireInit() {
|
|
57
79
|
if (!localOnly && !client) {
|
|
58
80
|
throw new Error("Call agentfences.init() before using Fences. For local usage: init({ localOnly: true })");
|
|
@@ -116,6 +138,7 @@ function outcome(run, failed, error) {
|
|
|
116
138
|
}
|
|
117
139
|
async function startRun(agentName, limits) {
|
|
118
140
|
const run = new Run(agentName, limits.budgetUsd ?? null, limits.maxIterations ?? DEFAULTS.maxIterations, limits.maxDurationMs ?? DEFAULTS.maxDurationMs, limits.maxTokens ?? DEFAULTS.maxTokens, events.currentContext());
|
|
141
|
+
run.budgetInCode = limits.budgetUsd != null;
|
|
119
142
|
if (!client) {
|
|
120
143
|
if (run.budgetUsd === null)
|
|
121
144
|
noBudget(agentName, "Set one with governed(fn, { budgetUsd: ... }).");
|
|
@@ -133,7 +156,8 @@ async function startRun(agentName, limits) {
|
|
|
133
156
|
if (resp.quarantined)
|
|
134
157
|
throw new AgentQuarantined(agentName, resp.detail);
|
|
135
158
|
// The server caps limits at the agent's ceilings: enforce the same numbers if it becomes unreachable later
|
|
136
|
-
|
|
159
|
+
// A null budget (unlimited) is taken only for a run whose code set none: a reply can't lift a budget from the code
|
|
160
|
+
control.adopt(run, resp.limits ?? {}, control.LIMITS, limits.budgetUsd == null);
|
|
137
161
|
for (const n of resp.notices ?? []) {
|
|
138
162
|
events.warnOnce(`${agentName}:${n.code}`, `${n.message ?? ""}${n.url ? ` ${n.url}` : ""}`);
|
|
139
163
|
}
|
|
@@ -243,7 +267,7 @@ function decide(run, resp) {
|
|
|
243
267
|
control.apply(run, resp.commands);
|
|
244
268
|
// The server is authoritative: adopt its totals and (on a breach) its current limits
|
|
245
269
|
control.adopt(run, resp, control.TOTALS);
|
|
246
|
-
control.adopt(run, resp, control.LIMITS,
|
|
270
|
+
control.adopt(run, resp, control.LIMITS, !run.budgetInCode);
|
|
247
271
|
// A fresh breach, or a 409 because the run is still fenced from an earlier one
|
|
248
272
|
run.lastBreach = resp.ok ? null : (resp.breach ?? run.lastBreach ?? "limit_reached");
|
|
249
273
|
}
|
package/dist/run.d.ts
CHANGED
|
@@ -60,6 +60,8 @@ export declare class Run {
|
|
|
60
60
|
reservedUsd: number;
|
|
61
61
|
/** Model calls' worst-case costs (see precheck.ts), released at the next checkpoint once they finish. */
|
|
62
62
|
holds: Hold[];
|
|
63
|
+
/** governed() set a budget: no server reply can make the run unlimited. */
|
|
64
|
+
budgetInCode: boolean;
|
|
63
65
|
/** "hold": wait at a limit for someone to raise it (see InitOptions.onBreach). */
|
|
64
66
|
onBreach: "hold" | null;
|
|
65
67
|
private readonly abort;
|
package/dist/run.js
CHANGED
|
@@ -28,6 +28,8 @@ export class Run {
|
|
|
28
28
|
reservedUsd = 0;
|
|
29
29
|
/** Model calls' worst-case costs (see precheck.ts), released at the next checkpoint once they finish. */
|
|
30
30
|
holds = [];
|
|
31
|
+
/** governed() set a budget: no server reply can make the run unlimited. */
|
|
32
|
+
budgetInCode = false;
|
|
31
33
|
/** "hold": wait at a limit for someone to raise it (see InitOptions.onBreach). */
|
|
32
34
|
onBreach = null;
|
|
33
35
|
abort = new AbortController();
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The one place the version lives; sent to Fences on every request. Keep in step with package.json. */
|
|
2
|
-
export declare const VERSION = "0.9.
|
|
2
|
+
export declare const VERSION = "0.9.1";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The one place the version lives; sent to Fences on every request. Keep in step with package.json. */
|
|
2
|
-
export const VERSION = "0.9.
|
|
2
|
+
export const VERSION = "0.9.1";
|
package/package.json
CHANGED