@arnilo/prism 0.3.1 → 0.4.0
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 +58 -0
- package/README.md +34 -57
- package/dist/agent-definitions.js +4 -1
- package/dist/agent-run-lifecycle.js +4 -0
- package/dist/agent-run-state.d.ts +4 -0
- package/dist/agent-run-state.js +18 -5
- package/dist/agent-session/session.d.ts +7 -0
- package/dist/agent-session/session.js +59 -2
- package/dist/cli-dev.d.ts +29 -0
- package/dist/cli-dev.js +52 -0
- package/dist/cli-init.d.ts +17 -2
- package/dist/cli-init.js +194 -21
- package/dist/cli-runner.d.ts +5 -1
- package/dist/cli-runner.js +12 -1
- package/dist/contracts-core/agent.d.ts +29 -2
- package/dist/contracts-protocol.d.ts +18 -0
- package/dist/contracts-run-state.d.ts +1 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/input.d.ts +8 -0
- package/dist/input.js +4 -0
- package/dist/rpc.d.ts +4 -1
- package/dist/rpc.js +5 -1
- package/dist/testing/persistence-schema.d.ts +1 -1
- package/dist/testing/persistence-schema.js +32 -28
- package/dist/testing/tool-conformance.d.ts +25 -0
- package/dist/testing/tool-conformance.js +128 -1
- package/dist/tool-search.d.ts +76 -0
- package/dist/tool-search.js +199 -0
- package/docs/0.1.0-readiness.md +2 -2
- package/docs/acp-agent.md +1 -1
- package/docs/agent-definitions.md +1 -1
- package/docs/antigravity-agent.md +1 -1
- package/docs/browser-automation.md +5 -5
- package/docs/caveman.md +2 -2
- package/docs/cli-rpc.md +26 -3
- package/docs/coding-agent-tools.md +7 -1
- package/docs/coding-security.md +1 -1
- package/docs/coding-tools.md +82 -0
- package/docs/compaction-and-retry.md +5 -2
- package/docs/compaction-llm.md +4 -4
- package/docs/compaction-observational-memory.md +49 -2
- package/docs/context-and-skills.md +2 -0
- package/docs/core.md +85 -0
- package/docs/credential-storage.md +1 -1
- package/docs/database-persistence.md +4 -0
- package/docs/dev-inspector.md +103 -0
- package/docs/diagrams.md +247 -0
- package/docs/documents.md +213 -0
- package/docs/evaluations.md +35 -1
- package/docs/extension-authoring.md +42 -0
- package/docs/graft.md +3 -3
- package/docs/guardrails.md +1 -1
- package/docs/host-security.md +4 -3
- package/docs/impeccable.md +2 -2
- package/docs/index.md +34 -23
- package/docs/mcp-tools.md +1 -1
- package/docs/migrate-to-0.4.md +312 -0
- package/docs/migration.md +22 -0
- package/docs/model-routing.md +1 -1
- package/docs/multi-agent-patterns.md +177 -0
- package/docs/multimodal-content.md +1 -1
- package/docs/obscura.md +10 -10
- package/docs/openapi-tools.md +1 -1
- package/docs/performance.md +23 -3
- package/docs/persistence-credentials-multimodality-primitives.md +1 -1
- package/docs/policy-and-audit.md +1 -1
- package/docs/ponytail.md +2 -2
- package/docs/prompt-registry.md +106 -0
- package/docs/provider-caching.md +32 -32
- package/docs/provider-conformance.md +1 -1
- package/docs/provider-packages.md +19 -19
- package/docs/provider-primitives.md +4 -4
- package/docs/providers/ai-sdk.md +3 -3
- package/docs/providers/alibaba.md +5 -5
- package/docs/providers/anthropic.md +6 -6
- package/docs/providers/azure.md +3 -3
- package/docs/providers/bedrock.md +3 -3
- package/docs/providers/clinepass.md +3 -3
- package/docs/providers/deepseek.md +3 -3
- package/docs/providers/google.md +4 -4
- package/docs/providers/kimi.md +3 -3
- package/docs/providers/neuralwatt.md +8 -8
- package/docs/providers/ollama.md +3 -3
- package/docs/providers/openai-compatible.md +1 -1
- package/docs/providers/openai.md +5 -5
- package/docs/providers/opencode-go.md +4 -4
- package/docs/providers/openrouter.md +3 -3
- package/docs/providers/vertex.md +5 -5
- package/docs/providers/xai.md +3 -3
- package/docs/providers/zai.md +3 -3
- package/docs/public-contracts.md +1 -1
- package/docs/rag.md +5 -5
- package/docs/release-and-install.md +116 -50
- package/docs/runs-and-usage.md +14 -1
- package/docs/server.md +90 -1
- package/docs/sheets.md +229 -0
- package/docs/supervisors.md +9 -1
- package/docs/thinking-and-reasoning.md +10 -10
- package/docs/tool-conformance.md +27 -2
- package/docs/tools.md +29 -2
- package/docs/web-tools.md +2 -2
- package/docs/wiki.md +24 -10
- package/docs/workflow-orchestration-primitives.md +24 -0
- package/docs/workflows.md +102 -8
- package/docs/working-and-semantic-memory.md +53 -5
- package/package.json +10 -30
- package/templates/README.md +23 -0
- package/templates/deep-research/README.md.tmpl +47 -0
- package/templates/deep-research/env.example.tmpl +12 -0
- package/templates/deep-research/gitignore.tmpl +7 -0
- package/templates/deep-research/manifest.json +12 -0
- package/templates/deep-research/package.json.tmpl +23 -0
- package/templates/deep-research/src/agent.ts.tmpl +81 -0
- package/templates/deep-research/src/index.ts.tmpl +53 -0
- package/templates/deep-research/src/tests/research.test.ts.tmpl +114 -0
- package/templates/deep-research/src/tools.ts.tmpl +86 -0
- package/templates/deep-research/src/types.ts.tmpl +45 -0
- package/templates/deep-research/src/workflow.ts.tmpl +156 -0
- package/templates/deep-research/tsconfig.json.tmpl +15 -0
- package/templates/init/manifest.json +5 -0
- package/templates/init/package.json.tmpl +2 -1
- package/templates/init/providers.json +16 -16
|
@@ -4,7 +4,7 @@ import { createHash } from "node:crypto";
|
|
|
4
4
|
// this module defines the shared table/index/pagination/migration expectations
|
|
5
5
|
// adapter authors implement and test against before shipping dialect-specific DDL.
|
|
6
6
|
/** Current shared persistence schema version for production database adapters. */
|
|
7
|
-
export const PERSISTENCE_SCHEMA_VERSION =
|
|
7
|
+
export const PERSISTENCE_SCHEMA_VERSION = 9;
|
|
8
8
|
/** Guidance adapters must follow: values are bound parameters, never interpolated. */
|
|
9
9
|
export const PARAMETERIZED_QUERY_GUIDANCE = "Bind every user-supplied value (session ids, idempotency keys, tenant ids, timestamps, JSON payloads) as a query parameter. Quote/validate schema and table identifiers only; never interpolate untrusted strings into SQL text.";
|
|
10
10
|
const TENANT_COLUMNS = [
|
|
@@ -157,6 +157,7 @@ export function createPersistenceSchemaModel() {
|
|
|
157
157
|
{ name: "error", type: "json", nullable: true },
|
|
158
158
|
...TENANT_COLUMNS,
|
|
159
159
|
{ name: "metadata", type: "json", nullable: true },
|
|
160
|
+
{ name: "prompt_version", type: "json", nullable: true },
|
|
160
161
|
],
|
|
161
162
|
foreignKeys: [{ columns: ["session_id"], referencesTable: "prism_sessions", referencesColumns: ["id"] }],
|
|
162
163
|
},
|
|
@@ -502,33 +503,35 @@ function migrationStep(version, name, description) {
|
|
|
502
503
|
? { indexes: ["prism_agent_events_owner_timestamp_sequence_idx"] }
|
|
503
504
|
: version === 8
|
|
504
505
|
? { table: "prism_sessions", columns: ["version"] }
|
|
505
|
-
: version ===
|
|
506
|
-
? {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
: version ===
|
|
513
|
-
? {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
"
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
506
|
+
: version === 9
|
|
507
|
+
? { table: "prism_runs", columns: ["prompt_version"] }
|
|
508
|
+
: version === 6
|
|
509
|
+
? {
|
|
510
|
+
tables: ["prism_agent_event_streams"],
|
|
511
|
+
indexes: ["prism_agent_events_run_sequence_idx"],
|
|
512
|
+
}
|
|
513
|
+
: version === 2
|
|
514
|
+
? { table: "prism_usage", columns: ["scope", "turn", "attempt"], indexes: ["prism_usage_session_scope_recorded_idx"] }
|
|
515
|
+
: version === 3
|
|
516
|
+
? {
|
|
517
|
+
tables: ["prism_run_feedback"],
|
|
518
|
+
indexes: model.indexes.filter((index) => index.name.startsWith("prism_run_feedback_")).map((index) => index.name),
|
|
519
|
+
}
|
|
520
|
+
: version === 4
|
|
521
|
+
? // Adapter-local FTS objects (SQLite FTS5 / Postgres tsvector) map to this canonical name.
|
|
522
|
+
{ search: ["prism_session_search"], indexes: ["prism_sessions_updated_id_idx"] }
|
|
523
|
+
: version === 5
|
|
524
|
+
? {
|
|
525
|
+
tables: ["prism_legal_holds", "prism_tenant_quotas"],
|
|
526
|
+
indexes: [
|
|
527
|
+
"prism_legal_holds_owner_resource_idx",
|
|
528
|
+
"prism_legal_holds_created_id_idx",
|
|
529
|
+
"prism_tenant_quotas_owner_kind_idx",
|
|
530
|
+
],
|
|
531
|
+
}
|
|
532
|
+
: (() => {
|
|
533
|
+
throw new Error(`Unknown migration version ${version}`);
|
|
534
|
+
})();
|
|
532
535
|
return {
|
|
533
536
|
version,
|
|
534
537
|
name,
|
|
@@ -550,6 +553,7 @@ export function createPersistenceMigrationContract() {
|
|
|
550
553
|
migrationStep(6, "006_agent_event_source", "Add transactional per-run event counters and unique durable event sequencing."),
|
|
551
554
|
migrationStep(7, "007_agent_event_retention_index", "Add an exact-owner durable-event retention cleanup index."),
|
|
552
555
|
migrationStep(8, "008_session_version", "Add a NOT NULL DEFAULT 0 version column to prism_sessions for appendSession metadata CAS."),
|
|
556
|
+
migrationStep(9, "009_run_prompt_version", "Add a nullable prompt_version JSON provenance ref column to prism_runs."),
|
|
553
557
|
],
|
|
554
558
|
lockGuidance: "Acquire a dialect-specific migration lock before applying steps (PostgreSQL advisory lock; SQLite exclusive transaction). Only one process should migrate at a time.",
|
|
555
559
|
leastPrivilegeGuidance: "Run migrations with a DDL-capable role; use a separate least-privilege runtime role limited to INSERT/SELECT/UPDATE on adapter tables. Never grant migration credentials to the agent runtime.",
|
|
@@ -37,3 +37,28 @@ export declare function dispatchAndCollect(probe: ToolDispatchProbeOptions): Pro
|
|
|
37
37
|
result: ToolResult;
|
|
38
38
|
events: AgentEvent[];
|
|
39
39
|
}>;
|
|
40
|
+
export interface ToolDisclosureConformanceOptions {
|
|
41
|
+
/** Host-active tool definitions; may include schema-bearing and oversized-description tools. */
|
|
42
|
+
readonly tools: readonly ToolDefinition[];
|
|
43
|
+
/** Host allow/deny bounds applied before disclosure (same input the runtime narrows). */
|
|
44
|
+
readonly filter?: ToolFilterInput;
|
|
45
|
+
/** Search options under test (topK). */
|
|
46
|
+
readonly search?: {
|
|
47
|
+
readonly topK?: number;
|
|
48
|
+
};
|
|
49
|
+
/** Turn text used for the relevance query. Defaults to a zero-match probe. */
|
|
50
|
+
readonly input?: string;
|
|
51
|
+
/** Secret values that must never appear in model-facing search output. */
|
|
52
|
+
readonly secrets?: readonly (string | undefined)[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Assert the tool-disclosure contract (plan 041) against the same narrowing the
|
|
56
|
+
* runtime applies: search mode only narrows (disclosed set is a subset of the
|
|
57
|
+
* allow/deny-filtered input, never wider, never zero, deterministic order); a
|
|
58
|
+
* denied tool is never described; the generated `search_tools` tool is always
|
|
59
|
+
* kept and its output is inert — names plus byte-truncated descriptions only,
|
|
60
|
+
* no JSON structure, no secret values — and activation stays disclosed beside
|
|
61
|
+
* the turn top-k next turn. Fails closed: an index over the hard cap discloses
|
|
62
|
+
* the full eligible list. Throws on the first violation.
|
|
63
|
+
*/
|
|
64
|
+
export declare function assertToolDisclosureConforms(options: ToolDisclosureConformanceOptions): void;
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
// Error; no test runner, no network. Execution is observed via the
|
|
8
8
|
// tool_execution_started / tool_execution_blocked events the runtime emits,
|
|
9
9
|
// not by mutating the caller's tool.
|
|
10
|
-
import {
|
|
10
|
+
import { createActiveToolSet, createSearchToolsTool, createToolSearchState, HARD_MAX_TOOLS_INDEX, SEARCH_TOOLS_TOOL_NAME, selectDisclosedTools, } from "../tool-search.js";
|
|
11
|
+
import { dispatchToolCall, filterTools } from "../tools.js";
|
|
11
12
|
const denyAllPermission = { check: () => ({ allowed: false, reason: "denied" }) };
|
|
12
13
|
const alwaysInvalidValidator = () => "invalid";
|
|
13
14
|
/**
|
|
@@ -98,7 +99,133 @@ export async function dispatchAndCollect(probe) {
|
|
|
98
99
|
});
|
|
99
100
|
return { result, events };
|
|
100
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Assert the tool-disclosure contract (plan 041) against the same narrowing the
|
|
104
|
+
* runtime applies: search mode only narrows (disclosed set is a subset of the
|
|
105
|
+
* allow/deny-filtered input, never wider, never zero, deterministic order); a
|
|
106
|
+
* denied tool is never described; the generated `search_tools` tool is always
|
|
107
|
+
* kept and its output is inert — names plus byte-truncated descriptions only,
|
|
108
|
+
* no JSON structure, no secret values — and activation stays disclosed beside
|
|
109
|
+
* the turn top-k next turn. Fails closed: an index over the hard cap discloses
|
|
110
|
+
* the full eligible list. Throws on the first violation.
|
|
111
|
+
*/
|
|
112
|
+
export function assertToolDisclosureConforms(options) {
|
|
113
|
+
const eligible = filterTools(options.tools, options.filter);
|
|
114
|
+
if (eligible.length === 0)
|
|
115
|
+
throw new Error("Disclosure conformance needs at least one eligible tool");
|
|
116
|
+
const activated = createActiveToolSet();
|
|
117
|
+
const topK = options.search?.topK ?? 16;
|
|
118
|
+
const state = createToolSearchState({ tools: eligible, activated, search: options.search });
|
|
119
|
+
const searchTool = createSearchToolsTool(state);
|
|
120
|
+
const runTools = [...eligible, searchTool];
|
|
121
|
+
const input = options.input ?? "zzzqqq unmatchable zero-match probe";
|
|
122
|
+
// 1. Narrowing stays a subset of the eligible list and always keeps search_tools.
|
|
123
|
+
const disclosed = selectDisclosedTools({ tools: runTools, input, search: options.search, activated });
|
|
124
|
+
const eligibleNames = new Set(eligible.map((tool) => tool.name));
|
|
125
|
+
eligibleNames.add(SEARCH_TOOLS_TOOL_NAME);
|
|
126
|
+
for (const tool of disclosed) {
|
|
127
|
+
if (!eligibleNames.has(tool.name)) {
|
|
128
|
+
throw new Error(`Disclosed tool ${tool.name} is not in the eligible list; search must never widen`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!disclosed.some((tool) => tool.name === SEARCH_TOOLS_TOOL_NAME)) {
|
|
132
|
+
throw new Error("Disclosed set dropped the generated search_tools tool");
|
|
133
|
+
}
|
|
134
|
+
if (disclosed.length === 0)
|
|
135
|
+
throw new Error("Disclosure disclosed zero tools; fail closed to a bounded non-empty set");
|
|
136
|
+
// 2. A deny-listed tool is never described to the provider.
|
|
137
|
+
const deniedName = eligible[0].name;
|
|
138
|
+
const deniedDisclosed = selectDisclosedTools({
|
|
139
|
+
tools: filterTools(runTools, { deny: [deniedName] }),
|
|
140
|
+
input,
|
|
141
|
+
search: options.search,
|
|
142
|
+
activated,
|
|
143
|
+
});
|
|
144
|
+
if (deniedDisclosed.some((tool) => tool.name === deniedName)) {
|
|
145
|
+
throw new Error(`Denied tool ${deniedName} was described to the provider`);
|
|
146
|
+
}
|
|
147
|
+
// 3. Deterministic order for identical turns.
|
|
148
|
+
const again = selectDisclosedTools({ tools: runTools, input, search: options.search, activated });
|
|
149
|
+
if (JSON.stringify(again.map((tool) => tool.name)) !== JSON.stringify(disclosed.map((tool) => tool.name))) {
|
|
150
|
+
throw new Error("Disclosure order is not deterministic for identical turns");
|
|
151
|
+
}
|
|
152
|
+
// 4. Fail closed past the index hard cap: full eligible list, never zero, never wider.
|
|
153
|
+
const oversized = Array.from({ length: HARD_MAX_TOOLS_INDEX + 1 }, (_, index) => ({
|
|
154
|
+
name: `cap_${index}`,
|
|
155
|
+
description: "Fixture tool beyond the frozen index cap.",
|
|
156
|
+
parameters: { type: "object", properties: {} },
|
|
157
|
+
execute: () => ({ toolCallId: "x", name: "cap" }),
|
|
158
|
+
}));
|
|
159
|
+
const overflowed = selectDisclosedTools({ tools: oversized, input, search: options.search });
|
|
160
|
+
if (overflowed.length !== oversized.length) {
|
|
161
|
+
throw new Error(`Index overflow must disclose the full list; disclosed ${overflowed.length} of ${oversized.length}`);
|
|
162
|
+
}
|
|
163
|
+
// 5. search_tools output is inert: names + byte-truncated descriptions, no JSON
|
|
164
|
+
// structure, no secret values; activation bounded to the eligible set.
|
|
165
|
+
const oversizedDescription = `${"padding ".repeat(160)}TAIL-MARKER-BEYOND-TRUNCATION`;
|
|
166
|
+
const probeEligible = [
|
|
167
|
+
...eligible,
|
|
168
|
+
{
|
|
169
|
+
name: "oversized_desc_tool",
|
|
170
|
+
description: oversizedDescription,
|
|
171
|
+
parameters: { type: "object", properties: { untrusted: { type: "string" } } },
|
|
172
|
+
execute: () => ({ toolCallId: "x", name: "oversized_desc_tool" }),
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
const probeState = createToolSearchState({ tools: probeEligible, activated, search: options.search });
|
|
176
|
+
const probeTool = createSearchToolsTool(probeState);
|
|
177
|
+
const probeResult = probeTool.execute({ query: "oversized_desc_tool" }, context("probe"));
|
|
178
|
+
if (probeResult.error)
|
|
179
|
+
throw new Error(`search_tools rejected a valid bounded query: ${probeResult.error.message}`);
|
|
180
|
+
const text = probeResult.content?.find((block) => block.type === "text");
|
|
181
|
+
if (text?.type !== "text" || !text.text.startsWith("- ")) {
|
|
182
|
+
throw new Error("search_tools returned no bounded name+description lines");
|
|
183
|
+
}
|
|
184
|
+
if (text.text.includes("TAIL-MARKER-BEYOND-TRUNCATION")) {
|
|
185
|
+
throw new Error("search_tools emitted an untruncated oversized description; descriptions are truncated, descriptions are never executed");
|
|
186
|
+
}
|
|
187
|
+
if (/[{}]/.test(text.text)) {
|
|
188
|
+
throw new Error("search_tools text carries JSON structure; output must be inert name+description lines only");
|
|
189
|
+
}
|
|
190
|
+
const activatedCount = activated.list().length;
|
|
191
|
+
if (activatedCount === 0 || activatedCount > topK)
|
|
192
|
+
throw new Error(`activation not bounded: ${activatedCount} names`);
|
|
193
|
+
const probeNames = new Set(probeEligible.map((tool) => tool.name));
|
|
194
|
+
for (const name of activated.list()) {
|
|
195
|
+
if (!probeNames.has(name))
|
|
196
|
+
throw new Error(`activated ${name} is not in the eligible set`);
|
|
197
|
+
}
|
|
198
|
+
// 5b. Secret scan: query every eligible tool by name; configured secret values
|
|
199
|
+
// must never surface in model-facing search output even when a host
|
|
200
|
+
// description carries one.
|
|
201
|
+
for (const tool of eligible) {
|
|
202
|
+
const probe = probeTool.execute({ query: tool.name }, context("probe-secret"));
|
|
203
|
+
const probeBlob = JSON.stringify(probe);
|
|
204
|
+
for (const secret of options.secrets ?? []) {
|
|
205
|
+
if (secret && probeBlob.includes(secret)) {
|
|
206
|
+
throw new Error("search output leaked a configured secret");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// 6. Activated tools stay disclosed on the next turn, beside the turn top-k.
|
|
211
|
+
// (Names outside the runtime list are inert by design — e.g. the probe's
|
|
212
|
+
// synthetic tool above — so only eligible names are asserted.)
|
|
213
|
+
const nextTurn = selectDisclosedTools({
|
|
214
|
+
tools: runTools,
|
|
215
|
+
input: "zzzqqq unmatchable next-turn probe",
|
|
216
|
+
search: options.search,
|
|
217
|
+
activated,
|
|
218
|
+
});
|
|
219
|
+
for (const name of activated.list()) {
|
|
220
|
+
if (eligibleNames.has(name) && !nextTurn.some((tool) => tool.name === name)) {
|
|
221
|
+
throw new Error(`activated tool ${name} was dropped from the next turn's disclosed set`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
101
225
|
function pickPolicy(options) {
|
|
102
226
|
return { permission: options.permission, validate: options.validate, filter: options.filter };
|
|
103
227
|
}
|
|
228
|
+
function context(toolCallId) {
|
|
229
|
+
return { sessionId: "conformance", runId: "r", toolCallId };
|
|
230
|
+
}
|
|
104
231
|
//# sourceMappingURL=tool-conformance.js.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Message, ToolDefinition } from "./contracts.js";
|
|
2
|
+
/** Same shape as `AgentInput`; kept structural here so core's assembler and this module never form a runtime cycle. */
|
|
3
|
+
type ToolSearchInput = string | Message | readonly Message[];
|
|
4
|
+
export type ToolsDisclosure = "all" | "search";
|
|
5
|
+
export interface ToolsSearchOptions {
|
|
6
|
+
/** Top-k tools disclosed per turn. Default 16; clamped to the hard cap. */
|
|
7
|
+
readonly topK?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare const SEARCH_TOOLS_TOOL_NAME = "search_tools";
|
|
10
|
+
export declare const DEFAULT_TOOLS_SEARCH_TOP_K = 16;
|
|
11
|
+
export declare const HARD_MAX_TOOLS_SEARCH_TOP_K = 64;
|
|
12
|
+
/** Frozen index cap; larger registries fail closed to full disclosure. */
|
|
13
|
+
export declare const HARD_MAX_TOOLS_INDEX = 1024;
|
|
14
|
+
export declare const DEFAULT_MAX_TOOLS_SEARCH_QUERY_BYTES = 4096;
|
|
15
|
+
export declare const HARD_MAX_TOOLS_SEARCH_QUERY_BYTES = 65536;
|
|
16
|
+
export declare const TOOL_DISCLOSURE_ERROR_CODE: "tool_disclosure_exceeded";
|
|
17
|
+
export declare class ToolDisclosureError extends Error {
|
|
18
|
+
readonly code: "tool_disclosure_exceeded";
|
|
19
|
+
constructor(message: string);
|
|
20
|
+
}
|
|
21
|
+
export declare function isToolDisclosureError(error: unknown): error is ToolDisclosureError;
|
|
22
|
+
/** Run options win over agent config (mirrors resolveSkillsDisclosure); default "all". */
|
|
23
|
+
export declare function resolveToolsDisclosure(run?: ToolsDisclosure, agent?: ToolsDisclosure): ToolsDisclosure;
|
|
24
|
+
/** Activation set: same shape as LoadedSkillSet (names-only persistence, per-session instance). */
|
|
25
|
+
export interface ActiveToolSet {
|
|
26
|
+
has(name: string): boolean;
|
|
27
|
+
add(name: string): void;
|
|
28
|
+
list(): readonly string[];
|
|
29
|
+
clear(): void;
|
|
30
|
+
}
|
|
31
|
+
export declare function createActiveToolSet(): ActiveToolSet;
|
|
32
|
+
interface PostingEntry {
|
|
33
|
+
readonly toolIndex: number;
|
|
34
|
+
tf: number;
|
|
35
|
+
}
|
|
36
|
+
export interface ToolSearchIndex {
|
|
37
|
+
readonly tools: readonly ToolDefinition[];
|
|
38
|
+
readonly postings: ReadonlyMap<string, PostingEntry[]>;
|
|
39
|
+
}
|
|
40
|
+
/** ponytail: O(n·d) lexical index, rebuilt on registry change by the caller — embedder-backed
|
|
41
|
+
* scoring via the @arnilo/prism-memory/rag seam if accuracy fixtures fall short (plan 041). */
|
|
42
|
+
export declare function createToolSearchIndex(tools: readonly ToolDefinition[], skip?: (tool: ToolDefinition) => boolean): ToolSearchIndex;
|
|
43
|
+
export interface ToolSearchMatch {
|
|
44
|
+
readonly name: string;
|
|
45
|
+
readonly description?: string;
|
|
46
|
+
/** Query terms present in name/description, most significant first. */
|
|
47
|
+
readonly matched: readonly string[];
|
|
48
|
+
}
|
|
49
|
+
/** Bounded lexical scoring: BM25-lite (tf × IDF, registry-derived DF). Ties keep registry order. */
|
|
50
|
+
export declare function scoreTools(index: ToolSearchIndex, query: string, k: number, queryByteCap?: number): readonly ToolSearchMatch[];
|
|
51
|
+
export interface ToolSearchState {
|
|
52
|
+
readonly index: ToolSearchIndex;
|
|
53
|
+
readonly activated: ActiveToolSet;
|
|
54
|
+
readonly topK: number;
|
|
55
|
+
}
|
|
56
|
+
export declare function createToolSearchState(options: {
|
|
57
|
+
readonly tools: readonly ToolDefinition[];
|
|
58
|
+
readonly activated: ActiveToolSet;
|
|
59
|
+
readonly search?: ToolsSearchOptions;
|
|
60
|
+
}): ToolSearchState;
|
|
61
|
+
/** Provider-facing narrowing: activated tools ∪ top-k for the turn. Fails closed — any
|
|
62
|
+
* scoring/index error discloses the full input list (never zero, never wider than input). */
|
|
63
|
+
export declare function selectDisclosedTools(options: {
|
|
64
|
+
readonly tools: readonly ToolDefinition[];
|
|
65
|
+
readonly input: ToolSearchInput;
|
|
66
|
+
readonly queryByteCap?: number;
|
|
67
|
+
readonly search?: ToolsSearchOptions;
|
|
68
|
+
readonly activated?: {
|
|
69
|
+
has(name: string): boolean;
|
|
70
|
+
};
|
|
71
|
+
}): readonly ToolDefinition[];
|
|
72
|
+
/** Model-facing activation tool, generated only in search mode. Results are inert
|
|
73
|
+
* name+description lines — no schemas or bodies — and activation re-checks allow/deny
|
|
74
|
+
* at dispatch time (blocked-reason matrix unchanged). */
|
|
75
|
+
export declare function createSearchToolsTool(state: ToolSearchState, queryByteCap?: number): ToolDefinition;
|
|
76
|
+
export {};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
export const SEARCH_TOOLS_TOOL_NAME = "search_tools";
|
|
2
|
+
export const DEFAULT_TOOLS_SEARCH_TOP_K = 16;
|
|
3
|
+
export const HARD_MAX_TOOLS_SEARCH_TOP_K = 64;
|
|
4
|
+
/** Frozen index cap; larger registries fail closed to full disclosure. */
|
|
5
|
+
export const HARD_MAX_TOOLS_INDEX = 1024;
|
|
6
|
+
export const DEFAULT_MAX_TOOLS_SEARCH_QUERY_BYTES = 4_096;
|
|
7
|
+
export const HARD_MAX_TOOLS_SEARCH_QUERY_BYTES = 65_536;
|
|
8
|
+
export const TOOL_DISCLOSURE_ERROR_CODE = "tool_disclosure_exceeded";
|
|
9
|
+
export class ToolDisclosureError extends Error {
|
|
10
|
+
code = TOOL_DISCLOSURE_ERROR_CODE;
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "ToolDisclosureError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function isToolDisclosureError(error) {
|
|
17
|
+
return error instanceof Error && error.code === TOOL_DISCLOSURE_ERROR_CODE;
|
|
18
|
+
}
|
|
19
|
+
/** Run options win over agent config (mirrors resolveSkillsDisclosure); default "all". */
|
|
20
|
+
export function resolveToolsDisclosure(run, agent) {
|
|
21
|
+
return run ?? agent ?? "all";
|
|
22
|
+
}
|
|
23
|
+
export function createActiveToolSet() {
|
|
24
|
+
const names = new Set();
|
|
25
|
+
return {
|
|
26
|
+
has(name) {
|
|
27
|
+
return names.has(name);
|
|
28
|
+
},
|
|
29
|
+
add(name) {
|
|
30
|
+
names.add(name);
|
|
31
|
+
},
|
|
32
|
+
list() {
|
|
33
|
+
return [...names];
|
|
34
|
+
},
|
|
35
|
+
clear() {
|
|
36
|
+
names.clear();
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Fixed tokenization: lowercase alphanumeric runs. No user-controlled regex anywhere. */
|
|
41
|
+
const WORD_SPLIT = /[^a-z0-9]+/;
|
|
42
|
+
function tokenize(text) {
|
|
43
|
+
return text
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.split(WORD_SPLIT)
|
|
46
|
+
.filter((token) => token.length > 0);
|
|
47
|
+
}
|
|
48
|
+
/** ponytail: O(n·d) lexical index, rebuilt on registry change by the caller — embedder-backed
|
|
49
|
+
* scoring via the @arnilo/prism-memory/rag seam if accuracy fixtures fall short (plan 041). */
|
|
50
|
+
export function createToolSearchIndex(tools, skip) {
|
|
51
|
+
if (tools.length > HARD_MAX_TOOLS_INDEX) {
|
|
52
|
+
throw new ToolDisclosureError(`Tool index exceeds hard cap (${HARD_MAX_TOOLS_INDEX} tools)`);
|
|
53
|
+
}
|
|
54
|
+
const postings = new Map();
|
|
55
|
+
for (const [toolIndex, tool] of tools.entries()) {
|
|
56
|
+
if (skip?.(tool))
|
|
57
|
+
continue;
|
|
58
|
+
// Name tokens weigh ×3 so an exact name match outranks description-only matches.
|
|
59
|
+
const seen = new Map();
|
|
60
|
+
for (const term of [...tokenize(tool.name), ...tokenize(tool.name), ...tokenize(tool.name), ...tokenize(tool.description ?? "")]) {
|
|
61
|
+
seen.set(term, (seen.get(term) ?? 0) + 1);
|
|
62
|
+
}
|
|
63
|
+
for (const [term, tf] of seen) {
|
|
64
|
+
const list = postings.get(term) ?? [];
|
|
65
|
+
list.push({ toolIndex, tf });
|
|
66
|
+
postings.set(term, list);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { tools, postings };
|
|
70
|
+
}
|
|
71
|
+
/** Bounded lexical scoring: BM25-lite (tf × IDF, registry-derived DF). Ties keep registry order. */
|
|
72
|
+
export function scoreTools(index, query, k, queryByteCap = DEFAULT_MAX_TOOLS_SEARCH_QUERY_BYTES) {
|
|
73
|
+
const bounded = query.slice(0, Math.trunc(queryByteCap));
|
|
74
|
+
const terms = tokenize(bounded);
|
|
75
|
+
if (terms.length === 0 || k <= 0 || index.tools.length === 0)
|
|
76
|
+
return [];
|
|
77
|
+
const limit = Math.min(Math.trunc(k), index.tools.length);
|
|
78
|
+
const total = index.tools.filter((tool) => tool.name !== SEARCH_TOOLS_TOOL_NAME).length;
|
|
79
|
+
const scores = new Array(index.tools.length).fill(0);
|
|
80
|
+
const matchedTerms = new Map();
|
|
81
|
+
for (const term of new Set(terms)) {
|
|
82
|
+
const list = index.postings.get(term);
|
|
83
|
+
if (!list)
|
|
84
|
+
continue;
|
|
85
|
+
const idf = 1 + Math.log(total / list.length);
|
|
86
|
+
for (const entry of list) {
|
|
87
|
+
scores[entry.toolIndex] += entry.tf * idf;
|
|
88
|
+
const names = matchedTerms.get(entry.toolIndex) ?? [];
|
|
89
|
+
if (names.length < 8)
|
|
90
|
+
names.push(term);
|
|
91
|
+
matchedTerms.set(entry.toolIndex, names);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const ranked = scores
|
|
95
|
+
.map((score, toolIndex) => ({ score, toolIndex }))
|
|
96
|
+
.filter((entry) => entry.score > 0)
|
|
97
|
+
.sort((a, b) => b.score - a.score || a.toolIndex - b.toolIndex)
|
|
98
|
+
.slice(0, limit);
|
|
99
|
+
return ranked.map(({ toolIndex }) => ({
|
|
100
|
+
name: index.tools[toolIndex].name,
|
|
101
|
+
description: index.tools[toolIndex].description,
|
|
102
|
+
matched: matchedTerms.get(toolIndex) ?? [],
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
export function createToolSearchState(options) {
|
|
106
|
+
const requested = options.search?.topK;
|
|
107
|
+
const topK = requested === undefined ? DEFAULT_TOOLS_SEARCH_TOP_K : Math.min(Math.max(1, Math.trunc(requested)), HARD_MAX_TOOLS_SEARCH_TOP_K);
|
|
108
|
+
// ponytail: index built once per run; mid-run registry mutation is not observed — rebuild by starting a new run.
|
|
109
|
+
const index = createToolSearchIndex(options.tools, (tool) => tool.name === SEARCH_TOOLS_TOOL_NAME);
|
|
110
|
+
return { index, activated: options.activated, topK };
|
|
111
|
+
}
|
|
112
|
+
/** Provider-facing narrowing: activated tools ∪ top-k for the turn. Fails closed — any
|
|
113
|
+
* scoring/index error discloses the full input list (never zero, never wider than input). */
|
|
114
|
+
export function selectDisclosedTools(options) {
|
|
115
|
+
if (options.tools.length === 0)
|
|
116
|
+
return options.tools;
|
|
117
|
+
try {
|
|
118
|
+
const keep = new Set();
|
|
119
|
+
for (const tool of options.tools)
|
|
120
|
+
if (tool.name === SEARCH_TOOLS_TOOL_NAME || options.activated?.has(tool.name) === true)
|
|
121
|
+
keep.add(tool.name);
|
|
122
|
+
const index = createToolSearchIndex(options.tools, (tool) => tool.name === SEARCH_TOOLS_TOOL_NAME);
|
|
123
|
+
const topK = Math.min(Math.max(1, Math.trunc(options.search?.topK ?? DEFAULT_TOOLS_SEARCH_TOP_K)), HARD_MAX_TOOLS_SEARCH_TOP_K);
|
|
124
|
+
for (const match of scoreTools(index, inputText(options.input), topK, options.queryByteCap ?? DEFAULT_MAX_TOOLS_SEARCH_QUERY_BYTES))
|
|
125
|
+
keep.add(match.name);
|
|
126
|
+
const disclosed = options.tools.filter((tool) => keep.has(tool.name));
|
|
127
|
+
if (disclosed.length > 0)
|
|
128
|
+
return disclosed;
|
|
129
|
+
// Nothing scored or activated and no always-on tools in the list: bounded deterministic
|
|
130
|
+
// prefix instead of zero tools (the wired session always keeps `search_tools`, so the
|
|
131
|
+
// un-wired caller is the only one that reaches this).
|
|
132
|
+
return options.tools.slice(0, Math.min(Math.max(1, Math.trunc(options.search?.topK ?? DEFAULT_TOOLS_SEARCH_TOP_K)) + 1, options.tools.length));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return options.tools;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function inputText(input) {
|
|
139
|
+
if (typeof input === "string")
|
|
140
|
+
return input;
|
|
141
|
+
const messages = input === null || input === undefined || Array.isArray(input) === false ? [input] : input;
|
|
142
|
+
let text = "";
|
|
143
|
+
for (const message of messages) {
|
|
144
|
+
for (const block of message.content) {
|
|
145
|
+
if (block.type === "text") {
|
|
146
|
+
text += `${block.text}\n`;
|
|
147
|
+
if (text.length > HARD_MAX_TOOLS_SEARCH_QUERY_BYTES)
|
|
148
|
+
return text;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return text;
|
|
153
|
+
}
|
|
154
|
+
/** Model-facing activation tool, generated only in search mode. Results are inert
|
|
155
|
+
* name+description lines — no schemas or bodies — and activation re-checks allow/deny
|
|
156
|
+
* at dispatch time (blocked-reason matrix unchanged). */
|
|
157
|
+
export function createSearchToolsTool(state, queryByteCap = DEFAULT_MAX_TOOLS_SEARCH_QUERY_BYTES) {
|
|
158
|
+
return {
|
|
159
|
+
name: SEARCH_TOOLS_TOOL_NAME,
|
|
160
|
+
description: `Search available tools by relevance. Returns up to k tool names with short descriptions and marks them active so their full definitions appear on the next turn. Try queries made of tool keywords.`,
|
|
161
|
+
parameters: {
|
|
162
|
+
type: "object",
|
|
163
|
+
properties: { query: { type: "string" }, k: { type: "integer", minimum: 1 } },
|
|
164
|
+
required: ["query"],
|
|
165
|
+
},
|
|
166
|
+
execute(args, context) {
|
|
167
|
+
const fail = (message) => ({
|
|
168
|
+
toolCallId: context.toolCallId,
|
|
169
|
+
name: SEARCH_TOOLS_TOOL_NAME,
|
|
170
|
+
error: { code: "ERR_PRISM_TOOL_SEARCH_INVALID", message },
|
|
171
|
+
});
|
|
172
|
+
if (typeof args.query !== "string" || args.query.trim().length === 0)
|
|
173
|
+
return fail("query must be a non-empty string");
|
|
174
|
+
if (Buffer.byteLength(args.query, "utf8") > HARD_MAX_TOOLS_SEARCH_QUERY_BYTES)
|
|
175
|
+
return fail(`query exceeds ${HARD_MAX_TOOLS_SEARCH_QUERY_BYTES} bytes`);
|
|
176
|
+
const k = args.k === undefined ? state.topK : typeof args.k === "number" && Number.isInteger(args.k) ? args.k : NaN;
|
|
177
|
+
if (Number.isNaN(k) || k < 1)
|
|
178
|
+
return fail("k must be an integer >= 1");
|
|
179
|
+
const matches = scoreTools(state.index, args.query, k, queryByteCap);
|
|
180
|
+
for (const match of matches)
|
|
181
|
+
state.activated.add(match.name);
|
|
182
|
+
return {
|
|
183
|
+
toolCallId: context.toolCallId,
|
|
184
|
+
name: SEARCH_TOOLS_TOOL_NAME,
|
|
185
|
+
content: [
|
|
186
|
+
{
|
|
187
|
+
type: "text",
|
|
188
|
+
text: matches.length === 0
|
|
189
|
+
? `No tools matched ${JSON.stringify(args.query.slice(0, 64))}. Try different keywords.`
|
|
190
|
+
: matches
|
|
191
|
+
.map((match) => `- ${match.name}${match.description ? `: ${match.description.slice(0, 512)}` : ""} [matched: ${match.matched.join(", ")}]`)
|
|
192
|
+
.join("\n"),
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=tool-search.js.map
|
package/docs/0.1.0-readiness.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 0.1.0 / 1.0 Readiness Gates
|
|
2
2
|
|
|
3
|
-
Status: **0.
|
|
3
|
+
Status: **0.4.0** is the current release line (plan 054 package consolidation: 11 active packages, family subpaths, `^0.4.0` peers); **0.3.3** was the terminal 0.3.x cut; **0.1.7** was the terminal 0.1.x baseline; **1.0** readiness remains operator-gated, not automatic.
|
|
4
4
|
|
|
5
5
|
This page distills runnable readiness gates into one command-per-gate table.
|
|
6
6
|
The **Last evidence** column records the 0.1.0-tree snapshot (plan 012 Tasks
|
|
@@ -20,7 +20,7 @@ Historical release lines (0.0.16 floor → 0.0.27 Phase 10 ACP interop → 0.1.0
|
|
|
20
20
|
keep their per-phase evidence in the pages above; this page records the 0.2.6
|
|
21
21
|
snapshot (plan 026) with the 0.1.x tables below as the historical record.
|
|
22
22
|
|
|
23
|
-
## Current line (0.
|
|
23
|
+
## Current line (0.4.0)
|
|
24
24
|
|
|
25
25
|
| Item | Status |
|
|
26
26
|
|---|---|
|
package/docs/acp-agent.md
CHANGED
|
@@ -56,7 +56,7 @@ The binary is pure wiring (~200 lines) — no protocol code lives here. It build
|
|
|
56
56
|
|
|
57
57
|
```ts
|
|
58
58
|
import { createSpawnableAgent, loadConfig } from "@arnilo/prism-acp-agent";
|
|
59
|
-
import { createOpenAIResponsesProvider } from "@arnilo/prism-
|
|
59
|
+
import { createOpenAIResponsesProvider } from "@arnilo/prism-providers/openai";
|
|
60
60
|
|
|
61
61
|
const agent = createSpawnableAgent({
|
|
62
62
|
config: loadConfig("prism-acp-agent.json"),
|
|
@@ -27,7 +27,7 @@ Do not use the bundle loader to discover providers — provider/model packages s
|
|
|
27
27
|
| --- | --- |
|
|
28
28
|
| `name` | Required agent name. |
|
|
29
29
|
| `description?` | Optional description. |
|
|
30
|
-
| `model?` | `ModelConfig` object, or a `"<provider>/<model>"` string resolved through `registries.models`. |
|
|
30
|
+
| `model?` | `ModelConfig` object, or a `"<provider>/<model>"` string resolved through `registries.models`. Optional at authoring time: when omitted, resolution falls back to `context.overrides.model` (host-injected selection); an explicit definition `model` drives registry resolution, and neither present fails closed with `Agent "<name>" has no model`. |
|
|
31
31
|
| `tools?` | Tool names to activate from the active tool registry / `registries.tools`. Omitted means no active tools unless `activateAllCapabilities: true` is passed for migration. |
|
|
32
32
|
| `skills?` | Skill names resolved via `resolveActiveSkills()`; omitted means no active skills unless `activateAllCapabilities: true` is passed for migration. `toolNames` enforcement applies at activation. |
|
|
33
33
|
| `context?` | Context provider names from `registries.contextProviders`. |
|
|
@@ -23,7 +23,7 @@ Use `@arnilo/prism-antigravity-agent` when:
|
|
|
23
23
|
- You want conversation continuation across multiple user turns in a persistent session.
|
|
24
24
|
|
|
25
25
|
Do **not** use it:
|
|
26
|
-
- As a generic LLM model provider. For direct Gemini API or Vertex AI foundation model inference without an autonomous loop, use [`@arnilo/prism-
|
|
26
|
+
- As a generic LLM model provider. For direct Gemini API or Vertex AI foundation model inference without an autonomous loop, use [`@arnilo/prism-providers/google`](providers/google.md) or [`@arnilo/prism-providers/vertex`](providers/vertex.md).
|
|
27
27
|
- If you require step-by-step turn replacement of Antigravity's internal model loop, compaction, or planning strategy.
|
|
28
28
|
- If you require unreleased raw internal chain-of-thought text. Antigravity reasoning effort is projected as token counts and timeline activity steps, not raw hidden thoughts.
|
|
29
29
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## What it does
|
|
4
4
|
|
|
5
|
-
`@arnilo/prism-browser` exposes six exclusive model-facing tools—`browser_open`, `browser_snapshot`, `browser_act`, `browser_close`, `browser_evaluate`, and `browser_observe`—over a host-supplied Playwright `Browser`. Prism creates one non-persistent `BrowserContext` per run, serializes actions, returns bounded AI-mode accessibility snapshots with snapshot-scoped refs, enforces egress/side-effect/upload/download/screenshot policy, and closes context/pages/listeners/quarantined downloads on close, abort, or manager disposal. Since 0.1.4 the package also rides playwright-core's existing CDP transport for bounded page evaluation, console/network observation, and network/emulation control on Chromium hosts — zero new dependencies, Prism still never launches or downloads browsers.
|
|
5
|
+
The `@arnilo/prism-web-tools/browser` subpath exposes six exclusive model-facing tools—`browser_open`, `browser_snapshot`, `browser_act`, `browser_close`, `browser_evaluate`, and `browser_observe`—over a host-supplied Playwright `Browser`. Prism creates one non-persistent `BrowserContext` per run, serializes actions, returns bounded AI-mode accessibility snapshots with snapshot-scoped refs, enforces egress/side-effect/upload/download/screenshot policy, and closes context/pages/listeners/quarantined downloads on close, abort, or manager disposal. Since 0.1.4 the package also rides playwright-core's existing CDP transport for bounded page evaluation, console/network observation, and network/emulation control on Chromium hosts — zero new dependencies, Prism still never launches or downloads browsers.
|
|
6
6
|
|
|
7
7
|
## When to use it
|
|
8
8
|
|
|
@@ -65,8 +65,8 @@ import {
|
|
|
65
65
|
createBrowserManager,
|
|
66
66
|
createBrowserTools,
|
|
67
67
|
createSharedSandboxBrowserOptions,
|
|
68
|
-
} from "@arnilo/prism-browser";
|
|
69
|
-
import { assertBrowserSandboxNetwork } from "@arnilo/prism-coding-security";
|
|
68
|
+
} from "@arnilo/prism-web-tools/browser";
|
|
69
|
+
import { assertBrowserSandboxNetwork } from "@arnilo/prism-coding-tools/security";
|
|
70
70
|
|
|
71
71
|
assertBrowserSandboxNetwork({
|
|
72
72
|
mode: "custom",
|
|
@@ -106,7 +106,7 @@ await browser.close();
|
|
|
106
106
|
- `networkPolicy` defaults to `requireContainedProxy: true` (fail closed). Hosts must supply `containedProxyAttestation: { proxyEndpoint, denyDirectEgress: true }`. Private/loopback/link-local, `file`/`data`/`blob`/`javascript`/`devtools` schemes are denied by default. Playwright routing is defense in depth — production DNS/private egress is a host firewall/proxy.
|
|
107
107
|
- Uploads require absolute paths under `uploads.roots` (realpath-contained; symlink escapes rejected). Downloads stream into `downloads.quarantine` with SHA-256/MIME/name metadata; `download_release` requires host `approveRelease`. Screenshots return bounded `ImageContent`.
|
|
108
108
|
- Observation (`snapshot`, `wait`, open-without-url, `close`) vs mutation/high-impact (`navigate`, click/form, dialog accept, upload, download release, popup select) is classified for `ExecutionPolicy` / `beforeSideEffect`.
|
|
109
|
-
- `createSharedSandboxBrowserOptions()` aligns browser uploads/downloads with Task 1 sandbox `/workspace` and `/downloads`. `assertBrowserSandboxNetwork()` in `@arnilo/prism-coding-security` fails closed for custom Docker networks without browser egress attestation.
|
|
109
|
+
- `createSharedSandboxBrowserOptions()` aligns browser uploads/downloads with Task 1 sandbox `/workspace` and `/downloads`. `assertBrowserSandboxNetwork()` in `@arnilo/prism-coding-tools/security` fails closed closed for custom Docker networks without browser egress attestation.
|
|
110
110
|
- Raw CSS/XPath: since 0.1.4 `{ css }` / `{ xpath }` targets resolve via Playwright's selector engine (`locator(css)` / `locator("xpath=…")`); ref resolution keeps the built-in `aria-ref=` selector with a package-owned snapshot ref table for staleness checks.
|
|
111
111
|
- CDP capabilities (0.1.4): `browser_evaluate` (bounded `Runtime.evaluate`), `browser_observe` (Runtime console/exception + Network request/response/failed events in a bounded ring with drain-on-read), and `block_urls`/`unblock_urls` (`Network.setBlockedURLs`), `throttle` (`Network.emulateNetworkConditions`), `emulate` (`Emulation.setDeviceMetricsOverride` + optional `setUserAgentOverride`). All CDP sessions are per-page via `context.newCDPSession(page)` and are detached on run close — network/emulation changes are run-scoped and reset with `browser_close`. `BrowserCdpOptions.mode` (`auto` | `on` | `off`, default `auto`) gates the surface: non-Chromium hosts or mode `off` return `ERR_PRISM_BROWSER_CDP_UNAVAILABLE` without affecting Playwright-only tools. Domains are limited to the Runtime/Network/Emulation allowlist — cookies, tracing, performance profiles, IndexedDB, and worker debugging are not exposed. CDP is not an egress bypass: page network still routes through the run's routing/blocking and `networkPolicy`.
|
|
112
112
|
- CDP bounds (0.1.4): evaluate expression ≤ `maxActionInputBytes` (64 KiB default / 256 KiB hard) and result capped at `maxEvaluateResultBytes` (64 KiB / 256 KiB) with truncation marking; `browser_observe` rings capped at `maxConsoleEntries` (200/500) and `maxNetworkRequests`; `block_urls` patterns ≤ `maxBlockedUrlPatterns` (32/128); throttle latency ≤ 120 s and throughput ≤ 1 Gbps; emulate dimensions ≤ 16 384 and scale ≤ 10, user agent ≤ 2 KiB. Evaluate is classified high-impact (arbitrary page-context code execution): `ExecutionPolicy` approval and the `beforeSideEffect` hook are mandatory, it charges the action budget, and results are marked `untrusted_external`. `browser_observe` is observation-only (no side-effect hook, no action charge) and **never captures request/response bodies, cookies, or auth headers** — only bounded URL/method/status/error-text/arg previews.
|
|
@@ -118,7 +118,7 @@ Observation tools declare `kind: none`; mutations are `external_mutation`/`unsup
|
|
|
118
118
|
|
|
119
119
|
Import is inert. Construction fails clearly when neither `browser` nor `manager` is supplied. Browser installation, launch, version, and control endpoint are host-owned. Prism never exposes init scripts, extensions, persistent profiles, or model-supplied Playwright launch options; CDP exposure is limited to the allowlisted Runtime/Network/Emulation surface above (evaluate is policy-gated arbitrary code execution — treat results as untrusted). Secrets and storage state must not appear in snapshots, tool results, logs, or checkpoints. Finite caps charge before context/page/action/queue/snapshot/network/artifact retention; snapshots retain no unbounded DOM, console, request, response, or trace history. Unreleased downloads are deleted on context close.
|
|
120
120
|
|
|
121
|
-
Default tests use fake Playwright APIs only. Protected live gate: `PRISM_LIVE_PLAYWRIGHT=1` (or `PRISM_TEST_PLAYWRIGHT=1`) `npm run test:live -w @arnilo/prism-
|
|
121
|
+
Default tests use fake Playwright APIs only. Protected live gate: `PRISM_LIVE_PLAYWRIGHT=1` (or `PRISM_TEST_PLAYWRIGHT=1`) `npm run test:live -w @arnilo/prism-web-tools` exercises a local loopback hostile HTML fixture for snapshot refs, stale-ref rejection, css/xpath targets, private/file deny, upload containment, screenshot bounds, download quarantine/release, and the CDP leg (real evaluate, observe, and emulate). Missing browser binaries fail closed when the gate is enabled. The protected coding journey (0.2.6, plan 026 Task 7) additionally runs a real browser inspection leg (local loopback fixture page, snapshot text assertion, run-owned context closed before the host browser) inside the packed consumer as part of scripts/phase26-coding-journey.test.mjs, gated by PRISM_LIVE_PLAYWRIGHT with the pinned playwright-core installed into the consumer; browser storage never appears in the retained report. Adversarial network-free fixtures live in `eval-fixtures.test.ts`; see [Evaluations](evaluations.md) and `examples/coding-browser-evaluation.ts`.
|
|
122
122
|
|
|
123
123
|
## Related APIs
|
|
124
124
|
|