@arnilo/prism 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +12 -11
- package/dist/agent-approval.d.ts +4 -0
- package/dist/agent-approval.js +5 -1
- package/dist/agent-run-lifecycle.js +29 -5
- package/dist/agent-run-state.d.ts +13 -1
- package/dist/agent-run-state.js +12 -2
- package/dist/agent-session/helpers.js +6 -1
- package/dist/agent-session/session/assemble.js +126 -5
- package/dist/agent-session/session/persist.d.ts +16 -0
- package/dist/agent-session/session/persist.js +58 -2
- package/dist/agent-session/session/provider-round.d.ts +3 -3
- package/dist/agent-session/session/provider-round.js +12 -6
- package/dist/agent-session/session/tool-round.js +4 -1
- package/dist/agent-session/session/types.d.ts +12 -1
- package/dist/agent-session/session.d.ts +2 -0
- package/dist/agent-session/session.js +2 -0
- package/dist/checkpoints.js +7 -11
- package/dist/contracts-core/content.d.ts +5 -0
- package/dist/contracts-core/loop.d.ts +42 -0
- package/dist/contracts-core/run-limits.d.ts +2 -0
- package/dist/contracts-protocol.d.ts +15 -3
- package/dist/contracts-run-state.d.ts +26 -5
- package/dist/evidence-grounding.d.ts +29 -0
- package/dist/evidence-grounding.js +162 -0
- package/dist/host-composition.d.ts +13 -0
- package/dist/host-composition.js +33 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -1
- package/dist/provider-events.d.ts +3 -1
- package/dist/provider-events.js +2 -2
- package/dist/providers/transport.d.ts +3 -1
- package/dist/providers/transport.js +36 -0
- package/dist/redaction.js +18 -2
- package/dist/run-bundle.d.ts +89 -0
- package/dist/run-bundle.js +149 -0
- package/dist/testing/state-concurrency-conformance.js +5 -12
- package/docs/ag-ui.md +5 -0
- package/docs/agent-loops.md +33 -0
- package/docs/agent-session-runtime.md +4 -3
- package/docs/coding-security.md +1 -0
- package/docs/coding-tools.md +0 -1
- package/docs/compaction-observational-memory.md +1 -1
- package/docs/connected-apps.md +116 -0
- package/docs/context-and-skills.md +13 -0
- package/docs/core.md +1 -1
- package/docs/diagrams.md +6 -6
- package/docs/document-reader.md +9 -9
- package/docs/documents.md +32 -11
- package/docs/durable-runs.md +87 -0
- package/docs/enterprise-postgres-state.md +4 -0
- package/docs/execution-timeline.md +6 -0
- package/docs/guardrails.md +33 -0
- package/docs/history/079-messaging-primitive-review.md +391 -0
- package/docs/history/080-messaging-followon-primitive-review.md +234 -0
- package/docs/history/081-connected-apps-primitive-review.md +74 -0
- package/docs/history/083-prism-work-primitive-review.md +84 -0
- package/docs/history/084-primitive-review.md +96 -0
- package/docs/history/085-honesty-and-cut-primitive-review.md +91 -0
- package/docs/history/README.md +5 -0
- package/docs/history/release-handoffs.md +38 -0
- package/docs/host-compositions.md +8 -6
- package/docs/host-security.md +2 -2
- package/docs/index.md +47 -29
- package/docs/live-testing.md +5 -3
- package/docs/mcp-tools.md +1 -0
- package/docs/messaging-channel-operations.md +166 -0
- package/docs/messaging-channels.md +150 -0
- package/docs/migrate-to-0.8.md +124 -0
- package/docs/migration.md +30 -0
- package/docs/model-registry.md +12 -2
- package/docs/openapi-tools.md +1 -1
- package/docs/operations.md +1 -3
- package/docs/options-index.md +34 -2
- package/docs/peer-dependencies.md +6 -6
- package/docs/postgres-persistence.md +1 -1
- package/docs/provider-layer.md +2 -2
- package/docs/provider-packages.md +20 -20
- package/docs/providers/neuralwatt.md +5 -1
- package/docs/rag.md +1 -1
- package/docs/release-and-install.md +52 -46
- package/docs/run-bundle.md +92 -0
- package/docs/runs-and-usage.md +14 -0
- package/docs/server.md +2 -0
- package/docs/sheets.md +9 -9
- package/docs/signal-channel.md +112 -0
- package/docs/speech.md +5 -1
- package/docs/sqlite-persistence.md +1 -1
- package/docs/telegram-channel.md +157 -0
- package/docs/testing.md +2 -2
- package/docs/wiki.md +1 -1
- package/docs/work-artifacts-and-review.md +1 -1
- package/docs/work-connectors.md +9 -9
- package/docs/work-sandbox.md +115 -0
- package/docs/work-tools.md +38 -16
- package/package.json +5 -3
- package/templates/business-worker/manifest.json +2 -1
- package/templates/business-worker/src/agent.ts.tmpl +1 -1
- package/templates/business-worker/src/tests/agent.test.ts.tmpl +1 -1
package/docs/sheets.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Spreadsheets, CSV parsing, and typed schema inference (`@arnilo/prism-
|
|
1
|
+
# Spreadsheets, CSV parsing, and typed schema inference (`@arnilo/prism-work/sheets`)
|
|
2
2
|
|
|
3
3
|
## What it does
|
|
4
4
|
|
|
5
|
-
The `@arnilo/prism-
|
|
5
|
+
The `@arnilo/prism-work/sheets` package provides fail-closed, high-fidelity spreadsheet (XLSX) and delimiter-separated (CSV/TSV/PSV) data ingestion with automatic dialect sniffing, typed column schema inference, and **strict financial decimal safety**.
|
|
6
6
|
|
|
7
7
|
### Headline Guarantee: Strict Financial Decimal Safety
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ The `@arnilo/prism-office/sheets` package provides fail-closed, high-fidelity sp
|
|
|
10
10
|
> **Zero Float Coercion on Decimal Paths**:
|
|
11
11
|
> In financial and enterprise data processing, floating-point rounding errors (IEEE-754 `double`) silently distort monetary totals, balance ledgers, and transaction reconciliations.
|
|
12
12
|
>
|
|
13
|
-
> In `@arnilo/prism-
|
|
13
|
+
> In `@arnilo/prism-work/sheets`:
|
|
14
14
|
> - Money-like and decimal values are **never converted to JavaScript numbers (`Number()`, `parseFloat()`, or unary `+`)**.
|
|
15
15
|
> - All decimal and currency values are parsed, normalized, and emitted as exact canonical decimal strings: `{ type: "decimal", value: "1234.56" }`.
|
|
16
16
|
> - Currency markers (`$`, `€`, `£`, `¥`, `₹`, `CHF`, `USD`, `EUR`, etc.) and accounting parentheses `($1,234.56)` are normalized safely into canonical strings (`"-1234.56"`).
|
|
@@ -29,7 +29,7 @@ The `@arnilo/prism-office/sheets` package provides fail-closed, high-fidelity sp
|
|
|
29
29
|
|
|
30
30
|
## When to use it
|
|
31
31
|
|
|
32
|
-
Use `@arnilo/prism-
|
|
32
|
+
Use `@arnilo/prism-work/sheets` when autonomous agents, data pipelines, or enterprise workflows need to:
|
|
33
33
|
1. Ingest untrusted customer XLSX or CSV files with strict, unbypassable byte, row, column, and sheet caps.
|
|
34
34
|
2. Parse tabular financial records, invoices, ledgers, or pricing sheets with mathematical decimal precision guarantees.
|
|
35
35
|
3. Automatically determine CSV delimiters, quotes, and headers without manual dialect configuration.
|
|
@@ -151,7 +151,7 @@ TXN-1003,"Hardware Device","£ 2,500.00",2500.00,"$ 0.00"
|
|
|
151
151
|
## Implementation example
|
|
152
152
|
|
|
153
153
|
```ts
|
|
154
|
-
import { parseWorkbook, parseCsv, type SheetsTelemetry } from "@arnilo/prism-
|
|
154
|
+
import { parseWorkbook, parseCsv, type SheetsTelemetry } from "@arnilo/prism-work/sheets";
|
|
155
155
|
|
|
156
156
|
// 1. Parse XLSX workbook with custom caps
|
|
157
157
|
const xlsxBytes = new Uint8Array([...]); // Untrusted file bytes
|
|
@@ -194,7 +194,7 @@ console.log(`Revenue value:`, csvResult.rows[1][2]);
|
|
|
194
194
|
## Extension and configuration notes
|
|
195
195
|
|
|
196
196
|
### Sub-package Pinning
|
|
197
|
-
To avoid pulling in CLI frameworks or extraneous dependencies, `@arnilo/prism-
|
|
197
|
+
To avoid pulling in CLI frameworks or extraneous dependencies, `@arnilo/prism-work/sheets` directly pins the exact underlying modular packages:
|
|
198
198
|
- `@office-open/xlsx@0.12.3`
|
|
199
199
|
- `@office-open/xml@0.12.3`
|
|
200
200
|
|
|
@@ -211,7 +211,7 @@ const telemetry: SheetsTelemetry = {
|
|
|
211
211
|
```
|
|
212
212
|
|
|
213
213
|
### Self-Hosting & Operational Notes
|
|
214
|
-
- **Zero Network & Storage Dependencies**: `@arnilo/prism-
|
|
214
|
+
- **Zero Network & Storage Dependencies**: `@arnilo/prism-work/sheets` does not write files or contact network services. Host engines own persistence, storage buckets, and lake datasets.
|
|
215
215
|
- **Fail-Closed Container Gating**: Malicious or non-standard files are rejected before allocation or XML decompression occurs.
|
|
216
216
|
|
|
217
217
|
## Security and performance notes
|
|
@@ -224,6 +224,6 @@ const telemetry: SheetsTelemetry = {
|
|
|
224
224
|
|
|
225
225
|
## Related APIs
|
|
226
226
|
|
|
227
|
-
- [`@arnilo/prism-
|
|
228
|
-
- [`@arnilo/prism-
|
|
227
|
+
- [`@arnilo/prism-work/documents`](./documents.md): Specification-compliant OpenXML document generation, parsing, patching, and preview rendering for DOCX, XLSX, and PPTX.
|
|
228
|
+
- [`@arnilo/prism-work/document-reader`](./document-reader.md): Bounded literal text extraction from PDF and DOCX documents for coding agent tools.
|
|
229
229
|
- [`@arnilo/prism-core/governance/observability`](./observability.md): OpenTelemetry instrumentation and trace adapters.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Signal channel (experimental)
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
`@arnilo/prism-channels/signal` is a Node-only, experimental adapter for an **externally supervised** [signal-cli v0.14.8](https://github.com/AsamK/signal-cli/tree/v0.14.8) Unix-socket daemon. It is not an official Signal bot API, does not make a bot official, and must be enabled only after an operator records the acceptable-use and GPL distribution decisions required for its deployment.
|
|
6
|
+
|
|
7
|
+
Importing or constructing the adapter does nothing: it never downloads, spawns, registers, links, relinks, configures, or health-checks `signal-cli`; opens no network connection; and has no environment or credential lookup. Host owns that lifecycle, account, state directory, encryption, backup/retention and process supervisor.
|
|
8
|
+
|
|
9
|
+
## When to use it
|
|
10
|
+
|
|
11
|
+
Use only after an operator records acceptable-use and GPL distribution decisions for an already-operated Signal account. Not an official bot API; not for groups, topics, attachments, identity linking, bulk messaging (including `MessagingRuntime.notify`, which is unicast to one existing DM binding only), automated account creation, or streaming previews (Signal has no Bot API draft equivalent, so this adapter ignores `MessagingRuntimeOptions.onAssistantDelta`).
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @arnilo/prism @arnilo/prism-channels
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Inputs / request
|
|
18
|
+
|
|
19
|
+
`SignalAdapterOptions`: host `connectionId`, absolute private `socketPath`, daemon `account`, `signalCliVersion` (must equal `SIGNAL_CLI_VERSION`), `policy` attestation, and service-owned `checkpoints`/`leases`/`cursorOwnership`. `signal-cli` is a host-operated binary, not an npm peer.
|
|
20
|
+
|
|
21
|
+
## Host setup
|
|
22
|
+
|
|
23
|
+
Use only a private, host-selected Unix socket on the same Node/Linux host. Do not expose a TCP or HTTP bridge. Protect socket parent/state directories with service-only permissions (for example `0700`) and protect backups. Signal transport decrypts at `signal-cli`; Prism stores and model providers receive plaintext according to host policy.
|
|
24
|
+
|
|
25
|
+
The supported daemon shape is manual receive mode, selected by the host for the pinned CLI version:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
signal-cli --data-dir /private/prism-signal -a +15550001111 \
|
|
29
|
+
daemon --socket=/private/run/prism-signal.sock --receive-mode=manual
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`signal-cli` is GPLv3 and its upstream compatibility can change after Signal service changes. It requires the pinned release's documented JRE/native dependencies (v0.14.8 documents JRE 25). Account registration/relinking may disrupt existing clients; never automate it from Prism or chat input. Consult current Signal terms and operator policy before enabling automated replies; terms/policy can prohibit an intended deployment.
|
|
33
|
+
|
|
34
|
+
## Request/response example
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { createMessagingRuntime } from "@arnilo/prism-channels";
|
|
38
|
+
import { createSignalAdapter, SIGNAL_CLI_VERSION } from "@arnilo/prism-channels/signal";
|
|
39
|
+
|
|
40
|
+
const adapter = createSignalAdapter({
|
|
41
|
+
connectionId: "support-signal",
|
|
42
|
+
socketPath: "/private/run/prism-signal.sock",
|
|
43
|
+
account: "+15550001111", // host daemon account, never taken from a message
|
|
44
|
+
signalCliVersion: SIGNAL_CLI_VERSION,
|
|
45
|
+
policy: {
|
|
46
|
+
acceptableUse: "operator_approved",
|
|
47
|
+
gplDistribution: "operator_approved",
|
|
48
|
+
termsVersion: "2026-09-review", // host's recorded policy review, not a Signal approval
|
|
49
|
+
},
|
|
50
|
+
checkpoints: serviceCheckpoints,
|
|
51
|
+
leases: serviceLeases,
|
|
52
|
+
cursorOwnership: { tenantId: "signal-service" },
|
|
53
|
+
// multiAccount: true, // only for a daemon where account is supplied on RPC requests
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const runtime = createMessagingRuntime({
|
|
57
|
+
authorize: hostAuthorizeSignalSender, // verify observed Signal UUID -> Prism identity/grant
|
|
58
|
+
resolveAgent: hostResolveAgent,
|
|
59
|
+
deliver: (reply) => adapter.send(reply),
|
|
60
|
+
checkpoints: userCheckpoints,
|
|
61
|
+
leases: userLeases,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await adapter.start((event) => runtime.admit(event));
|
|
65
|
+
// Shutdown: await adapter.stop(); await runtime.stop();
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Implementation example
|
|
69
|
+
|
|
70
|
+
[`examples/signal-agent.ts`](../examples/signal-agent.ts) contains the same host composition helper.
|
|
71
|
+
|
|
72
|
+
`start()` acquires `prism.channels.v1.signal.receiver`, probes its checkpoint writer, connects the persistent Unix socket, then calls only `subscribeReceive`. It renews its receiver lease; loss fails closed by pausing intake. `stop()` unsubscribes when possible, closes its socket, aborts reconnection and releases only its lease. A second receiver under the same service ownership cannot subscribe.
|
|
73
|
+
|
|
74
|
+
The adapter accepts manual `params.result.envelope` notifications only when they name the selected account and contain a direct text `dataMessage` from a valid Signal UUID. It ignores receipts, sync echoes, reactions, stories, edits, groups (including group topics, which have no supported thread mapping) and attachments before `admit`. UUID (not display name or phone number) becomes the external actor/conversation ID; the runtime must still authorize it. An account mismatch pauses the adapter before admission. `health()` reports independent bounded `bridge`, `subscription`, and `account` states for host monitoring without message text, phone numbers, socket path or key material.
|
|
75
|
+
|
|
76
|
+
## Outputs / response / events
|
|
77
|
+
|
|
78
|
+
`send()` accepts only replies for its configured `connectionId` and a UUID destination fixed by the runtime's authorized binding. It calls only `send`; chat text cannot choose an RPC method, account, socket, recipient or identity action. Replies are plain text and chunked at 2,000 UTF-16 code units without splitting surrogate pairs.
|
|
79
|
+
|
|
80
|
+
A server-issued approval control is rendered as plain reply text:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
Reply /approve <opaque-token> to allow once.
|
|
84
|
+
Reply /deny <opaque-token> to deny.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Only the shared runtime validates and consumes that opaque token. Free-form text remains untrusted input; no Signal reaction, attachment, identity-trust or linking action is exposed.
|
|
88
|
+
|
|
89
|
+
Known daemon errors map to a bounded reason (`signal_rate_limited`, `signal_captcha_required`, `signal_relink_required`, `signal_identity_changed`, or `signal_rpc_error`). A changed/untrusted identity stops normal sends until an operator verifies it and current authorization is re-established. Timeout/socket loss after a write — or a failure after an earlier chunk — throws an ambiguous-delivery error; the durable reply journal records `delivery_unknown` and must not automatically resend it.
|
|
90
|
+
|
|
91
|
+
## Extension and configuration notes
|
|
92
|
+
|
|
93
|
+
| Setting | Default | Range |
|
|
94
|
+
| --- | ---: | ---: |
|
|
95
|
+
| JSON-RPC line/frame | 128 KiB | 1 KiB–512 KiB |
|
|
96
|
+
| Pending RPC requests | 16 | 1–64 |
|
|
97
|
+
| Pending admitted notifications | 32 | 1–128 |
|
|
98
|
+
| RPC timeout | 10 s | 100 ms–60 s |
|
|
99
|
+
| Reconnect delay | 1 s | 1 ms–60 s (exponential, 30 s default max) |
|
|
100
|
+
| Receiver lease | 90 s | 5 s–5 min |
|
|
101
|
+
| Outbound chunk | 2,000 UTF-16 units | fixed |
|
|
102
|
+
|
|
103
|
+
The adapter pauses/unsubscribes on journal admission capacity/storage failure, malformed/oversized frames, account mismatch, or lease loss. It reconnects with bounded backoff after an established bridge loss and rechecks writer readiness before subscribing. It has **no documented application acknowledgment or replay log**: a daemon notification received before a crash and before durable commit can be lost. This adapter does not claim lossless intake, exactly-once execution, message read status, or end-to-end encryption through a model/provider.
|
|
104
|
+
|
|
105
|
+
## Security and performance notes
|
|
106
|
+
|
|
107
|
+
Construction is inert: no spawn, TCP/HTTP, environment lookup, or credential read. Chat text cannot choose RPC method, account, socket, recipient, or identity action. UUID (not phone or display name) is the only observed actor id. A changed identity stops sends until an operator verifies it. Receive-to-commit can lose a notification; treat that as a documented loss window, not a retry.
|
|
108
|
+
|
|
109
|
+
## Related APIs
|
|
110
|
+
|
|
111
|
+
- [Messaging channels](messaging-channels.md): authorization and durable replies.
|
|
112
|
+
- [Messaging channel operations](messaging-channel-operations.md): recovery and operator review.
|
package/docs/speech.md
CHANGED
|
@@ -14,7 +14,11 @@ and `runTranscriptionConformance` from `@arnilo/prism/testing/provider-conforman
|
|
|
14
14
|
## When to use it
|
|
15
15
|
|
|
16
16
|
Use it for one-shot voice output and batch/stream transcription where the host
|
|
17
|
-
owns playback, capture, and audio storage.
|
|
17
|
+
owns playback, capture, and audio storage. Messaging hosts wrap these providers
|
|
18
|
+
and hand the wrappers to a channel adapter — Telegram `transcribe` turns an
|
|
19
|
+
inbound voice note into turn text and `synthesize` adds a voice note next to a
|
|
20
|
+
final reply ([telegram-channel.md](telegram-channel.md)); `@arnilo/prism-channels`
|
|
21
|
+
never imports this package and never stores audio. Do not use it for interactive
|
|
18
22
|
bidirectional voice — that is the Realtime session contract
|
|
19
23
|
([`RealtimeSession`](public-contracts.md)), which keeps its own
|
|
20
24
|
`audio_delta`/`transcript_delta` events. Streaming here is one-directional:
|
|
@@ -54,7 +54,7 @@ import { createSqlitePersistence } from "@arnilo/prism-core/sessions/sqlite";
|
|
|
54
54
|
| `SessionStore.readBranchPath` | Recursive ancestor query from `leafId` (or latest leaf) in root→leaf order. |
|
|
55
55
|
| `RunLedger.append*` | Inserts run/event/tool/usage rows; events receive monotonic per-run `sequence` values. |
|
|
56
56
|
| `ProductionPersistenceStore.query*` | Parameterized cursor pagination on indexed columns. |
|
|
57
|
-
| `checkpoints` | Generic versioned `CheckpointStore` backed by `prism_checkpoints`; ownership, CAS/fencing checks, bounded pagination, and workflow suspended/denied/schedule/state/replay values without a schema migration. |
|
|
57
|
+
| `checkpoints` | Generic versioned `CheckpointStore` backed by `prism_checkpoints`; ownership, CAS/fencing checks, bounded pagination, and workflow suspended/denied/schedule/state/replay values without a schema migration. A load or delete under a non-matching ownership scope reads as absent and a cross-scope write fails as a generic `ERR_PRISM_CHECKPOINT_CONFLICT` (plan 080 Task 3) — no ownership-shaped existence oracle. |
|
|
58
58
|
| `leases` | Atomic `LeaseStore` backed by `prism_leases`; database-clock expiry, opaque renew/release token, monotonic takeover fence. |
|
|
59
59
|
| `close()` | Closes the underlying database when the adapter opened it. |
|
|
60
60
|
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Telegram channel
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
`@arnilo/prism-channels/telegram` is framework-free Telegram Bot API transport for text, with bounded attachment handling. It uses native `fetch`; importing it or creating a factory starts no listener, poller, credential lookup, environment lookup, or network request.
|
|
6
|
+
|
|
7
|
+
## When to use it
|
|
8
|
+
|
|
9
|
+
Use for official Telegram Bot API text and bounded media. Private DMs are the default; group/supergroup text (including forum topics) is opt-in per adapter with `allowGroups: true` and stays subject to host `authorize`. Optional ephemeral streaming previews (`sendDrafts`) are private-chat only. Opt-in host hooks add voice transcription, document extraction and speech synthesis; images reach a model only when it declares image input. Not for Secret Chats, unbounded or stored media, or model-selected destinations.
|
|
10
|
+
|
|
11
|
+
Install peer and channel package:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @arnilo/prism @arnilo/prism-channels
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Inputs / request
|
|
18
|
+
|
|
19
|
+
`TelegramAdapterOptions` (polling): host `connectionId`, `botToken` (`CredentialValueSource`), service-owned `checkpoints`/`leases`/`cursorOwnership`, optional `allowGroups`, `sendDrafts`, `maxAttachmentBytes`, `transcribe`, `synthesize` and `extractDocumentText`. `TelegramWebhookHandlerOptions` add independent `webhookSecret` and the `admit` callback (intake only — outbound sends, drafts and `fetchAttachment` use an adapter instance, which works without `start()`). Importing or constructing starts no network.
|
|
20
|
+
|
|
21
|
+
## Request/response example
|
|
22
|
+
|
|
23
|
+
Polling is explicit and restart-safe only with a service-owned `CheckpointStore` cursor and `LeaseStore` receiver lease. `start()` verifies the token with `getMe`, refuses to poll if `getWebhookInfo` reports an existing webhook, then runs 30-second `getUpdates` requests. It accepts private `message.text`/`caption` updates from non-bot senders, plus `group`/`supergroup` text and media when `allowGroups: true`. `photo`, `voice` and `document` become bounded attachment *refs* — see [Attachments and voice](#attachments-and-voice). Service, edit and bot-echo updates are acknowledged without agent execution, as are channel/anonymous-admin `sender_chat` messages and forum topics that fail the host grant.
|
|
24
|
+
|
|
25
|
+
A parsed group message keeps the admission keys transport-exact: `externalConversationId` is the chat id (negative for groups), `externalActorId` is `from.id`, and `message_thread_id` becomes `threadId`. Only `claims` carry untrusted context (`chatType`, `isForum`, `forwarded`), and `authorize` receives them so a host can refuse forwarded messages or non-forum groups. Threads and senders are separate sessions and separate grants: a grant for `(chat A, topic 7, user X)` admits neither user Y, chat B, nor topic 8.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { createMessagingRuntime } from "@arnilo/prism-channels";
|
|
29
|
+
import { createTelegramAdapter } from "@arnilo/prism-channels/telegram";
|
|
30
|
+
|
|
31
|
+
const adapter = createTelegramAdapter({
|
|
32
|
+
connectionId: "support-telegram",
|
|
33
|
+
botToken: hostCredentials.telegramBotToken, // string, function, or CredentialValueSource
|
|
34
|
+
checkpoints: serviceCheckpoints,
|
|
35
|
+
leases: serviceLeases,
|
|
36
|
+
cursorOwnership: { tenantId: "service" }, // service scope, not a Telegram claim
|
|
37
|
+
allowGroups: true, // opt-in groups/topics; still deny-by-default at authorize()
|
|
38
|
+
maxAttachmentBytes: 1024 * 1024, // per `getFile` download; hard ceiling 4 MiB
|
|
39
|
+
transcribe: (audio, format) => transcription.transcribe({ model: "whisper-1", audio, format }), // voice -> turn text
|
|
40
|
+
synthesize: (text) => speech.synthesize({ model: "tts-1", input: text }), // text -> voice note, final replies only
|
|
41
|
+
extractDocumentText: (bytes, mimeType) => hostExtractor(bytes, mimeType), // documents are inert without this
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const runtime = createMessagingRuntime({
|
|
45
|
+
// Exact observed tuple: chat id, optional topic id, sender id. Display names are never keys.
|
|
46
|
+
authorize: ({ externalConversationId, threadId, externalActorId, claims }) =>
|
|
47
|
+
claims?.forwarded === true
|
|
48
|
+
? false
|
|
49
|
+
: (hostGrants.get(externalConversationId, threadId, externalActorId) ?? false),
|
|
50
|
+
resolveAgent: hostResolveAgent,
|
|
51
|
+
deliver: (reply) => adapter.send(reply),
|
|
52
|
+
onAssistantDelta: (delta) => adapter.sendDraft(delta), // opt-in previews; needs sendDrafts: true
|
|
53
|
+
fetchAttachment: (ref) => adapter.fetchAttachment(ref), // lets a model that declares image input receive the image
|
|
54
|
+
checkpoints: userCheckpoints,
|
|
55
|
+
leases: userLeases,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await adapter.start((event) => runtime.admit(event));
|
|
59
|
+
// shutdown: await adapter.stop(); await runtime.stop();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
[`examples/telegram-agent.ts`](../examples/telegram-agent.ts) exposes same composition as a host-supplied helper.
|
|
63
|
+
|
|
64
|
+
The adapter awaits `runtime.admit()` before advancing `offset`. Return the admission result from the callback: `capacity`, `unavailable`, and `stopped` keep Telegram's update unacknowledged; all other settled dispositions advance the durable cursor. The operation journal then deduplicates any replay before model or tool work.
|
|
65
|
+
|
|
66
|
+
A connection receiver lease (`prism.channels.v1.telegram.receiver`) is held for the poll loop. A second poller or a webhook worker using the same `connectionId`, service ownership and `LeaseStore` receives no work. This fences stored intake state; it cannot retract an already accepted platform request from a paused process.
|
|
67
|
+
|
|
68
|
+
## Implementation example
|
|
69
|
+
|
|
70
|
+
Mount `createTelegramWebhookHandler` on a host HTTPS route. It is a Web-standard `(Request) => Promise<Response>` handler; Prism does not create a listener, configure DNS/TLS, or call `setWebhook` for you.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { createTelegramWebhookHandler } from "@arnilo/prism-channels/telegram";
|
|
74
|
+
|
|
75
|
+
const telegramWebhook = createTelegramWebhookHandler({
|
|
76
|
+
connectionId: "support-telegram",
|
|
77
|
+
botToken: hostCredentials.telegramBotToken,
|
|
78
|
+
webhookSecret: hostSecrets.telegramWebhookSecret, // 1–256 URL-safe characters
|
|
79
|
+
admit: (event) => runtime.admit(event),
|
|
80
|
+
leases: serviceLeases,
|
|
81
|
+
cursorOwnership: { tenantId: "service" },
|
|
82
|
+
allowGroups: true, // must match the poller you are replacing
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Example host adapter: route only this fixed HTTPS path to telegramWebhook(request).
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Configure Telegram deliberately after the route, TLS and independent secret exist. Do not set `drop_pending_updates` and do not remove an active webhook merely to switch modes.
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
curl --fail-with-body \
|
|
92
|
+
-H 'content-type: application/json' \
|
|
93
|
+
-d '{"url":"https://bot.example/telegram","secret_token":"<independent-secret>","allowed_updates":["message","callback_query"]}' \
|
|
94
|
+
"https://api.telegram.org/bot<BOT_TOKEN>/setWebhook"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The handler requires `POST` + JSON, verifies `X-Telegram-Bot-Api-Secret-Token` with a fixed-length hash comparison before body parsing, bounds body bytes, acquires the same receiver lease, and calls `admit` before returning `204`. Bad secret/body returns `401`/`400`/`413`; lease, capacity, stopped runtime, or journal unavailability returns `503` so Telegram retries. Untrusted but final inputs (bot/media/unknown sender/anonymous admin, or group updates when `allowGroups` is off) return `204` without an agent call. `allowGroups` is parsed identically in both modes; set it the same on the webhook worker and the poller you replaced.
|
|
98
|
+
|
|
99
|
+
`callback_query` is promptly acknowledged with `answerCallbackQuery` and only accepts the runtime's `p:a:<opaque-token>` / `p:d:<opaque-token>` callback shape from a non-bot sender in an admitted private, group, or topic conversation (a topic callback keeps its `message_thread_id` as `threadId`). The adapter maps it to an `approval` event; callback data never becomes model input. The runtime validates its short-lived server-side binding before core resume.
|
|
100
|
+
|
|
101
|
+
## Outputs / response / events
|
|
102
|
+
|
|
103
|
+
`send()` accepts only replies addressed to its own `connectionId`; the runtime fixes `externalConversationId` and the reply `threadId` from the authorized binding, so a topic answer posts to that topic's `message_thread_id` and the model cannot choose either. It sends plain text, chunks at 3,500 UTF-16 code units without splitting surrogate pairs, and retries documented `retry_after` delays up to five attempts. A bounded server-issued control set renders as an inline keyboard on the final chunk; arbitrary model text cannot create buttons. There is no Markdown parse mode.
|
|
104
|
+
|
|
105
|
+
Known API failures return `{ delivered: false, reason }`. A network/timeout outcome, or a later failure after a prior chunk was accepted, rejects with a bounded ambiguous-delivery error. The common durable reply journal records thrown delivery as `delivery_unknown`; do not automatically resend it. Bot token URLs are never included in adapter errors and redirects are forbidden.
|
|
106
|
+
|
|
107
|
+
Telegram's `getUpdates` and webhooks are mutually exclusive. Updates remain available for at most 24 hours; the cursor is an ordering/acknowledgment record, not an exactly-once agent execution guarantee. Bots are not Secret Chats.
|
|
108
|
+
|
|
109
|
+
### Streaming previews (drafts)
|
|
110
|
+
|
|
111
|
+
With `sendDrafts: true` and `MessagingRuntimeOptions.onAssistantDelta: (delta) => adapter.sendDraft(delta)`, the adapter renders the answer while it is produced through Bot API `sendMessageDraft`: same chat and `message_thread_id` as the bound turn, `draft_id` fixed at one preview slot per chat, text capped to the last 3,500 UTF-16 code units. Previews are coalesced — one request in flight, the latest partial wins — so a token stream never floods the API, and failures are ignored because a draft is only a preview. Cancel, `stop()` and run end stop further previews. Drafts are private-chat only: group and supergroup chats are skipped even when `allowGroups` and `sendDrafts` are both on. No draft is ever a `ChannelReply`: nothing is journaled, no controls or attachments can ride along, and the terminal `sendMessage` remains the only message the user keeps.
|
|
112
|
+
|
|
113
|
+
### Attachments and voice
|
|
114
|
+
|
|
115
|
+
A `photo`, `voice` or `document` message is parsed into at most eight `ChannelAttachmentRef` values on the event: `kind`, `transportFileId` (Telegram `file_id`), optional `mimeType`, `byteLength` and `fileName`. A photo collapses to its largest size. The event never carries bytes, a URL, a caption-as-identity, or a model-chosen id, and nothing here can be addressed by model output.
|
|
116
|
+
|
|
117
|
+
Before `admit`, the adapter runs the one bounded fetch the host opted into (Bot API `getFile` plus a capped download):
|
|
118
|
+
|
|
119
|
+
- `voice` + host `transcribe` → the transcript becomes ordinary turn text (`caption`, then transcript). The audio is discarded after the call.
|
|
120
|
+
- `document` + host `extractDocumentText` → the extracted text becomes turn text. Without the hook the document stays inert: the message admits with an attachment ref and no text, and the runtime answers with a bounded "cannot be processed" notice instead of a model call.
|
|
121
|
+
- `photo` is never fetched by the adapter. The runtime fetches it (through `fetchAttachment`) only when the resolved agent's model declares `image` input, then sends it as one image content block next to the caption; otherwise the turn is refused with the same notice and no model call. `image` is the only attachment kind that becomes model input — audio/documents are text-only because Prism receives their host-produced text.
|
|
122
|
+
|
|
123
|
+
Oversize fails closed twice: the runtime denies an event whose *declared* sizes exceed `maxAttachmentBytes` (`denied: oversized`, no fetch, no model call), and the adapter refuses to buffer a body whose `Content-Length` or streamed size exceeds the same cap. Failures (network, credential, API, malformed `file_path`) yield no bytes, so the turn is refused rather than run without the attachment.
|
|
124
|
+
|
|
125
|
+
Outbound, `synthesize` adds a Bot API `sendVoice` next to the text for `final` replies: same bound chat and `message_thread_id`, OGG/OPUS, MP3 or M4A formats only, best-effort — a synthesis or upload failure never fails or re-sends the delivered text reply. Notices stay text-only. Nothing hangs a voice note on model-selected destinations.
|
|
126
|
+
|
|
127
|
+
## Extension and configuration notes
|
|
128
|
+
|
|
129
|
+
| Setting | Default | Range |
|
|
130
|
+
| --- | ---: | ---: |
|
|
131
|
+
| Poll timeout | 30 s | 1–50 s |
|
|
132
|
+
| Poll batch | 100 | 1–100 |
|
|
133
|
+
| Receiver lease | 90 s | poll timeout + 10 s–5 min |
|
|
134
|
+
| Webhook/API body | 128 KiB | 1 KiB–1 MiB |
|
|
135
|
+
| Outbound chunk | 3,500 UTF-16 units | fixed (under Telegram's 4,096 limit) |
|
|
136
|
+
| Known-send attempts | 5 | fixed |
|
|
137
|
+
| Group/topic text (`allowGroups`) | off | boolean |
|
|
138
|
+
| Streaming previews (`sendDrafts`) | off | boolean (private chats only) |
|
|
139
|
+
| Attachment bytes (`maxAttachmentBytes`) | 1 MiB | 1 B–4 MiB |
|
|
140
|
+
| Voice transcription (`transcribe`) | off | host `TranscriptionProvider` wrapper |
|
|
141
|
+
| Speech synthesis (`synthesize`) | off | host `SpeechProvider` wrapper (final replies) |
|
|
142
|
+
| Document text (`extractDocumentText`) | off | host extractor |
|
|
143
|
+
|
|
144
|
+
## Security and performance notes
|
|
145
|
+
|
|
146
|
+
The webhook secret is independent of the bot token and compared with a fixed-length hash before body parsing. Redirects are forbidden; bot-token URLs never appear in adapter errors. Polling and webhooks are mutually exclusive. Model text cannot create inline buttons.
|
|
147
|
+
|
|
148
|
+
Groups and topics are off by default and never self-authorize: the adapter only parses an observed `(chat id, topic id?, user id)` and the host grant decides, so a group membership claim is not an identity. Chat titles, usernames and display names are never actor ids. Bots, `sender_chat` (channel posts and anonymous admins), and non-text updates are dropped before admission; forwarded messages are admitted but flagged in `claims` so the grant can refuse them. In groups Telegram privacy mode limits what a bot receives (commands and replies to it) unless an operator disables it in BotFather; Prism does not work around that platform behavior. Signal stays DM-only.
|
|
149
|
+
|
|
150
|
+
Drafts are display-only and cannot become input: they carry no controls or attachments, the destination chat/thread always comes from the bound event (never from model output or from the delta), text is redacted with the same redactor as finals, and the 3,500-unit cap keeps the request inside Bot API limits. Since `sendDraft` is fire-and-forget and failures are swallowed, a draft can never fail a turn, mask a delivery failure, or replace the journaled final reply.
|
|
151
|
+
|
|
152
|
+
Media is bounded on every axis. Only the current event's refs are ever fetched (`fetchAttachment` takes a ref, and the runtime only passes refs of the event it is running), the cap is enforced twice (declared size and streamed body), and downloads use the same fixed origin with redirects forbidden. Bytes live only for the fetch call: they are never journaled (`ChannelOperationRecord` and `ChannelReplyRecord` store text only), never logged, and never written to disk. A `file_id` is not a capability — the bot proves nothing to Telegram by holding one, and a caller-supplied or model-invented id yields at most one bounded request inside the cap. Hosts wire `transcribe`/`synthesize`/`extractDocumentText` to their own provider wrappers; `@arnilo/prism-channels` never depends on `@arnilo/prism-providers`.
|
|
153
|
+
|
|
154
|
+
## Related APIs
|
|
155
|
+
|
|
156
|
+
- [Messaging channels](messaging-channels.md): authorization, durable operations and reply recovery.
|
|
157
|
+
- [Web-standard server handler](server.md): mounting a `Request`/`Response` handler in a Node or framework host.
|
package/docs/testing.md
CHANGED
|
@@ -21,14 +21,14 @@ Documents how the hermetic suite runs, which stage a new suite belongs to, and t
|
|
|
21
21
|
| build race | `scripts/phase23-build-race.test.mjs` |
|
|
22
22
|
| workspace suites | `npm run test --workspaces --if-present` |
|
|
23
23
|
|
|
24
|
-
Protected-environment legs (Postgres, PTY, NATS, live credentials) are not part of `npm test`; they fail closed with one canonical `BLOCKED GATE <id> requires=<names> evidence=<surface> hint=<how to unblock>` record and a non-zero exit when their infrastructure is absent (registry and audit: `node scripts/blocked-gate.mjs`). Retired phase freeze/release gates live in `scripts/` for audit but are deliberately kept out of the chain. 0.7.0 host-completeness packed proof is `scripts/fixtures/e2e-070-host-completeness-journey.mjs` (same packed consumer as the full-surface journey) plus `scripts/host-completeness-evidence.test.mjs`; live legs stay skip-not-fail. R16/R17 stay blocked until plans 077/074 ship.
|
|
24
|
+
Protected-environment legs (Postgres, PTY, NATS, live credentials) are not part of `npm test`; they fail closed with one canonical `BLOCKED GATE <id> requires=<names> evidence=<surface> hint=<how to unblock>` record and a non-zero exit when their infrastructure is absent (registry and audit: `node scripts/blocked-gate.mjs`). A successful `PRISM_TEST_POSTGRES_URL=… npm run test:postgres` first removes stale evidence, then writes gitignored `scripts/postgres-evidence.json` with only current `gitHead`, capture time, and TAP counts; release evidence accepts it only at the same `HEAD`. Retired phase freeze/release gates live in `scripts/` for audit but are deliberately kept out of the chain. 0.7.0 host-completeness packed proof is `scripts/fixtures/e2e-070-host-completeness-journey.mjs` (same packed consumer as the full-surface journey) plus `scripts/host-completeness-evidence.test.mjs`; live legs stay skip-not-fail. R16/R17 stay blocked until plans 077/074 ship.
|
|
25
25
|
|
|
26
26
|
## Isolation rules
|
|
27
27
|
|
|
28
28
|
- **Scratch roots come from the OS.** A suite that writes anything creates its root with `mkdtempSync(join(tmpdir(), "prism-…"))` and removes it in `after()`. Never rely on `process.cwd()` for write targets: the same suite runs with different working directories (workspace stage vs. root stage), so a cwd-relative root silently writes into the repository.
|
|
29
29
|
- **Pass explicit roots.** Wiki, memory, and store helpers default `workspaceRoot` to `process.cwd()`; suites pass their scratch root (and a `wikiRoot` relative to it) instead of accepting the default.
|
|
30
30
|
- **Tracked fixtures stay byte-identical.** `packages/memory/.wiki/` is a tracked wiki fixture and `docs/` is a tracked corpus. `scripts/wiki-scratch-isolation.test.mjs` runs the wiki suites from the package and from the repository root and fails if the tracked fixture hashes change, if a new file appears inside the fixture, if `<repo>/.wiki/` is scaffolded, or if the old cwd-relative scratch directories reappear.
|
|
31
|
-
- **Gates never write inside the repository.** A gate asserts against tracked content and spawns suites in temporary directories only. A gate that spawns `node --test` must strip `NODE_TEST_CONTEXT`/`NODE_TEST_WORKER_ID` from the child environment (an inherited value makes the nested runner skip every file and still exit 0) and assert the child reported a non-zero pass count.
|
|
31
|
+
- **Gates never write inside the repository.** A gate asserts against tracked content and spawns suites in temporary directories only. A gate that spawns `node --test` must strip `NODE_TEST_CONTEXT`/`NODE_TEST_WORKER_ID` from the child environment (an inherited value makes the nested runner skip every file and still exit 0) and assert the child reported a non-zero pass count. The wiki gate additionally uses `--test-isolation=none`: all nested files run in its one runner process, avoiding process-worker IPC deserialization without retrying failures.
|
|
32
32
|
- **Wait by polling, not by sleeping.** Async browser state (download quarantine, idle reaping) is not awaitable from the outside — `manager.ts` settles it on a fire-and-forget listener promise — so a fixed sleep is a race that loses under CPU load and fails the assertion for a reason unrelated to the behavior under test. Suites poll observable state through `waitFor(read, ok, label, { timeoutMs, intervalMs })` in `packages/web-tools/src/browser/__tests__/wait-for.ts`, which returns as soon as the state appears and otherwise throws naming the label and the last observed value. Fixed sleeps remain only where real elapsed time is the subject of the test (idle TTLs).
|
|
33
33
|
|
|
34
34
|
## Related APIs
|
package/docs/wiki.md
CHANGED
|
@@ -85,7 +85,7 @@ The staging primitive behind `/wiki-ingest`, `wiki_ingest`, and the CLI. Accepts
|
|
|
85
85
|
| :--- | :--- |
|
|
86
86
|
| Text-like files and `text` | Decoded as UTF-8 (RAG text/markdown/html parsers) |
|
|
87
87
|
| Uncompressed PDF | Parsed by the RAG PDF parser (bounded pages/bytes) |
|
|
88
|
-
| Compressed PDF / DOCX | Throws a named error unless the host supplies `options.extractDocument` (e.g. wire `createDocumentReader()` from `@arnilo/prism-
|
|
88
|
+
| Compressed PDF / DOCX | Throws a named error unless the host supplies `options.extractDocument` (e.g. wire `createDocumentReader()` from `@arnilo/prism-work/document-reader`) |
|
|
89
89
|
| `url` | `assertSsrfAllowedUrl` runs first (private/link-local hosts rejected before any fetch); then the host `fetchUrl` hook supplies the bytes/text — missing or empty hook output fails closed. Staged filename comes from the hook, the URL extension (`doc.pdf`), or `source.md` |
|
|
90
90
|
| Images | Staged as-is; stub extract points at the staged `source.*` — no OCR; view the file |
|
|
91
91
|
| Unknown binary | Fails closed unless `extractDocument` claims it |
|
|
@@ -97,7 +97,7 @@ export const handler = createArtifactHandler({ service: artifacts, authorize: ho
|
|
|
97
97
|
|
|
98
98
|
## Business action drafts and editable approvals (0.7.0)
|
|
99
99
|
|
|
100
|
-
Business tools (e.g. mail, calendar, documents in `@arnilo/prism-
|
|
100
|
+
Business tools (e.g. mail, calendar, documents in `@arnilo/prism-work/connectors`) record mutations through durable `WorkDraftStore` drafts before execution. Human reviewers can approve, deny, or edit draft payloads directly:
|
|
101
101
|
- AG-UI clients advertise and send `approveWithEdits` with revised arguments (`editedArgs`/`modifiedArguments`).
|
|
102
102
|
- The server resume endpoint accepts `{ decision: "approve", modifiedArguments: { ... } }` under CAS `expectedVersion`.
|
|
103
103
|
- If arguments are modified, a new draft revision is created with bumped revision number and payload digest. The previous revision's approval is invalidated and the mutation requires approval for the revised content.
|
package/docs/work-connectors.md
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
# Work connectors
|
|
2
2
|
|
|
3
|
-
Least-privilege Microsoft 365 and Google Workspace connectors live in `@arnilo/prism-
|
|
3
|
+
Least-privilege Microsoft 365 and Google Workspace connectors live in `@arnilo/prism-work/connectors`.
|
|
4
4
|
|
|
5
5
|
## Principles
|
|
6
6
|
|
|
7
|
-
1. **Host-pinned binary** — Prism never downloads or shells an untrusted CLI path.
|
|
8
|
-
2. **Hard-coded
|
|
7
|
+
1. **Host-pinned binary or HTTP adapter** — Prism never downloads or shells an untrusted CLI path; HTTP adapters use fixed origins and pinned fetch.
|
|
8
|
+
2. **Hard-coded operation maps** — models choose typed tool args; they never supply command strings or request URLs.
|
|
9
9
|
3. **Draft-then-approve & durable resumption** — mutations create a draft with tracked revisions and payload digests; side effects run only after host approval binds to that exact revision; durable checkpoint persistence survives process restart.
|
|
10
10
|
4. **Idempotent retries** — `IdempotencyStore` keyed by identity + operation key.
|
|
11
11
|
5. **Isolated config** — per-identity `configDir` (CLI `HOME`); no credential argv.
|
|
12
|
-
6. **Shared result shapes** — mail/calendar/file/task list/get tools normalize onto `WorkMailMessage` / `WorkCalendarEvent` / `WorkFileItem` / `WorkTaskItem` without hiding provider-specific ops.
|
|
12
|
+
6. **Shared result shapes** — mail/calendar/file/task list/get tools normalize onto `WorkMailMessage` / `WorkCalendarEvent` / `WorkFileItem` / `WorkTaskItem` without hiding provider-specific ops. Binary file gets return only untrusted artifact/path metadata plus hash and byte length.
|
|
13
13
|
|
|
14
14
|
## Microsoft 365
|
|
15
15
|
|
|
16
|
-
See [Work tools](work-tools.md).
|
|
16
|
+
See [Work tools](work-tools.md). Adapters: `createMicrosoft365CliAdapter` or `createMicrosoft365HttpAdapter` from `@arnilo/prism-work/connectors`.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
The CLI adapter uses [@pnp/cli-microsoft365](https://pnp.github.io/cli-microsoft365/) commands such as `outlook message list|get`, `outlook mail send`, `outlook event list|add`, `file list|add|copy`, `spo file sharinglink add`. The HTTP adapter maps its fixed operation set to `graph.microsoft.com` through pinned fetch; tokens reach it only in `Authorization`. `m365_file_get` accepts only an item ID and downloads via fixed `/me/drive/items/{id}/content` into a scanned artifact and/or contained sandbox path; it never emits bytes to model context. Upload drafts accept a host path, artifact ref, or contained sandbox path and bind the content hash before approval. It requires direct Graph Drive-item URLs for one-request file list/upload/copy and rejects arbitrary SharePoint links. To Do / Planner / Teams remain capability-gated.
|
|
19
19
|
|
|
20
20
|
## Google Workspace
|
|
21
21
|
|
|
22
|
-
See [Work tools](work-tools.md).
|
|
22
|
+
See [Work tools](work-tools.md). Adapters: `createGoogleWorkspaceCliAdapter` or `createGoogleWorkspaceHttpAdapter` from `@arnilo/prism-work/connectors`.
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
The CLI adapter uses [`@googleworkspace/cli` (`gws`)](https://github.com/googleworkspace/cli): `gmail users messages list|get`, `gmail +send`, `calendar events list|insert`, `drive files list|create`, `drive permissions create`, `tasks tasks *`, and capability-gated Docs/Sheets/Slides create and fixed update commands. The HTTP adapter maps the same typed operations to Gmail, Calendar, Drive, Tasks, Docs, Sheets, and Slides REST origins with `pinnedFetch`; `gws_file_get` accepts only an item ID and uses fixed `Drive files.get?alt=media`, returning only scanned artifact/path metadata. Its fixed allowlist excludes model-supplied URLs. Docs/Sheets/Slides updates accept only replace/insert text or string-matrix values, never a model-provided batch request array. Discovery `schema` and `auth`/`login`/`setup` are forbidden from Prism argv.
|
|
25
25
|
|
|
26
26
|
Drive **knowledge synchronization** (RAG import of file text + host-mapped ACL via `changes.list`) is not this CLI adapter. Use `createGoogleDriveConnector` / `syncKnowledge` from `@arnilo/prism-memory/rag` — see [Knowledge synchronization](knowledge-sync.md).
|
|
27
27
|
|
|
28
28
|
## Scoped OAuth establishment (0.0.14)
|
|
29
29
|
|
|
30
|
-
Hosts establish, refresh, and revoke scoped OAuth credentials for these workloads through the existing `OAuthProvider` / credential-store seams (`@arnilo/prism-core/credentials/node`): `createMicrosoft365OAuthProvider` / `createGoogleWorkspaceOAuthProvider` (PKCE + device code), least-privilege scope bundles per capability (`resolveMicrosoft365Scopes` / `resolveGoogleWorkspaceScopes`, read vs mutation). Connectors consume a per-identity token via a late-bound `tokenProvider
|
|
30
|
+
Hosts establish, refresh, and revoke scoped OAuth credentials for these workloads through the existing `OAuthProvider` / credential-store seams (`@arnilo/prism-core/credentials/node`): `createMicrosoft365OAuthProvider` / `createGoogleWorkspaceOAuthProvider` (PKCE + device code), least-privilege scope bundles per capability (`resolveMicrosoft365Scopes` / `resolveGoogleWorkspaceScopes`, read vs mutation). Connectors consume a per-identity token via a late-bound `tokenProvider`: CLI adapters inject it into env and HTTP adapters send it only as `Authorization` — never argv or model context; revocation fails closed. See [Credential storage](credential-storage.md) and [Work tools](work-tools.md).
|
|
31
31
|
|
|
32
32
|
## Out of scope
|
|
33
33
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Work sandbox (`@arnilo/prism-work/sandbox`)
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
Host-built Docker image and in-process composition for document work. `WORK_SANDBOX_IMAGE` is a digest-pinned fixture (`name@sha256:<64-hex>`); hosts replace the zero digest after `docker build`. `createWorkComposition({ sandbox })` takes an injected `DisposableSandbox` (from `createDockerSandbox`, never forked here), copies its capability attestation, wires `createOfficeTools` filesystem mode plus `work_exec`, and optionally host-side `createWorkTools`. Connectors stay on the host. Default env has no `M365_*` / `GOOGLE_*` keys.
|
|
6
|
+
|
|
7
|
+
## When to use it
|
|
8
|
+
|
|
9
|
+
Use when Office parse/generate and Python/LibreOffice/poppler scripts must run inside a network-none container. Do **not** put Graph/Gmail tokens in the sandbox. Do not import `@arnilo/prism-coding-tools` from this package — the host constructs `createDockerSandbox({ image: WORK_SANDBOX_IMAGE, user: "65532:65532", network: { mode: "none" } })` and injects the session.
|
|
10
|
+
|
|
11
|
+
## Inputs / request
|
|
12
|
+
|
|
13
|
+
`createWorkComposition(options)`:
|
|
14
|
+
|
|
15
|
+
| Field | Meaning |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `sandbox` | Injected adapter with `execFile` and optional `readFile`/`writeFile`/`root`. Required. |
|
|
18
|
+
| `connectors?` | `WorkToolsOptions` for host-side M365/GWS tools. |
|
|
19
|
+
| `office?` | Extra `createOfficeTools` options (caps, artifacts, redactor). |
|
|
20
|
+
| `reader?` | Host-built `DocumentReader`; exposed on the composition, not turned into tools. |
|
|
21
|
+
| `filesystem?` | `WorkSandboxFilesystem` when the sandbox has no `readFile`/`writeFile`. |
|
|
22
|
+
| `env?` | Extra container env. `M365_*` and `GOOGLE_*` names throw. |
|
|
23
|
+
|
|
24
|
+
Image build context: `packages/prism-work` (`sandbox/Dockerfile` + `sandbox/soffice.sh` + `vendor/hermes-agent`). Vendored Hermes scripts land at `/opt/prism-work/skills`.
|
|
25
|
+
|
|
26
|
+
## Outputs / response / events
|
|
27
|
+
|
|
28
|
+
`{ tools, composition }`. `tools` always include `office_*` and `work_exec`. `composition.capabilities` is a frozen copy of the sandbox attestation (malformed metadata → every field `false`). `networkIsolated` is true only when the sandbox attests it — Docker reports that solely for `network: { mode: "none" }`. `composition.execFile` strips token env names and forces LibreOffice `-env:UserInstallation=file:///tmp/lo-profile` without `--accept` / macro flags.
|
|
29
|
+
|
|
30
|
+
## Request/response example
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { createDockerSandbox } from "@arnilo/prism-coding-tools/security";
|
|
34
|
+
import { createWorkComposition, WORK_SANDBOX_IMAGE } from "@arnilo/prism-work/sandbox";
|
|
35
|
+
|
|
36
|
+
const sandbox = await createDockerSandbox({
|
|
37
|
+
docker: "/usr/bin/docker",
|
|
38
|
+
image: WORK_SANDBOX_IMAGE, // replace zeros with the host-built digest
|
|
39
|
+
sourceRoot: workdir,
|
|
40
|
+
user: "65532:65532",
|
|
41
|
+
network: { mode: "none" },
|
|
42
|
+
});
|
|
43
|
+
const { tools, composition } = createWorkComposition({ sandbox, connectors });
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Implementation example
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { createWorkComposition, WORK_SANDBOX_IMAGE } from "@arnilo/prism-work/sandbox";
|
|
50
|
+
|
|
51
|
+
const { tools, composition } = createWorkComposition({
|
|
52
|
+
sandbox: fakeDisposableSandbox, // tests inject this; no Docker
|
|
53
|
+
});
|
|
54
|
+
composition.capabilities.networkIsolated; // copied, never invented
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Build:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
docker build -f packages/prism-work/sandbox/Dockerfile -t prism-work-sandbox packages/prism-work
|
|
61
|
+
docker image inspect --format '{{index .RepoDigests 0}}' prism-work-sandbox
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Extension and configuration notes
|
|
65
|
+
|
|
66
|
+
- Do not fork `createDockerSandbox`. Image pull/build stays outside Prism (`--pull=never`).
|
|
67
|
+
- Connectors optional and host-side. Bytes move via sandbox import/export and office filesystem tools.
|
|
68
|
+
- `work_exec` is argv-only (`file` + `args`); no model-supplied shell string.
|
|
69
|
+
- Protected image check: `PRISM_TEST_WORK_SANDBOX=1` runs `scripts/work-sandbox-image.test.mjs` and sandbox recalc/render/legacy-convert tests. Default `npm test` does not spawn `soffice`.
|
|
70
|
+
|
|
71
|
+
## Recalc and visual QA
|
|
72
|
+
|
|
73
|
+
In-process `SheetModel` does not evaluate formulas. Cached values are filled only by LibreOffice in this image (`network: none`, isolated `/tmp/lo-profile`, deleted with the container). Vendored scripts:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python3 /opt/prism-work/skills/skills/productivity/xlsx/scripts/xlsx_recalc.py /workspace/out.xlsx --timeout 60
|
|
77
|
+
python3 /opt/prism-work/skills/skills/productivity/powerpoint/scripts/pptx_render.py /workspace/deck.pptx --outdir /workspace/render
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Equivalent argv (wrapper already injects `-env:UserInstallation=file:///tmp/lo-profile --headless`):
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
soffice --headless -env:UserInstallation=file:///tmp/lo-profile --convert-to pdf --outdir /tmp/out /workspace/out.xlsx
|
|
84
|
+
pdftoppm -png -r 100 /tmp/out/out.pdf /tmp/out/page
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
External workbook links cannot be fetched. Recalc then fails closed: formula stays, cached value missing or error — Prism does not invent a number. `soffice` timeout ≤ 60 s.
|
|
88
|
+
|
|
89
|
+
## Legacy convert
|
|
90
|
+
|
|
91
|
+
Prism AST still refuses non-ZIP packages. Convert OLE `.doc` / `.xls` / `.ppt` inside this image (`network: none`, isolated `/tmp/lo-profile`, macros refused), then `office_parse`:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
soffice --headless -env:UserInstallation=file:///tmp/lo-profile --convert-to docx --outdir /workspace /workspace/legacy.doc
|
|
95
|
+
soffice --headless -env:UserInstallation=file:///tmp/lo-profile --convert-to xlsx --outdir /workspace /workspace/legacy.xls
|
|
96
|
+
soffice --headless -env:UserInstallation=file:///tmp/lo-profile --convert-to pptx --outdir /workspace /workspace/legacy.ppt
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Do not enable macros. Encrypted OOXML stays dropped. No in-process OLE parser.
|
|
100
|
+
|
|
101
|
+
## Security and performance notes
|
|
102
|
+
|
|
103
|
+
- Default network none. Composition does not claim isolation the sandbox did not attest.
|
|
104
|
+
- Token env keys denied by name (`M365_*`, `GOOGLE_*`) at composition construct and `execFile`.
|
|
105
|
+
- LibreOffice wrapper: private `/tmp/lo-profile`, `--headless`, no macro enable, `--accept` refused. No listening socket. Legacy convert uses the same wrapper.
|
|
106
|
+
- Zip bombs: existing office parse caps; sandbox export uses existing export caps.
|
|
107
|
+
- Image build is CI/protected, not default unit tests. Composition construct is in-process with a fake sandbox.
|
|
108
|
+
|
|
109
|
+
## Related APIs
|
|
110
|
+
|
|
111
|
+
- [Coding security](coding-security.md) — `createDockerSandbox` digest pin, user, network none
|
|
112
|
+
- [Work tools](work-tools.md) — host-side connectors
|
|
113
|
+
- [Documents](documents.md) — `createOfficeTools`
|
|
114
|
+
- [Document reader](document-reader.md) — optional `reader` injection
|
|
115
|
+
- [Context and skills](context-and-skills.md) — `loadWorkSkills()` (`docx`, `xlsx`, `powerpoint`, `pdf`)
|