@arnilo/prism 0.0.6 → 0.0.7
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 +11 -0
- package/dist/agent-loops.js +1 -0
- 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 +335 -43
- package/dist/contracts.d.ts +203 -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 +13 -3
- package/dist/index.js +8 -3
- package/dist/input.js +2 -0
- package/dist/resources.js +2 -1
- 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/agent-events.md +13 -1
- package/docs/agent-loops.md +11 -3
- package/docs/agent-session-runtime.md +33 -1
- package/docs/guardrails.md +75 -0
- package/docs/host-security.md +6 -2
- package/docs/index.md +5 -4
- package/docs/mcp-tools.md +7 -3
- package/docs/migration.md +18 -0
- package/docs/release-and-install.md +40 -40
- package/docs/runs-and-usage.md +29 -2
- package/docs/server.md +5 -2
- package/docs/tools.md +6 -1
- package/docs/workflows.md +1 -0
- package/package.json +1 -1
|
@@ -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();
|
package/docs/agent-events.md
CHANGED
|
@@ -38,11 +38,12 @@ The `AgentEvent` union (grouped by concern):
|
|
|
38
38
|
|
|
39
39
|
| Group | Variants |
|
|
40
40
|
| --- | --- |
|
|
41
|
-
| Agent lifecycle | `agent_started`, `agent_finished` |
|
|
41
|
+
| Agent lifecycle | `agent_started`, `agent_suspended`, `agent_resumed`, `agent_denied`, `agent_finished` |
|
|
42
42
|
| Turns | `turn_started`, `turn_finished` |
|
|
43
43
|
| Provider turns | `provider_turn_started`, `provider_turn_finished` |
|
|
44
44
|
| Assistant messages | `message_started`, `message_delta`, `message_finished` |
|
|
45
45
|
| Tool execution | `tool_execution_started`, `tool_execution_progress`, `tool_execution_finished`, `tool_execution_error`, `tool_execution_blocked` |
|
|
46
|
+
| Guardrails | `guardrail_decision` |
|
|
46
47
|
| Queue/subscribers | `queue_updated`, `event_subscriber_overflow` |
|
|
47
48
|
| Compaction | `compaction_started`, `compaction_finished` |
|
|
48
49
|
| Retry | `retry_scheduled` |
|
|
@@ -59,6 +60,9 @@ Agent / turn / message events:
|
|
|
59
60
|
| --- | --- |
|
|
60
61
|
| `agent_started` | `sessionId`, `runId` |
|
|
61
62
|
| `agent_finished` | `sessionId`, `runId`, `usage?: Usage` (aggregate of all usage-bearing provider turns) |
|
|
63
|
+
| `agent_suspended` | `sessionId`, `runId`, redacted `interruption`, checkpoint `version`; no tool side effect has started. |
|
|
64
|
+
| `agent_resumed` | `sessionId`, `runId`, checkpoint `version`. |
|
|
65
|
+
| `agent_denied` | `sessionId`, `runId`, redacted `interruption`, checkpoint `version`; no tool side effect runs. |
|
|
62
66
|
| `turn_started` / `turn_finished` | `sessionId`, `runId`, `turn: number` |
|
|
63
67
|
| `message_started` / `message_finished` | `sessionId`, `runId`, `message: Message` |
|
|
64
68
|
| `message_delta` | `sessionId`, `runId`, `content: ContentBlock` (`tool_call_delta` fragments may appear here for live UI streaming; stored messages use final `tool_call` blocks) |
|
|
@@ -75,6 +79,14 @@ Tool execution events:
|
|
|
75
79
|
| `tool_execution_error` | `sessionId`, `runId`, `call: ToolCallContent`, `error: ErrorInfo`, `metadata: ToolExecutionMetadata` |
|
|
76
80
|
| `tool_execution_blocked` | `sessionId`, `runId`, `toolCallId`, `name`, `reason: string`, `error: ErrorInfo`, `metadata: ToolExecutionMetadata` |
|
|
77
81
|
|
|
82
|
+
Guardrail events:
|
|
83
|
+
|
|
84
|
+
| Variant | Fields |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| `guardrail_decision` | `sessionId`, `runId`, optional `toolCallId`/`toolName`, and redacted bounded `record: GuardrailRecord` (`guardrail`, stage, action, reason, metadata). |
|
|
87
|
+
|
|
88
|
+
Guardrails emit their decision before a terminal run error or blocked tool result. Provider-output checks buffer assistant content, and tool-output checks discard blocked raw results before event/ledger/transcript exposure; see [Guardrails](guardrails.md).
|
|
89
|
+
|
|
78
90
|
Queue / subscriber / compaction / retry / provider events:
|
|
79
91
|
|
|
80
92
|
| Variant | Fields |
|
package/docs/agent-loops.md
CHANGED
|
@@ -57,7 +57,7 @@ await session.run(input, {
|
|
|
57
57
|
parser: hostParser, // optional; default treats assistant text as the value
|
|
58
58
|
repairer: hostRepairer, // optional; default stringifies validation.errors[].message
|
|
59
59
|
maxRevisions: 3, // optional; default 3
|
|
60
|
-
toolCalls: "bounded", // optional; default "disabled"; uses
|
|
60
|
+
toolCalls: "bounded", // optional; default "disabled"; uses limits.maxToolRounds
|
|
61
61
|
},
|
|
62
62
|
});
|
|
63
63
|
|
|
@@ -80,7 +80,7 @@ type AgentLoopOptions =
|
|
|
80
80
|
readonly parser?: ArtifactParser<unknown>;
|
|
81
81
|
readonly repairer?: ArtifactRepairer<unknown>;
|
|
82
82
|
readonly maxRevisions?: number;
|
|
83
|
-
/** Default "disabled". "bounded" dispatches sequentially up to
|
|
83
|
+
/** Default "disabled". "bounded" dispatches sequentially up to limits.maxToolRounds. */
|
|
84
84
|
readonly toolCalls?: "disabled" | "bounded";
|
|
85
85
|
};
|
|
86
86
|
```
|
|
@@ -109,6 +109,10 @@ Host callback contracts (all generic over host `T`):
|
|
|
109
109
|
| `appendMessage(message)` | Appends to the store under the run (redacted). |
|
|
110
110
|
| `emit(event)` | Emits a redacted `AgentEvent`. |
|
|
111
111
|
|
|
112
|
+
## Durable runs
|
|
113
|
+
|
|
114
|
+
`RunOptions.runState` supports only built-in loop options (`single-shot` and `generate-validate-revise`). A custom `AgentLoopStrategy` has arbitrary in-memory cursor state, so durable configuration rejects it before provider work. Built-in suspension occurs only before an input provider call or immediately before a tool side effect; completed provider turns remain in `SessionStore` history and are not repeated after `resumeAgentRun()`.
|
|
115
|
+
|
|
112
116
|
## Outputs / response / events
|
|
113
117
|
|
|
114
118
|
`AgentLoopStrategy.run(ctx)` returns `Promise<Usage | undefined>` as a fallback for custom loops. Core runtime independently accumulates every usage-bearing provider turn in O(turns), persists scoped turn/run rows, and emits `agent_finished` with the aggregate.
|
|
@@ -202,7 +206,7 @@ await session.run(input, { loop: twoShotLoop });
|
|
|
202
206
|
- `{ strategy: "single-shot" }` resolves to the exported `singleShotLoop`; `{ strategy: "generate-validate-revise", ... }` is mapped by `resolveLoop()` to `generateValidateReviseLoop(opts)`. An unknown `strategy` throws before the first turn. Passing an `AgentLoopStrategy` instance bypasses the options form entirely (custom-loop escape hatch).
|
|
203
207
|
- The loop is resolved once per run inside `RuntimeAgentSession.run()`, after the usual setup (provider/skills/tools resolution, history rebuild, model-change entry, input append, auto-compaction). The runtime's outer try/catch/finally, run-exclusivity, abort bridging, and subscriber close remain in place around `loop.run(ctx)`.
|
|
204
208
|
- `LoopContext.assemble(nextInput, toolResults?)` accepts an optional tool-result accumulator so `singleShotLoop` can pass its loop-local results. Bounded artifact tools append results directly to shared history, then assemble the next turn with empty new input; no second transcript path exists.
|
|
205
|
-
- `maxToolRounds` bounds both `singleShotLoop` and opt-in bounded artifact tool rounds across the whole run. Artifact mode always dispatches sequentially, regardless of `toolConcurrency`; all dispatches still use existing registry/filter/permission/validator/middleware/redactor/ledger guards.
|
|
209
|
+
- `limits.maxToolRounds` bounds both `singleShotLoop` and opt-in bounded artifact tool rounds across the whole run. Artifact mode always dispatches sequentially, regardless of `toolConcurrency`; all dispatches still use existing registry/filter/permission/validator/middleware/redactor/ledger guards. Deprecated `maxToolRounds` only narrows this limit.
|
|
206
210
|
- `maxRevisions` (default 3) counts only failed call-free artifact candidates. Bounded artifact runs make at most `1 + maxRevisions + maxToolRounds` provider turns. A tool-round limit is terminal and returns last usage after `artifact_failed`; it does not throw.
|
|
207
211
|
- A revision cycle appends one assistant draft and one repair user message per revision to the session store, so store entries reflect every attempted draft. The original user input is stored once by the runtime and pushed into loop history once on the first turn. Repair messages are assembled as the next provider `nextInput` and only pushed into live history after that revision request has been generated, so the model never receives a duplicated repair instruction.
|
|
208
212
|
|
|
@@ -215,6 +219,10 @@ await session.run(input, { loop: twoShotLoop });
|
|
|
215
219
|
- The loop is a plain object/factory; no class hierarchy, no background work, no extra dependencies. `LoopContext` is a single object literal of bound arrows built once per run.
|
|
216
220
|
- The Synapta-free boundary is guarded by tests: `src/` imports no `synapta*` package, and the `Artifact*`/`AgentLoop*`/`LoopContext` contracts contain no `workflow`/`node`/`step` field names. Hosts supply their own schema; no host domain type is imported by `src/`.
|
|
217
221
|
|
|
222
|
+
## Guardrails
|
|
223
|
+
|
|
224
|
+
Built-in loops and custom loops that use `LoopContext.generate()` / `LoopContext.dispatchToolCall()` inherit runtime guardrails. Provider output is checked before a loop appends assistant content; tool stages remain in shared dispatch. Do not call providers or `ToolDefinition.execute()` directly if guardrail enforcement is required; see [Guardrails](guardrails.md).
|
|
225
|
+
|
|
218
226
|
## Related APIs
|
|
219
227
|
- [Agent/session runtime](agent-session-runtime.md): `RuntimeAgentSession.run()` builds the `LoopContext` and delegates to the resolved loop.
|
|
220
228
|
- [Agent events](agent-events.md): the `artifact_*` event variants and ordering emitted by `generateValidateReviseLoop`.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
The agent/session runtime adds the minimal shared SDK surface for running provider turns, dispatching complete host-owned tool calls, and subscribing to session events:
|
|
6
6
|
|
|
7
7
|
- `createAgent(config)`
|
|
8
|
+
- `createSecureAgent(options)` for opt-in fail-closed composition
|
|
8
9
|
- `createAgentSession(config)`
|
|
9
10
|
- `agent.createSession(config)`
|
|
10
11
|
- `session.run(input, options)` → `AgentRunResult`
|
|
@@ -17,6 +18,8 @@ The agent/session runtime adds the minimal shared SDK surface for running provid
|
|
|
17
18
|
- `session.checkout(leafId?)`
|
|
18
19
|
- `session.fork(options?)`
|
|
19
20
|
- `session.clone(options?)`
|
|
21
|
+
- `resumeAgentRun(agent, ref, decision, options)`
|
|
22
|
+
- `createAgentRunLifecycle({ checkpoints, resolveAgent })` for host-selected remote status/resume adapters
|
|
20
23
|
|
|
21
24
|
The runtime streams provider text/tool-call content into `AgentEvent` values. Complete `tool_call` events are dispatched through the active host `ToolRegistry`, then returned as tool-result messages on the next provider turn. When a store is supplied, user, assistant, tool-result, and model-change entries are appended under the current branch leaf. Abort propagation and run exclusivity use native `AbortController`.
|
|
22
25
|
|
|
@@ -43,7 +46,9 @@ string | Message | readonly Message[]
|
|
|
43
46
|
|
|
44
47
|
`AgentSessionConfig.store` overrides `AgentConfig.store`; otherwise the session gets a private memory store. `AgentSessionConfig.leafId` selects the branch leaf to resume from.
|
|
45
48
|
|
|
46
|
-
`
|
|
49
|
+
`AgentConfig.limits` sets run ceilings; `RunOptions.limits` may only narrow configured agent values. Limits cover turns, provider attempts, tool rounds/calls, wall time, request/response bytes, tokens, and optional single-currency cost. A breach emits one `run_limit_exceeded` event and throws `AgentRunError` with `result.limit`; see [Runs and usage ledger](runs-and-usage.md#run-limits).
|
|
50
|
+
|
|
51
|
+
`RunOptions.model` can override the request model for a run. Model overrides append a `model_change` entry. `AgentConfig.inputLayout` selects the default input assembly layout (`"legacy"` by default, or opt-in `"cache_aware"`); `RunOptions.inputLayout` wins for one run. `AgentConfig.providerOptions`/`RunOptions.providerOptions` supply generic provider request options; `timeoutMs`, `maxRetries`, and `maxRetryDelayMs` are deprecated inert provider-level hints in first-party providers. Use `RunOptions.signal`/host abort controllers for timeouts and `AgentConfig.retry`/`RunOptions.retry` for retry. `AgentConfig.providerRequestPolicies`/`RunOptions.providerRequestPolicies` run before `AIProvider.generate()` and before `provider_request` middleware. `AgentConfig.systemPrompt` and `RunOptions.systemPrompt` add explicit layered system prompt contributions; `RunOptions.systemPrompt: false` disables configured prompt layers for that run while keeping `AgentConfig.instructions` as the base path. `RunOptions.compaction` can enable auto-compaction for that run or use `false` to disable configured auto-compaction. `RunOptions.retry` can enable provider-turn retry for that run or use `false` to disable configured retry. `RunOptions.metadata` is merged with agent/session metadata for assembly, provider requests, and tool contexts. Deprecated `RunOptions.maxToolRounds` narrows `limits.maxToolRounds`. `RunOptions.signal` is bridged into the per-run abort signal passed to assembly, providers, tools, auto-compaction, and retry backoff.
|
|
47
52
|
|
|
48
53
|
## Outputs / response / events
|
|
49
54
|
|
|
@@ -160,6 +165,33 @@ await agent.createSession().run("Hi", { model: overrideModel });
|
|
|
160
165
|
- Runtime events contain messages/content only; do not put secrets in prompts, metadata, provider events, session entries, or docs examples.
|
|
161
166
|
- The event broadcaster is in-memory, live-only, and bounded per subscriber by `SubscribeOptions`. It adds no dependency, timer, filesystem/network discovery, worker, or durable queue.
|
|
162
167
|
|
|
168
|
+
## Durable interruption
|
|
169
|
+
|
|
170
|
+
Set `runState` with a host-owned `CheckpointStore`, stable `definitionRevision`, and `interruptBeforeTool: true` to suspend at a persisted pre-side-effect boundary. A suspended result has `status: "suspended"`, a redacted `interruption`, and `runState.version`; it releases session resources before returning.
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const result = await session.run("Publish draft", {
|
|
174
|
+
runState: { checkpoints, definitionRevision: "2026-07-20.1", interruptBeforeTool: true },
|
|
175
|
+
});
|
|
176
|
+
if (result.status === "suspended") {
|
|
177
|
+
await resumeAgentRun(agent, { runId: result.runId, sessionId: result.sessionId }, {
|
|
178
|
+
decision: "approve", expectedVersion: result.runState!.version!,
|
|
179
|
+
}, { checkpoints, definitionRevision: "2026-07-20.1" });
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Resume requires exact checkpoint ownership, version, agent fingerprint, and revision. Prism CAS-claims approval before work, rechecks normal guardrail/permission/validation/limit paths, and marks a pending tool dispatched before its side effect. `createAgentRunLifecycle()` wraps the same core path for server/MCP hosts: adapters pass only authorized ownership, status returns only `{ state, version }`, and `resolveAgent()` supplies current agent/revision. Remote restart requires both checkpoint and session stores to be durable. A crash after that mark is ambiguous and is never replayed automatically; use host tool idempotency keyed by `runId`/`toolCallId` or resolve it manually. Checkpoints contain bounded redacted state plus session/leaf references, never provider objects, callbacks, signals, credentials, or raw secrets. Only built-in loop options are durable; custom `AgentLoopStrategy` rejects before provider work.
|
|
184
|
+
|
|
185
|
+
## Secure composition
|
|
186
|
+
|
|
187
|
+
`createSecureAgent()` is optional; `createAgent()` remains explicit and backward-compatible. Secure composition requires an ID, non-empty definition revision, exact non-empty ownership, redactor, permission and trust policies, finite explicit limits, a host `ToolArgumentValidator`, non-empty schema for every tool, and checkpoints. It builds a duplicate-error registry, rejects missing schemas, always enables durable pre-tool interruption, and reuses normal provider/request policies without discovery or background work.
|
|
188
|
+
|
|
189
|
+
Per-run options may narrow `limits` and append `guardrails`; they cannot replace secure ownership, redaction, validator, or durable checkpoint policy. Every active tool is trust-checked then permission-checked before validation and its side effect. See [`examples/secure-agent.ts`](../examples/secure-agent.ts).
|
|
190
|
+
|
|
191
|
+
## Guardrails
|
|
192
|
+
|
|
193
|
+
`AgentConfig.guardrails` applies typed input, output, tool-input, and tool-output checks to every run. `RunOptions.guardrails` appends checks for one run. Input checks run before session append; configured output checks buffer provider content until allowed, so blocked content is never emitted or stored. See [Guardrails](guardrails.md).
|
|
194
|
+
|
|
163
195
|
## Related APIs
|
|
164
196
|
|
|
165
197
|
- [Public contracts](public-contracts.md): `Agent`, `AgentSession`, `RunOptions`, and `AgentEvent` contracts.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Guardrails
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
Guardrails are typed, fail-closed checks at input, completed provider output, tool input, and raw tool output boundaries. `session.run()` evaluates configured stages through one core runner; `dispatchToolCall()` uses same runner for direct, MCP-server, and workflow tool calls.
|
|
6
|
+
|
|
7
|
+
## When to use it
|
|
8
|
+
|
|
9
|
+
Use guardrails to block unsafe prompts, model responses, tool arguments, or tool results before their next boundary. Use a redactor for known secrets. Do not treat guardrails as a sandbox, secret detector, permission policy, or validation replacement.
|
|
10
|
+
|
|
11
|
+
## Inputs / request
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import type { Guardrail, Guardrails } from "@arnilo/prism";
|
|
15
|
+
|
|
16
|
+
const pii: Guardrail<"input"> = {
|
|
17
|
+
name: "pii",
|
|
18
|
+
stage: "input",
|
|
19
|
+
evaluate: ({ value }) => JSON.stringify(value).includes("SSN")
|
|
20
|
+
? { action: "tripwire", reason: "pii" }
|
|
21
|
+
: { action: "allow" },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const guardrails: Guardrails = { input: [pii], maxConcurrency: 1 };
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Set `AgentConfig.guardrails` for every session run or `RunOptions.guardrails` to append checks for one run. `DispatchToolCallOptions.guardrails`, workflow `RunWorkflowOptions.guardrails`, and MCP server `CreatePrismMcpServerOptions.guardrails` apply tool stages to direct calls. A stage has `Guardrail<"input" | "output" | "tool_input" | "tool_output">`, a name, optional revision, and `evaluate(context)` result.
|
|
28
|
+
|
|
29
|
+
Decisions are `allow`, `block`, `tripwire`, or `interrupt`. Evaluation defaults to declaration-order sequential. `maxConcurrency` may be 1–16; records are emitted in declaration order. Thrown or malformed decisions become a fail-closed tripwire. Decision reasons are capped at 4 KiB and metadata at 16 KiB after JSON normalization and optional redaction.
|
|
30
|
+
|
|
31
|
+
## Outputs / response / events
|
|
32
|
+
|
|
33
|
+
Every evaluated guard produces a redacted `guardrail_decision` `AgentEvent` with a bounded `GuardrailRecord`. An input or output terminal decision rejects the run with `GuardrailError`; `tripwire` stops remaining evaluation. A tool-input or tool-output `block` returns a redacted blocked `ToolResult`; a `tripwire` rejects the enclosing run. `interrupt` is reserved for durable runs and currently fails closed with `ERR_PRISM_GUARDRAIL_INTERRUPT_UNAVAILABLE`.
|
|
34
|
+
|
|
35
|
+
Ordering is fixed:
|
|
36
|
+
|
|
37
|
+
1. input before session append, compaction, or provider work;
|
|
38
|
+
2. provider output is privately collected, then output checks run before any assistant message event or persistence;
|
|
39
|
+
3. tool input runs after tool-call middleware normalization and before lookup, permission, validation, execution policy, and side effect;
|
|
40
|
+
4. tool output runs after the side effect but before redaction, tool events, ledger rows, transcript append, or next turn.
|
|
41
|
+
|
|
42
|
+
With no output guardrails, provider streaming retains existing behavior. With output guardrails, message events are buffered until the completed provider turn is allowed.
|
|
43
|
+
|
|
44
|
+
## Request/response example
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"event": {
|
|
49
|
+
"type": "guardrail_decision",
|
|
50
|
+
"record": { "guardrail": "pii", "stage": "input", "action": "tripwire", "reason": "pii" }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Implementation example
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const agent = createAgent({ model, provider, guardrails: { input: [pii], output: [responseGuard] } });
|
|
59
|
+
await agent.createSession().run("Draft reply", { guardrails: { toolInput: [commandGuard] } });
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Extension and configuration notes
|
|
63
|
+
|
|
64
|
+
Guardrails are callbacks supplied by the host. Prism does not discover, load, retry, or persist callback code. `createSecureAgent()` keeps configured guardrails and only appends run-level checks; it never lets a run remove secure defaults. Custom loops receive guarded `LoopContext.generate()` and `LoopContext.dispatchToolCall()`; host code that directly calls a provider or `ToolDefinition.execute()` is outside the runtime boundary.
|
|
65
|
+
|
|
66
|
+
## Security and performance notes
|
|
67
|
+
|
|
68
|
+
Output buffering prevents blocked provider content from reaching subscribers, session entries, ledgers, parsers, delegation, or tools. Tool-output checks receive raw results but Prism discards blocked raw output before event, ledger, transcript, or MCP exposure. Redaction replaces exact known values only; it is not general secret detection. Parallel checks receive an abort signal, but callback code must honor it to stop in-flight work.
|
|
69
|
+
|
|
70
|
+
## Related APIs
|
|
71
|
+
|
|
72
|
+
- [Agent/session runtime](agent-session-runtime.md)
|
|
73
|
+
- [Tools](tools.md)
|
|
74
|
+
- [Agent events](agent-events.md)
|
|
75
|
+
- [Host security](host-security.md)
|
package/docs/host-security.md
CHANGED
|
@@ -25,10 +25,12 @@ Start from explicit host inputs. Do not let runtime code discover security state
|
|
|
25
25
|
| Permission decisions | allow/deny rules or approval UI result | `createStaticPermissionPolicy`, `assertPermission()` |
|
|
26
26
|
| Tool allow-list | active tools for this agent/session/run | `createToolRegistry`, `filterTools()`, `dispatchToolCall()` |
|
|
27
27
|
| Tool argument rules | host validator | `AgentConfig.validator`, `RunOptions.validate`, `ToolValidator` |
|
|
28
|
+
| Guardrail decisions | host callback allow/block/tripwire policy | `Guardrails`, `Guardrail`, `GuardrailError` |
|
|
28
29
|
| Coding execution policy | path/command approval adapter | `ExecutionPolicy`, `@arnilo/prism-coding-security` |
|
|
29
30
|
| Remote media policy | public/default pinned DNS or explicit trusted transport | `SsrfPolicy`, `resolveMediaContentBlock()` |
|
|
30
31
|
| Durable history | host database adapter | `SessionStore`, `assertSessionStoreConforms()` |
|
|
31
32
|
| Durable audit | host ledger adapter | `RunLedger`, `redactRunLedgerRecord()` |
|
|
33
|
+
| Durable interruption | host checkpoint + session stores, exact ownership | `RunOptions.runState`, `resumeAgentRun()`, `createAgentRunLifecycle()`, `createSecureAgent()` |
|
|
32
34
|
| Extensions | explicit package imports only | `createExtensionKernel`, `ExtensionAPI` |
|
|
33
35
|
| Remote agent/workflow API | host authentication + ownership mapping | `@arnilo/prism-server`, `createPrismHandler()` |
|
|
34
36
|
| MCP server exposure | host MCP auth + selected capability list | `createPrismMcpServer()`, `createPrismMcpWebHandler()` |
|
|
@@ -41,7 +43,9 @@ Security controls fail closed before side effects when wired at the guarded edge
|
|
|
41
43
|
- permission denial blocks extension setup, resource loading, and tool execution
|
|
42
44
|
- unknown or denied tools emit `tool_execution_blocked`
|
|
43
45
|
- validator failures emit `tool_execution_blocked` with `validation_failed`
|
|
44
|
-
- configured
|
|
46
|
+
- configured guardrails fail closed; output stages buffer blocked provider/tool content before events, ledgers, session entries, or MCP responses
|
|
47
|
+
- configured redactors scrub provider requests, agent events, session entries, ledger records, tool errors, extension errors, injector context, and durable run checkpoints
|
|
48
|
+
- durable resume requires host-derived exact ownership and checkpoint version; `createAgentRunLifecycle()` exposes only public state through explicitly selected server/MCP capabilities; never accept ownership or resume input from an approval body
|
|
45
49
|
|
|
46
50
|
These checks are explicit function calls during load, assembly, dispatch, append, or run handling. Prism adds no background watchers, filesystem scanners, network probes, credential polling, or automatic extension discovery.
|
|
47
51
|
|
|
@@ -104,7 +108,7 @@ const tools = createToolRegistry(filterTools([readNotes], { allow: ["notes/read"
|
|
|
104
108
|
void { apiKey, redactor, permission, trust, tools, validate };
|
|
105
109
|
```
|
|
106
110
|
|
|
107
|
-
Wire those values where they matter: provider adapters receive the resolved credential, agents/runs receive `redactor`, tool dispatch receives `permission
|
|
111
|
+
Wire those values where they matter: provider adapters receive the resolved credential, agents/runs receive `redactor`, tool dispatch receives `trust`, `permission`, and `validate`, resource/extension loaders receive `trust` and `permission`, and durable adapters receive already-redacted entries/records. `createSecureAgent()` is an opt-in shortcut that requires these agent/tool seams, strict schemas, finite limits, exact ownership, and durable approval before every tool side effect; low-level `createAgent()` stays explicit.
|
|
108
112
|
|
|
109
113
|
## Extension and configuration notes
|
|
110
114
|
|