@arnilo/prism 0.0.6 → 0.0.8
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/CHANGELOG.md +32 -0
- package/README.md +3 -1
- package/dist/agent-loops.js +8 -5
- package/dist/agent-run-lifecycle.d.ts +28 -0
- package/dist/agent-run-lifecycle.js +33 -0
- package/dist/agent-run-state.d.ts +53 -0
- package/dist/agent-run-state.js +127 -0
- package/dist/agents.d.ts +3 -1
- package/dist/agents.js +356 -45
- package/dist/contracts.d.ts +218 -3
- package/dist/contracts.js +4 -0
- package/dist/guardrails.d.ts +25 -0
- package/dist/guardrails.js +133 -0
- package/dist/index.d.ts +15 -3
- package/dist/index.js +9 -3
- package/dist/input.js +2 -0
- package/dist/resources.js +2 -1
- package/dist/run-ledger.d.ts +21 -0
- package/dist/run-ledger.js +115 -0
- package/dist/run-limits.d.ts +34 -0
- package/dist/run-limits.js +163 -0
- package/dist/secure-agent.d.ts +3 -0
- package/dist/secure-agent.js +63 -0
- package/dist/tools.d.ts +10 -2
- package/dist/tools.js +54 -4
- package/docs/a2a.md +61 -42
- package/docs/agent-events.md +15 -3
- package/docs/agent-loops.md +12 -4
- package/docs/agent-session-runtime.md +34 -1
- package/docs/credential-storage.md +9 -0
- package/docs/database-persistence.md +1 -1
- package/docs/evaluations.md +26 -3
- package/docs/guardrails.md +75 -0
- package/docs/host-security.md +31 -4
- package/docs/index.md +17 -14
- package/docs/mcp-tools.md +36 -8
- package/docs/migration.md +54 -0
- package/docs/observability.md +26 -14
- package/docs/performance.md +25 -0
- package/docs/postgres-persistence.md +1 -0
- package/docs/providers/kimi.md +16 -2
- package/docs/providers/opencode-go.md +43 -2
- package/docs/release-and-install.md +73 -62
- package/docs/resource-loading.md +4 -0
- package/docs/review-coverage-2026-07-19-phase-3.md +174 -0
- package/docs/run-ledger-conformance.md +1 -0
- package/docs/runs-and-usage.md +46 -4
- package/docs/server.md +5 -2
- package/docs/sqlite-persistence.md +1 -0
- package/docs/supervisors.md +2 -2
- package/docs/tools.md +8 -2
- package/docs/web-tools.md +78 -0
- package/docs/workflows.md +2 -0
- package/package.json +2 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export const DEFAULT_LEDGER_BATCH_ENTRIES = 128;
|
|
2
|
+
export const HARD_LEDGER_BATCH_ENTRIES = 4096;
|
|
3
|
+
export const DEFAULT_LEDGER_BATCH_BYTES = 512 * 1024;
|
|
4
|
+
export const HARD_LEDGER_BATCH_BYTES = 8 * 1024 * 1024;
|
|
5
|
+
export const DEFAULT_LEDGER_BATCH_DELAY_MS = 25;
|
|
6
|
+
export const HARD_LEDGER_BATCH_DELAY_MS = 60_000;
|
|
7
|
+
function integer(value, fallback, hard, name) {
|
|
8
|
+
const selected = value ?? fallback;
|
|
9
|
+
if (!Number.isInteger(selected) || selected < 1 || selected > hard)
|
|
10
|
+
throw new RangeError(`${name} must be an integer in [1, ${hard}]`);
|
|
11
|
+
return selected;
|
|
12
|
+
}
|
|
13
|
+
function terminal(record) {
|
|
14
|
+
return record.status !== undefined && record.status !== "queued" && record.status !== "running";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Wrap any RunLedger with one bounded FIFO. Inputs must already be redacted, as required by RunLedger.
|
|
18
|
+
* `buffered` may lose accepted records on process crash; call `flush()` for acknowledgement.
|
|
19
|
+
*/
|
|
20
|
+
export function createBatchedRunLedger(target, options = {}) {
|
|
21
|
+
const maxBatchEntries = integer(options.maxBatchEntries, DEFAULT_LEDGER_BATCH_ENTRIES, HARD_LEDGER_BATCH_ENTRIES, "maxBatchEntries");
|
|
22
|
+
const maxBatchBytes = integer(options.maxBatchBytes, DEFAULT_LEDGER_BATCH_BYTES, HARD_LEDGER_BATCH_BYTES, "maxBatchBytes");
|
|
23
|
+
const maxBufferedEntries = integer(options.maxBufferedEntries, Math.min(HARD_LEDGER_BATCH_ENTRIES, maxBatchEntries * 2), HARD_LEDGER_BATCH_ENTRIES, "maxBufferedEntries");
|
|
24
|
+
const maxBufferedBytes = integer(options.maxBufferedBytes, Math.min(HARD_LEDGER_BATCH_BYTES, maxBatchBytes * 2), HARD_LEDGER_BATCH_BYTES, "maxBufferedBytes");
|
|
25
|
+
const maxDelayMs = integer(options.maxDelayMs, DEFAULT_LEDGER_BATCH_DELAY_MS, HARD_LEDGER_BATCH_DELAY_MS, "maxDelayMs");
|
|
26
|
+
const durability = options.durability ?? "flush_on_terminal";
|
|
27
|
+
const queue = [];
|
|
28
|
+
let bufferedBytes = 0;
|
|
29
|
+
let accepted = 0;
|
|
30
|
+
let flushed = 0;
|
|
31
|
+
let timer;
|
|
32
|
+
let flushChain = Promise.resolve();
|
|
33
|
+
let disposed = false;
|
|
34
|
+
const status = () => ({ accepted, flushed, buffered: queue.length });
|
|
35
|
+
const cancelTimer = () => { if (timer)
|
|
36
|
+
clearTimeout(timer); timer = undefined; };
|
|
37
|
+
const schedule = () => {
|
|
38
|
+
if (timer || disposed || queue.length === 0)
|
|
39
|
+
return;
|
|
40
|
+
timer = setTimeout(() => { timer = undefined; void flush().catch(() => undefined); }, maxDelayMs);
|
|
41
|
+
timer.unref?.();
|
|
42
|
+
};
|
|
43
|
+
const write = (item) => {
|
|
44
|
+
if (item.kind === "run")
|
|
45
|
+
return target.appendRun(item.record);
|
|
46
|
+
if (item.kind === "event")
|
|
47
|
+
return target.appendEvent(item.record);
|
|
48
|
+
if (item.kind === "tool")
|
|
49
|
+
return target.appendToolCall(item.record);
|
|
50
|
+
return target.appendUsage(item.record);
|
|
51
|
+
};
|
|
52
|
+
const flush = () => {
|
|
53
|
+
cancelTimer();
|
|
54
|
+
const operation = flushChain.then(async () => {
|
|
55
|
+
let entries = 0;
|
|
56
|
+
let bytes = 0;
|
|
57
|
+
while (queue.length) {
|
|
58
|
+
const item = queue[0];
|
|
59
|
+
if (entries && (entries >= maxBatchEntries || bytes + item.bytes > maxBatchBytes)) {
|
|
60
|
+
entries = 0;
|
|
61
|
+
bytes = 0;
|
|
62
|
+
}
|
|
63
|
+
await write(item);
|
|
64
|
+
queue.shift();
|
|
65
|
+
bufferedBytes -= item.bytes;
|
|
66
|
+
flushed += 1;
|
|
67
|
+
entries += 1;
|
|
68
|
+
bytes += item.bytes;
|
|
69
|
+
}
|
|
70
|
+
return status();
|
|
71
|
+
});
|
|
72
|
+
flushChain = operation.then(() => undefined, () => undefined);
|
|
73
|
+
return operation;
|
|
74
|
+
};
|
|
75
|
+
const enqueue = async (item) => {
|
|
76
|
+
if (disposed)
|
|
77
|
+
throw new Error("batched run ledger is disposed");
|
|
78
|
+
const bytes = Buffer.byteLength(JSON.stringify(item.record));
|
|
79
|
+
if (bytes > maxBatchBytes || bytes > maxBufferedBytes)
|
|
80
|
+
throw new RangeError("run ledger record exceeds byte limit");
|
|
81
|
+
if (queue.length >= maxBufferedEntries || bufferedBytes + bytes > maxBufferedBytes)
|
|
82
|
+
await flush();
|
|
83
|
+
queue.push({ ...item, bytes });
|
|
84
|
+
bufferedBytes += bytes;
|
|
85
|
+
accepted += 1;
|
|
86
|
+
if (durability === "write_through" || queue.length >= maxBatchEntries || bufferedBytes >= maxBatchBytes || (item.kind === "run" && terminal(item.record) && durability === "flush_on_terminal"))
|
|
87
|
+
await flush();
|
|
88
|
+
else
|
|
89
|
+
schedule();
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
durability,
|
|
93
|
+
appendRun: (record) => enqueue({ kind: "run", record }),
|
|
94
|
+
appendEvent: (record) => enqueue({ kind: "event", record }),
|
|
95
|
+
appendToolCall: (record) => enqueue({ kind: "tool", record }),
|
|
96
|
+
appendUsage: (record) => enqueue({ kind: "usage", record }),
|
|
97
|
+
flush,
|
|
98
|
+
status,
|
|
99
|
+
async dispose(disposeOptions = {}) {
|
|
100
|
+
disposed = true;
|
|
101
|
+
cancelTimer();
|
|
102
|
+
if (disposeOptions.flush !== false)
|
|
103
|
+
await flush();
|
|
104
|
+
else {
|
|
105
|
+
await flushChain;
|
|
106
|
+
queue.length = 0;
|
|
107
|
+
bufferedBytes = 0;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function isFlushableRunLedger(ledger) {
|
|
113
|
+
return "flush" in ledger && typeof ledger.flush === "function";
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=run-ledger.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { RunLimitBreach, RunLimitCounters, RunLimitName, RunLimits, Usage } from "./contracts.js";
|
|
2
|
+
export declare const DEFAULT_RUN_LIMITS: Required<Omit<RunLimits, "maxCost">>;
|
|
3
|
+
export declare const HARD_MAX_RUN_COST = 10000;
|
|
4
|
+
export declare const HARD_RUN_LIMITS: Required<Omit<RunLimits, "maxCost">>;
|
|
5
|
+
export declare class RunLimitError extends Error {
|
|
6
|
+
readonly breach: RunLimitBreach;
|
|
7
|
+
readonly code = "ERR_PRISM_RUN_LIMIT";
|
|
8
|
+
constructor(breach: RunLimitBreach);
|
|
9
|
+
}
|
|
10
|
+
export interface RunLimitTrackerOptions {
|
|
11
|
+
readonly onExceeded?: (breach: RunLimitBreach) => void;
|
|
12
|
+
/** Durable resumption restores cumulative counters and original wall deadline. */
|
|
13
|
+
readonly snapshot?: RunLimitCounters;
|
|
14
|
+
readonly deadlineAt?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Validate one host-authored layer. Defaults are applied only after inheritance is resolved. */
|
|
17
|
+
export declare function resolveRunLimits(agent?: RunLimits, run?: RunLimits): Readonly<Required<Omit<RunLimits, "maxCost">> & Pick<RunLimits, "maxCost">>;
|
|
18
|
+
export declare class RunLimitTracker {
|
|
19
|
+
private readonly options;
|
|
20
|
+
readonly limits: Readonly<Required<Omit<RunLimits, "maxCost">> & Pick<RunLimits, "maxCost">>;
|
|
21
|
+
private readonly startedAt;
|
|
22
|
+
readonly deadlineAt: string;
|
|
23
|
+
private readonly counters;
|
|
24
|
+
private timer?;
|
|
25
|
+
private exceeded?;
|
|
26
|
+
constructor(limits: Readonly<Required<Omit<RunLimits, "maxCost">> & Pick<RunLimits, "maxCost">>, options?: RunLimitTrackerOptions);
|
|
27
|
+
get breach(): RunLimitBreach | undefined;
|
|
28
|
+
snapshot(): RunLimitCounters;
|
|
29
|
+
dispose(): void;
|
|
30
|
+
charge(limit: Exclude<RunLimitName, "maxCost">, delta?: number): void;
|
|
31
|
+
recordUsage(usage: Usage | undefined): void;
|
|
32
|
+
private exceed;
|
|
33
|
+
}
|
|
34
|
+
export declare function createRunLimitTracker(limits: RunLimits | undefined, options?: RunLimitTrackerOptions): RunLimitTracker;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
export const DEFAULT_RUN_LIMITS = Object.freeze({
|
|
2
|
+
maxTurns: 16,
|
|
3
|
+
maxProviderAttempts: 24,
|
|
4
|
+
maxToolRounds: 8,
|
|
5
|
+
maxToolCalls: 32,
|
|
6
|
+
maxWallTimeMs: 120_000,
|
|
7
|
+
maxRequestBytes: 8 * 1024 * 1024,
|
|
8
|
+
maxResponseBytes: 8 * 1024 * 1024,
|
|
9
|
+
maxInputTokens: 40_000,
|
|
10
|
+
maxOutputTokens: 10_000,
|
|
11
|
+
maxTotalTokens: 50_000,
|
|
12
|
+
});
|
|
13
|
+
export const HARD_MAX_RUN_COST = 10_000;
|
|
14
|
+
export const HARD_RUN_LIMITS = Object.freeze({
|
|
15
|
+
maxTurns: 64,
|
|
16
|
+
maxProviderAttempts: 256,
|
|
17
|
+
maxToolRounds: 64,
|
|
18
|
+
maxToolCalls: 256,
|
|
19
|
+
maxWallTimeMs: 30 * 60_000,
|
|
20
|
+
maxRequestBytes: 64 * 1024 * 1024,
|
|
21
|
+
maxResponseBytes: 64 * 1024 * 1024,
|
|
22
|
+
maxInputTokens: 1_000_000,
|
|
23
|
+
maxOutputTokens: 250_000,
|
|
24
|
+
maxTotalTokens: 1_000_000,
|
|
25
|
+
});
|
|
26
|
+
const LIMIT_NAMES = Object.keys(DEFAULT_RUN_LIMITS);
|
|
27
|
+
const COUNTER_FOR = {
|
|
28
|
+
maxTurns: "turns",
|
|
29
|
+
maxProviderAttempts: "providerAttempts",
|
|
30
|
+
maxToolRounds: "toolRounds",
|
|
31
|
+
maxToolCalls: "toolCalls",
|
|
32
|
+
maxWallTimeMs: "wallTimeMs",
|
|
33
|
+
maxRequestBytes: "requestBytes",
|
|
34
|
+
maxResponseBytes: "responseBytes",
|
|
35
|
+
maxInputTokens: "inputTokens",
|
|
36
|
+
maxOutputTokens: "outputTokens",
|
|
37
|
+
maxTotalTokens: "totalTokens",
|
|
38
|
+
maxCost: "cost",
|
|
39
|
+
};
|
|
40
|
+
export class RunLimitError extends Error {
|
|
41
|
+
breach;
|
|
42
|
+
code = "ERR_PRISM_RUN_LIMIT";
|
|
43
|
+
constructor(breach) {
|
|
44
|
+
super(`Run limit exceeded: ${breach.limit}`);
|
|
45
|
+
this.breach = breach;
|
|
46
|
+
this.name = "RunLimitError";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Validate one host-authored layer. Defaults are applied only after inheritance is resolved. */
|
|
50
|
+
export function resolveRunLimits(agent, run) {
|
|
51
|
+
const base = agent ? validateLimits(agent) : undefined;
|
|
52
|
+
const override = run ? validateLimits(run) : undefined;
|
|
53
|
+
const resolved = { ...DEFAULT_RUN_LIMITS };
|
|
54
|
+
for (const name of LIMIT_NAMES) {
|
|
55
|
+
if (base?.[name] !== undefined)
|
|
56
|
+
resolved[name] = base[name];
|
|
57
|
+
if (override?.[name] !== undefined)
|
|
58
|
+
resolved[name] = base ? Math.min(resolved[name], override[name]) : override[name];
|
|
59
|
+
}
|
|
60
|
+
const maxCost = override?.maxCost ?? base?.maxCost;
|
|
61
|
+
return Object.freeze({ ...resolved, ...(maxCost ? { maxCost: base?.maxCost && override?.maxCost ? { amount: Math.min(base.maxCost.amount, override.maxCost.amount), currency: base.maxCost.currency === override.maxCost.currency ? base.maxCost.currency : failCurrency() } : maxCost } : {}) });
|
|
62
|
+
}
|
|
63
|
+
function failCurrency() { throw new TypeError("Run limit currencies must match when narrowed"); }
|
|
64
|
+
function validateLimits(input) {
|
|
65
|
+
for (const name of LIMIT_NAMES) {
|
|
66
|
+
const value = input[name];
|
|
67
|
+
if (value === undefined)
|
|
68
|
+
continue;
|
|
69
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > HARD_RUN_LIMITS[name]) {
|
|
70
|
+
throw new TypeError(`${name} must be a positive safe integer at most ${HARD_RUN_LIMITS[name]}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (input.maxCost) {
|
|
74
|
+
const { amount, currency } = input.maxCost;
|
|
75
|
+
if (!Number.isFinite(amount) || amount < 0 || amount > HARD_MAX_RUN_COST || !currency.trim())
|
|
76
|
+
throw new TypeError(`maxCost requires a finite amount from 0 through ${HARD_MAX_RUN_COST} and currency`);
|
|
77
|
+
}
|
|
78
|
+
return input;
|
|
79
|
+
}
|
|
80
|
+
export class RunLimitTracker {
|
|
81
|
+
options;
|
|
82
|
+
limits;
|
|
83
|
+
startedAt = performance.now();
|
|
84
|
+
deadlineAt;
|
|
85
|
+
counters;
|
|
86
|
+
timer;
|
|
87
|
+
exceeded;
|
|
88
|
+
constructor(limits, options = {}) {
|
|
89
|
+
this.options = options;
|
|
90
|
+
this.limits = limits;
|
|
91
|
+
this.counters = { turns: 0, providerAttempts: 0, toolRounds: 0, toolCalls: 0, wallTimeMs: 0, requestBytes: 0, responseBytes: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, cost: 0, ...options.snapshot };
|
|
92
|
+
for (const [key, value] of Object.entries(this.counters)) {
|
|
93
|
+
if (!Number.isFinite(value) || value < 0 || (key !== "cost" && !Number.isSafeInteger(value))) {
|
|
94
|
+
throw new TypeError("Run limit snapshot must contain finite non-negative counters");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const deadline = options.deadlineAt ? Date.parse(options.deadlineAt) : Date.now() + limits.maxWallTimeMs;
|
|
98
|
+
if (!Number.isFinite(deadline))
|
|
99
|
+
throw new TypeError("Run limit deadlineAt is invalid");
|
|
100
|
+
this.deadlineAt = new Date(deadline).toISOString();
|
|
101
|
+
const remaining = Math.max(0, deadline - Date.now());
|
|
102
|
+
this.timer = setTimeout(() => this.exceed("maxWallTimeMs", limits.maxWallTimeMs), remaining);
|
|
103
|
+
this.timer.unref?.();
|
|
104
|
+
if (remaining === 0)
|
|
105
|
+
this.exceed("maxWallTimeMs", limits.maxWallTimeMs);
|
|
106
|
+
}
|
|
107
|
+
get breach() { return this.exceeded; }
|
|
108
|
+
snapshot() { return { ...this.counters, wallTimeMs: Math.min(this.limits.maxWallTimeMs, Math.ceil(performance.now() - this.startedAt)) }; }
|
|
109
|
+
dispose() { if (this.timer)
|
|
110
|
+
clearTimeout(this.timer); this.timer = undefined; }
|
|
111
|
+
charge(limit, delta = 1) {
|
|
112
|
+
if (!Number.isSafeInteger(delta) || delta < 0)
|
|
113
|
+
throw new TypeError("Run limit delta must be a non-negative safe integer");
|
|
114
|
+
const counter = COUNTER_FOR[limit];
|
|
115
|
+
const observed = this.counters[counter] + delta;
|
|
116
|
+
if (!Number.isSafeInteger(observed))
|
|
117
|
+
this.exceed(limit, Number.MAX_SAFE_INTEGER + 1);
|
|
118
|
+
this.counters[counter] = observed;
|
|
119
|
+
if (observed > this.limits[limit])
|
|
120
|
+
this.exceed(limit, observed);
|
|
121
|
+
}
|
|
122
|
+
recordUsage(usage) {
|
|
123
|
+
if (!usage) {
|
|
124
|
+
if (this.limits.maxCost)
|
|
125
|
+
this.exceed("maxCost", Number.POSITIVE_INFINITY);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
for (const key of ["inputTokens", "outputTokens", "totalTokens", "cacheReadTokens", "cacheWriteTokens"]) {
|
|
129
|
+
const value = usage[key];
|
|
130
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0))
|
|
131
|
+
throw new TypeError(`Provider usage ${key} must be a non-negative safe integer`);
|
|
132
|
+
}
|
|
133
|
+
const total = usage.totalTokens ?? ((usage.inputTokens ?? 0) + (usage.outputTokens ?? 0));
|
|
134
|
+
if (!Number.isSafeInteger(total))
|
|
135
|
+
throw new TypeError("Provider usage totalTokens is invalid");
|
|
136
|
+
this.charge("maxInputTokens", usage.inputTokens ?? 0);
|
|
137
|
+
this.charge("maxOutputTokens", usage.outputTokens ?? 0);
|
|
138
|
+
this.charge("maxTotalTokens", total);
|
|
139
|
+
if (usage.cost !== undefined && (!Number.isFinite(usage.cost) || usage.cost < 0))
|
|
140
|
+
throw new TypeError("Provider usage cost must be finite and non-negative");
|
|
141
|
+
if (!this.limits.maxCost)
|
|
142
|
+
return;
|
|
143
|
+
if (usage.cost === undefined || usage.currency !== this.limits.maxCost.currency)
|
|
144
|
+
this.exceed("maxCost", Number.POSITIVE_INFINITY);
|
|
145
|
+
const observed = this.counters.cost + usage.cost;
|
|
146
|
+
this.counters.cost = observed;
|
|
147
|
+
if (observed > this.limits.maxCost.amount)
|
|
148
|
+
this.exceed("maxCost", observed);
|
|
149
|
+
}
|
|
150
|
+
exceed(limit, observed) {
|
|
151
|
+
if (!this.exceeded) {
|
|
152
|
+
const maximum = limit === "maxCost" ? this.limits.maxCost?.amount ?? 0 : this.limits[limit];
|
|
153
|
+
this.exceeded = { limit, maximum, observed, ...(limit === "maxCost" && this.limits.maxCost ? { currency: this.limits.maxCost.currency } : {}) };
|
|
154
|
+
this.options.onExceeded?.(this.exceeded);
|
|
155
|
+
}
|
|
156
|
+
if (limit !== "maxWallTimeMs")
|
|
157
|
+
throw new RunLimitError(this.exceeded);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export function createRunLimitTracker(limits, options) {
|
|
161
|
+
return new RunLimitTracker(resolveRunLimits(undefined, limits), options);
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=run-limits.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createAgent } from "./agents.js";
|
|
2
|
+
import { validateRunStateOptions } from "./agent-run-state.js";
|
|
3
|
+
import { resolveRunLimits } from "./run-limits.js";
|
|
4
|
+
import { createToolParameterValidator, createToolRegistry } from "./tools.js";
|
|
5
|
+
/** Build an opt-in agent whose security-critical defaults cannot be replaced per run. */
|
|
6
|
+
export function createSecureAgent(options) {
|
|
7
|
+
if (!options.id.trim())
|
|
8
|
+
throw new TypeError("Secure agent requires a non-empty id");
|
|
9
|
+
if (!options.definitionRevision.trim())
|
|
10
|
+
throw new TypeError("Secure agent requires a non-empty definitionRevision");
|
|
11
|
+
if (!options.redactor || typeof options.redactor.redact !== "function")
|
|
12
|
+
throw new TypeError("Secure agent requires a redactor");
|
|
13
|
+
if (!options.permission || typeof options.permission.check !== "function")
|
|
14
|
+
throw new TypeError("Secure agent requires a permission policy");
|
|
15
|
+
if (!options.trust || typeof options.trust.check !== "function")
|
|
16
|
+
throw new TypeError("Secure agent requires a trust policy");
|
|
17
|
+
if (!options.toolArgumentValidator || typeof options.toolArgumentValidator.validate !== "function")
|
|
18
|
+
throw new TypeError("Secure agent requires a tool argument validator");
|
|
19
|
+
if (!options.limits || Object.keys(options.limits).length === 0)
|
|
20
|
+
throw new TypeError("Secure agent requires explicit limits");
|
|
21
|
+
if (!options.ownership || !Object.values(options.ownership).some((value) => typeof value === "string" && value.trim()))
|
|
22
|
+
throw new TypeError("Secure agent requires non-empty ownership");
|
|
23
|
+
for (const tool of options.tools) {
|
|
24
|
+
if (!tool.name.trim())
|
|
25
|
+
throw new TypeError("Secure agent tool names must be non-empty");
|
|
26
|
+
if (!tool.parameters || Object.keys(tool.parameters).length === 0)
|
|
27
|
+
throw new TypeError(`Secure agent tool ${tool.name} requires a non-empty parameters schema`);
|
|
28
|
+
}
|
|
29
|
+
resolveRunLimits(options.limits);
|
|
30
|
+
const runState = Object.freeze({ ...options.runState, definitionRevision: options.definitionRevision, interruptBeforeTool: true });
|
|
31
|
+
validateRunStateOptions(runState);
|
|
32
|
+
const config = Object.freeze({
|
|
33
|
+
...withoutSecureFields(options),
|
|
34
|
+
id: options.id,
|
|
35
|
+
tools: createToolRegistry(options.tools, { duplicate: "error" }),
|
|
36
|
+
validator: createToolParameterValidator(options.toolArgumentValidator, { missingSchema: "reject" }),
|
|
37
|
+
redactor: options.redactor,
|
|
38
|
+
permission: options.permission,
|
|
39
|
+
trust: options.trust,
|
|
40
|
+
ownership: Object.freeze({ ...options.ownership }),
|
|
41
|
+
limits: Object.freeze({ ...options.limits }),
|
|
42
|
+
guardrails: freezeGuardrails(options.guardrails),
|
|
43
|
+
runState,
|
|
44
|
+
secure: true,
|
|
45
|
+
});
|
|
46
|
+
return createAgent(config);
|
|
47
|
+
}
|
|
48
|
+
function withoutSecureFields(options) {
|
|
49
|
+
const { tools: _tools, toolArgumentValidator: _validator, redactor: _redactor, permission: _permission, trust: _trust, ownership: _ownership, limits: _limits, guardrails: _guardrails, definitionRevision: _revision, runState: _runState, ...config } = options;
|
|
50
|
+
return config;
|
|
51
|
+
}
|
|
52
|
+
function freezeGuardrails(guardrails) {
|
|
53
|
+
if (!guardrails)
|
|
54
|
+
return undefined;
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
...guardrails,
|
|
57
|
+
input: Object.freeze([...(guardrails.input ?? [])]),
|
|
58
|
+
output: Object.freeze([...(guardrails.output ?? [])]),
|
|
59
|
+
toolInput: Object.freeze([...(guardrails.toolInput ?? [])]),
|
|
60
|
+
toolOutput: Object.freeze([...(guardrails.toolOutput ?? [])]),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=secure-agent.js.map
|
package/dist/tools.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { AgentEvent, ErrorInfo, JsonObject, OwnershipScope, RunLedger, ToolCallContent, ToolDefinition, ToolExecutionContext, ToolRegistry, ToolResult } from "./contracts.js";
|
|
1
|
+
import type { AgentEvent, ErrorInfo, Guardrails, JsonObject, OwnershipScope, RunLedger, ToolCallContent, ToolDefinition, ToolExecutionContext, ToolRegistry, ToolResult } from "./contracts.js";
|
|
2
|
+
import type { RunLimitTracker } from "./run-limits.js";
|
|
2
3
|
import type { MiddlewareRegistry } from "./middleware.js";
|
|
3
4
|
import { type SecretRedactor } from "./redaction.js";
|
|
4
5
|
import { type DuplicateRegistrationOptions } from "./registry-options.js";
|
|
5
|
-
import { type PermissionPolicy } from "./security.js";
|
|
6
|
+
import { type PermissionPolicy, type TrustPolicy } from "./security.js";
|
|
6
7
|
export interface ToolFilter {
|
|
7
8
|
readonly allow?: readonly string[];
|
|
8
9
|
readonly deny?: readonly string[];
|
|
@@ -33,12 +34,19 @@ export interface DispatchToolCallOptions {
|
|
|
33
34
|
readonly filter?: ToolFilterInput;
|
|
34
35
|
readonly middleware?: MiddlewareRegistry;
|
|
35
36
|
readonly validate?: ToolValidator;
|
|
37
|
+
/** Adapter-specific policy check immediately before the tool side effect. */
|
|
38
|
+
readonly beforeExecute?: (call: ToolCallContent, tool: ToolDefinition, context: ToolExecutionContext) => void | Promise<void>;
|
|
36
39
|
readonly emit?: (event: AgentEvent) => void | Promise<void>;
|
|
37
40
|
readonly secrets?: readonly (string | undefined)[];
|
|
38
41
|
readonly permission?: PermissionPolicy;
|
|
42
|
+
readonly trust?: TrustPolicy;
|
|
39
43
|
readonly redactor?: SecretRedactor;
|
|
40
44
|
readonly ledger?: RunLedger;
|
|
41
45
|
readonly ownership?: OwnershipScope;
|
|
46
|
+
/** Tool stages run after middleware normalization and before side effects/exposure. */
|
|
47
|
+
readonly guardrails?: Guardrails;
|
|
48
|
+
/** Shared run tracker; direct hosts may supply one for their call scope. */
|
|
49
|
+
readonly limitTracker?: RunLimitTracker;
|
|
42
50
|
}
|
|
43
51
|
export interface ToolRegistryOptions extends DuplicateRegistrationOptions {
|
|
44
52
|
}
|
package/dist/tools.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { isJsonObject } from "./config.js";
|
|
2
2
|
import { createId } from "./ids.js";
|
|
3
|
+
import { GuardrailError, runGuardrails } from "./guardrails.js";
|
|
3
4
|
import { errorToErrorInfo, redactRunLedgerRecord, redactSecrets } from "./redaction.js";
|
|
4
5
|
import { assertCanRegister } from "./registry-options.js";
|
|
5
|
-
import { assertPermission } from "./security.js";
|
|
6
|
+
import { assertPermission, assertTrusted } from "./security.js";
|
|
6
7
|
/** Wrap a schema adapter as the existing `ToolValidator` seam used by dispatch and the agent runtime. */
|
|
7
8
|
export function createToolParameterValidator(validator, options = {}) {
|
|
8
9
|
const missingSchema = options.missingSchema ?? "allow";
|
|
@@ -59,10 +60,28 @@ function toolExecutionMetadata(startedAt, status) {
|
|
|
59
60
|
export async function dispatchToolCall(options) {
|
|
60
61
|
const secrets = options.secrets ?? [];
|
|
61
62
|
const startedAt = new Date().toISOString();
|
|
62
|
-
|
|
63
|
-
if (precheck)
|
|
64
|
-
return precheck;
|
|
63
|
+
options.limitTracker?.charge("maxToolCalls");
|
|
65
64
|
const mediatedCall = await (options.middleware?.run("tool_call", options.call) ?? options.call);
|
|
65
|
+
const inputGuards = await runGuardrails({
|
|
66
|
+
stage: "tool_input",
|
|
67
|
+
guardrails: options.guardrails,
|
|
68
|
+
value: mediatedCall,
|
|
69
|
+
context: {
|
|
70
|
+
sessionId: options.context.sessionId,
|
|
71
|
+
runId: options.context.runId,
|
|
72
|
+
toolCallId: mediatedCall.id,
|
|
73
|
+
toolName: mediatedCall.name,
|
|
74
|
+
metadata: options.context.metadata ?? {},
|
|
75
|
+
signal: options.context.signal,
|
|
76
|
+
},
|
|
77
|
+
redactor: options.redactor,
|
|
78
|
+
emit: options.emit,
|
|
79
|
+
});
|
|
80
|
+
if (inputGuards.terminal) {
|
|
81
|
+
if (inputGuards.terminal.action !== "block")
|
|
82
|
+
throw new GuardrailError(inputGuards.terminal);
|
|
83
|
+
return blocked(mediatedCall, options.context, "guardrail_blocked", { message: "Tool call blocked by guardrail" }, options, startedAt);
|
|
84
|
+
}
|
|
66
85
|
const tool = options.registry.get(mediatedCall.name);
|
|
67
86
|
const postcheck = await checkCall(mediatedCall, options, startedAt);
|
|
68
87
|
if (postcheck)
|
|
@@ -89,6 +108,7 @@ export async function dispatchToolCall(options) {
|
|
|
89
108
|
},
|
|
90
109
|
};
|
|
91
110
|
try {
|
|
111
|
+
await assertTrusted(options.trust, { kind: "tool", target: mediatedCall.name, capability: "execute", metadata: options.context.metadata });
|
|
92
112
|
await assertPermission(options.permission, { kind: "tool", action: "execute", target: mediatedCall.name, metadata: options.context.metadata });
|
|
93
113
|
}
|
|
94
114
|
catch (error) {
|
|
@@ -97,11 +117,39 @@ export async function dispatchToolCall(options) {
|
|
|
97
117
|
const validation = await options.validate?.(tool, mediatedCall.arguments, context);
|
|
98
118
|
if (validation)
|
|
99
119
|
return blocked(mediatedCall, context, "validation_failed", toErrorInfo(validation, secrets), options, startedAt);
|
|
120
|
+
try {
|
|
121
|
+
await options.beforeExecute?.(mediatedCall, tool, context);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error?.code === "ERR_PRISM_AGENT_RUN_SUSPENDED")
|
|
125
|
+
throw error;
|
|
126
|
+
return blocked(mediatedCall, context, "execution_denied", errorToErrorInfo(error, secrets), options, startedAt);
|
|
127
|
+
}
|
|
100
128
|
await options.emit?.({ type: "tool_execution_started", sessionId: context.sessionId, runId: context.runId, call: mediatedCall });
|
|
101
129
|
await appendToolCallRecord(options, "started", mediatedCall, startedAt, {});
|
|
102
130
|
try {
|
|
103
131
|
const raw = await tool.execute(mediatedCall.arguments, context);
|
|
104
132
|
const mediatedResult = await (options.middleware?.run("tool_result", raw) ?? raw);
|
|
133
|
+
const outputGuards = await runGuardrails({
|
|
134
|
+
stage: "tool_output",
|
|
135
|
+
guardrails: options.guardrails,
|
|
136
|
+
value: mediatedResult,
|
|
137
|
+
context: {
|
|
138
|
+
sessionId: context.sessionId,
|
|
139
|
+
runId: context.runId,
|
|
140
|
+
toolCallId: mediatedCall.id,
|
|
141
|
+
toolName: mediatedCall.name,
|
|
142
|
+
metadata: context.metadata ?? {},
|
|
143
|
+
signal: context.signal,
|
|
144
|
+
},
|
|
145
|
+
redactor: options.redactor,
|
|
146
|
+
emit: options.emit,
|
|
147
|
+
});
|
|
148
|
+
if (outputGuards.terminal) {
|
|
149
|
+
if (outputGuards.terminal.action !== "block")
|
|
150
|
+
throw new GuardrailError(outputGuards.terminal);
|
|
151
|
+
return blocked(mediatedCall, context, "guardrail_blocked", { message: "Tool result blocked by guardrail" }, options, startedAt);
|
|
152
|
+
}
|
|
105
153
|
const result = options.redactor?.redact(mediatedResult) ?? mediatedResult;
|
|
106
154
|
const finishedAt = new Date().toISOString();
|
|
107
155
|
const metadata = toolExecutionMetadata(startedAt, "finished");
|
|
@@ -110,6 +158,8 @@ export async function dispatchToolCall(options) {
|
|
|
110
158
|
return result;
|
|
111
159
|
}
|
|
112
160
|
catch (error) {
|
|
161
|
+
if (error instanceof GuardrailError)
|
|
162
|
+
throw error;
|
|
113
163
|
const info = errorToErrorInfo(error, secrets);
|
|
114
164
|
const result = { toolCallId: mediatedCall.id, name: mediatedCall.name, error: info };
|
|
115
165
|
const finishedAt = new Date().toISOString();
|