@rynx-ai/daemon 0.1.11-beta.3 → 0.1.11-beta.30
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/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +0 -12
- package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.js +3 -1
- package/bundled-plugins/plugins/lark/dist/host-adapters.d.ts +3 -3
- package/bundled-plugins/plugins/lark/dist/host-adapters.js +24 -18
- package/bundled-plugins/plugins/lark/dist/lark.js +18 -12
- package/bundled-plugins/plugins/lark/dist/runtime.js +6046 -6018
- package/bundled-plugins/plugins/lark/dist/settings.js +6006 -5987
- package/bundled-plugins/plugins/lark/package.json +1 -1
- package/bundled-plugins/plugins/lark/ui/assets/index-CGUVAMsv.css +1 -0
- package/bundled-plugins/plugins/lark/ui/index.html +2 -2
- package/dist/agent-file.js +0 -1
- package/dist/browser-artifact-management.js +4 -1
- package/dist/chrome-for-testing-runtime.js +4 -3
- package/dist/chrome-for-testing-store.js +29 -7
- package/dist/control-deps.d.ts +6 -2
- package/dist/control-deps.js +87 -4
- package/dist/control-link-store.js +4 -3
- package/dist/daemon-owner.js +4 -7
- package/dist/daemon-server.js +25 -14
- package/dist/db.d.ts +2 -2
- package/dist/db.js +133 -21
- package/dist/lifecycle.d.ts +1 -0
- package/dist/lifecycle.js +3 -0
- package/dist/migrations/cleanup-legacy-sessions.js +4 -3
- package/dist/plugin-cli.d.ts +10 -0
- package/dist/plugin-cli.js +19 -2
- package/dist/plugin-host-rpc.d.ts +31 -9
- package/dist/plugin-host-rpc.js +428 -80
- package/dist/plugin-installer.js +3 -2
- package/dist/plugin-management-service.js +0 -2
- package/dist/plugin-marketplace.js +2 -1
- package/dist/plugin-store.js +2 -4
- package/dist/plugin-supervisor.js +0 -4
- package/dist/remote-runtime-access-store.d.ts +2 -2
- package/dist/remote-runtime-access-store.js +6 -9
- package/dist/remote-runtime-connection-manager.js +1 -1
- package/dist/remote-runtime-target-store.d.ts +2 -2
- package/dist/remote-runtime-target-store.js +6 -6
- package/dist/session-emulator-binding-store.js +4 -6
- package/dist/session-fork-store.d.ts +1 -0
- package/dist/session-fork-store.js +82 -28
- package/dist/session-launch-operations.js +6 -5
- package/dist/session-log-store.d.ts +1 -0
- package/dist/session-log-store.js +19 -16
- package/dist/session-meta-store.d.ts +5 -0
- package/dist/session-meta-store.js +25 -2
- package/dist/session-portal-grant-store.d.ts +29 -0
- package/dist/session-portal-grant-store.js +85 -0
- package/dist/session-resource-store.d.ts +20 -6
- package/dist/session-resource-store.js +364 -68
- package/dist/setup-service.d.ts +1 -0
- package/dist/setup-service.js +1 -0
- package/dist/skills-catalog.d.ts +11 -2
- package/dist/skills-catalog.js +317 -10
- package/dist/sqlite.d.ts +22 -0
- package/dist/sqlite.js +57 -0
- package/dist/system-dependencies.d.ts +22 -0
- package/dist/system-dependencies.js +79 -0
- package/package.json +15 -12
- package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +0 -1
- /package/bundled-plugins/plugins/lark/ui/assets/{index-D9N-AkLX.js → index-D8Amoe6a.js} +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { chmodSync, closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, writeSync, } from "node:fs";
|
|
3
|
-
import { basename, join } from "node:path";
|
|
2
|
+
import { chmodSync, closeSync, constants, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, writeSync, } from "node:fs";
|
|
3
|
+
import { basename, extname, isAbsolute, join } from "node:path";
|
|
4
4
|
import { rynxHome, } from "@rynx-ai/core";
|
|
5
|
-
import { defaultSessionResourcePolicy, SESSION_RESOURCE_MAX_IMAGE_PIXELS, SESSION_RESOURCE_MAX_IMAGE_SIDE, SESSION_RESOURCE_MAX_MESSAGE_BYTES, SESSION_RESOURCE_TRANSFER_CHUNK_BYTES, } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
5
|
+
import { defaultSessionResourcePolicy, SESSION_RESOURCE_MAX_IMAGE_BYTES, SESSION_RESOURCE_MAX_IMAGE_PIXELS, SESSION_RESOURCE_MAX_IMAGE_SIDE, SESSION_RESOURCE_MAX_MESSAGE_BYTES, SESSION_RESOURCE_TRANSFER_CHUNK_BYTES, } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
6
6
|
import { MachineSessionServiceFailure, } from "@rynx-ai/server";
|
|
7
7
|
import { db } from "./db.js";
|
|
8
|
+
import { transaction } from "./sqlite.js";
|
|
8
9
|
const MAX_STAGED_RESOURCES_PER_SESSION = 16;
|
|
9
10
|
const STAGED_TTL_MS = 24 * 60 * 60 * 1_000;
|
|
10
11
|
export class SqliteSessionResourceStore {
|
|
@@ -25,7 +26,8 @@ export class SqliteSessionResourceStore {
|
|
|
25
26
|
WHERE session_id = ? AND client_upload_id = ?`)
|
|
26
27
|
.get(input.sessionId, input.clientUploadId);
|
|
27
28
|
if (existing) {
|
|
28
|
-
if (existing.
|
|
29
|
+
if (existing.kind !== resourceKind(input.type) ||
|
|
30
|
+
existing.filename !== filename ||
|
|
29
31
|
existing.media_type !== input.mediaType ||
|
|
30
32
|
existing.byte_length !== input.byteLength) {
|
|
31
33
|
throw conflict("clientUploadId was already used for different content");
|
|
@@ -43,23 +45,27 @@ export class SqliteSessionResourceStore {
|
|
|
43
45
|
: { resource: toMetadata(refreshed) }),
|
|
44
46
|
};
|
|
45
47
|
}
|
|
48
|
+
if (input.type === "input_image" && input.byteLength > SESSION_RESOURCE_MAX_IMAGE_BYTES) {
|
|
49
|
+
throw invalid("Image exceeds the Session resource policy");
|
|
50
|
+
}
|
|
46
51
|
const staged = db()
|
|
47
|
-
.prepare(`SELECT COUNT(*) AS count,
|
|
52
|
+
.prepare(`SELECT COUNT(*) AS count,
|
|
53
|
+
COALESCE(SUM(CASE WHEN kind = 'image' THEN byte_length ELSE 0 END), 0) AS bytes
|
|
48
54
|
FROM session_resources
|
|
49
55
|
WHERE session_id = ? AND state IN ('uploading', 'staged')`)
|
|
50
56
|
.get(input.sessionId);
|
|
51
57
|
if (staged.count >= MAX_STAGED_RESOURCES_PER_SESSION ||
|
|
52
|
-
staged.bytes + input.byteLength > SESSION_RESOURCE_MAX_MESSAGE_BYTES) {
|
|
53
|
-
throw unavailable("Session staged
|
|
58
|
+
(input.type === "input_image" && staged.bytes + input.byteLength > SESSION_RESOURCE_MAX_MESSAGE_BYTES)) {
|
|
59
|
+
throw unavailable("Session staged resource quota exceeded");
|
|
54
60
|
}
|
|
55
61
|
const resourceId = `res_${randomUUID()}`;
|
|
56
62
|
const uploadId = `upl_${randomUUID()}`;
|
|
57
63
|
const createdAt = Date.now();
|
|
58
64
|
ensurePrivateDirectory(this.sessionDirectory(input.sessionId));
|
|
59
65
|
db().prepare(`INSERT INTO session_resources
|
|
60
|
-
(id, session_id, client_upload_id, upload_id, filename, media_type,
|
|
66
|
+
(id, session_id, client_upload_id, upload_id, kind, filename, media_type,
|
|
61
67
|
byte_length, uploaded_bytes, state, created_at)
|
|
62
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, 0, 'uploading', ?)`).run(resourceId, input.sessionId, input.clientUploadId, uploadId, filename, input.mediaType, input.byteLength, createdAt);
|
|
68
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 'uploading', ?)`).run(resourceId, input.sessionId, input.clientUploadId, uploadId, resourceKind(input.type), filename, input.mediaType, input.byteLength, createdAt);
|
|
63
69
|
return { uploadId, resourceId, offset: 0, complete: false };
|
|
64
70
|
}
|
|
65
71
|
async writeUploadChunk(input) {
|
|
@@ -122,23 +128,25 @@ export class SqliteSessionResourceStore {
|
|
|
122
128
|
const partial = this.partialPath(row);
|
|
123
129
|
const final = this.finalPath(row);
|
|
124
130
|
if (!existsSync(partial) && existsSync(final)) {
|
|
125
|
-
|
|
126
|
-
return { resource: this.finalizeUpload(row, bytes) };
|
|
127
|
-
}
|
|
128
|
-
const bytes = readFileSync(partial);
|
|
129
|
-
const image = inspectImage(bytes, row.media_type);
|
|
130
|
-
if (image.width > SESSION_RESOURCE_MAX_IMAGE_SIDE ||
|
|
131
|
-
image.height > SESSION_RESOURCE_MAX_IMAGE_SIDE ||
|
|
132
|
-
image.width * image.height > SESSION_RESOURCE_MAX_IMAGE_PIXELS) {
|
|
133
|
-
throw invalid("Image dimensions exceed the Session resource policy");
|
|
131
|
+
return { resource: await this.finalizeStoredFile(row, final) };
|
|
134
132
|
}
|
|
133
|
+
const inspected = row.kind === "image"
|
|
134
|
+
? inspectImage(readFileSync(partial), imageMediaType(row.media_type))
|
|
135
|
+
: undefined;
|
|
136
|
+
validateImageDimensions(inspected);
|
|
135
137
|
renameSync(partial, final);
|
|
136
138
|
chmodSync(final, 0o600);
|
|
137
|
-
return { resource: this.
|
|
139
|
+
return { resource: await this.finalizeStoredFile(row, final, inspected) };
|
|
138
140
|
}
|
|
139
141
|
async getResource(input) {
|
|
140
142
|
return { resource: toMetadata(this.requireResource(input.sessionId, input.resourceId)) };
|
|
141
143
|
}
|
|
144
|
+
resolveCommittedLocalPath(sessionId, resourceId) {
|
|
145
|
+
const row = this.requireResource(sessionId, resourceId);
|
|
146
|
+
if (row.state !== "committed")
|
|
147
|
+
throw unavailable("Resource is not committed");
|
|
148
|
+
return this.finalPath(row);
|
|
149
|
+
}
|
|
142
150
|
async readResource(input) {
|
|
143
151
|
const row = this.requireResource(input.sessionId, input.resourceId);
|
|
144
152
|
if (row.state === "uploading")
|
|
@@ -173,7 +181,56 @@ export class SqliteSessionResourceStore {
|
|
|
173
181
|
db().prepare("DELETE FROM session_resources WHERE id = ?").run(row.id);
|
|
174
182
|
return { deleted: true };
|
|
175
183
|
}
|
|
176
|
-
async
|
|
184
|
+
async materializeGeneratedImage(input) {
|
|
185
|
+
const parsed = decodeGeneratedImage(input.result);
|
|
186
|
+
const clientUploadId = `gen_${createHash("sha256")
|
|
187
|
+
.update(`${input.sessionId}\0${input.sourceId}`)
|
|
188
|
+
.digest("hex")}`;
|
|
189
|
+
const begun = await this.beginUpload({
|
|
190
|
+
sessionId: input.sessionId,
|
|
191
|
+
clientUploadId,
|
|
192
|
+
type: "input_image",
|
|
193
|
+
filename: `generated-image.${imageExtension(parsed.mediaType)}`,
|
|
194
|
+
mediaType: parsed.mediaType,
|
|
195
|
+
byteLength: parsed.bytes.byteLength,
|
|
196
|
+
});
|
|
197
|
+
const expectedSha256 = createHash("sha256").update(parsed.bytes).digest("hex");
|
|
198
|
+
if (begun.complete && begun.resource?.sha256 !== expectedSha256) {
|
|
199
|
+
throw conflict("Generated image source changed after it was materialized");
|
|
200
|
+
}
|
|
201
|
+
if (!begun.complete) {
|
|
202
|
+
if (begun.offset > 0) {
|
|
203
|
+
const row = this.requireUpload(input.sessionId, begun.uploadId);
|
|
204
|
+
const existing = readFileSync(this.partialPath(row));
|
|
205
|
+
if (existing.byteLength !== begun.offset ||
|
|
206
|
+
!existing.equals(parsed.bytes.subarray(0, begun.offset))) {
|
|
207
|
+
throw conflict("Generated image source changed while it was being materialized");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
let offset = begun.offset;
|
|
211
|
+
while (offset < parsed.bytes.byteLength) {
|
|
212
|
+
const chunk = parsed.bytes.subarray(offset, Math.min(offset + SESSION_RESOURCE_TRANSFER_CHUNK_BYTES, parsed.bytes.byteLength));
|
|
213
|
+
const written = await this.writeUploadChunk({
|
|
214
|
+
sessionId: input.sessionId,
|
|
215
|
+
uploadId: begun.uploadId,
|
|
216
|
+
offset,
|
|
217
|
+
data: chunk.toString("base64"),
|
|
218
|
+
});
|
|
219
|
+
offset = written.offset;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const metadata = begun.complete && begun.resource
|
|
223
|
+
? begun.resource
|
|
224
|
+
: (await this.commitUpload({
|
|
225
|
+
sessionId: input.sessionId,
|
|
226
|
+
uploadId: begun.uploadId,
|
|
227
|
+
})).resource;
|
|
228
|
+
if (metadata.type !== "input_image")
|
|
229
|
+
throw conflict("Generated image resource changed type");
|
|
230
|
+
const { type: _type, state: _state, sha256: _sha256, createdAt: _createdAt, ...reference } = metadata;
|
|
231
|
+
return reference;
|
|
232
|
+
}
|
|
233
|
+
async prepareInput(sessionId, content, clientMessageId, operationExecution) {
|
|
177
234
|
const runtimeContent = [];
|
|
178
235
|
const canonicalContent = [];
|
|
179
236
|
const resourceIds = [];
|
|
@@ -186,55 +243,118 @@ export class SqliteSessionResourceStore {
|
|
|
186
243
|
title ||= part.text.trim();
|
|
187
244
|
continue;
|
|
188
245
|
}
|
|
246
|
+
if ("path" in part && part.path !== undefined) {
|
|
247
|
+
if (!isAbsolute(part.path))
|
|
248
|
+
throw invalid("File path must be absolute");
|
|
249
|
+
const stats = statSync(part.path);
|
|
250
|
+
if (!stats.isFile())
|
|
251
|
+
throw invalid("File path must reference a regular file");
|
|
252
|
+
const canonical = {
|
|
253
|
+
...part,
|
|
254
|
+
byteLength: part.byteLength ?? stats.size,
|
|
255
|
+
};
|
|
256
|
+
canonicalContent.push(canonical);
|
|
257
|
+
runtimeContent.push({
|
|
258
|
+
type: part.type === "input_image" ? "local_image" : "local_file",
|
|
259
|
+
path: part.path,
|
|
260
|
+
resource: canonical,
|
|
261
|
+
});
|
|
262
|
+
title ||= part.filename ?? basename(part.path);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
189
265
|
const row = this.requireResource(sessionId, part.resourceId);
|
|
190
266
|
if (row.state === "uploading")
|
|
191
|
-
throw unavailable("
|
|
192
|
-
const resource =
|
|
193
|
-
if (resource.
|
|
267
|
+
throw unavailable("Resource upload is incomplete");
|
|
268
|
+
const resource = toResourcePart(row);
|
|
269
|
+
if (resource.type !== part.type ||
|
|
270
|
+
resource.mediaType !== part.mediaType ||
|
|
194
271
|
(part.byteLength !== undefined && part.byteLength !== resource.byteLength)) {
|
|
195
|
-
throw conflict("
|
|
272
|
+
throw conflict("Resource reference metadata does not match the stored resource");
|
|
196
273
|
}
|
|
197
|
-
|
|
274
|
+
if (row.kind === "image")
|
|
275
|
+
totalImageBytes += row.byte_length;
|
|
198
276
|
resourceIds.push(row.id);
|
|
199
277
|
canonicalContent.push(resource);
|
|
200
278
|
runtimeContent.push({
|
|
201
|
-
type: "local_image",
|
|
279
|
+
type: resource.type === "input_image" ? "local_image" : "local_file",
|
|
202
280
|
path: this.finalPath(row),
|
|
203
|
-
resource,
|
|
281
|
+
resource: resource,
|
|
204
282
|
});
|
|
205
283
|
title ||= row.filename;
|
|
206
284
|
}
|
|
207
285
|
if (runtimeContent.length === 0 || totalImageBytes > SESSION_RESOURCE_MAX_MESSAGE_BYTES) {
|
|
208
286
|
throw invalid("Message content is empty or exceeds the image byte limit");
|
|
209
287
|
}
|
|
210
|
-
if (!runtimeContent.some((part) => part.type
|
|
211
|
-
throw invalid("Message content must contain text or
|
|
288
|
+
if (!runtimeContent.some((part) => part.type !== "text" || part.text.trim())) {
|
|
289
|
+
throw invalid("Message content must contain text or a file");
|
|
212
290
|
}
|
|
213
291
|
let operationState;
|
|
214
292
|
if (clientMessageId) {
|
|
215
|
-
const requestHash = hashMessageContent(canonicalContent);
|
|
216
|
-
|
|
293
|
+
const requestHash = hashMessageContent(canonicalContent, operationExecution?.selection);
|
|
294
|
+
const operation = this.prepareMessageOperation(sessionId, clientMessageId, requestHash, resourceIds, operationExecution?.snapshot);
|
|
295
|
+
operationState = operation.state;
|
|
296
|
+
return {
|
|
297
|
+
input: {
|
|
298
|
+
content: runtimeContent,
|
|
299
|
+
clientMessageId,
|
|
300
|
+
},
|
|
301
|
+
title: title || "File",
|
|
302
|
+
operationState,
|
|
303
|
+
...(operation.responseId ? { responseId: operation.responseId } : {}),
|
|
304
|
+
...(operation.messageItemId ? { messageItemId: operation.messageItemId } : {}),
|
|
305
|
+
...(operation.execution ? { operationExecution: operation.execution } : {}),
|
|
306
|
+
};
|
|
217
307
|
}
|
|
218
308
|
return {
|
|
219
309
|
input: {
|
|
220
310
|
content: runtimeContent,
|
|
221
311
|
...(clientMessageId ? { clientMessageId } : {}),
|
|
222
312
|
},
|
|
223
|
-
title: title || "
|
|
313
|
+
title: title || "File",
|
|
224
314
|
...(operationState ? { operationState } : {}),
|
|
225
315
|
};
|
|
226
316
|
}
|
|
227
|
-
markMessageInjecting(sessionId, clientMessageId) {
|
|
228
|
-
|
|
317
|
+
markMessageInjecting(sessionId, clientMessageId, identity) {
|
|
318
|
+
const current = db().prepare(`SELECT state, response_id, message_item_id
|
|
319
|
+
FROM session_message_operations
|
|
320
|
+
WHERE session_id = ? AND client_message_id = ?`).get(sessionId, clientMessageId);
|
|
321
|
+
if (!current ||
|
|
322
|
+
(identity && current.response_id && current.response_id !== identity.responseId) ||
|
|
323
|
+
(identity && current.message_item_id && current.message_item_id !== identity.messageItemId)) {
|
|
324
|
+
throw conflict("Message operation identity changed");
|
|
325
|
+
}
|
|
326
|
+
const result = db().prepare(`UPDATE session_message_operations
|
|
327
|
+
SET state = 'injecting', error = NULL,
|
|
328
|
+
response_id = COALESCE(response_id, ?),
|
|
329
|
+
message_item_id = COALESCE(message_item_id, ?),
|
|
330
|
+
updated_at = ?
|
|
331
|
+
WHERE session_id = ? AND client_message_id = ?
|
|
332
|
+
AND state IN ('prepared', 'failed_not_started')`).run(identity?.responseId ?? null, identity?.messageItemId ?? null, Date.now(), sessionId, clientMessageId);
|
|
333
|
+
if (result.changes === 0 && current.state !== "injecting" && current.state !== "mirrored") {
|
|
334
|
+
throw conflict("Message operation state changed");
|
|
335
|
+
}
|
|
229
336
|
}
|
|
230
|
-
markMessageInjected(sessionId, clientMessageId) {
|
|
231
|
-
|
|
337
|
+
markMessageInjected(sessionId, clientMessageId, identity) {
|
|
338
|
+
const current = db().prepare(`SELECT state, response_id
|
|
339
|
+
FROM session_message_operations
|
|
340
|
+
WHERE session_id = ? AND client_message_id = ?`).get(sessionId, clientMessageId);
|
|
341
|
+
if (!current ||
|
|
342
|
+
(identity && current.response_id && current.response_id !== identity.responseId)) {
|
|
343
|
+
throw conflict("Message operation identity changed");
|
|
344
|
+
}
|
|
345
|
+
const result = db().prepare(`UPDATE session_message_operations
|
|
346
|
+
SET state = 'injected', error = NULL,
|
|
347
|
+
response_id = COALESCE(response_id, ?),
|
|
348
|
+
updated_at = ?
|
|
349
|
+
WHERE session_id = ? AND client_message_id = ? AND state = 'injecting'`).run(identity?.responseId ?? null, Date.now(), sessionId, clientMessageId);
|
|
350
|
+
if (result.changes !== 1)
|
|
351
|
+
throw conflict("Message operation state changed");
|
|
232
352
|
}
|
|
233
353
|
markMessageOutcomeUnknown(sessionId, clientMessageId, reason) {
|
|
234
354
|
this.transitionOperation(sessionId, clientMessageId, ["injecting"], "outcome_unknown", reason);
|
|
235
355
|
}
|
|
236
356
|
markMessageFailedNotStarted(sessionId, clientMessageId, reason) {
|
|
237
|
-
this.transitionOperation(sessionId, clientMessageId, ["prepared", "injecting"], "failed_not_started", reason);
|
|
357
|
+
this.transitionOperation(sessionId, clientMessageId, ["prepared", "injecting", "mirrored"], "failed_not_started", reason);
|
|
238
358
|
}
|
|
239
359
|
deleteSession(sessionId) {
|
|
240
360
|
const rows = db()
|
|
@@ -244,37 +364,51 @@ export class SqliteSessionResourceStore {
|
|
|
244
364
|
rmSync(this.partialPath(row), { force: true });
|
|
245
365
|
rmSync(this.finalPath(row), { force: true });
|
|
246
366
|
}
|
|
247
|
-
db()
|
|
367
|
+
transaction(db(), () => {
|
|
248
368
|
db().prepare("DELETE FROM session_message_operations WHERE session_id = ?").run(sessionId);
|
|
249
369
|
db().prepare("DELETE FROM session_resources WHERE session_id = ?").run(sessionId);
|
|
250
370
|
})();
|
|
251
371
|
rmSync(this.sessionDirectory(sessionId), { recursive: true, force: true });
|
|
252
372
|
}
|
|
253
|
-
prepareMessageOperation(sessionId, clientMessageId, requestHash, resourceIds) {
|
|
373
|
+
prepareMessageOperation(sessionId, clientMessageId, requestHash, resourceIds, execution) {
|
|
254
374
|
const conn = db();
|
|
255
|
-
return
|
|
256
|
-
const existing = conn.prepare(`SELECT request_hash, state
|
|
375
|
+
return transaction(conn, () => {
|
|
376
|
+
const existing = conn.prepare(`SELECT request_hash, state, response_id, message_item_id, execution_snapshot
|
|
257
377
|
FROM session_message_operations
|
|
258
378
|
WHERE session_id = ? AND client_message_id = ?`).get(sessionId, clientMessageId);
|
|
259
379
|
if (existing) {
|
|
380
|
+
const storedExecution = parseOperationExecution(existing.execution_snapshot);
|
|
260
381
|
if (existing.request_hash !== requestHash) {
|
|
261
382
|
throw conflict("clientMessageId was already used for different content");
|
|
262
383
|
}
|
|
263
384
|
if (existing.state === "failed_not_started") {
|
|
385
|
+
const retryExecution = storedExecution ?? execution;
|
|
264
386
|
this.bindOperationResources(conn, sessionId, clientMessageId, resourceIds);
|
|
265
387
|
conn.prepare(`UPDATE session_message_operations
|
|
266
|
-
SET state = 'prepared', error = NULL,
|
|
267
|
-
|
|
268
|
-
|
|
388
|
+
SET state = 'prepared', error = NULL,
|
|
389
|
+
execution_snapshot = COALESCE(execution_snapshot, ?), updated_at = ?
|
|
390
|
+
WHERE session_id = ? AND client_message_id = ?`).run(execution ? JSON.stringify(execution) : null, Date.now(), sessionId, clientMessageId);
|
|
391
|
+
return {
|
|
392
|
+
state: "prepared",
|
|
393
|
+
...(existing.response_id ? { responseId: existing.response_id } : {}),
|
|
394
|
+
...(existing.message_item_id ? { messageItemId: existing.message_item_id } : {}),
|
|
395
|
+
...(retryExecution ? { execution: retryExecution } : {}),
|
|
396
|
+
};
|
|
269
397
|
}
|
|
270
|
-
return
|
|
398
|
+
return {
|
|
399
|
+
state: existing.state,
|
|
400
|
+
...(existing.response_id ? { responseId: existing.response_id } : {}),
|
|
401
|
+
...(existing.message_item_id ? { messageItemId: existing.message_item_id } : {}),
|
|
402
|
+
...(storedExecution ? { execution: storedExecution } : {}),
|
|
403
|
+
};
|
|
271
404
|
}
|
|
272
405
|
const now = Date.now();
|
|
273
406
|
conn.prepare(`INSERT INTO session_message_operations
|
|
274
|
-
(session_id, client_message_id, request_hash, state,
|
|
275
|
-
|
|
407
|
+
(session_id, client_message_id, request_hash, state, execution_snapshot,
|
|
408
|
+
created_at, updated_at)
|
|
409
|
+
VALUES (?, ?, ?, 'prepared', ?, ?, ?)`).run(sessionId, clientMessageId, requestHash, execution ? JSON.stringify(execution) : null, now, now);
|
|
276
410
|
this.bindOperationResources(conn, sessionId, clientMessageId, resourceIds);
|
|
277
|
-
return "prepared";
|
|
411
|
+
return { state: "prepared", ...(execution ? { execution } : {}) };
|
|
278
412
|
})();
|
|
279
413
|
}
|
|
280
414
|
bindOperationResources(conn, sessionId, clientMessageId, resourceIds) {
|
|
@@ -317,12 +451,12 @@ export class SqliteSessionResourceStore {
|
|
|
317
451
|
throw conflict("Message operation state changed");
|
|
318
452
|
}
|
|
319
453
|
}
|
|
320
|
-
|
|
321
|
-
const digest =
|
|
454
|
+
async finalizeStoredFile(row, file, inspected) {
|
|
455
|
+
const digest = await hashFile(file);
|
|
322
456
|
db().prepare(`UPDATE session_resources
|
|
323
457
|
SET state = 'staged', uploaded_bytes = byte_length, sha256 = ?,
|
|
324
458
|
width = ?, height = ?
|
|
325
|
-
WHERE id = ? AND state = 'uploading'`).run(digest, inspected
|
|
459
|
+
WHERE id = ? AND state = 'uploading'`).run(digest, inspected?.width ?? null, inspected?.height ?? null, row.id);
|
|
326
460
|
return toMetadata(this.requireResource(row.session_id, row.id));
|
|
327
461
|
}
|
|
328
462
|
reconcileUploadOffset(row) {
|
|
@@ -368,7 +502,7 @@ export class SqliteSessionResourceStore {
|
|
|
368
502
|
return join(this.sessionDirectory(row.session_id), `${row.id}.part`);
|
|
369
503
|
}
|
|
370
504
|
finalPath(row) {
|
|
371
|
-
return join(this.sessionDirectory(row.session_id), `${row.id}.${
|
|
505
|
+
return join(this.sessionDirectory(row.session_id), `${row.id}.${resourceExtension(row)}`);
|
|
372
506
|
}
|
|
373
507
|
sweepExpiredStagedResources(now = Date.now()) {
|
|
374
508
|
const rows = db()
|
|
@@ -382,17 +516,25 @@ export class SqliteSessionResourceStore {
|
|
|
382
516
|
}
|
|
383
517
|
}
|
|
384
518
|
}
|
|
385
|
-
function
|
|
386
|
-
if (row.state === "uploading" ||
|
|
387
|
-
!row.sha256 ||
|
|
388
|
-
!row.width ||
|
|
389
|
-
!row.height) {
|
|
519
|
+
function toResourcePart(row) {
|
|
520
|
+
if (row.state === "uploading" || !row.sha256) {
|
|
390
521
|
throw unavailable("Resource upload is incomplete");
|
|
391
522
|
}
|
|
523
|
+
if (row.kind === "file") {
|
|
524
|
+
return {
|
|
525
|
+
type: "input_file",
|
|
526
|
+
resourceId: row.id,
|
|
527
|
+
mediaType: row.media_type,
|
|
528
|
+
filename: row.filename,
|
|
529
|
+
byteLength: row.byte_length,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
if (!row.width || !row.height)
|
|
533
|
+
throw unavailable("Image upload is incomplete");
|
|
392
534
|
return {
|
|
393
535
|
type: "input_image",
|
|
394
536
|
resourceId: row.id,
|
|
395
|
-
mediaType: row.media_type,
|
|
537
|
+
mediaType: imageMediaType(row.media_type),
|
|
396
538
|
filename: row.filename,
|
|
397
539
|
byteLength: row.byte_length,
|
|
398
540
|
width: row.width,
|
|
@@ -400,7 +542,7 @@ function toImagePart(row) {
|
|
|
400
542
|
};
|
|
401
543
|
}
|
|
402
544
|
function toMetadata(row) {
|
|
403
|
-
const part =
|
|
545
|
+
const part = toResourcePart(row);
|
|
404
546
|
if (row.state === "uploading")
|
|
405
547
|
throw unavailable("Resource upload is incomplete");
|
|
406
548
|
return {
|
|
@@ -410,13 +552,38 @@ function toMetadata(row) {
|
|
|
410
552
|
createdAt: row.created_at,
|
|
411
553
|
};
|
|
412
554
|
}
|
|
413
|
-
function hashMessageContent(content) {
|
|
414
|
-
|
|
415
|
-
.update(JSON.stringify(content.map((part) => part.type === "input_text"
|
|
555
|
+
function hashMessageContent(content, execution) {
|
|
556
|
+
const canonicalContent = content.map((part) => part.type === "input_text"
|
|
416
557
|
? { type: part.type, text: part.text }
|
|
417
|
-
:
|
|
558
|
+
: "resourceId" in part
|
|
559
|
+
? { type: part.type, resourceId: part.resourceId }
|
|
560
|
+
: { type: part.type, path: part.path });
|
|
561
|
+
const hasExecutionSelection = execution && (Object.prototype.hasOwnProperty.call(execution, "model") ||
|
|
562
|
+
Object.prototype.hasOwnProperty.call(execution, "reasoningEffort"));
|
|
563
|
+
return createHash("sha256")
|
|
564
|
+
.update(JSON.stringify(hasExecutionSelection
|
|
565
|
+
? { content: canonicalContent, execution }
|
|
566
|
+
: canonicalContent))
|
|
418
567
|
.digest("hex");
|
|
419
568
|
}
|
|
569
|
+
function parseOperationExecution(value) {
|
|
570
|
+
if (!value)
|
|
571
|
+
return undefined;
|
|
572
|
+
try {
|
|
573
|
+
const parsed = JSON.parse(value);
|
|
574
|
+
if ((parsed.model !== null && typeof parsed.model !== "string") ||
|
|
575
|
+
(parsed.reasoningEffort !== null && typeof parsed.reasoningEffort !== "string")) {
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
model: parsed.model,
|
|
580
|
+
reasoningEffort: parsed.reasoningEffort,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
return undefined;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
420
587
|
function inspectImage(bytes, declared) {
|
|
421
588
|
const sniffed = sniffMediaType(bytes);
|
|
422
589
|
if (sniffed !== declared)
|
|
@@ -425,14 +592,22 @@ function inspectImage(bytes, declared) {
|
|
|
425
592
|
? validatePngContainer(bytes)
|
|
426
593
|
: sniffed === "image/jpeg"
|
|
427
594
|
? validateJpegContainer(bytes)
|
|
428
|
-
:
|
|
595
|
+
: sniffed === "image/webp"
|
|
596
|
+
? validateWebpContainer(bytes)
|
|
597
|
+
: sniffed === "image/gif"
|
|
598
|
+
? validateGifContainer(bytes)
|
|
599
|
+
: validateSvgContainer(bytes);
|
|
429
600
|
if (!structurallyValid)
|
|
430
601
|
throw invalid("Image file is incomplete or malformed");
|
|
431
602
|
const dimensions = sniffed === "image/png"
|
|
432
603
|
? pngDimensions(bytes)
|
|
433
604
|
: sniffed === "image/jpeg"
|
|
434
605
|
? jpegDimensions(bytes)
|
|
435
|
-
:
|
|
606
|
+
: sniffed === "image/webp"
|
|
607
|
+
? webpDimensions(bytes)
|
|
608
|
+
: sniffed === "image/gif"
|
|
609
|
+
? gifDimensions(bytes)
|
|
610
|
+
: svgDimensions(bytes);
|
|
436
611
|
if (!dimensions || dimensions.width <= 0 || dimensions.height <= 0) {
|
|
437
612
|
throw invalid("Image dimensions could not be decoded");
|
|
438
613
|
}
|
|
@@ -590,8 +765,67 @@ function sniffMediaType(bytes) {
|
|
|
590
765
|
bytes.toString("ascii", 0, 4) === "RIFF" &&
|
|
591
766
|
bytes.toString("ascii", 8, 12) === "WEBP")
|
|
592
767
|
return "image/webp";
|
|
768
|
+
if (bytes.length >= 10 &&
|
|
769
|
+
(bytes.toString("ascii", 0, 6) === "GIF87a" || bytes.toString("ascii", 0, 6) === "GIF89a"))
|
|
770
|
+
return "image/gif";
|
|
771
|
+
try {
|
|
772
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes).trimStart();
|
|
773
|
+
if (/^(?:<\?xml[^>]*>\s*)?(?:<!--[^]*?-->\s*)*<svg\b/i.test(text)) {
|
|
774
|
+
return "image/svg+xml";
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
catch {
|
|
778
|
+
// Not UTF-8 SVG.
|
|
779
|
+
}
|
|
593
780
|
return undefined;
|
|
594
781
|
}
|
|
782
|
+
function validateGifContainer(bytes) {
|
|
783
|
+
return bytes.length >= 14 && bytes.at(-1) === 0x3b;
|
|
784
|
+
}
|
|
785
|
+
function gifDimensions(bytes) {
|
|
786
|
+
if (bytes.length < 10)
|
|
787
|
+
return undefined;
|
|
788
|
+
return { width: bytes.readUInt16LE(6), height: bytes.readUInt16LE(8) };
|
|
789
|
+
}
|
|
790
|
+
function svgText(bytes) {
|
|
791
|
+
try {
|
|
792
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
793
|
+
}
|
|
794
|
+
catch {
|
|
795
|
+
return undefined;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
function validateSvgContainer(bytes) {
|
|
799
|
+
const text = svgText(bytes)?.trim();
|
|
800
|
+
if (!text)
|
|
801
|
+
return false;
|
|
802
|
+
const withoutProlog = text
|
|
803
|
+
.replace(/^<\?xml[^>]*>\s*/i, "")
|
|
804
|
+
.replace(/^(?:<!--[^]*?-->\s*)*/i, "");
|
|
805
|
+
return /^<svg\b/i.test(withoutProlog) && (/<\/svg>\s*$/i.test(text) || /\/\>\s*$/.test(text));
|
|
806
|
+
}
|
|
807
|
+
function svgDimensions(bytes) {
|
|
808
|
+
const open = svgText(bytes)?.match(/<svg\b([^>]*)>/i)?.[1];
|
|
809
|
+
if (open === undefined)
|
|
810
|
+
return undefined;
|
|
811
|
+
const width = numericSvgLength(open.match(/\bwidth\s*=\s*["']([^"']+)["']/i)?.[1]);
|
|
812
|
+
const height = numericSvgLength(open.match(/\bheight\s*=\s*["']([^"']+)["']/i)?.[1]);
|
|
813
|
+
if (width && height)
|
|
814
|
+
return { width, height };
|
|
815
|
+
const viewBox = open.match(/\bviewBox\s*=\s*["']([^"']+)["']/i)?.[1]
|
|
816
|
+
?.trim().split(/[\s,]+/).map(Number);
|
|
817
|
+
if (viewBox?.length === 4 && viewBox.every(Number.isFinite) && viewBox[2] > 0 && viewBox[3] > 0) {
|
|
818
|
+
return { width: Math.ceil(viewBox[2]), height: Math.ceil(viewBox[3]) };
|
|
819
|
+
}
|
|
820
|
+
return { width: width ?? 300, height: height ?? 150 };
|
|
821
|
+
}
|
|
822
|
+
function numericSvgLength(value) {
|
|
823
|
+
const match = value?.trim().match(/^(\d+(?:\.\d+)?)(?:px)?$/i);
|
|
824
|
+
if (!match)
|
|
825
|
+
return undefined;
|
|
826
|
+
const number = Number(match[1]);
|
|
827
|
+
return Number.isFinite(number) && number > 0 ? Math.ceil(number) : undefined;
|
|
828
|
+
}
|
|
595
829
|
function pngDimensions(bytes) {
|
|
596
830
|
if (bytes.toString("ascii", 12, 16) !== "IHDR")
|
|
597
831
|
return undefined;
|
|
@@ -665,13 +899,75 @@ function decodeBase64(value) {
|
|
|
665
899
|
}
|
|
666
900
|
return Buffer.from(value, "base64");
|
|
667
901
|
}
|
|
668
|
-
function
|
|
669
|
-
return
|
|
902
|
+
function resourceKind(type) {
|
|
903
|
+
return type === "input_image" ? "image" : "file";
|
|
904
|
+
}
|
|
905
|
+
function imageMediaType(mediaType) {
|
|
906
|
+
switch (mediaType) {
|
|
907
|
+
case "image/png":
|
|
908
|
+
case "image/jpeg":
|
|
909
|
+
case "image/webp":
|
|
910
|
+
case "image/gif":
|
|
911
|
+
case "image/svg+xml":
|
|
912
|
+
return mediaType;
|
|
913
|
+
default:
|
|
914
|
+
throw invalid("Image media type is invalid");
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
function resourceExtension(row) {
|
|
918
|
+
if (row.kind === "image") {
|
|
919
|
+
switch (imageMediaType(row.media_type)) {
|
|
920
|
+
case "image/png": return "png";
|
|
921
|
+
case "image/jpeg": return "jpg";
|
|
922
|
+
case "image/webp": return "webp";
|
|
923
|
+
case "image/gif": return "gif";
|
|
924
|
+
case "image/svg+xml": return "svg";
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
const extension = extname(row.filename).slice(1).toLowerCase();
|
|
928
|
+
return /^[a-z0-9][a-z0-9._-]{0,15}$/.test(extension) ? extension : "bin";
|
|
929
|
+
}
|
|
930
|
+
function decodeGeneratedImage(result) {
|
|
931
|
+
const dataUrl = /^data:(image\/[a-z0-9+.-]+);base64,([A-Za-z0-9+/=]+)$/i.exec(result);
|
|
932
|
+
const encoded = dataUrl?.[2] ?? result;
|
|
933
|
+
const bytes = decodeBase64(encoded);
|
|
934
|
+
if (bytes.byteLength === 0 || bytes.byteLength > SESSION_RESOURCE_MAX_IMAGE_BYTES) {
|
|
935
|
+
throw invalid("Generated image exceeds the Session image resource policy");
|
|
936
|
+
}
|
|
937
|
+
const mediaType = sniffMediaType(bytes);
|
|
938
|
+
if (!mediaType || (dataUrl && imageMediaType(dataUrl[1].toLowerCase()) !== mediaType)) {
|
|
939
|
+
throw invalid("Generated image bytes are invalid");
|
|
940
|
+
}
|
|
941
|
+
return { bytes, mediaType };
|
|
942
|
+
}
|
|
943
|
+
function imageExtension(mediaType) {
|
|
944
|
+
switch (mediaType) {
|
|
945
|
+
case "image/png": return "png";
|
|
946
|
+
case "image/jpeg": return "jpg";
|
|
947
|
+
case "image/webp": return "webp";
|
|
948
|
+
case "image/gif": return "gif";
|
|
949
|
+
case "image/svg+xml": return "svg";
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
function validateImageDimensions(image) {
|
|
953
|
+
if (!image)
|
|
954
|
+
return;
|
|
955
|
+
if (image.width > SESSION_RESOURCE_MAX_IMAGE_SIDE ||
|
|
956
|
+
image.height > SESSION_RESOURCE_MAX_IMAGE_SIDE ||
|
|
957
|
+
image.width * image.height > SESSION_RESOURCE_MAX_IMAGE_PIXELS) {
|
|
958
|
+
throw invalid("Image dimensions exceed the Session resource policy");
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async function hashFile(path) {
|
|
962
|
+
const hash = createHash("sha256");
|
|
963
|
+
for await (const chunk of createReadStream(path))
|
|
964
|
+
hash.update(chunk);
|
|
965
|
+
return hash.digest("hex");
|
|
670
966
|
}
|
|
671
967
|
function safeFilename(value) {
|
|
672
968
|
const name = basename(value).replace(/[\u0000-\u001f\u007f]/g, "").trim();
|
|
673
969
|
if (!name || name === "." || name === "..")
|
|
674
|
-
throw invalid("
|
|
970
|
+
throw invalid("Resource filename is invalid");
|
|
675
971
|
return name;
|
|
676
972
|
}
|
|
677
973
|
function ensurePrivateDirectory(path) {
|
package/dist/setup-service.d.ts
CHANGED
package/dist/setup-service.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createChromeForTestingArtifactStore, isChromeForTestingHostSupported }
|
|
|
4
4
|
import { ensureBundledOfficialPlugins } from "./bundled-plugins.js";
|
|
5
5
|
import { dbPath } from "./db.js";
|
|
6
6
|
import { listPlugins } from "./plugin-store.js";
|
|
7
|
+
export { inspectSystemDependencies, } from "./system-dependencies.js";
|
|
7
8
|
export async function prepareBundledPlugins() {
|
|
8
9
|
return ensureBundledOfficialPlugins();
|
|
9
10
|
}
|
package/dist/skills-catalog.d.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import { type SkillInstallRecipe, type SkillMeta } from "@rynx-ai/core";
|
|
2
|
-
import type { SkillDetail } from "@rynx-ai/protocol/control";
|
|
1
|
+
import { type LocalSkillRuntime, type SkillInstallRecipe, type SkillMeta } from "@rynx-ai/core";
|
|
2
|
+
import type { ControlLocalSkillCandidate, ControlLocalSkillImportResult, SkillDetail } from "@rynx-ai/protocol/control";
|
|
3
3
|
export declare function listCatalogSkills(): Promise<SkillMeta[]>;
|
|
4
4
|
/** Install a skill (or a repo's skills) into the catalog per a full install
|
|
5
5
|
* recipe; delegates to the core catalog layer (which also writes each skill's
|
|
6
6
|
* `.rynx-skill.json` install-info file — a catalog-only artifact). Returns
|
|
7
7
|
* the refreshed catalog. */
|
|
8
8
|
export declare function installCatalogSkill(recipe: SkillInstallRecipe): Promise<SkillMeta[]>;
|
|
9
|
+
export declare function scanLocalCatalogSkillCandidates(runtime: LocalSkillRuntime): Promise<ControlLocalSkillCandidate[]>;
|
|
10
|
+
/**
|
|
11
|
+
* Register complete local runtime Skill bundles into the owner catalog. Every
|
|
12
|
+
* content hash gets a durable immutable artifact; the mutable catalog entry's
|
|
13
|
+
* receipt points at that artifact so old Agent specs keep their exact bytes
|
|
14
|
+
* after a later catalog update.
|
|
15
|
+
*/
|
|
16
|
+
export declare function importLocalCatalogSkills(runtime: LocalSkillRuntime, keys: string[]): Promise<ControlLocalSkillImportResult[]>;
|
|
17
|
+
export declare function importCatalogSkillsFromArchive(filename: string, contentBase64: string): Promise<ControlLocalSkillImportResult[]>;
|
|
9
18
|
/** Detail for one catalog skill: its metadata + the file tree under its dir. */
|
|
10
19
|
export declare function getCatalogSkill(name: string): Promise<SkillDetail | null>;
|
|
11
20
|
/** Read one text file inside a catalog skill. Path-confined to the skill dir,
|