@tangle-network/agent-app 0.43.49 → 0.43.51

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/README.md CHANGED
@@ -12,6 +12,7 @@ The substrate packages — `@tangle-network/agent-runtime`, `agent-eval`, `agent
12
12
 
13
13
  - **Structured tool side channel** — `submit_proposal` (approval-gated), `schedule_followup`, `render_ui`, `add_citation`, exposed as validated tool calls over three surfaces (HTTP route, per-turn MCP server, agent-runtime executor). No fenced-text parsing.
14
14
  - **Bounded tool loop** — `runAppToolLoop` / `streamAppToolLoop`: stream a turn → collect tool calls → dispatch → fold results back → re-run, capped. Substrate-free behind a `streamTurn` seam, so it drives a sandboxed agent, a Worker, or an in-browser copilot unchanged.
15
+ - **Assembled chat vertical** — `createChatTurnRoutes` wires auth → thread/message store → streaming turn with buffered replay → uploads → sidecar question answering into one route factory, over `authorize` / `produce` / `store` / `interactions` seams. No hand-rolled orchestration. See [`examples/chat-app.md`](./examples/chat-app.md).
15
16
  - **Sandbox-optional** — the same tools, billing, eval, and loop work without a container. A `fetch`-only adapter maps any OpenAI-compatible stream (Tangle Router, tcloud) into the loop. See [`examples/browser-copilot.md`](./examples/browser-copilot.md).
16
17
  - **Resumable turns (sandbox-free path)** — for a browser/edge copilot streaming the Router directly, buffer a turn so a dropped tab loses nothing and a reconnecting client replays the tail. **Sandbox products don't need this** — the sandbox SDK already buffers + replays sessions (`streamPrompt` + `lastEventId`). See [`examples/resumable-turns.md`](./examples/resumable-turns.md).
17
18
  - **Composes the engine, never forks it** — `/eval` re-exports `@tangle-network/agent-eval`'s verifier; `/integrations` wraps the hub; `/tangle` and `/billing` take the tcloud client as a structural contract. Engines are **peer dependencies** — you pin the version, nothing is bundled.
@@ -96,6 +97,8 @@ const streamTurn = createOpenAICompatStreamTurn({ ...cfg, tools })
96
97
 
97
98
  The full three-transport walkthrough (Tangle Router, tcloud, Vercel AI SDK) is in [`examples/browser-copilot.md`](./examples/browser-copilot.md).
98
99
 
100
+ Building the full **server chat vertical** instead — auth, thread/message tables, a streaming turn with buffered replay, uploads, and sidecar question answering — is the job of `createChatTurnRoutes` (`/chat-routes`) and the modules around it. The end-to-end assembly, including the durable plan/question workflow and the client composer, is in [`examples/chat-app.md`](./examples/chat-app.md).
101
+
99
102
  ## How it's organised
100
103
 
101
104
  One rule decides where anything lives:
@@ -128,6 +131,20 @@ Each is an independent entry point — import only what you use.
128
131
  | [`/stream`](src/stream) | SSE normalization and turn identity: `normalizeToolEvent`, `resolveChatTurn`, `encodeEvent`, message-part merging. |
129
132
  | [`/redact`](src/redact) | `redactForIngestion` — PII redaction before content leaves the boundary. |
130
133
 
134
+ **The chat vertical** — the assembled server chat stack. Wire it with [`examples/chat-app.md`](./examples/chat-app.md).
135
+
136
+ | Subpath | What it gives you |
137
+ |---|---|
138
+ | [`/chat-routes`](src/chat-routes) | `createChatTurnRoutes` — the assembled turn vertical: NDJSON `turn` + buffered `replay` + `running` reconnect-discovery + composed `interactions` answer endpoints, over `authorize` / `produce` / `store` seams. Plus `createSandboxChatProducer`, `createUploadRoute`, `createSandboxFileIndexRoute`, `withDurableChatProjection`, and the import-free `./wire` contract (`chatTurnRequestInit`, `ChatTurnRequestPayload`). Composes agent-runtime's `handleChatTurn`; subpath-only (not re-exported from the root). |
139
+ | [`/chat-store`](src/chat-store) | `createChatTables` + `createChatStore` — drizzle thread/message persistence behind the `ChatStore` port, and the canonical `ChatMessagePart` parts vocabulary (`toChatMessageParts`, part guards). The drizzle store/schema is subpath-only. |
140
+ | [`/interactions`](src/interactions) | Human-in-the-loop ask channel: `createInteractionAnswerRoute` (list/answer endpoint factory — validation, 410 mapping, duplicate-answer safety), the server sidecar client, and the shared `ChatInteraction` wire/persisted-part contract + codecs. Composes `@tangle-network/agent-interface` types. |
141
+ | [`/durable-chat`](src/durable-chat) | Durable plan/question workflow around the authoritative channels: `createDurablePlanRoutes`, `createDurableChatScope`, `createDurableChatEventProjection`, `createDurableInteractionRoutePersistence`, the `DurablePlanStore` port, and `InMemoryDurableChatStateStore` (tests/demos only — production supplies the store). |
142
+ | [`/plans`](src/plans) | Browser-safe durable-plan chat projection: `parsePlanSubmittedEvent`, `planToPersistedPart` / `persistedPartToPlan`, `canTransitionPlanStatus`, and the `ChatPlan` union. Byte-matches the SDK's `SandboxSession.plan()`. |
143
+ | [`/object-store`](src/object-store) | Content-addressed durable attachment store: the `ObjectStore` port + `createR2ObjectStore`, `signObjectUrl` / `verifyObjectUrl`, `createProxiedArtifactRoute`, `objectKey`. |
144
+ | [`/app-auth`](src/app-auth) | `createAppAuth` — better-auth config factory + request guards (`requireApiUser`) over the standard users/sessions/accounts/verifications tables; composes `/platform`'s SSO cookie minter. Optional `better-auth` peer; subpath-only. |
145
+ | [`/sandbox`](src/sandbox) | Workspace sandbox provisioning + turn streaming: `ensureWorkspaceSandbox` / `peekWorkspaceSandbox`, `streamSandboxPrompt` / `runSandboxPrompt`, `createWorkspaceSandboxManager`, and terminal / runtime-proxy handlers. Peer `@tangle-network/sandbox`; subpath-only. |
146
+ | [`/platform`](src/platform) | Tangle platform glue: cross-site SSO (`createTangleSsoHandlers`), request guards (`createAuthGuard`, `guardResolution`), the hub proxy, and seat billing. |
147
+
131
148
  The root entry (`@tangle-network/agent-app`) re-exports every module, but importing the subpath keeps your bundle to what you use.
132
149
 
133
150
  ### Missions: id shape and product columns
@@ -10,7 +10,7 @@ import '../flow-types-Cb_AblZs.js';
10
10
  import '../sandbox-terminal-BIIC__CP.js';
11
11
  import '../catalog/index.js';
12
12
  import '../harness/index.js';
13
- import '../file-index-b26ee-_R.js';
13
+ import '../attachment-validation-DX2KIzMC.js';
14
14
  import '../stream-normalizer-DWvtmY6F.js';
15
15
 
16
16
  /**
@@ -3,9 +3,9 @@ import {
3
3
  ChatMessages,
4
4
  ModelPicker,
5
5
  ProviderLogo
6
- } from "../chunk-O6H2WD3I.js";
6
+ } from "../chunk-EIG7ZQW2.js";
7
7
  import "../chunk-65P3HJY3.js";
8
- import "../chunk-LCNY3DCM.js";
8
+ import "../chunk-3EKOSBYL.js";
9
9
  import "../chunk-2QI7XV2T.js";
10
10
  import "../chunk-JGYOYY5D.js";
11
11
  import "../chunk-5MG74GVQ.js";
@@ -0,0 +1,219 @@
1
+ import { K as FileMention } from './parts-IB-Kbb7z.js';
2
+
3
+ /**
4
+ * `createSandboxFileIndexRoute` — server side of `@`-file-mentions
5
+ * (companion to sandbox-ui#184's composer mention primitive). Serves a flat,
6
+ * ignore-filtered listing of the workspace sandbox so `useFileMentions`
7
+ * (`/web-react`) can filter it client-side without a round trip per
8
+ * keystroke.
9
+ *
10
+ * Same seam style as `createUploadRoute`: `authorize({ request })` resolves a
11
+ * structural `{ tree(path, opts) }` handle (the shape of the sandbox SDK's
12
+ * `box.fs.tree`) — no SDK import here. `authorize` also carries the
13
+ * cold-box signal: a sandbox that isn't running yet answers `{ status:
14
+ * 'warming' }` directly, never provisions-and-waits inside this route.
15
+ *
16
+ * A box can also be running with its workspace root not yet materialised, which
17
+ * `authorize` cannot see; the route recognises that one signal off `fs.tree`
18
+ * and answers `warming` too, so every consumer gets the retry-and-wait state
19
+ * instead of a 500. Every other `tree()` failure propagates.
20
+ */
21
+
22
+ /** One entry from a structural `tree()` scan. Mirrors the sandbox SDK's
23
+ * `FileTreeFile` (`path`, `size`, `mtime`) — `mtime` is unused here so it's
24
+ * omitted from the structural match. */
25
+ interface SandboxTreeFile {
26
+ path: string;
27
+ size: number;
28
+ }
29
+ /** Structural match of the sandbox SDK's `box.fs.tree` result shape
30
+ * (`FileTreeResult`). `stats.truncated` is the only stat this route reads;
31
+ * the rest ride through unread on the real SDK type. */
32
+ interface SandboxTreeResult {
33
+ root: string;
34
+ files: SandboxTreeFile[];
35
+ stats: {
36
+ truncated: boolean;
37
+ };
38
+ }
39
+ /** Structural match of the sandbox SDK's `box.fs` tree surface. */
40
+ interface SandboxFileTreeSource {
41
+ tree(path: string, options?: {
42
+ maxDepth?: number;
43
+ }): Promise<SandboxTreeResult>;
44
+ }
45
+ interface FileIndexReadyResponse {
46
+ status: 'ready';
47
+ /** Workspace-relative entries. Same shape as `FileMention` (`./wire`) so a
48
+ * client can hand a response entry straight to `fileMentionsToParts` /
49
+ * `buildMentionPromptBlock` without remapping. */
50
+ files: FileMention[];
51
+ /** True when either the underlying scan truncated (SDK-side cap) or this
52
+ * route's own `maxEntries` cap trimmed the filtered list. The client
53
+ * should show "showing first N files" rather than imply completeness. */
54
+ truncated: boolean;
55
+ generatedAt: string;
56
+ }
57
+ /** Cold-box answer: no provisioning happened, no files were scanned. The
58
+ * client shows a warming state and retries — this route never blocks on a
59
+ * box coming up. Two situations produce it: `authorize` reporting a box that
60
+ * is not running, and a running box whose workspace root does not exist yet
61
+ * (see `isMissingRootError`). */
62
+ interface FileIndexWarmingResponse {
63
+ status: 'warming';
64
+ }
65
+ type FileIndexResponse = FileIndexReadyResponse | FileIndexWarmingResponse;
66
+ /** Short-TTL cache seam so repeat popover opens in the same session don't
67
+ * re-scan the workspace. Host-provided (e.g. a KV binding); `key` is
68
+ * whatever `authorize` returns as `cacheKey` — this route treats it opaquely. */
69
+ interface FileIndexCache {
70
+ get(key: string): Promise<FileIndexReadyResponse | null> | FileIndexReadyResponse | null;
71
+ put(key: string, value: FileIndexReadyResponse, options?: {
72
+ ttlSeconds?: number;
73
+ }): Promise<void> | void;
74
+ }
75
+ type FileIndexAuthorization = {
76
+ status: 'ready';
77
+ /** Structural sandbox `fs` handle, usually `ensureWorkspaceSandbox(...)` → `box.fs`. */
78
+ fs: SandboxFileTreeSource;
79
+ /** Workspace root to index (e.g. `/home/agent`). */
80
+ root: string;
81
+ /** Extra ignore segments for this request, merged with the route's
82
+ * defaults + `CreateSandboxFileIndexRouteOptions.ignore`. */
83
+ ignore?: string[];
84
+ /** Opaque cache key for the optional cache seam. Omit to skip caching
85
+ * for this request (e.g. a workspace the host chooses not to cache). */
86
+ cacheKey?: string;
87
+ } | {
88
+ status: 'warming';
89
+ } | {
90
+ status: 'denied';
91
+ response: Response;
92
+ };
93
+ interface CreateSandboxFileIndexRouteOptions {
94
+ /** Authenticate the caller, resolve the sandbox `fs` handle, and signal a
95
+ * cold box — never provisions or waits. */
96
+ authorize(args: {
97
+ request: Request;
98
+ }): Promise<FileIndexAuthorization>;
99
+ /** Extra ignore segments beyond the route's defaults (node_modules, .git,
100
+ * dotfiles/dot-dirs, common build dirs). Matched as exact path-segment
101
+ * names, same rule as the defaults. */
102
+ ignore?: string[];
103
+ /** Passed to `fs.tree` as `options.maxDepth`. Default 12. */
104
+ maxDepth?: number;
105
+ /** Hard cap on entries returned after filtering. Default 5000. */
106
+ maxEntries?: number;
107
+ /** Optional host-provided cache seam. */
108
+ cache?: FileIndexCache;
109
+ /** Cache TTL in seconds when `cache` is set. Default 20. */
110
+ cacheTtlSeconds?: number;
111
+ }
112
+ declare function createSandboxFileIndexRoute(options: CreateSandboxFileIndexRouteOptions): (request: Request) => Promise<Response>;
113
+
114
+ /**
115
+ * Content-based binary/text classification, shared by the attachment upload
116
+ * route (server) and the composer's client-side pre-validation (browser) —
117
+ * both sides must agree on what counts as binary before a byte ever leaves
118
+ * the client. Extension-based allowlists lie (a renamed `.docx`, a PNG saved
119
+ * as `.txt`), so classification reads the actual bytes: a magic-byte table
120
+ * for common binary formats first, then a UTF-8 decode attempt for
121
+ * everything else.
122
+ *
123
+ * Lifted near-verbatim from gtm-agent's `src/lib/binary-sniff.ts` (the
124
+ * source PRs hardened this against real corruption/gate bugs: gtm#584,
125
+ * gtm#592). Import-free by design — `/web-react` re-exports `/chat-routes`
126
+ * modules into browser bundles (`tests/browser-safe-subpaths.test.ts` walks
127
+ * the graph), so nothing here may reach a Node builtin or an engine package.
128
+ */
129
+ interface SniffResult {
130
+ binary: boolean;
131
+ mime: string | null;
132
+ }
133
+ /** Decide whether uploaded bytes are binary or text, and identify the mime
134
+ * type when it can be determined from content. Magic bytes are checked
135
+ * first; anything unmatched falls back to a fatal UTF-8 decode. A NUL byte
136
+ * or a decode failure means binary. Valid UTF-8 that is an SVG document is
137
+ * binary (byte-identity matters for image tooling). Content that matches
138
+ * nothing and does not decode as text is binary with an unknown mime —
139
+ * extension-based guessing happens at the call site, not here. */
140
+ declare function sniffBinary(bytes: Uint8Array): SniffResult;
141
+
142
+ /**
143
+ * Shared attachment validation core — constants, type-gate, and filename
144
+ * sanitization used by BOTH the (server) attachment upload route and the
145
+ * (browser) composer's client-side pre-validation, so a rejection never
146
+ * differs depending on which side classified the bytes first.
147
+ *
148
+ * ≈ gtm-agent's `src/lib/attachment-limits.ts`, minus what agent-app already
149
+ * has (`ATTACHMENT_MAX_COUNT`/`MAX_ATTACHMENT_TOTAL_BYTES`/
150
+ * `attachmentTotalSizeErrorMessage` lived in `./resolve-attachments` and are
151
+ * re-homed here so the whole validation vocabulary — count cap, size caps,
152
+ * and type gate — has one address). Import-free besides `./wire`
153
+ * (`formatBytes`) and `./binary-sniff` (`SniffResult`): `/web-react`
154
+ * re-exports `/chat-routes` modules into browser bundles
155
+ * (`tests/browser-safe-subpaths.test.ts` walks the graph), so nothing here
156
+ * may reach a Node builtin or an engine package.
157
+ */
158
+
159
+ /** Ceiling on a binary attachment's raw (pre-encoding) byte size. */
160
+ declare const MAX_BINARY_ATTACHMENT_BYTES: number;
161
+ /** Ceiling on a text attachment's raw byte size. Text hydrates through
162
+ * inline prompt parts, a separate path that remains proxy-capped (see
163
+ * `INLINE_PARTS_MAX_BYTES` in `./wire`). */
164
+ declare const MAX_TEXT_ATTACHMENT_BYTES: number;
165
+ /** Most files a single request may carry: the composer staging cap, the
166
+ * upload route's per-request cap, and the chat body's `attachments` cap. */
167
+ declare const ATTACHMENT_MAX_COUNT = 10;
168
+ /** Aggregate raw-byte ceiling across one message's attachments. */
169
+ declare const MAX_ATTACHMENT_TOTAL_BYTES: number;
170
+ /**
171
+ * Accept list for the composer file picker + type validation, same grammar as
172
+ * the native `<input accept>` attribute. Images plus the text/doc types a
173
+ * product's store actually reads.
174
+ */
175
+ declare const ATTACHMENT_ACCEPT = "image/*,.pdf,.txt,.md,.csv,.json,.yaml,.yml,.html";
176
+ /** Sniffed-mime counterpart of `ATTACHMENT_ACCEPT`: the binary formats
177
+ * `sniffBinary` can identify from magic bytes among the accepted types.
178
+ * Values must match `sniffBinary`'s output strings verbatim, or every
179
+ * upload of that format fails the type gate. */
180
+ declare const ALLOWED_ATTACHMENT_SNIFFED_MIMES: ReadonlySet<string>;
181
+ type AttachmentTypeCheckResult = {
182
+ succeeded: true;
183
+ } | {
184
+ succeeded: false;
185
+ code: 'attachment_type_mismatch' | 'attachment_type_not_allowed';
186
+ message: string;
187
+ };
188
+ /**
189
+ * Cross-check a filename's extension against its sniffed content.
190
+ *
191
+ * Text content (`sniff.binary === false`) always passes here — it has no
192
+ * magic bytes to compare, so it rides the existing UTF-8 gate instead. For
193
+ * binary content: an extension with an unambiguous magic-byte family (e.g.
194
+ * `.pdf`) must match the sniffed mime, or the upload is a mismatch (a
195
+ * renamed file). Otherwise the sniffed mime must be one of `allowed`
196
+ * (default {@link ALLOWED_ATTACHMENT_SNIFFED_MIMES}), or the upload is
197
+ * rejected outright. The `allowed` param feeds a route's override seam (a
198
+ * product accepting a narrower or wider set than the default).
199
+ */
200
+ declare function checkAttachmentType(fileName: string, sniff: SniffResult, allowed?: ReadonlySet<string>): AttachmentTypeCheckResult;
201
+ /**
202
+ * Rewrite a filename into the store-path charset (`A-Za-z0-9._-` per
203
+ * segment) — attachment paths double as store keys, sandbox file paths, and
204
+ * in-message path references, none of which tolerate spaces or punctuation.
205
+ * Runs of unsupported characters collapse to one `-`; leading dots/dashes are
206
+ * stripped so the name can't read as a hidden segment. The original name is
207
+ * preserved separately (the returned `ChatAttachmentInput.name`), so
208
+ * sanitization loses nothing.
209
+ */
210
+ declare function sanitizeAttachmentFileName(name: string): string;
211
+ /** Human-readable error naming both the actual size and the limit that was
212
+ * exceeded. Shared so the server route and the composer pre-check report
213
+ * the same message shape. */
214
+ declare function attachmentSizeErrorMessage(name: string, actualBytes: number, limitBytes: number): string;
215
+ /** Human-readable error for a chat message whose combined attachments exceed
216
+ * the aggregate raw-byte ceiling. */
217
+ declare function attachmentTotalSizeErrorMessage(totalBytes: number, limitBytes: number): string;
218
+
219
+ export { ALLOWED_ATTACHMENT_SNIFFED_MIMES as A, type CreateSandboxFileIndexRouteOptions as C, type FileIndexAuthorization as F, MAX_ATTACHMENT_TOTAL_BYTES as M, type SandboxFileTreeSource as S, ATTACHMENT_ACCEPT as a, ATTACHMENT_MAX_COUNT as b, type AttachmentTypeCheckResult as c, type FileIndexCache as d, type FileIndexReadyResponse as e, type FileIndexResponse as f, type FileIndexWarmingResponse as g, MAX_BINARY_ATTACHMENT_BYTES as h, MAX_TEXT_ATTACHMENT_BYTES as i, type SandboxTreeFile as j, type SandboxTreeResult as k, type SniffResult as l, attachmentSizeErrorMessage as m, attachmentTotalSizeErrorMessage as n, checkAttachmentType as o, createSandboxFileIndexRoute as p, sniffBinary as q, sanitizeAttachmentFileName as s };
@@ -1,12 +1,12 @@
1
- import { L as ChatTurnRequestPayload, M as ChatTurnPartInput, g as ChatMessagePart, N as ChatTurnFilePartInput, a as ChatAttachmentPart, f as ChatMentionPart, C as ChatAttachmentKind } from '../parts-IB-Kbb7z.js';
1
+ import { L as ChatTurnRequestPayload, M as ChatTurnPartInput, g as ChatMessagePart, N as ChatTurnFilePartInput, a as ChatAttachmentPart, C as ChatAttachmentKind, f as ChatMentionPart } from '../parts-IB-Kbb7z.js';
2
2
  export { O as ChatAttachmentInput, e as ChatMentionKind, P as ChatTurnInputError, Q as ChatTurnTextPartInput, R as DISPATCH_MAX_MEDIA_PARTS, T as DISPATCH_MAX_PARTS, U as DISPATCH_REQUEST_MAX_BYTES, V as DISPATCH_STRUCTURAL_RESERVE_BYTES, K as FileMention, W as FileMentionsToPartsOptions, X as INLINE_PARTS_MAX_BYTES, Y as MENTION_MAX_COUNT, Z as SandboxMentionPathCheck, _ as assertPromptPartsWithinCap, $ as base64WireLen, a0 as buildMentionPromptBlock, a1 as chatTurnRequestInit, a2 as fileMentionsToParts, a3 as formatBytes, a4 as mediaTypeForMentionPath, a5 as mentionKindForPath, a6 as parseChatTurnParts, a7 as parseFileMentions, a8 as promptPartsByteSize, a9 as validateSandboxMentionPath } from '../parts-IB-Kbb7z.js';
3
+ export { A as ALLOWED_ATTACHMENT_SNIFFED_MIMES, a as ATTACHMENT_ACCEPT, b as ATTACHMENT_MAX_COUNT, c as AttachmentTypeCheckResult, C as CreateSandboxFileIndexRouteOptions, F as FileIndexAuthorization, d as FileIndexCache, e as FileIndexReadyResponse, f as FileIndexResponse, g as FileIndexWarmingResponse, M as MAX_ATTACHMENT_TOTAL_BYTES, h as MAX_BINARY_ATTACHMENT_BYTES, i as MAX_TEXT_ATTACHMENT_BYTES, S as SandboxFileTreeSource, j as SandboxTreeFile, k as SandboxTreeResult, l as SniffResult, m as attachmentSizeErrorMessage, n as attachmentTotalSizeErrorMessage, o as checkAttachmentType, p as createSandboxFileIndexRoute, s as sanitizeAttachmentFileName, q as sniffBinary } from '../attachment-validation-DX2KIzMC.js';
3
4
  import { ChatTurnIdentity, ChatTurnProducer } from '@tangle-network/agent-runtime';
4
5
  import { InteractionAnswerRoute, InteractionAnswerRouteOptions } from '../interactions/index.js';
5
6
  import { PersistedChatMessageForTurn } from '../stream/index.js';
6
7
  import { d as TurnEventStore } from '../turn-buffer-C9mEgoop.js';
7
8
  export { D as DEFAULT_STALE_TURN_LOCK_GRACE_MS, a as DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS, R as ReconcileStaleTurnLockOptions, b as ReconcileStaleTurnLockResult, S as StaleTurnLockSandboxProbeResult, c as StaleTurnLockSessionProbeResult, r as reconcileStaleTurnLock } from '../stale-turn-lock-C8Na1cFZ.js';
8
9
  import { J as JsonRecord } from '../stream-normalizer-DWvtmY6F.js';
9
- export { C as CreateSandboxFileIndexRouteOptions, F as FileIndexAuthorization, a as FileIndexCache, b as FileIndexReadyResponse, c as FileIndexResponse, d as FileIndexWarmingResponse, S as SandboxFileTreeSource, e as SandboxTreeFile, f as SandboxTreeResult, g as createSandboxFileIndexRoute } from '../file-index-b26ee-_R.js';
10
10
  import { SandboxExecChannel, PromptInputPart } from '../sandbox/index.js';
11
11
  import '@tangle-network/agent-interface';
12
12
  import '../contract-KfqJh_au.js';
@@ -442,13 +442,16 @@ declare function withDurableChatProjection(producer: ChatTurnRouteProducer, proj
442
442
  * The sink is structural (no sandbox-SDK import); products pass `box.fs`.
443
443
  *
444
444
  * @remarks Sole consumer today is the `--chat` scaffold (`create-agent-app
