@jini-ai/daemon 0.3.8 → 0.5.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.
Files changed (173) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +50 -8
  3. package/dist/agent-executor/contracts.d.ts +736 -0
  4. package/dist/agent-executor/contracts.js +28 -0
  5. package/dist/agent-executor/event-translation.d.ts +87 -0
  6. package/dist/agent-executor/event-translation.js +230 -0
  7. package/dist/agent-executor/events.d.ts +190 -0
  8. package/dist/agent-executor/events.js +848 -0
  9. package/dist/agent-executor/handoff.d.ts +18 -0
  10. package/dist/agent-executor/handoff.js +16 -0
  11. package/dist/agent-executor/index.d.ts +98 -0
  12. package/dist/agent-executor/index.js +98 -0
  13. package/dist/agent-executor/launch.d.ts +375 -0
  14. package/dist/agent-executor/launch.js +1001 -0
  15. package/dist/agent-executor/mcp-bridge.d.ts +338 -0
  16. package/dist/agent-executor/mcp-bridge.js +835 -0
  17. package/dist/agent-executor/values.d.ts +5 -0
  18. package/dist/agent-executor/values.js +13 -0
  19. package/dist/agent-executor.d.ts +375 -176
  20. package/dist/agent-executor.js +388 -335
  21. package/dist/artifacts/index.d.ts +14 -0
  22. package/dist/artifacts/index.js +14 -0
  23. package/dist/artifacts/manifest.d.ts +90 -0
  24. package/dist/artifacts/manifest.js +229 -0
  25. package/dist/artifacts/publication-guard.d.ts +28 -0
  26. package/dist/artifacts/publication-guard.js +77 -0
  27. package/dist/artifacts/runtime-compat.d.ts +37 -0
  28. package/dist/artifacts/runtime-compat.js +33 -0
  29. package/dist/artifacts/store.d.ts +85 -0
  30. package/dist/artifacts/store.js +92 -0
  31. package/dist/artifacts/stub-guard.d.ts +73 -0
  32. package/dist/artifacts/stub-guard.js +198 -0
  33. package/dist/artifacts/text-suppression.d.ts +68 -0
  34. package/dist/artifacts/text-suppression.js +184 -0
  35. package/dist/close-status.d.ts +11 -8
  36. package/dist/close-status.js +31 -19
  37. package/dist/continuation/continuation-transport.d.ts +3 -1
  38. package/dist/continuation/continuation-transport.js +1 -1
  39. package/dist/continuation/journal.d.ts +12 -5
  40. package/dist/continuation/journal.js +4 -4
  41. package/dist/continuation/run-scoped-context-store.d.ts +13 -4
  42. package/dist/continuation/run-scoped-context-store.js +6 -6
  43. package/dist/continuation/run-start-handler.d.ts +1 -1
  44. package/dist/continuation/run-start-handler.js +3 -16
  45. package/dist/delegated-tool-bridge.d.ts +7 -3
  46. package/dist/delegated-tool-bridge.js +32 -31
  47. package/dist/event-log.d.ts +7 -5
  48. package/dist/event-log.js +12 -11
  49. package/dist/frontend-capability-tools.d.ts +1 -1
  50. package/dist/frontend-capability-tools.js +3 -2
  51. package/dist/frontend-session-registry.d.ts +32 -9
  52. package/dist/frontend-session-registry.js +12 -11
  53. package/dist/http/active-context.d.ts +105 -0
  54. package/dist/http/active-context.js +67 -0
  55. package/dist/http/agents.d.ts +68 -0
  56. package/dist/http/agents.js +12 -0
  57. package/dist/http/attachments.d.ts +618 -0
  58. package/dist/http/attachments.js +1034 -0
  59. package/dist/http/cancel-owned-runs.d.ts +49 -0
  60. package/dist/http/cancel-owned-runs.js +35 -0
  61. package/dist/http/component-catalog.d.ts +84 -0
  62. package/dist/http/component-catalog.js +64 -0
  63. package/dist/http/connectors.d.ts +209 -0
  64. package/dist/http/connectors.js +409 -0
  65. package/dist/http/daemon-status.d.ts +76 -0
  66. package/dist/http/daemon-status.js +55 -0
  67. package/dist/http/db-ops.d.ts +123 -0
  68. package/dist/http/db-ops.js +189 -0
  69. package/dist/http/delegated-tools.d.ts +113 -0
  70. package/dist/http/delegated-tools.js +230 -0
  71. package/dist/http/frontend-control.d.ts +136 -0
  72. package/dist/http/frontend-control.js +79 -0
  73. package/dist/http/frontend-sessions.d.ts +110 -0
  74. package/dist/http/frontend-sessions.js +180 -0
  75. package/dist/http/health.d.ts +95 -0
  76. package/dist/http/health.js +40 -0
  77. package/dist/http/host-tools.d.ts +214 -0
  78. package/dist/http/host-tools.js +329 -0
  79. package/dist/http/index.d.ts +52 -0
  80. package/dist/http/index.js +27 -0
  81. package/dist/http/media.d.ts +146 -0
  82. package/dist/http/media.js +346 -0
  83. package/dist/http/memory.d.ts +265 -0
  84. package/dist/http/memory.js +328 -0
  85. package/dist/http/model-proxy.d.ts +49 -0
  86. package/dist/http/model-proxy.js +642 -0
  87. package/dist/http/remote-run-events.d.ts +70 -0
  88. package/dist/http/remote-run-events.js +224 -0
  89. package/dist/http/research.d.ts +48 -0
  90. package/dist/http/research.js +315 -0
  91. package/dist/http/route-manifest.d.ts +35 -0
  92. package/dist/http/route-manifest.js +103 -0
  93. package/dist/http/routines.d.ts +120 -0
  94. package/dist/http/routines.js +298 -0
  95. package/dist/http/run-credentials.d.ts +73 -0
  96. package/dist/http/run-credentials.js +71 -0
  97. package/dist/http/runs.d.ts +91 -0
  98. package/dist/http/runs.js +221 -0
  99. package/dist/http/terminals.d.ts +138 -0
  100. package/dist/http/terminals.js +325 -0
  101. package/dist/http/tool-catalog.d.ts +78 -0
  102. package/dist/http/tool-catalog.js +66 -0
  103. package/dist/http/workspace-root.d.ts +77 -0
  104. package/dist/http/workspace-root.js +63 -0
  105. package/dist/http/xai.d.ts +90 -0
  106. package/dist/http/xai.js +655 -0
  107. package/dist/image-prompt-delivery.d.ts +13 -3
  108. package/dist/image-prompt-delivery.js +5 -5
  109. package/dist/index.d.ts +556 -4
  110. package/dist/index.js +556 -4
  111. package/dist/legacy-data-migration.d.ts +59 -8
  112. package/dist/legacy-data-migration.js +38 -33
  113. package/dist/read-only-tools.d.ts +130 -0
  114. package/dist/read-only-tools.js +115 -0
  115. package/dist/remote-tool-bridge.d.ts +11 -5
  116. package/dist/remote-tool-bridge.js +15 -15
  117. package/dist/routines/index.d.ts +1 -1
  118. package/dist/routines/index.js +1 -1
  119. package/dist/routines/routine-store.d.ts +34 -12
  120. package/dist/routines/routine-store.js +21 -20
  121. package/dist/routines/schedule.d.ts +48 -10
  122. package/dist/routines/schedule.js +21 -21
  123. package/dist/routines/scheduler.d.ts +32 -11
  124. package/dist/routines/scheduler.js +61 -57
  125. package/dist/routines/types.d.ts +10 -3
  126. package/dist/run/core/result.d.ts +6 -2
  127. package/dist/run/core/result.js +3 -3
  128. package/dist/run/core/retry.d.ts +17 -4
  129. package/dist/run/core/retry.js +7 -11
  130. package/dist/run/diagnostics/diagnostics.d.ts +14 -4
  131. package/dist/run/diagnostics/diagnostics.js +7 -7
  132. package/dist/run/index.d.ts +1 -1
  133. package/dist/run/index.js +1 -1
  134. package/dist/run-credentials.d.ts +138 -0
  135. package/dist/run-credentials.js +152 -0
  136. package/dist/run-lifecycle.d.ts +92 -33
  137. package/dist/run-lifecycle.js +81 -74
  138. package/dist/scheduler.d.ts +8 -0
  139. package/dist/scheduler.js +2 -0
  140. package/dist/session-coordination.d.ts +115 -0
  141. package/dist/session-coordination.js +111 -0
  142. package/dist/store/agent-sessions/errors.d.ts +8 -0
  143. package/dist/store/agent-sessions/errors.js +10 -0
  144. package/dist/store/agent-sessions/index.d.ts +12 -0
  145. package/dist/store/agent-sessions/index.js +19 -0
  146. package/dist/store/agent-sessions/pglite.d.ts +12 -0
  147. package/dist/store/agent-sessions/pglite.js +7 -0
  148. package/dist/store/agent-sessions/ports.d.ts +24 -0
  149. package/dist/store/agent-sessions/ports.js +2 -0
  150. package/dist/store/agent-sessions/postgres.d.ts +12 -0
  151. package/dist/store/agent-sessions/postgres.js +7 -0
  152. package/dist/store/agent-sessions/schema.d.ts +3 -0
  153. package/dist/store/agent-sessions/schema.js +23 -0
  154. package/dist/store/agent-sessions/sql.d.ts +35 -0
  155. package/dist/store/agent-sessions/sql.js +59 -0
  156. package/dist/store/agent-sessions/sqlite.d.ts +117 -0
  157. package/dist/store/agent-sessions/sqlite.js +128 -0
  158. package/dist/store/event-log/sqlite.d.ts +75 -0
  159. package/dist/store/event-log/sqlite.js +230 -0
  160. package/dist/surface-exchanges.d.ts +97 -0
  161. package/dist/surface-exchanges.js +153 -0
  162. package/dist/terminal-session.d.ts +43 -11
  163. package/dist/terminal-session.js +22 -20
  164. package/dist/tokens.js +4 -4
  165. package/dist/tool-audit.d.ts +114 -0
  166. package/dist/tool-audit.js +95 -0
  167. package/dist/tool-executor.d.ts +22 -7
  168. package/dist/tool-executor.js +16 -16
  169. package/dist/tool-result-media.d.ts +3 -1
  170. package/dist/tool-result-media.js +1 -1
  171. package/dist/tool-result-surfaces.d.ts +3 -1
  172. package/dist/tool-result-surfaces.js +1 -1
  173. package/package.json +127 -9
