@arnilo/prism 0.2.0 → 0.2.2
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/cache-telemetry.js +4 -1
- package/dist/content.d.ts +2 -2
- package/dist/content.js +30 -50
- package/dist/contracts-core.d.ts +32 -2
- package/dist/contracts-core.js +20 -0
- package/dist/conversations.d.ts +2 -0
- package/dist/conversations.js +1 -0
- package/dist/event-multiplexer.d.ts +13 -0
- package/dist/event-multiplexer.js +39 -16
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -3
- package/dist/oauth-device-code.d.ts +58 -0
- package/dist/oauth-device-code.js +119 -0
- package/dist/pinned-fetch.d.ts +25 -0
- package/dist/pinned-fetch.js +246 -0
- package/dist/providers/openai-compatible.d.ts +2 -2
- package/dist/providers/openai-compatible.js +2 -2
- package/dist/providers/transport.d.ts +14 -1
- package/dist/providers/transport.js +43 -0
- package/dist/testing/persistence-schema.d.ts +1 -1
- package/dist/testing/persistence-schema.js +32 -28
- package/dist/testing/state-concurrency-conformance.d.ts +145 -0
- package/dist/testing/state-concurrency-conformance.js +348 -0
- package/docs/agent-events.md +1 -1
- package/docs/conversations.md +5 -2
- package/docs/credential-storage.md +1 -0
- package/docs/credentials-and-redaction.md +1 -0
- package/docs/database-persistence.md +4 -0
- package/docs/enterprise-postgres-state.md +4 -3
- package/docs/index.md +17 -17
- package/docs/mcp-tools.md +1 -1
- package/docs/migration.md +83 -0
- package/docs/model-routing.md +13 -10
- package/docs/multimodal-content.md +1 -0
- package/docs/policy-and-audit.md +1 -0
- package/docs/provider-caching.md +4 -1
- package/docs/provider-primitives.md +17 -0
- package/docs/providers/azure.md +1 -1
- package/docs/providers/bedrock.md +1 -0
- package/docs/providers/openai-compatible.md +4 -3
- package/docs/providers/vertex.md +1 -1
- package/docs/public-contracts.md +3 -3
- package/docs/release-and-install.md +39 -0
- package/docs/workflows.md +2 -1
- package/package.json +8 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.2] - 2026-08-13
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- **Release 0.2.2 (plan 022)** is the concurrent-state-and-durability-integrity cut on the 0.2.x review-remediation line. API surface **additive-only** vs 0.2.1 (plain reviewed compat gate at 0.2.2: deltas are the version literal plus `ModelRouterStateStore.reserveBudget`/`commitBudget`/`releaseBudget`, `ModelRouterReservation`, `ModelRouterBudgets.reservationTtlMs`, `ModelRouterLimits.maxRateKeys`/`maxBudgetKeys`, `SessionRecord.version` with `appendSession` `expectedVersion`, `EventMultiplexerError` `ERR_PRISM_EVENT_MULTIPLEXER_SINGLE_CONSUMER`, and the `@arnilo/prism/testing/state-concurrency-conformance` subpath; no removal; no `--allow-break`), four behavior tightenings documented in `docs/migration.md` `0.2.1 → 0.2.2`: (1) **atomic model-budget reservation** (`budget-reservation`, `@arnilo/prism-model-router` + `@arnilo/prism-enterprise-postgres`) — admission now runs `reserveBudget` (used + reserved + requested <= window max, `{reservationId, fencingToken, admitted, retryAfterMs?}`) and outcomes run `commitBudget` (actuals; a TTL-expired late commit charges the reserved amount with `unknownUsage: true`) or `releaseBudget`; reservations expire after `reservationTtlMs` (default 60 s, bounded to 31 days) so a crashed request never holds capacity forever; 0.2.1 `readBudget`/`addUsage` remain for cap-less requests and retrospective accounting, no longer admission authority; rate/budget key maps are capped (`maxRateKeys`/`maxBudgetKeys` 4,096 default / 65,536 hard; circuits unchanged) with LRU eviction that never drops a held-reservation row (capacity-exhausted fail-closed otherwise); durable store keeps reservations in a `reservations` JSONB column (enterprise migration 003, forward-only, checksummed). (2) **atomic conversation metadata** (`conversation-metadata-cas`, `@arnilo/prism-session-store-postgres` + `@arnilo/prism-session-store-sqlite`) — `SessionRecord.version` (fresh rows start at 1; migration 008 backfills legacy rows) and `appendSession` `expectedVersion` (`0` create-only, `N>0` exact-CAS update-only, omitted = 0.2.1 last-write-wins); stale writes throw `SessionMetadataConflictError` `metadata_conflict` carrying only `{id, expectedVersion, currentVersion}` (never metadata content), HTTP 409; concurrent create/branch/archive are single-statement with branch caps enforced inside the CAS write, archive wins over stale writers, retention-deleted rows are never resurrected. (3) **single-consumer EventMultiplexer** (`single-consumer`, core) — a second concurrent `subscribe()` throws `EventMultiplexerError` `ERR_PRISM_EVENT_MULTIPLEXER_SINGLE_CONSUMER` instead of silently sharing one queue and losing events; slot frees on completion/return-at-yield/close; in-repo consumers (workflow bus, supervisor) already single-subscriber. (4) **restart-stable NATS durable identity + bounded non-durable active-run registries** (`nats-durable`, `@arnilo/prism-session-store-nats`; `active-runs`, `@arnilo/prism-workflows`) — the durable consumer name is exactly `prism_<hmac16 of tenantId|sessionId|runId>` (random suffix removed) so a crash-resumed subscribe continues from the last ack; clean stops delete the consumer (cursor resume), orphaned 0.2.1 `prism_<digest>_<random>` consumers are reclaimed by the existing cleanup; the workflow active-run registry sweeps aborted/leaked entries on register and fails closed at the 512 cap (`ERR_PRISM_WORKFLOW_RUN_REGISTRY_OVERFLOW`), documented non-durable. New regression surface: `scripts/phase22-security.test.mjs` (5 tests: 4 blockers + gate accounting, built public entrypoints, wired into `security:threat-suites`), the packed plain-JS `security22.mjs` consumer in install-smoke, the `@arnilo/prism/testing/state-concurrency-conformance` harness (7 probes — approval, cursor, checkpoint CAS, idempotency, router reservation, conversation metadata, unknown-outcome — memory leg in npm test, durable legs in `test:postgres`/NATS seam, zero timing-only sleeps), and the `scripts/phase22-conformance.test.mjs` gate in the `test:postgres` chain; phase-22 evidence in `scripts/phase22-baseline.json` + `scripts/phase22-freeze-manifest.json`. Release graph stays **50** publishable manifests at exact **0.2.2**; zero new runtime dependency names (core remains dependency-free). Store compatibility with 0.2.1: **forward-only migrations** (`prism_sessions.version` migration 008, `prism_model_router_budgets.reservations` enterprise migration 003; 0.2.1 databases migrate in place, 0.2.1 binaries read 0.2.2 databases but ignore the new columns — see `docs/migration.md` rollback-risk). Exit gate green (core + script gates incl. phase21-freeze + phase22 conformance, `sdk:ready`, audit 0 moderate, pack dry-run 50/50 twice byte-identical, plain reviewed compat gate at 0.2.2, protected OIDC/OPA + durable state-concurrency evidence, evidence in `scripts/phase22-baseline.json`). **Publication remains the operator handoff** (`docs/release-and-install.md` `0.2.2 publish handoff` — signed `v0.2.2` tag + npm OIDC).
|
|
7
|
+
|
|
8
|
+
## [0.2.1] - 2026-08-13
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **Release 0.2.1 (plan 021)** is the provider-completion and outbound-trust-boundaries cut on the 0.2.x review-remediation line. API surface **additive-only** vs 0.2.0 (plain reviewed compat gate at 0.2.1: deltas are the version literal plus `@arnilo/prism-mcp` transport helpers re-exported from the lifted core primitives — same names/signatures, no removal; no `--allow-break`), five documented security-motivated behavior tightenings in `docs/migration.md` `0.2.0 → 0.2.1`: (1) **strict stream completion is the shared default** (`strict-completion`, core `createOpenAICompatibleProvider`) — `strictCompletion` defaults to `true` (explicit `false` stays the documented opt-out): a stream ending without `[DONE]` plus a choice-level `finish_reason` now fails with `ProviderTransportError` `incomplete_delta` instead of a successful `providerDone`, and done events carry usage only with completion evidence; applies to Azure, Bedrock, Vertex, OpenRouter, ZAI, NeuralWatt (Alibaba/Kimi/Ollama/OpenCode-go had already opted in). (2) **bounded success bodies** (`bounded-bodies`, core `@arnilo/prism/providers/transport`) — additive `readBoundedResponseJson` (65,536-byte UTF-8 ceiling, max JSON depth 32, max properties 4096, caller shape gate, abort, redacted errors, new `ProviderTransportError` code `response_body_shape`) replaces unbounded `response.json()` at all ten model-discovery sites plus NeuralWatt quota, Alibaba embeddings, OpenAI uploads, and both OAuth success paths. (3) **DNS-pinned OIDC/OPA/content fetch, redirects rejected** (`dns-pinning`, core `src/pinned-fetch.ts` + `@arnilo/prism-credentials-node`, `@arnilo/prism-policy`, `@arnilo/prism-mcp`) — `pinnedFetch`/`resolvePinnedAddress`/`requestPinned` lift the MCP pinning algorithm (one resolve, 1–32 bound, family check, loopback confinement, per-candidate `assertSsrfAllowedUrl`, pinned-lookup socket) into core; default JWKS, OPA decision, and content/media fetches route through it and **reject 3xx redirects outright** (`MediaContentError` code `redirect`); private/metadata/loopback answers fail closed `ssrf_denied`; MCP re-exports the lifted helpers with byte-identical behavior; egress `dns-pin.ts` deliberately NOT converged (0.3.x candidate). (4) **shared bounded OAuth device/token polling** (`oauth-consolidation`, core `src/oauth-device-code.ts`) — `pollDeviceCodeToken` serves both `@arnilo/prism-provider-openai` and `@arnilo/prism-credentials-node` device flows (RFC 8628 poll, `slow_down` +5 s, expiry deadline, cancellation, bounded reads, fail-closed token shape, `[REDACTED]` redaction); adapter fields stay plain options; behavior equivalent. (5) **edge fixes** (`edge-fixes`) — Azure/Vertex resolve rotating credentials **once per request** (inner provider re-created with the resolved token; never consumed twice); Bedrock SigV4 merges duplicate-case headers last-wins and sorts query params by encoded key then value (malformed duplicate-case signatures eliminated, single-case byte-identical); OpenAI upload cleanup retains file ids until their `DELETE` succeeds (no remote-file leak on failed cleanup); cache-telemetry `__overflow__` never carries cost (requests + token totals only). New regression surface: `scripts/phase21-security.test.mjs` (10 conformance tests over built public entrypoints covering all five items, wired into `security:threat-suites`) plus a packed plain-JS `security21.mjs` consumer in install-smoke; phase-21 freeze manifest `scripts/phase21-freeze-manifest.json` machine-checks each task's diff (preserved surface: egress dns-pin primitives, OAuth connector consumers, strictCompletion opt-in adapters, native streaming adapters). Release graph stays **50** publishable manifests (root + 49 workspace packages) at exact **0.2.1**; zero new runtime dependency names (core remains dependency-free). Store compatibility with 0.2.0: **compatible, no migration** (no persisted-shape change). Exit gate green (core + script gates incl. phase21-freeze done-phase, `sdk:ready`, audit 0 moderate, pack dry-run 50/50 twice byte-identical, plain reviewed compat gate at 0.2.1, live OIDC JWKS protected evidence + live OPA fail-closed evidence, evidence in `scripts/phase21-baseline.json`). **Publication remains the operator handoff** (`docs/release-and-install.md` `0.2.1 publish handoff` — signed `v0.2.1` tag + npm OIDC).
|
|
12
|
+
|
|
3
13
|
## [0.2.0] - 2026-08-13
|
|
4
14
|
|
|
5
15
|
### Changed
|
package/dist/cache-telemetry.js
CHANGED
|
@@ -72,7 +72,10 @@ export function createCacheTelemetry(options = {}) {
|
|
|
72
72
|
cacheReadTokens: sample.cacheReadTokens,
|
|
73
73
|
inputTokens: sample.inputTokens,
|
|
74
74
|
});
|
|
75
|
-
|
|
75
|
+
// The __overflow__ bucket aggregates mixed provider/model tokens, so it
|
|
76
|
+
// never carries cost: one model's cost metadata must not be applied to
|
|
77
|
+
// other models' tokens. It reports requests and token totals only.
|
|
78
|
+
if (model?.cost && sample !== overflowSample) {
|
|
76
79
|
// cacheSavings depends only on read tokens + cost metadata, so the
|
|
77
80
|
// aggregate equals the sum of per-call savings; feed it the totals
|
|
78
81
|
// to reuse the exact cache-helpers math rather than reimplementing it.
|
package/dist/content.d.ts
CHANGED
|
@@ -104,8 +104,8 @@ export declare class UnsupportedModalityError extends Error {
|
|
|
104
104
|
constructor(modality: ModelInputCapability, model: ModelConfig);
|
|
105
105
|
}
|
|
106
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" | "fetch_failed" | "fetch_timeout" | "resource_required" | "mime_mismatch" | "unsupported_url_scheme";
|
|
108
|
-
constructor(code: MediaContentError["code"], message: string);
|
|
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
109
|
}
|
|
110
110
|
export declare function contentBlockInputModality(block: ContentBlock): ModelInputCapability | undefined;
|
|
111
111
|
export declare function collectMessageContentBlocks(messages: readonly Message[]): ContentBlock[];
|
package/dist/content.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { lookup as dnsLookup } from "node:dns/promises";
|
|
2
|
-
import { request as httpRequest } from "node:http";
|
|
3
|
-
import { request as httpsRequest } from "node:https";
|
|
4
2
|
import { isIP } from "node:net";
|
|
3
|
+
import { pinnedFetch } from "./pinned-fetch.js";
|
|
5
4
|
import { assertPermission } from "./security.js";
|
|
6
5
|
/** Known model input capability tags for `ModelCapabilities.input`. */
|
|
7
6
|
export const MODEL_INPUT_CAPABILITIES = ["text", "image", "audio", "file", "document"];
|
|
@@ -29,8 +28,8 @@ export class UnsupportedModalityError extends Error {
|
|
|
29
28
|
}
|
|
30
29
|
export class MediaContentError extends Error {
|
|
31
30
|
code;
|
|
32
|
-
constructor(code, message) {
|
|
33
|
-
super(message);
|
|
31
|
+
constructor(code, message, options) {
|
|
32
|
+
super(message, options);
|
|
34
33
|
this.name = "MediaContentError";
|
|
35
34
|
this.code = code;
|
|
36
35
|
}
|
|
@@ -310,22 +309,34 @@ async function fetchBoundedMediaUrl(url, options) {
|
|
|
310
309
|
try {
|
|
311
310
|
if (options.fetch)
|
|
312
311
|
return await readFetchResponse(await options.fetch(url, { signal, redirect: "error" }), options.maxBytes);
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
312
|
+
if (options.requestUrl) {
|
|
313
|
+
// Test/custom seam: explicit resolve + caller-provided requester. The default
|
|
314
|
+
// path below routes through the shared pinnedFetch primitive instead.
|
|
315
|
+
const parsed = new URL(url);
|
|
316
|
+
const hostname = normalizeHostname(parsed.hostname);
|
|
317
|
+
const family = isIP(hostname);
|
|
318
|
+
const address = family
|
|
319
|
+
? { address: hostname, family: family }
|
|
320
|
+
: await resolvePublicAddress(hostname, options.resolveHostname ?? defaultMediaHostnameResolver, signal, options.ssrf);
|
|
321
|
+
const bytes = await options.requestUrl({
|
|
322
|
+
url: parsed,
|
|
323
|
+
address,
|
|
324
|
+
maxBytes: options.maxBytes,
|
|
325
|
+
signal,
|
|
326
|
+
});
|
|
327
|
+
if (bytes.byteLength > options.maxBytes) {
|
|
328
|
+
throw new MediaContentError("item_too_large", `Fetched media exceeded ${options.maxBytes} bytes`);
|
|
329
|
+
}
|
|
330
|
+
return bytes;
|
|
327
331
|
}
|
|
328
|
-
|
|
332
|
+
// Default: one DNS-pinned, redirect-free, byte-bounded fetch (task 4).
|
|
333
|
+
const response = await pinnedFetch(new URL(url), { signal, redirect: "manual" }, {
|
|
334
|
+
errorPrefix: "Media",
|
|
335
|
+
hostnameErrorPrefix: "Media",
|
|
336
|
+
resolver: options.resolveHostname ?? defaultMediaHostnameResolver,
|
|
337
|
+
ssrf: options.ssrf,
|
|
338
|
+
});
|
|
339
|
+
return await readFetchResponse(response, options.maxBytes);
|
|
329
340
|
}
|
|
330
341
|
catch (error) {
|
|
331
342
|
if (error instanceof MediaContentError)
|
|
@@ -411,37 +422,6 @@ async function readFetchResponse(response, maxBytes) {
|
|
|
411
422
|
}
|
|
412
423
|
return joinChunks(chunks, total);
|
|
413
424
|
}
|
|
414
|
-
function requestPinnedMediaUrl({ url, address, maxBytes, signal }) {
|
|
415
|
-
return new Promise((resolve, reject) => {
|
|
416
|
-
const request = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, {
|
|
417
|
-
agent: false,
|
|
418
|
-
family: address.family,
|
|
419
|
-
signal,
|
|
420
|
-
lookup: (_hostname, _options, callback) => callback(null, address.address, address.family),
|
|
421
|
-
}, (response) => readIncomingMessage(response, maxBytes).then(resolve, reject));
|
|
422
|
-
request.on("error", reject);
|
|
423
|
-
request.end();
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
async function readIncomingMessage(response, maxBytes) {
|
|
427
|
-
const status = response.statusCode ?? 0;
|
|
428
|
-
if (status < 200 || status >= 300) {
|
|
429
|
-
response.resume();
|
|
430
|
-
throw new MediaContentError("fetch_failed", `Media fetch failed with status ${status}`);
|
|
431
|
-
}
|
|
432
|
-
const chunks = [];
|
|
433
|
-
let total = 0;
|
|
434
|
-
for await (const chunk of response) {
|
|
435
|
-
const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk);
|
|
436
|
-
total += bytes.byteLength;
|
|
437
|
-
if (total > maxBytes) {
|
|
438
|
-
response.destroy();
|
|
439
|
-
throw new MediaContentError("item_too_large", `Fetched media exceeded ${maxBytes} bytes`);
|
|
440
|
-
}
|
|
441
|
-
chunks.push(bytes);
|
|
442
|
-
}
|
|
443
|
-
return joinChunks(chunks, total);
|
|
444
|
-
}
|
|
445
425
|
function joinChunks(chunks, total) {
|
|
446
426
|
const bytes = new Uint8Array(total);
|
|
447
427
|
let offset = 0;
|
package/dist/contracts-core.d.ts
CHANGED
|
@@ -823,6 +823,27 @@ export interface SessionBranchHandle {
|
|
|
823
823
|
}
|
|
824
824
|
/** Stable error code carried by `SessionAppendConflictError`. */
|
|
825
825
|
export declare const SESSION_APPEND_CONFLICT_CODE: "session_append_conflict";
|
|
826
|
+
/** CAS conflict code for `appendSession` metadata writes. Stable and message-independent. */
|
|
827
|
+
export declare const SESSION_METADATA_CONFLICT_CODE: "metadata_conflict";
|
|
828
|
+
/** Conflict details carried by `SessionMetadataConflictError`. Versions only; never metadata content. */
|
|
829
|
+
export interface SessionMetadataConflict {
|
|
830
|
+
readonly code: typeof SESSION_METADATA_CONFLICT_CODE;
|
|
831
|
+
readonly id: string;
|
|
832
|
+
readonly expectedVersion: number;
|
|
833
|
+
readonly currentVersion: number;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Thrown when `appendSession` is called with an `expectedVersion` CAS guard and the
|
|
837
|
+
* stored session's version no longer matches (concurrent create/branch/archive, or a
|
|
838
|
+
* delete raced the write). Recognize via the stable `code` or `isSessionMetadataConflict`.
|
|
839
|
+
*/
|
|
840
|
+
export declare class SessionMetadataConflictError extends Error {
|
|
841
|
+
readonly conflict: SessionMetadataConflict;
|
|
842
|
+
readonly code: "metadata_conflict";
|
|
843
|
+
constructor(conflict: SessionMetadataConflict);
|
|
844
|
+
}
|
|
845
|
+
/** Type guard keyed off the stable `code` (works across bundles; not message text). */
|
|
846
|
+
export declare function isSessionMetadataConflict(error: unknown): error is SessionMetadataConflictError;
|
|
826
847
|
/** Conflict details carried by `SessionAppendConflictError`. Carries no secrets. */
|
|
827
848
|
export interface SessionAppendConflict {
|
|
828
849
|
readonly code: typeof SESSION_APPEND_CONFLICT_CODE;
|
|
@@ -954,6 +975,8 @@ export interface SessionRecord extends OwnershipScope {
|
|
|
954
975
|
readonly expiresAt?: string;
|
|
955
976
|
readonly retentionPolicyId?: string;
|
|
956
977
|
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
978
|
+
/** Write version for optimistic metadata CAS; undefined on legacy/never-written rows. */
|
|
979
|
+
readonly version?: number;
|
|
957
980
|
}
|
|
958
981
|
/** Stored branch handle / leaf pointer. The leaf is the current entry id for the branch. */
|
|
959
982
|
export interface BranchRecord {
|
|
@@ -1182,8 +1205,15 @@ export interface ProductionPersistenceStore {
|
|
|
1182
1205
|
queryRetentionPolicies(query: RetentionPolicyQuery): Promise<PersistencePage<RetentionPolicy>>;
|
|
1183
1206
|
queryMigrations(query: MigrationQuery): Promise<PersistencePage<MigrationRecord>>;
|
|
1184
1207
|
/** Optional session-record write capability (conversation threads, host-managed sessions).
|
|
1185
|
-
* Upserts by id; ownership columns are set on create, `metadata`/`updatedAt` on update.
|
|
1186
|
-
|
|
1208
|
+
* Upserts by id; ownership columns are set on create, `metadata`/`updatedAt` on update.
|
|
1209
|
+
* Additive CAS: pass `expectedVersion` to require the stored version to match before the
|
|
1210
|
+
* write (0 = create-only, a positive number = exact current version); omit it for legacy
|
|
1211
|
+
* last-write-wins. Returns the new `version` when the underlying store supports it. */
|
|
1212
|
+
appendSession?(record: SessionRecord & {
|
|
1213
|
+
readonly expectedVersion?: number;
|
|
1214
|
+
}): Promise<{
|
|
1215
|
+
readonly version: number;
|
|
1216
|
+
} | void>;
|
|
1187
1217
|
/** DB-friendly branch read (mirrors `SessionStore.readBranchPath`): one ancestor-chain
|
|
1188
1218
|
* query instead of `queryEntries({ sessionId })` + in-memory walk. Optional. */
|
|
1189
1219
|
readBranchPath?(query: SessionBranchRead): Promise<PersistencePage<SessionEntry>>;
|
package/dist/contracts-core.js
CHANGED
|
@@ -84,6 +84,26 @@ export function isSessionSearchUnsupported(error) {
|
|
|
84
84
|
}
|
|
85
85
|
/** Stable error code carried by `SessionAppendConflictError`. */
|
|
86
86
|
export const SESSION_APPEND_CONFLICT_CODE = "session_append_conflict";
|
|
87
|
+
/** CAS conflict code for `appendSession` metadata writes. Stable and message-independent. */
|
|
88
|
+
export const SESSION_METADATA_CONFLICT_CODE = "metadata_conflict";
|
|
89
|
+
/**
|
|
90
|
+
* Thrown when `appendSession` is called with an `expectedVersion` CAS guard and the
|
|
91
|
+
* stored session's version no longer matches (concurrent create/branch/archive, or a
|
|
92
|
+
* delete raced the write). Recognize via the stable `code` or `isSessionMetadataConflict`.
|
|
93
|
+
*/
|
|
94
|
+
export class SessionMetadataConflictError extends Error {
|
|
95
|
+
conflict;
|
|
96
|
+
code = SESSION_METADATA_CONFLICT_CODE;
|
|
97
|
+
constructor(conflict) {
|
|
98
|
+
super(`session metadata conflict: expected version ${conflict.expectedVersion}, current ${conflict.currentVersion}`);
|
|
99
|
+
this.conflict = conflict;
|
|
100
|
+
this.name = "SessionMetadataConflictError";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Type guard keyed off the stable `code` (works across bundles; not message text). */
|
|
104
|
+
export function isSessionMetadataConflict(error) {
|
|
105
|
+
return error instanceof Error && error.code === SESSION_METADATA_CONFLICT_CODE;
|
|
106
|
+
}
|
|
87
107
|
/**
|
|
88
108
|
* Thrown when `SessionStore.append` rejects an entry under `SessionAppendOptions`
|
|
89
109
|
* (dangling/stale `expectedParentId`, stricter adapter CAS failure, or duplicate
|
package/dist/conversations.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface ConversationThread extends OwnershipScope {
|
|
|
21
21
|
readonly updatedAt: string;
|
|
22
22
|
/** Branch leaves recorded by the conversation service; the entry tree remains the content source of truth. */
|
|
23
23
|
readonly branches: readonly ConversationBranchRef[];
|
|
24
|
+
/** CAS write version (stored `SessionRecord.version`); undefined on legacy rows. */
|
|
25
|
+
readonly version?: number;
|
|
24
26
|
/** Host-supplied create metadata; never credentials or raw transcripts. */
|
|
25
27
|
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
26
28
|
}
|
package/dist/conversations.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export type EventOverflowPolicy = "close" | "drop_oldest" | "drop_newest";
|
|
2
|
+
export declare const EVENT_MULTIPLEXER_SINGLE_CONSUMER_CODE = "ERR_PRISM_EVENT_MULTIPLEXER_SINGLE_CONSUMER";
|
|
3
|
+
/** Thrown when a second consumer subscribes while one is active (single-consumer contract). */
|
|
4
|
+
export declare class EventMultiplexerError extends Error {
|
|
5
|
+
readonly code = "ERR_PRISM_EVENT_MULTIPLEXER_SINGLE_CONSUMER";
|
|
6
|
+
constructor(message?: string);
|
|
7
|
+
}
|
|
2
8
|
export interface EventOverflowInfo {
|
|
3
9
|
readonly droppedEvents: number;
|
|
4
10
|
readonly maxQueuedEvents: number;
|
|
@@ -14,6 +20,13 @@ export interface EventMultiplexerOptions<T> {
|
|
|
14
20
|
export interface EventMultiplexer<T> {
|
|
15
21
|
publish(event: T): void;
|
|
16
22
|
observe<S>(source: AsyncIterable<S>, map: (event: S) => T): () => void;
|
|
23
|
+
/**
|
|
24
|
+
* Single-consumer subscription. A second concurrent subscriber is rejected with
|
|
25
|
+
* `EventMultiplexerError` (ERR_PRISM_EVENT_MULTIPLEXER_SINGLE_CONSUMER); the slot
|
|
26
|
+
* frees when the active consumer's iterator completes or is `return()`ed at a
|
|
27
|
+
* yield, or when the multiplexer closes. A consumer parked awaiting an event is
|
|
28
|
+
* released by the next `publish`/`close` — `return()` while parked waits for it.
|
|
29
|
+
*/
|
|
17
30
|
subscribe(): AsyncIterable<T>;
|
|
18
31
|
close(): void;
|
|
19
32
|
readonly droppedEvents: number;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
export const EVENT_MULTIPLEXER_SINGLE_CONSUMER_CODE = "ERR_PRISM_EVENT_MULTIPLEXER_SINGLE_CONSUMER";
|
|
2
|
+
/** Thrown when a second consumer subscribes while one is active (single-consumer contract). */
|
|
3
|
+
export class EventMultiplexerError extends Error {
|
|
4
|
+
code = EVENT_MULTIPLEXER_SINGLE_CONSUMER_CODE;
|
|
5
|
+
constructor(message = "Event multiplexer already has an active subscriber") {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "EventMultiplexerError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
1
10
|
/** Bounded single-consumer fan-in for arbitrary async event sources. */
|
|
2
11
|
export function createEventMultiplexer(options = {}) {
|
|
3
12
|
const maxQueuedEvents = Math.max(1, options.maxQueuedEvents ?? 1024);
|
|
@@ -8,6 +17,9 @@ export function createEventMultiplexer(options = {}) {
|
|
|
8
17
|
let isClosed = false;
|
|
9
18
|
let droppedEvents = 0;
|
|
10
19
|
let overflowNotified = false;
|
|
20
|
+
// Single-consumer contract: the parked-waiter design corrupts the queue when two
|
|
21
|
+
// consumers iterate concurrently, so a second active consumer is rejected instead.
|
|
22
|
+
let activeConsumer = false;
|
|
11
23
|
const abort = () => close();
|
|
12
24
|
if (options.signal?.aborted)
|
|
13
25
|
isClosed = true;
|
|
@@ -108,6 +120,9 @@ export function createEventMultiplexer(options = {}) {
|
|
|
108
120
|
options.signal?.removeEventListener("abort", abort);
|
|
109
121
|
finishSources();
|
|
110
122
|
queue.length = 0;
|
|
123
|
+
// Free the single-consumer slot: a consumer suspended at a yield may never pull
|
|
124
|
+
// again, and after close a fresh subscriber terminates immediately anyway.
|
|
125
|
+
activeConsumer = false;
|
|
111
126
|
if (waiter) {
|
|
112
127
|
const resolve = waiter;
|
|
113
128
|
waiter = undefined;
|
|
@@ -115,23 +130,31 @@ export function createEventMultiplexer(options = {}) {
|
|
|
115
130
|
}
|
|
116
131
|
}
|
|
117
132
|
async function* subscribe() {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
133
|
+
if (activeConsumer)
|
|
134
|
+
throw new EventMultiplexerError();
|
|
135
|
+
activeConsumer = true;
|
|
136
|
+
try {
|
|
137
|
+
while (true) {
|
|
138
|
+
if (queue.length > 0) {
|
|
139
|
+
if (options.compare)
|
|
140
|
+
queue.sort(options.compare);
|
|
141
|
+
yield queue.shift();
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (isClosed)
|
|
145
|
+
return;
|
|
146
|
+
const next = await new Promise((resolve) => {
|
|
147
|
+
waiter = resolve;
|
|
148
|
+
});
|
|
149
|
+
if (next.done)
|
|
150
|
+
return;
|
|
151
|
+
if (next.value === WAKE)
|
|
152
|
+
continue;
|
|
153
|
+
yield next.value;
|
|
124
154
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
waiter = resolve;
|
|
129
|
-
});
|
|
130
|
-
if (next.done)
|
|
131
|
-
return;
|
|
132
|
-
if (next.value === WAKE)
|
|
133
|
-
continue;
|
|
134
|
-
yield next.value;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
activeConsumer = false;
|
|
135
158
|
}
|
|
136
159
|
}
|
|
137
160
|
return {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { resolveAgentDefinition } from "./agent-definitions.js";
|
|
2
|
+
export { abortableSleep, pollDeviceCodeToken, redactOAuthError, throwIfAborted, type OAuthTokenSuccessPayload, type PollDeviceCodeTokenOptions, } from "./oauth-device-code.js";
|
|
3
|
+
export { boundResponse, defaultResolver, isLoopbackAddress, isLoopbackHostname, normalizeHostname, pinnedFetch, raceAbort, requestPinned, resolvePinnedAddress, type PinnedFetchOptions, } from "./pinned-fetch.js";
|
|
2
4
|
export type { AgentEventSourceErrorCode } from "./agent-event-source.js";
|
|
3
5
|
export { AgentEventSourceError, createMemoryAgentEventSource } from "./agent-event-source.js";
|
|
4
6
|
export { dispatchToolCallsInOrder, generateValidateReviseLoop, isAgentLoopOptions, resolveLoop, resolveToolConcurrency, singleShotLoop, } from "./agent-loops.js";
|
|
@@ -25,7 +27,7 @@ export type { ContextBudget, ContextBudgetMessageGroups, ContextBudgetOmission,
|
|
|
25
27
|
export { applyContextBudget, CONTEXT_BUDGET_ERROR_CODE, CONTEXT_BUDGET_REPORT_METADATA_KEY, ContextBudgetError, DEFAULT_MAX_CONTEXT_BUDGET_OMISSIONS, estimateAssemblyTokens, estimateMessageBytes, estimateMessageTokens, estimateTextBytes, estimateTextTokens, getContextBudgetReport, HARD_MAX_CONTEXT_BUDGET_BYTES, HARD_MAX_CONTEXT_BUDGET_OMISSIONS, HARD_MAX_CONTEXT_BUDGET_TOKENS, isContextBudgetError, resolveContextBudget, } from "./context-budget.js";
|
|
26
28
|
export type * from "./contracts.js";
|
|
27
29
|
export type { ApprovalOutcome, DecisionScope, NestedRunApproval, NestedRunOutcome, NestedRunRef, PendingDecision, PendingDecisionKind, ProviderResolver, RealtimeCaps, RealtimeEvent, RealtimeSession, RealtimeSessionFactory, RealtimeSessionOptions, ResumeNestedRun, RunDecision, RunLimitCounters, RunLimitName, SecureAgentOptions, StickyDecision, ToolCallAuthority, ToolEffectClassifier, ToolEffectDeclaration, ToolEffectIdempotency, ToolEffectKey, ToolEffectKind, ToolEffectRecord, ToolEffectStatus, ToolEffectStore, ToolEffectTransition, ToolElicitationRequest, } from "./contracts.js";
|
|
28
|
-
export { AgentDecisionError, AgentDelegationSuspendedError, AgentLoopStateError, AgentRunError, AgentRunStateError, assertSessionMetadataKey, DEFAULT_MAX_PENDING_DECISIONS, DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, DEFAULT_MAX_SESSION_SEARCH_CURSOR_BYTES, DEFAULT_MAX_SESSION_SEARCH_FTS_CANDIDATES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_QUERY_BYTES, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, DEFAULT_MAX_STICKY_DECISIONS, DEFAULT_SESSION_SEARCH_LIMIT, HARD_MAX_ACTION_CONSTRAINT_BYTES, HARD_MAX_ACTION_CONSTRAINTS, HARD_MAX_DECISION_REASON_BYTES, HARD_MAX_ELICITATION_BYTES, HARD_MAX_PENDING_DECISIONS, HARD_MAX_PENDING_STEER_BYTES, HARD_MAX_PENDING_STEERS, HARD_MAX_SESSION_SEARCH_CURSOR_BYTES, HARD_MAX_SESSION_SEARCH_FTS_CANDIDATES, HARD_MAX_SESSION_SEARCH_LIMIT, HARD_MAX_SESSION_SEARCH_LINEAR_BYTES, HARD_MAX_SESSION_SEARCH_LINEAR_ENTRIES, HARD_MAX_SESSION_SEARCH_LINEAR_SESSIONS, HARD_MAX_SESSION_SEARCH_QUERY_BYTES, HARD_MAX_SESSION_SEARCH_SNIPPET_BYTES, HARD_MAX_STICKY_DECISIONS, isSessionAppendConflict, isSessionEntryKind, isSessionSearchUnsupported, MAX_ACTION_CONSTRAINT_BYTES, MAX_ACTION_CONSTRAINTS, MAX_ATTRIBUTION_DEPTH, MAX_DECISION_REASON_BYTES, MAX_ELICITATION_BYTES, resolveSessionSearchQuery, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SESSION_SEARCH_UNSUPPORTED_CODE, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SessionAppendConflictError, SessionSearchUnsupportedError, } from "./contracts.js";
|
|
30
|
+
export { AgentDecisionError, AgentDelegationSuspendedError, AgentLoopStateError, AgentRunError, AgentRunStateError, assertSessionMetadataKey, DEFAULT_MAX_PENDING_DECISIONS, DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, DEFAULT_MAX_SESSION_SEARCH_CURSOR_BYTES, DEFAULT_MAX_SESSION_SEARCH_FTS_CANDIDATES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_QUERY_BYTES, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, DEFAULT_MAX_STICKY_DECISIONS, DEFAULT_SESSION_SEARCH_LIMIT, HARD_MAX_ACTION_CONSTRAINT_BYTES, HARD_MAX_ACTION_CONSTRAINTS, HARD_MAX_DECISION_REASON_BYTES, HARD_MAX_ELICITATION_BYTES, HARD_MAX_PENDING_DECISIONS, HARD_MAX_PENDING_STEER_BYTES, HARD_MAX_PENDING_STEERS, HARD_MAX_SESSION_SEARCH_CURSOR_BYTES, HARD_MAX_SESSION_SEARCH_FTS_CANDIDATES, HARD_MAX_SESSION_SEARCH_LIMIT, HARD_MAX_SESSION_SEARCH_LINEAR_BYTES, HARD_MAX_SESSION_SEARCH_LINEAR_ENTRIES, HARD_MAX_SESSION_SEARCH_LINEAR_SESSIONS, HARD_MAX_SESSION_SEARCH_QUERY_BYTES, HARD_MAX_SESSION_SEARCH_SNIPPET_BYTES, HARD_MAX_STICKY_DECISIONS, isSessionAppendConflict, isSessionEntryKind, isSessionMetadataConflict, isSessionSearchUnsupported, MAX_ACTION_CONSTRAINT_BYTES, MAX_ACTION_CONSTRAINTS, MAX_ATTRIBUTION_DEPTH, MAX_DECISION_REASON_BYTES, MAX_ELICITATION_BYTES, resolveSessionSearchQuery, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SESSION_METADATA_CONFLICT_CODE, SESSION_SEARCH_UNSUPPORTED_CODE, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SessionAppendConflictError, SessionMetadataConflictError, SessionSearchUnsupportedError, } from "./contracts.js";
|
|
29
31
|
export { parseAgentFile, parseSkillFile } from "./contribution-parsing.js";
|
|
30
32
|
export type { ContributionRegistries, ContributionRegistriesOptions, ContributionRegistry, ContributionRegistryOptions, } from "./contributions.js";
|
|
31
33
|
export { createContributionRegistries, createContributionRegistry, registerDiscoveredContributions } from "./contributions.js";
|
|
@@ -36,7 +38,7 @@ export { createChainedCredentialResolver, createEnvCredentialResolver, createExp
|
|
|
36
38
|
export type { DeviceAdapter, DeviceAdmitRequest, DeviceChunkResult, DeviceConformanceResult, DeviceKind, DevicePolicyErrorCode, DevicePolicyOptions, DeviceStreamLimits, ResolvedDevicePolicy, } from "./devices.js";
|
|
37
39
|
export { acceptDeviceChunk, assertDeviceAdmit, DEFAULT_DEVICE_MAX_CHUNK_BYTES, DEFAULT_DEVICE_MAX_CONCURRENT_SESSIONS, DevicePolicyError, HARD_DEVICE_MAX_CHUNK_BYTES, HARD_DEVICE_MAX_CONCURRENT_SESSIONS, redactDeviceTelemetry, resolveDevicePolicy, runDevicePolicyConformance, } from "./devices.js";
|
|
38
40
|
export type { EventMultiplexer, EventMultiplexerOptions, EventOverflowInfo, EventOverflowPolicy } from "./event-multiplexer.js";
|
|
39
|
-
export { createEventMultiplexer } from "./event-multiplexer.js";
|
|
41
|
+
export { createEventMultiplexer, EVENT_MULTIPLEXER_SINGLE_CONSUMER_CODE, EventMultiplexerError } from "./event-multiplexer.js";
|
|
40
42
|
export type { ExecutionAction, ExecutionDecision, ExecutionPolicy, ExecutionRisk } from "./execution-policy.js";
|
|
41
43
|
export { applyExecutionDecision, assertExecutionAllowed, checkExecution, ExecutionDeniedError } from "./execution-policy.js";
|
|
42
44
|
export type { ExtensionErrorPolicy, ExtensionEventBus, ExtensionEventHandler, ExtensionKernel, ExtensionKernelOptions, ExtensionLoadPolicy, LoadedExtension, } from "./extensions.js";
|
|
@@ -107,5 +109,5 @@ export { createToolParameterValidator, createToolRegistry, dispatchToolCall, fil
|
|
|
107
109
|
export type { ResolvedUseCaseModel, ResolveUseCaseModelInput, UseCaseModelBinding, } from "./use-case-model.js";
|
|
108
110
|
export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
|
|
109
111
|
export declare const name = "prism";
|
|
110
|
-
export declare const version = "0.2.
|
|
112
|
+
export declare const version = "0.2.2";
|
|
111
113
|
export declare const description = "Agent harness for AI providers, agents, sessions, and tools.";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { resolveAgentDefinition } from "./agent-definitions.js";
|
|
2
|
+
export { abortableSleep, pollDeviceCodeToken, redactOAuthError, throwIfAborted, } from "./oauth-device-code.js";
|
|
3
|
+
export { boundResponse, defaultResolver, isLoopbackAddress, isLoopbackHostname, normalizeHostname, pinnedFetch, raceAbort, requestPinned, resolvePinnedAddress, } from "./pinned-fetch.js";
|
|
2
4
|
export { AgentEventSourceError, createMemoryAgentEventSource } from "./agent-event-source.js";
|
|
3
5
|
export { dispatchToolCallsInOrder, generateValidateReviseLoop, isAgentLoopOptions, resolveLoop, resolveToolConcurrency, singleShotLoop, } from "./agent-loops.js";
|
|
4
6
|
export { createAgentRunLifecycle } from "./agent-run-lifecycle.js";
|
|
@@ -12,13 +14,13 @@ export { createDefaultCompactionStrategy, isCompactionEntryData } from "./compac
|
|
|
12
14
|
export { assertJsonObject, isJsonObject, loadConfigLayers, mergeConfigLayers } from "./config.js";
|
|
13
15
|
export { assertDeclaredMediaTypeMatches, assertMediaBlocksWithinBounds, assertMessagesSupportModelCapabilities, assertModelSupportsContentBlocks, assertSsrfAllowedUrl, collectMessageContentBlocks, contentBlockInputModality, DEFAULT_MAX_AUDIO_DURATION_MS, DEFAULT_MAX_MEDIA_ITEM_BYTES, DEFAULT_MAX_MEDIA_ITEMS_PER_REQUEST, DEFAULT_MAX_MEDIA_REQUEST_BYTES, DEFAULT_MEDIA_FETCH_TIMEOUT_MS, loadBoundedBinaryResource, MediaContentError, MODEL_INPUT_CAPABILITIES, resolveMediaContentBlock, resolveMediaContentBlocks, sniffMediaMimeType, UnsupportedModalityError, } from "./content.js";
|
|
14
16
|
export { applyContextBudget, CONTEXT_BUDGET_ERROR_CODE, CONTEXT_BUDGET_REPORT_METADATA_KEY, ContextBudgetError, DEFAULT_MAX_CONTEXT_BUDGET_OMISSIONS, estimateAssemblyTokens, estimateMessageBytes, estimateMessageTokens, estimateTextBytes, estimateTextTokens, getContextBudgetReport, HARD_MAX_CONTEXT_BUDGET_BYTES, HARD_MAX_CONTEXT_BUDGET_OMISSIONS, HARD_MAX_CONTEXT_BUDGET_TOKENS, isContextBudgetError, resolveContextBudget, } from "./context-budget.js";
|
|
15
|
-
export { AgentDecisionError, AgentDelegationSuspendedError, AgentLoopStateError, AgentRunError, AgentRunStateError, assertSessionMetadataKey, DEFAULT_MAX_PENDING_DECISIONS, DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, DEFAULT_MAX_SESSION_SEARCH_CURSOR_BYTES, DEFAULT_MAX_SESSION_SEARCH_FTS_CANDIDATES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_QUERY_BYTES, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, DEFAULT_MAX_STICKY_DECISIONS, DEFAULT_SESSION_SEARCH_LIMIT, HARD_MAX_ACTION_CONSTRAINT_BYTES, HARD_MAX_ACTION_CONSTRAINTS, HARD_MAX_DECISION_REASON_BYTES, HARD_MAX_ELICITATION_BYTES, HARD_MAX_PENDING_DECISIONS, HARD_MAX_PENDING_STEER_BYTES, HARD_MAX_PENDING_STEERS, HARD_MAX_SESSION_SEARCH_CURSOR_BYTES, HARD_MAX_SESSION_SEARCH_FTS_CANDIDATES, HARD_MAX_SESSION_SEARCH_LIMIT, HARD_MAX_SESSION_SEARCH_LINEAR_BYTES, HARD_MAX_SESSION_SEARCH_LINEAR_ENTRIES, HARD_MAX_SESSION_SEARCH_LINEAR_SESSIONS, HARD_MAX_SESSION_SEARCH_QUERY_BYTES, HARD_MAX_SESSION_SEARCH_SNIPPET_BYTES, HARD_MAX_STICKY_DECISIONS, isSessionAppendConflict, isSessionEntryKind, isSessionSearchUnsupported, MAX_ACTION_CONSTRAINT_BYTES, MAX_ACTION_CONSTRAINTS, MAX_ATTRIBUTION_DEPTH, MAX_DECISION_REASON_BYTES, MAX_ELICITATION_BYTES, resolveSessionSearchQuery, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SESSION_SEARCH_UNSUPPORTED_CODE, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SessionAppendConflictError, SessionSearchUnsupportedError, } from "./contracts.js";
|
|
17
|
+
export { AgentDecisionError, AgentDelegationSuspendedError, AgentLoopStateError, AgentRunError, AgentRunStateError, assertSessionMetadataKey, DEFAULT_MAX_PENDING_DECISIONS, DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, DEFAULT_MAX_SESSION_SEARCH_CURSOR_BYTES, DEFAULT_MAX_SESSION_SEARCH_FTS_CANDIDATES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_QUERY_BYTES, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, DEFAULT_MAX_STICKY_DECISIONS, DEFAULT_SESSION_SEARCH_LIMIT, HARD_MAX_ACTION_CONSTRAINT_BYTES, HARD_MAX_ACTION_CONSTRAINTS, HARD_MAX_DECISION_REASON_BYTES, HARD_MAX_ELICITATION_BYTES, HARD_MAX_PENDING_DECISIONS, HARD_MAX_PENDING_STEER_BYTES, HARD_MAX_PENDING_STEERS, HARD_MAX_SESSION_SEARCH_CURSOR_BYTES, HARD_MAX_SESSION_SEARCH_FTS_CANDIDATES, HARD_MAX_SESSION_SEARCH_LIMIT, HARD_MAX_SESSION_SEARCH_LINEAR_BYTES, HARD_MAX_SESSION_SEARCH_LINEAR_ENTRIES, HARD_MAX_SESSION_SEARCH_LINEAR_SESSIONS, HARD_MAX_SESSION_SEARCH_QUERY_BYTES, HARD_MAX_SESSION_SEARCH_SNIPPET_BYTES, HARD_MAX_STICKY_DECISIONS, isSessionAppendConflict, isSessionEntryKind, isSessionMetadataConflict, isSessionSearchUnsupported, MAX_ACTION_CONSTRAINT_BYTES, MAX_ACTION_CONSTRAINTS, MAX_ATTRIBUTION_DEPTH, MAX_DECISION_REASON_BYTES, MAX_ELICITATION_BYTES, resolveSessionSearchQuery, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SESSION_METADATA_CONFLICT_CODE, SESSION_SEARCH_UNSUPPORTED_CODE, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SessionAppendConflictError, SessionMetadataConflictError, SessionSearchUnsupportedError, } from "./contracts.js";
|
|
16
18
|
export { parseAgentFile, parseSkillFile } from "./contribution-parsing.js";
|
|
17
19
|
export { createContributionRegistries, createContributionRegistry, registerDiscoveredContributions } from "./contributions.js";
|
|
18
20
|
export { CONVERSATION_METADATA_KEY, ConversationError, conversationMarkerMetadata, conversationThreadFromRecord, DEFAULT_MAX_CONVERSATION_CURSOR_BYTES, decodeConversationReplayCursor, encodeConversationReplayCursor, HARD_MAX_CONVERSATION_CURSOR_BYTES, } from "./conversations.js";
|
|
19
21
|
export { createChainedCredentialResolver, createEnvCredentialResolver, createExplicitCredentialResolver, createMemoryCredentialStore, refreshOAuthCredential, resolveCredentialValue, revokeOAuthCredential, } from "./credentials.js";
|
|
20
22
|
export { acceptDeviceChunk, assertDeviceAdmit, DEFAULT_DEVICE_MAX_CHUNK_BYTES, DEFAULT_DEVICE_MAX_CONCURRENT_SESSIONS, DevicePolicyError, HARD_DEVICE_MAX_CHUNK_BYTES, HARD_DEVICE_MAX_CONCURRENT_SESSIONS, redactDeviceTelemetry, resolveDevicePolicy, runDevicePolicyConformance, } from "./devices.js";
|
|
21
|
-
export { createEventMultiplexer } from "./event-multiplexer.js";
|
|
23
|
+
export { createEventMultiplexer, EVENT_MULTIPLEXER_SINGLE_CONSUMER_CODE, EventMultiplexerError } from "./event-multiplexer.js";
|
|
22
24
|
export { applyExecutionDecision, assertExecutionAllowed, checkExecution, ExecutionDeniedError } from "./execution-policy.js";
|
|
23
25
|
export { createExtensionEventBus, createExtensionKernel } from "./extensions.js";
|
|
24
26
|
export { createMemoryRunFeedbackStore, prepareRunFeedback, RunFeedbackError, requireRunFeedbackOwnership, runFeedbackPageLimit, } from "./feedback.js";
|
|
@@ -58,6 +60,6 @@ export { DEFAULT_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES, DEFAULT_TOOL_RESULT_FOLD_MI
|
|
|
58
60
|
export { createToolParameterValidator, createToolRegistry, dispatchToolCall, filterTools } from "./tools.js";
|
|
59
61
|
export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
|
|
60
62
|
export const name = "prism";
|
|
61
|
-
export const version = "0.2.
|
|
63
|
+
export const version = "0.2.2";
|
|
62
64
|
export const description = "Agent harness for AI providers, agents, sessions, and tools.";
|
|
63
65
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { OAuthCredentials, OAuthLoginCallbacks } from "./contracts-core.js";
|
|
2
|
+
/**
|
|
3
|
+
* Shared RFC 8628 device-code flow used by both the OpenAI Codex OAuth provider
|
|
4
|
+
* (provider-openai) and the generic OAuth 2.0 provider (credentials-node).
|
|
5
|
+
* Owns the device-code request, the poll loop (authorization_pending continue,
|
|
6
|
+
* slow_down backoff, expiry deadline), cancellation, bounded success/error
|
|
7
|
+
* body reads, secret redaction, and token-shape parsing. Adapter-specific
|
|
8
|
+
* fields are parameters/callbacks, not subclasses.
|
|
9
|
+
*/
|
|
10
|
+
export declare const REDACTED = "[REDACTED]";
|
|
11
|
+
export declare const DEFAULT_DEVICE_POLL_INTERVAL_MS = 5000;
|
|
12
|
+
export declare const SLOW_DOWN_INCREMENT_MS = 5000;
|
|
13
|
+
export interface OAuthDeviceCodePayload {
|
|
14
|
+
readonly device_code: string;
|
|
15
|
+
readonly user_code: string;
|
|
16
|
+
readonly verification_uri: string;
|
|
17
|
+
readonly expires_in?: number;
|
|
18
|
+
readonly interval?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface OAuthTokenSuccessPayload {
|
|
21
|
+
readonly access_token?: string;
|
|
22
|
+
readonly refresh_token?: string;
|
|
23
|
+
readonly expires_in?: number;
|
|
24
|
+
readonly account_id?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface OAuthTokenErrorPayload {
|
|
27
|
+
readonly error?: string;
|
|
28
|
+
readonly error_description?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface PollDeviceCodeTokenOptions {
|
|
31
|
+
readonly fetchImpl: typeof fetch;
|
|
32
|
+
readonly deviceCodeUrl: string;
|
|
33
|
+
readonly tokenUrl: string;
|
|
34
|
+
readonly clientId: string;
|
|
35
|
+
readonly scope?: string;
|
|
36
|
+
/** Extra token-request params merged into every poll body (e.g. client_secret, audience). Never logged. */
|
|
37
|
+
readonly extraTokenParams?: Readonly<Record<string, string>>;
|
|
38
|
+
readonly callbacks?: Pick<OAuthLoginCallbacks, "onDeviceCode" | "signal">;
|
|
39
|
+
/** Message prefix, e.g. "OpenAI" or the adapter id. */
|
|
40
|
+
readonly errorPrefix: string;
|
|
41
|
+
/** Test seam: override wall clock for device-code expiry. */
|
|
42
|
+
readonly now?: () => number;
|
|
43
|
+
/** Test seam: override poll delay between device-code token requests. */
|
|
44
|
+
readonly sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
45
|
+
/** Token-shape parsing; adapter-specific fields (e.g. accountId fallback) applied here. */
|
|
46
|
+
readonly parseTokenCredentials: (json: OAuthTokenSuccessPayload) => OAuthCredentials;
|
|
47
|
+
}
|
|
48
|
+
export declare function throwIfAborted(signal?: AbortSignal): void;
|
|
49
|
+
export declare function abortableSleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
50
|
+
export declare function redactOAuthError(error: Error, secrets: readonly (string | undefined)[]): Error;
|
|
51
|
+
/**
|
|
52
|
+
* Request a device code, surface it through `onDeviceCode`, then poll the token
|
|
53
|
+
* endpoint until success, expiry, a terminal OAuth error, or abort. All response
|
|
54
|
+
* bodies are read under the shared byte ceiling; success bodies must be bounded
|
|
55
|
+
* JSON with an `access_token` string (fail closed otherwise); error bodies are
|
|
56
|
+
* bounded text parsed as JSON with a redacted-text fallback.
|
|
57
|
+
*/
|
|
58
|
+
export declare function pollDeviceCodeToken(options: PollDeviceCodeTokenOptions): Promise<OAuthCredentials>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import { readBoundedResponseJson, readBoundedResponseText } from "./providers/transport.js";
|
|
3
|
+
/**
|
|
4
|
+
* Shared RFC 8628 device-code flow used by both the OpenAI Codex OAuth provider
|
|
5
|
+
* (provider-openai) and the generic OAuth 2.0 provider (credentials-node).
|
|
6
|
+
* Owns the device-code request, the poll loop (authorization_pending continue,
|
|
7
|
+
* slow_down backoff, expiry deadline), cancellation, bounded success/error
|
|
8
|
+
* body reads, secret redaction, and token-shape parsing. Adapter-specific
|
|
9
|
+
* fields are parameters/callbacks, not subclasses.
|
|
10
|
+
*/
|
|
11
|
+
export const REDACTED = "[REDACTED]";
|
|
12
|
+
export const DEFAULT_DEVICE_POLL_INTERVAL_MS = 5_000;
|
|
13
|
+
export const SLOW_DOWN_INCREMENT_MS = 5_000;
|
|
14
|
+
export function throwIfAborted(signal) {
|
|
15
|
+
if (signal?.aborted)
|
|
16
|
+
throw signal.reason ?? new Error("OAuth login aborted");
|
|
17
|
+
}
|
|
18
|
+
export async function abortableSleep(ms, signal) {
|
|
19
|
+
if (ms <= 0)
|
|
20
|
+
return;
|
|
21
|
+
throwIfAborted(signal);
|
|
22
|
+
await delay(ms, undefined, { signal });
|
|
23
|
+
}
|
|
24
|
+
export function redactOAuthError(error, secrets) {
|
|
25
|
+
let message = error.message;
|
|
26
|
+
for (const secret of secrets) {
|
|
27
|
+
if (secret)
|
|
28
|
+
message = message.split(secret).join(REDACTED);
|
|
29
|
+
}
|
|
30
|
+
return new Error(message);
|
|
31
|
+
}
|
|
32
|
+
const isDeviceCodePayload = (value) => {
|
|
33
|
+
if (typeof value !== "object" || value === null)
|
|
34
|
+
return false;
|
|
35
|
+
const candidate = value;
|
|
36
|
+
return (typeof candidate.device_code === "string" &&
|
|
37
|
+
candidate.device_code.length > 0 &&
|
|
38
|
+
typeof candidate.user_code === "string" &&
|
|
39
|
+
candidate.user_code.length > 0 &&
|
|
40
|
+
typeof candidate.verification_uri === "string" &&
|
|
41
|
+
candidate.verification_uri.length > 0);
|
|
42
|
+
};
|
|
43
|
+
const isTokenSuccessPayload = (value) => {
|
|
44
|
+
if (typeof value !== "object" || value === null)
|
|
45
|
+
return false;
|
|
46
|
+
return typeof value.access_token === "string";
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Request a device code, surface it through `onDeviceCode`, then poll the token
|
|
50
|
+
* endpoint until success, expiry, a terminal OAuth error, or abort. All response
|
|
51
|
+
* bodies are read under the shared byte ceiling; success bodies must be bounded
|
|
52
|
+
* JSON with an `access_token` string (fail closed otherwise); error bodies are
|
|
53
|
+
* bounded text parsed as JSON with a redacted-text fallback.
|
|
54
|
+
*/
|
|
55
|
+
export async function pollDeviceCodeToken(options) {
|
|
56
|
+
const { fetchImpl, deviceCodeUrl, tokenUrl, clientId, scope, extraTokenParams, callbacks, errorPrefix, parseTokenCredentials } = options;
|
|
57
|
+
const now = options.now ?? Date.now;
|
|
58
|
+
const sleep = options.sleep ?? abortableSleep;
|
|
59
|
+
const requestBody = { client_id: clientId };
|
|
60
|
+
if (scope)
|
|
61
|
+
requestBody.scope = scope;
|
|
62
|
+
const response = await fetchImpl(deviceCodeUrl, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: { "content-type": "application/json" },
|
|
65
|
+
body: JSON.stringify(requestBody),
|
|
66
|
+
signal: callbacks?.signal,
|
|
67
|
+
});
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
const detail = await readBoundedResponseText(response);
|
|
70
|
+
throw redactOAuthError(new Error(`${errorPrefix} device code failed: ${response.status}${detail ? ` ${detail}` : ""}`), []);
|
|
71
|
+
}
|
|
72
|
+
const json = await readBoundedResponseJson(response, { shape: isDeviceCodePayload });
|
|
73
|
+
const secrets = [json.device_code, json.user_code];
|
|
74
|
+
const expiresAtMs = now() + (json.expires_in ?? 0) * 1_000;
|
|
75
|
+
await callbacks?.onDeviceCode?.({
|
|
76
|
+
userCode: json.user_code,
|
|
77
|
+
verificationUri: json.verification_uri,
|
|
78
|
+
expiresAt: json.expires_in ? new Date(expiresAtMs).toISOString() : undefined,
|
|
79
|
+
});
|
|
80
|
+
let intervalMs = Math.max(1, (json.interval ?? DEFAULT_DEVICE_POLL_INTERVAL_MS / 1_000) * 1_000);
|
|
81
|
+
while (now() < expiresAtMs) {
|
|
82
|
+
throwIfAborted(callbacks?.signal);
|
|
83
|
+
await sleep(intervalMs, callbacks?.signal);
|
|
84
|
+
throwIfAborted(callbacks?.signal);
|
|
85
|
+
const tokenResponse = await fetchImpl(tokenUrl, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: { "content-type": "application/json" },
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
90
|
+
client_id: clientId,
|
|
91
|
+
device_code: json.device_code,
|
|
92
|
+
...extraTokenParams,
|
|
93
|
+
}),
|
|
94
|
+
signal: callbacks?.signal,
|
|
95
|
+
});
|
|
96
|
+
if (tokenResponse.ok) {
|
|
97
|
+
const payload = await readBoundedResponseJson(tokenResponse, { shape: isTokenSuccessPayload });
|
|
98
|
+
return parseTokenCredentials(payload);
|
|
99
|
+
}
|
|
100
|
+
const errorText = await readBoundedResponseText(tokenResponse, { secrets });
|
|
101
|
+
let errorPayload;
|
|
102
|
+
try {
|
|
103
|
+
errorPayload = JSON.parse(errorText);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
errorPayload = { error: "invalid_token_response", error_description: errorText };
|
|
107
|
+
}
|
|
108
|
+
const code = errorPayload.error ?? "unknown_error";
|
|
109
|
+
if (code === "authorization_pending")
|
|
110
|
+
continue;
|
|
111
|
+
if (code === "slow_down") {
|
|
112
|
+
intervalMs += SLOW_DOWN_INCREMENT_MS;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
throw redactOAuthError(new Error(`${errorPrefix} device code login failed: ${code}${errorPayload.error_description ? ` ${errorPayload.error_description}` : ""}`), secrets);
|
|
116
|
+
}
|
|
117
|
+
throw redactOAuthError(new Error(`${errorPrefix} device code login expired before authorization completed`), secrets);
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=oauth-device-code.js.map
|