@arnilo/prism 0.5.5 → 0.6.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 +46 -0
- package/README.md +10 -10
- package/dist/agent-approval.js +7 -6
- package/dist/agent-loops.js +51 -12
- package/dist/agent-session/session.d.ts +1 -0
- package/dist/agent-session/session.js +20 -2
- package/dist/agent-tool-dispatch.js +5 -4
- package/dist/cli-runner.d.ts +8 -1
- package/dist/cli-runner.js +97 -7
- package/dist/content.d.ts +3 -16
- package/dist/content.js +9 -99
- package/dist/context-budget.d.ts +12 -1
- package/dist/context-budget.js +42 -19
- package/dist/contracts-core/agent.d.ts +11 -0
- package/dist/contracts-core/agent.js +4 -1
- package/dist/extensions.d.ts +18 -1
- package/dist/extensions.js +10 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +4 -4
- package/dist/input.d.ts +6 -0
- package/dist/input.js +12 -1
- package/dist/media-types.d.ts +34 -0
- package/dist/media-types.js +158 -0
- package/dist/pinned-fetch.d.ts +2 -2
- package/dist/pinned-fetch.js +11 -12
- package/dist/redaction.js +74 -1
- package/dist/session-stores.d.ts +11 -0
- package/dist/session-stores.js +23 -8
- package/docs/acp.md +1 -1
- package/docs/ag-ui.md +4 -2
- package/docs/agent-events.md +2 -0
- package/docs/agent-loops.md +1 -1
- package/docs/agent-session-runtime.md +3 -1
- package/docs/browser-automation.md +5 -2
- package/docs/cli-rpc.md +15 -1
- package/docs/contributing.md +37 -0
- package/docs/core.md +2 -0
- package/docs/document-reader.md +2 -0
- package/docs/documents.md +1 -1
- package/docs/extension-authoring.md +8 -9
- package/docs/extensions.md +13 -1
- package/docs/graft.md +29 -5
- package/docs/history/release-handoffs.md +33 -0
- package/docs/host-security.md +2 -2
- package/docs/index.md +33 -17
- package/docs/input-and-prompt-assembly.md +4 -4
- package/docs/language-intelligence.md +1 -1
- package/docs/migrate-to-0.5.md +7 -2
- package/docs/migrate-to-0.6.md +89 -0
- package/docs/migration.md +30 -0
- package/docs/model-registry.md +1 -1
- package/docs/multimodal-content.md +1 -1
- package/docs/obscura.md +3 -1
- package/docs/options-index.md +286 -0
- package/docs/peer-dependencies.md +94 -0
- package/docs/performance.md +34 -2
- package/docs/ponytail.md +2 -0
- package/docs/postgres-persistence.md +3 -1
- package/docs/provider-conformance.md +1 -1
- package/docs/provider-packages.md +21 -21
- package/docs/provider-primitives.md +2 -1
- package/docs/providers/ai-sdk.md +5 -2
- package/docs/public-contracts.md +2 -2
- package/docs/release-and-install.md +75 -55
- package/docs/server.md +1 -1
- package/docs/session-stores.md +3 -1
- package/docs/sqlite-persistence.md +2 -0
- package/docs/testing.md +38 -0
- package/docs/tools.md +1 -1
- package/docs/wiki.md +47 -3
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
+
## [0.6.0] - 2026-09-12 (plans 070, 071)
|
|
2
|
+
|
|
3
|
+
> **0.5.7 was never published.** This release folds that cut's content (durable concurrent tool rounds, strict-provider tool results, host-tunable knobs, peer/options truth, the dependency refresh, and the module splits) together with the 0.6.0 changes below, so a host on 0.5.6 upgrades once. See [docs/migrate-to-0.6.md](docs/migrate-to-0.6.md).
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Concurrent tool dispatch dropped sibling results.** With `toolConcurrency > 1` a failed or aborted call threw before the round's results were appended, so every successful sibling result was lost and the next provider request carried unanswered `tool_use` blocks. Results are now persisted before the round fails: successes as-is, the failing call as a real redacted `tool_execution_failed` error, and calls that never started as `tool_call_not_dispatched`. Run-level control errors (`ERR_PRISM_AGENT_RUN_SUSPENDED`, `ERR_PRISM_DELEGATION_SUSPENDED`, `ERR_PRISM_LOOP_*`) still skip synthetic results so durable recovery re-dispatches instead of double-appending.
|
|
7
|
+
- **Content-less tool results serialized as an empty payload.** A `ToolResult` with no `content`/`result` now folds to `EMPTY_TOOL_RESULT_TEXT` (`(tool completed with no output)`) through the shared `serializeToolResultJson` seam (OpenAI-compatible, Alibaba, DeepSeek, Kimi/Moonshot, NeuralWatt, OpenCode-Go), so strict providers stop rejecting zero-length tool payloads.
|
|
8
|
+
- **A failing coverage child hid its own error.** `npm run test:coverage` reported a crashed or failed workspace suite as a bare `no coverage data (suite failed)`, with the child's output discarded (the diagnostic gap that made a one-off `@arnilo/prism-memory` flake unreadable). The summary now prints the child's output tail under the failing row and records `status`/`exitCode`/`tail` on its artifact row — redacted through the public `createSecretRedactor` (repo root and home become placeholders, credential-shaped env values become `[REDACTED]`) and char-bounded (last 40 lines, 8 KiB) so one pathological line cannot bloat the artifact. Passing rows are byte-identical to before.
|
|
9
|
+
- **Memory patch merge aliased caller objects.** `packages/memory`'s `mergeJsonObjects` deep-merged in place; it now delegates to core `mergeConfigLayers` (deep copy, strict JSON validation — `undefined`/`Date`/function values fail closed — with the `MemoryValidationError` taxonomy preserved).
|
|
10
|
+
- **`redactSecrets` was O(n·m) in needles.** A guarded single-pass alternation fast path handles large inputs (≥16 KiB, 2–32 non-overlapping needles) with byte-identical output (~13× faster on 1 MiB transcripts); the ordered loop remains the fallback for overlapping needle sets and small inputs.
|
|
11
|
+
- **Peer manifest resolution broke for packages that do not export `./package.json`** (e.g. `@dietrichgebert/ponytail`): upstream resolvers now resolve the package entry point and walk up to the manifest.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **Host-tunable context assembly:** `AssembleProviderInputOptions.tokenEstimator?: (text: string) => number` replaces the built-in UTF-16/4 heuristic for eviction accounting (byte caps stay estimator-independent).
|
|
15
|
+
- **Host-tunable session snapshot cache:** `AgentSessionConfig.snapshotCacheTtlMs` — `DEFAULT_SNAPSHOT_CACHE_TTL_MS` (1000), `HARD_MAX_SNAPSHOT_CACHE_TTL_MS` (30000), `0` disables the branch-rebuild cache.
|
|
16
|
+
- **Host-tunable memory-session search caps:** `createMemorySessionStore(entries, { search: { maxLinearSessions, maxLinearEntries, maxLinearBytes } })`, validated against the same hard bounds as the defaults.
|
|
17
|
+
- **SSRF allow-list:** `SsrfPolicy.allowedCidrs` accepts IPv4 and IPv6 CIDR entries so a host can explicitly reach a private range; hostname denials (metadata endpoints), credential checks, and the fail-closed unparseable-CIDR behavior are unchanged.
|
|
18
|
+
- **Browser run lifetime:** `BrowserLimitOptions.idleRunTtlMs` (default `0`, `HARD_IDLE_RUN_TTL_MS` 30 min) reaps runs with nothing queued; any interaction resets the idle clock.
|
|
19
|
+
- **Host onboarding docs:** [`docs/peer-dependencies.md`](docs/peer-dependencies.md) (every third-party peer declaration with range, optionality, subpath, install line, and network footprint) and [`docs/options-index.md`](docs/options-index.md) (123 public option/limits/config surfaces routed to the page that documents them), both gated by `live-doc-check` against the manifests and the source.
|
|
20
|
+
- **Real-peer and ACP round-trip coverage:** contract smoke tests for the `@dietrichgebert/ponytail` and `@nanonets/graft` layouts, and stdio round-trip tests driving a spawned ACP agent with the real `@agentclientprotocol/sdk` client (spawn→close, mode negotiation, config options, refused/cancelled permissions).
|
|
21
|
+
- **Release-truth gates:** `scripts/version-literal-gate.test.mjs` asserts every release-claim surface (manifests, internal ranges, `package-lock.json`, the `src/index.ts` version constant, the `docs/index.md` current line, `release.yml` tag lists, `scripts/package-truth.json`) equals the root manifest, so a half-finished cut fails the suite instead of shipping; `scripts/workflow-liveness.test.mjs` resolves every workflow script/workspace/`uses:` reference and requires 40-hex SHA pins for actions; `scripts/wiki-scratch-isolation.test.mjs` proves the memory wiki suites leave tracked fixtures and the repository root untouched; `scripts/run-all-tests.mjs` runs every test stage without short-circuiting, and the startup import budget now asserts a machine-relative ratio (absolute 250 ms ceiling only off-load) so a busy machine no longer reports a false regression.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- **Node floor raised to `>=22`** in all ten publishable packages (`@types/node` `^20.19.0` → `^22.20.0`; the `node20-compat` release leg becomes `node22-compat` on Node 22). Node 20 is upstream EOL since 2026-04-30; Node 24 stays the CI default. This is the host-breaking support-matrix change that the unreleased 0.5.7 deliberately deferred to a minor.
|
|
25
|
+
- **Dependency refresh:** `pg` 8.23, `playwright-core` 1.63.0, `zod` ^4.6.2, `@ai-sdk/provider` 4.0.13 (`@ai-sdk/openai` 4.0.65 with a new supported-version matrix entry), `@office-open/*` 0.14.5 (adapters use the `*Sync` parse variants), `@biomejs/biome` 2.5.13, `@agentclientprotocol/sdk` exact-pinned 1.4.0, and the `@arnilo/prism-memory` `@nanonets/graft` peer range widened to `^0.16.0 || ^0.18.0`.
|
|
26
|
+
- **Examples are Node-20-floor safe:** four runnable examples used `import.meta.main` (Node ≥22.18/≥24.2) on a Node-20 floor; they now use the house `import.meta.url` + `process.argv[1]` guard.
|
|
27
|
+
- **Release gate:** lockstep cuts now require every internal `@arnilo/*` range to be the cut version exactly (`0.6.0` or `^0.6.0`) instead of merely satisfying it, so one release line cannot resolve two first-party minors.
|
|
28
|
+
- **Test/coverage tooling:** the npm test chain runs every stage through `scripts/run-all-tests.mjs` (no short-circuit, one summary), coverage discovery finds nested `dist/**/__tests__` in all 9 workspace packages, `coverage-thresholds.json` may no longer name retired packages, and protected legs use one blocked-gate shape (`scripts/blocked-gate.mjs`) with env **names** only in the release evidence.
|
|
29
|
+
- **Internal structure, additive-only surface:** `runtime/server/artifacts.ts` split into four modules, `enterprise/postgres/model-router.ts` split into a directory (`util`/`circuit`/`capacity`/`reservations`/`expiry`/`state-store`), MCP OAuth discovery extracted to `oauth-metadata.ts`, one abort-aware `Semaphore` shared across `@arnilo/prism-coding-tools` (sandbox error types preserved), shared upstream persona primitives in `@arnilo/prism-coding-tools/src/upstream/`, shared provider HTTP retry primitives (`@arnilo/prism-providers/src/shared/retry-http.ts`), and the `content.ts` ↔ `pinned-fetch.ts` ESM cycle broken via the leaf module `src/media-types.ts`. Compat baselines were regenerated (`--update-baseline`): **69 added declarations, zero removals** — the five new host-tunable surfaces plus the helpers the moved modules now export from their new files.
|
|
30
|
+
|
|
31
|
+
### Removed
|
|
32
|
+
- **`@arnilo/prism-office` optional `playwright-core` peer** (test-only: `/diagrams` drives a host-supplied iframe). Hosts no longer install a browser for office; see [`docs/peer-dependencies.md`](docs/peer-dependencies.md).
|
|
33
|
+
|
|
34
|
+
### Notes
|
|
35
|
+
- **Lockstep `0.5.6` → `0.6.0` bump:** all 10 publishable manifests move to `0.6.0` with internal ranges `^0.6.0`; the predecessor published release is **0.5.6** (0.5.7 was never published and is superseded by this cut).
|
|
36
|
+
- **Retired-gate and test-truth fixes** landed in the same window (plans 070/071): frozen-era version markers derive from the root manifest, retired doc paths resolve through the frozen lineage, the packed-consumer enterprise journey no longer inherits a Postgres env var (the `pg` peer is not installed in a packed consumer), the `sandbox-browser` workflow references live packages again, the secret scan is operator-independent (tracked files only), the LSP restart-budget test awaits the exhaustion transition instead of racing a write, and the release workflow tag lists carry every published tag.
|
|
37
|
+
|
|
38
|
+
## [0.5.6] - 2026-09-09 (plan 069)
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
- **Trusted extension activation**: `activateKernel(kernel)` / `ActivatedKernelConfig` turn a loaded extension kernel into ready-to-spread `AgentConfig` contributions (tools, skills, context, middleware, commands, instruction injectors). No auto-picked single-slot builders/providers.
|
|
42
|
+
- **CLI `--extension`**: repeatable flag loads trusted extension packages — cwd-relative paths (realpath-contained) or `PRISM_EXTENSION_ALLOWLIST` specifiers — and merges their contributions into the run. Modules must export `createExtension()`, a default function, or a default `{name, setup}` object; `--config`/`--resource`/`--tool` stay rejected.
|
|
43
|
+
- **Wiki ingest**: `ingestWikiSource`, `/wiki-ingest` command, `wiki_ingest` tool, and `prism-wiki ingest` stage one external source (text, file, image, PDF; URL via a host `fetchUrl` hook with `assertSsrfAllowedUrl` first) into `raw/ingest/<utc>-<slug>/` as an immutable `source.*` + UTF-8 `extract.md`, then return a Karpathy/OKF filing brief (`metadata.trust: "untrusted_external"`). Compressed PDF/DOCX ride an optional host `extractDocument` hook. Caps: 32 MiB input / 2 MiB extract.
|
|
44
|
+
- **Ingest filing protocol**: `wiki-maintainer` skill + scaffolded `SCHEMA.md` gained the ten-step ingest procedure (catalog-first, integrate-don't-duplicate, OKF v0.2 frontmatter with `sources[].id` footnotes, index/log sync, raw layer read-only, one source per ingest).
|
|
45
|
+
- **Graft graph commands**: `/graft-init` (non-interactive `graft init --no-global`, host `initAgents`/`initYes`), `/graft-build-deep` (`build --deep` with host-configured `deepModel` — provider/model/base-url on argv, `GRAFT_API_KEY` env-only, never on argv, fails closed unconfigured) alongside the keyless `/graft-build`. New `runGraftExit` exit-code runner (build/init are plain text, not JSON) with separate `buildBudgetMs` (120s) / `deepBuildBudgetMs` (600s) / `buildMaxResultBytes` (2 MiB) budgets.
|
|
46
|
+
|
|
1
47
|
## [0.5.5] - 2026-09-08
|
|
2
48
|
|
|
3
49
|
### Fixed
|
package/README.md
CHANGED
|
@@ -162,16 +162,16 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
|
|
|
162
162
|
|
|
163
163
|
| package | version | notes |
|
|
164
164
|
| --- | --- | --- |
|
|
165
|
-
| `@arnilo/prism` | 0.
|
|
166
|
-
| `@arnilo/prism-coding-tools` | 0.
|
|
167
|
-
| `@arnilo/prism-core` | 0.
|
|
168
|
-
| `@arnilo/prism-providers` | 0.
|
|
169
|
-
| `@arnilo/prism-acp-agent` | 0.
|
|
170
|
-
| `@arnilo/prism-ag-ui` | 0.
|
|
171
|
-
| `@arnilo/prism-mcp` | 0.
|
|
172
|
-
| `@arnilo/prism-memory` | 0.
|
|
173
|
-
| `@arnilo/prism-office` | 0.
|
|
174
|
-
| `@arnilo/prism-web-tools` | 0.
|
|
165
|
+
| `@arnilo/prism` | 0.6.0 | core — runtime, CLI/RPC, templates, docs |
|
|
166
|
+
| `@arnilo/prism-coding-tools` | 0.6.0 | family — /agent, /security, /document-reader, /openapi, /computer-use-linux, /dev, /caveman, /ponytail, /impeccable subpaths |
|
|
167
|
+
| `@arnilo/prism-core` | 0.6.0 | family — /runtime, /sessions, /governance, /credentials, /enterprise, /work, /validation subpaths |
|
|
168
|
+
| `@arnilo/prism-providers` | 0.6.0 | family — all provider adapters as `/<adapter>` subpaths |
|
|
169
|
+
| `@arnilo/prism-acp-agent` | 0.6.0 | capability — ACP adapter |
|
|
170
|
+
| `@arnilo/prism-ag-ui` | 0.6.0 | capability — AG-UI/A2A/A2UI adapter |
|
|
171
|
+
| `@arnilo/prism-mcp` | 0.6.0 | capability — MCP client/server/OAuth interop |
|
|
172
|
+
| `@arnilo/prism-memory` | 0.6.0 | capability — memory plus /rag, /compaction/*, /graft, /wiki subpaths |
|
|
173
|
+
| `@arnilo/prism-office` | 0.6.0 | capability — /documents, /sheets, /diagrams subpaths |
|
|
174
|
+
| `@arnilo/prism-web-tools` | 0.6.0 | capability — Brave/Exa/Firecrawl plus peer-gated /browser and /obscura subpaths |
|
|
175
175
|
<!-- generated:package-truth:inventory end -->
|
|
176
176
|
|
|
177
177
|
## Scripts
|
package/dist/agent-approval.js
CHANGED
|
@@ -37,8 +37,9 @@ export function assertValidAgentRunResume(resume) {
|
|
|
37
37
|
if (!Number.isSafeInteger(resume.expectedVersion) || resume.expectedVersion <= 0) {
|
|
38
38
|
throw invalid("Resume expectedVersion must be a positive safe integer");
|
|
39
39
|
}
|
|
40
|
+
const decisions = resume.decisions;
|
|
40
41
|
const hasDecision = resume.decision !== undefined;
|
|
41
|
-
const hasDecisions =
|
|
42
|
+
const hasDecisions = decisions !== undefined;
|
|
42
43
|
if (hasDecision && hasDecisions)
|
|
43
44
|
throw invalid("Resume accepts exactly one of decision or decisions");
|
|
44
45
|
if (!hasDecision && !hasDecisions)
|
|
@@ -49,7 +50,6 @@ export function assertValidAgentRunResume(resume) {
|
|
|
49
50
|
}
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
|
-
const decisions = resume.decisions;
|
|
53
53
|
if (!Array.isArray(decisions))
|
|
54
54
|
throw invalid("Decision batch must be an array");
|
|
55
55
|
if (decisions.length === 0)
|
|
@@ -242,10 +242,11 @@ export function decisionScopesEqual(a, b) {
|
|
|
242
242
|
return false;
|
|
243
243
|
if (a.actionConstraints === undefined || b.actionConstraints === undefined)
|
|
244
244
|
return a.actionConstraints === b.actionConstraints;
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
245
|
+
const aConstraints = a.actionConstraints;
|
|
246
|
+
const bConstraints = b.actionConstraints;
|
|
247
|
+
const keys = Object.keys(aConstraints);
|
|
248
|
+
return (keys.length === Object.keys(bConstraints).length &&
|
|
249
|
+
keys.every((key) => key in bConstraints && canonicalToolEffectJson(aConstraints[key]) === canonicalToolEffectJson(bConstraints[key])));
|
|
249
250
|
}
|
|
250
251
|
export function nestedOutcomeToolResult(outcome, toolCallId, name) {
|
|
251
252
|
return outcome.status === "completed"
|
package/dist/agent-loops.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AgentLoopStateError } from "./contracts.js";
|
|
2
2
|
import { createId } from "./ids.js";
|
|
3
3
|
import { inputMessages, toToolResultMessage } from "./input.js";
|
|
4
|
+
import { errorToErrorInfo } from "./redaction.js";
|
|
4
5
|
import { artifactStructuredOutputRequest, withoutStructuredOutput } from "./structured-output.js";
|
|
5
6
|
function throwIfAborted(signal) {
|
|
6
7
|
if (signal.aborted)
|
|
@@ -207,12 +208,13 @@ export function generateValidateReviseLoop(opts) {
|
|
|
207
208
|
: { ok: true, value: text };
|
|
208
209
|
// Parse failure consumes revision budget like a validation failure; the
|
|
209
210
|
// repairer receives `undefined` value plus a synthetic parse issue.
|
|
210
|
-
const
|
|
211
|
-
? { ok: false, errors: [{ path: "$", message: parsed.error ?? "artifact parse failed" }], metadata: { reason: "parse_error" } }
|
|
212
|
-
: undefined;
|
|
211
|
+
const candidate = parsed.ok && parsed.value !== undefined ? { ok: true, value: parsed.value } : undefined;
|
|
213
212
|
const attempt = ++attempts;
|
|
214
213
|
ctx.emit({ type: "artifact_validation_started", sessionId: ctx.sessionId, runId: ctx.runId, turn, attempt });
|
|
215
|
-
const result =
|
|
214
|
+
const result = candidate
|
|
215
|
+
? await opts.validator(candidate.value, artifactCtx)
|
|
216
|
+
: { ok: false, errors: [{ path: "$", message: parsed.error ?? "artifact parse failed" }], metadata: { reason: "parse_error" } };
|
|
217
|
+
const parseFailure = candidate ? undefined : result;
|
|
216
218
|
ctx.emit({ type: "artifact_validation_finished", sessionId: ctx.sessionId, runId: ctx.runId, turn, attempt, result });
|
|
217
219
|
if (result.ok) {
|
|
218
220
|
ctx.emit({ type: "artifact_finished", sessionId: ctx.sessionId, runId: ctx.runId, turn, attempt, result });
|
|
@@ -271,39 +273,76 @@ export async function dispatchToolCallsInOrder(calls, ctx) {
|
|
|
271
273
|
let nextIndex = 0;
|
|
272
274
|
let stopped = false;
|
|
273
275
|
let firstFailure;
|
|
274
|
-
|
|
276
|
+
let failureIndex = -1;
|
|
277
|
+
const recordFailure = (error, index) => {
|
|
275
278
|
if (stopped)
|
|
276
279
|
return;
|
|
277
280
|
stopped = true;
|
|
278
281
|
firstFailure = error;
|
|
282
|
+
failureIndex = index;
|
|
279
283
|
};
|
|
280
284
|
const workers = Array.from({ length: concurrency }, async () => {
|
|
281
285
|
for (;;) {
|
|
282
286
|
if (stopped)
|
|
283
287
|
return;
|
|
288
|
+
let index = -1;
|
|
284
289
|
try {
|
|
285
290
|
throwIfAborted(ctx.signal);
|
|
286
|
-
|
|
291
|
+
index = nextIndex;
|
|
287
292
|
nextIndex += 1;
|
|
288
293
|
if (index >= calls.length)
|
|
289
294
|
return;
|
|
290
295
|
results[index] = await ctx.dispatchToolCall(calls[index]);
|
|
291
296
|
}
|
|
292
297
|
catch (error) {
|
|
293
|
-
recordFailure(error);
|
|
298
|
+
recordFailure(error, index >= 0 && index < calls.length ? index : -1);
|
|
294
299
|
return;
|
|
295
300
|
}
|
|
296
301
|
}
|
|
297
302
|
});
|
|
298
303
|
await Promise.allSettled(workers);
|
|
304
|
+
// Persist before rethrowing: a stopped batch must not leave the branch with `tool_call`
|
|
305
|
+
// ids that never got a `tool_result` (providers reject that history on the next turn).
|
|
306
|
+
// ponytail: synthetic rows carry `errorToErrorInfo` output only — no cause chain; the
|
|
307
|
+
// store redacts entries again at `appendEntry`, and `ctx.history` mirrors the run error.
|
|
308
|
+
const claimed = Math.min(nextIndex, calls.length);
|
|
309
|
+
for (let index = 0; index < calls.length; index += 1) {
|
|
310
|
+
const result = results[index];
|
|
311
|
+
if (result) {
|
|
312
|
+
await appendToolResultMessage(result, ctx);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
// Suspension/loop-state errors are not tool outcomes: their machinery appends the real
|
|
316
|
+
// result on resume, so a synthetic row here would duplicate it.
|
|
317
|
+
if (index === failureIndex && isRunControlError(firstFailure))
|
|
318
|
+
continue;
|
|
319
|
+
const call = calls[index];
|
|
320
|
+
if (call === undefined)
|
|
321
|
+
continue;
|
|
322
|
+
await appendToolResultMessage(syntheticFailureResult(call, index < claimed ? firstFailure : undefined), ctx);
|
|
323
|
+
}
|
|
299
324
|
if (stopped)
|
|
300
325
|
throw firstFailure;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
326
|
+
}
|
|
327
|
+
/** Run-level control errors rethrown by `dispatchToolCall` (mirrors `src/tools.ts`). */
|
|
328
|
+
function isRunControlError(error) {
|
|
329
|
+
const code = error?.code;
|
|
330
|
+
return (typeof code === "string" &&
|
|
331
|
+
(code === "ERR_PRISM_AGENT_RUN_SUSPENDED" || code === "ERR_PRISM_DELEGATION_SUSPENDED" || code.startsWith("ERR_PRISM_LOOP_")));
|
|
332
|
+
}
|
|
333
|
+
function syntheticFailureResult(call, failure) {
|
|
334
|
+
if (failure === undefined) {
|
|
335
|
+
return {
|
|
336
|
+
toolCallId: call.id,
|
|
337
|
+
name: call.name,
|
|
338
|
+
error: {
|
|
339
|
+
code: "tool_call_not_dispatched",
|
|
340
|
+
message: "Tool call was not dispatched: the batch stopped after an earlier call failed or the run was aborted.",
|
|
341
|
+
},
|
|
342
|
+
};
|
|
306
343
|
}
|
|
344
|
+
const info = errorToErrorInfo(failure);
|
|
345
|
+
return { toolCallId: call.id, name: call.name, error: { name: info.name, message: info.message, code: info.code } };
|
|
307
346
|
}
|
|
308
347
|
async function appendToolResultMessage(result, ctx) {
|
|
309
348
|
// Approval-gated calls return a marker instead of a real result; the transcript must not
|
|
@@ -60,6 +60,7 @@ export declare class RuntimeAgentSession implements AgentSession {
|
|
|
60
60
|
private ledgerFailure;
|
|
61
61
|
private snapshotGeneration;
|
|
62
62
|
private snapshotCache?;
|
|
63
|
+
private readonly snapshotCacheTtlMs;
|
|
63
64
|
constructor(config: AgentSessionConfig & {
|
|
64
65
|
readonly agent: Agent;
|
|
65
66
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** session (0.2.5 plan 025 Task 1 split). Moved verbatim from agent-session.ts; public surface unchanged behind the barrel. */
|
|
2
2
|
import { policyList } from "../agent-tool-dispatch.js";
|
|
3
3
|
import { createDefaultCompactionStrategy, isCompactionEntryData } from "../compaction.js";
|
|
4
|
-
import { DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS } from "../contracts.js";
|
|
4
|
+
import { DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, DEFAULT_SNAPSHOT_CACHE_TTL_MS, HARD_MAX_SNAPSHOT_CACHE_TTL_MS, } from "../contracts.js";
|
|
5
5
|
import { GuardrailError, runGuardrails } from "../guardrails.js";
|
|
6
6
|
import { applyDefaultProviderRequestOptions, createProviderRequestPolicyChain, normalizeProviderRequestPolicyResult, } from "../provider-request-policy.js";
|
|
7
7
|
import { redactAgentEvent, redactProviderRequest, redactRunLedgerRecord, redactSecrets, redactSessionEntry } from "../redaction.js";
|
|
@@ -78,12 +78,14 @@ export class RuntimeAgentSession {
|
|
|
78
78
|
ledgerFailure;
|
|
79
79
|
snapshotGeneration = 0;
|
|
80
80
|
snapshotCache;
|
|
81
|
+
snapshotCacheTtlMs;
|
|
81
82
|
constructor(config) {
|
|
82
83
|
this.id = config.id ?? randomId("session");
|
|
83
84
|
this.agent = config.agent;
|
|
84
85
|
this.metadata = config.metadata;
|
|
85
86
|
this.store = config.store ?? config.agent.config.store ?? createMemorySessionStore();
|
|
86
87
|
this.currentLeafId = config.leafId;
|
|
88
|
+
this.snapshotCacheTtlMs = resolveSnapshotCacheTtlMs(config.snapshotCacheTtlMs);
|
|
87
89
|
}
|
|
88
90
|
get leafId() {
|
|
89
91
|
return this.currentLeafId;
|
|
@@ -470,8 +472,24 @@ export class RuntimeAgentSession {
|
|
|
470
472
|
const value = reader
|
|
471
473
|
? await rebuildSessionContext(reader, { sessionId: this.id, leafId: this.currentLeafId })
|
|
472
474
|
: rebuildSessionContext(await this.store.list(this.id), { leafId: this.currentLeafId });
|
|
473
|
-
this.snapshotCache = {
|
|
475
|
+
this.snapshotCache = {
|
|
476
|
+
leafId: this.currentLeafId,
|
|
477
|
+
generation: this.snapshotGeneration,
|
|
478
|
+
expiresAt: now + this.snapshotCacheTtlMs,
|
|
479
|
+
value,
|
|
480
|
+
};
|
|
474
481
|
return value;
|
|
475
482
|
}
|
|
476
483
|
}
|
|
484
|
+
/**
|
|
485
|
+
* `snapshotCacheTtlMs` resolution: `0` disables the branch cache (a host that needs a
|
|
486
|
+
* fresh store read per snapshot), otherwise a safe integer up to the hard cap.
|
|
487
|
+
*/
|
|
488
|
+
function resolveSnapshotCacheTtlMs(value) {
|
|
489
|
+
const ttl = value ?? DEFAULT_SNAPSHOT_CACHE_TTL_MS;
|
|
490
|
+
if (!Number.isSafeInteger(ttl) || ttl < 0 || ttl > HARD_MAX_SNAPSHOT_CACHE_TTL_MS) {
|
|
491
|
+
throw new TypeError(`AgentSessionConfig.snapshotCacheTtlMs must be a safe integer from 0 to ${HARD_MAX_SNAPSHOT_CACHE_TTL_MS}`);
|
|
492
|
+
}
|
|
493
|
+
return ttl;
|
|
494
|
+
}
|
|
477
495
|
//# sourceMappingURL=session.js.map
|
|
@@ -51,8 +51,9 @@ export async function validateElicitationPayload(agent, state, target, payload,
|
|
|
51
51
|
// Tool-declared answer-shape validation, re-derived from the current registry (never persisted).
|
|
52
52
|
const call = state.pendingCalls?.find((entry) => entry.call.id === target.toolCallId)?.call;
|
|
53
53
|
const tool = call ? activeTools(agent.config.tools).registry.get(call.name) : undefined;
|
|
54
|
-
const
|
|
55
|
-
|
|
54
|
+
const elicitation = tool?.elicitation;
|
|
55
|
+
const validate = elicitation && call
|
|
56
|
+
? safeToolElicitationValidate(elicitation, call.arguments, {
|
|
56
57
|
sessionId: state.sessionId,
|
|
57
58
|
runId: state.runId,
|
|
58
59
|
toolCallId: target.toolCallId ?? "elicitation",
|
|
@@ -68,9 +69,9 @@ export async function validateElicitationPayload(agent, state, target, payload,
|
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
|
-
function safeToolElicitationValidate(
|
|
72
|
+
function safeToolElicitationValidate(elicitation, args, context) {
|
|
72
73
|
try {
|
|
73
|
-
return
|
|
74
|
+
return elicitation(args, context)?.validate;
|
|
74
75
|
}
|
|
75
76
|
catch {
|
|
76
77
|
return undefined;
|
package/dist/cli-runner.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Readable, Writable } from "node:stream";
|
|
2
2
|
import type { AgentSession, ContributionFileKind, InstructionInjector, Skill, SystemPromptContribution } from "./contracts.js";
|
|
3
|
+
import { type ActivatedKernelConfig } from "./index.js";
|
|
3
4
|
import type { AgentBundle } from "./node/agent-definitions.js";
|
|
4
5
|
import { type RpcSessionFactory } from "./rpc.js";
|
|
5
6
|
export type CliMode = "print" | "json" | "rpc";
|
|
@@ -35,6 +36,12 @@ export interface CliOptions {
|
|
|
35
36
|
readonly injectorFiles: readonly string[];
|
|
36
37
|
/** Runtime-populated: `--instruction`/`--injector-file` resolved to live injectors. */
|
|
37
38
|
readonly resolvedInstructionInjectors: readonly InstructionInjector[];
|
|
39
|
+
/** Parsed flag: `--extension <specifier>` (repeatable). Loaded via {@link loadCliExtensions}
|
|
40
|
+
* into {@link CliOptions.activatedExtensions} before session creation. */
|
|
41
|
+
readonly extensions: readonly string[];
|
|
42
|
+
/** Runtime-populated (not a parsed flag): activated contributions from `--extension` modules
|
|
43
|
+
* (`kernel.load()` → `activateKernel()`). Merged into `createAgent()` by `agentSession`. */
|
|
44
|
+
readonly activatedExtensions?: ActivatedKernelConfig;
|
|
38
45
|
/** Parsed flag: `--no-agents-md` / `--no-system-md` skip the corresponding auto-load. */
|
|
39
46
|
readonly noAgentsMd: boolean;
|
|
40
47
|
readonly noSystemMd: boolean;
|
|
@@ -71,7 +78,7 @@ export interface CliRuntime {
|
|
|
71
78
|
/** Test injection for `prism dev`: overrides `@arnilo/prism-dev` resolution. */
|
|
72
79
|
readonly loadDevCli?: () => Promise<unknown>;
|
|
73
80
|
}
|
|
74
|
-
export declare const usage = "Usage: prism [--mode print|json|rpc] [-p prompt] [options]\n prism init <dir> [--template <name>] [--list-templates] [--provider <name>] [--with-workflows] [--with-evals] [--force]\n prism providers add <name> [--base-url <url>] [--env-key <name>] [--model <id>] [--force]\n prism dev [--port <n>] [--host <addr>] (loopback inspector; delegates into @arnilo/prism-dev)\n\n\nOptions:\n -p, --prompt <text> Prompt to run in print/json mode\n --provider <name> Provider id from the init provider catalog ('mock' is built in;\n real providers need their @arnilo/prism-providers package +\n credential env var)\n --model <name> Explicit model name\n --session <id> Session id\n --system <text> System instructions\n --context <text> Context text\n --compact <entries> Auto-compaction threshold\n --max-tool-rounds <n> Maximum tool rounds\n --discover Enable workspace contribution discovery (opt-in)\n --discover-kinds <csv> Kinds to discover (default: skill; skill,tool,context,instructions)\n --no-discovery Disable discovery even if --discover is set\n --agents-config <path> App config root holding agents/<name>/AGENT.md bundles (opt-in)\n --no-agents-md Skip auto-loading <workspaceRoot>/AGENTS.md\n --no-system-md Skip auto-loading the global SYSTEM.md layer\n --agents-md-file <path> Read AGENTS.md from <path> instead (trust-gated, source: app)\n --system-md-file <path> Read SYSTEM.md from <path> instead (source: user)\n -h, --help Show this help\n";
|
|
81
|
+
export declare const usage = "Usage: prism [--mode print|json|rpc] [-p prompt] [options]\n prism init <dir> [--template <name>] [--list-templates] [--provider <name>] [--with-workflows] [--with-evals] [--force]\n prism providers add <name> [--base-url <url>] [--env-key <name>] [--model <id>] [--force]\n prism dev [--port <n>] [--host <addr>] (loopback inspector; delegates into @arnilo/prism-dev)\n\n\nOptions:\n -p, --prompt <text> Prompt to run in print/json mode\n --provider <name> Provider id from the init provider catalog ('mock' is built in;\n real providers need their @arnilo/prism-providers package +\n credential env var)\n --model <name> Explicit model name\n --session <id> Session id\n --system <text> System instructions\n --context <text> Context text\n --compact <entries> Auto-compaction threshold\n --max-tool-rounds <n> Maximum tool rounds\n --discover Enable workspace contribution discovery (opt-in)\n --discover-kinds <csv> Kinds to discover (default: skill; skill,tool,context,instructions)\n --no-discovery Disable discovery even if --discover is set\n --extension <specifier> Load a trusted extension module (repeatable). Relative paths load\n from the working directory; package names and absolute paths must\n be listed in PRISM_EXTENSION_ALLOWLIST (comma-separated).\n --agents-config <path> App config root holding agents/<name>/AGENT.md bundles (opt-in)\n --no-agents-md Skip auto-loading <workspaceRoot>/AGENTS.md\n --no-system-md Skip auto-loading the global SYSTEM.md layer\n --agents-md-file <path> Read AGENTS.md from <path> instead (trust-gated, source: app)\n --system-md-file <path> Read SYSTEM.md from <path> instead (source: user)\n -h, --help Show this help\n";
|
|
75
82
|
export declare function parseCliArgs(argv: readonly string[]): CliOptions;
|
|
76
83
|
export declare function runCli(argv: readonly string[], runtime: CliRuntime): Promise<number>;
|
|
77
84
|
export declare function runPromptMode(session: AgentSession, options: CliOptions, stdout: Writable, mode: "print" | "json"): Promise<void>;
|
package/dist/cli-runner.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { basename, dirname } from "node:path";
|
|
1
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, isAbsolute, resolve, sep } from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
4
5
|
import { runPrismDevSubcommand } from "./cli-dev.js";
|
|
5
6
|
import { initUsage, loadProvidersCatalog, runInitCommand } from "./cli-init.js";
|
|
6
7
|
import { providerAddUsage, runProviderAddCommand } from "./cli-provider-add.js";
|
|
7
8
|
import { createContributionRegistries, registerDiscoveredContributions } from "./contributions.js";
|
|
8
|
-
import { createAgent, createContributionRegistry, createMockProvider, providerDone, providerTextDelta, resolveInstructionInjectors, } from "./index.js";
|
|
9
|
+
import { activateKernel, createAgent, createContributionRegistry, createExtensionKernel, createMockProvider, providerDone, providerTextDelta, resolveInstructionInjectors, } from "./index.js";
|
|
9
10
|
import { discoverAgentBundles } from "./node/agent-definitions.js";
|
|
10
11
|
import { discoverContributions } from "./node/contribution-discovery.js";
|
|
11
12
|
import { registerDiscoveredInstructionInjectors } from "./node/instruction-injectors.js";
|
|
@@ -33,6 +34,9 @@ Options:
|
|
|
33
34
|
--discover Enable workspace contribution discovery (opt-in)
|
|
34
35
|
--discover-kinds <csv> Kinds to discover (default: skill; skill,tool,context,instructions)
|
|
35
36
|
--no-discovery Disable discovery even if --discover is set
|
|
37
|
+
--extension <specifier> Load a trusted extension module (repeatable). Relative paths load
|
|
38
|
+
from the working directory; package names and absolute paths must
|
|
39
|
+
be listed in PRISM_EXTENSION_ALLOWLIST (comma-separated).
|
|
36
40
|
--agents-config <path> App config root holding agents/<name>/AGENT.md bundles (opt-in)
|
|
37
41
|
--no-agents-md Skip auto-loading <workspaceRoot>/AGENTS.md
|
|
38
42
|
--no-system-md Skip auto-loading the global SYSTEM.md layer
|
|
@@ -56,11 +60,12 @@ const valueFlags = new Set([
|
|
|
56
60
|
"--agents-md-file",
|
|
57
61
|
"--system-md-file",
|
|
58
62
|
"--agents-config",
|
|
63
|
+
"--extension",
|
|
59
64
|
]);
|
|
60
65
|
const boolFlags = new Set(["--discover", "--no-discovery", "--no-agents-md", "--no-system-md"]);
|
|
61
66
|
// Known-but-inert flags: parsed by earlier builds, never wired to any behavior.
|
|
62
67
|
// Rejected loudly (rather than silently ignored) until a CLI-harness plan wires them.
|
|
63
|
-
const unsupportedFlags = new Set(["--config", "--resource", "--
|
|
68
|
+
const unsupportedFlags = new Set(["--config", "--resource", "--tool"]);
|
|
64
69
|
const ALL_KINDS = ["skill", "tool", "context", "instructions"];
|
|
65
70
|
export function parseCliArgs(argv) {
|
|
66
71
|
let mode = "print";
|
|
@@ -83,6 +88,7 @@ export function parseCliArgs(argv) {
|
|
|
83
88
|
const context = [];
|
|
84
89
|
const instructions = [];
|
|
85
90
|
const injectorFiles = [];
|
|
91
|
+
const extensions = [];
|
|
86
92
|
for (let i = 0; i < argv.length; i += 1) {
|
|
87
93
|
const flag = argv[i];
|
|
88
94
|
if (flag === "-h" || flag === "--help") {
|
|
@@ -163,6 +169,9 @@ export function parseCliArgs(argv) {
|
|
|
163
169
|
case "--agents-config":
|
|
164
170
|
agentsConfig = value;
|
|
165
171
|
break;
|
|
172
|
+
case "--extension":
|
|
173
|
+
extensions.push(value);
|
|
174
|
+
break;
|
|
166
175
|
}
|
|
167
176
|
}
|
|
168
177
|
return {
|
|
@@ -180,6 +189,7 @@ export function parseCliArgs(argv) {
|
|
|
180
189
|
discoverKinds,
|
|
181
190
|
noDiscovery,
|
|
182
191
|
agentsConfig,
|
|
192
|
+
extensions,
|
|
183
193
|
discoveredSkills: [],
|
|
184
194
|
discoveredInjectors: [],
|
|
185
195
|
discoveredAgents: [],
|
|
@@ -317,6 +327,13 @@ export async function runCli(argv, runtime) {
|
|
|
317
327
|
});
|
|
318
328
|
options = { ...options, systemPromptLayers: layers };
|
|
319
329
|
}
|
|
330
|
+
// ponytail: 069 — --extension imports trusted modules only (cwd-contained relative paths,
|
|
331
|
+
// or allow-listed package/absolute specifiers), then activates their array contributions
|
|
332
|
+
// into the CLI agent. Throw policy: a broken extension is a usage error, not a silent skip.
|
|
333
|
+
if (options.extensions.length > 0) {
|
|
334
|
+
const activatedExtensions = await loadCliExtensions(options.extensions);
|
|
335
|
+
options = { ...options, activatedExtensions };
|
|
336
|
+
}
|
|
320
337
|
const session = await (runtime.createSession ?? defaultCreateSession)(options);
|
|
321
338
|
await runPromptMode(session, options, runtime.stdout, mode);
|
|
322
339
|
return 0;
|
|
@@ -386,6 +403,8 @@ function mockSession(options) {
|
|
|
386
403
|
return agentSession({ ...options, providerInstance: createMockProvider([providerTextDelta("Hello"), providerDone()]), modelConfig });
|
|
387
404
|
}
|
|
388
405
|
function agentSession(options) {
|
|
406
|
+
const activated = options.activatedExtensions;
|
|
407
|
+
const skills = activated ? [...options.discoveredSkills, ...activated.skills] : options.discoveredSkills;
|
|
389
408
|
return createAgent({
|
|
390
409
|
model: options.modelConfig,
|
|
391
410
|
provider: options.providerInstance,
|
|
@@ -393,11 +412,13 @@ function agentSession(options) {
|
|
|
393
412
|
// ponytail: Phase 31 — file layers compose with `instructions` (base) via the existing
|
|
394
413
|
// composeSystemPrompt pipeline; rank order (user<package<app<run) is enforced inside.
|
|
395
414
|
...(options.systemPromptLayers.length > 0 ? { systemPrompt: options.systemPromptLayers } : {}),
|
|
396
|
-
// ponytail: discovered skills become selectable via RunOptions.activeSkills (set by runOptions below).
|
|
397
|
-
...(
|
|
415
|
+
// ponytail: discovered/extension skills become selectable via RunOptions.activeSkills (set by runOptions below).
|
|
416
|
+
...(skills.length > 0 ? { skills: createSkillRegistry(skills) } : {}),
|
|
417
|
+
...(activated ? { tools: activated.tools, context: activated.context, middleware: activated.middleware } : {}),
|
|
398
418
|
}).createSession({ id: options.session });
|
|
399
419
|
}
|
|
400
420
|
function runOptions(options) {
|
|
421
|
+
const injectors = [...(options.activatedExtensions?.instructionInjectors ?? []), ...options.resolvedInstructionInjectors];
|
|
401
422
|
return {
|
|
402
423
|
...(options.maxToolRounds !== undefined ? { limits: { maxToolRounds: options.maxToolRounds } } : {}),
|
|
403
424
|
compaction: options.compact ? { thresholdEntries: options.compact } : undefined,
|
|
@@ -405,7 +426,7 @@ function runOptions(options) {
|
|
|
405
426
|
...(options.discover && !options.noDiscovery && options.discoveredSkills.length > 0
|
|
406
427
|
? { activeSkills: options.discoveredSkills.map((s) => s.name) }
|
|
407
428
|
: {}),
|
|
408
|
-
...(
|
|
429
|
+
...(injectors.length > 0 ? { instructionInjectors: injectors } : {}),
|
|
409
430
|
};
|
|
410
431
|
}
|
|
411
432
|
// ponytail: resolve --instruction names against discovered injectors (fail-closed) and load
|
|
@@ -432,6 +453,75 @@ function positiveInt(value, flag) {
|
|
|
432
453
|
throw new CliUsageError(`Invalid value for ${flag}: ${value}`);
|
|
433
454
|
return parsed;
|
|
434
455
|
}
|
|
456
|
+
/** Load `--extension` specifiers into one kernel and activate it. Trust model:
|
|
457
|
+
* the loaded code is trusted host code (same level as the provider factory
|
|
458
|
+
* import in `defaultCreateSession`) — the gates decide WHICH code may load,
|
|
459
|
+
* not sandbox what loaded code does.
|
|
460
|
+
* - Relative `./`/`../` paths: no allow-list needed, but must `realpath`
|
|
461
|
+
* contain inside the working directory (symlinks cannot escape).
|
|
462
|
+
* - Bare package names and absolute paths: exact match in
|
|
463
|
+
* `PRISM_EXTENSION_ALLOWLIST` (comma-separated), evaluated before `import()`.
|
|
464
|
+
* Accepted module shapes: `createExtension()` export, a default function, or
|
|
465
|
+
* a default `{ name, setup }` Extension object. */
|
|
466
|
+
async function loadCliExtensions(specifiers) {
|
|
467
|
+
const allowList = (process.env.PRISM_EXTENSION_ALLOWLIST ?? "")
|
|
468
|
+
.split(",")
|
|
469
|
+
.map((entry) => entry.trim())
|
|
470
|
+
.filter(Boolean);
|
|
471
|
+
const kernel = createExtensionKernel({ errorPolicy: "throw" });
|
|
472
|
+
const extensions = [];
|
|
473
|
+
for (const specifier of specifiers) {
|
|
474
|
+
extensions.push(await importTrustedExtension(specifier, allowList));
|
|
475
|
+
}
|
|
476
|
+
await kernel.load(extensions);
|
|
477
|
+
return activateKernel(kernel);
|
|
478
|
+
}
|
|
479
|
+
async function importTrustedExtension(specifier, allowList) {
|
|
480
|
+
if (specifier.includes("\0"))
|
|
481
|
+
throw new CliUsageError(`Invalid value for --extension: ${specifier}`);
|
|
482
|
+
const relative = specifier.startsWith("./") || specifier.startsWith("../");
|
|
483
|
+
let moduleSpecifier;
|
|
484
|
+
if (relative) {
|
|
485
|
+
const real = await realpath(resolve(specifier)).catch(() => {
|
|
486
|
+
throw new CliUsageError(`--extension "${specifier}" does not exist`);
|
|
487
|
+
});
|
|
488
|
+
const realCwd = await realpath(process.cwd());
|
|
489
|
+
if (real !== realCwd && !real.startsWith(realCwd + sep)) {
|
|
490
|
+
throw new CliUsageError(`--extension "${specifier}" escapes the working directory`);
|
|
491
|
+
}
|
|
492
|
+
moduleSpecifier = pathToFileURL(real).href;
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
if (!allowList.includes(specifier)) {
|
|
496
|
+
throw new CliUsageError(`--extension "${specifier}" is not in PRISM_EXTENSION_ALLOWLIST (package names and absolute paths require it; relative extension paths need ./ or ../)`);
|
|
497
|
+
}
|
|
498
|
+
moduleSpecifier = isAbsolute(specifier) ? pathToFileURL(specifier).href : specifier;
|
|
499
|
+
}
|
|
500
|
+
let mod;
|
|
501
|
+
try {
|
|
502
|
+
mod = await import(moduleSpecifier);
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
throw new CliUsageError(`--extension "${specifier}" failed to load: ${error instanceof Error ? error.message : String(error)}`);
|
|
506
|
+
}
|
|
507
|
+
return extensionFromModule(mod, specifier);
|
|
508
|
+
}
|
|
509
|
+
function extensionFromModule(mod, specifier) {
|
|
510
|
+
const record = mod;
|
|
511
|
+
if (typeof record.createExtension === "function")
|
|
512
|
+
return record.createExtension();
|
|
513
|
+
if (typeof record.default === "function")
|
|
514
|
+
return record.default();
|
|
515
|
+
if (isExtensionObject(record.default))
|
|
516
|
+
return record.default;
|
|
517
|
+
throw new CliUsageError(`--extension "${specifier}" must export createExtension(), a default function, or a default { name, setup } extension`);
|
|
518
|
+
}
|
|
519
|
+
function isExtensionObject(value) {
|
|
520
|
+
return (typeof value === "object" &&
|
|
521
|
+
value !== null &&
|
|
522
|
+
typeof value.name === "string" &&
|
|
523
|
+
typeof value.setup === "function");
|
|
524
|
+
}
|
|
435
525
|
function write(stream, text) {
|
|
436
526
|
stream.write(text);
|
|
437
527
|
}
|
package/dist/content.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { ContentBlock, ImageContent, Message, ModelConfig, ResourceLoadContext, ResourceLoader, VideoContent } from "./contracts.js";
|
|
2
|
+
import { type MediaHostAddress, type MediaHostnameResolver, type SsrfPolicy } from "./media-types.js";
|
|
3
|
+
export type { MediaHostAddress, MediaHostnameResolver, SsrfPolicy } from "./media-types.js";
|
|
4
|
+
export { assertSsrfAllowedUrl, MediaContentError } from "./media-types.js";
|
|
2
5
|
/** Known model input capability tags for `ModelCapabilities.input`. */
|
|
3
6
|
export declare const MODEL_INPUT_CAPABILITIES: readonly ["text", "image", "audio", "file", "document", "video"];
|
|
4
7
|
export type ModelInputCapability = (typeof MODEL_INPUT_CAPABILITIES)[number];
|
|
@@ -53,21 +56,10 @@ export interface MediaContentBounds {
|
|
|
53
56
|
readonly maxAudioDurationMs?: number;
|
|
54
57
|
readonly fetchTimeoutMs?: number;
|
|
55
58
|
}
|
|
56
|
-
export interface SsrfPolicy {
|
|
57
|
-
/** When true (default), deny private/link-local/metadata hostnames and IPs. */
|
|
58
|
-
readonly denyPrivateHosts?: boolean;
|
|
59
|
-
/** Optional hostname allow-list. When set, only listed hosts are permitted. */
|
|
60
|
-
readonly allowedHostnames?: readonly string[];
|
|
61
|
-
}
|
|
62
59
|
export interface MediaMimePolicy {
|
|
63
60
|
/** Reject when magic bytes disagree with declared media type. Default `true`. */
|
|
64
61
|
readonly strictMagicValidation?: boolean;
|
|
65
62
|
}
|
|
66
|
-
export interface MediaHostAddress {
|
|
67
|
-
readonly address: string;
|
|
68
|
-
readonly family: 4 | 6;
|
|
69
|
-
}
|
|
70
|
-
export type MediaHostnameResolver = (hostname: string, signal: AbortSignal) => Promise<readonly MediaHostAddress[]>;
|
|
71
63
|
export interface MediaUrlRequest {
|
|
72
64
|
readonly url: URL;
|
|
73
65
|
readonly address: MediaHostAddress;
|
|
@@ -103,16 +95,11 @@ export declare class UnsupportedModalityError extends Error {
|
|
|
103
95
|
readonly model: string;
|
|
104
96
|
constructor(modality: ModelInputCapability, model: ModelConfig);
|
|
105
97
|
}
|
|
106
|
-
export declare class MediaContentError extends Error {
|
|
107
|
-
readonly code: "ambiguous_source" | "missing_source" | "item_too_large" | "request_too_large" | "too_many_items" | "audio_too_long" | "invalid_base64" | "ssrf_denied" | "redirect" | "fetch_failed" | "fetch_timeout" | "resource_required" | "mime_mismatch" | "unsupported_url_scheme";
|
|
108
|
-
constructor(code: MediaContentError["code"], message: string, options?: ErrorOptions);
|
|
109
|
-
}
|
|
110
98
|
export declare function contentBlockInputModality(block: ContentBlock): ModelInputCapability | undefined;
|
|
111
99
|
export declare function collectMessageContentBlocks(messages: readonly Message[]): ContentBlock[];
|
|
112
100
|
export declare function assertModelSupportsContentBlocks(model: ModelConfig, blocks: readonly ContentBlock[]): void;
|
|
113
101
|
export declare function assertMessagesSupportModelCapabilities(model: ModelConfig, messages: readonly Message[]): void;
|
|
114
102
|
export declare function assertMediaBlocksWithinBounds(blocks: readonly MediaContentBlock[], bounds?: MediaContentBounds): void;
|
|
115
|
-
export declare function assertSsrfAllowedUrl(url: string, policy?: SsrfPolicy): void;
|
|
116
103
|
export declare function sniffMediaMimeType(bytes: Uint8Array): string | undefined;
|
|
117
104
|
export declare function assertDeclaredMediaTypeMatches(declaredMediaType: string, bytes: Uint8Array, policy?: MediaMimePolicy): void;
|
|
118
105
|
export declare function resolveMediaContentBlock(block: MediaContentBlock, options?: ResolveMediaContentOptions): Promise<ResolvedMediaContent>;
|