@@ -0,0 +1,618 @@
1
+ import type { Express, Request, Response } from 'express';
2
+ import type { AdapterContext } from '@jini-ai/http-kit';
3
+ /**
4
+ * A staged upload as it crosses the wire and as `claim()` returns it.
5
+ *
6
+ * Deliberately declared here rather than imported from `@jini-ai/chat/core`, whose `ChatAttachment`
7
+ * this mirrors field-for-field: the same reason `media.ts` declares its own `MediaTask`/
8
+ * `MediaDispatchEngine` port types instead of depending on `@jini-ai/media`. A transport package
9
+ * should not acquire a dependency on a *domain* package (`jini.domain: "chat"`) to describe an
10
+ * upload that has nothing chat-specific about it. `attachments.test.ts` holds a compile-time
11
+ * assignability check in both directions against the real `ChatAttachment`, so the mirror cannot
12
+ * silently drift — the drift risk is paid for with a test rather than with a dependency edge.
13
+ *
14
+ * `path` means two different things at two different times, on purpose:
15
+ * - as returned by `register()` / over the wire: an opaque `attachment:<uuid>` capability id;
16
+ * - as returned by `claim()`: the real absolute filesystem path, server-side only.
17
+ */
18
+ export interface StoredAttachment {
19
+ path: string;
20
+ name: string;
21
+ kind: 'image' | 'file';
22
+ size?: number;
23
+ /**
24
+ * User-visible ordering for the turn that carries this attachment. Present for exact structural
25
+ * parity with `ChatAttachment`; this store never sets it, because display ordering is the
26
+ * renderer's concern and nothing here would be able to reconstruct it.
27
+ */
28
+ order?: number;
29
+ }
30
+ /** What `claim()` hands back: trusted paths plus the one directory an agent may be granted. */
31
+ export interface AttachmentClaim {
32
+ attachments: StoredAttachment[];
33
+ /** Absent only for an empty claim — there is no directory to grant when nothing was claimed. */
34
+ batchDirectory?: string;
35
+ }
36
+ /** Why an attachment operation was refused. Maps to an HTTP status in `statusForRejection`. */
37
+ export type AttachmentRejectionReason =
38
+ /** The batch id is missing or not of the accepted shape. */
39
+ 'invalid-batch'
40
+ /** One file exceeded the per-attachment byte cap. */
41
+ | 'attachment-too-large'
42
+ /** The batch already holds `maxAttachments` files. */
43
+ | 'batch-count-exceeded'
44
+ /** This file would push the batch past `maxBatchBytes`. */
45
+ | 'batch-too-large'
46
+ /** The store as a whole is at `maxStoredAttachments` / `maxStoredBytes`. */
47
+ | 'storage-full'
48
+ /** Nothing was uploaded — a zero-byte body. */
49
+ | 'empty-attachment'
50
+ /** More concurrent uploads than `maxConcurrentUploads`. */
51
+ | 'too-many-concurrent-uploads'
52
+ /** The cleanup request body was not a `{ batchId, paths }` of the accepted shape. */
53
+ | 'invalid-cleanup-request'
54
+ /**
55
+ * The request stream was already drained before this route saw it — a host body-parser ordering
56
+ * problem, not anything the caller did. See this module's doc.
57
+ */
58
+ | 'attachment-body-consumed'
59
+ /**
60
+ * The stored file is not the canonical regular file it was registered as. Reported opaquely over
61
+ * HTTP: this means either a bug or an active attempt to redirect a claim.
62
+ */
63
+ | 'attachment-integrity'
64
+ /** More attachments in one claim than `maxAttachments` allows. */
65
+ | 'too-many-attachments'
66
+ /** The same capability id appeared twice in one claim. */
67
+ | 'duplicate-attachment'
68
+ /** No such capability id, or a run already claimed it. */
69
+ | 'attachment-unknown-or-claimed'
70
+ /** One claim spanned more than one batch. */
71
+ | 'mixed-batch';
72
+ /**
73
+ * A refusal this module classified itself, as opposed to an unexpected filesystem/programming
74
+ * error. The route pack turns the `reason` into a status code and lets the `message` through only
75
+ * for caller-safe rejections below 500. Classified server-side failures and unexpected errors are
76
+ * redacted to a generic `INTERNAL_ERROR`, preserving the server-state disclosure boundary.
77
+ */
78
+ export declare class AttachmentRejectedError extends Error {
79
+ readonly reason: AttachmentRejectionReason;
80
+ constructor({ reason, message }: {
81
+ readonly reason: AttachmentRejectionReason;
82
+ readonly message: string;
83
+ }, _optional?: Record<string, never>);
84
+ }
85
+ /**
86
+ * Registers, validates, and expires uploads staged for a run. `createDiskAttachmentStore` is the
87
+ * implementation this package ships; a host with its own storage (object store, tmpfs, a quota
88
+ * system of its own) can satisfy this port instead and keep the route pack.
89
+ */
90
+ export interface AttachmentStore {
91
+ /** Creates (idempotently) the private directory that holds one batch's files. */
92
+ createBatchDirectory: ({ batchId }: {
93
+ readonly batchId: string;
94
+ }, _optional?: Record<string, never>) => Promise<string>;
95
+ /**
96
+ * Takes ownership of an already-written file and returns its opaque capability record. Rejects —
97
+ * and deletes the file — if it is not a canonical regular file directly inside its batch
98
+ * directory, or if any quota would be exceeded.
99
+ */
100
+ register: (requiredArgs: {
101
+ readonly input: {
102
+ batchId: string;
103
+ path: string;
104
+ name: string;
105
+ kind: StoredAttachment['kind'];
106
+ size: number;
107
+ /** See `AttachmentRecord.ownerId`'s own doc. Omitted by a host with no owner concept. */
108
+ ownerId?: string;
109
+ };
110
+ }, optionalArgs?: Record<string, never>) => Promise<StoredAttachment>;
111
+ /** Exchanges capability ids for real paths, exactly once, binding them to `runId`. */
112
+ claim: ({ attachments, runId }: {
113
+ readonly attachments: readonly StoredAttachment[];
114
+ readonly runId: string;
115
+ }, _optional?: Record<string, never>) => Promise<AttachmentClaim>;
116
+ /**
117
+ * Resolves ONE still-known attachment to its real path for `runId`, without the "exactly once
118
+ * across the whole batch" shape `claim()` has — the read-side counterpart for a caller that wants
119
+ * to look up a single attachment it may or may not already own, rather than atomically reserving a
120
+ * batch at run start.
121
+ *
122
+ * `ref` accepts either identifier a caller may actually be holding: the opaque `attachment:<uuid>`
123
+ * capability id `register()` returned, or the real absolute path this run was already told about.
124
+ * Both resolve to the same record. This dual form exists because `@jini-ai/daemon`'s
125
+ * `image-prompt-delivery.ts` narrates the resolved PATH into the run's prompt text for an
126
+ * already-claimed attachment — never the id — so a caller built from that prompt has the path, not
127
+ * the id; a caller with the id (from the original upload response) can still use it directly.
128
+ *
129
+ * Ownership is enforced here, which is new: an attachment nobody has claimed yet is claimed for
130
+ * `runId` on this call (identical effect to `claim()`, same integrity re-check); an attachment
131
+ * already claimed BY `runId` is simply re-verified and returned again (idempotent — a run may look
132
+ * this up more than once); an attachment claimed by any OTHER run throws the same
133
+ * `'attachment-unknown-or-claimed'` rejection `claim()` uses for a genuinely unknown id, so a
134
+ * caller cannot distinguish "no such attachment" from "a different run owns this" — the same
135
+ * non-disclosure `claim()` already practices for its own rejections. This is the run-ownership
136
+ * check `claim()`'s own FIRST reservation does not have (see this module's trust-model doc on the
137
+ * deliberate capability-bearer model for a brand-new claim); `resolveForRun` adds it for every
138
+ * lookup made through this method, so a second run/session on this same daemon can never read an
139
+ * attachment already bound to someone else's run by calling this method with a guessed or
140
+ * overheard `ref`.
141
+ *
142
+ * Returns `undefined` only for a `ref` this store has never heard of (never uploaded, or its
143
+ * record was already deleted by `cleanupRun`/`pruneExpired`/`dispose`).
144
+ */
145
+ resolveForRun: ({ ref, runId }: {
146
+ readonly ref: string;
147
+ readonly runId: string;
148
+ }, _optional?: Record<string, never>) => Promise<StoredAttachment | undefined>;
149
+ /**
150
+ * Lists every still-unclaimed attachment registered with `ownerId` — the discovery counterpart to
151
+ * `resolveForRun`'s single-lookup: a caller that does not yet hold a specific ref at all (an
152
+ * attachment uploaded in an earlier turn, never named in this run's prompt) has no id to look up
153
+ * with `resolveForRun` in the first place. This is what makes that attachment findable.
154
+ *
155
+ * **This is the one method in this port that widens the trust model documented at the top of this
156
+ * file.** `register()`/`claim()`/`resolveForRun` all work from a caller-supplied opaque id or path
157
+ * — the "you must already hold the unguessable capability" property that makes an unauthenticated
158
+ * `claim()` first-reservation safe. A listing necessarily hands back ids the caller never held, so
159
+ * it can only be safe if it is scoped to something the caller is actually entitled to — here, the
160
+ * same principal id a host recorded via `register()`'s `ownerId` for the ORIGINAL upload request.
161
+ *
162
+ * Two things make that scoping real rather than decorative:
163
+ * - an attachment registered with NO `ownerId` (a host that never wired `resolveOwnerId`, or any
164
+ * attachment from before this method existed) is excluded from every caller's results, never
165
+ * just "unscoped" — this method is not callable with `ownerId: undefined`, so there is no input
166
+ * that could accidentally match an ownerless record;
167
+ * - a claimed attachment is excluded outright, whether or not this caller's own `runId` claimed
168
+ * it, so this method can never be used to re-discover something already handed to a run — the
169
+ * same "runs, not listings, are the reach here" boundary `resolveForRun` draws for a single ref.
170
+ *
171
+ * What this does NOT scope by: batch, conversation, or run — none of those are recorded on an
172
+ * `AttachmentRecord` today. Two different conversations run by the SAME `ownerId` will each see
173
+ * the other's pending attachments through this method. A host for whom that is too wide needs a
174
+ * finer-grained id than `ownerId` to pass into `register()` — this method does not itself assume
175
+ * `ownerId` means "one admin account" rather than "one conversation, one composer, one browser
176
+ * tab"; it only assumes the host's `ownerId` is something the CALLER of this method is authorized
177
+ * to see everything under.
178
+ *
179
+ * Sorted oldest-first (`createdAt` ascending) — arrival order, matching how a person would expect
180
+ * to review what is waiting.
181
+ *
182
+ * @complexity O(n) in the number of tracked records (bounded by `maxStoredAttachments`), matching
183
+ * every other method on this port.
184
+ */
185
+ listPendingForOwner: ({ ownerId }: {
186
+ readonly ownerId: string;
187
+ }, _optional?: Record<string, never>) => Promise<PendingAttachmentSummary[]>;
188
+ /** Deletes the named still-unclaimed uploads, then the batch directory if it is now empty. */
189
+ deleteUnclaimed: ({ batchId, paths }: {
190
+ readonly batchId: string;
191
+ readonly paths: readonly string[];
192
+ }, _optional?: Record<string, never>) => Promise<void>;
193
+ /** Deletes everything `runId` claimed. Safe to call for a run that claimed nothing. */
194
+ cleanupRun: ({ runId }: {
195
+ readonly runId: string;
196
+ }, _optional?: Record<string, never>) => Promise<void>;
197
+ /** Deletes unclaimed uploads older than the retention window. */
198
+ pruneExpired: (_required: Record<string, never>, optionalArgs?: {
199
+ readonly now?: number | undefined;
200
+ }) => Promise<void>;
201
+ /** Deletes every tracked upload. For host shutdown. */
202
+ dispose: () => Promise<void>;
203
+ }
204
+ export interface CreateDiskAttachmentStoreOptions {
205
+ /** Root directory this store owns outright — it is emptied on construction. */
206
+ readonly uploadDirectory: string;
207
+ /** Files per batch, i.e. per composer turn. Also caps one `claim()`. Defaults to 10. */
208
+ readonly maxAttachments?: number;
209
+ /** Total bytes per batch. Defaults to 50 MB. */
210
+ readonly maxBatchBytes?: number;
211
+ /** Tracked files across all batches. Defaults to 100. */
212
+ readonly maxStoredAttachments?: number;
213
+ /** Tracked bytes across all batches. Defaults to 200 MB. */
214
+ readonly maxStoredBytes?: number;
215
+ /** How long an unclaimed upload survives `pruneExpired`. Defaults to one hour. */
216
+ readonly retentionMs?: number;
217
+ /**
218
+ * Adopts the uploads a previous process left behind instead of emptying the directory on
219
+ * construction, so an attachment staged seconds before a restart is still there — and still
220
+ * listable — afterwards.
221
+ *
222
+ * **Defaults to `false`, which is exactly this pack's pre-existing behavior.** Opting in is a
223
+ * one-word change at the call site and reverting is deleting it; nothing about a store
224
+ * constructed without this option differs in any way from before the option existed.
225
+ *
226
+ * Adoption is authenticated, not blind: see {@link loadPersistedAttachments}.
227
+ */
228
+ readonly retainAcrossRestarts?: boolean;
229
+ }
230
+ /**
231
+ * `true` when `body` opens with an ISO-BMFF `ftyp` box whose major brand or compatible-brand list
232
+ * names AVIF. This is a line-for-line mirror of the CMS package's `content-type-sniffer.ts`
233
+ * `isAvif` (no shared dependency exists between HTTP kit and the CMS package today — see
234
+ * this module's handoff notes on whether extracting one is worth it): the major brand alone is not
235
+ * sufficient, because a great many real AVIF files declare the generic `mif1` (HEIF image) major
236
+ * brand and name `avif` only in the compatible-brand list, so both are checked. HEIC declares
237
+ * neither and is therefore never matched here — deliberate; nothing downstream of this attachment
238
+ * store has confirmed it can render a HEIC as an `<img>`, so labelling one `'image'` would risk the
239
+ * same "sniffed as a type the surface cannot actually display" failure this whole fix addresses.
240
+ *
241
+ * Must run before any generic `ftyp` check: {@link hasIsoBmffFtypTag} alone cannot tell AVIF, HEIC,
242
+ * and MP4 apart, so a brand-blind caller would misclassify every ISO-BMFF file it sees as one type.
243
+ */
244
+ export declare function hasAvifSignature({ body }: {
245
+ readonly body: Uint8Array;
246
+ }, _optional?: Record<string, never>): boolean;
247
+ export interface AttachmentRecord {
248
+ id: string;
249
+ filePath: string;
250
+ name: string;
251
+ kind: StoredAttachment['kind'];
252
+ size: number;
253
+ batchId: string;
254
+ batchDirectory: string;
255
+ dev: number;
256
+ ino: number;
257
+ createdAt: number;
258
+ claimedRunId?: string;
259
+ /**
260
+ * The principal a host's `AttachmentsHttpDeps.resolveOwnerId` reported for the request that
261
+ * registered this attachment. Absent when the host supplies no `resolveOwnerId` (this pack's
262
+ * behavior before ownership existed), which is why `listPendingForOwner` treats an absent
263
+ * `ownerId` as "cannot be scoped" rather than as a wildcard match — see that method's own doc.
264
+ */
265
+ ownerId?: string;
266
+ }
267
+ /**
268
+ * What `listPendingForOwner` hands back for one still-unclaimed attachment: enough for a caller to
269
+ * show a person what is waiting, and to name it again (`ref`) to `claim()`/`resolveForRun`.
270
+ */
271
+ export interface PendingAttachmentSummary {
272
+ /** The same opaque `attachment:<uuid>` id `register()` returned over the wire. */
273
+ ref: string;
274
+ name: string;
275
+ kind: StoredAttachment['kind'];
276
+ size: number;
277
+ /** `Date.now()` at registration — when the file arrived, not when it was looked up. */
278
+ createdAt: number;
279
+ }
280
+ /** What registration recorded about a file, as `isUnchangedAttachment` needs it. */
281
+ export interface RecordedAttachmentIdentity {
282
+ readonly filePath: string;
283
+ readonly dev: number;
284
+ readonly ino: number;
285
+ readonly size: number;
286
+ }
287
+ /**
288
+ * What the filesystem reports about a file right now — the subset of `fs.Stats` (plus `realpath`)
289
+ * an integrity check needs.
290
+ *
291
+ * `isRegularFile` comes from `lstat().isFile()`, which is false for a symlink *and* for a
292
+ * directory. There is deliberately no separate `isSymbolicLink` field: `lstat` reports exactly one
293
+ * file type, so `isFile()` and `isSymbolicLink()` are mutually exclusive and a symlink check after
294
+ * an `isFile()` check could never be the deciding one.
295
+ */
296
+ export interface ObservedAttachmentIdentity {
297
+ readonly isRegularFile: boolean;
298
+ readonly dev: number;
299
+ readonly ino: number;
300
+ readonly size: number;
301
+ /** `realpath` of the file — differs from its own path when any path component became a symlink. */
302
+ readonly canonicalPath: string;
303
+ }
304
+ /**
305
+ * `true` when the file on disk is still the same file registration accepted.
306
+ *
307
+ * Every condition is a distinct real attack: a regular file swapped for a symlink or directory, a
308
+ * parent directory swapped for a symlink pointing elsewhere, the file replaced by a different file
309
+ * at the same path (new inode), the same inode truncated or appended to (new size), or the path
310
+ * now resolving onto a different device.
311
+ *
312
+ * Pure and exported on purpose. Staging a *device* change for a file that keeps its path is not
313
+ * something a test can do on a real filesystem, and a check that can only be exercised in
314
+ * production is a check nobody knows works — a fake identity states each case directly.
315
+ */
316
+ export declare function isUnchangedAttachment({ recorded, observed }: {
317
+ readonly recorded: RecordedAttachmentIdentity;
318
+ readonly observed: ObservedAttachmentIdentity;
319
+ }, _optional?: Record<string, never>): boolean;
320
+ /**
321
+ * Phase 1 of `claim()`: synchronously reserves `attachments` for `runId`, or throws (releasing
322
+ * whatever it already reserved this call) the first time a requested attachment turns out to be
323
+ * unknown or already claimed.
324
+ *
325
+ * The reservation loop deliberately has no `await`: this store's exactly-once guarantee is what
326
+ * stops two runs being handed the same real path on disk, and `claim` is reachable concurrently
327
+ * (two run starts, one shared attachment). Nothing between `records.get` and the assignment of
328
+ * `record.claimedRunId` may ever become asynchronous — that window is exactly where a concurrent
329
+ * call would get its turn and could observe the same record as still unclaimed.
330
+ *
331
+ * Exported so this invariant can be exercised directly against a plain `Map` of fabricated
332
+ * records, without going through a disk-backed store.
333
+ */
334
+ export declare function reserveAttachmentRecords({ attachments, records, runId, maxAttachments }: {
335
+ readonly attachments: readonly StoredAttachment[];
336
+ readonly records: ReadonlyMap<string, AttachmentRecord>;
337
+ readonly runId: string;
338
+ readonly maxAttachments: number;
339
+ }, _optional?: Record<string, never>): AttachmentRecord[];
340
+ /**
341
+ * Phase 2 of `claim()`: re-verifies every already-reserved record against the filesystem — not
342
+ * merely re-read, but re-checked against what registration recorded, so someone able to write into
343
+ * the batch directory between upload and run start cannot get the agent to read a file of their
344
+ * choosing — and checks every claimed attachment shares one batch. Returns the shared batch
345
+ * directory on success (or `''` for an empty `claimed`, which `claim()` never passes in — it
346
+ * returns before calling this — but which a direct caller can still treat as "no batch").
347
+ *
348
+ * Throws without releasing `claimed`'s reservations; releasing is the caller's job (`claim`'s own
349
+ * `catch`), so a rejected claim still leaves nothing half-claimed and the caller can retry with a
350
+ * corrected set.
351
+ *
352
+ * Exported so the integrity and mixed-batch checks can be exercised directly against fabricated
353
+ * records over real files on disk, without a full `createDiskAttachmentStore` around them.
354
+ */
355
+ export declare function verifyClaimedAttachments({ claimed }: {
356
+ readonly claimed: readonly AttachmentRecord[];
357
+ }, _optional?: Record<string, never>): Promise<string>;
358
+ /**
359
+ * Reduces an untrusted upload filename to a display-only basename over a conservative allowlist.
360
+ * Never used as the name of the stored file — see this module's trust-model doc.
361
+ */
362
+ export declare function sanitizeAttachmentName({ requestedName }: {
363
+ readonly requestedName: unknown;
364
+ }, _optional?: Record<string, never>): string;
365
+ /** `true` when `body`'s first 8 bytes are the PNG signature. */
366
+ export declare function hasPngSignature({ body }: {
367
+ readonly body: Uint8Array;
368
+ }, _optional?: Record<string, never>): boolean;
369
+ /** `true` when `body`'s first 3 bytes are the JPEG start-of-image marker. */
370
+ export declare function hasJpegSignature({ body }: {
371
+ readonly body: Uint8Array;
372
+ }, _optional?: Record<string, never>): boolean;
373
+ /** `true` when `body`'s first 6 bytes spell either GIF version tag. */
374
+ export declare function hasGifSignature({ body }: {
375
+ readonly body: Uint8Array;
376
+ }, _optional?: Record<string, never>): boolean;
377
+ /** `true` when `body` opens with a RIFF container whose form type is WEBP. */
378
+ export declare function hasWebpSignature({ body }: {
379
+ readonly body: Uint8Array;
380
+ }, _optional?: Record<string, never>): boolean;
381
+ /**
382
+ * Infers `'image'` from the leading bytes rather than from a renderer-controlled MIME type or file
383
+ * extension. PNG, JPEG, GIF87a/89a, WEBP, and AVIF (brand-checked ISO-BMFF, see
384
+ * {@link hasAvifSignature}) are recognized; everything else is `'file'` — including a plain MP4 or
385
+ * other non-AVIF ISO-BMFF file, which shares AVIF's `ftyp` tag but is deliberately NOT matched here
386
+ * (see {@link hasIsoBmffFtypTag}'s doc for why a brand-blind `ftyp` check would be wrong).
387
+ *
388
+ * `kind` decides whether a path is later passed to `AgentExecutor.run()`'s `imagePaths`, so letting
389
+ * a renderer assert it would let a renderer choose how the agent runtime parses the bytes.
390
+ */
391
+ export declare function detectAttachmentKind({ body }: {
392
+ readonly body: Uint8Array;
393
+ }, _optional?: Record<string, never>): StoredAttachment['kind'];
394
+ /**
395
+ * Streams a request body straight to a private file under a hard byte cap, keeping only the
396
+ * leading signature bytes in memory so an upload never costs memory proportional to its size.
397
+ *
398
+ * Opened `wx`, so this can never overwrite an existing file. A partial write is removed before the
399
+ * rejection propagates: the cap is enforced *during* the stream, which necessarily means some bytes
400
+ * already reached disk by the time it trips.
401
+ */
402
+ export declare function writeBoundedAttachmentBody(requiredArgs: Omit<{
403
+ request: AsyncIterable<unknown>;
404
+ filePath: string;
405
+ maxBytes: number;
406
+ mode?: number;
407
+ }, 'mode'>, optionalArgs?: Pick<{
408
+ request: AsyncIterable<unknown>;
409
+ filePath: string;
410
+ maxBytes: number;
411
+ mode?: number;
412
+ }, 'mode'>): Promise<{
413
+ size: number;
414
+ signature: Uint8Array;
415
+ }>;
416
+ /**
417
+ * One attachment as it is written to its sidecar — `AttachmentRecord` minus two fields, both
418
+ * omitted deliberately:
419
+ *
420
+ * - **`batchDirectory`**, because it is re-derived from the upload root on read rather than trusted.
421
+ * A sidecar is a file on disk; if it could name the directory a claim later hands to an agent, a
422
+ * forged one would turn this store into an arbitrary-file-read primitive. Re-deriving it through
423
+ * the same `BATCH_ID_PATTERN` + `resolve` containment argument `resolveBatchDirectory` uses means
424
+ * a forged sidecar can only ever point inside the upload root.
425
+ * - **`claimedRunId`**, because no run survives the process that owned it. Everything adopted comes
426
+ * back unclaimed; a file whose run died is re-listable to its own uploader (never to anyone else
427
+ * — `claim`/`resolveForRun` still demand the unguessable id or path) and expires on the normal
428
+ * TTL. See {@link loadPersistedAttachments} for the retention consequence that carries.
429
+ */
430
+ export interface PersistedAttachmentRecord {
431
+ readonly id: string;
432
+ readonly filePath: string;
433
+ readonly name: string;
434
+ readonly kind: StoredAttachment['kind'];
435
+ readonly size: number;
436
+ readonly batchId: string;
437
+ readonly dev: number;
438
+ readonly ino: number;
439
+ readonly createdAt: number;
440
+ readonly ownerId?: string;
441
+ }
442
+ /**
443
+ * Turns one sidecar's parsed JSON back into an `AttachmentRecord`, or `undefined` for anything that
444
+ * is not a well-formed record naming a file inside its own batch directory.
445
+ *
446
+ * `batchDirectory` is re-derived here and `filePath`'s containment is re-checked against it with
447
+ * the same parent-equality test `register` uses — the two checks that make an adopted record no
448
+ * more trusted than one this process registered itself. Pure and exported so both can be exercised
449
+ * against hostile input directly, without staging files on disk.
450
+ */
451
+ export declare function parsePersistedAttachment({ raw, canonicalUploadDirectory }: {
452
+ readonly raw: unknown;
453
+ readonly canonicalUploadDirectory: string;
454
+ }, _optional?: Record<string, never>): AttachmentRecord | undefined;
455
+ /**
456
+ * Sidecar filename for a record id. Reduces the id to the same `[a-zA-Z0-9-]` allowlist
457
+ * `BATCH_ID_PATTERN` uses before appending the extension, so no id — however this store's own id
458
+ * format later changes — can put a `/`, a `..`, or a second extension into the path. Ids are
459
+ * `attachment:<uuid>`, so the reduction stays injective in practice: the UUID is what makes it
460
+ * unique and the UUID survives unchanged.
461
+ */
462
+ export declare function attachmentSidecarFileName({ id }: {
463
+ readonly id: string;
464
+ }, _optional?: Record<string, never>): string;
465
+ /**
466
+ * Rebuilds the record map from the sidecar directory, adopting only records that are (a) well
467
+ * formed and contained, (b) still within the retention window, and (c) still backed by the exact
468
+ * file registration accepted. Anything failing any of those is deleted — sidecar first, then the
469
+ * file it named, and the file ONLY when containment passed, so an unparseable sidecar can never
470
+ * make this an arbitrary-file-delete primitive for whatever the daemon can unlink.
471
+ *
472
+ * **The retention consequence, stated rather than assumed harmless.** Because `claimedRunId` is not
473
+ * persisted (see {@link PersistedAttachmentRecord}), a file claimed by a run that died with the
474
+ * process is adopted as unclaimed instead of being wiped, so it now occupies disk and quota for up
475
+ * to `retentionMs` rather than until the next start. Bounded by the same `maxStoredAttachments` /
476
+ * `maxStoredBytes` quotas as any live upload, and cleared by the `pruneExpired` every upload
477
+ * already runs — but it is a real change in when that disk comes back.
478
+ *
479
+ * @complexity O(n) in sidecars present, each costing one read and one `lstat`/`realpath` pair.
480
+ */
481
+ export declare function loadPersistedAttachments(requiredArgs: Omit<{
482
+ readonly canonicalUploadDirectory: string;
483
+ readonly retentionMs: number;
484
+ readonly now?: number;
485
+ }, 'now'>, optionalArgs?: Pick<{
486
+ readonly canonicalUploadDirectory: string;
487
+ readonly retentionMs: number;
488
+ readonly now?: number;
489
+ }, 'now'>): Promise<Map<string, AttachmentRecord>>;
490
+ /**
491
+ * Removes every upload the sidecar pass did not adopt. Without this, a file whose sidecar was lost
492
+ * (or was never written, because it predates this option) would survive forever with nothing
493
+ * tracking it — trading the wipe's data loss for unbounded disk growth, which is not a trade worth
494
+ * making. This is what keeps "never wipe on construction" bounded.
495
+ */
496
+ export declare function removeUnadoptedUploads({ canonicalUploadDirectory, adopted }: {
497
+ readonly canonicalUploadDirectory: string;
498
+ readonly adopted: ReadonlyMap<string, AttachmentRecord>;
499
+ }, _optional?: Record<string, never>): Promise<void>;
500
+ /**
501
+ * Decides what a newly-constructed store starts holding: nothing (the default — empty the directory
502
+ * outright, this pack's behavior before `retainAcrossRestarts` existed), or the authenticated
503
+ * survivors of the previous process.
504
+ *
505
+ * Exported so the whole restart decision can be exercised without standing a store up around it.
506
+ */
507
+ export declare function prepareAttachmentStorage(requiredArgs: Omit<{
508
+ readonly canonicalUploadDirectory: string;
509
+ readonly retainAcrossRestarts: boolean;
510
+ readonly retentionMs: number;
511
+ readonly now?: number;
512
+ }, 'now'>, optionalArgs?: Pick<{
513
+ readonly canonicalUploadDirectory: string;
514
+ readonly retainAcrossRestarts: boolean;
515
+ readonly retentionMs: number;
516
+ readonly now?: number;
517
+ }, 'now'>): Promise<Map<string, AttachmentRecord>>;
518
+ /**
519
+ * The disk-backed `AttachmentStore` this package ships. Every default matches what a chat composer
520
+ * needs out of the box; a host that wants different quotas passes them rather than reimplementing
521
+ * the port.
522
+ *
523
+ * @complexity `register`/`claim` are O(n) in the number of tracked records (a small bounded number
524
+ * — `maxStoredAttachments`), which is what keeps the quota decision synchronous; see `register`.
525
+ */
526
+ export declare function createDiskAttachmentStore(requiredArgs: Omit<CreateDiskAttachmentStoreOptions, 'maxAttachments' | 'maxBatchBytes' | 'maxStoredAttachments' | 'maxStoredBytes' | 'retentionMs' | 'retainAcrossRestarts'>, optionalArgs?: Pick<CreateDiskAttachmentStoreOptions, 'maxAttachments' | 'maxBatchBytes' | 'maxStoredAttachments' | 'maxStoredBytes' | 'retentionMs' | 'retainAcrossRestarts'>): Promise<AttachmentStore>;
527
+ /**
528
+ * Diagnostic detail for an internal-error response the public API deliberately does not disclose
529
+ * (SEC-005), matching `MediaInternalErrorContext`'s precedent.
530
+ */
531
+ export interface AttachmentsInternalErrorContext {
532
+ readonly source: 'attachment-upload' | 'attachment-cleanup';
533
+ readonly batchId: string | null;
534
+ readonly correlationId: string;
535
+ readonly error: unknown;
536
+ }
537
+ export interface AttachmentsHttpDeps {
538
+ readonly store: AttachmentStore;
539
+ /**
540
+ * Uploads accepted at once before further requests are rate-limited. Bounds concurrent
541
+ * filesystem work, which matters because each in-flight upload holds an open handle and may be
542
+ * writing up to `maxAttachmentBytes`. Defaults to 4.
543
+ */
544
+ readonly maxConcurrentUploads?: number;
545
+ /**
546
+ * Hard per-request byte cap, enforced while streaming. Should be at or below the store's
547
+ * `maxBatchBytes`. Defaults to 20 MB.
548
+ */
549
+ readonly maxAttachmentBytes?: number;
550
+ /** Capability ids one `DELETE` may name. Defaults to 10, matching the store's batch cap. */
551
+ readonly maxCleanupPaths?: number;
552
+ /**
553
+ * Rejects a request whose `Origin`/`Host` is not the local daemon's, as every other mutating
554
+ * route in this package does. Defaults to `true`; a host whose browser sits on a *different*
555
+ * local port than the daemon (a dev-server proxy, say) either sets that port via `JINI_WEB_PORT`
556
+ * so the guard recognizes it, or opts out here and accepts that it has no CSRF protection on
557
+ * this route.
558
+ */
559
+ readonly requireSameOrigin?: boolean;
560
+ /** Host-owned sink for the real exception behind a generic `INTERNAL_ERROR` (SEC-005). Defaults to `console.error`. */
561
+ readonly onInternalError?: (context: AttachmentsInternalErrorContext) => void;
562
+ /**
563
+ * Resolves the principal id that owns this upload, if any, passed through to `store.register()`'s
564
+ * `ownerId` (see that field's own doc, and `AttachmentStore.listPendingForOwner`). Optional: a
565
+ * host with no owner concept omits it and every attachment registers ownerless, matching this
566
+ * pack's behavior before ownership existed — `listPendingForOwner` simply never returns those.
567
+ *
568
+ * A host is expected to read this from a header/context IT ALREADY TRUSTS (e.g. a reverse proxy's
569
+ * own session-verified principal header, asserted only downstream of that proxy's auth gate) —
570
+ * this pack has no session concept of its own and never authenticates `req` itself. Returning a
571
+ * caller-controlled value here (an inbound header nothing has verified) would let any uploader
572
+ * assert an arbitrary `ownerId` and make its files discoverable by whoever that id names.
573
+ */
574
+ readonly resolveOwnerId?: ({ req }: {
575
+ req: Request;
576
+ }) => string | undefined;
577
+ }
578
+ export declare const ATTACHMENTS_ROUTE_PATH = "/api/attachments";
579
+ export interface AttachmentUploadResponse {
580
+ readonly attachment: StoredAttachment;
581
+ }
582
+ /**
583
+ * Handles one upload: rate-limit, batch directory, bounded stream to disk, signature sniff,
584
+ * register. Exported so a host mounting its own path (or its own framework) can reuse the whole
585
+ * body without re-deriving the ordering, which is load-bearing — see the `finally`.
586
+ */
587
+ export declare function handleAttachmentUpload({ req, res, deps, state }: {
588
+ readonly req: Request;
589
+ readonly res: Response;
590
+ readonly deps: AttachmentsHttpDeps;
591
+ readonly state: {
592
+ activeUploads: number;
593
+ };
594
+ }, _optional?: Record<string, never>): Promise<void>;
595
+ /**
596
+ * Handles a client abandoning a batch (a failed multi-file upload, a cleared composer). Only
597
+ * *unclaimed* uploads can be deleted this way, so a caller cannot use it to pull files out from
598
+ * under a run that already claimed them.
599
+ */
600
+ export declare function handleAttachmentCleanup({ req, res, deps }: {
601
+ readonly req: Request;
602
+ readonly res: Response;
603
+ readonly deps: AttachmentsHttpDeps;
604
+ }, _optional?: Record<string, never>): Promise<void>;
605
+ /**
606
+ * Mounts `POST`/`DELETE /api/attachments` on `app`. A pack's `http(app, services)` calls this
607
+ * directly.
608
+ *
609
+ * Hand-mounted rather than built from `defineJsonRoute`/`mountJsonRoute` for two concrete reasons:
610
+ * the upload reads the raw request stream (a JSON-parsed `req.body` is exactly what must not have
611
+ * happened), and the cleanup answers `204` with no body, which a JSON responder cannot express.
612
+ */
613
+ export declare function registerAttachmentRoutes({ app, deps, adapter }: {
614
+ readonly app: Express;
615
+ readonly deps: AttachmentsHttpDeps;
616
+ readonly adapter: AdapterContext;
617
+ }, _optional?: Record<string, never>): void;
618
+ //# sourceMappingURL=attachments.d.ts.map