@axiom-lattice/core 7.0.3 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -7016,7 +7016,13 @@ If the Worker has task capability, it may truthfully update the same task.
7016
7016
  Otherwise, the owning caller interprets the returned observation.
7017
7017
 
7018
7018
  An execution result is an observation, not automatic proof of business
7019
- completion. \`task\` returns its exact \`executionResultId\`. After \`task\`
7019
+ completion. \`task\` returns its exact \`executionResultId\` and the task's
7020
+ \`taskStatus\` (re-read **after** the worker settled). **Check \`taskStatus\`
7021
+ before completing:** if it is already \`completed\` the task is done \u2014 do NOT
7022
+ complete it again (use \`add_activity\` for extra notes); if it is still
7023
+ \`in_progress\` you are the single completer and may \`set_status completed\`;
7024
+ if it is \`failed\` or \`interrupted\`, act on that (retry / resume / new task)
7025
+ instead. After \`task\`
7020
7026
  returns, inspect the observation, then pass that \`executionResultId\` to the
7021
7027
  meaningful \`manage_task set_status\` (status transition), \`manage_task
7022
7028
  update\` (content fields), or \`manage_task add_activity\` that interprets
@@ -7060,7 +7066,11 @@ handled by the configured HITL lifecycle; \`review_required\` is not a task stat
7060
7066
  Create the active parent and each started subtask as records: \`manage_task
7061
7067
  create\` always persists \`pending\`. **Process the task you were assigned in this
7062
7068
  run: set it \`in_progress\` with \`manage_task set_status\` when you start (a
7063
- dispatched task is claimed automatically by the engine).** To bring
7069
+ dispatched task is claimed automatically by the engine).** \`in_progress\` means a
7070
+ run is executing that task: set it yourself ONLY for the task THIS run executes,
7071
+ and for a task you delegate leave it \`pending\` \u2014 \`task\` dispatch claims it.
7072
+ Complete a task exactly once: if it is already \`completed\`, do NOT complete it
7073
+ again (the terminal state stands; use \`add_activity\` for extra notes). To bring
7064
7074
  in a more suitable expert, create a subtask (\`pending\`) and dispatch THAT with
7065
7075
  \`task { taskId, agentId }\` (a specialist, or agentId "general-purpose" to run your
7066
7076
  own capabilities in an isolated run). **Never re-dispatch the task you are
@@ -8626,7 +8636,9 @@ criteria are truly met \u2014 never as a workaround.
8626
8636
  \`manage_task create(title: <goal>, description: <contract + Belief State>, ownerType: "agent")\`
8627
8637
  \`create\` always yields a \`pending\` record \u2014 it never starts execution. Record
8628
8638
  the returned parent task id, then **process it in this run**, setting it
8629
- \`in_progress\` with \`manage_task set_status\` when you start. To decompose, create each subtask
8639
+ \`in_progress\` with \`manage_task set_status\` when you start. Set \`in_progress\`
8640
+ only for a task THIS run executes; a task you delegate stays \`pending\` and is
8641
+ claimed by \`task\` dispatch. To decompose, create each subtask
8630
8642
  with \`manage_task create\` (\`pending\`) and delegate it with \`task { taskId,
8631
8643
  agentId }\` to the agent that should execute it (a specialist, or
8632
8644
  "general-purpose" for your own capabilities). The executor does not need
@@ -8659,10 +8671,13 @@ ${TASK_BELIEF_REFERENCE_EXAMPLE}
8659
8671
  ## Status discipline \u2014 MANDATORY
8660
8672
 
8661
8673
  - Execution ownership: process the task you were assigned in this run; set it
8662
- \`in_progress\` with \`manage_task set_status\` when you start. To involve a more
8674
+ \`in_progress\` with \`manage_task set_status\` when you start. Set it only for a
8675
+ task THIS run executes; a task you delegate stays \`pending\`. To involve a more
8663
8676
  suitable expert, create a subtask and dispatch it with \`task { taskId, agentId }\`
8664
8677
  \u2014 never re-dispatch the task you are processing. Report the outcome with
8665
8678
  \`set_status completed\` (result + beliefImpact), \`failed\`, or \`interrupted\`.
8679
+ Complete a task exactly once: if it is already \`completed\`, do not complete it
8680
+ again \u2014 use \`add_activity\` for extra notes.
8666
8681
  - All status transitions go through \`manage_task set_status\` (with
8667
8682
  \`result\` on completed, \`failureReason\` on failed). \`update\` is for
8668
8683
  content fields only and rejects a status field.
@@ -19345,6 +19360,7 @@ async function runDiscoverHandler(executor, manifest, handler, config, tenantId2
19345
19360
  var PLUGINS_ROOT2 = "/root/.agents/plugins";
19346
19361
  var PLUGIN_TYPE_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
19347
19362
  var DEFAULT_HASH_TTL_MS = 3e4;
19363
+ var BACKUP_DIR_NAME = ".backup";
19348
19364
  function pluginTypeFromPath(dirPath) {
19349
19365
  return dirPath.slice(dirPath.lastIndexOf("/") + 1);
19350
19366
  }
@@ -19356,6 +19372,10 @@ function topLevelPluginDirs(entries) {
19356
19372
  return remainder.length > 0 && !remainder.includes("/");
19357
19373
  }).sort((a, b) => a.path.localeCompare(b.path));
19358
19374
  }
19375
+ function isUnderBackup(path8, pluginDir) {
19376
+ const backupDir = `${pluginDir}/${BACKUP_DIR_NAME}`;
19377
+ return path8 === backupDir || path8.startsWith(`${backupDir}/`);
19378
+ }
19359
19379
  var TenantPluginCatalog = class {
19360
19380
  /**
19361
19381
  * @param deps - Sandbox resolver, executor, reserved types, and cache/logger seams
@@ -19391,6 +19411,177 @@ var TenantPluginCatalog = class {
19391
19411
  const snapshot = await this.ensureLoaded(tenantId2);
19392
19412
  return snapshot.manifests[type];
19393
19413
  }
19414
+ /**
19415
+ * Validate a single plugin by reading its files from the sandbox volume.
19416
+ *
19417
+ * Dry-run counterpart to {@link reload}: it lists the plugin directory, reads
19418
+ * `plugin.json`, and runs the same manifest validation the catalog uses when
19419
+ * loading, with handler existence checked against the files actually present
19420
+ * on disk. It never touches the cache and never publishes anything, so a
19421
+ * caller can validate an on-disk plugin without making it live.
19422
+ *
19423
+ * @param tenantId - Tenant that owns the plugin
19424
+ * @param type - Plugin type (directory name); must match the kebab-case type pattern
19425
+ * @returns The structured `diagnostics` and whether the manifest passed fatal
19426
+ * validation (`valid`). A non-kebab-case `type`, an unreadable directory, or
19427
+ * a missing/invalid `plugin.json` yields a diagnostic with `valid: false`.
19428
+ */
19429
+ async validatePluginFromDisk(tenantId2, type) {
19430
+ if (typeof type !== "string" || !PLUGIN_TYPE_RE.test(type)) {
19431
+ return {
19432
+ diagnostics: [
19433
+ {
19434
+ level: "error",
19435
+ code: "TYPE_INVALID",
19436
+ pluginType: type,
19437
+ message: `type "${type}" must match ${PLUGIN_TYPE_RE}`
19438
+ }
19439
+ ],
19440
+ valid: false
19441
+ };
19442
+ }
19443
+ const pluginDir = `${PLUGINS_ROOT2}/${type}`;
19444
+ const sandbox = await this.deps.resolveSandbox({ tenantId: tenantId2 });
19445
+ let listing;
19446
+ try {
19447
+ listing = await sandbox.file.listPath(pluginDir, { recursive: true });
19448
+ } catch (err) {
19449
+ return {
19450
+ diagnostics: [
19451
+ {
19452
+ level: "error",
19453
+ code: "PLUGINS_LIST_FAILED",
19454
+ pluginType: type,
19455
+ message: err instanceof Error ? err.message : String(err)
19456
+ }
19457
+ ],
19458
+ valid: false
19459
+ };
19460
+ }
19461
+ let raw;
19462
+ let manifestJson;
19463
+ try {
19464
+ const { content } = await sandbox.file.readFile(`${pluginDir}/plugin.json`);
19465
+ raw = JSON.parse(content);
19466
+ manifestJson = JSON.stringify(raw);
19467
+ } catch (err) {
19468
+ return {
19469
+ diagnostics: [
19470
+ {
19471
+ level: "error",
19472
+ code: "MANIFEST_READ",
19473
+ pluginType: type,
19474
+ message: err instanceof Error ? err.message : String(err)
19475
+ }
19476
+ ],
19477
+ valid: false
19478
+ };
19479
+ }
19480
+ const files = listing.files ?? [];
19481
+ const handlerExists = (rel) => files.some((f) => !f.is_dir && f.path === `${pluginDir}/${rel}`);
19482
+ const knownTypes = new Set(
19483
+ Object.keys(this.cache.get(tenantId2)?.value.manifests ?? {}).filter((t) => t !== type)
19484
+ );
19485
+ const result = validateSandboxPluginManifest({
19486
+ raw,
19487
+ dirName: type,
19488
+ reservedTypes: this.deps.reservedTypes(),
19489
+ knownTypes,
19490
+ handlerExists,
19491
+ rawSizeBytes: Buffer.byteLength(manifestJson, "utf8")
19492
+ });
19493
+ return { diagnostics: result.diagnostics, valid: Boolean(result.manifest) };
19494
+ }
19495
+ /**
19496
+ * Snapshot a plugin's current files into its in-directory backup.
19497
+ *
19498
+ * Keeps a single backup (`<pluginDir>/.backup/`): the previous backup is
19499
+ * replaced. Files already under `.backup/` are excluded, so a backup never
19500
+ * nests. Manual action — it is not triggered by {@link writePlugin}.
19501
+ *
19502
+ * @param tenantId - Tenant that owns the plugin
19503
+ * @param type - Plugin type (directory name)
19504
+ * @returns The number of files snapshotted (`0` when the plugin has no files)
19505
+ */
19506
+ async backupPlugin(tenantId2, type) {
19507
+ this.assertSafeType(type);
19508
+ const pluginDir = `${PLUGINS_ROOT2}/${type}`;
19509
+ const backupDir = `${pluginDir}/${BACKUP_DIR_NAME}`;
19510
+ const sandbox = await this.deps.resolveSandbox({ tenantId: tenantId2 });
19511
+ const entries = (await sandbox.file.listPath(pluginDir, { recursive: true })).files ?? [];
19512
+ const files = entries.filter((entry) => !entry.is_dir && !isUnderBackup(entry.path, pluginDir));
19513
+ if (files.length === 0) {
19514
+ return { files: 0 };
19515
+ }
19516
+ if (entries.some((entry) => isUnderBackup(entry.path, pluginDir))) {
19517
+ await sandbox.file.deletePath(backupDir);
19518
+ }
19519
+ let count = 0;
19520
+ for (const entry of files) {
19521
+ const rel = entry.path.slice(pluginDir.length + 1);
19522
+ const { content } = await sandbox.file.readFile(entry.path);
19523
+ await this.writeRelativeFile(sandbox, backupDir, rel, content);
19524
+ count += 1;
19525
+ }
19526
+ return { files: count };
19527
+ }
19528
+ /**
19529
+ * Restore a plugin's files from its in-directory backup.
19530
+ *
19531
+ * The plugin directory (excluding `.backup/`) is made to match the backup:
19532
+ * files present in the backup are (re)written and files absent from it are
19533
+ * removed. Callers should {@link reload} afterward so the restored shape is
19534
+ * picked up.
19535
+ *
19536
+ * @param tenantId - Tenant that owns the plugin
19537
+ * @param type - Plugin type (directory name)
19538
+ * @returns The number of files restored
19539
+ * @throws Error when the plugin has no backup
19540
+ */
19541
+ async restorePlugin(tenantId2, type) {
19542
+ this.assertSafeType(type);
19543
+ const pluginDir = `${PLUGINS_ROOT2}/${type}`;
19544
+ const backupDir = `${pluginDir}/${BACKUP_DIR_NAME}`;
19545
+ const sandbox = await this.deps.resolveSandbox({ tenantId: tenantId2 });
19546
+ const backupFiles = await (async () => {
19547
+ try {
19548
+ return ((await sandbox.file.listPath(backupDir, { recursive: true })).files ?? []).filter((entry) => !entry.is_dir);
19549
+ } catch {
19550
+ return [];
19551
+ }
19552
+ })();
19553
+ if (backupFiles.length === 0) {
19554
+ throw new Error(`no backup found for plugin "${type}"`);
19555
+ }
19556
+ const backupRels = new Set(backupFiles.map((entry) => entry.path.slice(backupDir.length + 1)));
19557
+ const currentFiles = ((await sandbox.file.listPath(pluginDir, { recursive: true })).files ?? []).filter((entry) => !entry.is_dir && !isUnderBackup(entry.path, pluginDir));
19558
+ for (const entry of currentFiles) {
19559
+ const rel = entry.path.slice(pluginDir.length + 1);
19560
+ if (!backupRels.has(rel)) await sandbox.file.deletePath(entry.path);
19561
+ }
19562
+ let count = 0;
19563
+ for (const entry of backupFiles) {
19564
+ const rel = entry.path.slice(backupDir.length + 1);
19565
+ const { content } = await sandbox.file.readFile(entry.path);
19566
+ await this.writeRelativeFile(sandbox, pluginDir, rel, content);
19567
+ count += 1;
19568
+ }
19569
+ return { files: count };
19570
+ }
19571
+ /**
19572
+ * Write a file under a base directory, creating its parent directory first.
19573
+ *
19574
+ * @param sandbox - Sandbox whose file service receives the write
19575
+ * @param baseDir - Absolute target directory (a plugin or backup directory)
19576
+ * @param rel - Safe path relative to `baseDir`
19577
+ * @param content - File contents
19578
+ * @returns Nothing once the file is written
19579
+ */
19580
+ async writeRelativeFile(sandbox, baseDir, rel, content) {
19581
+ const slash = rel.lastIndexOf("/");
19582
+ if (slash !== -1) await sandbox.file.createDirectory(`${baseDir}/${rel.slice(0, slash)}`);
19583
+ await sandbox.file.writeFile(`${baseDir}/${rel}`, content);
19584
+ }
19394
19585
  /**
19395
19586
  * Drop the cached snapshot for a tenant.
19396
19587
  *
@@ -19467,6 +19658,30 @@ var TenantPluginCatalog = class {
19467
19658
  const { content } = await sandbox.file.readFile(`${PLUGINS_ROOT2}/${type}/${relPath}`);
19468
19659
  return content;
19469
19660
  }
19661
+ /**
19662
+ * List the relative file paths currently present under a plugin directory.
19663
+ *
19664
+ * Used to decide whether a plugin already exists before scaffolding, and to
19665
+ * surface a plugin's files without reading their contents.
19666
+ *
19667
+ * @param tenantId - Tenant that owns the plugin
19668
+ * @param type - Plugin type (directory name); must match the kebab-case type pattern
19669
+ * @returns Relative paths (e.g. `plugin.json`, `handlers/x.js`); empty when the
19670
+ * directory is missing or the listing fails
19671
+ */
19672
+ async listPluginFiles(tenantId2, type) {
19673
+ this.assertSafeType(type);
19674
+ const sandbox = await this.deps.resolveSandbox({ tenantId: tenantId2 });
19675
+ const pluginDir = `${PLUGINS_ROOT2}/${type}`;
19676
+ let listing;
19677
+ try {
19678
+ listing = await sandbox.file.listPath(pluginDir, { recursive: true });
19679
+ } catch {
19680
+ return [];
19681
+ }
19682
+ const prefix = `${pluginDir}/`;
19683
+ return listing.files.filter((entry) => !entry.is_dir).map((entry) => entry.path.startsWith(prefix) ? entry.path.slice(prefix.length) : entry.path);
19684
+ }
19470
19685
  /**
19471
19686
  * Write a plugin manifest and handler files into the tenant volume.
19472
19687
  *
@@ -19491,11 +19706,7 @@ var TenantPluginCatalog = class {
19491
19706
  await sandbox.file.createDirectory(dir);
19492
19707
  await sandbox.file.writeFile(`${dir}/plugin.json`, JSON.stringify(manifest, null, 2));
19493
19708
  for (const [rel, content] of Object.entries(handlers)) {
19494
- const slash = rel.lastIndexOf("/");
19495
- if (slash !== -1) {
19496
- await sandbox.file.createDirectory(`${dir}/${rel.slice(0, slash)}`);
19497
- }
19498
- await sandbox.file.writeFile(`${dir}/${rel}`, content);
19709
+ await this.writeRelativeFile(sandbox, dir, rel, content);
19499
19710
  }
19500
19711
  }
19501
19712
  /**
@@ -19600,7 +19811,7 @@ var TenantPluginCatalog = class {
19600
19811
  const dirFiles = entries.filter(
19601
19812
  (entry) => entry.path === dir.path || entry.path.startsWith(`${dir.path}/`)
19602
19813
  );
19603
- const relPaths = dirFiles.filter((entry) => !entry.is_dir && entry.path !== `${dir.path}/plugin.json`).map((entry) => entry.path.slice(dir.path.length + 1)).sort();
19814
+ const relPaths = dirFiles.filter((entry) => !entry.is_dir && entry.path !== `${dir.path}/plugin.json` && !isUnderBackup(entry.path, dir.path)).map((entry) => entry.path.slice(dir.path.length + 1)).sort();
19604
19815
  const manifestPath = `${dir.path}/plugin.json`;
19605
19816
  let raw;
19606
19817
  let manifestJson;
@@ -19631,7 +19842,7 @@ var TenantPluginCatalog = class {
19631
19842
  const manifests = {};
19632
19843
  for (const candidate of candidates) {
19633
19844
  try {
19634
- const handlerExists = (rel) => candidate.files.some((f) => f.path === `${candidate.dirPath}/${rel}`);
19845
+ const handlerExists = (rel) => candidate.files.some((f) => !f.is_dir && f.path === `${candidate.dirPath}/${rel}`);
19635
19846
  const result = validateSandboxPluginManifest({
19636
19847
  raw: candidate.raw,
19637
19848
  dirName: candidate.type,
@@ -38375,7 +38586,11 @@ var TaskLifecycleService = class {
38375
38586
  expectedStatuses: ["completed", "cancelled"],
38376
38587
  updates: { status: "in_progress", failureReason: null, result: null },
38377
38588
  summary: "Task reopened",
38378
- detail: { type: "reopen" }
38589
+ detail: { type: "reopen" },
38590
+ // Preserve an existing thread by default (the project runtime reuses the
38591
+ // canonical Task Thread); a dispatch that allocated a fresh run thread
38592
+ // opts in to replacing it.
38593
+ replaceThread: input.replaceThread ?? false
38379
38594
  });
38380
38595
  }
38381
38596
  /**
@@ -40236,6 +40451,39 @@ function createTaskRunBridge(deps) {
40236
40451
  transitioned: true
40237
40452
  };
40238
40453
  }
40454
+ if (task.status === "completed" || task.status === "cancelled") {
40455
+ if (task.ownerType !== "agent") {
40456
+ return {
40457
+ success: false,
40458
+ code: "AGENT_TASK_REQUIRED",
40459
+ error: "Lifecycle transitions only support agent tasks."
40460
+ };
40461
+ }
40462
+ const reopened = await deps.lifecycle.reopenTask({
40463
+ tenantId: tenantId2,
40464
+ taskId: task.id,
40465
+ actor,
40466
+ ...threadId !== void 0 ? { threadId } : {},
40467
+ // The caller allocated a fresh run thread for this dispatch.
40468
+ replaceThread: true
40469
+ });
40470
+ if (!reopened.success) {
40471
+ return {
40472
+ success: false,
40473
+ code: reopened.code,
40474
+ error: reopened.error,
40475
+ ...reopened.hint !== void 0 ? { hint: reopened.hint } : {},
40476
+ ...reopened.retryable !== void 0 ? { retryable: reopened.retryable } : {}
40477
+ };
40478
+ }
40479
+ return {
40480
+ success: true,
40481
+ task: reopened.task,
40482
+ warnings: reopened.warnings ?? [],
40483
+ mutated: true,
40484
+ transitioned: true
40485
+ };
40486
+ }
40239
40487
  return { success: true, task, warnings, mutated: false, transitioned: false };
40240
40488
  },
40241
40489
  async settleRun(input) {
@@ -40408,6 +40656,45 @@ async function tryAcquireTaskRun(input) {
40408
40656
  };
40409
40657
  }
40410
40658
 
40659
+ // src/services/taskLiveness.ts
40660
+ function registeredLeaseStore() {
40661
+ try {
40662
+ return storeLatticeManager.getStoreLattice("default", "lease").store;
40663
+ } catch {
40664
+ return void 0;
40665
+ }
40666
+ }
40667
+ async function isTaskRunLeaseHeld(task) {
40668
+ const store = registeredLeaseStore();
40669
+ if (!store) return false;
40670
+ try {
40671
+ if (await store.get(taskRunLeaseKey(task.id))) return true;
40672
+ const threadId = typeof task.context?.thread_id === "string" ? task.context.thread_id : void 0;
40673
+ if (threadId && task.ownerId && await store.get(threadRunLeaseKey(task.tenantId, task.ownerId, threadId))) {
40674
+ return true;
40675
+ }
40676
+ return false;
40677
+ } catch {
40678
+ return true;
40679
+ }
40680
+ }
40681
+ function createTaskRunLiveness() {
40682
+ return async (task) => taskRunRegistry.isLive(task.id) || isTaskRunLeaseHeld(task);
40683
+ }
40684
+ function isMultiReplicaDeclared() {
40685
+ const id = process.env.AXIOM_REPLICA_ID;
40686
+ return typeof id === "string" && id.trim() !== "";
40687
+ }
40688
+ function isSharedLeaseStore() {
40689
+ const store = registeredLeaseStore();
40690
+ const name = store?.constructor?.name;
40691
+ return typeof name === "string" && !name.startsWith("InMemory");
40692
+ }
40693
+ function canConvergeOrphan() {
40694
+ if (!isMultiReplicaDeclared()) return true;
40695
+ return isSharedLeaseStore();
40696
+ }
40697
+
40411
40698
  // src/services/ExecutionAuthorityRegistry.ts
40412
40699
  var ManagedTaskStartError = class extends Error {
40413
40700
  /**
@@ -40491,7 +40778,10 @@ async function prepareTaskDispatch(deps, input) {
40491
40778
  }
40492
40779
  };
40493
40780
  }
40494
- if (task.status === "completed" || task.status === "cancelled") {
40781
+ const managedAuthority = executionAuthorityRegistry.resolve(task);
40782
+ const terminal = task.status === "completed" || task.status === "cancelled";
40783
+ const reopenableTerminal = terminal && task.ownerType === "agent" && !managedAuthority;
40784
+ if (terminal && !reopenableTerminal) {
40495
40785
  return {
40496
40786
  kind: "failure",
40497
40787
  failure: {
@@ -40515,19 +40805,18 @@ async function prepareTaskDispatch(deps, input) {
40515
40805
  }
40516
40806
  };
40517
40807
  }
40518
- if (task.status !== "pending" && task.status !== "interrupted" && task.status !== "failed") {
40808
+ if (task.status !== "pending" && task.status !== "interrupted" && task.status !== "failed" && !reopenableTerminal) {
40519
40809
  return {
40520
40810
  kind: "failure",
40521
40811
  failure: {
40522
40812
  success: false,
40523
40813
  code: "TASK_NOT_DISPATCHABLE",
40524
- error: "Only a `pending`, `interrupted`, or `failed` task can be dispatched.",
40525
- hint: `This task is '${task.status}'. A \`pending\` task starts, an \`interrupted\` task resumes, and a \`failed\` task retries. A task already \`in_progress\` is held by a live run; terminal tasks cannot be dispatched.`,
40814
+ error: "Only a `pending`, `interrupted`, `failed`, or reopenable terminal task can be dispatched.",
40815
+ hint: `This task is '${task.status}'. A \`pending\` task starts, an \`interrupted\` task resumes, a \`failed\` task retries, and an agent-owned \`completed\`/\`cancelled\` task reopens. A task already \`in_progress\` is held by a live run.`,
40526
40816
  taskId: task.id
