@arnilo/prism 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -1
- package/README.md +1 -0
- package/dist/field-policy.d.ts +119 -0
- package/dist/field-policy.js +418 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/redaction.d.ts +6 -5
- package/dist/redaction.js +18 -10
- package/docs/0.1.0-readiness.md +10 -10
- package/docs/acp.md +2 -0
- package/docs/audit-export.md +151 -0
- package/docs/browser-automation.md +1 -1
- package/docs/coding-agent-tools.md +5 -4
- package/docs/coding-review-and-diagnostics.md +76 -0
- package/docs/coding-security.md +2 -0
- package/docs/coding-workspaces.md +69 -0
- package/docs/data-classification.md +82 -0
- package/docs/disaster-recovery.md +71 -0
- package/docs/enterprise-postgres-state.md +60 -7
- package/docs/evaluations.md +45 -0
- package/docs/forge-integration.md +6 -0
- package/docs/host-security.md +4 -0
- package/docs/index.md +12 -7
- package/docs/indexed-code-search.md +82 -0
- package/docs/language-intelligence.md +15 -0
- package/docs/migration.md +29 -0
- package/docs/operations.md +104 -0
- package/docs/policy-and-audit.md +34 -0
- package/docs/process-sessions.md +58 -3
- package/docs/release-0.2.7-evidence.md +514 -0
- package/docs/release-and-install.md +74 -3
- package/docs/work-artifacts-and-review.md +4 -0
- package/docs/workflows.md +48 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
## [0.2.7] - 2026-08-17
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
- **Release 0.2.7 (plan 027)** is the enterprise ERP production-readiness cut on the 0.2.x review-remediation line. API surface **additive-only** (plain reviewed compat gate at 0.2.7: expected deltas are the version literal plus the plan 027 additive exports — ERP outbox/inbox stores + bounded dispatcher, saga compensation/reconciliation engine, multi-party SoD approvals, signed hash-chained audit export with WORM/SIEM seams, field-level classification + fail-closed redaction, deterministic ERP invariant evals; zero removals; baselines regenerated with `--update-baseline`, no `--allow-break`; freeze manifest `scripts/phase27-freeze-manifest.json` records per-task evidence tokens, state machines, caps, the demand registry, measured HA/DR/classification/journey numbers, and the 0.3.0 blocker). Nine roadmap items with threat model ERP-T1–ERP-T10, ownership/tenant tests, no exactly-once claim, no implicit credential discovery, package budget, docs entries, and protected end-to-end evidence: (1) **transactional outbox/inbox** (`erp-messaging`, Task 1) — `ErpOutboxStore`/`ErpInboxStore` + bounded `ErpOutboxDispatcher` with claim-token CAS, `FOR UPDATE SKIP LOCKED`, `ON CONFLICT DO NOTHING` idempotent append, at-least-once delivery with explicit unknown-outcome, dead-letter/replay requiring verified tenant `AgentIdentity`; migrations 004 (`prism_erp_outbox`/`prism_erp_inbox`). (2) **saga compensation and reconciliation** (`saga`, Task 2) — `defineSaga`/`runSaga`/`resumeSaga` over existing CheckpointStore + LeaseStore with reverse-order compensation, unknown-outcome detection, manual resolution requiring verified identity + bounded reason + audit ref, stable tenant-scoped operation keys, redacted snapshots, `MAX_SAGA_STEPS=100`. (3) **multi-party SoD approvals** (`approvals`, Task 3) — `ApprovalStore` with role/quorum rules, requester/approver separation, any-party-veto rejection, delegated authority (max depth 8), expiry checked at every protected transition, atomic grant consumption in the host transaction, `policyRevision` pin denying on mismatch; migration 005 (`prism_erp_approvals`). (4) **tamper-evident audit export** (`audit-export`, Task 4) — `createAuditExporter` with WORM-then-SIEM ordering, hash-chained record envelopes (genesis 0x64 zeros), `verifyAuditBatch` independent verification, `AuditCursorStore` CAS, SIEM best-effort pending replay (8-entry cap), legal-hold flag preservation, RFC 8785 canonical JSON for digests. (5) **secret-manager adapters** (Task 5) — Vault/AWS/Azure/GCP stay **deferred** behind the demand gate (no named consumer; no adapter ships; `scripts/phase27-demand-gate.mjs` enforces zero ambient discovery). (6) **HA registries and recovery** (`ha-dr`, Task 6) — two-replica drill on real Postgres proves failover within lease TTL+5s (measured 4100 ms vs 9000 ms ceiling), idempotent outbox re-append on uncertain-commit replay, stale fence/revision write rejection, exactly-one lease owner, tenant isolation fail-closed. (7) **backup/restore/migration rollback evidence** (Task 7) — `pg_dump`/`pg_restore` custom-format backup (108,291 B / 122 ms / 382 ms restore), 0.2.6→0.2.7 migration forward+rollback rehearsed (5 migrations), PITR RPO 0 s / RTO 1 s (recovery 1163 ms); production rollback is roll-forward repair only (no down migrations). (8) **field-level classification and fail-closed redaction** (`field-policy`, Task 8) — `applyFieldPolicy`/`FieldPolicy`/`createProtectedFieldPolicy` at the redaction, audit-export, and OpenTelemetry seams; unknown-label deny-on-outbound fail-closed default, sparse-copy walker, measured overhead peak 99.8% of the redactor-walk baseline (cap 110%). (9) **ERP release journey** (`erp-evals`, Task 9) — `erpInvariantDataset` + `createErpInvariantScorers` (8 hard 0/1 gates consuming structured facts only) + `scripts/phase27-erp-journey.test.mjs` exercising identity/policy/budget/SoD-approval/outbox/saga-compensation/audit-export/legal-hold/classification/failover/restore end-to-end (4815 ms, all 8 invariants pass). Release graph stays **50** publishable manifests at exact **0.2.7**; zero new runtime dependency names (core remains dependency-free). Store compatibility with 0.2.6: **compatible, additive** (two new ERP migrations 004/005 + outbox/inbox/approvals tables; rollback = stop 0.2.7 workers, restore the 0.2.6 manifests/tag — see `docs/migration.md` `0.2.6 → 0.2.7`). Exit gate green (core + script gates incl. `phase27-freeze` 9, `phase27-release` 12, `phase27-ha`/`phase27-dr`/`phase27-erp-journey` protected legs, coverage gate, `sdk:ready`, audit 0 moderate, pack dry-run 50/50 twice byte-identical, plain reviewed compat gate at 0.2.7 — version literal + additive exports only, release-evidence manifest with zero blocked surfaces, evidence in `scripts/phase27-release-evidence.json`). **"ERP production ready" remains blocked until the 0.3.0 live-service matrix is recorded** — passing 0.2.7 gates unblocks the release cut, not the 0.3.0 live-service matrix. **Publication remains the operator handoff** (`docs/release-and-install.md` `0.2.7 publish handoff` — signed `v0.2.7` tag + npm OIDC).
|
|
5
|
+
|
|
6
|
+
## [0.2.6] - 2026-08-16
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- **Release 0.2.6 (plan 026)** is the fully-featured coding-agent-readiness cut on the 0.2.x review-remediation line. API surface **additive-only** (plain reviewed compat gate at 0.2.6: expected deltas are the version literal plus the Task 1-6 additive exports — PTY backend/handle types, the indexed-search seam, the workspace lifecycle, process/ACP recovery, the review manifest + diagnostics; zero removals; no `--allow-break`; freeze manifest `scripts/phase26-freeze-manifest.json` records per-task evidence tokens, state machines, caps, and the demand registry). Seven roadmap items with threat model T1-T8, ownership tests, no implicit activation, package budget, docs entry, and protected end-to-end evidence: (1) **host-selected PTY/interactive terminal backend** (`pty-backend`, plan 026 Task 1) — `createProcessSessions` gains an optional `ptyBackend` seam with explicit `capabilities.resize` (never duck-typed); `pty: true` without a backend fails byte-compatibly with `ERR_PRISM_PROCESS_PTY_UNSUPPORTED` before spawn; bounded terminal geometry/TERM/attach-timeout/resize-rate/metadata caps (`ERR_PRISM_PROCESS_PTY_LIMIT`), generic `ERR_PRISM_PROCESS_PTY_BACKEND` errors that never leak backend text, NUL rejected in PTY input as a policy error, terminal output stays untrusted with no parser/emulator, backend loss surfaces as `unknown` with `exitCode: null`. (2) **scalable indexed code-search seam** (`indexed-search`, Task 2) — `createIndexedRepositoryOperations` composes a host-owned incremental index backend (`update/remove/search/status/dispose`, `capabilities.semantic` explicit) with the bounded literal search as the unchanged default; `indexed_literal`/`semantic` modes fail closed on stale/failed/unsupported/untrusted indexes (`ERR_PRISM_INDEX_*`), no silent semantic-to-literal downgrade, results labeled `untrusted_index`, containment/score/snippet caps enforced, 100k-entry benchmark p95 <= 250 ms. (3) **ownership-scoped multi-repository and worktree lifecycle** (`workspace-lifecycle`, Task 3) — `createCodingWorkspaceLifecycle` over CheckpointStore CAS + LeaseStore fencing (`prism.coding-agent.workspace.v1` records, schemaVersion 1): deterministic `ws-` ids, locked worktrees with `prism-workspace:` reasons, credential-free remote fingerprints (sha256 of redacted URL + default branch, never the URL), idempotent create, verify revalidating repository/worktree identity, and a cleanup refusal matrix (dirty/locked/unowned/missing/mismatched/main) with `ERR_PRISM_WORKSPACE_*` codes; `GitOperations` gains worktree `lock`/`unlock` and `fingerprint()`. (4) **forge breadth demand-gated** (Task 4) — GitLab/Bitbucket adapters stay **deferred** in the demand registry (no named consumer/date/use case recorded), so no adapter source ships and the forge barrel contains no provider name; activation requires a recorded named consumer. (5) **durable ACP/live-task and managed-process recovery** (`durable-recovery`, Task 5) — `ProcessSessions` persists bounded intent/metadata before spawn in `prism.coding-agent.process.v1` records (never handles/env/tokens/raw output), serializes per-record transition CAS writes, and `recover()` is attach-if-attested reporting `attached|terminal|unknown` with no fabricated exit code and no PID probing; per-record leases (30 s/300 s) fence two replicas (split-brain conformance green on real Postgres); `PersistedAcpSession` gains an additive optional `activeRun` ref (0.2.5 records stay readable) and `createAcpRunRecovery` re-resolves status against `AgentRunLifecycle` (suspended keeps pending approval ids, unprovable in-flight -> unknown, never a restarted prompt) with durable ownership/version/fence-checked cancellation in `prism.coding-agent.cancel.v1` markers that never replays a pending/dispatched tool; `ERR_PRISM_RECOVERY_*` codes. (6) **bounded patch review and incremental diagnostics** (`review-diagnostics`, Task 6) — `createCodingPatchReviewManifest` builds a digest-bound manifest (patch sha256 + repository/worktree/base/head identity + changed paths/diffstat + checks + diagnostic summaries, never a raw patch body) composed over the server `ArtifactService`; `assertCodingPatchAccepted` derives `pending|accepted|rejected|superseded` bound to the exact artifact revision and digest, refuses stale acceptance, and never applies/commits/pushes/merges; `LanguageIntelligence` gains opt-in full-content `syncDocument` (monotonic versions) and `diagnosticDelta` (push/pull with resultId reuse, stale-version guards) plus `normalizeDiagnostics` with deterministic added/removed/unchanged deltas and host-supplied check parsers; LSP stays strictly opt-in (nothing spawns from tool factories or agent assembly). (7) **protected real coding journey** (`coding-journey`, Task 7) — `scripts/phase26-coding-journey.test.mjs` runs a packed consumer through real host services only (provider call, digest-pinned Docker sandbox, Postgres worktree lifecycle, provider-driven ACP edit with policy approval, named check + `diagnosticDelta`, patch review over the server artifact store, cross-replica process recovery, durable cancellation, real GitHub push/lookup-before-create PR/reconcile/cleanup, host Playwright inspection, host PTY adapter in the frozen profile) under the frozen wall/cleanup ceilings with run-suffix side effects and per-step idempotent cleanup; missing credentials/services or skipped substeps record `blocked`, never a passing skip; the retained `scripts/phase26-coding-journey-report.json` carries timings/states/ids only and gates release evidence (pass/blocked/protected). Release graph stays **50** publishable manifests at exact **0.2.6**; zero new runtime dependency names (core remains dependency-free). Store compatibility with 0.2.5: **compatible, additive** (three new versioned record namespaces + the optional ACP `activeRun` field; rollback = stop 0.2.6 workers, mark active PTY/process/recovery records unknown, restore the 0.2.5 manifests/tag — see `docs/migration.md` `0.2.5 → 0.2.6`). Exit gate green (core + script gates incl. `phase26-freeze` 11, `phase26-index-benchmark`, coding-agent 348 + ag-ui 203 workspace suites, coverage gate, `sdk:ready`, audit 0 moderate, pack dry-run 50/50 twice byte-identical, plain reviewed compat gate at 0.2.6 — version literal + additive exports only, protected Postgres recovery/workspace conformance 8/8, protected PTY leg 4/4, protected real coding journey per the retained report, release-evidence manifest with zero blocked surfaces, evidence in `scripts/phase26-baseline.json`). **Publication remains the operator handoff** (`docs/release-and-install.md` `0.2.6 publish handoff` — signed `v0.2.6` tag + npm OIDC).
|
|
2
10
|
|
|
3
11
|
## [0.2.5] - 2026-08-15
|
|
4
12
|
|
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ packages. Prism defines contracts, not apps.
|
|
|
11
11
|
- **Agent/session runtime**: `createAgent`/`createAgentSession`, run prompts,
|
|
12
12
|
dispatch host tools, subscribe to normalized `AgentEvent` streams, abort runs,
|
|
13
13
|
compact, and navigate branches.
|
|
14
|
+
- **Field-level classification (0.2.7)**: `applyFieldPolicy` + the fail-closed protected default walk JSON-like values across prompt/tool/artifact/audit/telemetry/persistence/export boundaries with `allow`/`redact`/`tokenize`/`deny` decisions, explicit per-boundary `labelFor` hints, bounded traversal, and sparse-copy allocation; seams at the egress redaction functions, the audit-export redactor hook, and the OpenTelemetry attribute policy. See [docs/data-classification.md](docs/data-classification.md).
|
|
14
15
|
- **Providers and models**: provider/model registries, provider event helpers,
|
|
15
16
|
credential redaction helpers, mock provider, and an optional
|
|
16
17
|
OpenAI-compatible provider subpath. Cache support is provider-specific:
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field-level classification and fail-closed redaction (plan 027 Task 8).
|
|
3
|
+
*
|
|
4
|
+
* A dependency-free, JSON-like structured-value walker that evaluates an
|
|
5
|
+
* explicit host policy against every field of a value destined to cross a
|
|
6
|
+
* data boundary (provider prompt egress, tool dispatch/result persistence,
|
|
7
|
+
* artifact write/read/export, audit hashing, telemetry attributes/events,
|
|
8
|
+
* lifecycle export). The policy classifies fields by label, path, destination,
|
|
9
|
+
* and tenant; unknown fields fail closed under the protected default.
|
|
10
|
+
*
|
|
11
|
+
* Non-goals (frozen): no automatic sensitive-data discovery (labels come from
|
|
12
|
+
* an explicit `labelFor` hint function supplied by the boundary owner), no
|
|
13
|
+
* global registry, no decorator framework, no second policy language. Existing
|
|
14
|
+
* hardcoded secret redaction (`createSecretRedactor`) stays in place as
|
|
15
|
+
* defense in depth and runs *after* policy transformation at egress seams.
|
|
16
|
+
*/
|
|
17
|
+
export type FieldPolicyAction = "allow" | "redact" | "tokenize" | "deny";
|
|
18
|
+
export interface FieldPolicyDecision {
|
|
19
|
+
readonly action: FieldPolicyAction;
|
|
20
|
+
/** Bounded reason recorded in audit redaction lists; never includes values. */
|
|
21
|
+
readonly reason?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface FieldPolicyInput {
|
|
24
|
+
/** Dot/bracket path from the value root, e.g. `user.credentials[2].apiKey`. */
|
|
25
|
+
readonly path: string;
|
|
26
|
+
/** Boundary destination: prompt, tool, artifact, audit, telemetry, persistence, export, ... */
|
|
27
|
+
readonly destination: string;
|
|
28
|
+
/** Explicit caller-assigned label from `labelFor`; undefined = unknown. */
|
|
29
|
+
readonly label?: string;
|
|
30
|
+
readonly kind: "string" | "number" | "boolean" | "null" | "array" | "object";
|
|
31
|
+
readonly tenantId?: string;
|
|
32
|
+
readonly direction: "inbound" | "outbound";
|
|
33
|
+
readonly purpose?: string;
|
|
34
|
+
}
|
|
35
|
+
export type FieldPolicy = (input: FieldPolicyInput) => FieldPolicyDecision;
|
|
36
|
+
export interface ApplyFieldPolicyOptions {
|
|
37
|
+
readonly destination: string;
|
|
38
|
+
readonly direction?: "inbound" | "outbound";
|
|
39
|
+
readonly tenantId?: string;
|
|
40
|
+
readonly purpose?: string;
|
|
41
|
+
/** Explicit key→label hints owned by the boundary; no automatic discovery. */
|
|
42
|
+
readonly labelFor?: (key: string, path: string) => string | undefined;
|
|
43
|
+
/** Called for every non-allow decision with {path, reason}; used by audit adapters. */
|
|
44
|
+
readonly onRedact?: (path: string, reason: string) => void;
|
|
45
|
+
readonly maxDepth?: number;
|
|
46
|
+
readonly maxKeys?: number;
|
|
47
|
+
readonly maxChars?: number;
|
|
48
|
+
/** Wall-clock budget for the whole walk; a slow policy trips it (fail closed). */
|
|
49
|
+
readonly maxPolicyMs?: number;
|
|
50
|
+
/** Deterministic token prefix (stable across runs; safe for audit chains). */
|
|
51
|
+
readonly tokenPrefix?: string;
|
|
52
|
+
}
|
|
53
|
+
export declare class FieldPolicyError extends Error {
|
|
54
|
+
readonly code = "ERR_PRISM_FIELD_POLICY";
|
|
55
|
+
readonly path: string;
|
|
56
|
+
constructor(path: string, message: string);
|
|
57
|
+
}
|
|
58
|
+
export declare const FIELD_POLICY_LIMITS: Readonly<{
|
|
59
|
+
readonly maxDepth: 32;
|
|
60
|
+
readonly maxKeys: 10000;
|
|
61
|
+
readonly maxChars: 1000000;
|
|
62
|
+
readonly maxPolicyMs: 5000;
|
|
63
|
+
readonly tokenPrefix: "tok_";
|
|
64
|
+
}>;
|
|
65
|
+
/**
|
|
66
|
+
* Applies `policy` to every field of a JSON-like value.
|
|
67
|
+
*
|
|
68
|
+
* Decisions: `allow` keeps the value; `redact` replaces string leaves with
|
|
69
|
+
* `[REDACTED]` while preserving container shape; `tokenize` replaces string
|
|
70
|
+
* leaves with a deterministic token; `deny` replaces the value with `[DENIED]`.
|
|
71
|
+
* Unknown labels fail closed under the protected default. Hostile values
|
|
72
|
+
* (cycles, unsupported types, over-budget walks) throw `FieldPolicyError`
|
|
73
|
+
* instead of stringifying guesses. The input is never mutated; only fields the
|
|
74
|
+
* policy changed are allocated.
|
|
75
|
+
*/
|
|
76
|
+
export declare function applyFieldPolicy<T>(value: T, policy: FieldPolicy, options: ApplyFieldPolicyOptions): T;
|
|
77
|
+
export interface ProtectedFieldPolicyOptions {
|
|
78
|
+
/** Labels allowed to cross outbound/persisted boundaries unchanged. */
|
|
79
|
+
readonly publicLabels?: readonly string[];
|
|
80
|
+
/** Labels denied with the given reason; default `secret` and `financial`. */
|
|
81
|
+
readonly deniedLabels?: Readonly<Record<string, string>>;
|
|
82
|
+
/** Labels redacted with the given reason; default `personal`. */
|
|
83
|
+
readonly redactedLabels?: Readonly<Record<string, string>>;
|
|
84
|
+
/** Labels tokenized; default none. */
|
|
85
|
+
readonly tokenizedLabels?: Readonly<Record<string, string>>;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Fail-closed default for the protected profile (frozen behavior):
|
|
89
|
+
* unknown labels are denied on outbound boundaries and allowed inbound;
|
|
90
|
+
* `secret`/`financial` denied, `personal` redacted, listed public labels
|
|
91
|
+
* allowed. Labels are never auto-discovered — `labelFor` decides them.
|
|
92
|
+
*/
|
|
93
|
+
export declare function createProtectedFieldPolicy(options?: ProtectedFieldPolicyOptions): FieldPolicy;
|
|
94
|
+
/** Identity policy: every field allowed (explicit override for trusted marks). */
|
|
95
|
+
export declare const ALLOW_FIELD_POLICY: FieldPolicy;
|
|
96
|
+
export interface AuditFieldRedaction {
|
|
97
|
+
readonly path: string;
|
|
98
|
+
readonly reason: string;
|
|
99
|
+
}
|
|
100
|
+
/** Structural match for `AuditRedactionPolicy` in @arnilo/prism-policy. */
|
|
101
|
+
export interface AuditFieldRedactorLike {
|
|
102
|
+
apply(record: Readonly<Record<string, unknown>>): {
|
|
103
|
+
record: Readonly<Record<string, unknown>>;
|
|
104
|
+
redactions?: readonly AuditFieldRedaction[];
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export interface AuditFieldRedactorOptions {
|
|
108
|
+
readonly tenantId?: string;
|
|
109
|
+
readonly purpose?: string;
|
|
110
|
+
readonly labelFor?: (key: string, path: string) => string | undefined;
|
|
111
|
+
/** Default true; denial instead would fail export (protected profile fails closed). */
|
|
112
|
+
readonly collectProvenance?: boolean;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Adapts a `FieldPolicy` to the audit-export redaction hook. Transformation
|
|
116
|
+
* runs BEFORE canonical hashing so the exported chain verifies exactly.
|
|
117
|
+
* Only `{path, reason}` provenance survives; values never echo into the list.
|
|
118
|
+
*/
|
|
119
|
+
export declare function createAuditFieldRedactor(policy: FieldPolicy, options?: AuditFieldRedactorOptions): AuditFieldRedactorLike;
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field-level classification and fail-closed redaction (plan 027 Task 8).
|
|
3
|
+
*
|
|
4
|
+
* A dependency-free, JSON-like structured-value walker that evaluates an
|
|
5
|
+
* explicit host policy against every field of a value destined to cross a
|
|
6
|
+
* data boundary (provider prompt egress, tool dispatch/result persistence,
|
|
7
|
+
* artifact write/read/export, audit hashing, telemetry attributes/events,
|
|
8
|
+
* lifecycle export). The policy classifies fields by label, path, destination,
|
|
9
|
+
* and tenant; unknown fields fail closed under the protected default.
|
|
10
|
+
*
|
|
11
|
+
* Non-goals (frozen): no automatic sensitive-data discovery (labels come from
|
|
12
|
+
* an explicit `labelFor` hint function supplied by the boundary owner), no
|
|
13
|
+
* global registry, no decorator framework, no second policy language. Existing
|
|
14
|
+
* hardcoded secret redaction (`createSecretRedactor`) stays in place as
|
|
15
|
+
* defense in depth and runs *after* policy transformation at egress seams.
|
|
16
|
+
*/
|
|
17
|
+
export class FieldPolicyError extends Error {
|
|
18
|
+
code = "ERR_PRISM_FIELD_POLICY";
|
|
19
|
+
path;
|
|
20
|
+
constructor(path, message) {
|
|
21
|
+
super(`field policy rejected value at ${path}: ${message}`);
|
|
22
|
+
this.name = "FieldPolicyError";
|
|
23
|
+
this.path = path;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export const FIELD_POLICY_LIMITS = Object.freeze({
|
|
27
|
+
maxDepth: 32,
|
|
28
|
+
maxKeys: 10_000,
|
|
29
|
+
maxChars: 1_000_000,
|
|
30
|
+
maxPolicyMs: 5_000,
|
|
31
|
+
tokenPrefix: "tok_",
|
|
32
|
+
});
|
|
33
|
+
const REDACTED = "[REDACTED]";
|
|
34
|
+
const DENIED = "[DENIED]";
|
|
35
|
+
function tokenOf(prefix, path, original, sha) {
|
|
36
|
+
const hash = sha(`${path}\u0000${original}`);
|
|
37
|
+
return `${prefix}${hash.slice(0, 12)}`;
|
|
38
|
+
}
|
|
39
|
+
const ALLOW = { action: "allow" };
|
|
40
|
+
const DENY_UNKNOWN = Object.freeze({ action: "deny", reason: "unknown-field" });
|
|
41
|
+
const DENY_UNKNOWN_LABEL = Object.freeze({ action: "deny", reason: "unknown-label" });
|
|
42
|
+
const denied = (reason) => Object.freeze({ action: "deny", reason });
|
|
43
|
+
function normalizeDecision(value, path) {
|
|
44
|
+
if (value && typeof value === "object" && typeof value.action === "string") {
|
|
45
|
+
const candidate = value;
|
|
46
|
+
const action = candidate.action;
|
|
47
|
+
if (action === "allow" || action === "redact" || action === "tokenize" || action === "deny") {
|
|
48
|
+
// Returned decision objects are used as-is (host policies may reuse
|
|
49
|
+
// frozen constants; a hostile mutation is their own risk).
|
|
50
|
+
if (candidate.reason !== undefined && typeof candidate.reason !== "string") {
|
|
51
|
+
throw new FieldPolicyError(path, "policy returned an invalid reason");
|
|
52
|
+
}
|
|
53
|
+
return candidate;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
throw new FieldPolicyError(path, "policy returned an invalid decision");
|
|
57
|
+
}
|
|
58
|
+
const JSON_PRIMITIVE_KINDS = new Set(["string", "number", "boolean"]);
|
|
59
|
+
const kindOf = (value) => {
|
|
60
|
+
if (value === null)
|
|
61
|
+
return "null";
|
|
62
|
+
if (Array.isArray(value))
|
|
63
|
+
return "array";
|
|
64
|
+
if (typeof value === "object")
|
|
65
|
+
return "object";
|
|
66
|
+
return (JSON_PRIMITIVE_KINDS.has(typeof value) ? typeof value : "object");
|
|
67
|
+
};
|
|
68
|
+
function checkLimits(ctx, path, depth) {
|
|
69
|
+
// Time budget only when explicitly configured; the default walk is pure
|
|
70
|
+
// structural (depth/keys/bytes below), so the hot path avoids Date.now().
|
|
71
|
+
// Time budget only when explicitly configured; the default walk is pure
|
|
72
|
+
// structural (depth/keys/bytes below), so the hot path avoids Date.now().
|
|
73
|
+
if (ctx.hasTimeBudget && Date.now() - ctx.startedAt > ctx.maxPolicyMs) {
|
|
74
|
+
throw new FieldPolicyError(path, `policy walk exceeded ${ctx.maxPolicyMs}ms`);
|
|
75
|
+
}
|
|
76
|
+
if (depth > ctx.maxDepth)
|
|
77
|
+
throw new FieldPolicyError(path, `depth exceeds ${ctx.maxDepth}`);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Applies `policy` to every field of a JSON-like value.
|
|
81
|
+
*
|
|
82
|
+
* Decisions: `allow` keeps the value; `redact` replaces string leaves with
|
|
83
|
+
* `[REDACTED]` while preserving container shape; `tokenize` replaces string
|
|
84
|
+
* leaves with a deterministic token; `deny` replaces the value with `[DENIED]`.
|
|
85
|
+
* Unknown labels fail closed under the protected default. Hostile values
|
|
86
|
+
* (cycles, unsupported types, over-budget walks) throw `FieldPolicyError`
|
|
87
|
+
* instead of stringifying guesses. The input is never mutated; only fields the
|
|
88
|
+
* policy changed are allocated.
|
|
89
|
+
*/
|
|
90
|
+
export function applyFieldPolicy(value, policy, options) {
|
|
91
|
+
if (typeof policy !== "function")
|
|
92
|
+
throw new FieldPolicyError("", "policy must be a function");
|
|
93
|
+
const ctx = {
|
|
94
|
+
policy,
|
|
95
|
+
options,
|
|
96
|
+
tokenSha: (text) => sha256Hex(text),
|
|
97
|
+
maxDepth: options.maxDepth ?? FIELD_POLICY_LIMITS.maxDepth,
|
|
98
|
+
maxKeys: options.maxKeys ?? FIELD_POLICY_LIMITS.maxKeys,
|
|
99
|
+
maxChars: options.maxChars ?? FIELD_POLICY_LIMITS.maxChars,
|
|
100
|
+
hasTimeBudget: options.maxPolicyMs !== undefined,
|
|
101
|
+
maxPolicyMs: options.maxPolicyMs,
|
|
102
|
+
labelCache: new Map(),
|
|
103
|
+
input: {
|
|
104
|
+
path: "",
|
|
105
|
+
destination: options.destination,
|
|
106
|
+
label: undefined,
|
|
107
|
+
kind: "object",
|
|
108
|
+
direction: options.direction ?? "outbound",
|
|
109
|
+
...(options.tenantId !== undefined ? { tenantId: options.tenantId } : {}),
|
|
110
|
+
...(options.purpose !== undefined ? { purpose: options.purpose } : {}),
|
|
111
|
+
},
|
|
112
|
+
budgetRemaining: options.maxChars ?? FIELD_POLICY_LIMITS.maxChars,
|
|
113
|
+
nodesVisited: 0,
|
|
114
|
+
startedAt: Date.now(),
|
|
115
|
+
};
|
|
116
|
+
return walkValue(value, ctx, "", 0);
|
|
117
|
+
}
|
|
118
|
+
function walkValue(input, ctx, path, depth) {
|
|
119
|
+
const active = new Set();
|
|
120
|
+
const climb = (value, p, d, key) => {
|
|
121
|
+
if (value === null || typeof value !== "object") {
|
|
122
|
+
return leaf(value, ctx, p, d, key);
|
|
123
|
+
}
|
|
124
|
+
if (value instanceof Date || value instanceof RegExp || ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
|
|
125
|
+
return leaf(value, ctx, p, d, key);
|
|
126
|
+
}
|
|
127
|
+
if (d > ctx.maxDepth)
|
|
128
|
+
throw new FieldPolicyError(p, `depth exceeds ${ctx.maxDepth}`);
|
|
129
|
+
if (active.has(value))
|
|
130
|
+
throw new FieldPolicyError(p, "cyclic reference is not supported (fail closed)");
|
|
131
|
+
active.add(value);
|
|
132
|
+
try {
|
|
133
|
+
// The root has no key and never substitutes the whole payload; its own
|
|
134
|
+
// decision governs the walk of children only.
|
|
135
|
+
const decision = p === "" ? ALLOW : decide(ctx, value, p, d, undefined, key);
|
|
136
|
+
if (decision.action === "deny")
|
|
137
|
+
return deny();
|
|
138
|
+
if (decision.action === "redact")
|
|
139
|
+
return shapePreserving(value, ctx, p, d, "redact", active);
|
|
140
|
+
if (decision.action === "tokenize")
|
|
141
|
+
return shapePreserving(value, ctx, p, d, "tokenize", active);
|
|
142
|
+
return walkChildren(value, ctx, p, d, climb);
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
active.delete(value);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
return climb(input, path, depth, "");
|
|
149
|
+
}
|
|
150
|
+
/** labelFor results are key-scoped (paths only reach the first call); cached per walk. */
|
|
151
|
+
function labelForKey(ctx, key, path) {
|
|
152
|
+
const labelFor = ctx.options.labelFor;
|
|
153
|
+
if (!labelFor)
|
|
154
|
+
return undefined;
|
|
155
|
+
if (ctx.labelCache.has(key))
|
|
156
|
+
return ctx.labelCache.get(key);
|
|
157
|
+
const label = labelFor(key, path);
|
|
158
|
+
ctx.labelCache.set(key, label);
|
|
159
|
+
return label;
|
|
160
|
+
}
|
|
161
|
+
function decide(ctx, value, path, depth, override, key) {
|
|
162
|
+
if (override)
|
|
163
|
+
return override;
|
|
164
|
+
const kind = kindOf(value);
|
|
165
|
+
const label = labelForKey(ctx, key, path);
|
|
166
|
+
const input = ctx.input; // reused per node; see the reuse contract on WalkContext
|
|
167
|
+
input.path = path;
|
|
168
|
+
input.label = label;
|
|
169
|
+
input.kind = kind;
|
|
170
|
+
let decision;
|
|
171
|
+
try {
|
|
172
|
+
decision = ctx.policy(input);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
throw new FieldPolicyError(path, `policy threw ${error instanceof Error ? error.name : typeof error} (value never echoed)`);
|
|
176
|
+
}
|
|
177
|
+
const normalized = normalizeDecision(decision, path);
|
|
178
|
+
checkLimits(ctx, path, depth); // slow policy over budget trips here
|
|
179
|
+
if (normalized.action !== "allow") {
|
|
180
|
+
ctx.options.onRedact?.(path, normalized.reason ?? normalized.action);
|
|
181
|
+
}
|
|
182
|
+
return normalized;
|
|
183
|
+
}
|
|
184
|
+
function leaf(input, ctx, path, depth, key) {
|
|
185
|
+
if (input === undefined || typeof input === "bigint" || typeof input === "symbol" || typeof input === "function") {
|
|
186
|
+
throw new FieldPolicyError(path, `unsupported value type ${typeof input} (fail closed, no stringification)`);
|
|
187
|
+
}
|
|
188
|
+
if (typeof input === "string") {
|
|
189
|
+
ctx.budgetRemaining -= input.length;
|
|
190
|
+
if (ctx.budgetRemaining < 0)
|
|
191
|
+
throw new FieldPolicyError(path, `string budget exceeds ${ctx.maxChars}`);
|
|
192
|
+
}
|
|
193
|
+
const decision = decide(ctx, input, path, depth, undefined, key);
|
|
194
|
+
switch (decision.action) {
|
|
195
|
+
case "allow":
|
|
196
|
+
return input;
|
|
197
|
+
case "redact":
|
|
198
|
+
return typeof input === "string" ? REDACTED : input;
|
|
199
|
+
case "tokenize":
|
|
200
|
+
return typeof input === "string"
|
|
201
|
+
? tokenOf(ctx.options.tokenPrefix ?? FIELD_POLICY_LIMITS.tokenPrefix, path, input, ctx.tokenSha)
|
|
202
|
+
: input;
|
|
203
|
+
case "deny":
|
|
204
|
+
return deny();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// decide() already recorded the redaction entry for this path
|
|
208
|
+
function deny() {
|
|
209
|
+
return DENIED;
|
|
210
|
+
}
|
|
211
|
+
function shapePreserving(input, ctx, path, depth, mode, active) {
|
|
212
|
+
if (active.has(input))
|
|
213
|
+
throw new FieldPolicyError(path, "cyclic reference is not supported (fail closed)");
|
|
214
|
+
const forced = { action: mode };
|
|
215
|
+
if (Array.isArray(input)) {
|
|
216
|
+
const out = new Array(input.length);
|
|
217
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
218
|
+
out[index] = safeChild(input[index], ctx, `${path}[${index}]`, depth + 1, forced, active);
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
if (isPlainRecord(input)) {
|
|
223
|
+
const out = {};
|
|
224
|
+
let keysChecked = 0;
|
|
225
|
+
for (const [key, item] of Object.entries(input)) {
|
|
226
|
+
keysChecked += 1;
|
|
227
|
+
if (keysChecked > ctx.maxKeys)
|
|
228
|
+
throw new FieldPolicyError(`${path}.${key}`, `key count exceeds ${ctx.maxKeys}`);
|
|
229
|
+
out[key] = safeChild(item, ctx, joinPath(path, key), depth + 1, forced, active);
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
if (input instanceof Map) {
|
|
234
|
+
const out = {};
|
|
235
|
+
for (const [key, item] of input) {
|
|
236
|
+
out[String(key)] = safeChild(item, ctx, joinPath(path, String(key)), depth + 1, forced, active);
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
if (input instanceof Set) {
|
|
241
|
+
return [...input].map((item, index) => safeChild(item, ctx, `${path}[${index}]`, depth + 1, forced, active));
|
|
242
|
+
}
|
|
243
|
+
throw new FieldPolicyError(path, `unsupported value ${input?.constructor?.name ?? "object"} (fail closed, no stringification)`);
|
|
244
|
+
}
|
|
245
|
+
function walkChildren(input, ctx, path, depth, climb) {
|
|
246
|
+
if (Array.isArray(input)) {
|
|
247
|
+
let changed = false;
|
|
248
|
+
const out = new Array(input.length);
|
|
249
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
250
|
+
const item = input[index];
|
|
251
|
+
const next = climb(item, `${path}[${index}]`, depth + 1, String(index));
|
|
252
|
+
if (next !== item)
|
|
253
|
+
changed = true;
|
|
254
|
+
out[index] = next;
|
|
255
|
+
}
|
|
256
|
+
return changed ? out : input;
|
|
257
|
+
}
|
|
258
|
+
if (isPlainRecord(input)) {
|
|
259
|
+
let changed = false;
|
|
260
|
+
let keysChecked = 0;
|
|
261
|
+
const out = {};
|
|
262
|
+
for (const key in input) {
|
|
263
|
+
if (!Object.hasOwn(input, key))
|
|
264
|
+
continue;
|
|
265
|
+
keysChecked += 1;
|
|
266
|
+
if (keysChecked > ctx.maxKeys)
|
|
267
|
+
throw new FieldPolicyError(joinPath(path, key), `key count exceeds ${ctx.maxKeys}`);
|
|
268
|
+
const item = input[key];
|
|
269
|
+
if (item === undefined)
|
|
270
|
+
continue;
|
|
271
|
+
const next = climb(item, joinPath(path, key), depth + 1, key);
|
|
272
|
+
out[key] = next;
|
|
273
|
+
if (next !== item)
|
|
274
|
+
changed = true;
|
|
275
|
+
}
|
|
276
|
+
return changed ? out : input; // sparse copy: untouched subtrees share the input
|
|
277
|
+
}
|
|
278
|
+
if (input instanceof Map) {
|
|
279
|
+
let changed = false;
|
|
280
|
+
const out = {};
|
|
281
|
+
for (const [entryKey, item] of input) {
|
|
282
|
+
out[String(entryKey)] = climb(item, joinPath(path, String(entryKey)), depth + 1, String(entryKey));
|
|
283
|
+
if (out[String(entryKey)] !== item)
|
|
284
|
+
changed = true;
|
|
285
|
+
}
|
|
286
|
+
return changed ? out : input;
|
|
287
|
+
}
|
|
288
|
+
if (input instanceof Set) {
|
|
289
|
+
let changed = false;
|
|
290
|
+
const items = [...input];
|
|
291
|
+
const out = new Array(items.length);
|
|
292
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
293
|
+
out[index] = climb(items[index], `${path}[${index}]`, depth + 1, String(index));
|
|
294
|
+
if (out[index] !== items[index])
|
|
295
|
+
changed = true;
|
|
296
|
+
}
|
|
297
|
+
return changed ? out : input;
|
|
298
|
+
}
|
|
299
|
+
throw new FieldPolicyError(path, `unsupported value ${input?.constructor?.name ?? "object"} (fail closed, no stringification)`);
|
|
300
|
+
}
|
|
301
|
+
function safeChild(item, ctx, path, depth, forced, active) {
|
|
302
|
+
if (item === null || typeof item !== "object") {
|
|
303
|
+
return applyForcedLeaf(item, ctx, path, forced);
|
|
304
|
+
}
|
|
305
|
+
if (item instanceof Date || item instanceof RegExp || ArrayBuffer.isView(item) || item instanceof ArrayBuffer) {
|
|
306
|
+
return item;
|
|
307
|
+
}
|
|
308
|
+
checkLimits(ctx, path, depth);
|
|
309
|
+
return shapePreserving(item, ctx, path, depth, forced.action, active);
|
|
310
|
+
}
|
|
311
|
+
function applyForcedLeaf(item, ctx, path, forced) {
|
|
312
|
+
if (typeof item === "string") {
|
|
313
|
+
ctx.budgetRemaining -= item.length;
|
|
314
|
+
if (ctx.budgetRemaining < 0)
|
|
315
|
+
throw new FieldPolicyError(path, `string budget exceeds ${ctx.maxChars}`);
|
|
316
|
+
}
|
|
317
|
+
switch (forced.action) {
|
|
318
|
+
case "redact":
|
|
319
|
+
return typeof item === "string" ? REDACTED : item;
|
|
320
|
+
case "tokenize":
|
|
321
|
+
return typeof item === "string"
|
|
322
|
+
? tokenOf(ctx.options.tokenPrefix ?? FIELD_POLICY_LIMITS.tokenPrefix, path, item, ctx.tokenSha)
|
|
323
|
+
: item;
|
|
324
|
+
case "deny":
|
|
325
|
+
return DENIED;
|
|
326
|
+
default:
|
|
327
|
+
return item;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function isPlainRecord(value) {
|
|
331
|
+
if (value === null || typeof value !== "object")
|
|
332
|
+
return false;
|
|
333
|
+
const proto = Object.getPrototypeOf(value);
|
|
334
|
+
return proto === Object.prototype || proto === null;
|
|
335
|
+
}
|
|
336
|
+
/** `a.b` for object keys; arrays use `[i]`; root fields use bare `key`. */
|
|
337
|
+
function joinPath(base, key) {
|
|
338
|
+
return base ? `${base}.${key}` : key;
|
|
339
|
+
}
|
|
340
|
+
function sha256Hex(text) {
|
|
341
|
+
// FNV-1a is enough for token differentiation and avoids a crypto import at
|
|
342
|
+
// every leaf; collisions change a token value, never cross boundaries.
|
|
343
|
+
let hash = 0x811c9dc5;
|
|
344
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
345
|
+
hash ^= text.charCodeAt(i);
|
|
346
|
+
hash = Math.imul(hash, 0x01000193);
|
|
347
|
+
}
|
|
348
|
+
return (hash >>> 0).toString(16).padStart(8, "0") + (hash >>> 8).toString(16).padStart(8, "0").slice(0, 4);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Fail-closed default for the protected profile (frozen behavior):
|
|
352
|
+
* unknown labels are denied on outbound boundaries and allowed inbound;
|
|
353
|
+
* `secret`/`financial` denied, `personal` redacted, listed public labels
|
|
354
|
+
* allowed. Labels are never auto-discovered — `labelFor` decides them.
|
|
355
|
+
*/
|
|
356
|
+
export function createProtectedFieldPolicy(options = {}) {
|
|
357
|
+
const publicLabels = new Set(options.publicLabels ?? ["public"]);
|
|
358
|
+
const deniedLabels = {
|
|
359
|
+
secret: "secret-egress",
|
|
360
|
+
financial: "financial-egress",
|
|
361
|
+
...options.deniedLabels,
|
|
362
|
+
};
|
|
363
|
+
const LABEL_DENY_REASONS = deniedLabels;
|
|
364
|
+
const redactedLabels = {
|
|
365
|
+
personal: "personal-data",
|
|
366
|
+
...options.redactedLabels,
|
|
367
|
+
};
|
|
368
|
+
const tokenizedLabels = { token: "tokenization", ...options.tokenizedLabels };
|
|
369
|
+
const outbound = new Set(["prompt", "tool", "artifact", "audit", "telemetry", "export", "persistence"]);
|
|
370
|
+
return (input) => {
|
|
371
|
+
const { label } = input;
|
|
372
|
+
if (label === undefined || label === "") {
|
|
373
|
+
// Fail closed: unknown fields do not cross outbound/persisted boundaries.
|
|
374
|
+
if (input.direction === "outbound" && outbound.has(input.destination))
|
|
375
|
+
return DENY_UNKNOWN;
|
|
376
|
+
return ALLOW;
|
|
377
|
+
}
|
|
378
|
+
if (publicLabels.has(label))
|
|
379
|
+
return ALLOW;
|
|
380
|
+
if (label in deniedLabels)
|
|
381
|
+
return denied(LABEL_DENY_REASONS[label] ?? "denied");
|
|
382
|
+
if (label in redactedLabels)
|
|
383
|
+
return { action: "redact", reason: redactedLabels[label] };
|
|
384
|
+
if (label in tokenizedLabels)
|
|
385
|
+
return { action: "tokenize", reason: tokenizedLabels[label] };
|
|
386
|
+
return DENY_UNKNOWN_LABEL;
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
/** Identity policy: every field allowed (explicit override for trusted marks). */
|
|
390
|
+
export const ALLOW_FIELD_POLICY = () => ALLOW;
|
|
391
|
+
/**
|
|
392
|
+
* Adapts a `FieldPolicy` to the audit-export redaction hook. Transformation
|
|
393
|
+
* runs BEFORE canonical hashing so the exported chain verifies exactly.
|
|
394
|
+
* Only `{path, reason}` provenance survives; values never echo into the list.
|
|
395
|
+
*/
|
|
396
|
+
export function createAuditFieldRedactor(policy, options = {}) {
|
|
397
|
+
return {
|
|
398
|
+
apply(record) {
|
|
399
|
+
const redactions = [];
|
|
400
|
+
const transformed = applyFieldPolicy(record, policy, {
|
|
401
|
+
destination: "audit",
|
|
402
|
+
direction: "outbound",
|
|
403
|
+
tenantId: options.tenantId,
|
|
404
|
+
purpose: options.purpose ?? "audit-export",
|
|
405
|
+
labelFor: options.labelFor,
|
|
406
|
+
onRedact: (path, reason) => {
|
|
407
|
+
if (options.collectProvenance !== false)
|
|
408
|
+
redactions.push({ path, reason });
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
return {
|
|
412
|
+
record: transformed,
|
|
413
|
+
...(redactions.length ? { redactions } : {}),
|
|
414
|
+
};
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
//# sourceMappingURL=field-policy.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -72,6 +72,8 @@ export { createProviderRequestPolicyChain, createSessionCachePolicy, mergeProvid
|
|
|
72
72
|
export type { ProviderRegistry, ProviderRegistryOptions } from "./providers.js";
|
|
73
73
|
export { createProviderRegistry, createProviderResolver } from "./providers.js";
|
|
74
74
|
export type { SecretRedactor } from "./redaction.js";
|
|
75
|
+
export { ALLOW_FIELD_POLICY, applyFieldPolicy, createAuditFieldRedactor, createProtectedFieldPolicy, FieldPolicyError, FIELD_POLICY_LIMITS, } from "./field-policy.js";
|
|
76
|
+
export type { ApplyFieldPolicyOptions, AuditFieldRedaction, AuditFieldRedactorLike, AuditFieldRedactorOptions, FieldPolicy, FieldPolicyAction, FieldPolicyDecision, FieldPolicyInput, ProtectedFieldPolicyOptions, } from "./field-policy.js";
|
|
75
77
|
export { createSecretRedactor, errorToErrorInfo, redactAgentEvent, redactMessage, redactProviderRequest, redactRunLedgerRecord, redactSecrets, redactSessionEntry, resolveRedactor, } from "./redaction.js";
|
|
76
78
|
export type { DuplicateRegistrationOptions, DuplicateRegistrationPolicy } from "./registry-options.js";
|
|
77
79
|
export type { LoadBinaryResourceOptions } from "./resources.js";
|
|
@@ -109,5 +111,5 @@ export { createToolParameterValidator, createToolRegistry, dispatchToolCall, fil
|
|
|
109
111
|
export type { ResolvedUseCaseModel, ResolveUseCaseModelInput, UseCaseModelBinding, } from "./use-case-model.js";
|
|
110
112
|
export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
|
|
111
113
|
export declare const name = "prism";
|
|
112
|
-
export declare const version = "0.2.
|
|
114
|
+
export declare const version = "0.2.7";
|
|
113
115
|
export declare const description = "Agent harness for AI providers, agents, sessions, and tools.";
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ export { providerContentDelta, providerContinuationRequired, providerDone, provi
|
|
|
39
39
|
export { authMethodKey, defineProviderPackage, systemPromptContributionKey } from "./provider-packages.js";
|
|
40
40
|
export { createProviderRequestPolicyChain, createSessionCachePolicy, mergeProviderRequestOptions } from "./provider-request-policy.js";
|
|
41
41
|
export { createProviderRegistry, createProviderResolver } from "./providers.js";
|
|
42
|
+
export { ALLOW_FIELD_POLICY, applyFieldPolicy, createAuditFieldRedactor, createProtectedFieldPolicy, FieldPolicyError, FIELD_POLICY_LIMITS, } from "./field-policy.js";
|
|
42
43
|
export { createSecretRedactor, errorToErrorInfo, redactAgentEvent, redactMessage, redactProviderRequest, redactRunLedgerRecord, redactSecrets, redactSessionEntry, resolveRedactor, } from "./redaction.js";
|
|
43
44
|
export { loadBinaryResource, loadJsonResource, loadManifestResource, loadTextResource } from "./resources.js";
|
|
44
45
|
export { createDefaultRetryPolicy, isTransientErrorInfo, waitForRetry } from "./retry.js";
|
|
@@ -60,6 +61,6 @@ export { DEFAULT_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES, DEFAULT_TOOL_RESULT_FOLD_MI
|
|
|
60
61
|
export { createToolParameterValidator, createToolRegistry, dispatchToolCall, filterTools } from "./tools.js";
|
|
61
62
|
export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
|
|
62
63
|
export const name = "prism";
|
|
63
|
-
export const version = "0.2.
|
|
64
|
+
export const version = "0.2.7";
|
|
64
65
|
export const description = "Agent harness for AI providers, agents, sessions, and tools.";
|
|
65
66
|
//# sourceMappingURL=index.js.map
|
package/dist/redaction.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { AgentEvent, ErrorInfo, Message, ProviderRequest, RunLedgerRecord, SessionEntry } from "./contracts.js";
|
|
2
|
+
import type { FieldPolicy } from "./field-policy.js";
|
|
2
3
|
export interface SecretRedactor {
|
|
3
4
|
redact<T>(value: T): T;
|
|
4
5
|
}
|
|
5
6
|
export declare function createSecretRedactor(secrets: readonly (string | undefined)[]): SecretRedactor;
|
|
6
|
-
export declare function redactMessage(message: Message, redactor?: SecretRedactor): Message;
|
|
7
|
-
export declare function redactAgentEvent(event: AgentEvent, redactor?: SecretRedactor): AgentEvent;
|
|
8
|
-
export declare function redactSessionEntry(entry: SessionEntry, redactor?: SecretRedactor): SessionEntry;
|
|
9
|
-
export declare function redactProviderRequest(request: ProviderRequest, redactor?: SecretRedactor): ProviderRequest;
|
|
10
|
-
export declare function redactRunLedgerRecord<T extends RunLedgerRecord>(record: T, redactor?: SecretRedactor): T;
|
|
7
|
+
export declare function redactMessage(message: Message, redactor?: SecretRedactor, fieldPolicy?: FieldPolicy, destination?: string, labelFor?: (key: string, path: string) => string | undefined): Message;
|
|
8
|
+
export declare function redactAgentEvent(event: AgentEvent, redactor?: SecretRedactor, fieldPolicy?: FieldPolicy, destination?: string, labelFor?: (key: string, path: string) => string | undefined): AgentEvent;
|
|
9
|
+
export declare function redactSessionEntry(entry: SessionEntry, redactor?: SecretRedactor, fieldPolicy?: FieldPolicy, destination?: string, labelFor?: (key: string, path: string) => string | undefined): SessionEntry;
|
|
10
|
+
export declare function redactProviderRequest(request: ProviderRequest, redactor?: SecretRedactor, fieldPolicy?: FieldPolicy, destination?: string, labelFor?: (key: string, path: string) => string | undefined): ProviderRequest;
|
|
11
|
+
export declare function redactRunLedgerRecord<T extends RunLedgerRecord>(record: T, redactor?: SecretRedactor, fieldPolicy?: FieldPolicy, destination?: string, labelFor?: (key: string, path: string) => string | undefined): T;
|
|
11
12
|
export declare function resolveRedactor(redactor?: SecretRedactor, secrets?: readonly (string | undefined)[]): SecretRedactor | undefined;
|
|
12
13
|
export declare function redactSecrets<T>(value: T, secrets: readonly (string | undefined)[]): T;
|
|
13
14
|
export declare function errorToErrorInfo(error: unknown, secrets?: readonly (string | undefined)[]): ErrorInfo;
|