@arnilo/prism 0.0.12 → 0.0.14
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 +23 -0
- package/README.md +8 -2
- package/dist/agents.js +21 -2
- package/dist/artifacts.d.ts +78 -0
- package/dist/artifacts.js +24 -0
- package/dist/contracts.d.ts +35 -1
- package/dist/contracts.js +8 -0
- package/dist/conversations.d.ts +50 -0
- package/dist/conversations.js +97 -0
- package/dist/credentials.d.ts +14 -0
- package/dist/credentials.js +9 -0
- package/dist/devices.d.ts +94 -0
- package/dist/devices.js +138 -0
- package/dist/extensions.d.ts +11 -0
- package/dist/extensions.js +15 -0
- package/dist/identity.d.ts +92 -0
- package/dist/identity.js +257 -0
- package/dist/index.d.ts +15 -5
- package/dist/index.js +8 -3
- package/dist/persistence-lifecycle.d.ts +103 -0
- package/dist/persistence-lifecycle.js +204 -0
- package/dist/providers/openai-compatible.d.ts +5 -1
- package/dist/providers/openai-compatible.js +15 -6
- package/dist/providers/openai-primitives.js +5 -2
- package/dist/secure-agent.js +7 -1
- package/dist/testing/persistence-schema.d.ts +2 -2
- package/dist/testing/persistence-schema.js +35 -2
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +6 -0
- package/docs/a2a.md +2 -0
- package/docs/ag-ui.md +5 -0
- package/docs/agent-identity.md +111 -0
- package/docs/browser-automation.md +3 -0
- package/docs/conversations.md +135 -0
- package/docs/credential-storage.md +31 -1
- package/docs/credentials-and-redaction.md +2 -0
- package/docs/database-persistence.md +22 -7
- package/docs/device-adapters.md +97 -0
- package/docs/extensions.md +1 -0
- package/docs/guardrails.md +3 -0
- package/docs/host-security.md +9 -3
- package/docs/index.md +26 -13
- package/docs/mcp-tools.md +2 -0
- package/docs/migration.md +48 -0
- package/docs/model-routing.md +102 -0
- package/docs/observability.md +2 -0
- package/docs/performance.md +21 -0
- package/docs/policy-and-audit.md +128 -0
- package/docs/postgres-persistence.md +1 -1
- package/docs/provider-caching.md +4 -0
- package/docs/provider-packages.md +12 -2
- package/docs/provider-request-policies.md +2 -0
- package/docs/providers/alibaba.md +179 -0
- package/docs/providers/azure.md +74 -0
- package/docs/providers/bedrock.md +72 -0
- package/docs/providers/google.md +1 -0
- package/docs/providers/ollama.md +166 -0
- package/docs/providers/openai-compatible.md +3 -1
- package/docs/providers/openrouter.md +2 -0
- package/docs/providers/vertex.md +71 -0
- package/docs/public-contracts.md +3 -1
- package/docs/release-and-install.md +149 -7
- package/docs/review-coverage-2026-07-23-phase-8.md +245 -0
- package/docs/review-coverage-2026-07-25-phase-9.md +256 -0
- package/docs/runs-and-usage.md +2 -0
- package/docs/server.md +37 -4
- package/docs/sqlite-persistence.md +1 -1
- package/docs/supervisors.md +2 -0
- package/docs/work-artifacts-and-review.md +100 -0
- package/docs/work-connectors.md +32 -0
- package/docs/work-tools.md +117 -0
- package/docs/workflows.md +4 -0
- package/docs/working-and-semantic-memory.md +20 -5
- package/package.json +4 -1
- package/templates/init/providers.json +22 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Agent identity
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
Authenticated `Principal` / `AgentIdentity` contracts let hosts attach verified tenant, sponsor/owner, delegated actor, scopes, credential references, issued/expiry, and revocation metadata to runs and tools. Core helpers assert activity, narrow scopes for delegation, project onto `OwnershipScope`, refuse silent widening, and emit redacted telemetry attributes. Prism does not store identities or verify tokens itself — hosts supply an `IdentityVerifier`.
|
|
6
|
+
|
|
7
|
+
## When to use it
|
|
8
|
+
|
|
9
|
+
Use these APIs when embedding Prism in multi-tenant or enterprise hosts that already authenticate callers (for example Microsoft Entra Agent ID governance). Use them before tools, providers, MCP, A2A, workflows, or persistence that must carry attributable identity.
|
|
10
|
+
|
|
11
|
+
Do not treat optional `ownership` strings as identity provenance. Do not accept caller-asserted identity headers without a host verifier. Do not put JWTs or secret credential material on identity records.
|
|
12
|
+
|
|
13
|
+
## Inputs / request
|
|
14
|
+
|
|
15
|
+
| Field | Meaning |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `Principal` | Actor id/kind (user, service, agent) plus optional display name |
|
|
18
|
+
| `AgentIdentity` | Verified context: required `tenantId`, optional account/user, principal, sponsor/owner, scopes, credential refs, issued/expiry/revocation, `verified: true` |
|
|
19
|
+
| `IdentityVerifier.verify(input)` | Host-owned authentication → `AgentIdentity` |
|
|
20
|
+
| `RunOptions.identity` / `AgentConfig.identity` | Optional verified identity for a run or agent default |
|
|
21
|
+
| Server/MCP/A2A `authorization.identity` | Optional verified identity on authorize results |
|
|
22
|
+
|
|
23
|
+
Frozen caps (defaults / hard): scopes `64 / 256`, scope bytes `128 / 512`, metadata `4 KiB / 16 KiB`, credential ref / principal id `256 B / 2 KiB`.
|
|
24
|
+
|
|
25
|
+
## Outputs / response / events
|
|
26
|
+
|
|
27
|
+
- `assertIdentityActive` — fail closed on unverified/expired/revoked/wrong-tenant/over-limit shapes (sync, no network).
|
|
28
|
+
- `narrowIdentity` — child scopes ⊆ parent; tenant and ownership ids immutable; expiry cannot extend.
|
|
29
|
+
- `ownershipFromIdentity` — projects tenant/account/user onto existing ownership seams.
|
|
30
|
+
- `assertIdentityMatchesOwnership` / `assertIdentityPropagation` — refuse widen across ownership or boundary hop.
|
|
31
|
+
- `identityTelemetryAttributes` — redacted refs for metadata/OTel (`prism.identity.*`); never includes credential secrets or raw tokens.
|
|
32
|
+
- Tool `ToolExecutionContext.identity` — set when a run carries verified identity.
|
|
33
|
+
|
|
34
|
+
## Request/response example
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"tenantId": "tenant-1",
|
|
39
|
+
"userId": "user-1",
|
|
40
|
+
"principal": { "kind": "agent", "id": "agent-42" },
|
|
41
|
+
"sponsor": { "kind": "user", "id": "sponsor-7" },
|
|
42
|
+
"scopes": ["mail.read", "mail.draft"],
|
|
43
|
+
"credentialRefs": ["m365:tenant-1:user-1"],
|
|
44
|
+
"issuedAt": "2026-07-23T00:00:00.000Z",
|
|
45
|
+
"expiresAt": "2026-07-23T01:00:00.000Z",
|
|
46
|
+
"verified": true
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Implementation example
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import {
|
|
54
|
+
assertIdentityActive,
|
|
55
|
+
createAgent,
|
|
56
|
+
identityTelemetryAttributes,
|
|
57
|
+
narrowIdentity,
|
|
58
|
+
ownershipFromIdentity,
|
|
59
|
+
type AgentIdentity,
|
|
60
|
+
type IdentityVerifier,
|
|
61
|
+
} from "@arnilo/prism";
|
|
62
|
+
|
|
63
|
+
const verifier: IdentityVerifier = {
|
|
64
|
+
async verify(request) {
|
|
65
|
+
// Host validates JWT/session, then returns AgentIdentity with verified: true
|
|
66
|
+
return hostVerifiedIdentityFrom(request);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const identity = await verifier.verify(incomingRequest);
|
|
71
|
+
assertIdentityActive(identity);
|
|
72
|
+
const child = narrowIdentity(identity, { scopes: ["mail.read"] });
|
|
73
|
+
|
|
74
|
+
const agent = createAgent({
|
|
75
|
+
model,
|
|
76
|
+
provider,
|
|
77
|
+
ownership: ownershipFromIdentity(identity),
|
|
78
|
+
identity,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await agent.createSession().run("Summarize inbox", {
|
|
82
|
+
identity: child,
|
|
83
|
+
ownership: ownershipFromIdentity(child),
|
|
84
|
+
metadata: identityTelemetryAttributes(child),
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Server / MCP / A2A authorize callbacks may include the same `identity` beside `ownership`. Handlers assert activity and ownership match before admitting work.
|
|
89
|
+
|
|
90
|
+
## Extension and configuration notes
|
|
91
|
+
|
|
92
|
+
Identity is optional. Hosts that only set `ownership` keep prior behavior. When identity is present, run start and tool dispatch assert it before side effects. Workflows forward `RunWorkflowOptions.identity` into agent nodes. Credential values stay behind `CredentialResolver` keys listed in `credentialRefs`.
|
|
93
|
+
|
|
94
|
+
## Security and performance notes
|
|
95
|
+
|
|
96
|
+
- Caller-asserted identity without `IdentityVerifier` is unsupported at trust boundaries.
|
|
97
|
+
- Delegation only narrows scopes; tenant/account/user cannot widen on propagation.
|
|
98
|
+
- Credential refs never expand to secrets in events, ledgers, or telemetry attributes.
|
|
99
|
+
- Checks are O(fields) and network-free in core; remote auth stays in the host verifier.
|
|
100
|
+
- Raising hard caps requires updating `docs/review-coverage-2026-07-23-phase-8.md`, tests, and docs.
|
|
101
|
+
|
|
102
|
+
## Related APIs
|
|
103
|
+
|
|
104
|
+
- [Policy and audit](policy-and-audit.md)
|
|
105
|
+
- [Host security guide](host-security.md)
|
|
106
|
+
- [Public contracts](public-contracts.md)
|
|
107
|
+
- [Server](server.md)
|
|
108
|
+
- [Supervisors](supervisors.md) / [A2A](a2a.md)
|
|
109
|
+
- [MCP tools](mcp-tools.md)
|
|
110
|
+
- [Observability](observability.md)
|
|
111
|
+
- [Runs and usage ledger](runs-and-usage.md)
|
|
@@ -106,6 +106,7 @@ await browser.close();
|
|
|
106
106
|
- Observation (`snapshot`, `wait`, open-without-url, `close`) vs mutation/high-impact (`navigate`, click/form, dialog accept, upload, download release, popup select) is classified for `ExecutionPolicy` / `beforeSideEffect`.
|
|
107
107
|
- `createSharedSandboxBrowserOptions()` aligns browser uploads/downloads with Task 1 sandbox `/workspace` and `/downloads`. `assertBrowserSandboxNetwork()` in `@arnilo/prism-coding-security` fails closed for custom Docker networks without browser egress attestation.
|
|
108
108
|
- Raw CSS is absent from production defaults. Ref resolution uses Playwright’s built-in `aria-ref=` selector with a package-owned snapshot ref table for staleness checks.
|
|
109
|
+
- Verified-state checkpoints (0.0.14): `createBrowserCheckpointLedger()` records navigation state — URL, a domain-state hash, and host-owned data refs — never serialized browser internals (cookies/storage/contexts), which are fragile and secret-bearing. Frozen caps: URL 8 KiB/16 KiB, domain-state hash 256 B/1 KiB, host-data ref 2 KiB/8 KiB (refs only, never bodies), 16/64 checkpoints per run (oldest evicted). After any resume/interruption `markResumed(runId)` marks state stale; `assertVerifiedBeforeSideEffect(runId)` fails closed until the host reloads + `verify()`s, so side effects never replay on stale state. Checkpoints are run-scoped: a conversation thread composes through the run it owns, reusing the manager's sandbox/egress/approval/limit policy above.
|
|
109
110
|
|
|
110
111
|
## Security and performance notes
|
|
111
112
|
|
|
@@ -121,4 +122,6 @@ Default tests use fake Playwright APIs only. Protected live gate: `PRISM_LIVE_PL
|
|
|
121
122
|
- [Host security](host-security.md): browser endpoint, approval, egress proxy, and artifact trust boundaries.
|
|
122
123
|
- [Performance and resource limits](performance.md): browser ceilings and charging points.
|
|
123
124
|
- [Coding execution approval and sandboxing](coding-security.md): optional shared disposable sandbox for coding+browser.
|
|
125
|
+
- [Conversations](conversations.md): durable threads that own the runs browser checkpoints scope to.
|
|
126
|
+
- [Device adapters](device-adapters.md): deny-by-default voice/desktop-control contracts (no vendor package in 0.0.14).
|
|
124
127
|
- [Migration](migration.md): additive optional package activation.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Conversations
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
`@arnilo/prism-server` ships a durable, user-scoped conversation service: create/list/get/continue/branch/archive/export/delete conversation threads on top of the existing session and event-ledger seams. A thread **is** an ownership-scoped session branch plus a `prismConversation` marker in `SessionRecord.metadata`; content stays in session entries and the redacted event ledger. Reconnectable replay pages durable redacted events without ever rerunning a provider or tool.
|
|
6
|
+
|
|
7
|
+
Core (`@arnilo/prism`) exports only conversation **types and pure helpers** (`ConversationThread`, `ConversationError`, `CONVERSATION_METADATA_KEY`, thread-bound replay cursor codec, `conversationThreadFromRecord`, `conversationMarkerMetadata`). The service and optional HTTP handler live in `@arnilo/prism-server`.
|
|
8
|
+
|
|
9
|
+
## When to use it
|
|
10
|
+
|
|
11
|
+
Use it when a host needs persistent personal/work-agent conversations with reconnect, branch, archive, export, and deletion semantics without building a second session/event system. Hosts own authentication, agent selection, UI, transport chrome, and blob storage.
|
|
12
|
+
|
|
13
|
+
Do not use it as a chat UI, a push/always-on daemon, or a file store. Slack/Teams channels, realtime voice, and desktop-control vendors are deferred (0.1.x); device adapters are contract + deny-by-default conformance only in 0.0.14.
|
|
14
|
+
|
|
15
|
+
## Inputs / request
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createConversationService, createConversationHandler } from "@arnilo/prism-server";
|
|
19
|
+
import { createSqlitePersistence } from "@arnilo/prism-session-store-sqlite";
|
|
20
|
+
|
|
21
|
+
const persistence = createSqlitePersistence({ filename }); // implements ConversationServiceStore
|
|
22
|
+
const service = createConversationService(persistence, {
|
|
23
|
+
redactor, // required: replay/export serve redacted rows only; continue runs with it
|
|
24
|
+
sessionFactory: ({ thread, leafId, ownership, signal }) =>
|
|
25
|
+
agent.createSession({ id: thread.id, ...(leafId ? { leafId } : {}) }), // host binds agent/store/leaf
|
|
26
|
+
runOptions?, // narrowable RunOptions minus ownership/identity/signal/redactor/idempotencyKey
|
|
27
|
+
limits?: ConversationLimits, // frozen defaults/caps below
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await service.create({ ownership, title?, id?, requestId?, metadata? });
|
|
31
|
+
await service.list({ ownership, cursor?, limit? });
|
|
32
|
+
await service.get({ ownership, threadId });
|
|
33
|
+
await service.continue({ ownership, threadId, message, requestId?, leafId? });
|
|
34
|
+
await service.branch({ ownership, threadId, leafId });
|
|
35
|
+
await service.archive({ ownership, threadId });
|
|
36
|
+
await service.export({ ownership, threadId, cursor? });
|
|
37
|
+
await service.delete({ ownership, threadId });
|
|
38
|
+
await service.replay({ ownership, threadId, cursor?, limit? });
|
|
39
|
+
|
|
40
|
+
const handler = createConversationHandler({ service, authorize, basePath?: "/prism/conversations", redactor?, limits? });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`ConversationServiceStore` is a narrow `Pick` of `ProductionPersistenceStore`: `querySessions`, `queryEvents`, optional `appendSession` (required at factory time; sqlite/postgres implement it), and optional `lifecycle.applyRetention` (required for delete). Stores without `appendSession` fail closed at construction.
|
|
44
|
+
|
|
45
|
+
## Outputs / response / events
|
|
46
|
+
|
|
47
|
+
- `create`/`get`/`branch`/`archive` → `ConversationThread` (`id`, `title?`, `state: "active" | "archived"`, `branches`, timestamps, ownership projection, host metadata).
|
|
48
|
+
- `list` → ownership-scoped `PersistencePage<ConversationThread>` (newest first), marker-filtered; non-conversation sessions never appear.
|
|
49
|
+
- `continue` → `AgentRunResult` from one agent turn on the thread session; history rebuilds from durable entries, so the agent sees prior turns.
|
|
50
|
+
- `replay` → `{ records: AgentEventRecord[], nextCursor?, terminal }`; records are durable redacted ledger rows ordered by `(timestamp, id)`; `terminal` marks `agent_finished`/`agent_denied`/`error`.
|
|
51
|
+
- `export` → `{ thread, events, nextCursor?, truncated }`; redacted, byte/page-capped, cursor-resumable.
|
|
52
|
+
- `delete` → `{ deleted, held }` via persistence lifecycle; legal holds always win.
|
|
53
|
+
- HTTP handler routes: `POST {base}` create · `GET {base}` list · `GET {base}/{id}` · `DELETE {base}/{id}` · `POST {base}/{id}/continue|branch|archive|export` · `GET {base}/{id}/events?cursor=&limit=` replay.
|
|
54
|
+
|
|
55
|
+
## Request/response example
|
|
56
|
+
|
|
57
|
+
```http
|
|
58
|
+
POST /prism/conversations HTTP/1.1
|
|
59
|
+
content-type: application/json
|
|
60
|
+
|
|
61
|
+
{ "title": "Q3 planning" }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{ "id": "conv_9b0f…", "title": "Q3 planning", "state": "active", "branches": [], "createdAt": "…", "updatedAt": "…", "tenantId": "t1", "userId": "u1" }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Reconnect after a dropped connection: `GET {base}/{id}/events` (optionally with the last `nextCursor`) pages the same durable events; clients dedupe by stable record `id` (at-least-once across the page boundary). No provider or tool call is re-executed by replay/export.
|
|
69
|
+
|
|
70
|
+
## Implementation example
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const thread = await service.create({ ownership, title: "draft review" });
|
|
74
|
+
await service.continue({ ownership, threadId: thread.id, message: "summarize the attached plan", requestId: "ui-req-1" });
|
|
75
|
+
|
|
76
|
+
// Branch from a known leaf (e.g. last entry id), then fork a continue from it.
|
|
77
|
+
const branched = await service.branch({ ownership, threadId: thread.id, leafId });
|
|
78
|
+
await service.continue({ ownership, threadId: thread.id, message: "try a shorter version", leafId });
|
|
79
|
+
|
|
80
|
+
// Reconnectable replay.
|
|
81
|
+
let cursor: string | undefined;
|
|
82
|
+
do {
|
|
83
|
+
const page = await service.replay({ ownership, threadId: thread.id, ...(cursor ? { cursor } : {}) });
|
|
84
|
+
render(page.records);
|
|
85
|
+
cursor = page.nextCursor;
|
|
86
|
+
} while (cursor);
|
|
87
|
+
|
|
88
|
+
await service.archive({ ownership, threadId: thread.id });
|
|
89
|
+
const result = await service.delete({ ownership, threadId: thread.id }); // { deleted: true, held: false }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Extension and configuration notes
|
|
93
|
+
|
|
94
|
+
Frozen limits (default / hard cap; hosts may tighten, never raise past hard caps):
|
|
95
|
+
|
|
96
|
+
| Resource | Default / hard cap |
|
|
97
|
+
| --- | ---: |
|
|
98
|
+
| Thread list page | 50 / 200 |
|
|
99
|
+
| Replay/export page rows | 100 / 500 |
|
|
100
|
+
| Replay/export cursor | 4 KiB / 16 KiB |
|
|
101
|
+
| Thread title | 256 B / 2 KiB |
|
|
102
|
+
| Client request id | 256 B / 2 KiB |
|
|
103
|
+
| Active branches per thread | 16 / 64 |
|
|
104
|
+
| Export payload per request | 8 MiB / 32 MiB |
|
|
105
|
+
| Export pages per request | 100 / 500 |
|
|
106
|
+
| Handler request body | 64 KiB / 1 MiB |
|
|
107
|
+
|
|
108
|
+
Behavior notes:
|
|
109
|
+
|
|
110
|
+
- `create` with an explicit `id` is idempotent get-or-create; generated ids are `conv_<uuid>`.
|
|
111
|
+
- `continue` `requestId` flows into session-append idempotency (`RunRecord.idempotencyKey` + append dedup), so exact retries deduplicate.
|
|
112
|
+
- `continue` on an archived thread fails closed (`thread_archived`); `leafId` must be a branch ref recorded by `branch()`.
|
|
113
|
+
- Replay cursors are thread-bound: a cursor minted for one thread is rejected on another (`cursor_thread_mismatch`).
|
|
114
|
+
- Ledger rows from runs that had no redactor are never served by replay/export (fail-closed skip).
|
|
115
|
+
- Export truncates at page granularity when the next page would exceed `exportBytes`; a single page larger than `exportBytes` cannot be exported (raise the cap or page via `replay`).
|
|
116
|
+
- Branch refs live in thread metadata (read-modify-write); concurrent `branch()` calls can lose a ref, so the cap is approximate and the entry tree remains the content source of truth.
|
|
117
|
+
- Deletion purges the whole session ledger (entries, runs, events, tool calls, usage, branches, search rows) through `lifecycle.applyRetention`; legal holds block deletion and report `held: true`.
|
|
118
|
+
|
|
119
|
+
## Security and performance notes
|
|
120
|
+
|
|
121
|
+
- Every operation starts from host-verified ownership (and optional `AgentIdentity`, which must project onto ownership without widening); wrong-user access returns not-found, never leaked existence.
|
|
122
|
+
- `appendSession` upserts set ownership columns only on create; metadata/`updatedAt` on update — ownership is immutable after create.
|
|
123
|
+
- Replay/export serve `redacted: true` ledger rows only and pass through the service redactor; no local paths, raw tool payloads, or secrets are emitted.
|
|
124
|
+
- All loops are bounded by the frozen caps above; review/agent turns consume shared `RunLimits` via the host's `runOptions`.
|
|
125
|
+
- No new permission surface: conversations reuse session/event/identity/redaction/lifecycle seams (roadmap gate 8).
|
|
126
|
+
|
|
127
|
+
## Related APIs
|
|
128
|
+
|
|
129
|
+
- [Web-standard server handler](server.md): authorized agent/workflow routes; the conversation handler mounts beside it.
|
|
130
|
+
- [Session stores](session-stores.md): branch/append/checkout semantics a thread builds on.
|
|
131
|
+
- [Database persistence](database-persistence.md): `ProductionPersistenceStore`, `appendSession`, `SessionQuery` id/metadataKey filters, retention/legal-hold lifecycle.
|
|
132
|
+
- [Agents and sessions](agent-identity.md): verified identity and ownership projection.
|
|
133
|
+
- [Credentials and redaction](credentials-and-redaction.md): `SecretRedactor` used by replay/export/continue.
|
|
134
|
+
- [Browser automation](browser-automation.md): verified-state checkpoints scope to the runs a thread owns; reload/verify before side effect.
|
|
135
|
+
- [Device adapters](device-adapters.md): deny-by-default voice/desktop sessions bind to a thread's run and consume shared `RunLimits`.
|
|
@@ -146,6 +146,33 @@ await refreshOAuthCredential({
|
|
|
146
146
|
});
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
+
Workload OAuth providers (0.0.14) — Microsoft 365 / Google Workspace over the shared OAuth2 seam (PKCE + device code + refresh + revoke), least-privilege scopes per read/mutation bundle:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { revokeOAuthCredential } from "@arnilo/prism";
|
|
153
|
+
import {
|
|
154
|
+
createMicrosoft365OAuthProvider,
|
|
155
|
+
createGoogleWorkspaceOAuthProvider,
|
|
156
|
+
createOAuthWorkTokenProvider,
|
|
157
|
+
createOAuthCredentialStoreAdapter,
|
|
158
|
+
} from "@arnilo/prism-credentials-node";
|
|
159
|
+
|
|
160
|
+
// Read-only mail/calendar (no mutation scopes requested).
|
|
161
|
+
const m365 = createMicrosoft365OAuthProvider({ clientId: "<app-id>", capabilities: ["mail", "calendar"], access: "read" });
|
|
162
|
+
const creds = await m365.login({ onDeviceCode: ({ userCode, verificationUri }) => host.showCode(userCode, verificationUri) });
|
|
163
|
+
await createOAuthCredentialStoreAdapter(store).set("microsoft365", creds);
|
|
164
|
+
|
|
165
|
+
// Late-bound, per-identity token for a work-tools connector (env var, never argv/model context).
|
|
166
|
+
const tokenProvider = createOAuthWorkTokenProvider({
|
|
167
|
+
provider: m365,
|
|
168
|
+
store: createOAuthCredentialStoreAdapter(store),
|
|
169
|
+
envVar: "M365_ACCESSTOKEN",
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Revocation: best-effort upstream (GWS supports RFC 7009; M365 does not) + mandatory local delete.
|
|
173
|
+
await revokeOAuthCredential({ provider: m365, credentials: creds, store: createOAuthCredentialStoreAdapter(store) });
|
|
174
|
+
```
|
|
175
|
+
|
|
149
176
|
Passphrase rotation:
|
|
150
177
|
|
|
151
178
|
```ts
|
|
@@ -205,7 +232,8 @@ const providers = createOpenAIProviderPackage({ apiKey });
|
|
|
205
232
|
- Use distinct `namespace` or vault paths per tenant/environment.
|
|
206
233
|
- Keychain `list()` / `listOAuth()` are intentionally unsupported — enumerate credentials through host configuration instead of scanning the OS store.
|
|
207
234
|
- Combine with `createExplicitCredentialResolver()` so runtime overrides still win over stored values.
|
|
208
|
-
- Wire `createOAuthCredentialStoreAdapter(store)` into `refreshOAuthCredential()` only for an OAuth flow explicitly selected by the host and authorized by that provider. In 0.0.12 that means OpenAI Codex; Anthropic and Google packages accept API keys only. Never import or migrate Claude Code/Gemini CLI credential files, setup tokens, browser sessions, or CLI OAuth rows into this store.
|
|
235
|
+
- Wire `createOAuthCredentialStoreAdapter(store)` into `refreshOAuthCredential()` only for an OAuth flow explicitly selected by the host and authorized by that provider. In 0.0.12 that means OpenAI Codex; in 0.0.14 the Microsoft 365 / Google Workspace workload providers (`createMicrosoft365OAuthProvider` / `createGoogleWorkspaceOAuthProvider`) are added over the same seam with least-privilege read/mutation scope bundles. Anthropic and Google *model* packages still accept API keys only. Never import or migrate Claude Code/Gemini CLI credential files, setup tokens, browser sessions, or CLI OAuth rows into this store.
|
|
236
|
+
- Enterprise cloud providers (`azure` / `bedrock` / `vertex`) expect host workload-identity callbacks (Entra / IAM / ADC), not this local encrypted/keychain store as a cloud token minting service. Store may hold opaque refresh material only when the host already owns the cloud auth flow.
|
|
209
237
|
|
|
210
238
|
## Security and performance notes
|
|
211
239
|
|
|
@@ -216,6 +244,7 @@ const providers = createOpenAIProviderPackage({ apiKey });
|
|
|
216
244
|
- Keychain operations use `@napi-rs/keyring`'s abort-aware `AsyncEntry`, so native work runs outside the JavaScript event loop. A main-loop timer aborts and rejects at `timeoutMs`; native cancellation remains OS/backend-dependent and may briefly retain one libuv worker after rejection.
|
|
217
245
|
- Keychain payloads are bytes rather than password strings and are zeroed after parse/write. Unknown native errors are mapped to sanitized typed errors; no native message or secret value is echoed.
|
|
218
246
|
- Never log passphrases, derived keys, or decrypted credential payloads.
|
|
247
|
+
- Optional host KMS: `encryptWithHostKms` / `decryptWithHostKms` wrap a random AES-256-GCM DEK via host `HostKms.wrapKey`/`unwrapKey` (timeout ≤ 60 s). Envelope sizes reuse vault/file caps. Keys are never logged. `createMemoryHostKms` is for tests only.
|
|
219
248
|
- Storage is not OAuth eligibility. A durable store may persist credentials for a provider only after the host selects a provider-authorized flow; it must not be used to piggyback on a vendor CLI or consumer subscription.
|
|
220
249
|
- Live keychain tests are opt-in (`PRISM_TEST_KEYCHAIN=1`); default `npm test` stays offline.
|
|
221
250
|
|
|
@@ -230,6 +259,7 @@ MCP credentials remain host inputs: resolve them before constructing client `req
|
|
|
230
259
|
## Related APIs
|
|
231
260
|
|
|
232
261
|
- [Credentials and redaction](credentials-and-redaction.md): core resolver helpers and `refreshOAuthCredential()`
|
|
262
|
+
- [Azure OpenAI / Foundry](providers/azure.md) / [Amazon Bedrock](providers/bedrock.md) / [Google Vertex AI](providers/vertex.md): enterprise workload-identity credential callbacks
|
|
233
263
|
- [Web search, fetch, and extraction](web-tools.md): late-bound Brave/Exa/Firecrawl credentials
|
|
234
264
|
- [Security/auth/trust](settings-auth-trust-security.md): host-owned settings/credentials boundaries
|
|
235
265
|
- [Persistence, credentials, and multimodality primitives](persistence-credentials-multimodality-primitives.md): Plan 056 threat model and conformance matrix rows 7–10
|
|
@@ -8,6 +8,7 @@ Prism provides small helpers for host-owned credentials and known-secret redacti
|
|
|
8
8
|
- `createExplicitCredentialResolver()`: tries named resolver sources in caller-provided order, such as runtime override → stored → env object → fallback.
|
|
9
9
|
- `createEnvCredentialResolver()`: reads only a caller-supplied env-like object and map.
|
|
10
10
|
- `refreshOAuthCredential()`: calls a provider OAuth refresh function and writes the result to a caller-owned store when supplied.
|
|
11
|
+
- `revokeOAuthCredential()`: best-effort upstream revocation (`OAuthProvider.revoke?`) followed by a mandatory caller-owned store delete, so a revoked token fails closed locally even if the provider has no revocation endpoint.
|
|
11
12
|
- `CredentialValueSource`: the accepted source type for `resolveCredentialValue()`.
|
|
12
13
|
- `redactSecrets()`: replaces known secret string values inside strings, arrays, and plain objects.
|
|
13
14
|
- `errorToErrorInfo()`: converts unknown errors into `ErrorInfo` and redacts known secret values from error text.
|
|
@@ -41,6 +42,7 @@ resolveCredentialValue(
|
|
|
41
42
|
createExplicitCredentialResolver(sources: readonly CredentialResolverSource[]): CredentialResolver
|
|
42
43
|
createEnvCredentialResolver(env: Readonly<Record<string, string | undefined>>, map: Readonly<Record<string, string>>): CredentialResolver
|
|
43
44
|
refreshOAuthCredential(options: { provider: OAuthProvider; credentials: OAuthCredentials; store?: OAuthCredentialStore }): Promise<OAuthCredentials>
|
|
45
|
+
revokeOAuthCredential(options: { provider: OAuthProvider; credentials: OAuthCredentials; store?: RevocableOAuthCredentialStore }): Promise<void>
|
|
44
46
|
redactSecrets<T>(value: T, secrets: readonly (string | undefined)[]): T
|
|
45
47
|
errorToErrorInfo(error: unknown, secrets?: readonly (string | undefined)[]): ErrorInfo
|
|
46
48
|
```
|
|
@@ -72,6 +72,10 @@ Important shapes:
|
|
|
72
72
|
| `RetentionPolicy` | Policy with `maxAgeDays`, `maxEntriesPerSession`, `maxTotalBytes`, `archiveStore`, and `appliedKinds`. |
|
|
73
73
|
| `MigrationRecord` | Applied migration with name, version, timestamp, checksum, and applied-by. |
|
|
74
74
|
|
|
75
|
+
Optional session-record write seam (0.0.14): `appendSession?(record: SessionRecord)` upserts a session row — ownership columns are set on create only, `metadata`/`updatedAt` on update — so hosts (e.g. the [conversation service](conversations.md)) can durably mark and title sessions without entry writes. `SessionQuery` gained two bounded filters for the same seam: `id` (exact session lookup) and `metadataKey` (sessions whose `metadata` object contains a top-level key, validated by `assertSessionMetadataKey`). SQLite implements it with `json_extract`, PostgreSQL with a `jsonb` existence check; both keep ownership filtering intact.
|
|
76
|
+
|
|
77
|
+
Artifact co-work review (0.0.14) reuses the generic `CheckpointStore` rather than adding a dedicated table: the [artifact service](work-artifacts-and-review.md) stores each artifact as a versioned checkpoint value (namespace `prism.artifact`, key `threadId:artifactId`, category `artifact`). The checkpoint `version` is the compare-and-swap counter that resolves concurrent reviewers; revision numbers, approvals, and `lastValidatedVersion` live inside the JSON value. SQLite/Postgres already persist checkpoints durably, so there is no separate artifact schema or migration, and records carry metadata/hashes/refs only — never file bodies.
|
|
78
|
+
|
|
75
79
|
## Outputs / response / events
|
|
76
80
|
|
|
77
81
|
Each `query*` method returns a `PersistencePage<T>`:
|
|
@@ -334,19 +338,30 @@ Use `cursor`/`limit` when an adapter pages very long branches. Do not implement
|
|
|
334
338
|
|
|
335
339
|
## Retention policies
|
|
336
340
|
|
|
337
|
-
A retention policy is a host-managed rule attached to sessions via `retention_policy_id`.
|
|
341
|
+
A retention policy is a host-managed rule attached to sessions via `retention_policy_id`. Phase 8 adds optional `ProductionPersistenceStore.lifecycle` (`createMemoryPersistenceLifecycle`, SQLite/Postgres `persistence.lifecycle`) for bounded apply/hold/export/quota:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
await store.lifecycle.putLegalHold({ tenantId, userId, resourceKind: "session", resourceId, reason });
|
|
345
|
+
await store.lifecycle.applyRetention({ tenantId, userId, policy, candidates }); // hold wins over delete; SQL adapters purge the whole session ledger (entries, runs, events, tool calls, usage, branches, search rows) in FK order
|
|
346
|
+
await store.lifecycle.exportUnderHold({ tenantId, userId, cursor, limit }); // redacted
|
|
347
|
+
await store.lifecycle.setTenantQuota({ tenantId, userId, resourceKind: "run", limit: 100 });
|
|
348
|
+
await store.lifecycle.consumeTenantQuota({ tenantId, userId, resourceKind: "run" }); // fails closed when exhausted
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Schema **v5** migration `005_lifecycle_hold_quota` adds `prism_legal_holds` and `prism_tenant_quotas`. Legal hold always blocks delete for the held resource id. Export pages are redacted refs only (no prompts/bodies/secrets).
|
|
352
|
+
|
|
353
|
+
Host background jobs may still:
|
|
338
354
|
|
|
339
355
|
1. Select policies whose `max_age_days`, `max_entries_per_session`, or `max_total_bytes` thresholds are exceeded.
|
|
340
|
-
2.
|
|
341
|
-
3. Respect `applied_kinds`
|
|
342
|
-
4.
|
|
343
|
-
5. Write audit metadata to the migration or host audit log; do not delete the policy row unless explicitly requested.
|
|
356
|
+
2. Pass candidate session ids into `applyRetention` (or let SQL adapters discover expired sessions).
|
|
357
|
+
3. Respect `applied_kinds` when selecting candidates.
|
|
358
|
+
4. Never silently purge under hold.
|
|
344
359
|
|
|
345
|
-
Retention jobs should not run inside the agent/session runtime.
|
|
360
|
+
Retention jobs should not run inside the agent/session runtime.
|
|
346
361
|
|
|
347
362
|
## Migrations
|
|
348
363
|
|
|
349
|
-
Hosts own schema migrations. Prism publishes only the TypeScript contracts; no DDL is generated or executed by the core library. First-party SQLite/PostgreSQL adapters automatically verify their checked-in schema-
|
|
364
|
+
Hosts own schema migrations. Prism publishes only the TypeScript contracts; no DDL is generated or executed by the core library. First-party SQLite/PostgreSQL adapters automatically verify their checked-in schema-v5 history and catalog at open, before runtime writes. Their catalog reads are bounded metadata queries/PRAGMAs, not table-data scans.
|
|
350
365
|
|
|
351
366
|
Each new adapter-owned migration row records the contract SHA-256 checksum. A complete known v0.0.5 history whose checksum values are all `NULL` is a one-time compatibility case: under the SQLite transaction or PostgreSQL advisory transaction lock, the adapter verifies the full current shape, backfills all checksums, and continues. Unknown/duplicate/out-of-order/name-version/checksum mismatch, mixed/partial legacy values, or any missing/renamed/wrong-type/null/default/key/index artifact fails closed. Restore or apply a reviewed host migration; never edit checksums to silence drift.
|
|
352
367
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Device adapters
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
Optional realtime voice and desktop OS / computer-control surface for Prism agents, shipped in 0.0.14 as a **contract + deny-by-default policy only** in `@arnilo/prism` (`src/devices.ts`). No vendor voice or desktop-control implementation ships in 0.0.14 — those are demand-gated to 0.1.x. The contract composes over the existing `PermissionPolicy`, `RunLimits`, approval (`tool_approval`), and redactor seams; it adds no second approval runtime and no device framework.
|
|
6
|
+
|
|
7
|
+
## When to use it
|
|
8
|
+
|
|
9
|
+
- A host wants to admit a realtime voice or desktop-control device for an agent run and needs a fail-closed policy boundary before writing the vendor adapter.
|
|
10
|
+
- You need conformance fixtures (denial, approval, stream bounds, session budget, run accounting, redaction) to validate a future vendor adapter against the deny-by-default contract.
|
|
11
|
+
- You must guarantee device side effects never run without explicit consent + sandbox + approval, and never replay after reconnect.
|
|
12
|
+
|
|
13
|
+
Do **not** use it to broaden consent, memory, network, file, browser, connector, or tool permissions (roadmap gate 8 forbids this).
|
|
14
|
+
|
|
15
|
+
## Inputs / request
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import type { DeviceAdapter, DevicePolicyOptions, DeviceAdmitRequest } from "@arnilo/prism";
|
|
19
|
+
|
|
20
|
+
const adapter: DeviceAdapter = {
|
|
21
|
+
kind: "voice", // "voice" | "desktop-control"
|
|
22
|
+
enabled: false, // deny-by-default: admit only on explicit true
|
|
23
|
+
requireApproval: true, // every side effect requires approval
|
|
24
|
+
sandbox: "sandbox-a", // host-owned sandbox id (required to admit)
|
|
25
|
+
network: "egress-strict", // host-owned network/egress policy id
|
|
26
|
+
limits: { maxChunkBytes: 1_048_576, maxConcurrentSessions: 1 },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const options: DevicePolicyOptions = { runLimits: { maxTurns: 8, maxToolCalls: 50 } };
|
|
30
|
+
const admit: DeviceAdmitRequest = { approved: true, activeSessions: 0 };
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Outputs / response / events
|
|
34
|
+
|
|
35
|
+
| Export | Purpose |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `resolveDevicePolicy(adapter, options?)` | Resolve caps; reject unknown kinds and caps above the hard ceiling. |
|
|
38
|
+
| `assertDeviceAdmit(policy, request)` | Fail-closed admission gate (disabled / unsandboxed / unapproved / over-budget / unaccounted all deny). |
|
|
39
|
+
| `acceptDeviceChunk(policy, bytes)` | Stream bound: oversize chunks dropped with `marker: "dropped_oversize"`, never forwarded. |
|
|
40
|
+
| `redactDeviceTelemetry(redactor, telemetry)` | Metadata-safe telemetry: apply the host redactor before any emit/persist. |
|
|
41
|
+
| `runDevicePolicyConformance(adapter, options?)` | Conformance pair for future vendor adapters; returns `{ passed }`. |
|
|
42
|
+
| `DevicePolicyError` | Stable error (`ERR_PRISM_DEVICE_DISABLED` / `_APPROVAL` / `_SESSIONS` / `_CHUNK` / `_RUN_LIMITS` / `_INPUT`). |
|
|
43
|
+
|
|
44
|
+
## Request/response example
|
|
45
|
+
|
|
46
|
+
```jsonc
|
|
47
|
+
// assertDeviceAdmit on a disabled device throws (fail closed):
|
|
48
|
+
// DevicePolicyError: voice device is disabled by default (ERR_PRISM_DEVICE_DISABLED)
|
|
49
|
+
|
|
50
|
+
// acceptDeviceChunk(policy, 9_000_000) with a 1 MiB cap:
|
|
51
|
+
{ "accepted": false, "bytes": 9000000, "marker": "dropped_oversize" }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Implementation example
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import {
|
|
58
|
+
assertDeviceAdmit,
|
|
59
|
+
acceptDeviceChunk,
|
|
60
|
+
redactDeviceTelemetry,
|
|
61
|
+
resolveDevicePolicy,
|
|
62
|
+
createSecretRedactor,
|
|
63
|
+
} from "@arnilo/prism";
|
|
64
|
+
|
|
65
|
+
const policy = resolveDevicePolicy(
|
|
66
|
+
{ kind: "desktop-control", enabled: true, requireApproval: true, sandbox: "sandbox-a" },
|
|
67
|
+
{ runLimits: { maxTurns: 8, maxToolCalls: 50 } },
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Re-admit on every resume (side effects never replay after reconnect).
|
|
71
|
+
assertDeviceAdmit(policy, { approved: hostApprovedSideEffect, activeSessions: currentSessions });
|
|
72
|
+
|
|
73
|
+
// Stream bound + redaction on each audio/screenshot chunk.
|
|
74
|
+
const chunk = acceptDeviceChunk(policy, frameBytes);
|
|
75
|
+
if (chunk.accepted) emit(redactDeviceTelemetry(createSecretRedactor([token]), frame));
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Extension and configuration notes
|
|
79
|
+
|
|
80
|
+
- Frozen caps: audio/screenshot/stream chunk **1 MiB / 8 MiB**; concurrent device sessions per identity **1 / 4**. Device wall time / turns / tool calls consume the shared `RunLimits` (admission fails closed without run accounting).
|
|
81
|
+
- `enabled` resolves to `true` only on an explicit `true`; any other value is disabled. `requireApproval` stays `true` unless the host explicitly sets `false` (it should not).
|
|
82
|
+
- Vendor voice / desktop-control packages are **deferred to 0.1.x** and ship only if Task 0 records measured demand. This page documents the contract they must satisfy via `runDevicePolicyConformance`.
|
|
83
|
+
|
|
84
|
+
## Security and performance notes
|
|
85
|
+
|
|
86
|
+
- Deny-by-default: admission requires explicit `enabled`, an explicit `sandbox`, approval (when required), an under-budget session count, and shared `RunLimits` — any missing condition fails closed.
|
|
87
|
+
- Side effects never replay after reconnect: hosts must re-admit on every resume/interruption.
|
|
88
|
+
- Secrets are isolated from audio/screenshot/stream paths: apply `redactDeviceTelemetry` before any emit/persist; telemetry must be metadata-safe.
|
|
89
|
+
- No permission broadening: device adapters cannot widen consent, memory, network, file, browser, connector, or tool permissions (gate 8).
|
|
90
|
+
|
|
91
|
+
## Related APIs
|
|
92
|
+
|
|
93
|
+
- [Browser automation](browser-automation.md): verified-state checkpoints + reload/verify-before-side-effect for browser composition.
|
|
94
|
+
- [Conversations](conversations.md): durable threads that own the runs device sessions bind to.
|
|
95
|
+
- [Host security](host-security.md): approval, sandbox, and egress trust boundaries device adapters compose over.
|
|
96
|
+
- [Performance and resource limits](performance.md): shared `RunLimits` accounting.
|
|
97
|
+
- [Migration](migration.md): 0.0.14 additive seams and 0.1.x device vendor deferral.
|
package/docs/extensions.md
CHANGED
|
@@ -122,6 +122,7 @@ await kernel.middleware.run("provider_request", { metadata: {} });
|
|
|
122
122
|
- Do not put resolved credential values in extension events, registry metadata, docs, logs, prompts, or session stores.
|
|
123
123
|
- Event and middleware dispatch are ordered and dependency-free. They use no timers, background workers, filesystem discovery, network calls, provider calls, or tool execution.
|
|
124
124
|
- Extension middleware cannot bypass host tool permissions: tool dispatch re-checks active registry lookup, filters, and object arguments after `tool_call` middleware. Skills that reference `toolNames` are checked against host-active tools by `resolveActiveSkills()`.
|
|
125
|
+
- Optional `loadPolicy: { allowList?, verifySignature? }` on `createExtensionKernel` runs before `setup`. Unallowlisted or unsigned (when `verifySignature` is set) extensions fail closed. Put host-attested digests on `Extension.signature`.
|
|
125
126
|
|
|
126
127
|
## Related APIs
|
|
127
128
|
|
package/docs/guardrails.md
CHANGED
|
@@ -65,10 +65,13 @@ Guardrails are callbacks supplied by the host. Prism does not discover, load, re
|
|
|
65
65
|
|
|
66
66
|
## Security and performance notes
|
|
67
67
|
|
|
68
|
+
Optional `@arnilo/prism-policy` can record guardrail outcomes via `recordGuardrailDecision` (evidence refs only; see [Policy and audit](policy-and-audit.md)).
|
|
69
|
+
|
|
68
70
|
Output buffering prevents blocked provider content from reaching subscribers, session entries, ledgers, parsers, delegation, or tools. Tool-output checks receive raw results but Prism discards blocked raw output before event, ledger, transcript, or MCP exposure. Redaction replaces exact known values only; it is not general secret detection. Parallel checks receive an abort signal, but callback code must honor it to stop in-flight work. Browser snapshots and page text from `@arnilo/prism-browser` are untrusted external content: never allow them to modify tools, permissions, credentials, or policy. Browser mutations still require host `ExecutionPolicy`/approval; prompt-injection text in a page cannot grant upload/download release.
|
|
69
71
|
|
|
70
72
|
## Related APIs
|
|
71
73
|
|
|
74
|
+
- [Policy and audit](policy-and-audit.md): optional attributable decision ledger for guardrail outcomes.
|
|
72
75
|
- [Agent/session runtime](agent-session-runtime.md)
|
|
73
76
|
- [Tools](tools.md)
|
|
74
77
|
- [Browser automation](browser-automation.md)
|
package/docs/host-security.md
CHANGED
|
@@ -34,6 +34,8 @@ Start from explicit host inputs. Do not let runtime code discover security state
|
|
|
34
34
|
| Durable interruption | host checkpoint + session stores, exact ownership | `RunOptions.runState`, `resumeAgentRun()`, `createAgentRunLifecycle()`, `createSecureAgent()` |
|
|
35
35
|
| Extensions | explicit package imports only | `createExtensionKernel`, `ExtensionAPI` |
|
|
36
36
|
| Remote agent/workflow API | host authentication + ownership mapping | `@arnilo/prism-server`, `createPrismHandler()` |
|
|
37
|
+
| Authenticated identity | host `IdentityVerifier` → verified `AgentIdentity` | [Agent identity](agent-identity.md), `assertIdentityActive`, `narrowIdentity` |
|
|
38
|
+
| Policy decision audit | optional redacted ledger + host WORM sink | [Policy and audit](policy-and-audit.md), `@arnilo/prism-policy` |
|
|
37
39
|
| MCP server exposure | host MCP auth + selected capability list | `createPrismMcpServer()`, `createPrismMcpWebHandler()` |
|
|
38
40
|
|
|
39
41
|
## Outputs / response / events
|
|
@@ -133,18 +135,21 @@ Wire those values where they matter: provider adapters receive the resolved cred
|
|
|
133
135
|
- Redaction is exact known-secret replacement only. It is not arbitrary secret detection, entropy scanning, or DLP.
|
|
134
136
|
- Known secrets must be passed into redactors before data is emitted or persisted. Redact again in host adapters if they transform records after Prism redaction.
|
|
135
137
|
- Tool `parameters` metadata is not validated by default. Add a `ToolValidator`, use `createToolParameterValidator()` with a schema adapter, or install `@arnilo/prism-tool-validator-json-schema` before side effects. Its untrusted-schema adapter rejects non-local refs, forbidden keys/cycles/non-finite values and bounds bytes/depth/properties/keywords/refs plus its LRU cache before Ajv compilation; do not raise caps above documented hard limits.
|
|
136
|
-
- Treat embeddings as untrusted numeric input. `@arnilo/prism-memory` rejects empty, non-number, NaN, and infinite vectors before in-memory similarity or pgvector parameters; custom `Embedder`/`VectorStore` implementations must retain the same boundary.
|
|
138
|
+
- Treat embeddings as untrusted numeric input. `@arnilo/prism-memory` rejects empty, non-number, NaN, and infinite vectors before in-memory similarity or pgvector parameters; custom `Embedder`/`VectorStore` implementations must retain the same boundary. Memory entries carry consent/source/visibility; revoked, invisible, or (in strict mode) consent-less entries never enter prompts, events, exports, or telemetry, and `forget`/`applyRetention` are real bounded deletes.
|
|
137
139
|
- Evaluation trace readers require exact supplied ownership plus session/run identity, reject cursor/identity drift, and redact before bounded scorer/judge input. Model-judge callbacks receive no credential resolver, tools, or workspace; keep live judges outside default CI and redact report artifacts.
|
|
138
140
|
- Prism-generated session/run/tool/workflow/evaluation IDs use Node cryptographic UUIDs. Keep host-provided IDs authorization-scoped and validate them as untrusted identifiers; do not substitute timestamps or `Math.random()` for durable/security-relevant IDs.
|
|
139
141
|
- MCP client tools from `@arnilo/prism-mcp` are untrusted remote servers. Stdio remains an explicit host executable. Streamable HTTP requires exact HTTPS origins, rejects credentials/fragments/redirects/private or mixed DNS, pins a validated address on every SDK request/reconnect, and bounds each response; plaintext is explicit loopback-only development mode. Discovery has finite page/tool/cursor/metadata/schema totals and commits atomically. Every result branch shares byte/depth/property bounds before core dispatch; supply a known-secret `SecretRedactor`, `PermissionPolicy`, and `ToolValidator` there. MCP server direction exposes only passed tools/commands/resources/prompts, requires per-operation `authorize`, and retains core gates. Sampling, roots, model/credential selection, and elicitation consent stay host-owned; URL elicitation is never opened automatically. Stateful web mode requires host `resolveAuthInfo` plus `resolveIdentity`, exact origin policy, and binds every POST/GET/DELETE/SSE request to one non-secret principal; mismatches return 404. Handler still needs TLS and edge rate limiting. See [MCP client/server exposure](mcp-tools.md).
|
|
140
|
-
- `@arnilo/prism-server` exposes no agent/workflow by default and requires `authorize()` for every matched operation. Derive complete tenant/account/user ownership from validated host identity, never request JSON. Workflow active identity and cancellation compare exact ownership; a tenant-only scope intentionally cannot cancel a checkpoint/run carrying account or user identity. Pass the current explicitly revised workflow definition so recursive hash mismatch fails before abort or durable mutation. Configure exact host/origin allow-lists where needed, wire redaction before execution, retain tool/workflow policy checks, and adapt the Web handler behind host TLS/rate limits. Disconnect abort is default; persistent reconnect/status belongs to durable workflow checkpoints, not an invented in-memory agent result cache.
|
|
142
|
+
- `@arnilo/prism-server` exposes no agent/workflow by default and requires `authorize()` for every matched operation. Derive complete tenant/account/user ownership from validated host identity, never request JSON. Workflow active identity and cancellation compare exact ownership; a tenant-only scope intentionally cannot cancel a checkpoint/run carrying account or user identity. The artifact review service (`createArtifactService`) requires authenticated identity + thread ownership on every attach/revise/compare/approve/reject/download, resolves concurrent reviewers via checkpoint CAS (no lost approvals), rejects local filesystem paths in `uri`/citations, redacts records before persist and on response, and serves downloads only through signed expiring links that are reauthorized against the token's ownership per request. Pass the current explicitly revised workflow definition so recursive hash mismatch fails before abort or durable mutation. Configure exact host/origin allow-lists where needed, wire redaction before execution, retain tool/workflow policy checks, and adapt the Web handler behind host TLS/rate limits. Disconnect abort is default; persistent reconnect/status belongs to durable workflow checkpoints, not an invented in-memory agent result cache.
|
|
141
143
|
- Coding tools from `@arnilo/prism-coding-agent` accept an optional `ExecutionPolicy` checked inside each tool before side effects; shared policy propagation includes `createReadOnlyTools()`. They enforce finite text-scan/image/edit/write/shell limits, repository list/search depth/entry/match/scan/time caps, structured Git path/ref/message/output/patch/worktree caps, named-check concurrency/output caps, a 600-second default shell wall time, and a 64 MiB default total-output ceiling. Opt-in `createGitTools()` uses argument arrays with hooks/credential prompts/external diff disabled, requires host `commitIdentity` for commits, and never pushes or opens PRs. Successful truncated shell output leaves a host-owned exclusive `0600` temp file; delete `metadata.fullOutputPath` after use. Error/abort/timeout/overflow removes unpublished spills. Custom read/edit/shell/repository backends must honor supplied caps/signals. Use `@arnilo/prism-coding-security` for path roots, command rules, identity-scoped approval caching, required `workspaceMode` on `createSandboxCodingComposition()` / `createSandboxCodingTools()`, and the optional `createDockerSandbox()` reference adapter. **Host mode is never contained execution** (`containmentClaim: false`). Sandbox mode claims containment only when FS backends target the disposable tree; mixed wiring requires `allowMixedWorkspaceWiring` and still does not claim containment. Limits alone are not containment: construct the Docker adapter (absolute CLI, digest-pinned image, network none by default) or an equivalent host sandbox before treating coding execution as production-safe. Docker daemon/image trust, egress firewall/proxy, and artifact retention remain host-owned.
|
|
142
144
|
- Optional `@arnilo/prism-browser` requires a host-supplied Playwright Browser (`playwright-core@1.61.0` peer). Import is inert. One non-persistent context belongs to one run; actions serialize; refs are snapshot-scoped; CSS/evaluate/CDP/persistent profiles are denied. Context routing + `serviceWorkers: "block"` deny file/data/blob/devtools/private/loopback by default and require contained-proxy attestation for external egress (Playwright routing is defense in depth, not DNS containment). Uploads are realpath-rooted; downloads quarantine with hash/MIME until host `approveRelease`; screenshots return bounded `ImageContent`. Observation vs mutation/high-impact actions map to `ExecutionPolicy`. Treat snapshot/page text as untrusted external content. Close contexts with `browser_close` or `manager.closeRun(runId)` on abort/terminal. Browser control endpoint, binary/image pin, and real egress firewall/proxy remain host-owned. Shared sandbox: `createSharedSandboxBrowserOptions()` + `assertBrowserSandboxNetwork()`.
|
|
145
|
+
- Browser verified-state checkpoints (0.0.14, `createBrowserCheckpointLedger()`) store URL + domain-state hash + host data refs only — never serialized browser internals (cookies/storage/contexts). After any resume/interruption the ledger fails closed (`assertVerifiedBeforeSideEffect`) until the host reloads + verifies, so side effects never replay on stale state.
|
|
146
|
+
- Device adapters (0.0.14, `resolveDevicePolicy`/`assertDeviceAdmit`) are deny-by-default: admission fails closed without explicit `enabled`, an explicit sandbox, approval (when required), an under-budget session count, and shared `RunLimits`. Stream chunks over the frozen cap are dropped with a marker; telemetry is redacted before emit/persist. No vendor voice/desktop package ships in 0.0.14 (demand-gated 0.1.x); device adapters cannot broaden consent/memory/network/file/browser/connector/tool permissions (gate 8).
|
|
143
147
|
- `@arnilo/prism-credentials-node` rejects oversized/malformed envelopes and excessive scrypt work before KDF allocation, uses async scrypt, and requires restrictive existing/new Unix vault modes. Keep vault ownership and parent-directory access host-controlled; review before `chmod 600`, never auto-weaken a file policy. Keychain calls use abort-aware native async work with finite timeout/payload caps and sanitized errors. OS prompts, service availability, and whether a native backend promptly honors cancellation remain host/platform boundaries; no plaintext fallback is attempted.
|
|
144
148
|
- LLM compaction always sends finite summary `maxTokens`, retains bounded deltas/events, and bounds/redacts provider/factory/policy error detail. Observational-memory workers cap turns, calls, arguments, results, transcript, and surfaced errors; unknown tools fail before execution, while invalid results can only be rejected after a host tool returns and may therefore follow side effects. Pass all known provider/credential/tool secrets into compaction/runtime options; exact replacement is not secret discovery.
|
|
145
149
|
- Default remote-media loading resolves every DNS answer, rejects the hostname if any address is non-public, and pins one validated address through the request. Explicit `allowedHostnames` can trust private destinations. A host-supplied `fetch` owns DNS/rebinding/proxy/redirect safety; a custom `requestUrl` must connect to its supplied validated address.
|
|
146
150
|
- Permission checks happen before tool validation and before `tool.execute()`. Middleware cannot grant permission by renaming a tool.
|
|
147
|
-
- Session stores and ledgers receive redacted values when a redactor is active, but durable storage remains host-owned. Enforce tenant/account/user ownership and
|
|
151
|
+
- Session stores and ledgers receive redacted values when a redactor is active, but durable storage remains host-owned. Enforce tenant/account/user ownership, retention, legal hold, and quotas via `ProductionPersistenceStore.lifecycle` (or host-equivalent DB controls). Hold always blocks delete.
|
|
152
|
+
- Prefer `createExtensionKernel({ loadPolicy })` allow-list/signature checks before loading third-party extension packages.
|
|
148
153
|
- Provider-owned auth/content/session/cache/security headers win over caller headers in adapters that merge headers.
|
|
149
154
|
- Security checks are bounded explicit calls on the active path. Prism adds no hidden global middleware, background workers, watchers, network calls, or filesystem scans.
|
|
150
155
|
|
|
@@ -172,6 +177,7 @@ PostgreSQL TLS/network policy, MCP endpoint trust/credentials and egress policy
|
|
|
172
177
|
## Web research boundaries
|
|
173
178
|
|
|
174
179
|
- Construct `@arnilo/prism-web-tools` with one host-selected Brave or Exa adapter; never expose adapter/provider/credential/schema selection to model arguments.
|
|
180
|
+
- Construct `@arnilo/prism-work-tools` with host-pinned CLI binary + isolated `configDir` + verified `AgentIdentity` (M365 and/or GWS). Never pass model-built command strings, `login`/`setup`/`auth`/`schema`/`--debug`, or credentials in argv. Mutations require draft approval; external recipients and anonymous/`anyone` shares fail closed.
|
|
175
181
|
- Provider API origins are fixed exact HTTPS origins and redirects fail. Credentials resolve immediately before I/O; remote bodies and secrets are excluded from errors/results/telemetry.
|
|
176
182
|
- Firecrawl targets reject userinfo, non-HTTP(S), private literals, and policy-denied hosts. Supply `validateUrl` for host DNS/rebinding/egress checks. Firecrawl performs remote retrieval, so Prism cannot pin target DNS after handoff.
|
|
177
183
|
- Treat every snippet, highlight, Markdown byte, metadata field, and extracted JSON value as prompt-injection-capable untrusted data. Never elevate it into system instructions or let it modify tools, permissions, trust, credentials, routing, or extraction schema.
|