@axiom-lattice/core 7.0.3 → 8.0.1

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,
@@ -31102,6 +31313,12 @@ var _AgentLatticeManager = class _AgentLatticeManager extends BaseLatticeManager
31102
31313
  * {@link buildingKeys}.
31103
31314
  */
31104
31315
  this.buildContext = new AsyncLocalStorage();
31316
+ /**
31317
+ * Store-backed assistant ids previously reconciled per tenant. Used to
31318
+ * unregister assistants that were deleted on another replica without
31319
+ * touching built-in / plugin agents (which are re-ensured on every reload).
31320
+ */
31321
+ this.storedAssistantKeys = /* @__PURE__ */ new Map();
31105
31322
  this.subscribeToAssistantEvents();
31106
31323
  }
31107
31324
  /**
@@ -31330,11 +31547,29 @@ var _AgentLatticeManager = class _AgentLatticeManager extends BaseLatticeManager
31330
31547
  try {
31331
31548
  const storeLattice = getStoreLattice("default", "assistant");
31332
31549
  const assistants = await storeLattice.store.getAllAssistants(tenantId2);
31550
+ const currentIds = /* @__PURE__ */ new Set();
31333
31551
  for (const assistant of assistants) {
31334
- if (this.hasAgentLatticeWithTenant(tenantId2, assistant.id)) continue;
31552
+ currentIds.add(assistant.id);
31335
31553
  const config = assistantToConfig(assistant);
31336
- this.registerLatticeWithTenant(tenantId2, config);
31554
+ const existing = this.getAgentConfigWithTenant(tenantId2, assistant.id);
31555
+ if (existing === void 0) {
31556
+ this.registerLatticeWithTenant(tenantId2, config);
31557
+ continue;
31558
+ }
31559
+ if (JSON.stringify(existing) !== JSON.stringify(config)) {
31560
+ this.removeWithTenant(tenantId2, assistant.id);
31561
+ this.registerLatticeWithTenant(tenantId2, config);
31562
+ }
31563
+ }
31564
+ const previousIds = this.storedAssistantKeys.get(tenantId2);
31565
+ if (previousIds) {
31566
+ for (const id of previousIds) {
31567
+ if (!currentIds.has(id)) {
31568
+ this.removeWithTenant(tenantId2, id);
31569
+ }
31570
+ }
31337
31571
  }
31572
+ this.storedAssistantKeys.set(tenantId2, currentIds);
31338
31573
  } catch (error) {
31339
31574
  console.error(`Failed to initialize stored assistants for tenant ${tenantId2}:`, error);
31340
31575
  }
@@ -38375,7 +38610,11 @@ var TaskLifecycleService = class {
38375
38610
  expectedStatuses: ["completed", "cancelled"],
38376
38611
  updates: { status: "in_progress", failureReason: null, result: null },
38377
38612
  summary: "Task reopened",
38378
- detail: { type: "reopen" }
38613
+ detail: { type: "reopen" },
38614
+ // Preserve an existing thread by default (the project runtime reuses the
38615
+ // canonical Task Thread); a dispatch that allocated a fresh run thread
38616
+ // opts in to replacing it.
38617
+ replaceThread: input.replaceThread ?? false
38379
38618
  });
38380
38619
  }
