@arnilo/prism 0.1.6 → 0.2.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 +10 -0
- package/dist/agent-approval.d.ts +10 -1
- package/dist/agent-approval.js +81 -0
- package/dist/agent-run-lifecycle.js +6 -8
- package/dist/cache-telemetry.d.ts +58 -0
- package/dist/cache-telemetry.js +102 -0
- package/dist/cli-provider-add.d.ts +37 -0
- package/dist/cli-provider-add.js +293 -0
- package/dist/cli-runner.d.ts +5 -1
- package/dist/cli-runner.js +13 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/docs/agent-session-runtime.md +2 -0
- package/docs/cli-rpc.md +33 -0
- package/docs/coding-security.md +27 -6
- package/docs/host-security.md +1 -1
- package/docs/index.md +7 -7
- package/docs/migration.md +25 -0
- package/docs/model-routing.md +45 -0
- package/docs/provider-caching.md +63 -0
- package/docs/provider-packages.md +2 -0
- package/docs/public-contracts.md +1 -1
- package/docs/release-and-install.md +43 -0
- package/docs/work-tools.md +24 -1
- package/package.json +3 -3
- package/templates/provider/CHANGELOG.md.tmpl +5 -0
- package/templates/provider/README.md.tmpl +41 -0
- package/templates/provider/docs/providers/NAME.md.tmpl +61 -0
- package/templates/provider/package.json.tmpl +49 -0
- package/templates/provider/src/cache.ts.tmpl +20 -0
- package/templates/provider/src/index.ts.tmpl +33 -0
- package/templates/provider/src/models.ts.tmpl +16 -0
- package/templates/provider/src/provider.ts.tmpl +23 -0
- package/templates/provider/src/tests/provider.test.ts.tmpl +104 -0
- package/templates/provider/tsconfig.json.tmpl +16 -0
|
@@ -274,6 +274,49 @@ git push origin v0.1.2 # tag push triggers release.yml publish job (prove
|
|
|
274
274
|
|
|
275
275
|
**Rollback notes.** `release:publish --version 0.1.2 --resume --report release-artifacts/publish-report.json` resumes an interrupted publication and skips only registry versions whose internal dependency fingerprint matches the local manifest. A failed package aborts the run with its status written to the report; re-run after fixing the cause. npm cannot unpublish the `0.1.2` line after 72 hours — a post-publication defect ships as a `0.1.x` patch (additive-only compat promise, `release:gate` enforced), or as a documented break in the next line with a `docs/migration.md` entry. `0.1.2` is store-compatible with `0.1.1` in **both directions** (no migration ran — same checksum-protected contract), so an operator may defer or roll back the patch without a database rollback.
|
|
276
276
|
|
|
277
|
+
### 0.2.0 publish handoff (plan 020 Task 6)
|
|
278
|
+
|
|
279
|
+
**Decision: GO when the operator prerequisites below are recorded.** Release **0.2.0** (plan 020) is the first cut of the 0.2.x review-remediation line — fail-closed runtime and sandbox security. API surface **additive-only** vs 0.1.7 (plain compat gate at 0.2.0: 0 breaking declaration deltas — the three blockers are behavior tightenings, not removals; `containmentClaim` retained deprecated; baseline text regenerated with `--update-baseline`, no `--allow-break` anywhere; freeze manifest `scripts/phase20-freeze-manifest.json` machine-checks each task's diff stayed inside its allowed files). Shipped: (1) **durable-resume input validation** — `assertValidAgentRunResume` at the top of `prepareAgentRunResume` covers all four public resume entrypoints; unknown legacy decisions (`"sideways"`), malformed batches, oversized reasons/elicitation, duplicate approval ids fail closed `ERR_PRISM_DECISION_*` with zero checkpoint writes/tool calls (server parser stays defense in depth); (2) **work-tool environment isolation** — `createCliRunner` children get a fixed base allow-list + explicit env + forced HOME/telemetry controls + late-bound per-identity tokens, 64-name/64-KiB caps `ERR_PRISM_WORK_ENV`, absolute binary/configDir, linear output capture; (3) **explicit sandbox capabilities** — `SandboxAdapter.capabilities` (six immutable booleans, omission/malformed ⇒ all false), composition capabilities from verified wiring, `containmentClaim` deprecated as the conservative projection; Docker reports only verified controls, native reports filesystem/process/privilege false; docs/coding-security.md capability table, docs/host-security.md authorization guidance. New regression surface: `scripts/phase20-security.test.mjs` (public built entrypoints, wired into `security:threat-suites`), packed plain-JS consumer regressions in install-smoke, and the sandbox-browser workflow's fail-loud 0.2.0 blocker gate recording Docker/native capability evidence — **0.2.0 does not ship while any blocker is skipped**. Store compatibility with 0.1.7: **compatible, no migration** (no persisted-shape change; `docs/migration.md` `0.1.7 → 0.2.0` section). Exit gate green: npm test core + script gates (incl. phase20-freeze done-phase), `sdk:ready` exit 0, audit 0 moderate, pack dry-run 50/50 twice byte-identical, plain reviewed compat gate at 0.2.0, Docker daemon + native netns protected evidence; evidence in `scripts/phase20-baseline.json` `exitGate`. Rollback = restore the 0.1.7 manifests/tag — but rollback restores the three defects, so hosts should disable resume side effects and work-tool execution at their own boundary if rollback is unavoidable.
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
# Operator prerequisites recorded: clean tree at the v0.2.0 tag candidate, GPG key, npm OIDC publisher.
|
|
283
|
+
npm test # core + workspace suites + all script gates
|
|
284
|
+
npm run security:threat-suites # phase8-11 + phase20 public-entry conformance
|
|
285
|
+
npm run sdk:ready # typecheck, lint, format, test, coverage, pack, release:gate
|
|
286
|
+
node scripts/release.mjs gate --version 0.2.0 # plain reviewed additive gate, 0 breaking deltas
|
|
287
|
+
npm run pack:dry-run # twice; diff reports — deterministic
|
|
288
|
+
npm audit --audit-level=moderate
|
|
289
|
+
npm run release:check -- --version 0.2.0 --report /tmp/prism-0.2.0-preflight.json
|
|
290
|
+
npm run release:publish -- --version 0.2.0 --dry-run --allow-dirty --allow-untagged --report /tmp/prism-0.2.0-dry-run.json
|
|
291
|
+
# run the dry-run twice and diff the reports: deterministic, byte-identical
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Protected evidence (never a passing skip): `docker info` + digest-pinned image (e.g. `PRISM_TEST_DOCKER_SANDBOX=1 PRISM_TEST_DOCKER_BIN=/usr/bin/docker PRISM_TEST_DOCKER_IMAGE=ubuntu@sha256:... npm test -w @arnilo/prism-coding-security -- --test-name-pattern "protected Docker"`) and native netns capability (`unshare --net` / `--net --map-root-user` must succeed; T9 native capability test runs, not skips). The sandbox-browser workflow fails loudly when this evidence is missing.
|
|
295
|
+
|
|
296
|
+
### 0.1.7 publish handoff (plan 019 Task 6)
|
|
297
|
+
|
|
298
|
+
**Decision: GO when the operator prerequisites below are recorded.** Release **0.1.7** (plan 019) is the performance-and-DX patch on the frozen 0.1.x line — **additive-only** vs 0.1.6 (plain compat gate at 0.1.7 passed with 0 breaking declaration deltas; the baseline text was regenerated with `--update-baseline` for the version literal only, no `--allow-break` anywhere; freeze manifest `scripts/phase19-freeze-manifest.json` machine-checks each task's diff stayed inside its allowed files). Shipped: (1) **prompt-cache telemetry surface** — dependency-free `createCacheTelemetry()` aggregator in core, host-activated, per-provider/model request counts + aggregate hit rate + cache-read/write token totals + estimated savings, bounded cardinality (cap 256 distinct keys, `__overflow__` bucket), token counters/rates only (never prompt content, cache keys, or identity), O(1) `record()`; (2) **model-router selection policies** — additive `ModelRouterSelectionPolicy` on `createModelRouter` (default ordered behavior byte-identical) with the reference `createCostLatencySelection` ranking by `ModelCost` then in-memory latency EMA fed from `recordOutcome({ latencyMs })`, permutation-only reorder of already-allowed candidates, misbehavior fails closed `ERR_PRISM_MODEL_ROUTER_POLICY`; (3) **async AgUiProjection closeout** — plan 009 Task 15 surface verified with evidence (`asyncHooks: {verified: true, gapFound: false}` in `scripts/phase19-baseline.json`), no new code; (4) **`prism providers add <name>` scaffold** — new CLI subcommand generating an OpenAI-compatible provider package (manifest, provider via `createOpenAICompatibleProvider`, starter models, cache helpers, offline conformance test, docs stub) with npm-name/traversal/symlink-escape validation and placeholders only — never secrets; scaffold output is host-chosen and never auto-registered. Store compatibility with 0.1.6: **compatible, no migration** (additive-only; no persisted-shape change; `docs/migration.md` gains no entries). Exit gate green: npm test core + script gates (incl. phase19-freeze done-phase), `sdk:ready` exit 0, audit 0 moderate, pack dry-run 50/50 twice byte-identical, budget/benchmark gates green; evidence in `scripts/phase19-baseline.json` `exitGate`. Rollback = restore the 0.1.6 manifests/tag.
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
# Operator prerequisites recorded: clean tree at the v0.1.7 tag candidate, GPG key, npm OIDC publisher.
|
|
302
|
+
npm test # core + workspace suites + all script gates
|
|
303
|
+
npm run sdk:ready # typecheck, lint, format, test, pack, release:gate
|
|
304
|
+
node scripts/release.mjs gate --version 0.1.7 # plain additive gate, 0 breaking deltas
|
|
305
|
+
npm run pack:dry-run # twice; diff reports — deterministic
|
|
306
|
+
npm audit --audit-level=moderate
|
|
307
|
+
npm run release:check -- --version 0.1.7 --report /tmp/prism-0.1.7-preflight.json
|
|
308
|
+
npm run release:publish -- --version 0.1.7 --dry-run --allow-dirty --allow-untagged --report /tmp/prism-0.1.7-dry-run.json
|
|
309
|
+
# run the dry-run twice and diff the reports: deterministic, byte-identical
|
|
310
|
+
|
|
311
|
+
# Sign the release on the clean tagged tree (operator GPG key):
|
|
312
|
+
git tag -s v0.1.7 -m "Prism 0.1.7 — performance and DX (additive)"
|
|
313
|
+
git verify-tag v0.1.7
|
|
314
|
+
git push origin v0.1.7 # tag push triggers release.yml publish job (provenance, attestations)
|
|
315
|
+
|
|
316
|
+
# Real publication never bypasses the gates: release.mjs refuses
|
|
317
|
+
# --allow-dirty/--allow-untagged without --dry-run.
|
|
318
|
+
```
|
|
319
|
+
|
|
277
320
|
### 0.1.6 publish handoff (plan 018 Task 7)
|
|
278
321
|
|
|
279
322
|
**Decision: GO when the operator prerequisites below are recorded.** Release **0.1.6** (plan 018) is the coding-agent capability-closeouts patch on the frozen 0.1.x line — **additive-only** vs 0.1.5 (plain compat gate at 0.1.6 passed with 0 breaking declaration deltas; the baseline text was regenerated with `--update-baseline` for the version literal only, no `--allow-break` anywhere). Five demand-gated closeouts shipped, each flipped to `demanded` by named demand evidence (operator `arn` for native-sandbox/doc-reader/delete-glob/checkpoint-bodies, user `Clay` for acp-session-store) before its task landed; the demand-gate registry (`scripts/phase18-freeze-manifest.json`) machine-checks demanded ⇒ implemented, deferred ⇒ untouched. Shipped: (1) **durable ACP session store** — `@arnilo/prism-ag-ui` `AcpSessionStore` host seam (`save`/`loadAll`/`evict`), persisted `{sessionId, ownership, modeId, configValues, cwd, additionalDirectories, updatedAt}`, lazy ownership-scoped restore, fail-closed drops, absent seam = 0.1.5 behavior; (2) **network-free native sandbox** — `createNativeSandbox` in `@arnilo/prism-coding-security` (fresh netns per command via the OS `unshare` binary, chained ulimits with `|| exit 126`, argv-only exec, cwd containment, process-group kill, env allow-list, Linux-only fail-closed); (3) **bounded PDF/Office document reader** — new optional package `@arnilo/prism-document-reader` (the 50th manifest, graph 49 → 50) with optional `pdf-parse`/`mammoth` peers fail-closed at creation, magic-byte gating, null fall-through, caps + redaction at the adapter boundary; (4) **recursive delete + brace-expanding glob** — per-call `recursive: true` with fan-out cap and symlink-unlink-not-follow, host-selected/per-call `braceExpansion` bounded to 128 alternatives / 4096 expanded bytes, fail-closed on overflow/malformed braces; (5) **checkpoint persistence for loaded-skill bodies** — opt-in `includeSkillBodies` on run + resume options (names-only stays default, 0.1.3 shapes byte-identical), ≤64 bodies / ≤256-char names / ≤262144-byte bodies / ≤1 MiB total, `maxStateBytes` refusal, redacted at rest, registry-independent resume render. Store compatibility with 0.1.5: **compatible, no migration** (additive-only; no persisted-shape change; `docs/migration.md` gains no entries). Exit gate green: npm test core 1,433/1,433 + 190 script gates (incl. phase18-freeze done-phase), `sdk:ready` exit 0, audit 0 moderate, pack dry-run 50/50 twice byte-identical, budget/benchmark gates green; evidence in `scripts/phase18-baseline.json` `exitGate`. Rollback = restore the 0.1.5 manifests/tag.
|
package/docs/work-tools.md
CHANGED
|
@@ -106,6 +106,28 @@ Mutation tools (`*_mail_draft_send`, `*_draft_*`) create an in-adapter draft and
|
|
|
106
106
|
|
|
107
107
|
Call `begin({ identity, key, op })` **before** the external effect. After it succeeds, call `complete`, `fail`, or `markUnknown` with the returned claim token and version. The connector effect stays outside the database transaction, so this is claim-before-effect/deduplication—not exactly-once delivery. Claims default to 15 minutes (hard 60 minutes); expired claims transition to `unknown`; attempts default to 3 (hard 5). Stored rows contain no request body, token, raw provider response, or unrestricted payload.
|
|
108
108
|
|
|
109
|
+
## Subprocess environment isolation (0.2.0, plan 020 Task 3)
|
|
110
|
+
|
|
111
|
+
`createCliRunner` never inherits the host environment. The child process receives only:
|
|
112
|
+
|
|
113
|
+
1. **Fixed platform base** — allow-listed locale/system keys copied from the host: `PATH`, `LANG`, `LC_ALL`, `TZ`, `SYSTEMROOT`/`SystemRoot`, `TEMP`, `TMP`, `PATHEXT`, `COMSPEC`. Nothing else from `process.env` crosses the boundary, so unrelated ambient variables (e.g. `PRISM_PROOF_SECRET`) cannot reach the CLI. Additions to the list are deliberate one-line allow-list changes.
|
|
114
|
+
2. **Explicit host env** — non-secret values passed via the `env` option (e.g. `{ LANG: "C.UTF-8" }`).
|
|
115
|
+
3. **Late-bound per-identity token env** — the `tokenProvider` result, merged per call; never argv, never model context.
|
|
116
|
+
4. **Forced reserved controls** — `HOME` is always the isolated `configDir` and `CLIMICROSOFT365_DISABLETELEMETRY` is always `"1"`; neither the explicit map nor the token layer can override them (any attempt fails closed with `ERR_PRISM_WORK_ENV` before spawn).
|
|
117
|
+
|
|
118
|
+
Environment maps are validated before spawn: NUL-free, `[A-Za-z_][A-Za-z0-9_]*` names, string values, no case-insensitive duplicate or reserved keys (Windows canonicalizes PATH/system key casing so Node's first-lexicographic-key behavior cannot select an attacker-controlled duplicate), and fixed caps of 64 names / 64 KiB total (`ERR_PRISM_WORK_LIMIT`).
|
|
119
|
+
|
|
120
|
+
### Host-pinned absolute paths
|
|
121
|
+
|
|
122
|
+
`binary` and `configDir` must be **absolute** paths (`path.isAbsolute`); empty, relative, or NUL-containing values are rejected at construction with `ERR_PRISM_WORK_BINARY` / `ERR_PRISM_WORK_CONFIG` before any spawn.
|
|
123
|
+
|
|
124
|
+
### Migration (0.1.7 → 0.2.0)
|
|
125
|
+
|
|
126
|
+
- Any host env your CLI needed beyond the fixed base must move into the explicit `env` map (non-secret) or the per-identity token layer (secrets).
|
|
127
|
+
- Relative `binary`/`configDir` values now fail at construction; resolve them to absolute paths.
|
|
128
|
+
- Per-call `runOpts.env` keys colliding case-insensitively with `HOME` or the telemetry-disable control now fail closed instead of being silently overridden.
|
|
129
|
+
- Output capture is linear: chunks are accumulated in an array with one final `Buffer.concat`, and the process is killed/rejected before bytes beyond the stdout/stderr caps are retained.
|
|
130
|
+
|
|
109
131
|
## Limits
|
|
110
132
|
|
|
111
133
|
| Resource | Default / hard |
|
|
@@ -126,7 +148,8 @@ Approved mutations require core-derived `context.idempotencyKey` and a configure
|
|
|
126
148
|
- Connector tokens (0.0.14): an optional `tokenProvider` resolves a per-identity access token into an env var per call — never argv, never model context. A missing/expired/revoked/cross-identity/wrong-tenant token fails the call closed before any exec. Refresh is late-bound and single-flighted per account (no refresh storm under reconnect). Build one with `createOAuthWorkTokenProvider()` from `@arnilo/prism-credentials-node`.
|
|
127
149
|
- External mail recipients fail closed unless `externalRecipients.allow` returns true.
|
|
128
150
|
- Anonymous / `anyone` sharing denied.
|
|
129
|
-
- CLI stdout/stderr capped; NDJSON page streams strictly parsed and page-capped; process killed on timeout/abort/overflow.
|
|
151
|
+
- CLI stdout/stderr capped (linear chunk capture, killed/rejected before bytes beyond the cap are retained); NDJSON page streams strictly parsed and page-capped; process killed on timeout/abort/overflow.
|
|
152
|
+
- Subprocess environment isolated (0.2.0): fixed allow-listed base + explicit `env` + late-bound token env; `HOME`/telemetry controls forced; reserved/duplicate/NUL/over-cap env and non-absolute binary/configDir fail before spawn. See [Subprocess environment isolation](#subprocess-environment-isolation-020-plan-020-task-3).
|
|
130
153
|
|
|
131
154
|
## Related
|
|
132
155
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arnilo/prism",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Agent harness for AI providers, agents, sessions, and tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
"build": "npm run build:core && npm run build --workspaces --if-present",
|
|
144
144
|
"typecheck": "npm run build && npm run typecheck --workspaces --if-present && tsc -p examples --noEmit",
|
|
145
145
|
"sweep:unused": "node scripts/sweep-unused.mjs",
|
|
146
|
-
"test": "npm run build && node --test dist/__tests__/*.test.js && node --test scripts/release-gate.test.mjs scripts/tooling-gate.test.mjs scripts/budget-gate.test.mjs scripts/phase8-conformance.test.mjs scripts/phase9-conformance.test.mjs scripts/phase10-conformance.test.mjs scripts/phase11-conformance.test.mjs scripts/phase11-freeze.test.mjs scripts/phase12-freeze.test.mjs scripts/phase13-freeze.test.mjs scripts/phase14-freeze.test.mjs scripts/phase15-freeze.test.mjs scripts/phase16-freeze.test.mjs scripts/phase17-freeze.test.mjs scripts/phase18-freeze.test.mjs scripts/benchmark-0.1.0.test.mjs scripts/sweep-unused.test.mjs scripts/e2e-enterprise-journey.test.mjs scripts/e2e-coding-journey.test.mjs && npm run test --workspaces --if-present",
|
|
146
|
+
"test": "npm run build && node --test dist/__tests__/*.test.js && node --test scripts/release-gate.test.mjs scripts/tooling-gate.test.mjs scripts/budget-gate.test.mjs scripts/phase8-conformance.test.mjs scripts/phase9-conformance.test.mjs scripts/phase10-conformance.test.mjs scripts/phase11-conformance.test.mjs scripts/phase11-freeze.test.mjs scripts/phase12-freeze.test.mjs scripts/phase13-freeze.test.mjs scripts/phase14-freeze.test.mjs scripts/phase15-freeze.test.mjs scripts/phase16-freeze.test.mjs scripts/phase17-freeze.test.mjs scripts/phase18-freeze.test.mjs scripts/phase19-freeze.test.mjs scripts/phase20-freeze.test.mjs scripts/benchmark-0.1.0.test.mjs scripts/sweep-unused.test.mjs scripts/e2e-enterprise-journey.test.mjs scripts/e2e-coding-journey.test.mjs && npm run test --workspaces --if-present",
|
|
147
147
|
"test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=60 --test-coverage-functions=70 --test-coverage-branches=75 --test-coverage-exclude='**/__tests__/**' --test-coverage-exclude='**/node_modules/**' --test-coverage-exclude='**/scripts/**' --test-coverage-exclude='**/packages/**' --test-coverage-exclude='**/examples/**' dist/__tests__/*.test.js && node scripts/coverage-summary.mjs",
|
|
148
148
|
"coverage:summary": "node scripts/coverage-summary.mjs",
|
|
149
149
|
"lint": "biome lint .",
|
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
"release:publish": "node scripts/release.mjs publish",
|
|
157
157
|
"sdk:ready": "npm run typecheck && npm run lint && npm run format:check && npm test && npm run test:coverage && npm run pack:dry-run && npm run release:gate",
|
|
158
158
|
"release:gate": "node scripts/release.mjs gate",
|
|
159
|
-
"security:threat-suites": "node --test scripts/phase8-conformance.test.mjs scripts/phase9-conformance.test.mjs scripts/phase10-conformance.test.mjs scripts/phase11-conformance.test.mjs"
|
|
159
|
+
"security:threat-suites": "node --test scripts/phase8-conformance.test.mjs scripts/phase9-conformance.test.mjs scripts/phase10-conformance.test.mjs scripts/phase11-conformance.test.mjs scripts/phase20-security.test.mjs"
|
|
160
160
|
},
|
|
161
161
|
"devDependencies": {
|
|
162
162
|
"@biomejs/biome": "^2.5.5",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# __PACKAGE_NAME__
|
|
2
|
+
|
|
3
|
+
__PROVIDER_ID__ provider package for Prism (OpenAI-compatible Chat Completions).
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install __PACKAGE_NAME__ @arnilo/prism
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Wire the provider and models into your Prism host. The package registers an
|
|
12
|
+
`api_key` auth method; hosts resolve the credential value — typically from the
|
|
13
|
+
`__ENV_KEY__` environment variable:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createResolver } from "@arnilo/prism";
|
|
17
|
+
import { create__PROVIDER_PASCAL__ProviderPackage } from "__PACKAGE_NAME__";
|
|
18
|
+
|
|
19
|
+
const resolver = createResolver();
|
|
20
|
+
resolver.registerProviderPackage(
|
|
21
|
+
create__PROVIDER_PASCAL__ProviderPackage({
|
|
22
|
+
apiKey: () => process.env.__ENV_KEY__,
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Models
|
|
28
|
+
|
|
29
|
+
Starter catalog in `src/models.ts` (`__MODEL_ID__`). Replace with
|
|
30
|
+
docs-verified model metadata (limits, costs, cache behavior) before publishing.
|
|
31
|
+
|
|
32
|
+
## Conformance
|
|
33
|
+
|
|
34
|
+
`npm test` builds the package and runs the offline conformance suite wired to
|
|
35
|
+
`@arnilo/prism/testing/provider-conformance` (stream shape, tool-call delta
|
|
36
|
+
reconstruction, header ownership, secret-leak redaction, serialized content
|
|
37
|
+
coverage).
|
|
38
|
+
|
|
39
|
+
## Docs
|
|
40
|
+
|
|
41
|
+
See `docs/providers/__PROVIDER_ID__.md`.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# __PROVIDER_ID__ provider
|
|
2
|
+
|
|
3
|
+
> Scaffold stub — replace with docs-verified provider documentation before publishing.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
`__PACKAGE_NAME__` is an OpenAI-compatible provider package for Prism
|
|
8
|
+
(chat-completions style). It builds on `createOpenAICompatibleProvider` from
|
|
9
|
+
`@arnilo/prism/providers/openai-compatible`.
|
|
10
|
+
|
|
11
|
+
## When to use it
|
|
12
|
+
|
|
13
|
+
Use it for OpenAI-compatible endpoints that follow the Chat Completions
|
|
14
|
+
convention. Skip it for providers with bespoke serialization or auth.
|
|
15
|
+
|
|
16
|
+
## Inputs / request
|
|
17
|
+
|
|
18
|
+
- Base URL: `__BASE_URL__` (`--base-url` at scaffold time).
|
|
19
|
+
- Auth: `api_key` auth method; hosts resolve the credential value (e.g. from
|
|
20
|
+
`__ENV_KEY__`).
|
|
21
|
+
|
|
22
|
+
## Outputs / response / events
|
|
23
|
+
|
|
24
|
+
Standard `ProviderEvent` stream: `content_delta`, `done` (with usage), or
|
|
25
|
+
`error`. Tool calls arrive as deltas and are reconstructed by the host.
|
|
26
|
+
|
|
27
|
+
## Request/response example
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { createResolver } from "@arnilo/prism";
|
|
31
|
+
import { create__PROVIDER_PASCAL__ProviderPackage } from "__PACKAGE_NAME__";
|
|
32
|
+
|
|
33
|
+
const resolver = createResolver();
|
|
34
|
+
resolver.registerProviderPackage(
|
|
35
|
+
create__PROVIDER_PASCAL__ProviderPackage({ apiKey: () => process.env.__ENV_KEY__ }),
|
|
36
|
+
);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Implementation example
|
|
40
|
+
|
|
41
|
+
`src/provider.ts` calls `createOpenAICompatibleProvider` with the scaffolded
|
|
42
|
+
base URL, api key, and `doneUsage: true`. Model metadata lives in `src/models.ts`;
|
|
43
|
+
cache-hint mapping helpers live in `src/cache.ts`.
|
|
44
|
+
|
|
45
|
+
## Extension and configuration notes
|
|
46
|
+
|
|
47
|
+
- Override `baseUrl`, `id`, `models`, `apiKey`, and `fetch` per provider
|
|
48
|
+
package/instance.
|
|
49
|
+
- Cache behavior is scaffolded as `cache: { kind: "implicit" }` — confirm
|
|
50
|
+
against the provider's actual caching before relying on it.
|
|
51
|
+
|
|
52
|
+
## Security and performance notes
|
|
53
|
+
|
|
54
|
+
- API keys are host-resolved credentials; generated code stores no secrets.
|
|
55
|
+
- Cache keys are identifiers only, sanitized via shared core helpers.
|
|
56
|
+
|
|
57
|
+
## Related APIs
|
|
58
|
+
|
|
59
|
+
- [OpenAI-compatible provider base](../../providers/openai-compatible.md)
|
|
60
|
+
- [Provider conformance](../../provider-conformance.md)
|
|
61
|
+
- [Provider layer](../../provider-layer.md)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PACKAGE_NAME__",
|
|
3
|
+
"version": "__PRISM_VERSION__",
|
|
4
|
+
"description": "__PROVIDER_ID__ provider package for Prism.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"!dist/__tests__",
|
|
17
|
+
"!dist/**/*.map",
|
|
18
|
+
"README.md",
|
|
19
|
+
"CHANGELOG.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.json",
|
|
23
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
24
|
+
"test": "npm run build && node --test dist/__tests__/*.test.js",
|
|
25
|
+
"pack:dry-run": "npm pack --dry-run"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@arnilo/prism": "__PRISM_VERSION__"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.0.0",
|
|
32
|
+
"typescript": "^5.7.0"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20"
|
|
36
|
+
},
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"keywords": [
|
|
39
|
+
"prism",
|
|
40
|
+
"provider",
|
|
41
|
+
"__PROVIDER_ID__",
|
|
42
|
+
"agent",
|
|
43
|
+
"llm"
|
|
44
|
+
],
|
|
45
|
+
"sideEffects": false,
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ModelConfig, ProviderRequestOptions } from "@arnilo/prism";
|
|
2
|
+
import { mapCacheRetention, sanitizeCacheKey } from "@arnilo/prism";
|
|
3
|
+
|
|
4
|
+
/** OpenAI-compatible `prompt_cache_key` accepted length cap. */
|
|
5
|
+
export const __PROVIDER_UPPER___PROMPT_CACHE_KEY_MAX_LENGTH = 64;
|
|
6
|
+
|
|
7
|
+
export function __PROVIDER_ID__PromptCacheKey(options: ProviderRequestOptions | undefined): string | undefined {
|
|
8
|
+
// Sanitize + clamp via the shared core helper so cache keys cannot carry
|
|
9
|
+
// disallowed characters or exceed the provider limit. Cache keys are
|
|
10
|
+
// session/customer identifiers only, never credentials.
|
|
11
|
+
return sanitizeCacheKey(options?.cacheKey ?? options?.sessionId, __PROVIDER_UPPER___PROMPT_CACHE_KEY_MAX_LENGTH);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Retention mapping via the shared core helper; `"short"`/`"long"` only when the model supports it. */
|
|
15
|
+
export function __PROVIDER_ID__PromptCacheRetention(
|
|
16
|
+
retention: ProviderRequestOptions["cacheRetention"] | undefined,
|
|
17
|
+
model: ModelConfig,
|
|
18
|
+
): "short" | "long" | undefined {
|
|
19
|
+
return mapCacheRetention(retention, model);
|
|
20
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type CredentialValueSource, defineProviderPackage, type ModelConfig, type ProviderPackage } from "@arnilo/prism";
|
|
2
|
+
import { __PROVIDER_ID__Models } from "./models.js";
|
|
3
|
+
import { create__PROVIDER_PASCAL__Provider } from "./provider.js";
|
|
4
|
+
|
|
5
|
+
export interface __PROVIDER_PASCAL__ProviderPackageOptions {
|
|
6
|
+
readonly apiKey?: CredentialValueSource;
|
|
7
|
+
readonly fetch?: typeof fetch;
|
|
8
|
+
readonly baseUrl?: string;
|
|
9
|
+
readonly id?: string;
|
|
10
|
+
readonly models?: readonly ModelConfig[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function create__PROVIDER_PASCAL__ProviderPackage(options: __PROVIDER_PASCAL__ProviderPackageOptions = {}): ProviderPackage {
|
|
14
|
+
const providerId = options.id ?? "__PROVIDER_ID__";
|
|
15
|
+
return defineProviderPackage({
|
|
16
|
+
name: "__PACKAGE_NAME__",
|
|
17
|
+
description: "__PROVIDER_ID__ provider package for Prism.",
|
|
18
|
+
docs: { links: ["docs/providers/__PROVIDER_ID__.md"] },
|
|
19
|
+
setup(api) {
|
|
20
|
+
api.registerProvider(create__PROVIDER_PASCAL__Provider(options));
|
|
21
|
+
for (const model of options.models ?? __PROVIDER_ID__Models) api.registerModel({ ...model, provider: providerId });
|
|
22
|
+
api.registerAuthMethod({ kind: "api_key", provider: providerId, credentialName: "apiKey" });
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { __PROVIDER_ID__Models, type __PROVIDER_PASCAL__ModelConfig } from "./models.js";
|
|
28
|
+
export { create__PROVIDER_PASCAL__Provider, __PROVIDER_UPPER___DEFAULT_BASE_URL, type __PROVIDER_PASCAL__ProviderOptions } from "./provider.js";
|
|
29
|
+
export {
|
|
30
|
+
__PROVIDER_ID__PromptCacheKey,
|
|
31
|
+
__PROVIDER_ID__PromptCacheRetention,
|
|
32
|
+
__PROVIDER_UPPER___PROMPT_CACHE_KEY_MAX_LENGTH,
|
|
33
|
+
} from "./cache.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ModelConfig } from "@arnilo/prism";
|
|
2
|
+
|
|
3
|
+
/** Starter catalog for __PROVIDER_ID__: replace with docs-verified model metadata. */
|
|
4
|
+
export interface __PROVIDER_PASCAL__ModelConfig extends Omit<ModelConfig, "provider"> {
|
|
5
|
+
readonly provider: "__PROVIDER_ID__";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const __PROVIDER_ID__Models: readonly __PROVIDER_PASCAL__ModelConfig[] = [
|
|
9
|
+
{
|
|
10
|
+
provider: "__PROVIDER_ID__",
|
|
11
|
+
model: "__MODEL_ID__",
|
|
12
|
+
limits: { contextWindow: 128_000 },
|
|
13
|
+
cost: { input: 1, output: 2, cacheRead: 0.5, unit: "per_million_tokens" },
|
|
14
|
+
cache: { kind: "implicit" },
|
|
15
|
+
},
|
|
16
|
+
];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AIProvider, CredentialValueSource } from "@arnilo/prism";
|
|
2
|
+
import { createOpenAICompatibleProvider } from "@arnilo/prism/providers/openai-compatible";
|
|
3
|
+
|
|
4
|
+
/** Default Chat Completions base URL for __PROVIDER_ID__ (override per provider docs). */
|
|
5
|
+
export const __PROVIDER_UPPER___DEFAULT_BASE_URL = "__BASE_URL__";
|
|
6
|
+
|
|
7
|
+
export interface __PROVIDER_PASCAL__ProviderOptions {
|
|
8
|
+
readonly id?: string;
|
|
9
|
+
readonly baseUrl?: string;
|
|
10
|
+
readonly apiKey?: CredentialValueSource;
|
|
11
|
+
readonly fetch?: typeof fetch;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function create__PROVIDER_PASCAL__Provider(options: __PROVIDER_PASCAL__ProviderOptions = {}): AIProvider {
|
|
15
|
+
return createOpenAICompatibleProvider({
|
|
16
|
+
id: options.id ?? "__PROVIDER_ID__",
|
|
17
|
+
baseUrl: (options.baseUrl ?? __PROVIDER_UPPER___DEFAULT_BASE_URL).replace(/\/+$/, ""),
|
|
18
|
+
apiKey: options.apiKey,
|
|
19
|
+
fetch: options.fetch,
|
|
20
|
+
doneUsage: true,
|
|
21
|
+
requestFailedPrefix: "__PROVIDER_PASCAL__ request failed",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import type { ProviderRequest } from "@arnilo/prism";
|
|
4
|
+
import {
|
|
5
|
+
assertNoSecretLeak,
|
|
6
|
+
assertProviderOwnedHeadersWin,
|
|
7
|
+
assertProviderStreamConforms,
|
|
8
|
+
assertSerializedRequestCoversContent,
|
|
9
|
+
assertToolCallDeltasReconstruct,
|
|
10
|
+
} from "@arnilo/prism/testing/provider-conformance";
|
|
11
|
+
import { create__PROVIDER_PASCAL__Provider } from "../index.js";
|
|
12
|
+
import { __PROVIDER_ID__Models } from "../models.js";
|
|
13
|
+
|
|
14
|
+
const request: ProviderRequest = {
|
|
15
|
+
model: __PROVIDER_ID__Models[0],
|
|
16
|
+
messages: [
|
|
17
|
+
{ role: "system", content: [{ type: "text", text: "developer instructions" }] },
|
|
18
|
+
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
19
|
+
],
|
|
20
|
+
tools: [{ name: "lookup", parameters: { type: "object" }, execute: () => ({ toolCallId: "call_1", name: "lookup", content: [] }) }],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const API_KEY = "fake-__PROVIDER_ID__-key";
|
|
24
|
+
|
|
25
|
+
describe("__PROVIDER_ID__ provider scaffold", () => {
|
|
26
|
+
it("streams text, usage, and done; owns its headers; leaks no secrets", async () => {
|
|
27
|
+
let captured: RequestInit | undefined;
|
|
28
|
+
const provider = create__PROVIDER_PASCAL__Provider({
|
|
29
|
+
apiKey: API_KEY,
|
|
30
|
+
fetch: (async (_input, init) => {
|
|
31
|
+
captured = init;
|
|
32
|
+
return ok(
|
|
33
|
+
sse([
|
|
34
|
+
{ id: "chatcmpl-1", object: "chat.completion.chunk", choices: [{ index: 0, delta: { role: "assistant", content: "hi" } }] },
|
|
35
|
+
{
|
|
36
|
+
id: "chatcmpl-1",
|
|
37
|
+
object: "chat.completion.chunk",
|
|
38
|
+
choices: [{ index: 0, delta: {} }],
|
|
39
|
+
usage: { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 },
|
|
40
|
+
},
|
|
41
|
+
]),
|
|
42
|
+
);
|
|
43
|
+
}) as typeof fetch,
|
|
44
|
+
});
|
|
45
|
+
const events = await assertProviderStreamConforms({
|
|
46
|
+
provider,
|
|
47
|
+
request: {
|
|
48
|
+
...request,
|
|
49
|
+
options: {
|
|
50
|
+
...request.options,
|
|
51
|
+
headers: { authorization: "Bearer caller-key", "content-type": "text/plain", "x-caller": "kept" },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
expect: { text: "hi", usage: { inputTokens: 5, outputTokens: 2, totalTokens: 7 } },
|
|
55
|
+
});
|
|
56
|
+
assertNoSecretLeak(events, [API_KEY]);
|
|
57
|
+
const headers = new Headers(captured?.headers);
|
|
58
|
+
assertProviderOwnedHeadersWin(headers, {
|
|
59
|
+
owned: { authorization: `Bearer ${API_KEY}`, "content-type": "application/json" },
|
|
60
|
+
caller: { authorization: "Bearer caller-key", "content-type": "text/plain", "x-caller": "kept" },
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("serializes request content and reconstructs tool-call deltas", async () => {
|
|
65
|
+
let body: unknown;
|
|
66
|
+
const provider = create__PROVIDER_PASCAL__Provider({
|
|
67
|
+
apiKey: API_KEY,
|
|
68
|
+
fetch: (async (_input, init) => {
|
|
69
|
+
body = JSON.parse(String(init?.body));
|
|
70
|
+
return ok(
|
|
71
|
+
sse([
|
|
72
|
+
{
|
|
73
|
+
id: "c1",
|
|
74
|
+
object: "chat.completion.chunk",
|
|
75
|
+
choices: [{ index: 0, delta: { tool_calls: [{ index: 0, id: "call_1", function: { name: "lookup", arguments: "" } }] } }],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "c1",
|
|
79
|
+
object: "chat.completion.chunk",
|
|
80
|
+
choices: [{ index: 0, delta: { tool_calls: [{ index: 0, function: { arguments: '{"q":"x"}' } }] } }],
|
|
81
|
+
},
|
|
82
|
+
]),
|
|
83
|
+
);
|
|
84
|
+
}) as typeof fetch,
|
|
85
|
+
});
|
|
86
|
+
const events = await assertProviderStreamConforms({ provider, request });
|
|
87
|
+
assertToolCallDeltasReconstruct(events, [{ index: 0, id: "call_1", name: "lookup", arguments: { q: "x" } }]);
|
|
88
|
+
assertSerializedRequestCoversContent(request, body);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
function ok(body: ReadableStream<Uint8Array>): Response {
|
|
93
|
+
return new Response(body, { status: 200 });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sse(events: readonly object[]): ReadableStream<Uint8Array> {
|
|
97
|
+
const text = `${events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("")}data: [DONE]\n\n`;
|
|
98
|
+
return new ReadableStream({
|
|
99
|
+
start(controller) {
|
|
100
|
+
controller.enqueue(new TextEncoder().encode(text));
|
|
101
|
+
controller.close();
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"types": ["node"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["src"]
|
|
16
|
+
}
|