445
- * --chat` → `template-chat/src/chat.ts`), the reference multimodal path. The
446
- * fleet apps (gtm/tax/legal/insurance) each keep their OWN upload route into a
447
- * durable vault (KV, or AES-GCM-encrypted R2) a different persistence model
448
- * from this route's inline-`data:`-or-ephemeral-sandbox-workspace split, so
449
- * they don't (and shouldn't) route through it. This stays the scaffold's proven
450
- * upload pattern, not a fleet primitive; keep that distinction in mind before
451
- * widening its surface.
445
+ * --chat` → `template-chat/src/chat.ts`), the reference multimodal path — its
446
+ * inline-`data:`-or-ephemeral-sandbox-workspace split stays the scaffold's
447
+ * proven upload pattern, not a fleet primitive; keep that distinction in mind
448
+ * before widening its surface. Fleet apps with a durable store of their own
449
+ * (KV, or AES-GCM-encrypted R2) no longer need to hand-roll a vault upload
450
+ * route: `createAttachmentUploadRoute` (`./attachment-upload`, agent-app#234)
451
+ * is the shared hardened path for that persistence model — a content-sniffed
452
+ * type gate, two-phase atomic batch writes, and per-kind/aggregate size caps,
453
+ * all seamed through an injected `WriteAttachmentFn`. Point readers there
454
+ * instead of widening this route to cover both models.
452
455
  */
453
456
 
454
457
  /** 700 KiB: base64 inflates ~4/3, so an inline part stays comfortably under
@@ -622,16 +625,6 @@ type AttachmentPathCheck = {
622
625
  succeeded: false;
623
626
  error: string;
624
627
  };
625
- /** Most files a single request may carry. */
626
- declare const ATTACHMENT_MAX_COUNT = 10;
627
- /** Aggregate raw-byte ceiling across one message's attachments. */
628
- declare const MAX_ATTACHMENT_TOTAL_BYTES: number;
629
- /** Human-readable error for a message whose combined attachments exceed the
630
- * aggregate raw-byte ceiling. Ported to match gtm's `attachmentTotalSizeErrorMessage`
631
- * (attachment-limits.ts:93-95) verbatim, via the shared {@link formatBytes} —
632
- * e.g. "Attachments total 25MB; each message is limited to 25MB", not raw
633
- * byte counts. */
634
- declare function attachmentTotalSizeErrorMessage(totalBytes: number, limitBytes: number): string;
635
628
  /**
636
629
  * Default path validator when a caller supplies none. Rejects the ways a path
637
630
  * picked in a client can escape the store root — traversal (`..` segment),
@@ -669,6 +662,83 @@ interface ResolveChatAttachmentsOptions {
669
662
  */
670
663
  declare function resolveChatAttachments(value: unknown, options: ResolveChatAttachmentsOptions): Promise<ResolveChatAttachmentsResult>;
671
664
 
665
+ /**
666
+ * `createAttachmentUploadRoute` — the fleet-primitive durable-store upload
667
+ * route: a two-phase atomic batch (every file is validated before any file is
668
+ * written — a batch never partially lands), a content-sniffed type gate
669
+ * (`checkAttachmentType` over `sniffBinary`'s magic-byte read, not the
670
+ * extension or the browser-reported MIME), per-kind + aggregate byte caps,
671
+ * and sanitized filenames. Storage is fully seamed through the injected
672
+ * `WriteAttachmentFn` (`./attachment-store`) — no default store, the product
673
+ * owns where bytes actually live (vault, object store, …) — and auth/rate
674
+ * limiting is entirely the injected `authorize` seam's job: this factory
675
+ * never invents a 401 or 429 response, it only returns `auth.response`
676
+ * verbatim on failure.
677
+ *
678
+ * Lifted from gtm-agent's `src/routes/api.vault.upload.ts` (the hardening
679
+ * lineage other lifted modules in this vertical cite: gtm#584 binary
680
+ * corruption, gtm#592 sniff gate/caps, gtm#593 batch-atomic writes) and
681
+ * generalized the way `resolve-attachments.ts` generalized gtm's read path —
682
+ * the vault-specific pieces (KV vault paths, frontmatter, per-user rate
683
+ * limiting) are all injected seams here, while the validate-then-write phase
684
+ * split and the type/size gate ordering survive byte-for-byte.
685
+ *
686
+ * @remarks Sibling to, NOT an extension of, `./upload.ts`'s
687
+ * `createUploadRoute` — a different persistence model (durable product store
688
+ * vs. inline-`data:`-or-ephemeral-sandbox-workspace). See that module's doc
689
+ * comment for the up-to-date framing between the two.
690
+ */
691
+
692
+ /** Outcome of the injected `authorize` seam: auth + rate limiting +
693
+ * scope resolution, all in one place so a 429 rides `{ok:false, response}`
694
+ * exactly like a 401 does — this factory has no rate-limit opinion of its
695
+ * own. `writeAttachment` lets a single request override the option-level
696
+ * store (e.g. routing per-tenant), defaulting to `options.writeAttachment`
697
+ * when absent. */
698
+ type AttachmentUploadAuthorization = {
699
+ ok: true;
700
+ scopeId: string;
701
+ writeAttachment?: WriteAttachmentFn;
702
+ } | {
703
+ ok: false;
704
+ response: Response;
705
+ };
706
+ interface CreateAttachmentUploadRouteOptions {
707
+ /** Authenticate the caller, rate-limit, and resolve the store scope
708
+ * (workspace/tenant id) — never a query param. */
709
+ authorize(args: {
710
+ request: Request;
711
+ }): Promise<AttachmentUploadAuthorization>;
712
+ /** Default store writer. `authorize` may override it per-request. */
713
+ writeAttachment: WriteAttachmentFn;
714
+ /** Overridable caps. Defaults come from `./attachment-validation`. */
715
+ limits?: {
716
+ /** Most files one request may carry. Default {@link ATTACHMENT_MAX_COUNT}. */
717
+ maxCount?: number;
718
+ /** Ceiling on a binary file's raw size. Default {@link MAX_BINARY_ATTACHMENT_BYTES}. */
719
+ maxBinaryBytes?: number;
720
+ /** Ceiling on a text file's raw size. Default {@link MAX_TEXT_ATTACHMENT_BYTES}. */
721
+ maxTextBytes?: number;
722
+ /** Aggregate raw-byte ceiling across the batch. Default {@link MAX_ATTACHMENT_TOTAL_BYTES}. */
723
+ maxTotalBytes?: number;
724
+ };
725
+ /** Attachment kinds this route accepts. Default `['image', 'file']`. */
726
+ allowedKinds?: ChatAttachmentKind[];
727
+ /** Sniffed-mime allowlist fed to `checkAttachmentType`. Default
728
+ * {@link ALLOWED_ATTACHMENT_SNIFFED_MIMES}. */
729
+ allowedSniffedMimes?: ReadonlySet<string>;
730
+ /** Sanitized-name → store path. Default identity (the sanitized name IS
731
+ * the path); gtm passes `vaultFolderForFileName`, a tenant product a
732
+ * scope prefix. */
733
+ pathFor?: (name: string) => string;
734
+ /** Store-path validator. Default {@link defaultValidateAttachmentPath}. */
735
+ validatePath?: (path: string) => AttachmentPathCheck;
736
+ /** Last-resort media-type hook for text content the sniffer can't type.
737
+ * Default {@link sniffMimeFromName}. */
738
+ sniffMime?: (name: string) => string;
739
+ }
740
+ declare function createAttachmentUploadRoute(options: CreateAttachmentUploadRouteOptions): (request: Request) => Promise<Response>;
741
+
672
742
  /**
673
743
  * `buildDispatchParts` — assemble the `PromptInputPart[]` a turn carrying
674
744
  * attachments and/or `@`-mentions dispatches to the sandbox. `parts[0]` is
@@ -835,4 +905,4 @@ interface PromoteAgentFilePartOptions {
835
905
  }
836
906
  declare function promoteAgentFilePart(options: PromoteAgentFilePartOptions): Promise<PromoteFilePartResult>;
837
907
 
838
- export { ATTACHMENT_MAX_COUNT, type AttachmentPathArgs, type AttachmentPathCheck, type AttachmentReadResult, type AttachmentWriteResult, type BuildDispatchPartsInput, ChatAttachmentKind, type ChatRouteDurableProjection, type ChatRouteDurableProjectionLogger, type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, ChatTurnFilePartInput, type ChatTurnGateResult, type ChatTurnHeartbeat, type ChatTurnInputPatch, type ChatTurnLifecycle, type ChatTurnLifecycleComplete, type ChatTurnLifecycleError, type ChatTurnLifecycleStart, type ChatTurnLock, type ChatTurnLockResult, type ChatTurnMessageStore, ChatTurnPartInput, type ChatTurnProduceArgs, ChatTurnRequestPayload, type ChatTurnRouteProducer, type ChatTurnRoutes, type ChatTurnUsage, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, type DispatchPartsOutcome, type FilePartPromotionOutcome, MAX_ATTACHMENT_TOTAL_BYTES, PROMOTE_MAX_FILE_BYTES, type PromoteAgentFilePartOptions, type PromoteFilePartResult, PromptInputPart, type RawAgentFilePart, type ReadAttachmentFn, type ReadSandboxMentionFn, type ResolveChatAttachmentsOptions, type ResolveChatAttachmentsResult, type SandboxChatProducerOptions, type SandboxUploadSink, UPLOAD_INLINE_MAX_BYTES, UPLOAD_MAX_FILE_BYTES, type UploadAuthorization, type UploadedChatFile, type WriteAttachmentFn, attachmentTotalSizeErrorMessage, buildDispatchParts, bytesToBase64, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, defaultValidateAttachmentPath, promoteAgentFilePart, resolveChatAttachments, sanitizeUploadFilename, sniffMimeFromName, withDurableChatProjection };
908
+ export { type AttachmentPathArgs, type AttachmentPathCheck, type AttachmentReadResult, type AttachmentUploadAuthorization, type AttachmentWriteResult, type BuildDispatchPartsInput, ChatAttachmentKind, type ChatRouteDurableProjection, type ChatRouteDurableProjectionLogger, type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, ChatTurnFilePartInput, type ChatTurnGateResult, type ChatTurnHeartbeat, type ChatTurnInputPatch, type ChatTurnLifecycle, type ChatTurnLifecycleComplete, type ChatTurnLifecycleError, type ChatTurnLifecycleStart, type ChatTurnLock, type ChatTurnLockResult, type ChatTurnMessageStore, ChatTurnPartInput, type ChatTurnProduceArgs, ChatTurnRequestPayload, type ChatTurnRouteProducer, type ChatTurnRoutes, type ChatTurnUsage, type CreateAttachmentUploadRouteOptions, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, type DispatchPartsOutcome, type FilePartPromotionOutcome, PROMOTE_MAX_FILE_BYTES, type PromoteAgentFilePartOptions, type PromoteFilePartResult, PromptInputPart, type RawAgentFilePart, type ReadAttachmentFn, type ReadSandboxMentionFn, type ResolveChatAttachmentsOptions, type ResolveChatAttachmentsResult, type SandboxChatProducerOptions, type SandboxUploadSink, UPLOAD_INLINE_MAX_BYTES, UPLOAD_MAX_FILE_BYTES, type UploadAuthorization, type UploadedChatFile, type WriteAttachmentFn, buildDispatchParts, bytesToBase64, createAttachmentUploadRoute, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, defaultValidateAttachmentPath, promoteAgentFilePart, resolveChatAttachments, sanitizeUploadFilename, sniffMimeFromName, withDurableChatProjection };