38381
38620
  /**
@@ -40236,6 +40475,39 @@ function createTaskRunBridge(deps) {
40236
40475
  transitioned: true
40237
40476
  };
40238
40477
  }
40478
+ if (task.status === "completed" || task.status === "cancelled") {
40479
+ if (task.ownerType !== "agent") {
40480
+ return {
40481
+ success: false,
40482
+ code: "AGENT_TASK_REQUIRED",
40483
+ error: "Lifecycle transitions only support agent tasks."
40484
+ };
40485
+ }
40486
+ const reopened = await deps.lifecycle.reopenTask({
40487
+ tenantId: tenantId2,
40488
+ taskId: task.id,
40489
+ actor,
40490
+ ...threadId !== void 0 ? { threadId } : {},
40491
+ // The caller allocated a fresh run thread for this dispatch.
40492
+ replaceThread: true
40493
+ });
40494
+ if (!reopened.success) {
40495
+ return {
40496
+ success: false,
40497
+ code: reopened.code,
40498
+ error: reopened.error,
40499
+ ...reopened.hint !== void 0 ? { hint: reopened.hint } : {},
40500
+ ...reopened.retryable !== void 0 ? { retryable: reopened.retryable } : {}
40501
+ };
40502
+ }
40503
+ return {
40504
+ success: true,
40505
+ task: reopened.task,
40506
+ warnings: reopened.warnings ?? [],
40507
+ mutated: true,
40508
+ transitioned: true
40509
+ };
40510
+ }
40239
40511
  return { success: true, task, warnings, mutated: false, transitioned: false };
40240
40512
  },
40241
40513
  async settleRun(input) {
@@ -40408,6 +40680,45 @@ async function tryAcquireTaskRun(input) {
40408
40680
  };
40409
40681
  }
40410
40682
 
40683
+ // src/services/taskLiveness.ts
40684
+ function registeredLeaseStore() {
40685
+ try {
40686
+ return storeLatticeManager.getStoreLattice("default", "lease").store;
40687
+ } catch {
40688
+ return void 0;
40689
+ }
40690
+ }
40691
+ async function isTaskRunLeaseHeld(task) {
40692
+ const store = registeredLeaseStore();
40693
+ if (!store) return false;
40694
+ try {
40695
+ if (await store.get(taskRunLeaseKey(task.id))) return true;
40696
+ const threadId = typeof task.context?.thread_id === "string" ? task.context.thread_id : void 0;
40697
+ if (threadId && task.ownerId && await store.get(threadRunLeaseKey(task.tenantId, task.ownerId, threadId))) {
40698
+ return true;
40699
+ }
40700
+ return false;
40701
+ } catch {
40702
+ return true;
40703
+ }
40704
+ }
40705
+ function createTaskRunLiveness() {
40706
+ return async (task) => taskRunRegistry.isLive(task.id) || isTaskRunLeaseHeld(task);
40707
+ }
40708
+ function isMultiReplicaDeclared() {
40709
+ const id = process.env.AXIOM_REPLICA_ID;
40710
+ return typeof id === "string" && id.trim() !== "";
40711
+ }
40712
+ function isSharedLeaseStore() {
40713
+ const store = registeredLeaseStore();
40714
+ const name = store?.constructor?.name;
40715
+ return typeof name === "string" && !name.startsWith("InMemory");
40716
+ }
40717
+ function canConvergeOrphan() {
40718
+ if (!isMultiReplicaDeclared()) return true;
40719
+ return isSharedLeaseStore();
40720
+ }
40721
+
40411
40722
  // src/services/ExecutionAuthorityRegistry.ts
40412
40723
  var ManagedTaskStartError = class extends Error {
40413
40724
  /**
@@ -40491,7 +40802,10 @@ async function prepareTaskDispatch(deps, input) {
40491
40802
  }
40492
40803
  };
40493
40804
  }
40494
- if (task.status === "completed" || task.status === "cancelled") {
40805
+ const managedAuthority = executionAuthorityRegistry.resolve(task);
40806
+ const terminal = task.status === "completed" || task.status === "cancelled";
40807
+ const reopenableTerminal = terminal && task.ownerType === "agent" && !managedAuthority;
40808
+ if (terminal && !reopenableTerminal) {
40495
40809
  return {
40496
40810
  kind: "failure",
40497
40811
  failure: {
@@ -40515,19 +40829,18 @@ async function prepareTaskDispatch(deps, input) {
40515
40829
  }
40516
40830
  };
40517
40831
  }
40518
- if (task.status !== "pending" && task.status !== "interrupted" && task.status !== "failed") {
40832
+ if (task.status !== "pending" && task.status !== "interrupted" && task.status !== "failed" && !reopenableTerminal) {
40519
40833
  return {
40520
40834
  kind: "failure",
40521
40835
  failure: {
40522
40836
  success: false,
40523
40837
  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.`,
40838
+ error: "Only a `pending`, `interrupted`, `failed`, or reopenable terminal task can be dispatched.",
40839
+ 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
40840
  taskId: task.id
40527
40841
  }
40528
40842
  };
40529
40843
  }
40530
- const managedAuthority = executionAuthorityRegistry.resolve(task);
40531
40844
  if (managedAuthority) {
40532
40845
  if (managedAuthority.onManagedStart) {
40533
40846
  try {
@@ -40697,7 +41010,8 @@ function persistedExecutionObservation(item, attempted) {
40697
41010
  }
40698
41011
  function buildWorkerMessage(task, parent, dependencies) {
40699
41012
  const sections2 = [
40700
- "Execute the following persistent task. Return your result as an observation; do not assume the task is complete.",
41013
+ `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.`,
41014
+ "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
41015
  `Persisted Task ID: \`${task.id}\``,
40702
41016
  `# Task: ${task.title}`,
40703
41017
  task.description?.trim() || "No task description was provided."
@@ -40729,9 +41043,17 @@ function createTaskExecutionService(deps) {
40729
41043
  taskStore: deps.taskStore,
40730
41044
  workItemStore: deps.workItemStore
40731
41045
  });
41046
+ const isTaskHeldByLiveRun = deps.isTaskHeldByLiveRun ?? createTaskRunLiveness();
40732
41047
  const run = async (plan) => {
40733
41048
  const { tenantId: tenantId2, agentId, actor, parent, completedDependencies } = plan;
40734
41049
  let task = plan.task;
41050
+ const readTaskStatus = async () => {
41051
+ try {
41052
+ return (await deps.taskStore.getById(tenantId2, task.id))?.status;
41053
+ } catch {
41054
+ return void 0;
41055
+ }
41056
+ };
40735
41057
  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
41058
  const storedRunId = typeof task.context?.run_id === "string" && task.context.run_id.trim() !== "" ? task.context.run_id : void 0;
40737
41059
  let reuseCurrentRun = false;
@@ -40790,7 +41112,7 @@ function createTaskExecutionService(deps) {
40790
41112
  task = transfer.task;
40791
41113
  }
40792
41114
  const warnings = [];
40793
- if (task.status === "pending" || task.status === "interrupted" || task.status === "failed") {
41115
+ if (task.status === "pending" || task.status === "interrupted" || task.status === "failed" || task.status === "completed" || task.status === "cancelled") {
40794
41116
  const begun = await bridge.beginRun({
40795
41117
  tenantId: tenantId2,
40796
41118
  task,
@@ -40917,6 +41239,7 @@ function createTaskExecutionService(deps) {
40917
41239
  ...warnings.length > 0 ? { warnings } : {}
40918
41240
  };
40919
41241
  }
41242
+ const settledStatus = await readTaskStatus();
40920
41243
  return {
40921
41244
  success: false,
40922
41245
  code: failureCode,
@@ -40925,6 +41248,7 @@ function createTaskExecutionService(deps) {
40925
41248
  agentId,
40926
41249
  threadId,
40927
41250
  executionResultId: eventKey,
41251
+ ...settledStatus !== void 0 ? { taskStatus: settledStatus } : {},
40928
41252
  ...warnings.length > 0 ? { warnings } : {}
40929
41253
  };
40930
41254
  } catch (persistenceError) {
@@ -40973,6 +41297,7 @@ function createTaskExecutionService(deps) {
40973
41297
  ...warnings.length > 0 ? { warnings } : {}
40974
41298
  };
40975
41299
  }
41300
+ const taskStatus = await readTaskStatus();
40976
41301
  return {
40977
41302
  success: true,
40978
41303
  taskId: task.id,
@@ -40980,6 +41305,7 @@ function createTaskExecutionService(deps) {
40980
41305
  threadId,
40981
41306
  observation: validated.observation,
40982
41307
  executionResultId: eventKey,
41308
+ ...taskStatus !== void 0 ? { taskStatus } : {},
40983
41309
  ...warnings.length > 0 ? { warnings } : {}
40984
41310
  };
40985
41311
  } catch (error) {
@@ -41000,6 +41326,23 @@ function createTaskExecutionService(deps) {
41000
41326
  };
41001
41327
  return {
41002
41328
  start: async (input) => {
41329
+ let current;
41330
+ try {
41331
+ current = await deps.taskStore.getById(input.tenantId, input.taskId);
41332
+ } catch {
41333
+ current = null;
41334
+ }
41335
+ if (current && current.status === "in_progress" && current.ownerType === "agent" && canConvergeOrphan() && !executionAuthorityRegistry.resolve(current) && !await isTaskHeldByLiveRun(current)) {
41336
+ try {
41337
+ await lifecycle.failTask({
41338
+ tenantId: input.tenantId,
41339
+ taskId: current.id,
41340
+ actor: input.actor,
41341
+ failureReason: "Run is no longer executing this task (no_executor)"
41342
+ });
41343
+ } catch {
41344
+ }
41345
+ }
41003
41346
  const prepared = await prepareTaskDispatch(deps, input);
41004
41347
  if (prepared.kind === "managed") return prepared.result;
41005
41348
  if (prepared.kind === "failure") return prepared.failure;
@@ -43144,9 +43487,85 @@ Use \`imagine_html\`. Wrap the entire thing in a single raised card. All content
43144
43487
  </div>
43145
43488
  </div>
43146
43489
  \`\`\``;
43490
+ var ANNOTATABLE_MOCKUP = `## Annotatable drafts (reviewable mockups)
43491
+
43492
+ 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.)
43493
+
43494
+ - 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\`.
43495
+ - 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.
43496
+ - 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).
43497
+ - Handle \`contextmenu\` with event delegation; \`preventDefault()\` + \`stopPropagation()\` (this replaces the native browser menu, which is fine on a draft).
43498
+ - 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.
43499
+ - 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.
43500
+
43501
+ \`\`\`html
43502
+ <style>
43503
+ .anno{position:relative}
43504
+ .anno:hover{outline:1px dashed var(--color-border-info);outline-offset:-2px;border-radius:var(--border-radius-md)}
43505
+ .anno.marked{outline:1.5px solid var(--color-text-info);outline-offset:-2px}
43506
+ .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}
43507
+ .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}
43508
+ .modal .mt{font-size:14px;font-weight:500;margin-bottom:2px}
43509
+ .modal .ms{font-size:11px;color:var(--color-text-tertiary);margin-bottom:10px}
43510
+ .modal textarea{width:100%;box-sizing:border-box;height:auto;min-height:72px;padding:8px 10px;resize:vertical}
43511
+ .modal .mrow{display:flex;gap:8px;justify-content:flex-end;margin-top:10px}
43512
+ .primary{background:var(--color-text-info);color:var(--color-background-primary);border-color:var(--color-text-info)}
43513
+ .primary:hover{background:var(--color-text-info);opacity:.9}
43514
+ </style>
43515
+ <div class="panel" style="position:relative">
43516
+ <div style="font-size:12px;color:var(--color-text-tertiary);margin-bottom:8px">Right-click an element to comment</div>
43517
+ <div class="anno" data-el="Title" data-val="Q4 revenue">Q4 revenue</div>
43518
+ <div class="anno" data-el="Primary action" data-val="Export">Export</div>
43519
+ </div>
43520
+ <script>
43521
+ var panel = document.querySelector('.panel');
43522
+ var current = null;
43523
+ function closeBox(){
43524
+ if (current) current.classList.remove('marked');
43525
+ var ov = panel.querySelector('.overlay');
43526
+ if (ov) ov.remove();
43527
+ current = null;
43528
+ }
43529
+ function openBox(el){
43530
+ closeBox();
43531
+ current = el;
43532
+ el.classList.add('marked');
43533
+ var ov = document.createElement('div');
43534
+ ov.className = 'overlay';
43535
+ ov.innerHTML = '<div class="modal">' +
43536
+ '<div class="mt">Note: ' + el.dataset.el + '</div>' +
43537
+ (el.dataset.val ? '<div class="ms">Current: ' + el.dataset.val + '</div>' : '') +
43538
+ '<textarea id="cmt" placeholder="What should change? e.g. bolder bar / expand by default / add a year filter"></textarea>' +
43539
+ '<div class="mrow"><button id="cancel">Cancel</button><button class="primary" id="send">Send note \u2197</button></div>' +
43540
+ '</div>';
43541
+ panel.appendChild(ov);
43542
+ ov.querySelector('#cmt').focus();
43543
+ ov.querySelector('#send').onclick = function(){
43544
+ var text = ov.querySelector('#cmt').value.trim();
43545
+ if (text && typeof sendPrompt === 'function') {
43546
+ sendPrompt('[design note] ' + el.dataset.el + (el.dataset.val ? ' (current: ' + el.dataset.val + ')' : '') + ' \u2014 change: ' + text);
43547
+ }
43548
+ closeBox();
43549
+ };
43550
+ ov.querySelector('#cancel').onclick = closeBox;
43551
+ ov.addEventListener('click', function(e){ if (e.target === ov) closeBox(); });
43552
+ }
43553
+ document.addEventListener('keydown', function(e){ if (e.key === 'Escape') closeBox(); });
43554
+ panel.addEventListener('contextmenu', function(e){
43555
+ var el = e.target.closest('.anno');
43556
+ if (!el) return;
43557
+ e.preventDefault(); e.stopPropagation();
43558
+ openBox(el);
43559
+ });
43560
+ </script>
43561
+ \`\`\`
43562
+
43563
+ - 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\`).
43564
+
43565
+ Keep it lightweight: annotate only meaningful elements, reuse one overlay, no libraries, and put \`<script>\` last so the draft streams in top-down.`;
43147
43566
  var MODULE_SECTIONS = {
43148
43567
  art: [SVG_SETUP, ART_AND_ILLUSTRATION],
43149
- mockup: [UI_COMPONENTS, COLOR_PALETTE],
43568
+ mockup: [UI_COMPONENTS, COLOR_PALETTE, ANNOTATABLE_MOCKUP],
43150
43569
  interactive: [UI_COMPONENTS, COLOR_PALETTE],
43151
43570
  chart: [UI_COMPONENTS, COLOR_PALETTE, CHARTS_CHART_JS],
43152
43571
  diagram: [COLOR_PALETTE, SVG_SETUP, DIAGRAM_TYPES]
@@ -43653,7 +44072,7 @@ function renderTaskBinding(taskId) {
43653
44072
 
43654
44073
  ## Bound Task
43655
44074
 
43656
- You are executing task \`${taskId}\`. Read its contract with \`manage_task get\`, update it as you work, and complete it when done.
44075
+ 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
44076
  `;
43658
44077
  }
43659
44078
  function createBoundTaskLifecycle() {
@@ -44172,7 +44591,7 @@ var manageTaskSchema = z70.object({
44172
44591
  title: z70.string().optional().describe("Task title (required for create)"),
44173
44592
  description: z70.string().optional().describe("Task description in Markdown"),
44174
44593
  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"),
44594
+ 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
44595
  dueDate: z70.string().optional().describe("Due date (ISO 8601 format)"),
44177
44596
  metadata: z70.record(z70.unknown()).optional().describe("Structured metadata (e.g. projectId, module)"),
44178
44597
  metadataFilter: z70.record(
@@ -45763,8 +46182,8 @@ ${TASK_CONVERGENCE_GUIDANCE}
45763
46182
  - update: Update a task's content fields (title/description/priority/dueDate/metadata/parentId/sourceId/context/ownerId/result/failureReason/requireReview/dependencies/files)
45764
46183
  Status changes can also be done here, but use set_status for a simpler dedicated status update.
45765
46184
  - 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)
46185
+ 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.
46186
+ 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
46187
  To mark failed: set_status with status='failed', failureReason='...'
45769
46188
  To pause for input: set_status with status='interrupted', summary='what is needed'
45770
46189
  Valid status values: pending, in_progress, review, failed, interrupted, completed, cancelled.
@@ -45781,16 +46200,16 @@ Report existing keys directly; set 'isNew: true' to add a key the owner does not
45781
46200
  track yet. Belief is evidence metadata: it never blocks a completion, and an
45782
46201
  unmatched or malformed entry is recorded without changing the owner's table.
45783
46202
  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
46203
+ execution is dispatched. Set \`in_progress\` yourself ONLY for a task THIS run
46204
+ executes; a task you delegate stays \`pending\` and is claimed automatically by
46205
+ \`task\` dispatch. Use \`task { taskId, agentId }\` to delegate a subtask
45787
46206
  to an expert \u2014 never re-dispatch the task you are processing.`,
45788
46207
  schema: manageTaskSchema
45789
46208
  }
45790
46209
  ),
45791
46210
  tool64(handleStartTask, {
45792
46211
  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.`,
46212
+ 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
46213
  schema: startTaskSchema
45795
46214
  })
45796
46215
  ]
@@ -47493,7 +47912,7 @@ A plugin is the SET of classes it needs, e.g. \`connection + tools + skills\` or
47493
47912
  | Tool shows an interactive panel | \`sandbox-plugins-ui\` |
47494
47913
  | Tool orchestrates platform tools/agents | \`sandbox-plugins-consume-open\` |
47495
47914
  | Tool callable by external MCP clients | \`sandbox-plugins-expose\` |
47496
- | Run as a long-lived resident service | \`sandbox-plugins-deploy\` |
47915
+ | Run as a long-lived resident service (only on the human's explicit request) | \`sandbox-plugins-deploy\` |
47497
47916
  | Validate / test / debug | \`sandbox-plugins-testing\` |
47498
47917
 
47499
47918
  ## Agents (not a supported authoring path)
@@ -47535,8 +47954,8 @@ chart, table, viewer\u2026). It can also call the plugin's OWN tools back from t
47535
47954
  - It is NOT the tool's JSON \`schema\` (that is the input contract for the model).
47536
47955
  - It is NOT the tool \`description\`.
47537
47956
  - 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\`.
47957
+ - Saying "add a UI" means: add a \`ui\` field to the tool in \`plugin.json\` and write
47958
+ \`ui/<tool>.html\` (with \`write_file\`). Details in \`sandbox-plugins-ui\`.
47540
47959
 
47541
47960
  ## Decide first \u2014 clarify these with the user before writing files
47542
47961
  Ask for anything not already stated; do not guess silently:
@@ -47551,7 +47970,7 @@ Ask for anything not already stated; do not guess silently:
47551
47970
  multi-select? connection test needed? \u2192 read \`sandbox-plugins-connection\`.
47552
47971
  - **UI**: should the tool show an interactive panel instead of only text? what
47553
47972
  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
47973
+ \`sandbox-plugins-ui\` and add \`ui\` + \`ui/<tool>.html\` (step 6 below). If the
47555
47974
  user says no UI, skip it \u2014 text-only is fine.
47556
47975
  - **Skills**: procedure or knowledge? who consumes it? trigger phrase? tied to a
47557
47976
  specific tool?
@@ -47584,13 +48003,34 @@ Ask for anything not already stated; do not guess silently:
47584
48003
  \`\`\`
47585
48004
 
47586
48005
  ## 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\`
48006
+ 1. **Design first, and iterate \u2014 then get explicit approval before you build (non-trivial plugins).**
48007
+ Before writing any file, produce the design and review it with the human. It has two parts,
48008
+ with different treatments:
48009
+ - **Functional design** (always): an **interactive** component map of what you intend to
48010
+ build \u2014 the tools, their inputs/outputs, whether a connection is needed, whether a UI
48011
+ is needed, and how the agent calls them. Make it click-to-explore (e.g. click a tool to
48012
+ reveal its handler / IO / connection); it is a map to **confirm**, not to annotate.
48013
+ - **UI design** (only when the UI class applies): an **annotatable** mock of the tool's UI \u2014
48014
+ the human right-clicks elements to leave notes (see \`sandbox-plugins-ui\` \u2192
48015
+ "Reviewable drafts").
48016
+ Show both drafts with the \`widget\` middleware, then run the review loop:
48017
+ - Ask the human to review (say how to comment). On each round of comments, **restate the
48018
+ feedback** (element + requested change), then **regenerate the draft with the feedback and
48019
+ present it again** \u2014 comments are "revise and re-show", never "start building".
48020
+ - **Do NOT scaffold or write any file during the loop.** Implement only after the human
48021
+ **explicitly approves**, and ask for that go-ahead yourself before you start.
48022
+ Skip the drafts only for a trivial single-tool plugin whose shape is already agreed \u2014 but
48023
+ still confirm the goal.
48024
+ 2. Scaffold with \`manage_plugin { action: "create", type, name }\` \u2014 creates
48025
+ \`/root/.agents/plugins/<type>/\` with a stub \`plugin.json\`. It carries no content.
48026
+ 3. Read \`sandbox-plugins-manifest\` and write \`plugin.json\` with \`write_file\`.
48027
+ 4. Read \`sandbox-plugins-handlers\` and write each handler with \`write_file\`.
48028
+ 5. If the plugin needs credentials: read \`sandbox-plugins-connection\`.
48029
+ 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\`.
48030
+ 7. If a tool must be callable by external MCP clients: read \`sandbox-plugins-expose\`
47592
48031
  and add \`expose: true\` (plus \`readOnly\`/\`destructive\` accuracy hints).
47593
- 6. Read \`sandbox-plugins-testing\` and iterate using \`manage_plugin\`.
48032
+ 8. Read \`sandbox-plugins-testing\` and iterate: edit files with \`edit_file\`, then
48033
+ \`validate\` \u2192 \`reload\` \u2192 \`test\`.
47594
48034
 
47595
48035
  Templates: \`templates/plugin.json\`, \`templates/handler.js\`, \`templates/test.js\`, \`templates/discover.js\`, \`templates/ui.html\`.
47596
48036
  `,
@@ -47652,6 +48092,20 @@ description: Add an interactive HTML UI to a tenant plugin tool (rendered as a s
47652
48092
  A tool can declare an HTML file that Axiom renders as a sandboxed MCP App when
47653
48093
  the tool runs. The app can call the plugin's OWN tools back.
47654
48094
 
48095
+ ## Reviewable drafts (before you build the real UI)
48096
+ When you show a **UI mock with \`show_widget\`** for the user to react to (the design
48097
+ step, before writing \`ui/<tool>.html\`), make it **annotatable**: the user right-clicks
48098
+ any element to leave a comment that comes back to you as a message. Turn it on by
48099
+ default for review drafts and include a small visible hint (e.g. "\u53F3\u952E\u4EFB\u610F\u5143\u7D20\u6279\u6CE8");
48100
+ the \`widget\` mockup guidelines have the pattern. Omit it only when the draft is meant
48101
+ to be purely experienced (a live demo with no review) \u2014 then leave out the hint and
48102
+ the handlers. This is only for the **mock**: the real \`ui/<tool>.html\` is the plugin's
48103
+ product and is never annotatable.
48104
+ A round of annotations is **NOT approval**: restate the comments you received, regenerate
48105
+ the mock from them, and show it again, iterating until the human is satisfied. Build
48106
+ \`ui/<tool>.html\` only after the human **explicitly approves** \u2014 and ask for that go-ahead
48107
+ yourself.
48108
+
47655
48109
  ## Declare it
47656
48110
  \`\`\`
47657
48111
  /root/.agents/plugins/<type>/
@@ -47684,11 +48138,10 @@ the tool runs. The app can call the plugin's OWN tools back.
47684
48138
  - Omit \`csp\` for the restrictive default (\`default-src 'none'\`). WebAssembly is allowed by default.
47685
48139
 
47686
48140
  ## 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\`.
48141
+ Write the HTML to \`/root/.agents/plugins/<type>/ui/<tool>.html\` with \`write_file\`,
48142
+ then call \`manage_plugin { action: "reload", type }\` so the host serves the new HTML.
48143
+ The file must be declared by a tool's \`ui.resource\` in \`plugin.json\`, otherwise it is
48144
+ treated as missing (\`TOOL_UI\`).
47692
48145
 
47693
48146
  ## How it behaves at runtime
47694
48147
  - 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 +48237,7 @@ grants apply to the UI too). See \`sandbox-plugins-expose\`.
47784
48237
  ## Gotchas
47785
48238
  - \`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
48239
  - \`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.
48240
+ - 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
48241
 
47789
48242
  Start from \`templates/ui.html\`.
47790
48243
  `,
@@ -48048,21 +48501,23 @@ description: Validate, test, and reload a tenant sandbox plugin using the manage
48048
48501
 
48049
48502
  Use the \`manage_plugin\` tool. The loop:
48050
48503
 
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.
48504
+ 1. Write/edit the plugin files with the filesystem tools (\`write_file\`/\`edit_file\`) under
48505
+ \`/root/.agents/plugins/<type>/\`; \`create\` only scaffolds the directory + stub.
48506
+ 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
48507
  3. If the plugin has \`connection.fields\` and a tool reads credentials: a **human**
48054
48508
  must configure a connection of this plugin type. You cannot create credentials.
48055
48509
  Call \`list_connections { type }\` first \u2014 if none exist, use \`ask_user_to_clarify\`
48056
48510
  to state explicitly that only the human can configure the connector, ask them to
48057
48511
  add it (Console \u2192 Connections) and to give you the connection key. Do not claim
48058
48512
  the tool works until a real \`connectionKey\` test passes.
48059
- 4. \`action: "test"\` to exercise a handler without an LLM:
48513
+ 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).
48514
+ 5. \`action: "test"\` to exercise a handler without an LLM:
48060
48515
  - Tool: \`{ type, target: { kind: "tool", name, input, connectionKey? } }\`
48061
48516
  - Connection: \`{ type, target: { kind: "connection-test" } }\` or \`{ kind: "connection-discover" }\`
48062
48517
  Returns \`{ ok, result, exitCode, durationMs, stdout, stderr }\`.
48063
48518
  For credentialed tools, pass the real \`connectionKey\`. Without one, the handler
48064
48519
  receives an empty connection \u2014 that only checks plumbing, NOT functional success.
48065
- 5. \`action: "reload"\` if you edited files directly.
48520
+ 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
48521
 
48067
48522
  ## Secrets
48068
48523
  - \`list_connections\` returns identifiers only (\`key\`/\`name\`/\`description\`), never config or secrets.
@@ -48077,10 +48532,10 @@ Use the \`manage_plugin\` tool. The loop:
48077
48532
  | HANDLER_MISSING | handler path wrong or file not written |
48078
48533
  | TOOL_SCHEMA | unsupported JSON Schema keyword |
48079
48534
  | non-zero exit | handler threw; read stdout/stderr from \`test\` |
48080
- | old behaviour after edit | version/hash unchanged \u2014 bump \`version\` or call \`reload\` |
48535
+ | old behaviour after edit | the catalog caches by content hash/TTL \u2014 call \`reload\` |
48081
48536
  | empty connection in handler | no \`connectionKey\` passed, or no connection configured for this type |
48082
48537
 
48083
- Fall back to \`execute_code\`/\`shell\` to run \`node handlers/x.js\` manually with
48538
+ Fall back to \`shell_exec\` to run \`node handlers/x.js\` manually with
48084
48539
  \`PLUGIN_INPUT\` and a synthetic \`PLUGIN_CONNECTION_FILE\` to reproduce logic bugs.
48085
48540
  A synthetic fixture only verifies plumbing \u2014 a real upstream call requires a real
48086
48541
  \`connectionKey\` configured by a human.
@@ -48091,7 +48546,7 @@ A synthetic fixture only verifies plumbing \u2014 a real upstream call requires
48091
48546
  version: "1.0.0",
48092
48547
  content: `---
48093
48548
  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.
48549
+ 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
48550
  ---
48096
48551
 
48097
48552
  # Deploying a Plugin as a Resident Service
@@ -48101,10 +48556,13 @@ Optionally, you can **deploy** the plugin as a **resident service**: one
48101
48556
  long-lived sandbox per plugin that serves every call. This is faster for
48102
48557
  frequent or heavy calls, but costs a running sandbox.
48103
48558
 
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.
48559
+ ## Precondition: human request, then tests must pass first
48560
+ Deployment is **opt-in and human-gated**: NEVER deploy on your own after tests
48561
+ pass. Wait until the human explicitly asks for it or approves it. A passing test
48562
+ is necessary but NOT sufficient to deploy.
48563
+ Then confirm the plugin is validated AND its tests pass:
48564
+ 1. Files written with the filesystem tools and \`reload\` run.
48565
+ 2. \`manage_plugin validate\` \u2014 no diagnostics.
48108
48566
  3. \`manage_plugin test\` \u2014 every tool/connection test returns \`ok: true\`
48109
48567
  (a real \`connectionKey\` test for credentialed tools).
48110
48568
  Never deploy a plugin you have not tested: a resident service wraps the same
@@ -48172,6 +48630,30 @@ load "sandbox-plugins-expose" and follow it (\`expose: true\` + key grants).
48172
48630
  If a tool must call the platform's own tools/agents from its handler, load
48173
48631
  "sandbox-plugins-consume-open".
48174
48632
 
48633
+ Design first, and iterate on it before you build \u2014 the design is a conversation, not
48634
+ a one-shot. Use the \`widget\` middleware to present it (a decision surface, not a showcase):
48635
+ - **Functional design** (always): an **interactive** component map of the shape you
48636
+ intend to build \u2014 which tools, their inputs/outputs, whether a connection or a UI
48637
+ is needed, how the agent calls them. Make it click-to-explore (and prefer it over
48638
+ an SVG flowchart); it is a map to **confirm**, NOT an annotatable draft.
48639
+ - **UI design** (UI class only): an **annotatable** mock of the tool's UI \u2014 the human
48640
+ right-clicks elements to leave notes.
48641
+ Then run a review loop:
48642
+ - Ask the human to review (tell them how to comment). When they send comments, **restate the
48643
+ feedback** (which element, what change) so they can confirm you understood, then **build a
48644
+ NEW draft that incorporates the feedback and present it again**; keep iterating until
48645
+ they are satisfied. Comments mean "revise and re-show", NOT "start building".
48646
+ - Do NOT scaffold or write any file during the loop. Implement only once the human
48647
+ **explicitly approves** the design \u2014 and still ASK for that go-ahead yourself; never
48648
+ start building on your own initiative.
48649
+ Also surface genuinely different options (scope, layout, which tools) visually and let
48650
+ the human choose, instead of describing them in prose.
48651
+
48652
+ How to render a good widget is the widget middleware's own concern \u2014 follow it
48653
+ there; keep this prompt to plugin-design intent. These widgets are design aids
48654
+ only; the plugin's actual UI is still the MCP App HTML you author and test with
48655
+ \`manage_plugin\`.
48656
+
48175
48657
  Plan with persistent Tasks: MANDATORY until you load "task-tracking" with the
48176
48658
  \`skill\` tool, call neither \`manage_task\` nor any other task operation. Use
48177
48659
  persistent Tasks as the development planning surface and create one Task per
@@ -48179,7 +48661,16 @@ independently verifiable outcome (for example "manifest written", "handlers
48179
48661
  implemented", "UI added", "validated/tested via manage_plugin"). As you work,
48180
48662
  update each Task's status to reflect reality. Never use \`write_todos\`.
48181
48663
 
48182
- Author with the \`manage_plugin\` tool: validate, then create/update, then test.
48664
+ After the human confirms the design, author the plugin files directly with the filesystem tools, then verify with \`manage_plugin\`.
48665
+ 1. Scaffold once: \`manage_plugin { action: "create", type: "<type>", name }\` \u2014 it creates
48666
+ \`/root/.agents/plugins/<type>/\` with a stub \`plugin.json\`. It never carries content.
48667
+ 2. Write and edit every file yourself with \`write_file\` / \`edit_file\` (read back with
48668
+ \`read_file\`) under \`/root/.agents/plugins/<type>/\`: \`plugin.json\`, \`handlers/*.js\`, and
48669
+ (UI class) \`ui/*.html\`. Change one file at a time; edit in place instead of rewriting.
48670
+ 3. Verify in order: \`manage_plugin { action: "validate", type }\` (reads the files back from
48671
+ the sandbox) \u2192 \`manage_plugin { action: "reload", type }\` (the catalog caches, so a
48672
+ freshly written file is invisible to \`test\` until you reload) \u2192 \`manage_plugin
48673
+ { action: "test", type, target }\`.
48183
48674
  If a tool needs credentials, a human must configure the connection \u2014 use
48184
48675
  \`list_connections\` to check, and \`ask_user_to_clarify\` to ask the human to add
48185
48676
  the connector and give you its key. Only after a real \`connectionKey\` test
@@ -48188,14 +48679,15 @@ unverified (missing credentials), not as passing. Use \`shell_exec\` (the tool
48188
48679
  provided by the \`code_eval\` middleware) to run \`node handlers/x.js\` for manual
48189
48680
  checks when reproducing a failure.
48190
48681
 
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\`
48682
+ Deployment is human-gated: do NOT deploy on your own after the tests pass.
48683
+ Only deploy when the human explicitly asks for it or approves it, then load
48684
+ "sandbox-plugins-deploy" and follow it. Choose \`onDemand\`
48193
48685
  (default) or \`alwaysOn\` for hot paths, call \`manage_plugin deploy\` with the
48194
48686
  chosen \`mode\`, then confirm with \`manage_plugin status\` (\`running\`). Only claim
48195
48687
  the plugin is deployed and serving once \`status\` reports \`running\`; if the
48196
48688
  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.`;
48689
+ unavailable \u2014 do NOT claim success. Report the plugin as ready and let the human
48690
+ decide; \`undeploy\` removes the service without touching the plugin files.`;
48199
48691
 
48200
48692
  // src/middlewares/sandboxPluginsUi.ts
48201
48693
  var SANDBOX_PLUGINS_UI_RESOURCE = "ui://sandbox-plugins/app";
@@ -48371,6 +48863,7 @@ var SANDBOX_PLUGINS_APP_HTML = `<!doctype html>
48371
48863
  `;
48372
48864
 
48373
48865
  // src/middlewares/sandboxPluginsMiddleware.ts
48866
+ var PLUGINS_ROOT3 = "/root/.agents/plugins";
48374
48867
  var SANDBOX_PLUGINS_UI_REF = {
48375
48868
  kind: "plugin",
48376
48869
  pluginType: "sandbox-plugins",
@@ -48427,18 +48920,19 @@ var managePluginSchema = z73.object({
48427
48920
  "get",
48428
48921
  "validate",
48429
48922
  "create",
48430
- "update",
48431
48923
  "delete",
48432
48924
  "reload",
48433
48925
  "test",
48926
+ "backup",
48927
+ "restore",
48434
48928
  "deploy",
48435
48929
  "undeploy",
48436
48930
  "status",
48437
48931
  "deployments"
48438
48932
  ]),
48439
48933
  type: z73.string().optional(),
48440
- manifest: z73.record(z73.unknown()).optional(),
48441
- handlers: z73.record(z73.string()).optional(),
48934
+ name: z73.string().optional(),
48935
+ description: z73.string().optional(),
48442
48936
  mode: z73.enum(["onDemand", "alwaysOn"]).optional(),
48443
48937
  target: z73.object({
48444
48938
  kind: z73.enum(["tool", "connection-test", "connection-discover"]),
@@ -48452,12 +48946,12 @@ var sandboxPluginsPlugin = {
48452
48946
  type: "sandbox-plugins",
48453
48947
  category: "execution",
48454
48948
  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.",
48949
+ 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
48950
  recommends: ["skill"],
48457
48951
  tools: [
48458
48952
  {
48459
48953
  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.",
48954
+ 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
48955
  ui: { resource: SANDBOX_PLUGINS_UI_RESOURCE, displayMode: "inline" }
48462
48956
  }
48463
48957
  ],
@@ -48484,7 +48978,7 @@ var sandboxPluginsPlugin = {
48484
48978
  id: "sandbox-plugins",
48485
48979
  type: "sandbox-plugins",
48486
48980
  name: "Sandbox Plugins",
48487
- description: "Author and manage tenant plugins (list/validate/create/update/delete/reload/test/deploy/undeploy/status)",
48981
+ description: "Author and manage tenant plugins (list/validate/create/delete/reload/test/backup/restore/deploy/undeploy/status)",
48488
48982
  enabled: true,
48489
48983
  config: {}
48490
48984
  },
@@ -48504,6 +48998,14 @@ var sandboxPluginsPlugin = {
48504
48998
  enabled: true,
48505
48999
  config: {}
48506
49000
  },
49001
+ {
49002
+ id: "widget",
49003
+ type: "widget",
49004
+ name: "Widget",
49005
+ 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.",
49006
+ enabled: true,
49007
+ config: {}
49008
+ },
48507
49009
  {
48508
49010
  id: "ask_user_to_clarify",
48509
49011
  type: "ask_user_to_clarify",
@@ -48544,6 +49046,7 @@ var sandboxPluginsPlugin = {
48544
49046
  return JSON.stringify({
48545
49047
  plugins: loaded.plugins.map((p) => ({
48546
49048
  type: p.meta.type,
49049
+ path: `${PLUGINS_ROOT3}/${p.meta.type}`,
48547
49050
  name: p.meta.name,
48548
49051
  tools: p.meta.tools?.map((t) => t.name) ?? [],
48549
49052
  source: p.meta.source
@@ -48554,32 +49057,87 @@ var sandboxPluginsPlugin = {
48554
49057
  }
48555
49058
  case "get": {
48556
49059
  if (!input.type) return "type is required";
48557
- return await catalog2.readPluginFile(tenantId2, input.type, "plugin.json");
49060
+ const content = await catalog2.readPluginFile(tenantId2, input.type, "plugin.json");
49061
+ let manifest = content;
49062
+ try {
49063
+ manifest = JSON.parse(content);
49064
+ } catch {
49065
+ }
49066
+ return JSON.stringify({
49067
+ type: input.type,
49068
+ path: `${PLUGINS_ROOT3}/${input.type}`,
49069
+ manifest
49070
+ });
48558
49071
  }
48559
49072
  case "reload": {
48560
49073
  const loaded = await catalog2.reload(tenantId2);
48561
49074
  void publishPluginCatalogChanged(tenantId2);
48562
49075
  return JSON.stringify({ reloaded: true, plugins: loaded.plugins.length, diagnostics: loaded.diagnostics });
48563
49076
  }
48564
- case "create":
48565
- case "update": {
48566
- if (!input.type || !input.manifest) return "type and manifest are required";
48567
- const handlers = input.handlers ?? {};
49077
+ case "backup": {
49078
+ if (!input.type) return "type is required";
49079
+ try {
49080
+ const result = await catalog2.backupPlugin(tenantId2, input.type);
49081
+ if (result.files === 0) {
49082
+ return JSON.stringify({ backedUp: false, error: "nothing to back up (plugin has no files)" });
49083
+ }
49084
+ return JSON.stringify({ backedUp: true, files: result.files });
49085
+ } catch (err) {
49086
+ return JSON.stringify({ backedUp: false, error: errorText(err) });
49087
+ }
49088
+ }
49089
+ case "restore": {
49090
+ if (!input.type) return "type is required";
49091
+ let restored;
49092
+ let failure3;
49093
+ try {
49094
+ restored = await catalog2.restorePlugin(tenantId2, input.type);
49095
+ } catch (err) {
49096
+ failure3 = errorText(err);
49097
+ }
49098
+ try {
49099
+ await catalog2.reload(tenantId2);
49100
+ void publishPluginCatalogChanged(tenantId2);
49101
+ } catch {
49102
+ }
49103
+ if (failure3) return JSON.stringify({ restored: false, error: failure3 });
49104
+ return JSON.stringify({ restored: true, files: restored?.files ?? 0 });
49105
+ }
49106
+ case "create": {
49107
+ if (!input.type) return "type is required";
48568
49108
  const known = new Set(
48569
49109
  (catalog2.getCached(tenantId2)?.plugins ?? []).map((p) => p.meta.type).filter((t) => t !== input.type)
48570
49110
  );
49111
+ const stub = {
49112
+ schemaVersion: 1,
49113
+ type: input.type,
49114
+ name: input.name ?? input.type,
49115
+ description: input.description ?? "",
49116
+ version: "0.1.0",
49117
+ tools: []
49118
+ };
48571
49119
  const check = validateSandboxPluginManifest({
48572
- raw: input.manifest,
49120
+ raw: stub,
48573
49121
  dirName: input.type,
48574
49122
  reservedTypes: new Set(PluginRegistry.list()),
48575
49123
  knownTypes: known,
48576
- handlerExists: (rel) => rel in handlers
49124
+ handlerExists: () => true
48577
49125
  });
48578
- if (!check.manifest) return JSON.stringify({ written: false, diagnostics: check.diagnostics });
48579
- await catalog2.writePlugin(tenantId2, input.type, input.manifest, handlers);
49126
+ if (!check.manifest) {
49127
+ return JSON.stringify({ created: false, diagnostics: check.diagnostics });
49128
+ }
49129
+ const files = await catalog2.listPluginFiles(tenantId2, input.type);
49130
+ if (files.includes("plugin.json")) {
49131
+ return JSON.stringify({ created: false, error: "plugin already exists" });
49132
+ }
49133
+ await catalog2.writePlugin(tenantId2, input.type, check.manifest, {});
48580
49134
  const loaded = await catalog2.reload(tenantId2);
48581
49135
  void publishPluginCatalogChanged(tenantId2);
48582
- return JSON.stringify({ written: true, diagnostics: loaded.diagnostics, hash: loaded.hash });
49136
+ return JSON.stringify({
49137
+ created: true,
49138
+ path: `${PLUGINS_ROOT3}/${input.type}`,
49139
+ diagnostics: loaded.diagnostics
49140
+ });
48583
49141
  }
48584
49142
  case "delete": {
48585
49143
  if (!input.type) return "type is required";
@@ -48590,18 +49148,8 @@ var sandboxPluginsPlugin = {
48590
49148
  }
48591
49149
  case "validate": {
48592
49150
  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) });
49151
+ const result = await catalog2.validatePluginFromDisk(tenantId2, input.type);
49152
+ return JSON.stringify({ diagnostics: result.diagnostics, valid: result.valid });
48605
49153
  }
48606
49154
  case "test": {
48607
49155
  if (!input.type || !input.target) return "type and target are required";
@@ -48675,7 +49223,16 @@ var sandboxPluginsPlugin = {
48675
49223
  assistantId: typeof runCfg.assistant_id === "string" ? runCfg.assistant_id : void 0
48676
49224
  }
48677
49225
  });
48678
- return handlerResultText(redactSecretValues(result, secrets));
49226
+ const text = handlerResultText(redactSecretValues(result, secrets));
49227
+ const ui = toolDef.ui;
49228
+ if (!ui?.resource) return text;
49229
+ const uiRef = {
49230
+ kind: "plugin",
49231
+ pluginType: input.type,
49232
+ resource: ui.resource,
49233
+ displayMode: ui.displayMode ?? "inline"
49234
+ };
49235
+ return appendUiFence(text, uiRef);
48679
49236
  } catch (err) {
48680
49237
  return JSON.stringify(redactSecretValues({ ok: false, error: errorText(err) }, secrets));
48681
49238
  }
@@ -48747,7 +49304,7 @@ var sandboxPluginsPlugin = {
48747
49304
  },
48748
49305
  {
48749
49306
  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.",
49307
+ 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
49308
  schema: managePluginSchema
48752
49309
  }
48753
49310
  )
@@ -49923,6 +50480,7 @@ export {
49923
50480
  buildTableName,
49924
50481
  buildTaskMutationFallback,
49925
50482
  buildTaskNormalizationFallback,
50483
+ canConvergeOrphan,
49926
50484
  checkEmptyContent,
49927
50485
  clearEncryptionKeyCache,
49928
50486
  clearEvalRunService,
@@ -49972,6 +50530,7 @@ export {
49972
50530
  createTaskLifecycleService,
49973
50531
  createTaskMiddleware,
49974
50532
  createTaskRunBridge,
50533
+ createTaskRunLiveness,
49975
50534
  createTeamMiddleware,
49976
50535
  createTeammateTools,
49977
50536
  createTrustedAgentMessageDispatcher,
@@ -50069,6 +50628,7 @@ export {
50069
50628
  isMcpConnectionError,
50070
50629
  isPluginSkillAuthorizedInSandbox,
50071
50630
  isPluginSkillManagedInSandbox,
50631
+ isTaskRunLeaseHeld,
50072
50632
  isUsingDefaultKey,
50073
50633
  isValidCronExpression,
50074
50634
  isValidSandboxName,