@hydraharness/harness-attachment 0.1.1-rc.10
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/LICENSE +21 -0
- package/README.md +25 -0
- package/lib/index.js +217 -0
- package/lib/invariant.js +17 -0
- package/lib/types/admission.d.ts +22 -0
- package/lib/types/admission.js +46 -0
- package/lib/types/brand.d.ts +11 -0
- package/lib/types/brand.js +10 -0
- package/lib/types/error.d.ts +35 -0
- package/lib/types/error.js +49 -0
- package/lib/types/index.d.ts +77 -0
- package/lib/types/index.js +79 -0
- package/lib/types/invariant.d.ts +13 -0
- package/lib/types/invariant.js +16 -0
- package/lib/types/presentation.d.ts +26 -0
- package/lib/types/presentation.js +58 -0
- package/lib/types/types.d.ts +89 -0
- package/lib/types/types.js +3 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @hydraharness/harness-attachment
|
|
2
|
+
|
|
3
|
+
The durable attachment seam. `ctx.attachments` validates and durably commits immutable image bytes, then returns a serializable `ImageAttachmentRef`; consumers never persist browser paths, object URLs, provider URLs, or base64 in session events.
|
|
4
|
+
|
|
5
|
+
Unsent composer images remain browser-owned temporary drafts. `validateImage` runs the same admission policy without persisting. `saveImages` owns batch count and aggregate-byte limits, validates every member before writing any member, then commits in order and returns references only after the complete batch succeeds. A later storage failure returns no partial references, although an earlier immutable content-addressed object may remain unreachable until reference-aware garbage collection exists. `AttachmentError.code` uses the closed `AttachmentErrorCode` string union. Its `ImageAdmissionErrorCode` subset marks caller-correctable image-input failures; `isImageAdmissionError` recognizes that subset at runtime so each protocol adapter can map its own error vocabulary. `saveImage` commits one accepted image before any model-visible session event is published, and `readImage` verifies the content-addressed object against its logged metadata. Callers may cancel `readImage`; implementations observe cancellation around backend and verification work and preserve it instead of translating it into a storage failure.
|
|
6
|
+
|
|
7
|
+
`admitEncodedImages(attachments, images)` is the shared wire entry used by every RPC endpoint that accepts browser uploads (the session prompt endpoint and the command executor): it enforces canonical base64 on every member, then delegates batch admission — limits, validation, ordered commit — to `saveImages`. The base64 upload form is `EncodedImageAttachment`, exported from `@hydraharness/harness-attachment/types` so wire contracts can reference it.
|
|
8
|
+
|
|
9
|
+
`toolImageReferences(meta)` validates recognized `tool-images` metadata and returns its opaque image references. Unrelated metadata returns an empty list; malformed recognized metadata throws. The [tool-presentation format](../../../docs/subsystems/attachment.md#tool-presentation-images) supports UI images that do not enter model history.
|
|
10
|
+
|
|
11
|
+
`toolVideoReferences(meta)` applies the same policy to `tool-videos`. A `VideoAttachmentRef` extends stored file metadata with `video/mp4` or `video/webm`; producers save final bytes through `saveFile` or `saveFileStream` before publishing the result and own video-format admission. Session reads verify the stored file and authorize its logged reference; ZIP export includes the exact file bytes. `toolMediaLabels(meta)` reads string-valued provider/model labels for display.
|
|
12
|
+
|
|
13
|
+
## Model Experience
|
|
14
|
+
|
|
15
|
+
Indirectly, through the role-neutral core `ImageBlock` and provider adapters that resolve its durable reference.
|
|
16
|
+
|
|
17
|
+
#### KV Cache effect
|
|
18
|
+
|
|
19
|
+
Adding an image changes the provider request and therefore invalidates the affected request suffix.
|
|
20
|
+
|
|
21
|
+
## Known Limitations and Deferred Work
|
|
22
|
+
|
|
23
|
+
- Version one accepts PNG, JPEG, WebP, and GIF only.
|
|
24
|
+
- Retention and garbage collection are deferred because resumed and forked sessions may share immutable objects.
|
|
25
|
+
- Generic files, audio, video, and persistent unsent drafts require separate lifecycle and provider contracts.
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { Service } from "@hydraharness/cordis";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
//#region lib/types/error.js
|
|
4
|
+
/** Runtime membership for structurally compatible errors crossing package boundaries. */
|
|
5
|
+
const IMAGE_ADMISSION_ERROR_CODE_SET = new Set([
|
|
6
|
+
"TOO_MANY_IMAGES",
|
|
7
|
+
"IMAGES_TOO_LARGE",
|
|
8
|
+
"UNSUPPORTED_IMAGE_TYPE",
|
|
9
|
+
"INVALID_IMAGE_BASE64",
|
|
10
|
+
"INVALID_IMAGE",
|
|
11
|
+
"IMAGE_TYPE_MISMATCH",
|
|
12
|
+
"IMAGE_TOO_LARGE",
|
|
13
|
+
"IMAGE_TOO_MANY_PIXELS",
|
|
14
|
+
"IMAGE_DIMENSION_TOO_LARGE"
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* Stable failures suitable for host RPC error mapping.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately re-implements the `HarnessError` shape instead of extending it:
|
|
20
|
+
* the base lives in `@hydraharness/harness-llm`, which itself depends on this
|
|
21
|
+
* package (`ImageBlock` references `ImageAttachmentRef`), so sharing the base
|
|
22
|
+
* would create a dependency cycle. Consumers route on `code`, never on the
|
|
23
|
+
* prototype chain, so the shapes stay interchangeable at the wire boundary.
|
|
24
|
+
*/
|
|
25
|
+
var AttachmentError = class extends Error {
|
|
26
|
+
/** Stable machine-routing failure code. */
|
|
27
|
+
code;
|
|
28
|
+
/**
|
|
29
|
+
* @param message - human-readable failure description without raw bytes or host paths.
|
|
30
|
+
* @param code - stable machine-routing code.
|
|
31
|
+
* @param options - optional chained cause.
|
|
32
|
+
*/
|
|
33
|
+
constructor(message, code, options) {
|
|
34
|
+
super(message, options);
|
|
35
|
+
this.name = "AttachmentError";
|
|
36
|
+
this.code = code;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Distinguish caller-correctable image admission failures from storage faults.
|
|
41
|
+
* @param error - failure raised while validating or persisting an image batch.
|
|
42
|
+
* @returns whether the caller can correct the proposed image content or batch.
|
|
43
|
+
*/
|
|
44
|
+
function isImageAdmissionError(error) {
|
|
45
|
+
return error instanceof Error && "code" in error && typeof error.code === "string" && IMAGE_ADMISSION_ERROR_CODE_SET.has(error.code);
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region lib/types/brand.js
|
|
49
|
+
/** Attachment identifier brand. @module @hydraharness/harness-attachment/brand */
|
|
50
|
+
/**
|
|
51
|
+
* Brand a validated storage identifier.
|
|
52
|
+
* @param value - backend-produced opaque identifier.
|
|
53
|
+
* @returns the branded identifier.
|
|
54
|
+
*/
|
|
55
|
+
function AttachmentId(value) {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region lib/types/admission.js
|
|
60
|
+
/** Wire-form admission of base64-encoded image uploads. @module @hydraharness/harness-attachment/admission */
|
|
61
|
+
/** Decode one upload payload while rejecting non-canonical base64 forms. */
|
|
62
|
+
function decodeBase64(data) {
|
|
63
|
+
const decoded = Buffer.from(data, "base64");
|
|
64
|
+
if (data.length === 0 || decoded.toString("base64") !== data) throw new AttachmentError("Image upload is not canonical base64.", "INVALID_IMAGE_BASE64");
|
|
65
|
+
return new Uint8Array(decoded);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Admit one generic file upload, including zero-byte files.
|
|
69
|
+
* @param attachments - attachment store that owns durable file storage.
|
|
70
|
+
* @param file - canonical base64 upload and optional display name.
|
|
71
|
+
* @returns the durable file reference.
|
|
72
|
+
*/
|
|
73
|
+
async function admitEncodedFile(attachments, file) {
|
|
74
|
+
const decoded = Buffer.from(file.data, "base64");
|
|
75
|
+
if (file.data.length > 0 && decoded.toString("base64") !== file.data) throw new AttachmentError("File upload is not canonical base64.", "INVALID_FILE_BASE64");
|
|
76
|
+
return attachments.saveFile({
|
|
77
|
+
data: new Uint8Array(decoded),
|
|
78
|
+
...file.name === void 0 ? {} : { name: file.name }
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** Store input for one decoded upload. */
|
|
82
|
+
function saveInput(image) {
|
|
83
|
+
return {
|
|
84
|
+
data: decodeBase64(image.data),
|
|
85
|
+
mediaType: image.mediaType,
|
|
86
|
+
...image.name === void 0 ? {} : { name: image.name }
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Admit one wire image batch: enforce canonical base64 on every member, then
|
|
91
|
+
* delegate batch admission — count and aggregate-byte limits, media-type and
|
|
92
|
+
* per-image validation, ordered commit — to {@link AttachmentStore.saveImages}.
|
|
93
|
+
* The shared entry for every RPC endpoint accepting browser uploads.
|
|
94
|
+
* @param attachments - the deployment attachment store owning batch policy.
|
|
95
|
+
* @param images - base64-encoded uploads in caller order.
|
|
96
|
+
* @returns durable references in the same order as `images`.
|
|
97
|
+
* @throws AttachmentError on a non-canonical payload or a refused batch.
|
|
98
|
+
*/
|
|
99
|
+
async function admitEncodedImages(attachments, images) {
|
|
100
|
+
return attachments.saveImages(images.map(saveInput));
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region lib/types/presentation.js
|
|
104
|
+
/**
|
|
105
|
+
* Read image references from recognized tool presentation metadata.
|
|
106
|
+
* @param meta - untrusted durable presentation metadata.
|
|
107
|
+
* @returns validated image references, or an empty list for other metadata.
|
|
108
|
+
* @throws TypeError when recognized metadata contains an invalid reference.
|
|
109
|
+
*/
|
|
110
|
+
function toolImageReferences(meta) {
|
|
111
|
+
if (typeof meta !== "object" || meta === null || !("kind" in meta) || meta.kind !== "tool-images") return [];
|
|
112
|
+
const images = meta.images;
|
|
113
|
+
if (!Array.isArray(images) || images.some((image) => {
|
|
114
|
+
if (typeof image !== "object" || image === null) return true;
|
|
115
|
+
const ref = image;
|
|
116
|
+
return typeof ref.attachmentId !== "string" || ref.attachmentId.length === 0 || ![
|
|
117
|
+
"image/png",
|
|
118
|
+
"image/jpeg",
|
|
119
|
+
"image/webp",
|
|
120
|
+
"image/gif"
|
|
121
|
+
].includes(ref.mediaType ?? "") || ![
|
|
122
|
+
ref.bytes,
|
|
123
|
+
ref.width,
|
|
124
|
+
ref.height
|
|
125
|
+
].every((value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0) || ref.name !== void 0 && typeof ref.name !== "string";
|
|
126
|
+
})) throw new TypeError("Invalid tool-image presentation metadata.");
|
|
127
|
+
return images;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Read durable video references from tool presentation metadata.
|
|
131
|
+
* @param meta - untrusted durable presentation metadata.
|
|
132
|
+
* @returns validated video references, or an empty list for other metadata.
|
|
133
|
+
* @throws TypeError when recognized metadata contains an invalid reference.
|
|
134
|
+
*/
|
|
135
|
+
function toolVideoReferences(meta) {
|
|
136
|
+
if (typeof meta !== "object" || meta === null || !("kind" in meta) || meta.kind !== "tool-videos") return [];
|
|
137
|
+
const videos = meta.videos;
|
|
138
|
+
if (!Array.isArray(videos) || videos.some((video) => {
|
|
139
|
+
if (typeof video !== "object" || video === null) return true;
|
|
140
|
+
const ref = video;
|
|
141
|
+
return typeof ref.attachmentId !== "string" || ref.attachmentId.length === 0 || !["video/mp4", "video/webm"].includes(ref.mediaType ?? "") || typeof ref.bytes !== "number" || !Number.isSafeInteger(ref.bytes) || ref.bytes <= 0 || typeof ref.name !== "string" || ref.name.length === 0 || /[\\/\u0000-\u001f\u007f]/u.test(ref.name);
|
|
142
|
+
})) throw new TypeError("Invalid tool-video presentation metadata.");
|
|
143
|
+
return videos;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Read the actual generating route's display labels from durable metadata.
|
|
147
|
+
* @param meta - tool presentation metadata.
|
|
148
|
+
* @returns only string-valued provider and model labels.
|
|
149
|
+
*/
|
|
150
|
+
function toolMediaLabels(meta) {
|
|
151
|
+
if (typeof meta !== "object" || meta === null) return {};
|
|
152
|
+
return {
|
|
153
|
+
..."model" in meta && typeof meta.model === "string" ? { model: meta.model } : {},
|
|
154
|
+
..."provider" in meta && typeof meta.provider === "string" ? { provider: meta.provider } : {}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region lib/types/index.js
|
|
159
|
+
/** Durable attachment storage seam (`ctx.attachments`). @module @hydraharness/harness-attachment */
|
|
160
|
+
/** Immutable binary attachment service. Implementations validate bytes before publishing a reference. */
|
|
161
|
+
var AttachmentStore = class extends Service {
|
|
162
|
+
constructor(ctx) {
|
|
163
|
+
super(ctx, "attachments");
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Validate one ordered image batch before committing any member.
|
|
167
|
+
* Validation failures start no writes; storage failures return no partial
|
|
168
|
+
* references, although already published content-addressed objects may stay
|
|
169
|
+
* unreachable until a future retention policy collects them.
|
|
170
|
+
* @param inputs - encoded images in their owning message order.
|
|
171
|
+
* @returns durable references in the exact input order.
|
|
172
|
+
*/
|
|
173
|
+
async saveImages(inputs) {
|
|
174
|
+
const { maxImagesPerMessage, maxMessageImageBytes, mediaTypes } = this.imageLimits;
|
|
175
|
+
if (inputs.length > maxImagesPerMessage) throw new AttachmentError("Image batch exceeds the configured image-count limit.", "TOO_MANY_IMAGES");
|
|
176
|
+
if (inputs.reduce((sum, input) => sum + input.data.byteLength, 0) > maxMessageImageBytes) throw new AttachmentError("Image batch exceeds the configured aggregate image-byte limit.", "IMAGES_TOO_LARGE");
|
|
177
|
+
for (const input of inputs) if (!mediaTypes.includes(input.mediaType)) throw new AttachmentError(`Image type ${input.mediaType} is not accepted by this deployment.`, "UNSUPPORTED_IMAGE_TYPE");
|
|
178
|
+
for (const input of inputs) await this.validateImage(input);
|
|
179
|
+
const refs = [];
|
|
180
|
+
for (const input of inputs) refs.push(await this.saveImage(input));
|
|
181
|
+
return refs;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Persist one file verbatim and return its durable reference.
|
|
185
|
+
* @param input - complete file bytes and optional display name.
|
|
186
|
+
* @returns the content-addressed durable reference.
|
|
187
|
+
*/
|
|
188
|
+
saveFile(input) {
|
|
189
|
+
return Promise.reject(new AttachmentError("The mounted attachment provider cannot store files.", "ATTACHMENT_FILES_UNSUPPORTED"));
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Persist streamed file bytes without requiring one complete input buffer.
|
|
193
|
+
* @param input - ordered byte chunks, cancellation, and optional display name.
|
|
194
|
+
* @returns the content-addressed durable reference.
|
|
195
|
+
*/
|
|
196
|
+
saveFileStream(input) {
|
|
197
|
+
return Promise.reject(new AttachmentError("The mounted attachment provider cannot store files.", "ATTACHMENT_FILES_UNSUPPORTED"));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Read one durable file as verified byte chunks.
|
|
201
|
+
* @param ref - durable file reference to verify.
|
|
202
|
+
* @param signal - optional cancellation signal.
|
|
203
|
+
* @returns an async sequence of exact file bytes.
|
|
204
|
+
*/
|
|
205
|
+
async *readFileStream(ref, signal) {
|
|
206
|
+
signal?.throwIfAborted();
|
|
207
|
+
await Promise.reject(new AttachmentError("The mounted attachment provider cannot read files.", "ATTACHMENT_FILES_UNSUPPORTED"));
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Resolve a durable file to a host path when this provider is host-backed.
|
|
211
|
+
* @param ref - durable file reference.
|
|
212
|
+
* @returns an absolute host path, or undefined when unavailable.
|
|
213
|
+
*/
|
|
214
|
+
fileHostPath(ref) {}
|
|
215
|
+
};
|
|
216
|
+
//#endregion
|
|
217
|
+
export { AttachmentError, AttachmentId, AttachmentStore, AttachmentStore as default, admitEncodedFile, admitEncodedImages, isImageAdmissionError, toolImageReferences, toolMediaLabels, toolVideoReferences };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/** Package-owned invariant companion for `@hydraharness/harness-attachment`. @module @hydraharness/harness-attachment/invariant */
|
|
3
|
+
const PACKAGE_NAME = "@hydraharness/harness-attachment";
|
|
4
|
+
/** Cordis companion plugin name. */
|
|
5
|
+
const name = "attachment-invariant";
|
|
6
|
+
/** Service required before package ownership can be reserved. */
|
|
7
|
+
const inject = ["invariants"];
|
|
8
|
+
/** No runtime invariant: this stateless seam owns types while implementations enforce immutable-store checks. */
|
|
9
|
+
const install = () => {};
|
|
10
|
+
/**
|
|
11
|
+
* Register the package invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the registration disposer.
|
|
14
|
+
*/
|
|
15
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
16
|
+
//#endregion
|
|
17
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Wire-form admission of base64-encoded image uploads. @module @hydraharness/harness-attachment/admission */
|
|
2
|
+
import type { AttachmentStore } from './index.ts';
|
|
3
|
+
import type { EncodedFileAttachment, EncodedImageAttachment, FileAttachmentRef, ImageAttachmentRef } from './types.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Admit one generic file upload, including zero-byte files.
|
|
6
|
+
* @param attachments - attachment store that owns durable file storage.
|
|
7
|
+
* @param file - canonical base64 upload and optional display name.
|
|
8
|
+
* @returns the durable file reference.
|
|
9
|
+
*/
|
|
10
|
+
export declare function admitEncodedFile(attachments: AttachmentStore, file: EncodedFileAttachment): Promise<FileAttachmentRef>;
|
|
11
|
+
/**
|
|
12
|
+
* Admit one wire image batch: enforce canonical base64 on every member, then
|
|
13
|
+
* delegate batch admission — count and aggregate-byte limits, media-type and
|
|
14
|
+
* per-image validation, ordered commit — to {@link AttachmentStore.saveImages}.
|
|
15
|
+
* The shared entry for every RPC endpoint accepting browser uploads.
|
|
16
|
+
* @param attachments - the deployment attachment store owning batch policy.
|
|
17
|
+
* @param images - base64-encoded uploads in caller order.
|
|
18
|
+
* @returns durable references in the same order as `images`.
|
|
19
|
+
* @throws AttachmentError on a non-canonical payload or a refused batch.
|
|
20
|
+
*/
|
|
21
|
+
export declare function admitEncodedImages(attachments: AttachmentStore, images: readonly EncodedImageAttachment[]): Promise<readonly ImageAttachmentRef[]>;
|
|
22
|
+
//# sourceMappingURL=admission.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Wire-form admission of base64-encoded image uploads. @module @hydraharness/harness-attachment/admission */
|
|
2
|
+
import { Buffer } from 'node:buffer';
|
|
3
|
+
import { AttachmentError } from "./error.js";
|
|
4
|
+
/** Decode one upload payload while rejecting non-canonical base64 forms. */
|
|
5
|
+
function decodeBase64(data) {
|
|
6
|
+
const decoded = Buffer.from(data, 'base64');
|
|
7
|
+
if (data.length === 0 || decoded.toString('base64') !== data) {
|
|
8
|
+
throw new AttachmentError('Image upload is not canonical base64.', 'INVALID_IMAGE_BASE64');
|
|
9
|
+
}
|
|
10
|
+
return new Uint8Array(decoded);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Admit one generic file upload, including zero-byte files.
|
|
14
|
+
* @param attachments - attachment store that owns durable file storage.
|
|
15
|
+
* @param file - canonical base64 upload and optional display name.
|
|
16
|
+
* @returns the durable file reference.
|
|
17
|
+
*/
|
|
18
|
+
export async function admitEncodedFile(attachments, file) {
|
|
19
|
+
const decoded = Buffer.from(file.data, 'base64');
|
|
20
|
+
if ((file.data.length > 0 && decoded.toString('base64') !== file.data)) {
|
|
21
|
+
throw new AttachmentError('File upload is not canonical base64.', 'INVALID_FILE_BASE64');
|
|
22
|
+
}
|
|
23
|
+
return attachments.saveFile({ data: new Uint8Array(decoded), ...(file.name === undefined ? {} : { name: file.name }) });
|
|
24
|
+
}
|
|
25
|
+
/** Store input for one decoded upload. */
|
|
26
|
+
function saveInput(image) {
|
|
27
|
+
return {
|
|
28
|
+
data: decodeBase64(image.data),
|
|
29
|
+
mediaType: image.mediaType,
|
|
30
|
+
...image.name === undefined ? {} : { name: image.name },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Admit one wire image batch: enforce canonical base64 on every member, then
|
|
35
|
+
* delegate batch admission — count and aggregate-byte limits, media-type and
|
|
36
|
+
* per-image validation, ordered commit — to {@link AttachmentStore.saveImages}.
|
|
37
|
+
* The shared entry for every RPC endpoint accepting browser uploads.
|
|
38
|
+
* @param attachments - the deployment attachment store owning batch policy.
|
|
39
|
+
* @param images - base64-encoded uploads in caller order.
|
|
40
|
+
* @returns durable references in the same order as `images`.
|
|
41
|
+
* @throws AttachmentError on a non-canonical payload or a refused batch.
|
|
42
|
+
*/
|
|
43
|
+
export async function admitEncodedImages(attachments, images) {
|
|
44
|
+
return attachments.saveImages(images.map(saveInput));
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=admission.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Attachment identifier brand. @module @hydraharness/harness-attachment/brand */
|
|
2
|
+
import type { Branded } from '@hydraharness/harness-brand';
|
|
3
|
+
/** Opaque content-addressed identifier for one immutable attachment object. */
|
|
4
|
+
export type AttachmentId = Branded<'AttachmentId'>;
|
|
5
|
+
/**
|
|
6
|
+
* Brand a validated storage identifier.
|
|
7
|
+
* @param value - backend-produced opaque identifier.
|
|
8
|
+
* @returns the branded identifier.
|
|
9
|
+
*/
|
|
10
|
+
export declare function AttachmentId(value: string): AttachmentId;
|
|
11
|
+
//# sourceMappingURL=brand.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Attachment identifier brand. @module @hydraharness/harness-attachment/brand */
|
|
2
|
+
/**
|
|
3
|
+
* Brand a validated storage identifier.
|
|
4
|
+
* @param value - backend-produced opaque identifier.
|
|
5
|
+
* @returns the branded identifier.
|
|
6
|
+
*/
|
|
7
|
+
export function AttachmentId(value) {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=brand.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Attachment failure class. @module @hydraharness/harness-attachment/error */
|
|
2
|
+
declare const IMAGE_ADMISSION_ERROR_CODES: readonly ["TOO_MANY_IMAGES", "IMAGES_TOO_LARGE", "UNSUPPORTED_IMAGE_TYPE", "INVALID_IMAGE_BASE64", "INVALID_IMAGE", "IMAGE_TYPE_MISMATCH", "IMAGE_TOO_LARGE", "IMAGE_TOO_MANY_PIXELS", "IMAGE_DIMENSION_TOO_LARGE"];
|
|
3
|
+
/** Caller-correctable attachment failure codes raised while admitting image input. */
|
|
4
|
+
export type ImageAdmissionErrorCode = typeof IMAGE_ADMISSION_ERROR_CODES[number];
|
|
5
|
+
/** Stable attachment failure codes used for protocol error routing. */
|
|
6
|
+
export type AttachmentErrorCode = ImageAdmissionErrorCode | 'INVALID_FILE_BASE64' | 'INVALID_ATTACHMENT_REF' | 'ATTACHMENT_CORRUPT' | 'ATTACHMENT_WRITE_FAILED' | 'ATTACHMENT_NOT_FOUND' | 'ATTACHMENT_READ_FAILED' | 'ATTACHMENT_FILES_UNSUPPORTED';
|
|
7
|
+
/**
|
|
8
|
+
* Stable failures suitable for host RPC error mapping.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately re-implements the `HarnessError` shape instead of extending it:
|
|
11
|
+
* the base lives in `@hydraharness/harness-llm`, which itself depends on this
|
|
12
|
+
* package (`ImageBlock` references `ImageAttachmentRef`), so sharing the base
|
|
13
|
+
* would create a dependency cycle. Consumers route on `code`, never on the
|
|
14
|
+
* prototype chain, so the shapes stay interchangeable at the wire boundary.
|
|
15
|
+
*/
|
|
16
|
+
export declare class AttachmentError extends Error {
|
|
17
|
+
/** Stable machine-routing failure code. */
|
|
18
|
+
readonly code: AttachmentErrorCode;
|
|
19
|
+
/**
|
|
20
|
+
* @param message - human-readable failure description without raw bytes or host paths.
|
|
21
|
+
* @param code - stable machine-routing code.
|
|
22
|
+
* @param options - optional chained cause.
|
|
23
|
+
*/
|
|
24
|
+
constructor(message: string, code: AttachmentErrorCode, options?: ErrorOptions);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Distinguish caller-correctable image admission failures from storage faults.
|
|
28
|
+
* @param error - failure raised while validating or persisting an image batch.
|
|
29
|
+
* @returns whether the caller can correct the proposed image content or batch.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isImageAdmissionError(error: unknown): error is AttachmentError & {
|
|
32
|
+
readonly code: ImageAdmissionErrorCode;
|
|
33
|
+
};
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=error.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Attachment failure class. @module @hydraharness/harness-attachment/error */
|
|
2
|
+
const IMAGE_ADMISSION_ERROR_CODES = [
|
|
3
|
+
'TOO_MANY_IMAGES',
|
|
4
|
+
'IMAGES_TOO_LARGE',
|
|
5
|
+
'UNSUPPORTED_IMAGE_TYPE',
|
|
6
|
+
'INVALID_IMAGE_BASE64',
|
|
7
|
+
'INVALID_IMAGE',
|
|
8
|
+
'IMAGE_TYPE_MISMATCH',
|
|
9
|
+
'IMAGE_TOO_LARGE',
|
|
10
|
+
'IMAGE_TOO_MANY_PIXELS',
|
|
11
|
+
'IMAGE_DIMENSION_TOO_LARGE',
|
|
12
|
+
];
|
|
13
|
+
/** Runtime membership for structurally compatible errors crossing package boundaries. */
|
|
14
|
+
const IMAGE_ADMISSION_ERROR_CODE_SET = new Set(IMAGE_ADMISSION_ERROR_CODES);
|
|
15
|
+
/**
|
|
16
|
+
* Stable failures suitable for host RPC error mapping.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately re-implements the `HarnessError` shape instead of extending it:
|
|
19
|
+
* the base lives in `@hydraharness/harness-llm`, which itself depends on this
|
|
20
|
+
* package (`ImageBlock` references `ImageAttachmentRef`), so sharing the base
|
|
21
|
+
* would create a dependency cycle. Consumers route on `code`, never on the
|
|
22
|
+
* prototype chain, so the shapes stay interchangeable at the wire boundary.
|
|
23
|
+
*/
|
|
24
|
+
export class AttachmentError extends Error {
|
|
25
|
+
/** Stable machine-routing failure code. */
|
|
26
|
+
code;
|
|
27
|
+
/**
|
|
28
|
+
* @param message - human-readable failure description without raw bytes or host paths.
|
|
29
|
+
* @param code - stable machine-routing code.
|
|
30
|
+
* @param options - optional chained cause.
|
|
31
|
+
*/
|
|
32
|
+
constructor(message, code, options) {
|
|
33
|
+
super(message, options);
|
|
34
|
+
this.name = 'AttachmentError';
|
|
35
|
+
this.code = code;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Distinguish caller-correctable image admission failures from storage faults.
|
|
40
|
+
* @param error - failure raised while validating or persisting an image batch.
|
|
41
|
+
* @returns whether the caller can correct the proposed image content or batch.
|
|
42
|
+
*/
|
|
43
|
+
export function isImageAdmissionError(error) {
|
|
44
|
+
return error instanceof Error
|
|
45
|
+
&& 'code' in error
|
|
46
|
+
&& typeof error.code === 'string'
|
|
47
|
+
&& IMAGE_ADMISSION_ERROR_CODE_SET.has(error.code);
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=error.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** Durable attachment storage seam (`ctx.attachments`). @module @hydraharness/harness-attachment */
|
|
2
|
+
import { Context, Service } from '@hydraharness/cordis';
|
|
3
|
+
import type { ImageAttachmentLimits, ImageAttachmentRef, FileAttachmentRef, SaveFileAttachment, SaveFileStreamAttachment, SaveImageAttachment, StoredImageAttachment } from './types.ts';
|
|
4
|
+
export { AttachmentId } from './brand.ts';
|
|
5
|
+
export { AttachmentError, isImageAdmissionError } from './error.ts';
|
|
6
|
+
export type { AttachmentErrorCode, ImageAdmissionErrorCode } from './error.ts';
|
|
7
|
+
export { admitEncodedFile, admitEncodedImages } from './admission.ts';
|
|
8
|
+
export { toolImageReferences, toolVideoReferences, toolMediaLabels } from './presentation.ts';
|
|
9
|
+
export type { AttachmentId as AttachmentIdType, EncodedImageAttachment, EncodedFileAttachment, ImageAttachmentLimits, ImageAttachmentRef, FileAttachmentRef, ImageMediaType, ToolImagePresentationMeta, VideoAttachmentRef, ToolVideoPresentationMeta, SaveImageAttachment, SaveFileAttachment, SaveFileStreamAttachment, StoredImageAttachment, } from './types.ts';
|
|
10
|
+
declare module '@hydraharness/cordis' {
|
|
11
|
+
interface Context {
|
|
12
|
+
attachments: AttachmentStore;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Immutable binary attachment service. Implementations validate bytes before publishing a reference. */
|
|
16
|
+
export declare abstract class AttachmentStore extends Service {
|
|
17
|
+
constructor(ctx: Context);
|
|
18
|
+
/** Deployment-resolved image policy used by authoritative and fast-path validation. */
|
|
19
|
+
abstract readonly imageLimits: ImageAttachmentLimits;
|
|
20
|
+
/**
|
|
21
|
+
* Validate one image without persisting it.
|
|
22
|
+
* Batch callers validate every member before saving any member.
|
|
23
|
+
* @param input - encoded bytes, declared media type, and optional display name.
|
|
24
|
+
* @returns completion after the encoded raster has been fully decoded.
|
|
25
|
+
*/
|
|
26
|
+
abstract validateImage(input: SaveImageAttachment): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Validate one ordered image batch before committing any member.
|
|
29
|
+
* Validation failures start no writes; storage failures return no partial
|
|
30
|
+
* references, although already published content-addressed objects may stay
|
|
31
|
+
* unreachable until a future retention policy collects them.
|
|
32
|
+
* @param inputs - encoded images in their owning message order.
|
|
33
|
+
* @returns durable references in the exact input order.
|
|
34
|
+
*/
|
|
35
|
+
saveImages(inputs: readonly SaveImageAttachment[]): Promise<readonly ImageAttachmentRef[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Validate and durably commit one image before its owning session event is appended.
|
|
38
|
+
* @param input - encoded bytes, declared media type, and optional display name.
|
|
39
|
+
* @returns a durable content-addressed reference.
|
|
40
|
+
*/
|
|
41
|
+
abstract saveImage(input: SaveImageAttachment): Promise<ImageAttachmentRef>;
|
|
42
|
+
/**
|
|
43
|
+
* Read one image and verify that bytes still match the recorded reference.
|
|
44
|
+
* @param ref - durable reference from the session log.
|
|
45
|
+
* @param signal - optional cancellation for backend read and verification work.
|
|
46
|
+
* @returns the verified bytes and canonical reference.
|
|
47
|
+
* @throws the signal reason when aborted, or a storage error when verification fails.
|
|
48
|
+
*/
|
|
49
|
+
abstract readImage(ref: ImageAttachmentRef, signal?: AbortSignal): Promise<StoredImageAttachment>;
|
|
50
|
+
/**
|
|
51
|
+
* Persist one file verbatim and return its durable reference.
|
|
52
|
+
* @param input - complete file bytes and optional display name.
|
|
53
|
+
* @returns the content-addressed durable reference.
|
|
54
|
+
*/
|
|
55
|
+
saveFile(input: SaveFileAttachment): Promise<FileAttachmentRef>;
|
|
56
|
+
/**
|
|
57
|
+
* Persist streamed file bytes without requiring one complete input buffer.
|
|
58
|
+
* @param input - ordered byte chunks, cancellation, and optional display name.
|
|
59
|
+
* @returns the content-addressed durable reference.
|
|
60
|
+
*/
|
|
61
|
+
saveFileStream(input: SaveFileStreamAttachment): Promise<FileAttachmentRef>;
|
|
62
|
+
/**
|
|
63
|
+
* Read one durable file as verified byte chunks.
|
|
64
|
+
* @param ref - durable file reference to verify.
|
|
65
|
+
* @param signal - optional cancellation signal.
|
|
66
|
+
* @returns an async sequence of exact file bytes.
|
|
67
|
+
*/
|
|
68
|
+
readFileStream(ref: FileAttachmentRef, signal?: AbortSignal): AsyncIterable<Uint8Array>;
|
|
69
|
+
/**
|
|
70
|
+
* Resolve a durable file to a host path when this provider is host-backed.
|
|
71
|
+
* @param ref - durable file reference.
|
|
72
|
+
* @returns an absolute host path, or undefined when unavailable.
|
|
73
|
+
*/
|
|
74
|
+
fileHostPath(ref: FileAttachmentRef): string | undefined;
|
|
75
|
+
}
|
|
76
|
+
export default AttachmentStore;
|
|
77
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Durable attachment storage seam (`ctx.attachments`). @module @hydraharness/harness-attachment */
|
|
2
|
+
import { Service } from '@hydraharness/cordis';
|
|
3
|
+
import { AttachmentError } from "./error.js";
|
|
4
|
+
export { AttachmentId } from "./brand.js";
|
|
5
|
+
export { AttachmentError, isImageAdmissionError } from "./error.js";
|
|
6
|
+
export { admitEncodedFile, admitEncodedImages } from "./admission.js";
|
|
7
|
+
export { toolImageReferences, toolVideoReferences, toolMediaLabels } from "./presentation.js";
|
|
8
|
+
/** Immutable binary attachment service. Implementations validate bytes before publishing a reference. */
|
|
9
|
+
export class AttachmentStore extends Service {
|
|
10
|
+
constructor(ctx) {
|
|
11
|
+
super(ctx, 'attachments');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Validate one ordered image batch before committing any member.
|
|
15
|
+
* Validation failures start no writes; storage failures return no partial
|
|
16
|
+
* references, although already published content-addressed objects may stay
|
|
17
|
+
* unreachable until a future retention policy collects them.
|
|
18
|
+
* @param inputs - encoded images in their owning message order.
|
|
19
|
+
* @returns durable references in the exact input order.
|
|
20
|
+
*/
|
|
21
|
+
async saveImages(inputs) {
|
|
22
|
+
const { maxImagesPerMessage, maxMessageImageBytes, mediaTypes } = this.imageLimits;
|
|
23
|
+
if (inputs.length > maxImagesPerMessage) {
|
|
24
|
+
throw new AttachmentError('Image batch exceeds the configured image-count limit.', 'TOO_MANY_IMAGES');
|
|
25
|
+
}
|
|
26
|
+
const totalBytes = inputs.reduce((sum, input) => sum + input.data.byteLength, 0);
|
|
27
|
+
if (totalBytes > maxMessageImageBytes) {
|
|
28
|
+
throw new AttachmentError('Image batch exceeds the configured aggregate image-byte limit.', 'IMAGES_TOO_LARGE');
|
|
29
|
+
}
|
|
30
|
+
for (const input of inputs) {
|
|
31
|
+
if (!mediaTypes.includes(input.mediaType)) {
|
|
32
|
+
throw new AttachmentError(`Image type ${input.mediaType} is not accepted by this deployment.`, 'UNSUPPORTED_IMAGE_TYPE');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const input of inputs)
|
|
36
|
+
await this.validateImage(input);
|
|
37
|
+
const refs = [];
|
|
38
|
+
for (const input of inputs)
|
|
39
|
+
refs.push(await this.saveImage(input));
|
|
40
|
+
return refs;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Persist one file verbatim and return its durable reference.
|
|
44
|
+
* @param input - complete file bytes and optional display name.
|
|
45
|
+
* @returns the content-addressed durable reference.
|
|
46
|
+
*/
|
|
47
|
+
saveFile(input) {
|
|
48
|
+
void input;
|
|
49
|
+
return Promise.reject(new AttachmentError('The mounted attachment provider cannot store files.', 'ATTACHMENT_FILES_UNSUPPORTED'));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Persist streamed file bytes without requiring one complete input buffer.
|
|
53
|
+
* @param input - ordered byte chunks, cancellation, and optional display name.
|
|
54
|
+
* @returns the content-addressed durable reference.
|
|
55
|
+
*/
|
|
56
|
+
saveFileStream(input) {
|
|
57
|
+
void input;
|
|
58
|
+
return Promise.reject(new AttachmentError('The mounted attachment provider cannot store files.', 'ATTACHMENT_FILES_UNSUPPORTED'));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Read one durable file as verified byte chunks.
|
|
62
|
+
* @param ref - durable file reference to verify.
|
|
63
|
+
* @param signal - optional cancellation signal.
|
|
64
|
+
* @returns an async sequence of exact file bytes.
|
|
65
|
+
*/
|
|
66
|
+
async *readFileStream(ref, signal) {
|
|
67
|
+
signal?.throwIfAborted();
|
|
68
|
+
void ref;
|
|
69
|
+
await Promise.reject(new AttachmentError('The mounted attachment provider cannot read files.', 'ATTACHMENT_FILES_UNSUPPORTED'));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Resolve a durable file to a host path when this provider is host-backed.
|
|
73
|
+
* @param ref - durable file reference.
|
|
74
|
+
* @returns an absolute host path, or undefined when unavailable.
|
|
75
|
+
*/
|
|
76
|
+
fileHostPath(ref) { void ref; return undefined; }
|
|
77
|
+
}
|
|
78
|
+
export default AttachmentStore;
|
|
79
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Package-owned invariant companion for `@hydraharness/harness-attachment`. @module @hydraharness/harness-attachment/invariant */
|
|
2
|
+
import type { Context } from '@hydraharness/cordis';
|
|
3
|
+
/** Cordis companion plugin name. */
|
|
4
|
+
export declare const name = "attachment-invariant";
|
|
5
|
+
/** Service required before package ownership can be reserved. */
|
|
6
|
+
export declare const inject: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Register the package invariant companion.
|
|
9
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
10
|
+
* @returns the registration disposer.
|
|
11
|
+
*/
|
|
12
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
13
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Package-owned invariant companion for `@hydraharness/harness-attachment`. @module @hydraharness/harness-attachment/invariant */
|
|
2
|
+
const PACKAGE_NAME = '@hydraharness/harness-attachment';
|
|
3
|
+
/** Cordis companion plugin name. */
|
|
4
|
+
export const name = 'attachment-invariant';
|
|
5
|
+
/** Service required before package ownership can be reserved. */
|
|
6
|
+
export const inject = ['invariants'];
|
|
7
|
+
/** No runtime invariant: this stateless seam owns types while implementations enforce immutable-store checks. */
|
|
8
|
+
const install = () => { };
|
|
9
|
+
/**
|
|
10
|
+
* Register the package invariant companion.
|
|
11
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
12
|
+
* @returns the registration disposer.
|
|
13
|
+
*/
|
|
14
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
15
|
+
/* jscpd:ignore-end */
|
|
16
|
+
//# sourceMappingURL=invariant.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Durable images shown with a tool result without entering model history. */
|
|
2
|
+
import type { ImageAttachmentRef, VideoAttachmentRef } from './types.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Read image references from recognized tool presentation metadata.
|
|
5
|
+
* @param meta - untrusted durable presentation metadata.
|
|
6
|
+
* @returns validated image references, or an empty list for other metadata.
|
|
7
|
+
* @throws TypeError when recognized metadata contains an invalid reference.
|
|
8
|
+
*/
|
|
9
|
+
export declare function toolImageReferences(meta: unknown): readonly ImageAttachmentRef[];
|
|
10
|
+
/**
|
|
11
|
+
* Read durable video references from tool presentation metadata.
|
|
12
|
+
* @param meta - untrusted durable presentation metadata.
|
|
13
|
+
* @returns validated video references, or an empty list for other metadata.
|
|
14
|
+
* @throws TypeError when recognized metadata contains an invalid reference.
|
|
15
|
+
*/
|
|
16
|
+
export declare function toolVideoReferences(meta: unknown): readonly VideoAttachmentRef[];
|
|
17
|
+
/**
|
|
18
|
+
* Read the actual generating route's display labels from durable metadata.
|
|
19
|
+
* @param meta - tool presentation metadata.
|
|
20
|
+
* @returns only string-valued provider and model labels.
|
|
21
|
+
*/
|
|
22
|
+
export declare function toolMediaLabels(meta: unknown): {
|
|
23
|
+
model?: string;
|
|
24
|
+
provider?: string;
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=presentation.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read image references from recognized tool presentation metadata.
|
|
3
|
+
* @param meta - untrusted durable presentation metadata.
|
|
4
|
+
* @returns validated image references, or an empty list for other metadata.
|
|
5
|
+
* @throws TypeError when recognized metadata contains an invalid reference.
|
|
6
|
+
*/
|
|
7
|
+
export function toolImageReferences(meta) {
|
|
8
|
+
if (typeof meta !== 'object' || meta === null || !('kind' in meta) || meta.kind !== 'tool-images')
|
|
9
|
+
return [];
|
|
10
|
+
const images = meta.images;
|
|
11
|
+
if (!Array.isArray(images) || images.some((image) => {
|
|
12
|
+
if (typeof image !== 'object' || image === null)
|
|
13
|
+
return true;
|
|
14
|
+
const ref = image;
|
|
15
|
+
return typeof ref.attachmentId !== 'string' || ref.attachmentId.length === 0
|
|
16
|
+
|| !['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(ref.mediaType ?? '')
|
|
17
|
+
|| ![ref.bytes, ref.width, ref.height].every(value => typeof value === 'number' && Number.isSafeInteger(value) && value > 0)
|
|
18
|
+
|| (ref.name !== undefined && typeof ref.name !== 'string');
|
|
19
|
+
}))
|
|
20
|
+
throw new TypeError('Invalid tool-image presentation metadata.');
|
|
21
|
+
return images;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Read durable video references from tool presentation metadata.
|
|
25
|
+
* @param meta - untrusted durable presentation metadata.
|
|
26
|
+
* @returns validated video references, or an empty list for other metadata.
|
|
27
|
+
* @throws TypeError when recognized metadata contains an invalid reference.
|
|
28
|
+
*/
|
|
29
|
+
export function toolVideoReferences(meta) {
|
|
30
|
+
if (typeof meta !== 'object' || meta === null || !('kind' in meta) || meta.kind !== 'tool-videos')
|
|
31
|
+
return [];
|
|
32
|
+
const videos = meta.videos;
|
|
33
|
+
if (!Array.isArray(videos) || videos.some((video) => {
|
|
34
|
+
if (typeof video !== 'object' || video === null)
|
|
35
|
+
return true;
|
|
36
|
+
const ref = video;
|
|
37
|
+
return typeof ref.attachmentId !== 'string' || ref.attachmentId.length === 0
|
|
38
|
+
|| !['video/mp4', 'video/webm'].includes(ref.mediaType ?? '')
|
|
39
|
+
|| typeof ref.bytes !== 'number' || !Number.isSafeInteger(ref.bytes) || ref.bytes <= 0
|
|
40
|
+
|| typeof ref.name !== 'string' || ref.name.length === 0 || /[\\/\u0000-\u001f\u007f]/u.test(ref.name);
|
|
41
|
+
}))
|
|
42
|
+
throw new TypeError('Invalid tool-video presentation metadata.');
|
|
43
|
+
return videos;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read the actual generating route's display labels from durable metadata.
|
|
47
|
+
* @param meta - tool presentation metadata.
|
|
48
|
+
* @returns only string-valued provider and model labels.
|
|
49
|
+
*/
|
|
50
|
+
export function toolMediaLabels(meta) {
|
|
51
|
+
if (typeof meta !== 'object' || meta === null)
|
|
52
|
+
return {};
|
|
53
|
+
return {
|
|
54
|
+
...'model' in meta && typeof meta.model === 'string' ? { model: meta.model } : {},
|
|
55
|
+
...'provider' in meta && typeof meta.provider === 'string' ? { provider: meta.provider } : {},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=presentation.js.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** Durable attachment vocabulary. @module @hydraharness/harness-attachment/types */
|
|
2
|
+
import type { AttachmentId } from './brand.ts';
|
|
3
|
+
export type { AttachmentId } from './brand.ts';
|
|
4
|
+
/** Raster image formats accepted by the version-one attachment path. */
|
|
5
|
+
export type ImageMediaType = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif';
|
|
6
|
+
/** Durable, serializable metadata for one immutable image object. */
|
|
7
|
+
export interface ImageAttachmentRef {
|
|
8
|
+
/** Opaque storage identifier; never a filesystem path or bearer URL. */
|
|
9
|
+
attachmentId: AttachmentId;
|
|
10
|
+
/** Media type verified from the stored bytes. */
|
|
11
|
+
mediaType: ImageMediaType;
|
|
12
|
+
/** Exact encoded byte length. */
|
|
13
|
+
bytes: number;
|
|
14
|
+
/** Intrinsic encoded width in pixels. */
|
|
15
|
+
width: number;
|
|
16
|
+
/** Intrinsic encoded height in pixels. */
|
|
17
|
+
height: number;
|
|
18
|
+
/** Optional display name stripped of local path information. */
|
|
19
|
+
name?: string;
|
|
20
|
+
}
|
|
21
|
+
/** Tool-result presentation images, excluded from derived model messages. */
|
|
22
|
+
export interface ToolImagePresentationMeta {
|
|
23
|
+
kind: 'tool-images';
|
|
24
|
+
images: ImageAttachmentRef[];
|
|
25
|
+
}
|
|
26
|
+
/** Stored video bytes presented to users without entering model history. */
|
|
27
|
+
export interface VideoAttachmentRef extends FileAttachmentRef {
|
|
28
|
+
mediaType: 'video/mp4' | 'video/webm';
|
|
29
|
+
}
|
|
30
|
+
/** Tool-result videos read through session-authorized attachment access. */
|
|
31
|
+
export interface ToolVideoPresentationMeta {
|
|
32
|
+
kind: 'tool-videos';
|
|
33
|
+
videos: VideoAttachmentRef[];
|
|
34
|
+
}
|
|
35
|
+
/** Durable reference to verbatim stored file bytes. */
|
|
36
|
+
export interface FileAttachmentRef {
|
|
37
|
+
attachmentId: AttachmentId;
|
|
38
|
+
name: string;
|
|
39
|
+
bytes: number;
|
|
40
|
+
}
|
|
41
|
+
/** Base64-encoded file upload. */
|
|
42
|
+
export interface EncodedFileAttachment {
|
|
43
|
+
data: string;
|
|
44
|
+
name?: string;
|
|
45
|
+
}
|
|
46
|
+
/** Request to persist exact file bytes. */
|
|
47
|
+
export interface SaveFileAttachment {
|
|
48
|
+
data: Uint8Array;
|
|
49
|
+
name?: string;
|
|
50
|
+
}
|
|
51
|
+
/** Streamed request to persist exact file bytes. */
|
|
52
|
+
export interface SaveFileStreamAttachment {
|
|
53
|
+
data: AsyncIterable<Uint8Array>;
|
|
54
|
+
signal?: AbortSignal;
|
|
55
|
+
name?: string;
|
|
56
|
+
}
|
|
57
|
+
/** Deployment-resolved limits used by upload admission and request buffering. */
|
|
58
|
+
export interface ImageAttachmentLimits {
|
|
59
|
+
maxImageBytes: number;
|
|
60
|
+
maxImagesPerMessage: number;
|
|
61
|
+
maxMessageImageBytes: number;
|
|
62
|
+
maxImagePixels: number;
|
|
63
|
+
/** Maximum intrinsic width and maximum intrinsic height in pixels for one image. */
|
|
64
|
+
maxImageDimension: number;
|
|
65
|
+
mediaTypes: readonly ImageMediaType[];
|
|
66
|
+
}
|
|
67
|
+
/** Base64-encoded image upload accompanying one wire request. */
|
|
68
|
+
export interface EncodedImageAttachment {
|
|
69
|
+
/** Declared media type, verified against the decoded bytes during admission. */
|
|
70
|
+
mediaType: ImageMediaType;
|
|
71
|
+
/** Canonical base64 encoding of the image bytes. */
|
|
72
|
+
data: string;
|
|
73
|
+
/** Optional display name; it is never interpreted as a path. */
|
|
74
|
+
name?: string;
|
|
75
|
+
}
|
|
76
|
+
/** Request to validate and durably commit one image. */
|
|
77
|
+
export interface SaveImageAttachment {
|
|
78
|
+
data: Uint8Array;
|
|
79
|
+
/** Caller-declared media type, checked against fully decoded bytes. */
|
|
80
|
+
mediaType: ImageMediaType;
|
|
81
|
+
/** Optional browser/provider display name; it is never interpreted as a path. */
|
|
82
|
+
name?: string;
|
|
83
|
+
}
|
|
84
|
+
/** Stored image bytes returned after reference and digest verification. */
|
|
85
|
+
export interface StoredImageAttachment {
|
|
86
|
+
ref: ImageAttachmentRef;
|
|
87
|
+
data: Uint8Array;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=types.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hydraharness/harness-attachment",
|
|
3
|
+
"description": "Durable immutable attachment storage seam for the Hydra harness",
|
|
4
|
+
"version": "0.1.1-rc.10",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/MaiHongPhong2010/Hydra-Harness.git",
|
|
11
|
+
"directory": "packages/attachment/attachment"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./types": {
|
|
26
|
+
"types": "./lib/types/types.d.ts",
|
|
27
|
+
"default": "./lib/types/types.js"
|
|
28
|
+
},
|
|
29
|
+
"./src/*": "./src/*",
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"lib/index.js",
|
|
34
|
+
"lib/invariant.js",
|
|
35
|
+
"lib/types/**/*.js",
|
|
36
|
+
"lib/types/**/*.d.ts"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@hydraharness/harness-invariants": "^0.1.1-rc.10",
|
|
41
|
+
"@hydraharness/cordis": "^4.0.2",
|
|
42
|
+
"@hydraharness/harness-brand": "^0.1.1-rc.10"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@hydraharness/harness-brand": "^0.1.1-rc.10",
|
|
46
|
+
"@hydraharness/cordis": "^4.0.2",
|
|
47
|
+
"@hydraharness/harness-invariants": "^0.1.1-rc.10"
|
|
48
|
+
}
|
|
49
|
+
}
|