@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.
Files changed (61) hide show
  1. package/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +0 -12
  2. package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.js +3 -1
  3. package/bundled-plugins/plugins/lark/dist/host-adapters.d.ts +3 -3
  4. package/bundled-plugins/plugins/lark/dist/host-adapters.js +24 -18
  5. package/bundled-plugins/plugins/lark/dist/lark.js +18 -12
  6. package/bundled-plugins/plugins/lark/dist/runtime.js +6046 -6018
  7. package/bundled-plugins/plugins/lark/dist/settings.js +6006 -5987
  8. package/bundled-plugins/plugins/lark/package.json +1 -1
  9. package/bundled-plugins/plugins/lark/ui/assets/index-CGUVAMsv.css +1 -0
  10. package/bundled-plugins/plugins/lark/ui/index.html +2 -2
  11. package/dist/agent-file.js +0 -1
  12. package/dist/browser-artifact-management.js +4 -1
  13. package/dist/chrome-for-testing-runtime.js +4 -3
  14. package/dist/chrome-for-testing-store.js +29 -7
  15. package/dist/control-deps.d.ts +6 -2
  16. package/dist/control-deps.js +87 -4
  17. package/dist/control-link-store.js +4 -3
  18. package/dist/daemon-owner.js +4 -7
  19. package/dist/daemon-server.js +25 -14
  20. package/dist/db.d.ts +2 -2
  21. package/dist/db.js +133 -21
  22. package/dist/lifecycle.d.ts +1 -0
  23. package/dist/lifecycle.js +3 -0
  24. package/dist/migrations/cleanup-legacy-sessions.js +4 -3
  25. package/dist/plugin-cli.d.ts +10 -0
  26. package/dist/plugin-cli.js +19 -2
  27. package/dist/plugin-host-rpc.d.ts +31 -9
  28. package/dist/plugin-host-rpc.js +428 -80
  29. package/dist/plugin-installer.js +3 -2
  30. package/dist/plugin-management-service.js +0 -2
  31. package/dist/plugin-marketplace.js +2 -1
  32. package/dist/plugin-store.js +2 -4
  33. package/dist/plugin-supervisor.js +0 -4
  34. package/dist/remote-runtime-access-store.d.ts +2 -2
  35. package/dist/remote-runtime-access-store.js +6 -9
  36. package/dist/remote-runtime-connection-manager.js +1 -1
  37. package/dist/remote-runtime-target-store.d.ts +2 -2
  38. package/dist/remote-runtime-target-store.js +6 -6
  39. package/dist/session-emulator-binding-store.js +4 -6
  40. package/dist/session-fork-store.d.ts +1 -0
  41. package/dist/session-fork-store.js +82 -28
  42. package/dist/session-launch-operations.js +6 -5
  43. package/dist/session-log-store.d.ts +1 -0
  44. package/dist/session-log-store.js +19 -16
  45. package/dist/session-meta-store.d.ts +5 -0
  46. package/dist/session-meta-store.js +25 -2
  47. package/dist/session-portal-grant-store.d.ts +29 -0
  48. package/dist/session-portal-grant-store.js +85 -0
  49. package/dist/session-resource-store.d.ts +20 -6
  50. package/dist/session-resource-store.js +364 -68
  51. package/dist/setup-service.d.ts +1 -0
  52. package/dist/setup-service.js +1 -0
  53. package/dist/skills-catalog.d.ts +11 -2
  54. package/dist/skills-catalog.js +317 -10
  55. package/dist/sqlite.d.ts +22 -0
  56. package/dist/sqlite.js +57 -0
  57. package/dist/system-dependencies.d.ts +22 -0
  58. package/dist/system-dependencies.js +79 -0
  59. package/package.json +15 -12
  60. package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +0 -1
  61. /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.filename !== filename ||
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, COALESCE(SUM(byte_length), 0) AS bytes
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 image quota exceeded");
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
- const bytes = readFileSync(final);
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.finalizeUpload(row, bytes, image) };
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 prepareInput(sessionId, content, clientMessageId) {
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("Image upload is incomplete");
192
- const resource = toImagePart(row);
193
- if (resource.mediaType !== part.mediaType ||
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("Image reference metadata does not match the stored resource");
272
+ throw conflict("Resource reference metadata does not match the stored resource");
196
273
  }
197
- totalImageBytes += row.byte_length;
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 === "local_image" || part.text.trim())) {
211
- throw invalid("Message content must contain text or an image");
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
- operationState = this.prepareMessageOperation(sessionId, clientMessageId, requestHash, resourceIds);
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 || "Image",
313
+ title: title || "File",
224
314
  ...(operationState ? { operationState } : {}),
225
315
  };
226
316
  }
227
- markMessageInjecting(sessionId, clientMessageId) {
228
- this.transitionOperation(sessionId, clientMessageId, ["prepared", "failed_not_started"], "injecting");
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
- this.transitionOperation(sessionId, clientMessageId, ["injecting"], "injected");
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().transaction(() => {
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 conn.transaction(() => {
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, updated_at = ?
267
- WHERE session_id = ? AND client_message_id = ?`).run(Date.now(), sessionId, clientMessageId);
268
- return "prepared";
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 existing.state;
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, created_at, updated_at)
275
- VALUES (?, ?, ?, 'prepared', ?, ?)`).run(sessionId, clientMessageId, requestHash, now, now);
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
- finalizeUpload(row, bytes, inspected = inspectImage(bytes, row.media_type)) {
321
- const digest = createHash("sha256").update(bytes).digest("hex");
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.width, inspected.height, row.id);
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}.${extension(row.media_type)}`);
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 toImagePart(row) {
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 = toImagePart(row);
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
- return createHash("sha256")
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
- : { type: part.type, resourceId: part.resourceId })))
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
- : validateWebpContainer(bytes);
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
- : webpDimensions(bytes);
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 extension(mediaType) {
669
- return mediaType === "image/png" ? "png" : mediaType === "image/jpeg" ? "jpg" : "webp";
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("Image filename is invalid");
970
+ throw invalid("Resource filename is invalid");
675
971
  return name;
676
972
  }
677
973
  function ensurePrivateDirectory(path) {
@@ -1,3 +1,4 @@
1
+ export { inspectSystemDependencies, type SystemDependencySnapshot, } from "./system-dependencies.js";
1
2
  export interface DaemonDiagnosticSnapshot {
2
3
  databasePath: string;
3
4
  databaseExists: boolean;
@@ -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
  }
@@ -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,