40527
40817
  }
40528
40818
  };
40529
40819
  }
40530
- const managedAuthority = executionAuthorityRegistry.resolve(task);
40531
40820
  if (managedAuthority) {
40532
40821
  if (managedAuthority.onManagedStart) {
40533
40822
  try {
@@ -40697,7 +40986,8 @@ function persistedExecutionObservation(item, attempted) {
40697
40986
  }
40698
40987
  function buildWorkerMessage(task, parent, dependencies) {
40699
40988
  const sections2 = [
40700
- "Execute the following persistent task. Return your result as an observation; do not assume the task is complete.",
40989
+ `You have been assigned persistent task \`${task.id}\` to execute in THIS run. Do NOT dispatch this task \u2014 \`task\` is only for delegating a NEW child subtask; this task is already yours.`,
40990
+ "When the acceptance criteria are truly met, record the outcome yourself with `manage_task set_status completed` (result + beliefImpact), or `failed`/`interrupted`. If you do not have `manage_task`, return your result as an observation for the caller to record.",
40701
40991
  `Persisted Task ID: \`${task.id}\``,
40702
40992
  `# Task: ${task.title}`,
40703
40993
  task.description?.trim() || "No task description was provided."
@@ -40729,9 +41019,17 @@ function createTaskExecutionService(deps) {
40729
41019
  taskStore: deps.taskStore,
40730
41020
  workItemStore: deps.workItemStore
40731
41021
  });
41022
+ const isTaskHeldByLiveRun = deps.isTaskHeldByLiveRun ?? createTaskRunLiveness();
40732
41023
  const run = async (plan) => {
40733
41024
  const { tenantId: tenantId2, agentId, actor, parent, completedDependencies } = plan;
40734
41025
  let task = plan.task;
41026
+ const readTaskStatus = async () => {
41027
+ try {
41028
+ return (await deps.taskStore.getById(tenantId2, task.id))?.status;
41029
+ } catch {
41030
+ return void 0;
41031
+ }
41032
+ };
40735
41033
  const storedThreadId = (task.status === "interrupted" || task.status === "failed") && typeof task.context?.thread_id === "string" && task.context.thread_id.trim() !== "" ? task.context.thread_id : void 0;
40736
41034
  const storedRunId = typeof task.context?.run_id === "string" && task.context.run_id.trim() !== "" ? task.context.run_id : void 0;
40737
41035
  let reuseCurrentRun = false;
@@ -40790,7 +41088,7 @@ function createTaskExecutionService(deps) {
40790
41088
  task = transfer.task;
40791
41089
  }
40792
41090
  const warnings = [];
40793
- if (task.status === "pending" || task.status === "interrupted" || task.status === "failed") {
41091
+ if (task.status === "pending" || task.status === "interrupted" || task.status === "failed" || task.status === "completed" || task.status === "cancelled") {
40794
41092
  const begun = await bridge.beginRun({
40795
41093
  tenantId: tenantId2,
40796
41094
  task,
@@ -40917,6 +41215,7 @@ function createTaskExecutionService(deps) {
40917
41215
  ...warnings.length > 0 ? { warnings } : {}
40918
41216
  };
40919
41217
  }
41218
+ const settledStatus = await readTaskStatus();
40920
41219
  return {
40921
41220
  success: false,
40922
41221
  code: failureCode,
@@ -40925,6 +41224,7 @@ function createTaskExecutionService(deps) {
40925
41224
  agentId,
40926
41225
  threadId,
40927
41226
  executionResultId: eventKey,
41227
+ ...settledStatus !== void 0 ? { taskStatus: settledStatus } : {},
40928
41228
  ...warnings.length > 0 ? { warnings } : {}
40929
41229
  };
40930
41230
  } catch (persistenceError) {
@@ -40973,6 +41273,7 @@ function createTaskExecutionService(deps) {
40973
41273
  ...warnings.length > 0 ? { warnings } : {}
40974
41274
  };
40975
41275
  }
41276
+ const taskStatus = await readTaskStatus();
40976
41277
  return {
40977
41278
  success: true,
40978
41279
  taskId: task.id,
@@ -40980,6 +41281,7 @@ function createTaskExecutionService(deps) {
40980
41281
  threadId,
40981
41282
  observation: validated.observation,
40982
41283
  executionResultId: eventKey,
41284
+ ...taskStatus !== void 0 ? { taskStatus } : {},
40983
41285
  ...warnings.length > 0 ? { warnings } : {}
40984
41286
  };
40985
41287
  } catch (error) {
@@ -41000,6 +41302,23 @@ function createTaskExecutionService(deps) {
41000
41302
  };
41001
41303
  return {
41002
41304
  start: async (input) => {
41305
+ let current;
41306
+ try {
41307
+ current = await deps.taskStore.getById(input.tenantId, input.taskId);
41308
+ } catch {
41309
+ current = null;
41310
+ }
41311
+ if (current && current.status === "in_progress" && current.ownerType === "agent" && canConvergeOrphan() && !executionAuthorityRegistry.resolve(current) && !await isTaskHeldByLiveRun(current)) {
41312
+ try {
41313
+ await lifecycle.failTask({
41314
+ tenantId: input.tenantId,
41315
+ taskId: current.id,
41316
+ actor: input.actor,
41317
+ failureReason: "Run is no longer executing this task (no_executor)"
41318
+ });
41319
+ } catch {
41320
+ }
41321
+ }
41003
41322
  const prepared = await prepareTaskDispatch(deps, input);
41004
41323
  if (prepared.kind === "managed") return prepared.result;
41005
41324
  if (prepared.kind === "failure") return prepared.failure;
@@ -43144,9 +43463,85 @@ Use \`imagine_html\`. Wrap the entire thing in a single raised card. All content
43144
43463
  </div>
43145
43464
  </div>
43146
43465
  \`\`\``;
43466
+ var ANNOTATABLE_MOCKUP = `## Annotatable drafts (reviewable mockups)
43467
+
43468
+ When the output is a **UI design** \u2014 a draft the user critiques element-by-element \u2014 make it **annotatable** so feedback comes back to the conversation and you can revise. **Right-click** (\`contextmenu\`) any marked element to leave a note; a plain left-click stays free for the mock's own interactions. (A structural or functional overview is different: make it **click-to-explore interactive** to confirm, not annotatable.)
43469
+
43470
+ - Show a small **always-visible hint** (in the user's language) so the draft is discoverable, e.g. \`\u53F3\u952E\u4EFB\u610F\u5143\u7D20\u6279\u6CE8\` / \`Right-click an element to comment\`.
43471
+ - Mark each element worth commenting on with \`class="anno"\`; highlight hover with an **\`outline\`** (never \`border\` \u2014 outline does not reflow the layout). Give it a **negative \`outline-offset\`** (e.g. \`-2px\`) so the frame sits *inside* the element: a positive offset can fall outside the container and get clipped, hiding the cue.
43472
+ - Wrap the whole draft in a \`position: relative\` container and put the comment box in an overlay inside it with \`position: absolute; inset: 0\` \u2014 **never** \`position: fixed\` (it collapses the iframe to min-height).
43473
+ - Handle \`contextmenu\` with event delegation; \`preventDefault()\` + \`stopPropagation()\` (this replaces the native browser menu, which is fine on a draft).
43474
+ - The dialog shows the element name and its **current value**, a note field, and **Cancel / Send** buttons (Send is primary). Cancel, clicking the scrim, and \`Escape\` all close without sending. Mark the element being commented (add \`.marked\`, a solid outline) so it is obvious which one is active.
43475
+ - Send the note back with \`sendPrompt()\` as a clear instruction \u2014 the element, its current value, and the requested change \u2014 so the agent knows exactly what to change. The agent then restates the feedback and re-shows a revised draft.
43476
+
43477
+ \`\`\`html
43478
+ <style>
43479
+ .anno{position:relative}
43480
+ .anno:hover{outline:1px dashed var(--color-border-info);outline-offset:-2px;border-radius:var(--border-radius-md)}
43481
+ .anno.marked{outline:1.5px solid var(--color-text-info);outline-offset:-2px}
43482
+ .overlay{position:absolute;inset:0;background:rgba(0,0,0,.35);display:flex;align-items:center;justify-content:center;border-radius:var(--border-radius-lg);z-index:10}
43483
+ .modal{background:var(--color-background-primary);border:0.5px solid var(--color-border-secondary);border-radius:var(--border-radius-lg);padding:16px;width:88%;max-width:420px}
43484
+ .modal .mt{font-size:14px;font-weight:500;margin-bottom:2px}
43485
+ .modal .ms{font-size:11px;color:var(--color-text-tertiary);margin-bottom:10px}
43486
+ .modal textarea{width:100%;box-sizing:border-box;height:auto;min-height:72px;padding:8px 10px;resize:vertical}
43487
+ .modal .mrow{display:flex;gap:8px;justify-content:flex-end;margin-top:10px}
43488
+ .primary{background:var(--color-text-info);color:var(--color-background-primary);border-color:var(--color-text-info)}
43489
+ .primary:hover{background:var(--color-text-info);opacity:.9}
43490
+ </style>
43491
+ <div class="panel" style="position:relative">
43492
+ <div style="font-size:12px;color:var(--color-text-tertiary);margin-bottom:8px">Right-click an element to comment</div>
43493
+ <div class="anno" data-el="Title" data-val="Q4 revenue">Q4 revenue</div>
43494
+ <div class="anno" data-el="Primary action" data-val="Export">Export</div>
43495
+ </div>
43496
+ <script>
43497
+ var panel = document.querySelector('.panel');
43498
+ var current = null;
43499
+ function closeBox(){
43500
+ if (current) current.classList.remove('marked');
43501
+ var ov = panel.querySelector('.overlay');
43502
+ if (ov) ov.remove();
43503
+ current = null;
43504
+ }
43505
+ function openBox(el){
43506
+ closeBox();
43507
+ current = el;
43508
+ el.classList.add('marked');
43509
+ var ov = document.createElement('div');
43510
+ ov.className = 'overlay';
43511
+ ov.innerHTML = '<div class="modal">' +
43512
+ '<div class="mt">Note: ' + el.dataset.el + '</div>' +
43513
+ (el.dataset.val ? '<div class="ms">Current: ' + el.dataset.val + '</div>' : '') +
43514
+ '<textarea id="cmt" placeholder="What should change? e.g. bolder bar / expand by default / add a year filter"></textarea>' +
43515
+ '<div class="mrow"><button id="cancel">Cancel</button><button class="primary" id="send">Send note \u2197</button></div>' +
43516
+ '</div>';
43517
+ panel.appendChild(ov);
43518
+ ov.querySelector('#cmt').focus();
43519
+ ov.querySelector('#send').onclick = function(){
43520
+ var text = ov.querySelector('#cmt').value.trim();
43521
+ if (text && typeof sendPrompt === 'function') {
43522
+ sendPrompt('[design note] ' + el.dataset.el + (el.dataset.val ? ' (current: ' + el.dataset.val + ')' : '') + ' \u2014 change: ' + text);
43523
+ }
43524
+ closeBox();
43525
+ };
43526
+ ov.querySelector('#cancel').onclick = closeBox;
43527
+ ov.addEventListener('click', function(e){ if (e.target === ov) closeBox(); });
43528
+ }
43529
+ document.addEventListener('keydown', function(e){ if (e.key === 'Escape') closeBox(); });
43530
+ panel.addEventListener('contextmenu', function(e){
43531
+ var el = e.target.closest('.anno');
43532
+ if (!el) return;
43533
+ e.preventDefault(); e.stopPropagation();
43534
+ openBox(el);
43535
+ });
43536
+ </script>
43537
+ \`\`\`
43538
+
43539
+ - The host already pre-styles bare \`<button>\` and \`<textarea>\` (token borders, radius, hover/focus) \u2014 use them as-is and add only what is missing: a \`.primary\` variant for the send button and a \`width\`/\`min-height\` override on the \`textarea\`. Do NOT re-declare the base button/field styles. Add a \`\u2197\` to the send button (it triggers \`sendPrompt\`).
43540
+
43541
+ Keep it lightweight: annotate only meaningful elements, reuse one overlay, no libraries, and put \`<script>\` last so the draft streams in top-down.`;
43147
43542
  var MODULE_SECTIONS = {
43148
43543
  art: [SVG_SETUP, ART_AND_ILLUSTRATION],
43149
- mockup: [UI_COMPONENTS, COLOR_PALETTE],
43544
+ mockup: [UI_COMPONENTS, COLOR_PALETTE, ANNOTATABLE_MOCKUP],
43150
43545
  interactive: [UI_COMPONENTS, COLOR_PALETTE],
43151
43546
  chart: [UI_COMPONENTS, COLOR_PALETTE, CHARTS_CHART_JS],
43152
43547
  diagram: [COLOR_PALETTE, SVG_SETUP, DIAGRAM_TYPES]
@@ -43653,7 +44048,7 @@ function renderTaskBinding(taskId) {
43653
44048
 
43654
44049
  ## Bound Task
43655
44050
 
43656
- You are executing task \`${taskId}\`. Read its contract with \`manage_task get\`, update it as you work, and complete it when done.
44051
+ You are executing task \`${taskId}\` in THIS run \u2014 it is already assigned to you. Read its contract with \`manage_task get\`, update it as you work, and complete it when done. Do NOT dispatch this task with \`task\`; \`task\` is only for delegating a NEW child subtask.
43657
44052
  `;
43658
44053
  }
43659
44054
  function createBoundTaskLifecycle() {
@@ -44172,7 +44567,7 @@ var manageTaskSchema = z70.object({
44172
44567
  title: z70.string().optional().describe("Task title (required for create)"),
44173
44568
  description: z70.string().optional().describe("Task description in Markdown"),
44174
44569
  priority: z70.enum(["low", "medium", "high"]).optional().describe("Priority level"),
44175
- status: z70.enum(["pending", "in_progress", "review", "failed", "interrupted", "completed", "cancelled"]).optional().describe("Task status. For status-only changes, prefer set_status action. Set 'in_progress' when you start processing a task you hold; 'completed'/'failed' require the task to be 'in_progress'. Agent summary-only interrupted updates default to missing_input"),
44570
+ status: z70.enum(["pending", "in_progress", "review", "failed", "interrupted", "completed", "cancelled"]).optional().describe("Task status. For status-only changes, prefer set_status action. Set 'in_progress' only for a task THIS run executes (a delegated task is claimed by `task` dispatch); 'completed'/'failed' require the task to be 'in_progress'. Agent summary-only interrupted updates default to missing_input"),
44176
44571
  dueDate: z70.string().optional().describe("Due date (ISO 8601 format)"),
44177
44572
  metadata: z70.record(z70.unknown()).optional().describe("Structured metadata (e.g. projectId, module)"),
44178
44573
  metadataFilter: z70.record(
@@ -45763,8 +46158,8 @@ ${TASK_CONVERGENCE_GUIDANCE}
45763
46158
  - update: Update a task's content fields (title/description/priority/dueDate/metadata/parentId/sourceId/context/ownerId/result/failureReason/requireReview/dependencies/files)
45764
46159
  Status changes can also be done here, but use set_status for a simpler dedicated status update.
45765
46160
  - set_status: Change a task's status (id and status required). Use this when you only need to change the status.
45766
- Start processing a task you hold: set_status with status='in_progress' (the engine binds it to this run's thread).
45767
- To mark complete: set_status with status='completed', result='...' (the task must be in_progress)
46161
+ Start a task THIS run executes: set_status with status='in_progress' (the engine binds it to this run's thread). Do NOT set in_progress on a task you will delegate \u2014 leave it pending; \`task\` dispatch claims it.
46162
+ To mark complete: set_status with status='completed', result='...' (the task must be in_progress). Complete a task exactly once: if it is already completed, do not complete it again \u2014 use add_activity for extra notes.
45768
46163
  To mark failed: set_status with status='failed', failureReason='...'
45769
46164
  To pause for input: set_status with status='interrupted', summary='what is needed'
45770
46165
  Valid status values: pending, in_progress, review, failed, interrupted, completed, cancelled.
@@ -45781,16 +46176,16 @@ Report existing keys directly; set 'isNew: true' to add a key the owner does not
45781
46176
  track yet. Belief is evidence metadata: it never blocks a completion, and an
45782
46177
  unmatched or malformed entry is recorded without changing the owner's table.
45783
46178
  Create always persists a pending record \u2014 a supplied status is coerced and no
45784
- execution is dispatched. When you start processing a task you hold, set it
45785
- \`in_progress\` with \`manage_task set_status\`; a dispatched task is claimed
45786
- automatically by the engine. Use \`task { taskId, agentId }\` to delegate a subtask
46179
+ execution is dispatched. Set \`in_progress\` yourself ONLY for a task THIS run
46180
+ executes; a task you delegate stays \`pending\` and is claimed automatically by
46181
+ \`task\` dispatch. Use \`task { taskId, agentId }\` to delegate a subtask
45787
46182
  to an expert \u2014 never re-dispatch the task you are processing.`,
45788
46183
  schema: manageTaskSchema
45789
46184
  }
45790
46185
  ),
45791
46186
  tool64(handleStartTask, {
45792
46187
  name: "task",
45793
- description: `Start a persisted TaskItem with any existing Agent in a fresh isolated thread. Use this to delegate work \u2014 dispatch a subtask you created to the agent that should execute it; never re-dispatch the task you are currently processing (give the expert a child task instead). Pass agentId "general-purpose" for the general-purpose worker \u2014 ${DEFAULT_GENERAL_PURPOSE_DESCRIPTION} It has access to all the caller's tools, model, and middleware, so use it for general-purpose subtasks, to isolate context and token usage, and whenever no specialized agent fits. The target does NOT need task-management tools: it runs and returns an observation plus an executionResultId, and the caller passes that executionResultId to the manage_task update or add_activity that interprets and records it. A project task instead returns a managed result (success:true, managed:true) because the project-lifecycle queue owns execution.`,
46188
+ description: `Start a persisted TaskItem with any existing Agent in a fresh isolated thread. Use this to delegate work \u2014 dispatch a subtask you created to the agent that should execute it; never re-dispatch the task you are currently processing (give the expert a child task instead). Pass agentId "general-purpose" for the general-purpose worker \u2014 ${DEFAULT_GENERAL_PURPOSE_DESCRIPTION} It has access to all the caller's tools, model, and middleware, so use it for general-purpose subtasks, to isolate context and token usage, and whenever no specialized agent fits. The target does NOT need task-management tools: it runs and returns an observation, an executionResultId, and the task's taskStatus (re-read after the worker settled). Check taskStatus before completing: if it is already 'completed' the worker finished it \u2014 do NOT complete again (use manage_task add_activity); if it is still 'in_progress' you are the single completer and pass the executionResultId to manage_task set_status completed; if it is 'failed' or 'interrupted', act on that. The caller reconciles via manage_task, which interprets the result and records the outcome. A project task instead returns a managed result (success:true, managed:true) because the project-lifecycle queue owns execution.`,
45794
46189
  schema: startTaskSchema
45795
46190
  })
45796
46191
  ]
@@ -47493,7 +47888,7 @@ A plugin is the SET of classes it needs, e.g. \`connection + tools + skills\` or
47493
47888
  | Tool shows an interactive panel | \`sandbox-plugins-ui\` |
47494
47889
  | Tool orchestrates platform tools/agents | \`sandbox-plugins-consume-open\` |
47495
47890
  | Tool callable by external MCP clients | \`sandbox-plugins-expose\` |
47496
- | Run as a long-lived resident service | \`sandbox-plugins-deploy\` |
47891
+ | Run as a long-lived resident service (only on the human's explicit request) | \`sandbox-plugins-deploy\` |
47497
47892
  | Validate / test / debug | \`sandbox-plugins-testing\` |
47498
47893
 
47499
47894
  ## Agents (not a supported authoring path)
@@ -47535,8 +47930,8 @@ chart, table, viewer\u2026). It can also call the plugin's OWN tools back from t
47535
47930
  - It is NOT the tool's JSON \`schema\` (that is the input contract for the model).
47536
47931
  - It is NOT the tool \`description\`.
47537
47932
  - It is NOT required: a text/JSON-returning tool needs no UI.
47538
- - Saying "add a UI" means: add a \`ui\` field to the tool in \`plugin.json\` + create
47539
- \`ui/<tool>.html\`. Details in \`sandbox-plugins-ui\`.
47933
+ - Saying "add a UI" means: add a \`ui\` field to the tool in \`plugin.json\` and write
47934
+ \`ui/<tool>.html\` (with \`write_file\`). Details in \`sandbox-plugins-ui\`.
47540
47935
 
47541
47936
  ## Decide first \u2014 clarify these with the user before writing files
47542
47937
  Ask for anything not already stated; do not guess silently:
@@ -47551,7 +47946,7 @@ Ask for anything not already stated; do not guess silently:
47551
47946
  multi-select? connection test needed? \u2192 read \`sandbox-plugins-connection\`.
47552
47947
  - **UI**: should the tool show an interactive panel instead of only text? what
47553
47948
  does it show, how do users interact, external origins? \u2192 read
47554
- \`sandbox-plugins-ui\` and add \`ui\` + \`ui/<tool>.html\` (step 4 below). If the
47949
+ \`sandbox-plugins-ui\` and add \`ui\` + \`ui/<tool>.html\` (step 6 below). If the
47555
47950
  user says no UI, skip it \u2014 text-only is fine.
47556
47951
  - **Skills**: procedure or knowledge? who consumes it? trigger phrase? tied to a
47557
47952
  specific tool?
@@ -47584,13 +47979,34 @@ Ask for anything not already stated; do not guess silently:
47584
47979
  \`\`\`
47585
47980
 
47586
47981
  ## Flow
47587
- 1. Read \`sandbox-plugins-manifest\` and write \`plugin.json\`.
47588
- 2. Read \`sandbox-plugins-handlers\` and write each handler.
47589
- 3. If the plugin needs credentials: read \`sandbox-plugins-connection\`.
47590
- 4. **Only if a tool needs a UI** (UI class in step 3 above): read \`sandbox-plugins-ui\`, add \`ui\` to that tool in \`plugin.json\`, and create \`ui/<tool>.html\` (pass the HTML in the \`manage_plugin\` \`handlers\` map, or write it directly under \`ui/\` and \`reload\`).
47591
- 5. If a tool must be callable by external MCP clients: read \`sandbox-plugins-expose\`
47982
+ 1. **Design first, and iterate \u2014 then get explicit approval before you build (non-trivial plugins).**
47983
+ Before writing any file, produce the design and review it with the human. It has two parts,
47984
+ with different treatments:
47985
+ - **Functional design** (always): an **interactive** component map of what you intend to
47986
+ build \u2014 the tools, their inputs/outputs, whether a connection is needed, whether a UI
47987
+ is needed, and how the agent calls them. Make it click-to-explore (e.g. click a tool to
47988
+ reveal its handler / IO / connection); it is a map to **confirm**, not to annotate.
47989
+ - **UI design** (only when the UI class applies): an **annotatable** mock of the tool's UI \u2014
47990
+ the human right-clicks elements to leave notes (see \`sandbox-plugins-ui\` \u2192
47991
+ "Reviewable drafts").
47992
+ Show both drafts with the \`widget\` middleware, then run the review loop:
47993
+ - Ask the human to review (say how to comment). On each round of comments, **restate the
47994
+ feedback** (element + requested change), then **regenerate the draft with the feedback and
47995
+ present it again** \u2014 comments are "revise and re-show", never "start building".
47996
+ - **Do NOT scaffold or write any file during the loop.** Implement only after the human
47997
+ **explicitly approves**, and ask for that go-ahead yourself before you start.
47998
+ Skip the drafts only for a trivial single-tool plugin whose shape is already agreed \u2014 but
47999
+ still confirm the goal.
48000
+ 2. Scaffold with \`manage_plugin { action: "create", type, name }\` \u2014 creates
48001
+ \`/root/.agents/plugins/<type>/\` with a stub \`plugin.json\`. It carries no content.
48002
+ 3. Read \`sandbox-plugins-manifest\` and write \`plugin.json\` with \`write_file\`.
48003
+ 4. Read \`sandbox-plugins-handlers\` and write each handler with \`write_file\`.
48004
+ 5. If the plugin needs credentials: read \`sandbox-plugins-connection\`.
48005
+ 6. **Only if a tool needs a UI** (the UI class from \`sandbox-plugins-patterns\`): read \`sandbox-plugins-ui\`, add \`ui\` to that tool in \`plugin.json\`, and write \`ui/<tool>.html\` with \`write_file\`.
48006
+ 7. If a tool must be callable by external MCP clients: read \`sandbox-plugins-expose\`
47592
48007
  and add \`expose: true\` (plus \`readOnly\`/\`destructive\` accuracy hints).
47593
- 6. Read \`sandbox-plugins-testing\` and iterate using \`manage_plugin\`.
48008
+ 8. Read \`sandbox-plugins-testing\` and iterate: edit files with \`edit_file\`, then
48009
+ \`validate\` \u2192 \`reload\` \u2192 \`test\`.
47594
48010
 
47595
48011
  Templates: \`templates/plugin.json\`, \`templates/handler.js\`, \`templates/test.js\`, \`templates/discover.js\`, \`templates/ui.html\`.
47596
48012
  `,
@@ -47652,6 +48068,20 @@ description: Add an interactive HTML UI to a tenant plugin tool (rendered as a s
47652
48068
  A tool can declare an HTML file that Axiom renders as a sandboxed MCP App when
47653
48069
  the tool runs. The app can call the plugin's OWN tools back.
47654
48070
 
48071
+ ## Reviewable drafts (before you build the real UI)
48072
+ When you show a **UI mock with \`show_widget\`** for the user to react to (the design
48073
+ step, before writing \`ui/<tool>.html\`), make it **annotatable**: the user right-clicks
48074
+ any element to leave a comment that comes back to you as a message. Turn it on by
48075
+ default for review drafts and include a small visible hint (e.g. "\u53F3\u952E\u4EFB\u610F\u5143\u7D20\u6279\u6CE8");
48076
+ the \`widget\` mockup guidelines have the pattern. Omit it only when the draft is meant
48077
+ to be purely experienced (a live demo with no review) \u2014 then leave out the hint and
48078
+ the handlers. This is only for the **mock**: the real \`ui/<tool>.html\` is the plugin's
48079
+ product and is never annotatable.
48080
+ A round of annotations is **NOT approval**: restate the comments you received, regenerate
48081
+ the mock from them, and show it again, iterating until the human is satisfied. Build
48082
+ \`ui/<tool>.html\` only after the human **explicitly approves** \u2014 and ask for that go-ahead
48083
+ yourself.
48084
+
47655
48085
  ## Declare it
47656
48086
  \`\`\`
47657
48087
  /root/.agents/plugins/<type>/
@@ -47684,11 +48114,10 @@ the tool runs. The app can call the plugin's OWN tools back.
47684
48114
  - Omit \`csp\` for the restrictive default (\`default-src 'none'\`). WebAssembly is allowed by default.
47685
48115
 
47686
48116
  ## How to write the HTML file
47687
- The HTML must end up at \`/root/.agents/plugins/<type>/ui/<tool>.html\`. Two ways:
47688
- - \`manage_plugin\` create/update: pass it in the \`handlers\` map \u2014 that map carries ALL
47689
- plugin files, not just JS handlers. Example: \`handlers\` = \`{ "handlers/render_report.js": "<js...>", "ui/report.html": "<!doctype html>..." }\`.
47690
- The validator's existence check runs against these keys, so an HTML file NOT in this map is treated as missing (\`TOOL_UI\`).
47691
- - Or write the file directly in the sandbox under \`ui/\` and call \`manage_plugin reload\`.
48117
+ Write the HTML to \`/root/.agents/plugins/<type>/ui/<tool>.html\` with \`write_file\`,
48118
+ then call \`manage_plugin { action: "reload", type }\` so the host serves the new HTML.
48119
+ The file must be declared by a tool's \`ui.resource\` in \`plugin.json\`, otherwise it is
48120
+ treated as missing (\`TOOL_UI\`).
47692
48121
 
47693
48122
  ## How it behaves at runtime
47694
48123
  - The tool result carries a hidden \`mcp_app\` marker; Axiom renders \`ui/<tool>.html\` inline. \`displayMode\` is recorded but NOT yet honored \u2014 fullscreen happens only when the app itself calls \`ui/request-display-mode\`.
@@ -47784,7 +48213,7 @@ grants apply to the UI too). See \`sandbox-plugins-expose\`.
47784
48213
  ## Gotchas
47785
48214
  - \`ui.resource\` must be a safe relative path ending in \`.html\` that EXISTS; otherwise the UI is dropped (diagnostic \`TOOL_UI\`) but the tool still loads.
47786
48215
  - \`displayMode\`, \`permissions\`, and \`prefersBorder\` are accepted but NOT yet applied.
47787
- - After editing the HTML, call \`manage_plugin reload\` (or bump \`version\`); the host caches the HTML (TTL ~5 min) and, if a fetch fails, may keep serving a stale copy until it succeeds.
48216
+ - After editing the HTML, call \`manage_plugin { action: "reload", type }\`; the host caches the resource, so a reload is what makes it re-read the new file.
47788
48217
 
47789
48218
  Start from \`templates/ui.html\`.
47790
48219
  `,
@@ -48048,21 +48477,23 @@ description: Validate, test, and reload a tenant sandbox plugin using the manage
48048
48477
 
48049
48478
  Use the \`manage_plugin\` tool. The loop:
48050
48479
 
48051
- 1. \`action: "validate"\` with \`{ type, manifest, handlers }\` \u2014 dry-run, no writes, returns diagnostics.
48052
- 2. \`action: "create"\` or \`"update"\` \u2014 writes files, bumps version, reloads.
48480
+ 1. Write/edit the plugin files with the filesystem tools (\`write_file\`/\`edit_file\`) under
48481
+ \`/root/.agents/plugins/<type>/\`; \`create\` only scaffolds the directory + stub.
48482
+ 2. \`action: "validate"\` with \`{ type }\` \u2014 reads the plugin back **from the sandbox** and returns diagnostics (no writes). The files must already be on disk; a \`type\` with no files reports a read error.
48053
48483
  3. If the plugin has \`connection.fields\` and a tool reads credentials: a **human**
48054
48484
  must configure a connection of this plugin type. You cannot create credentials.
48055
48485
  Call \`list_connections { type }\` first \u2014 if none exist, use \`ask_user_to_clarify\`
48056
48486
  to state explicitly that only the human can configure the connector, ask them to
48057
48487
  add it (Console \u2192 Connections) and to give you the connection key. Do not claim
48058
48488
  the tool works until a real \`connectionKey\` test passes.
48059
- 4. \`action: "test"\` to exercise a handler without an LLM:
48489
+ 4. \`action: "reload"\` \u2014 refresh the compiled catalog after writing/editing files (the catalog caches; an edited file is invisible to \`test\` until you reload).
48490
+ 5. \`action: "test"\` to exercise a handler without an LLM:
48060
48491
  - Tool: \`{ type, target: { kind: "tool", name, input, connectionKey? } }\`
48061
48492
  - Connection: \`{ type, target: { kind: "connection-test" } }\` or \`{ kind: "connection-discover" }\`
48062
48493
  Returns \`{ ok, result, exitCode, durationMs, stdout, stderr }\`.
48063
48494
  For credentialed tools, pass the real \`connectionKey\`. Without one, the handler
48064
48495
  receives an empty connection \u2014 that only checks plumbing, NOT functional success.
48065
- 5. \`action: "reload"\` if you edited files directly.
48496
+ 6. \`action: "backup"\` / \`"restore"\` \u2014 manual snapshot and rollback. \`backup\` copies the plugin into \`.backup/\` **inside its own directory** (keeps only the latest), and \`restore\` makes the plugin match that snapshot (adds back missing files, removes files added since). Use it before a risky rewrite.
48066
48497
 
48067
48498
  ## Secrets
48068
48499
  - \`list_connections\` returns identifiers only (\`key\`/\`name\`/\`description\`), never config or secrets.
@@ -48077,10 +48508,10 @@ Use the \`manage_plugin\` tool. The loop:
48077
48508
  | HANDLER_MISSING | handler path wrong or file not written |
48078
48509
  | TOOL_SCHEMA | unsupported JSON Schema keyword |
48079
48510
  | non-zero exit | handler threw; read stdout/stderr from \`test\` |
48080
- | old behaviour after edit | version/hash unchanged \u2014 bump \`version\` or call \`reload\` |
48511
+ | old behaviour after edit | the catalog caches by content hash/TTL \u2014 call \`reload\` |
48081
48512
  | empty connection in handler | no \`connectionKey\` passed, or no connection configured for this type |
48082
48513
 
48083
- Fall back to \`execute_code\`/\`shell\` to run \`node handlers/x.js\` manually with
48514
+ Fall back to \`shell_exec\` to run \`node handlers/x.js\` manually with
48084
48515
  \`PLUGIN_INPUT\` and a synthetic \`PLUGIN_CONNECTION_FILE\` to reproduce logic bugs.
48085
48516
  A synthetic fixture only verifies plumbing \u2014 a real upstream call requires a real
48086
48517
  \`connectionKey\` configured by a human.
@@ -48091,7 +48522,7 @@ A synthetic fixture only verifies plumbing \u2014 a real upstream call requires
48091
48522
  version: "1.0.0",
48092
48523
  content: `---
48093
48524
  name: sandbox-plugins-deploy
48094
- description: Deploy a validated tenant plugin as a resident service (alwaysOn vs onDemand), check its status, and undeploy it. Trigger: after a plugin's tests pass and it should run as a long-lived service, or when inspecting/removing a deployment.
48525
+ description: Deploy a validated tenant plugin as a resident service (alwaysOn vs onDemand), check its status, and undeploy it. Trigger: the human explicitly asks to run a plugin as a long-lived resident service (or to inspect/remove one) \u2014 deployment is human-gated, never automatic.
48095
48526
  ---
48096
48527
 
48097
48528
  # Deploying a Plugin as a Resident Service
@@ -48101,10 +48532,13 @@ Optionally, you can **deploy** the plugin as a **resident service**: one
48101
48532
  long-lived sandbox per plugin that serves every call. This is faster for
48102
48533
  frequent or heavy calls, but costs a running sandbox.
48103
48534
 
48104
- ## Precondition: tests must pass first
48105
- Deploy ONLY after the plugin is validated AND its tests pass:
48106
- 1. \`manage_plugin validate\` \u2014 no diagnostics.
48107
- 2. \`manage_plugin create\`/\`update\` \u2014 written and reloaded.
48535
+ ## Precondition: human request, then tests must pass first
48536
+ Deployment is **opt-in and human-gated**: NEVER deploy on your own after tests
48537
+ pass. Wait until the human explicitly asks for it or approves it. A passing test
48538
+ is necessary but NOT sufficient to deploy.
48539
+ Then confirm the plugin is validated AND its tests pass:
48540
+ 1. Files written with the filesystem tools and \`reload\` run.
48541
+ 2. \`manage_plugin validate\` \u2014 no diagnostics.
48108
48542
  3. \`manage_plugin test\` \u2014 every tool/connection test returns \`ok: true\`
48109
48543
  (a real \`connectionKey\` test for credentialed tools).
48110
48544
  Never deploy a plugin you have not tested: a resident service wraps the same
@@ -48172,6 +48606,30 @@ load "sandbox-plugins-expose" and follow it (\`expose: true\` + key grants).
48172
48606
  If a tool must call the platform's own tools/agents from its handler, load
48173
48607
  "sandbox-plugins-consume-open".
48174
48608
 
48609
+ Design first, and iterate on it before you build \u2014 the design is a conversation, not
48610
+ a one-shot. Use the \`widget\` middleware to present it (a decision surface, not a showcase):
48611
+ - **Functional design** (always): an **interactive** component map of the shape you
48612
+ intend to build \u2014 which tools, their inputs/outputs, whether a connection or a UI
48613
+ is needed, how the agent calls them. Make it click-to-explore (and prefer it over
48614
+ an SVG flowchart); it is a map to **confirm**, NOT an annotatable draft.
48615
+ - **UI design** (UI class only): an **annotatable** mock of the tool's UI \u2014 the human
48616
+ right-clicks elements to leave notes.
48617
+ Then run a review loop:
48618
+ - Ask the human to review (tell them how to comment). When they send comments, **restate the
48619
+ feedback** (which element, what change) so they can confirm you understood, then **build a
48620
+ NEW draft that incorporates the feedback and present it again**; keep iterating until
48621
+ they are satisfied. Comments mean "revise and re-show", NOT "start building".
48622
+ - Do NOT scaffold or write any file during the loop. Implement only once the human
48623
+ **explicitly approves** the design \u2014 and still ASK for that go-ahead yourself; never
48624
+ start building on your own initiative.
48625
+ Also surface genuinely different options (scope, layout, which tools) visually and let
48626
+ the human choose, instead of describing them in prose.
48627
+
48628
+ How to render a good widget is the widget middleware's own concern \u2014 follow it
48629
+ there; keep this prompt to plugin-design intent. These widgets are design aids
48630
+ only; the plugin's actual UI is still the MCP App HTML you author and test with
48631
+ \`manage_plugin\`.
48632
+
48175
48633
  Plan with persistent Tasks: MANDATORY until you load "task-tracking" with the
48176
48634
  \`skill\` tool, call neither \`manage_task\` nor any other task operation. Use
48177
48635
  persistent Tasks as the development planning surface and create one Task per
@@ -48179,7 +48637,16 @@ independently verifiable outcome (for example "manifest written", "handlers
48179
48637
  implemented", "UI added", "validated/tested via manage_plugin"). As you work,
48180
48638
  update each Task's status to reflect reality. Never use \`write_todos\`.
48181
48639
 
48182
- Author with the \`manage_plugin\` tool: validate, then create/update, then test.
48640
+ After the human confirms the design, author the plugin files directly with the filesystem tools, then verify with \`manage_plugin\`.
48641
+ 1. Scaffold once: \`manage_plugin { action: "create", type: "<type>", name }\` \u2014 it creates
48642
+ \`/root/.agents/plugins/<type>/\` with a stub \`plugin.json\`. It never carries content.
48643
+ 2. Write and edit every file yourself with \`write_file\` / \`edit_file\` (read back with
48644
+ \`read_file\`) under \`/root/.agents/plugins/<type>/\`: \`plugin.json\`, \`handlers/*.js\`, and
48645
+ (UI class) \`ui/*.html\`. Change one file at a time; edit in place instead of rewriting.
48646
+ 3. Verify in order: \`manage_plugin { action: "validate", type }\` (reads the files back from
48647
+ the sandbox) \u2192 \`manage_plugin { action: "reload", type }\` (the catalog caches, so a
48648
+ freshly written file is invisible to \`test\` until you reload) \u2192 \`manage_plugin
48649
+ { action: "test", type, target }\`.
48183
48650
  If a tool needs credentials, a human must configure the connection \u2014 use
48184
48651
  \`list_connections\` to check, and \`ask_user_to_clarify\` to ask the human to add
48185
48652
  the connector and give you its key. Only after a real \`connectionKey\` test
@@ -48188,14 +48655,15 @@ unverified (missing credentials), not as passing. Use \`shell_exec\` (the tool
48188
48655
  provided by the \`code_eval\` middleware) to run \`node handlers/x.js\` for manual
48189
48656
  checks when reproducing a failure.
48190
48657
 
48191
- After the plugin passes \`validate\` and its tests, deploy it as a resident
48192
- service: load "sandbox-plugins-deploy" and follow it. Choose \`onDemand\`
48658
+ Deployment is human-gated: do NOT deploy on your own after the tests pass.
48659
+ Only deploy when the human explicitly asks for it or approves it, then load
48660
+ "sandbox-plugins-deploy" and follow it. Choose \`onDemand\`
48193
48661
  (default) or \`alwaysOn\` for hot paths, call \`manage_plugin deploy\` with the
48194
48662
  chosen \`mode\`, then confirm with \`manage_plugin status\` (\`running\`). Only claim
48195
48663
  the plugin is deployed and serving once \`status\` reports \`running\`; if the
48196
48664
  environment reports no deployment service, say so and treat deployment as
48197
- unavailable \u2014 do NOT claim success. \`undeploy\` removes the service without
48198
- touching the plugin files.`;
48665
+ unavailable \u2014 do NOT claim success. Report the plugin as ready and let the human
48666
+ decide; \`undeploy\` removes the service without touching the plugin files.`;
48199
48667
 
48200
48668
  // src/middlewares/sandboxPluginsUi.ts
48201
48669
  var SANDBOX_PLUGINS_UI_RESOURCE = "ui://sandbox-plugins/app";
@@ -48371,6 +48839,7 @@ var SANDBOX_PLUGINS_APP_HTML = `<!doctype html>
48371
48839
  `;
48372
48840
 
48373
48841
  // src/middlewares/sandboxPluginsMiddleware.ts
48842
+ var PLUGINS_ROOT3 = "/root/.agents/plugins";
48374
48843
  var SANDBOX_PLUGINS_UI_REF = {
48375
48844
  kind: "plugin",
48376
48845
  pluginType: "sandbox-plugins",
@@ -48427,18 +48896,19 @@ var managePluginSchema = z73.object({
48427
48896
  "get",
48428
48897
  "validate",
48429
48898
  "create",
48430
- "update",
48431
48899
  "delete",
48432
48900
  "reload",
48433
48901
  "test",
48902
+ "backup",
48903
+ "restore",
48434
48904
  "deploy",
48435
48905
  "undeploy",
48436
48906
  "status",
48437
48907
  "deployments"
48438
48908
  ]),
48439
48909
  type: z73.string().optional(),
48440
- manifest: z73.record(z73.unknown()).optional(),
48441
- handlers: z73.record(z73.string()).optional(),
48910
+ name: z73.string().optional(),
48911
+ description: z73.string().optional(),
48442
48912
  mode: z73.enum(["onDemand", "alwaysOn"]).optional(),
48443
48913
  target: z73.object({
48444
48914
  kind: z73.enum(["tool", "connection-test", "connection-discover"]),
@@ -48452,12 +48922,12 @@ var sandboxPluginsPlugin = {
48452
48922
  type: "sandbox-plugins",
48453
48923
  category: "execution",
48454
48924
  name: "Sandbox Plugins",
48455
- description: "Author and manage tenant plugins stored in the sandbox: list/validate/create/update/delete/reload/test, and deploy/undeploy/status/deployments to run a plugin as a resident service. Tools are agent-callable JS handlers and may declare an optional interactive HTML UI (rendered as a sandboxed MCP App). Pairs with the Skill plugin for authoring guidance.",
48925
+ description: "Author and manage tenant plugins stored in the sandbox: list/validate/create/delete/reload/test/backup/restore, and deploy/undeploy/status/deployments to run a plugin as a resident service. Tools are agent-callable JS handlers and may declare an optional interactive HTML UI (rendered as a sandboxed MCP App). Pairs with the Skill plugin for authoring guidance.",
48456
48926
  recommends: ["skill"],
48457
48927
  tools: [
48458
48928
  {
48459
48929
  name: "manage_plugin",
48460
- description: "Author and manage tenant sandbox plugins (tools, optional per-tool HTML UI, connections, Open/MCP exposure, and resident deployment). Actions: list/get/validate/create/update/delete/reload/test, deploy/undeploy/status, and `deployments` to list every resident deployment for the tenant.",
48930
+ description: "Author and manage tenant sandbox plugins (tools, optional per-tool HTML UI, connections, Open/MCP exposure, and resident deployment). Actions: list/get/validate/create/delete/reload/test/backup/restore, deploy/undeploy/status, and `deployments` to list every resident deployment for the tenant.",
48461
48931
  ui: { resource: SANDBOX_PLUGINS_UI_RESOURCE, displayMode: "inline" }
48462
48932
  }
48463
48933
  ],
@@ -48484,7 +48954,7 @@ var sandboxPluginsPlugin = {
48484
48954
  id: "sandbox-plugins",
48485
48955
  type: "sandbox-plugins",
48486
48956
  name: "Sandbox Plugins",
48487
- description: "Author and manage tenant plugins (list/validate/create/update/delete/reload/test/deploy/undeploy/status)",
48957
+ description: "Author and manage tenant plugins (list/validate/create/delete/reload/test/backup/restore/deploy/undeploy/status)",
48488
48958
  enabled: true,
48489
48959
  config: {}
48490
48960
  },
@@ -48504,6 +48974,14 @@ var sandboxPluginsPlugin = {
48504
48974
  enabled: true,
48505
48975
  config: {}
48506
48976
  },
48977
+ {
48978
+ id: "widget",
48979
+ type: "widget",
48980
+ name: "Widget",
48981
+ description: "Align on the plugin design before building it: show the shape you intend to build (tools, IO, connections, UI) and a UI mock for the human to correct scope before files exist.",
48982
+ enabled: true,
48983
+ config: {}
48984
+ },
48507
48985
  {
48508
48986
  id: "ask_user_to_clarify",
48509
48987
  type: "ask_user_to_clarify",
@@ -48544,6 +49022,7 @@ var sandboxPluginsPlugin = {
48544
49022
  return JSON.stringify({
48545
49023
  plugins: loaded.plugins.map((p) => ({
48546
49024
  type: p.meta.type,
49025
+ path: `${PLUGINS_ROOT3}/${p.meta.type}`,
48547
49026
  name: p.meta.name,
48548
49027
  tools: p.meta.tools?.map((t) => t.name) ?? [],
48549
49028
  source: p.meta.source
@@ -48554,32 +49033,87 @@ var sandboxPluginsPlugin = {
48554
49033
  }
48555
49034
  case "get": {
48556
49035
  if (!input.type) return "type is required";
48557
- return await catalog2.readPluginFile(tenantId2, input.type, "plugin.json");
49036
+ const content = await catalog2.readPluginFile(tenantId2, input.type, "plugin.json");
49037
+ let manifest = content;
49038
+ try {
49039
+ manifest = JSON.parse(content);
49040
+ } catch {
49041
+ }
49042
+ return JSON.stringify({
49043
+ type: input.type,
49044
+ path: `${PLUGINS_ROOT3}/${input.type}`,
49045
+ manifest
49046
+ });
48558
49047
  }
48559
49048
  case "reload": {
48560
49049
  const loaded = await catalog2.reload(tenantId2);
48561
49050
  void publishPluginCatalogChanged(tenantId2);
48562
49051
  return JSON.stringify({ reloaded: true, plugins: loaded.plugins.length, diagnostics: loaded.diagnostics });
48563
49052
  }
48564
- case "create":
48565
- case "update": {
48566
- if (!input.type || !input.manifest) return "type and manifest are required";
48567
- const handlers = input.handlers ?? {};
49053
+ case "backup": {
49054
+ if (!input.type) return "type is required";
49055
+ try {
49056
+ const result = await catalog2.backupPlugin(tenantId2, input.type);
49057
+ if (result.files === 0) {
49058
+ return JSON.stringify({ backedUp: false, error: "nothing to back up (plugin has no files)" });
49059
+ }
49060
+ return JSON.stringify({ backedUp: true, files: result.files });
49061
+ } catch (err) {
49062
+ return JSON.stringify({ backedUp: false, error: errorText(err) });
49063
+ }
49064
+ }
49065
+ case "restore": {
49066
+ if (!input.type) return "type is required";
49067
+ let restored;
49068
+ let failure3;
49069
+ try {
49070
+ restored = await catalog2.restorePlugin(tenantId2, input.type);
49071
+ } catch (err) {
49072
+ failure3 = errorText(err);
49073
+ }
49074
+ try {
49075
+ await catalog2.reload(tenantId2);
49076
+ void publishPluginCatalogChanged(tenantId2);
49077
+ } catch {
49078
+ }
49079
+ if (failure3) return JSON.stringify({ restored: false, error: failure3 });
49080
+ return JSON.stringify({ restored: true, files: restored?.files ?? 0 });
49081
+ }
49082
+ case "create": {
49083
+ if (!input.type) return "type is required";
48568
49084
  const known = new Set(
48569
49085
  (catalog2.getCached(tenantId2)?.plugins ?? []).map((p) => p.meta.type).filter((t) => t !== input.type)
48570
49086
  );
49087
+ const stub = {
49088
+ schemaVersion: 1,
49089
+ type: input.type,
49090
+ name: input.name ?? input.type,
49091
+ description: input.description ?? "",
49092
+ version: "0.1.0",
49093
+ tools: []
49094
+ };
48571
49095
  const check = validateSandboxPluginManifest({
48572
- raw: input.manifest,
49096
+ raw: stub,
48573
49097
  dirName: input.type,
48574
49098
  reservedTypes: new Set(PluginRegistry.list()),
48575
49099
  knownTypes: known,
48576
- handlerExists: (rel) => rel in handlers
49100
+ handlerExists: () => true
48577
49101
  });
48578
- if (!check.manifest) return JSON.stringify({ written: false, diagnostics: check.diagnostics });
48579
- await catalog2.writePlugin(tenantId2, input.type, input.manifest, handlers);
49102
+ if (!check.manifest) {
49103
+ return JSON.stringify({ created: false, diagnostics: check.diagnostics });
49104
+ }
49105
+ const files = await catalog2.listPluginFiles(tenantId2, input.type);
49106
+ if (files.includes("plugin.json")) {
49107
+ return JSON.stringify({ created: false, error: "plugin already exists" });
49108
+ }
49109
+ await catalog2.writePlugin(tenantId2, input.type, check.manifest, {});
48580
49110
  const loaded = await catalog2.reload(tenantId2);
48581
49111
  void publishPluginCatalogChanged(tenantId2);
48582
- return JSON.stringify({ written: true, diagnostics: loaded.diagnostics, hash: loaded.hash });
49112
+ return JSON.stringify({
49113
+ created: true,
49114
+ path: `${PLUGINS_ROOT3}/${input.type}`,
49115
+ diagnostics: loaded.diagnostics
49116
+ });
48583
49117
  }
48584
49118
  case "delete": {
48585
49119
  if (!input.type) return "type is required";
@@ -48590,18 +49124,8 @@ var sandboxPluginsPlugin = {
48590
49124
  }
48591
49125
  case "validate": {
48592
49126
  if (!input.type) return "type is required";
48593
- const handlers = input.handlers ?? {};
48594
- const known = new Set(
48595
- (catalog2.getCached(tenantId2)?.plugins ?? []).map((p) => p.meta.type).filter((t) => t !== input.type)
48596
- );
48597
- const result = validateSandboxPluginManifest({
48598
- raw: input.manifest ?? {},
48599
- dirName: input.type,
48600
- reservedTypes: new Set(PluginRegistry.list()),
48601
- knownTypes: known,
48602
- handlerExists: (rel) => rel in handlers
48603
- });
48604
- return JSON.stringify({ diagnostics: result.diagnostics, valid: Boolean(result.manifest) });
49127
+ const result = await catalog2.validatePluginFromDisk(tenantId2, input.type);
49128
+ return JSON.stringify({ diagnostics: result.diagnostics, valid: result.valid });
48605
49129
  }
48606
49130
  case "test": {
48607
49131
  if (!input.type || !input.target) return "type and target are required";
@@ -48675,7 +49199,16 @@ var sandboxPluginsPlugin = {
48675
49199
  assistantId: typeof runCfg.assistant_id === "string" ? runCfg.assistant_id : void 0
48676
49200
  }
48677
49201
  });
48678
- return handlerResultText(redactSecretValues(result, secrets));
49202
+ const text = handlerResultText(redactSecretValues(result, secrets));
49203
+ const ui = toolDef.ui;
49204
+ if (!ui?.resource) return text;
49205
+ const uiRef = {
49206
+ kind: "plugin",
49207
+ pluginType: input.type,
49208
+ resource: ui.resource,
49209
+ displayMode: ui.displayMode ?? "inline"
49210
+ };
49211
+ return appendUiFence(text, uiRef);
48679
49212
  } catch (err) {
48680
49213
  return JSON.stringify(redactSecretValues({ ok: false, error: errorText(err) }, secrets));
48681
49214
  }
@@ -48747,7 +49280,7 @@ var sandboxPluginsPlugin = {
48747
49280
  },
48748
49281
  {
48749
49282
  name: "manage_plugin",
48750
- description: "Author and manage tenant sandbox plugins (list/get/validate/create/update/delete/reload/test) and deploy/undeploy/status a plugin as a resident service.",
49283
+ description: "Author and manage tenant sandbox plugins (list/get/validate/create/delete/reload/test/backup/restore) and deploy/undeploy/status a plugin as a resident service.",
48751
49284
  schema: managePluginSchema
48752
49285
  }
48753
49286
  )
@@ -49923,6 +50456,7 @@ export {
49923
50456
  buildTableName,
49924
50457
  buildTaskMutationFallback,
49925
50458
  buildTaskNormalizationFallback,
50459
+ canConvergeOrphan,
49926
50460
  checkEmptyContent,
49927
50461
  clearEncryptionKeyCache,
49928
50462
  clearEvalRunService,
@@ -49972,6 +50506,7 @@ export {
49972
50506
  createTaskLifecycleService,
49973
50507
  createTaskMiddleware,
49974
50508
  createTaskRunBridge,
50509
+ createTaskRunLiveness,
49975
50510
  createTeamMiddleware,
49976
50511
  createTeammateTools,
49977
50512
  createTrustedAgentMessageDispatcher,
@@ -50069,6 +50604,7 @@ export {
50069
50604
  isMcpConnectionError,
50070
50605
  isPluginSkillAuthorizedInSandbox,
50071
50606
  isPluginSkillManagedInSandbox,
50607
+ isTaskRunLeaseHeld,
50072
50608
  isUsingDefaultKey,
50073
50609
  isValidCronExpression,
50074
50610
  isValidSandboxName,