@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.js CHANGED
@@ -1800,6 +1800,7 @@ __export(index_exports, {
1800
1800
  buildTableName: () => buildTableName,
1801
1801
  buildTaskMutationFallback: () => buildTaskMutationFallback,
1802
1802
  buildTaskNormalizationFallback: () => buildTaskNormalizationFallback,
1803
+ canConvergeOrphan: () => canConvergeOrphan,
1803
1804
  checkEmptyContent: () => checkEmptyContent,
1804
1805
  clearEncryptionKeyCache: () => clearEncryptionKeyCache,
1805
1806
  clearEvalRunService: () => clearEvalRunService,
@@ -1849,6 +1850,7 @@ __export(index_exports, {
1849
1850
  createTaskLifecycleService: () => createTaskLifecycleService,
1850
1851
  createTaskMiddleware: () => createTaskMiddleware,
1851
1852
  createTaskRunBridge: () => createTaskRunBridge,
1853
+ createTaskRunLiveness: () => createTaskRunLiveness,
1852
1854
  createTeamMiddleware: () => createTeamMiddleware,
1853
1855
  createTeammateTools: () => createTeammateTools,
1854
1856
  createTrustedAgentMessageDispatcher: () => createTrustedAgentMessageDispatcher,
@@ -1946,6 +1948,7 @@ __export(index_exports, {
1946
1948
  isMcpConnectionError: () => isMcpConnectionError,
1947
1949
  isPluginSkillAuthorizedInSandbox: () => isPluginSkillAuthorizedInSandbox,
1948
1950
  isPluginSkillManagedInSandbox: () => isPluginSkillManagedInSandbox,
1951
+ isTaskRunLeaseHeld: () => isTaskRunLeaseHeld,
1949
1952
  isUsingDefaultKey: () => isUsingDefaultKey,
1950
1953
  isValidCronExpression: () => isValidCronExpression,
1951
1954
  isValidSandboxName: () => isValidSandboxName,
@@ -9058,7 +9061,13 @@ If the Worker has task capability, it may truthfully update the same task.
9058
9061
  Otherwise, the owning caller interprets the returned observation.
9059
9062
 
9060
9063
  An execution result is an observation, not automatic proof of business
9061
- completion. \`task\` returns its exact \`executionResultId\`. After \`task\`
9064
+ completion. \`task\` returns its exact \`executionResultId\` and the task's
9065
+ \`taskStatus\` (re-read **after** the worker settled). **Check \`taskStatus\`
9066
+ before completing:** if it is already \`completed\` the task is done \u2014 do NOT
9067
+ complete it again (use \`add_activity\` for extra notes); if it is still
9068
+ \`in_progress\` you are the single completer and may \`set_status completed\`;
9069
+ if it is \`failed\` or \`interrupted\`, act on that (retry / resume / new task)
9070
+ instead. After \`task\`
9062
9071
  returns, inspect the observation, then pass that \`executionResultId\` to the
9063
9072
  meaningful \`manage_task set_status\` (status transition), \`manage_task
9064
9073
  update\` (content fields), or \`manage_task add_activity\` that interprets
@@ -9102,7 +9111,11 @@ handled by the configured HITL lifecycle; \`review_required\` is not a task stat
9102
9111
  Create the active parent and each started subtask as records: \`manage_task
9103
9112
  create\` always persists \`pending\`. **Process the task you were assigned in this
9104
9113
  run: set it \`in_progress\` with \`manage_task set_status\` when you start (a
9105
- dispatched task is claimed automatically by the engine).** To bring
9114
+ dispatched task is claimed automatically by the engine).** \`in_progress\` means a
9115
+ run is executing that task: set it yourself ONLY for the task THIS run executes,
9116
+ and for a task you delegate leave it \`pending\` \u2014 \`task\` dispatch claims it.
9117
+ Complete a task exactly once: if it is already \`completed\`, do NOT complete it
9118
+ again (the terminal state stands; use \`add_activity\` for extra notes). To bring
9106
9119
  in a more suitable expert, create a subtask (\`pending\`) and dispatch THAT with
9107
9120
  \`task { taskId, agentId }\` (a specialist, or agentId "general-purpose" to run your
9108
9121
  own capabilities in an isolated run). **Never re-dispatch the task you are
@@ -10668,7 +10681,9 @@ criteria are truly met \u2014 never as a workaround.
10668
10681
  \`manage_task create(title: <goal>, description: <contract + Belief State>, ownerType: "agent")\`
10669
10682
  \`create\` always yields a \`pending\` record \u2014 it never starts execution. Record
10670
10683
  the returned parent task id, then **process it in this run**, setting it
10671
- \`in_progress\` with \`manage_task set_status\` when you start. To decompose, create each subtask
10684
+ \`in_progress\` with \`manage_task set_status\` when you start. Set \`in_progress\`
10685
+ only for a task THIS run executes; a task you delegate stays \`pending\` and is
10686
+ claimed by \`task\` dispatch. To decompose, create each subtask
10672
10687
  with \`manage_task create\` (\`pending\`) and delegate it with \`task { taskId,
10673
10688
  agentId }\` to the agent that should execute it (a specialist, or
10674
10689
  "general-purpose" for your own capabilities). The executor does not need
@@ -10701,10 +10716,13 @@ ${TASK_BELIEF_REFERENCE_EXAMPLE}
10701
10716
  ## Status discipline \u2014 MANDATORY
10702
10717
 
10703
10718
  - Execution ownership: process the task you were assigned in this run; set it
10704
- \`in_progress\` with \`manage_task set_status\` when you start. To involve a more
10719
+ \`in_progress\` with \`manage_task set_status\` when you start. Set it only for a
10720
+ task THIS run executes; a task you delegate stays \`pending\`. To involve a more
10705
10721
  suitable expert, create a subtask and dispatch it with \`task { taskId, agentId }\`
10706
10722
  \u2014 never re-dispatch the task you are processing. Report the outcome with
10707
10723
  \`set_status completed\` (result + beliefImpact), \`failed\`, or \`interrupted\`.
10724
+ Complete a task exactly once: if it is already \`completed\`, do not complete it
10725
+ again \u2014 use \`add_activity\` for extra notes.
10708
10726
  - All status transitions go through \`manage_task set_status\` (with
10709
10727
  \`result\` on completed, \`failureReason\` on failed). \`update\` is for
10710
10728
  content fields only and rejects a status field.
@@ -21382,6 +21400,7 @@ async function runDiscoverHandler(executor, manifest, handler, config, tenantId2
21382
21400
  var PLUGINS_ROOT2 = "/root/.agents/plugins";
21383
21401
  var PLUGIN_TYPE_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
21384
21402
  var DEFAULT_HASH_TTL_MS = 3e4;
21403
+ var BACKUP_DIR_NAME = ".backup";
21385
21404
  function pluginTypeFromPath(dirPath) {
21386
21405
  return dirPath.slice(dirPath.lastIndexOf("/") + 1);
21387
21406
  }
@@ -21393,6 +21412,10 @@ function topLevelPluginDirs(entries) {
21393
21412
  return remainder.length > 0 && !remainder.includes("/");
21394
21413
  }).sort((a, b) => a.path.localeCompare(b.path));
21395
21414
  }
21415
+ function isUnderBackup(path8, pluginDir) {
21416
+ const backupDir = `${pluginDir}/${BACKUP_DIR_NAME}`;
21417
+ return path8 === backupDir || path8.startsWith(`${backupDir}/`);
21418
+ }
21396
21419
  var TenantPluginCatalog = class {
21397
21420
  /**
21398
21421
  * @param deps - Sandbox resolver, executor, reserved types, and cache/logger seams
@@ -21428,6 +21451,177 @@ var TenantPluginCatalog = class {
21428
21451
  const snapshot = await this.ensureLoaded(tenantId2);
21429
21452
  return snapshot.manifests[type];
21430
21453
  }
21454
+ /**
21455
+ * Validate a single plugin by reading its files from the sandbox volume.
21456
+ *
21457
+ * Dry-run counterpart to {@link reload}: it lists the plugin directory, reads
21458
+ * `plugin.json`, and runs the same manifest validation the catalog uses when
21459
+ * loading, with handler existence checked against the files actually present
21460
+ * on disk. It never touches the cache and never publishes anything, so a
21461
+ * caller can validate an on-disk plugin without making it live.
21462
+ *
21463
+ * @param tenantId - Tenant that owns the plugin
21464
+ * @param type - Plugin type (directory name); must match the kebab-case type pattern
21465
+ * @returns The structured `diagnostics` and whether the manifest passed fatal
21466
+ * validation (`valid`). A non-kebab-case `type`, an unreadable directory, or
21467
+ * a missing/invalid `plugin.json` yields a diagnostic with `valid: false`.
21468
+ */
21469
+ async validatePluginFromDisk(tenantId2, type) {
21470
+ if (typeof type !== "string" || !PLUGIN_TYPE_RE.test(type)) {
21471
+ return {
21472
+ diagnostics: [
21473
+ {
21474
+ level: "error",
21475
+ code: "TYPE_INVALID",
21476
+ pluginType: type,
21477
+ message: `type "${type}" must match ${PLUGIN_TYPE_RE}`
21478
+ }
21479
+ ],
21480
+ valid: false
21481
+ };
21482
+ }
21483
+ const pluginDir = `${PLUGINS_ROOT2}/${type}`;
21484
+ const sandbox = await this.deps.resolveSandbox({ tenantId: tenantId2 });
21485
+ let listing;
21486
+ try {
21487
+ listing = await sandbox.file.listPath(pluginDir, { recursive: true });
21488
+ } catch (err) {
21489
+ return {
21490
+ diagnostics: [
21491
+ {
21492
+ level: "error",
21493
+ code: "PLUGINS_LIST_FAILED",
21494
+ pluginType: type,
21495
+ message: err instanceof Error ? err.message : String(err)
21496
+ }
21497
+ ],
21498
+ valid: false
21499
+ };
21500
+ }
21501
+ let raw;
21502
+ let manifestJson;
21503
+ try {
21504
+ const { content } = await sandbox.file.readFile(`${pluginDir}/plugin.json`);
21505
+ raw = JSON.parse(content);
21506
+ manifestJson = JSON.stringify(raw);
21507
+ } catch (err) {
21508
+ return {
21509
+ diagnostics: [
21510
+ {
21511
+ level: "error",
21512
+ code: "MANIFEST_READ",
21513
+ pluginType: type,
21514
+ message: err instanceof Error ? err.message : String(err)
21515
+ }
21516
+ ],
21517
+ valid: false
21518
+ };
21519
+ }
21520
+ const files = listing.files ?? [];
21521
+ const handlerExists = (rel) => files.some((f) => !f.is_dir && f.path === `${pluginDir}/${rel}`);
21522
+ const knownTypes = new Set(
21523
+ Object.keys(this.cache.get(tenantId2)?.value.manifests ?? {}).filter((t) => t !== type)
21524
+ );
21525
+ const result = validateSandboxPluginManifest({
21526
+ raw,
21527
+ dirName: type,
21528
+ reservedTypes: this.deps.reservedTypes(),
21529
+ knownTypes,
21530
+ handlerExists,
21531
+ rawSizeBytes: Buffer.byteLength(manifestJson, "utf8")
21532
+ });
21533
+ return { diagnostics: result.diagnostics, valid: Boolean(result.manifest) };
21534
+ }
21535
+ /**
21536
+ * Snapshot a plugin's current files into its in-directory backup.
21537
+ *
21538
+ * Keeps a single backup (`<pluginDir>/.backup/`): the previous backup is
21539
+ * replaced. Files already under `.backup/` are excluded, so a backup never
21540
+ * nests. Manual action — it is not triggered by {@link writePlugin}.
21541
+ *
21542
+ * @param tenantId - Tenant that owns the plugin
21543
+ * @param type - Plugin type (directory name)
21544
+ * @returns The number of files snapshotted (`0` when the plugin has no files)
21545
+ */
21546
+ async backupPlugin(tenantId2, type) {
21547
+ this.assertSafeType(type);
21548
+ const pluginDir = `${PLUGINS_ROOT2}/${type}`;
21549
+ const backupDir = `${pluginDir}/${BACKUP_DIR_NAME}`;
21550
+ const sandbox = await this.deps.resolveSandbox({ tenantId: tenantId2 });
21551
+ const entries = (await sandbox.file.listPath(pluginDir, { recursive: true })).files ?? [];
21552
+ const files = entries.filter((entry) => !entry.is_dir && !isUnderBackup(entry.path, pluginDir));
21553
+ if (files.length === 0) {
21554
+ return { files: 0 };
21555
+ }
21556
+ if (entries.some((entry) => isUnderBackup(entry.path, pluginDir))) {
21557
+ await sandbox.file.deletePath(backupDir);
21558
+ }
21559
+ let count = 0;
21560
+ for (const entry of files) {
21561
+ const rel = entry.path.slice(pluginDir.length + 1);
21562
+ const { content } = await sandbox.file.readFile(entry.path);
21563
+ await this.writeRelativeFile(sandbox, backupDir, rel, content);
21564
+ count += 1;
21565
+ }
21566
+ return { files: count };
21567
+ }
21568
+ /**
21569
+ * Restore a plugin's files from its in-directory backup.
21570
+ *
21571
+ * The plugin directory (excluding `.backup/`) is made to match the backup:
21572
+ * files present in the backup are (re)written and files absent from it are
21573
+ * removed. Callers should {@link reload} afterward so the restored shape is
21574
+ * picked up.
21575
+ *
21576
+ * @param tenantId - Tenant that owns the plugin
21577
+ * @param type - Plugin type (directory name)
21578
+ * @returns The number of files restored
21579
+ * @throws Error when the plugin has no backup
21580
+ */
21581
+ async restorePlugin(tenantId2, type) {
21582
+ this.assertSafeType(type);
21583
+ const pluginDir = `${PLUGINS_ROOT2}/${type}`;
21584
+ const backupDir = `${pluginDir}/${BACKUP_DIR_NAME}`;
21585
+ const sandbox = await this.deps.resolveSandbox({ tenantId: tenantId2 });
21586
+ const backupFiles = await (async () => {
21587
+ try {
21588
+ return ((await sandbox.file.listPath(backupDir, { recursive: true })).files ?? []).filter((entry) => !entry.is_dir);
21589
+ } catch {
21590
+ return [];
21591
+ }
21592
+ })();
21593
+ if (backupFiles.length === 0) {
21594
+ throw new Error(`no backup found for plugin "${type}"`);
21595
+ }
21596
+ const backupRels = new Set(backupFiles.map((entry) => entry.path.slice(backupDir.length + 1)));
21597
+ const currentFiles = ((await sandbox.file.listPath(pluginDir, { recursive: true })).files ?? []).filter((entry) => !entry.is_dir && !isUnderBackup(entry.path, pluginDir));
21598
+ for (const entry of currentFiles) {
21599
+ const rel = entry.path.slice(pluginDir.length + 1);
21600
+ if (!backupRels.has(rel)) await sandbox.file.deletePath(entry.path);
21601
+ }
21602
+ let count = 0;
21603
+ for (const entry of backupFiles) {
21604
+ const rel = entry.path.slice(backupDir.length + 1);
21605
+ const { content } = await sandbox.file.readFile(entry.path);
21606
+ await this.writeRelativeFile(sandbox, pluginDir, rel, content);
21607
+ count += 1;
21608
+ }
21609
+ return { files: count };
21610
+ }
21611
+ /**
21612
+ * Write a file under a base directory, creating its parent directory first.
21613
+ *
21614
+ * @param sandbox - Sandbox whose file service receives the write
21615
+ * @param baseDir - Absolute target directory (a plugin or backup directory)
21616
+ * @param rel - Safe path relative to `baseDir`
21617
+ * @param content - File contents
21618
+ * @returns Nothing once the file is written
21619
+ */
21620
+ async writeRelativeFile(sandbox, baseDir, rel, content) {
21621
+ const slash = rel.lastIndexOf("/");
21622
+ if (slash !== -1) await sandbox.file.createDirectory(`${baseDir}/${rel.slice(0, slash)}`);
21623
+ await sandbox.file.writeFile(`${baseDir}/${rel}`, content);
21624
+ }
21431
21625
  /**
21432
21626
  * Drop the cached snapshot for a tenant.
21433
21627
  *
@@ -21504,6 +21698,30 @@ var TenantPluginCatalog = class {
21504
21698
  const { content } = await sandbox.file.readFile(`${PLUGINS_ROOT2}/${type}/${relPath}`);
21505
21699
  return content;
21506
21700
  }
21701
+ /**
21702
+ * List the relative file paths currently present under a plugin directory.
21703
+ *
21704
+ * Used to decide whether a plugin already exists before scaffolding, and to
21705
+ * surface a plugin's files without reading their contents.
21706
+ *
21707
+ * @param tenantId - Tenant that owns the plugin
21708
+ * @param type - Plugin type (directory name); must match the kebab-case type pattern
21709
+ * @returns Relative paths (e.g. `plugin.json`, `handlers/x.js`); empty when the
21710
+ * directory is missing or the listing fails
21711
+ */
21712
+ async listPluginFiles(tenantId2, type) {
21713
+ this.assertSafeType(type);
21714
+ const sandbox = await this.deps.resolveSandbox({ tenantId: tenantId2 });
21715
+ const pluginDir = `${PLUGINS_ROOT2}/${type}`;
21716
+ let listing;
21717
+ try {
21718
+ listing = await sandbox.file.listPath(pluginDir, { recursive: true });
21719
+ } catch {
21720
+ return [];
21721
+ }
21722
+ const prefix = `${pluginDir}/`;
21723
+ return listing.files.filter((entry) => !entry.is_dir).map((entry) => entry.path.startsWith(prefix) ? entry.path.slice(prefix.length) : entry.path);
21724
+ }
21507
21725
  /**
21508
21726
  * Write a plugin manifest and handler files into the tenant volume.
21509
21727
  *
@@ -21528,11 +21746,7 @@ var TenantPluginCatalog = class {
21528
21746
  await sandbox.file.createDirectory(dir);
21529
21747
  await sandbox.file.writeFile(`${dir}/plugin.json`, JSON.stringify(manifest, null, 2));
21530
21748
  for (const [rel, content] of Object.entries(handlers)) {
21531
- const slash = rel.lastIndexOf("/");
21532
- if (slash !== -1) {
21533
- await sandbox.file.createDirectory(`${dir}/${rel.slice(0, slash)}`);
21534
- }
21535
- await sandbox.file.writeFile(`${dir}/${rel}`, content);
21749
+ await this.writeRelativeFile(sandbox, dir, rel, content);
21536
21750
  }
21537
21751
  }
21538
21752
  /**
@@ -21637,7 +21851,7 @@ var TenantPluginCatalog = class {
21637
21851
  const dirFiles = entries.filter(
21638
21852
  (entry) => entry.path === dir.path || entry.path.startsWith(`${dir.path}/`)
21639
21853
  );
21640
- const relPaths = dirFiles.filter((entry) => !entry.is_dir && entry.path !== `${dir.path}/plugin.json`).map((entry) => entry.path.slice(dir.path.length + 1)).sort();
21854
+ 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();
21641
21855
  const manifestPath = `${dir.path}/plugin.json`;
21642
21856
  let raw;
21643
21857
  let manifestJson;
@@ -21668,7 +21882,7 @@ var TenantPluginCatalog = class {
21668
21882
  const manifests = {};
21669
21883
  for (const candidate of candidates) {
21670
21884
  try {
21671
- const handlerExists = (rel) => candidate.files.some((f) => f.path === `${candidate.dirPath}/${rel}`);
21885
+ const handlerExists = (rel) => candidate.files.some((f) => !f.is_dir && f.path === `${candidate.dirPath}/${rel}`);
21672
21886
  const result = validateSandboxPluginManifest({
21673
21887
  raw: candidate.raw,
21674
21888
  dirName: candidate.type,
@@ -33126,6 +33340,12 @@ var _AgentLatticeManager = class _AgentLatticeManager extends BaseLatticeManager
33126
33340
  * {@link buildingKeys}.
33127
33341
  */
33128
33342
  this.buildContext = new import_async_hooks.AsyncLocalStorage();
33343
+ /**
33344
+ * Store-backed assistant ids previously reconciled per tenant. Used to
33345
+ * unregister assistants that were deleted on another replica without
33346
+ * touching built-in / plugin agents (which are re-ensured on every reload).
33347
+ */
33348
+ this.storedAssistantKeys = /* @__PURE__ */ new Map();
33129
33349
  this.subscribeToAssistantEvents();
33130
33350
  }
33131
33351
  /**
@@ -33354,11 +33574,29 @@ var _AgentLatticeManager = class _AgentLatticeManager extends BaseLatticeManager
33354
33574
  try {
33355
33575
  const storeLattice = getStoreLattice("default", "assistant");
33356
33576
  const assistants = await storeLattice.store.getAllAssistants(tenantId2);
33577
+ const currentIds = /* @__PURE__ */ new Set();
33357
33578
  for (const assistant of assistants) {
33358
- if (this.hasAgentLatticeWithTenant(tenantId2, assistant.id)) continue;
33579
+ currentIds.add(assistant.id);
33359
33580
  const config = assistantToConfig(assistant);
33360
- this.registerLatticeWithTenant(tenantId2, config);
33581
+ const existing = this.getAgentConfigWithTenant(tenantId2, assistant.id);
33582
+ if (existing === void 0) {
33583
+ this.registerLatticeWithTenant(tenantId2, config);
33584
+ continue;
33585
+ }
33586
+ if (JSON.stringify(existing) !== JSON.stringify(config)) {
33587
+ this.removeWithTenant(tenantId2, assistant.id);
33588
+ this.registerLatticeWithTenant(tenantId2, config);
33589
+ }
33590
+ }
33591
+ const previousIds = this.storedAssistantKeys.get(tenantId2);
33592
+ if (previousIds) {
33593
+ for (const id of previousIds) {
33594
+ if (!currentIds.has(id)) {
33595
+ this.removeWithTenant(tenantId2, id);
33596
+ }
33597
+ }
33361
33598
  }
33599
+ this.storedAssistantKeys.set(tenantId2, currentIds);
33362
33600
  } catch (error) {
33363
33601
  console.error(`Failed to initialize stored assistants for tenant ${tenantId2}:`, error);
33364
33602
  }
@@ -40404,7 +40642,11 @@ var TaskLifecycleService = class {
40404
40642
  expectedStatuses: ["completed", "cancelled"],
40405
40643
  updates: { status: "in_progress", failureReason: null, result: null },
40406
40644
  summary: "Task reopened",
40407
- detail: { type: "reopen" }
40645
+ detail: { type: "reopen" },
40646
+ // Preserve an existing thread by default (the project runtime reuses the
40647
+ // canonical Task Thread); a dispatch that allocated a fresh run thread
40648
+ // opts in to replacing it.
40649
+ replaceThread: input.replaceThread ?? false
40408
40650
  });
40409
40651
  }
40410
40652
  /**
@@ -42265,6 +42507,39 @@ function createTaskRunBridge(deps) {
42265
42507
  transitioned: true
42266
42508
  };
42267
42509
  }
42510
+ if (task.status === "completed" || task.status === "cancelled") {
42511
+ if (task.ownerType !== "agent") {
42512
+ return {
42513
+ success: false,
42514
+ code: "AGENT_TASK_REQUIRED",
42515
+ error: "Lifecycle transitions only support agent tasks."
42516
+ };
42517
+ }
42518
+ const reopened = await deps.lifecycle.reopenTask({
42519
+ tenantId: tenantId2,
42520
+ taskId: task.id,
42521
+ actor,
42522
+ ...threadId !== void 0 ? { threadId } : {},
42523
+ // The caller allocated a fresh run thread for this dispatch.
42524
+ replaceThread: true
42525
+ });
42526
+ if (!reopened.success) {
42527
+ return {
42528
+ success: false,
42529
+ code: reopened.code,
42530
+ error: reopened.error,
42531
+ ...reopened.hint !== void 0 ? { hint: reopened.hint } : {},
42532
+ ...reopened.retryable !== void 0 ? { retryable: reopened.retryable } : {}
42533
+ };
42534
+ }
42535
+ return {
42536
+ success: true,
42537
+ task: reopened.task,
42538
+ warnings: reopened.warnings ?? [],
42539
+ mutated: true,
42540
+ transitioned: true
42541
+ };
42542
+ }
42268
42543
  return { success: true, task, warnings, mutated: false, transitioned: false };
42269
42544
  },
42270
42545
  async settleRun(input) {
@@ -42437,6 +42712,45 @@ async function tryAcquireTaskRun(input) {
42437
42712
  };
42438
42713
  }
42439
42714
 
42715
+ // src/services/taskLiveness.ts
42716
+ function registeredLeaseStore() {
42717
+ try {
42718
+ return storeLatticeManager.getStoreLattice("default", "lease").store;
42719
+ } catch {
42720
+ return void 0;
42721
+ }
42722
+ }
42723
+ async function isTaskRunLeaseHeld(task) {
42724
+ const store = registeredLeaseStore();
42725
+ if (!store) return false;
42726
+ try {
42727
+ if (await store.get(taskRunLeaseKey(task.id))) return true;
42728
+ const threadId = typeof task.context?.thread_id === "string" ? task.context.thread_id : void 0;
42729
+ if (threadId && task.ownerId && await store.get(threadRunLeaseKey(task.tenantId, task.ownerId, threadId))) {
42730
+ return true;
42731
+ }
42732
+ return false;
42733
+ } catch {
42734
+ return true;
42735
+ }
42736
+ }
42737
+ function createTaskRunLiveness() {
42738
+ return async (task) => taskRunRegistry.isLive(task.id) || isTaskRunLeaseHeld(task);
42739
+ }
42740
+ function isMultiReplicaDeclared() {
42741
+ const id = process.env.AXIOM_REPLICA_ID;
42742
+ return typeof id === "string" && id.trim() !== "";
42743
+ }
42744
+ function isSharedLeaseStore() {
42745
+ const store = registeredLeaseStore();
42746
+ const name = store?.constructor?.name;
42747
+ return typeof name === "string" && !name.startsWith("InMemory");
42748
+ }
42749
+ function canConvergeOrphan() {
42750
+ if (!isMultiReplicaDeclared()) return true;
42751
+ return isSharedLeaseStore();
42752
+ }
42753
+
42440
42754
  // src/services/ExecutionAuthorityRegistry.ts
42441
42755
  var ManagedTaskStartError = class extends Error {
42442
42756
  /**
@@ -42520,7 +42834,10 @@ async function prepareTaskDispatch(deps, input) {
42520
42834
  }
42521
42835
  };
42522
42836
  }
42523
- if (task.status === "completed" || task.status === "cancelled") {
42837
+ const managedAuthority = executionAuthorityRegistry.resolve(task);
42838
+ const terminal = task.status === "completed" || task.status === "cancelled";
42839
+ const reopenableTerminal = terminal && task.ownerType === "agent" && !managedAuthority;
42840
+ if (terminal && !reopenableTerminal) {
42524
42841
  return {
42525
42842
  kind: "failure",
42526
42843
  failure: {
@@ -42544,19 +42861,18 @@ async function prepareTaskDispatch(deps, input) {
42544
42861
  }
42545
42862
  };
42546
42863
  }
42547
- if (task.status !== "pending" && task.status !== "interrupted" && task.status !== "failed") {
42864
+ if (task.status !== "pending" && task.status !== "interrupted" && task.status !== "failed" && !reopenableTerminal) {
42548
42865
  return {
42549
42866
  kind: "failure",
42550
42867
  failure: {
42551
42868
  success: false,
42552
42869
  code: "TASK_NOT_DISPATCHABLE",
42553
- error: "Only a `pending`, `interrupted`, or `failed` task can be dispatched.",
42554
- 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.`,
42870
+ error: "Only a `pending`, `interrupted`, `failed`, or reopenable terminal task can be dispatched.",
42871
+ 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.`,
42555
42872
  taskId: task.id
42556
42873
  }
42557
42874
  };
42558
42875
  }
42559
- const managedAuthority = executionAuthorityRegistry.resolve(task);
42560
42876
  if (managedAuthority) {
42561
42877
  if (managedAuthority.onManagedStart) {
42562
42878
  try {
@@ -42726,7 +43042,8 @@ function persistedExecutionObservation(item, attempted) {
42726
43042
  }
42727
43043
  function buildWorkerMessage(task, parent, dependencies) {
42728
43044
  const sections2 = [
42729
- "Execute the following persistent task. Return your result as an observation; do not assume the task is complete.",
43045
+ `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.`,
43046
+ "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.",
42730
43047
  `Persisted Task ID: \`${task.id}\``,
42731
43048
  `# Task: ${task.title}`,
42732
43049
  task.description?.trim() || "No task description was provided."
@@ -42758,9 +43075,17 @@ function createTaskExecutionService(deps) {
42758
43075
  taskStore: deps.taskStore,
42759
43076
  workItemStore: deps.workItemStore
42760
43077
  });
43078
+ const isTaskHeldByLiveRun = deps.isTaskHeldByLiveRun ?? createTaskRunLiveness();
42761
43079
  const run = async (plan) => {
42762
43080
  const { tenantId: tenantId2, agentId, actor, parent, completedDependencies } = plan;
42763
43081
  let task = plan.task;
43082
+ const readTaskStatus = async () => {
43083
+ try {
43084
+ return (await deps.taskStore.getById(tenantId2, task.id))?.status;
43085
+ } catch {
43086
+ return void 0;
43087
+ }
43088
+ };
42764
43089
  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;
42765
43090
  const storedRunId = typeof task.context?.run_id === "string" && task.context.run_id.trim() !== "" ? task.context.run_id : void 0;
42766
43091
  let reuseCurrentRun = false;
@@ -42819,7 +43144,7 @@ function createTaskExecutionService(deps) {
42819
43144
  task = transfer.task;
42820
43145
  }
42821
43146
  const warnings = [];
42822
- if (task.status === "pending" || task.status === "interrupted" || task.status === "failed") {
43147
+ if (task.status === "pending" || task.status === "interrupted" || task.status === "failed" || task.status === "completed" || task.status === "cancelled") {
42823
43148
  const begun = await bridge.beginRun({
42824
43149
  tenantId: tenantId2,
42825
43150
  task,
@@ -42946,6 +43271,7 @@ function createTaskExecutionService(deps) {
42946
43271
  ...warnings.length > 0 ? { warnings } : {}
42947
43272
  };
42948
43273
  }
43274
+ const settledStatus = await readTaskStatus();
42949
43275
  return {
42950
43276
  success: false,
42951
43277
  code: failureCode,
@@ -42954,6 +43280,7 @@ function createTaskExecutionService(deps) {
42954
43280
  agentId,
42955
43281
  threadId,
42956
43282
  executionResultId: eventKey,
43283
+ ...settledStatus !== void 0 ? { taskStatus: settledStatus } : {},
42957
43284
  ...warnings.length > 0 ? { warnings } : {}
42958
43285
  };
42959
43286
  } catch (persistenceError) {
@@ -43002,6 +43329,7 @@ function createTaskExecutionService(deps) {
43002
43329
  ...warnings.length > 0 ? { warnings } : {}
43003
43330
  };
43004
43331
  }
43332
+ const taskStatus = await readTaskStatus();
43005
43333
  return {
43006
43334
  success: true,
43007
43335
  taskId: task.id,
@@ -43009,6 +43337,7 @@ function createTaskExecutionService(deps) {
43009
43337
  threadId,
43010
43338
  observation: validated.observation,
43011
43339
  executionResultId: eventKey,
43340
+ ...taskStatus !== void 0 ? { taskStatus } : {},
43012
43341
  ...warnings.length > 0 ? { warnings } : {}
43013
43342
  };
43014
43343
  } catch (error) {
@@ -43029,6 +43358,23 @@ function createTaskExecutionService(deps) {
43029
43358
  };
43030
43359
  return {
43031
43360
  start: async (input) => {
43361
+ let current;
43362
+ try {
43363
+ current = await deps.taskStore.getById(input.tenantId, input.taskId);
43364
+ } catch {
43365
+ current = null;
43366
+ }
43367
+ if (current && current.status === "in_progress" && current.ownerType === "agent" && canConvergeOrphan() && !executionAuthorityRegistry.resolve(current) && !await isTaskHeldByLiveRun(current)) {
43368
+ try {
43369
+ await lifecycle.failTask({
43370
+ tenantId: input.tenantId,
43371
+ taskId: current.id,
43372
+ actor: input.actor,
43373
+ failureReason: "Run is no longer executing this task (no_executor)"
43374
+ });
43375
+ } catch {
43376
+ }
43377
+ }
43032
43378
  const prepared = await prepareTaskDispatch(deps, input);
43033
43379
  if (prepared.kind === "managed") return prepared.result;
43034
43380
  if (prepared.kind === "failure") return prepared.failure;
@@ -45173,9 +45519,85 @@ Use \`imagine_html\`. Wrap the entire thing in a single raised card. All content
45173
45519
  </div>
45174
45520
  </div>
45175
45521
  \`\`\``;
45522
+ var ANNOTATABLE_MOCKUP = `## Annotatable drafts (reviewable mockups)
45523
+
45524
+ 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.)
45525
+
45526
+ - 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\`.
45527
+ - 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.
45528
+ - 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).
45529
+ - Handle \`contextmenu\` with event delegation; \`preventDefault()\` + \`stopPropagation()\` (this replaces the native browser menu, which is fine on a draft).
45530
+ - 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.
45531
+ - 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.
45532
+
45533
+ \`\`\`html
45534
+ <style>
45535
+ .anno{position:relative}
45536
+ .anno:hover{outline:1px dashed var(--color-border-info);outline-offset:-2px;border-radius:var(--border-radius-md)}
45537
+ .anno.marked{outline:1.5px solid var(--color-text-info);outline-offset:-2px}
45538
+ .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}
45539
+ .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}
45540
+ .modal .mt{font-size:14px;font-weight:500;margin-bottom:2px}
45541
+ .modal .ms{font-size:11px;color:var(--color-text-tertiary);margin-bottom:10px}
45542
+ .modal textarea{width:100%;box-sizing:border-box;height:auto;min-height:72px;padding:8px 10px;resize:vertical}
45543
+ .modal .mrow{display:flex;gap:8px;justify-content:flex-end;margin-top:10px}
45544
+ .primary{background:var(--color-text-info);color:var(--color-background-primary);border-color:var(--color-text-info)}
45545
+ .primary:hover{background:var(--color-text-info);opacity:.9}
45546
+ </style>
45547
+ <div class="panel" style="position:relative">
45548
+ <div style="font-size:12px;color:var(--color-text-tertiary);margin-bottom:8px">Right-click an element to comment</div>
45549
+ <div class="anno" data-el="Title" data-val="Q4 revenue">Q4 revenue</div>
45550
+ <div class="anno" data-el="Primary action" data-val="Export">Export</div>
45551
+ </div>
45552
+ <script>
45553
+ var panel = document.querySelector('.panel');
45554
+ var current = null;
45555
+ function closeBox(){
45556
+ if (current) current.classList.remove('marked');
45557
+ var ov = panel.querySelector('.overlay');
45558
+ if (ov) ov.remove();
45559
+ current = null;
45560
+ }
45561
+ function openBox(el){
45562
+ closeBox();
45563
+ current = el;
45564
+ el.classList.add('marked');
45565
+ var ov = document.createElement('div');
45566
+ ov.className = 'overlay';
45567
+ ov.innerHTML = '<div class="modal">' +
45568
+ '<div class="mt">Note: ' + el.dataset.el + '</div>' +
45569
+ (el.dataset.val ? '<div class="ms">Current: ' + el.dataset.val + '</div>' : '') +
45570
+ '<textarea id="cmt" placeholder="What should change? e.g. bolder bar / expand by default / add a year filter"></textarea>' +
45571
+ '<div class="mrow"><button id="cancel">Cancel</button><button class="primary" id="send">Send note \u2197</button></div>' +
45572
+ '</div>';
45573
+ panel.appendChild(ov);
45574
+ ov.querySelector('#cmt').focus();
45575
+ ov.querySelector('#send').onclick = function(){
45576
+ var text = ov.querySelector('#cmt').value.trim();
45577
+ if (text && typeof sendPrompt === 'function') {
45578
+ sendPrompt('[design note] ' + el.dataset.el + (el.dataset.val ? ' (current: ' + el.dataset.val + ')' : '') + ' \u2014 change: ' + text);
45579
+ }
45580
+ closeBox();
45581
+ };
45582
+ ov.querySelector('#cancel').onclick = closeBox;
45583
+ ov.addEventListener('click', function(e){ if (e.target === ov) closeBox(); });
45584
+ }
45585
+ document.addEventListener('keydown', function(e){ if (e.key === 'Escape') closeBox(); });
45586
+ panel.addEventListener('contextmenu', function(e){
45587
+ var el = e.target.closest('.anno');
45588
+ if (!el) return;
45589
+ e.preventDefault(); e.stopPropagation();
45590
+ openBox(el);
45591
+ });
45592
+ </script>
45593
+ \`\`\`
45594
+
45595
+ - 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\`).
45596
+
45597
+ Keep it lightweight: annotate only meaningful elements, reuse one overlay, no libraries, and put \`<script>\` last so the draft streams in top-down.`;
45176
45598
  var MODULE_SECTIONS = {
45177
45599
  art: [SVG_SETUP, ART_AND_ILLUSTRATION],
45178
- mockup: [UI_COMPONENTS, COLOR_PALETTE],
45600
+ mockup: [UI_COMPONENTS, COLOR_PALETTE, ANNOTATABLE_MOCKUP],
45179
45601
  interactive: [UI_COMPONENTS, COLOR_PALETTE],
45180
45602
  chart: [UI_COMPONENTS, COLOR_PALETTE, CHARTS_CHART_JS],
45181
45603
  diagram: [COLOR_PALETTE, SVG_SETUP, DIAGRAM_TYPES]
@@ -45677,7 +46099,7 @@ function renderTaskBinding(taskId) {
45677
46099
 
45678
46100
  ## Bound Task
45679
46101
 
45680
- You are executing task \`${taskId}\`. Read its contract with \`manage_task get\`, update it as you work, and complete it when done.
46102
+ 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.
45681
46103
  `;
45682
46104
  }
45683
46105
  function createBoundTaskLifecycle() {
@@ -46196,7 +46618,7 @@ var manageTaskSchema = import_zod68.z.object({
46196
46618
  title: import_zod68.z.string().optional().describe("Task title (required for create)"),
46197
46619
  description: import_zod68.z.string().optional().describe("Task description in Markdown"),
46198
46620
  priority: import_zod68.z.enum(["low", "medium", "high"]).optional().describe("Priority level"),
46199
- status: import_zod68.z.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"),
46621
+ status: import_zod68.z.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"),
46200
46622
  dueDate: import_zod68.z.string().optional().describe("Due date (ISO 8601 format)"),
46201
46623
  metadata: import_zod68.z.record(import_zod68.z.unknown()).optional().describe("Structured metadata (e.g. projectId, module)"),
46202
46624
  metadataFilter: import_zod68.z.record(
@@ -47787,8 +48209,8 @@ ${TASK_CONVERGENCE_GUIDANCE}
47787
48209
  - update: Update a task's content fields (title/description/priority/dueDate/metadata/parentId/sourceId/context/ownerId/result/failureReason/requireReview/dependencies/files)
47788
48210
  Status changes can also be done here, but use set_status for a simpler dedicated status update.
47789
48211
  - set_status: Change a task's status (id and status required). Use this when you only need to change the status.
47790
- Start processing a task you hold: set_status with status='in_progress' (the engine binds it to this run's thread).
47791
- To mark complete: set_status with status='completed', result='...' (the task must be in_progress)
48212
+ 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.
48213
+ 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.
47792
48214
  To mark failed: set_status with status='failed', failureReason='...'
47793
48215
  To pause for input: set_status with status='interrupted', summary='what is needed'
47794
48216
  Valid status values: pending, in_progress, review, failed, interrupted, completed, cancelled.
@@ -47805,16 +48227,16 @@ Report existing keys directly; set 'isNew: true' to add a key the owner does not
47805
48227
  track yet. Belief is evidence metadata: it never blocks a completion, and an
47806
48228
  unmatched or malformed entry is recorded without changing the owner's table.
47807
48229
  Create always persists a pending record \u2014 a supplied status is coerced and no
47808
- execution is dispatched. When you start processing a task you hold, set it
47809
- \`in_progress\` with \`manage_task set_status\`; a dispatched task is claimed
47810
- automatically by the engine. Use \`task { taskId, agentId }\` to delegate a subtask
48230
+ execution is dispatched. Set \`in_progress\` yourself ONLY for a task THIS run
48231
+ executes; a task you delegate stays \`pending\` and is claimed automatically by
48232
+ \`task\` dispatch. Use \`task { taskId, agentId }\` to delegate a subtask
47811
48233
  to an expert \u2014 never re-dispatch the task you are processing.`,
47812
48234
  schema: manageTaskSchema
47813
48235
  }
47814
48236
  ),
47815
48237
  (0, import_langchain83.tool)(handleStartTask, {
47816
48238
  name: "task",
47817
- 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.`,
48239
+ 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.`,
47818
48240
  schema: startTaskSchema
47819
48241
  })
47820
48242
  ]
@@ -49512,7 +49934,7 @@ A plugin is the SET of classes it needs, e.g. \`connection + tools + skills\` or
49512
49934
  | Tool shows an interactive panel | \`sandbox-plugins-ui\` |
49513
49935
  | Tool orchestrates platform tools/agents | \`sandbox-plugins-consume-open\` |
49514
49936
  | Tool callable by external MCP clients | \`sandbox-plugins-expose\` |
49515
- | Run as a long-lived resident service | \`sandbox-plugins-deploy\` |
49937
+ | Run as a long-lived resident service (only on the human's explicit request) | \`sandbox-plugins-deploy\` |
49516
49938
  | Validate / test / debug | \`sandbox-plugins-testing\` |
49517
49939
 
49518
49940
  ## Agents (not a supported authoring path)
@@ -49554,8 +49976,8 @@ chart, table, viewer\u2026). It can also call the plugin's OWN tools back from t
49554
49976
  - It is NOT the tool's JSON \`schema\` (that is the input contract for the model).
49555
49977
  - It is NOT the tool \`description\`.
49556
49978
  - It is NOT required: a text/JSON-returning tool needs no UI.
49557
- - Saying "add a UI" means: add a \`ui\` field to the tool in \`plugin.json\` + create
49558
- \`ui/<tool>.html\`. Details in \`sandbox-plugins-ui\`.
49979
+ - Saying "add a UI" means: add a \`ui\` field to the tool in \`plugin.json\` and write
49980
+ \`ui/<tool>.html\` (with \`write_file\`). Details in \`sandbox-plugins-ui\`.
49559
49981
 
49560
49982
  ## Decide first \u2014 clarify these with the user before writing files
49561
49983
  Ask for anything not already stated; do not guess silently:
@@ -49570,7 +49992,7 @@ Ask for anything not already stated; do not guess silently:
49570
49992
  multi-select? connection test needed? \u2192 read \`sandbox-plugins-connection\`.
49571
49993
  - **UI**: should the tool show an interactive panel instead of only text? what
49572
49994
  does it show, how do users interact, external origins? \u2192 read
49573
- \`sandbox-plugins-ui\` and add \`ui\` + \`ui/<tool>.html\` (step 4 below). If the
49995
+ \`sandbox-plugins-ui\` and add \`ui\` + \`ui/<tool>.html\` (step 6 below). If the
49574
49996
  user says no UI, skip it \u2014 text-only is fine.
49575
49997
  - **Skills**: procedure or knowledge? who consumes it? trigger phrase? tied to a
49576
49998
  specific tool?
@@ -49603,13 +50025,34 @@ Ask for anything not already stated; do not guess silently:
49603
50025
  \`\`\`
49604
50026
 
49605
50027
  ## Flow
49606
- 1. Read \`sandbox-plugins-manifest\` and write \`plugin.json\`.
49607
- 2. Read \`sandbox-plugins-handlers\` and write each handler.
49608
- 3. If the plugin needs credentials: read \`sandbox-plugins-connection\`.
49609
- 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\`).
49610
- 5. If a tool must be callable by external MCP clients: read \`sandbox-plugins-expose\`
50028
+ 1. **Design first, and iterate \u2014 then get explicit approval before you build (non-trivial plugins).**
50029
+ Before writing any file, produce the design and review it with the human. It has two parts,
50030
+ with different treatments:
50031
+ - **Functional design** (always): an **interactive** component map of what you intend to
50032
+ build \u2014 the tools, their inputs/outputs, whether a connection is needed, whether a UI
50033
+ is needed, and how the agent calls them. Make it click-to-explore (e.g. click a tool to
50034
+ reveal its handler / IO / connection); it is a map to **confirm**, not to annotate.
50035
+ - **UI design** (only when the UI class applies): an **annotatable** mock of the tool's UI \u2014
50036
+ the human right-clicks elements to leave notes (see \`sandbox-plugins-ui\` \u2192
50037
+ "Reviewable drafts").
50038
+ Show both drafts with the \`widget\` middleware, then run the review loop:
50039
+ - Ask the human to review (say how to comment). On each round of comments, **restate the
50040
+ feedback** (element + requested change), then **regenerate the draft with the feedback and
50041
+ present it again** \u2014 comments are "revise and re-show", never "start building".
50042
+ - **Do NOT scaffold or write any file during the loop.** Implement only after the human
50043
+ **explicitly approves**, and ask for that go-ahead yourself before you start.
50044
+ Skip the drafts only for a trivial single-tool plugin whose shape is already agreed \u2014 but
50045
+ still confirm the goal.
50046
+ 2. Scaffold with \`manage_plugin { action: "create", type, name }\` \u2014 creates
50047
+ \`/root/.agents/plugins/<type>/\` with a stub \`plugin.json\`. It carries no content.
50048
+ 3. Read \`sandbox-plugins-manifest\` and write \`plugin.json\` with \`write_file\`.
50049
+ 4. Read \`sandbox-plugins-handlers\` and write each handler with \`write_file\`.
50050
+ 5. If the plugin needs credentials: read \`sandbox-plugins-connection\`.
50051
+ 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\`.
50052
+ 7. If a tool must be callable by external MCP clients: read \`sandbox-plugins-expose\`
49611
50053
  and add \`expose: true\` (plus \`readOnly\`/\`destructive\` accuracy hints).
49612
- 6. Read \`sandbox-plugins-testing\` and iterate using \`manage_plugin\`.
50054
+ 8. Read \`sandbox-plugins-testing\` and iterate: edit files with \`edit_file\`, then
50055
+ \`validate\` \u2192 \`reload\` \u2192 \`test\`.
49613
50056
 
49614
50057
  Templates: \`templates/plugin.json\`, \`templates/handler.js\`, \`templates/test.js\`, \`templates/discover.js\`, \`templates/ui.html\`.
49615
50058
  `,
@@ -49671,6 +50114,20 @@ description: Add an interactive HTML UI to a tenant plugin tool (rendered as a s
49671
50114
  A tool can declare an HTML file that Axiom renders as a sandboxed MCP App when
49672
50115
  the tool runs. The app can call the plugin's OWN tools back.
49673
50116
 
50117
+ ## Reviewable drafts (before you build the real UI)
50118
+ When you show a **UI mock with \`show_widget\`** for the user to react to (the design
50119
+ step, before writing \`ui/<tool>.html\`), make it **annotatable**: the user right-clicks
50120
+ any element to leave a comment that comes back to you as a message. Turn it on by
50121
+ default for review drafts and include a small visible hint (e.g. "\u53F3\u952E\u4EFB\u610F\u5143\u7D20\u6279\u6CE8");
50122
+ the \`widget\` mockup guidelines have the pattern. Omit it only when the draft is meant
50123
+ to be purely experienced (a live demo with no review) \u2014 then leave out the hint and
50124
+ the handlers. This is only for the **mock**: the real \`ui/<tool>.html\` is the plugin's
50125
+ product and is never annotatable.
50126
+ A round of annotations is **NOT approval**: restate the comments you received, regenerate
50127
+ the mock from them, and show it again, iterating until the human is satisfied. Build
50128
+ \`ui/<tool>.html\` only after the human **explicitly approves** \u2014 and ask for that go-ahead
50129
+ yourself.
50130
+
49674
50131
  ## Declare it
49675
50132
  \`\`\`
49676
50133
  /root/.agents/plugins/<type>/
@@ -49703,11 +50160,10 @@ the tool runs. The app can call the plugin's OWN tools back.
49703
50160
  - Omit \`csp\` for the restrictive default (\`default-src 'none'\`). WebAssembly is allowed by default.
49704
50161
 
49705
50162
  ## How to write the HTML file
49706
- The HTML must end up at \`/root/.agents/plugins/<type>/ui/<tool>.html\`. Two ways:
49707
- - \`manage_plugin\` create/update: pass it in the \`handlers\` map \u2014 that map carries ALL
49708
- plugin files, not just JS handlers. Example: \`handlers\` = \`{ "handlers/render_report.js": "<js...>", "ui/report.html": "<!doctype html>..." }\`.
49709
- The validator's existence check runs against these keys, so an HTML file NOT in this map is treated as missing (\`TOOL_UI\`).
49710
- - Or write the file directly in the sandbox under \`ui/\` and call \`manage_plugin reload\`.
50163
+ Write the HTML to \`/root/.agents/plugins/<type>/ui/<tool>.html\` with \`write_file\`,
50164
+ then call \`manage_plugin { action: "reload", type }\` so the host serves the new HTML.
50165
+ The file must be declared by a tool's \`ui.resource\` in \`plugin.json\`, otherwise it is
50166
+ treated as missing (\`TOOL_UI\`).
49711
50167
 
49712
50168
  ## How it behaves at runtime
49713
50169
  - 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\`.
@@ -49803,7 +50259,7 @@ grants apply to the UI too). See \`sandbox-plugins-expose\`.
49803
50259
  ## Gotchas
49804
50260
  - \`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.
49805
50261
  - \`displayMode\`, \`permissions\`, and \`prefersBorder\` are accepted but NOT yet applied.
49806
- - 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.
50262
+ - 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.
49807
50263
 
49808
50264
  Start from \`templates/ui.html\`.
49809
50265
  `,
@@ -50067,21 +50523,23 @@ description: Validate, test, and reload a tenant sandbox plugin using the manage
50067
50523
 
50068
50524
  Use the \`manage_plugin\` tool. The loop:
50069
50525
 
50070
- 1. \`action: "validate"\` with \`{ type, manifest, handlers }\` \u2014 dry-run, no writes, returns diagnostics.
50071
- 2. \`action: "create"\` or \`"update"\` \u2014 writes files, bumps version, reloads.
50526
+ 1. Write/edit the plugin files with the filesystem tools (\`write_file\`/\`edit_file\`) under
50527
+ \`/root/.agents/plugins/<type>/\`; \`create\` only scaffolds the directory + stub.
50528
+ 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.
50072
50529
  3. If the plugin has \`connection.fields\` and a tool reads credentials: a **human**
50073
50530
  must configure a connection of this plugin type. You cannot create credentials.
50074
50531
  Call \`list_connections { type }\` first \u2014 if none exist, use \`ask_user_to_clarify\`
50075
50532
  to state explicitly that only the human can configure the connector, ask them to
50076
50533
  add it (Console \u2192 Connections) and to give you the connection key. Do not claim
50077
50534
  the tool works until a real \`connectionKey\` test passes.
50078
- 4. \`action: "test"\` to exercise a handler without an LLM:
50535
+ 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).
50536
+ 5. \`action: "test"\` to exercise a handler without an LLM:
50079
50537
  - Tool: \`{ type, target: { kind: "tool", name, input, connectionKey? } }\`
50080
50538
  - Connection: \`{ type, target: { kind: "connection-test" } }\` or \`{ kind: "connection-discover" }\`
50081
50539
  Returns \`{ ok, result, exitCode, durationMs, stdout, stderr }\`.
50082
50540
  For credentialed tools, pass the real \`connectionKey\`. Without one, the handler
50083
50541
  receives an empty connection \u2014 that only checks plumbing, NOT functional success.
50084
- 5. \`action: "reload"\` if you edited files directly.
50542
+ 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.
50085
50543
 
50086
50544
  ## Secrets
50087
50545
  - \`list_connections\` returns identifiers only (\`key\`/\`name\`/\`description\`), never config or secrets.
@@ -50096,10 +50554,10 @@ Use the \`manage_plugin\` tool. The loop:
50096
50554
  | HANDLER_MISSING | handler path wrong or file not written |
50097
50555
  | TOOL_SCHEMA | unsupported JSON Schema keyword |
50098
50556
  | non-zero exit | handler threw; read stdout/stderr from \`test\` |
50099
- | old behaviour after edit | version/hash unchanged \u2014 bump \`version\` or call \`reload\` |
50557
+ | old behaviour after edit | the catalog caches by content hash/TTL \u2014 call \`reload\` |
50100
50558
  | empty connection in handler | no \`connectionKey\` passed, or no connection configured for this type |
50101
50559
 
50102
- Fall back to \`execute_code\`/\`shell\` to run \`node handlers/x.js\` manually with
50560
+ Fall back to \`shell_exec\` to run \`node handlers/x.js\` manually with
50103
50561
  \`PLUGIN_INPUT\` and a synthetic \`PLUGIN_CONNECTION_FILE\` to reproduce logic bugs.
50104
50562
  A synthetic fixture only verifies plumbing \u2014 a real upstream call requires a real
50105
50563
  \`connectionKey\` configured by a human.
@@ -50110,7 +50568,7 @@ A synthetic fixture only verifies plumbing \u2014 a real upstream call requires
50110
50568
  version: "1.0.0",
50111
50569
  content: `---
50112
50570
  name: sandbox-plugins-deploy
50113
- 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.
50571
+ 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.
50114
50572
  ---
50115
50573
 
50116
50574
  # Deploying a Plugin as a Resident Service
@@ -50120,10 +50578,13 @@ Optionally, you can **deploy** the plugin as a **resident service**: one
50120
50578
  long-lived sandbox per plugin that serves every call. This is faster for
50121
50579
  frequent or heavy calls, but costs a running sandbox.
50122
50580
 
50123
- ## Precondition: tests must pass first
50124
- Deploy ONLY after the plugin is validated AND its tests pass:
50125
- 1. \`manage_plugin validate\` \u2014 no diagnostics.
50126
- 2. \`manage_plugin create\`/\`update\` \u2014 written and reloaded.
50581
+ ## Precondition: human request, then tests must pass first
50582
+ Deployment is **opt-in and human-gated**: NEVER deploy on your own after tests
50583
+ pass. Wait until the human explicitly asks for it or approves it. A passing test
50584
+ is necessary but NOT sufficient to deploy.
50585
+ Then confirm the plugin is validated AND its tests pass:
50586
+ 1. Files written with the filesystem tools and \`reload\` run.
50587
+ 2. \`manage_plugin validate\` \u2014 no diagnostics.
50127
50588
  3. \`manage_plugin test\` \u2014 every tool/connection test returns \`ok: true\`
50128
50589
  (a real \`connectionKey\` test for credentialed tools).
50129
50590
  Never deploy a plugin you have not tested: a resident service wraps the same
@@ -50191,6 +50652,30 @@ load "sandbox-plugins-expose" and follow it (\`expose: true\` + key grants).
50191
50652
  If a tool must call the platform's own tools/agents from its handler, load
50192
50653
  "sandbox-plugins-consume-open".
50193
50654
 
50655
+ Design first, and iterate on it before you build \u2014 the design is a conversation, not
50656
+ a one-shot. Use the \`widget\` middleware to present it (a decision surface, not a showcase):
50657
+ - **Functional design** (always): an **interactive** component map of the shape you
50658
+ intend to build \u2014 which tools, their inputs/outputs, whether a connection or a UI
50659
+ is needed, how the agent calls them. Make it click-to-explore (and prefer it over
50660
+ an SVG flowchart); it is a map to **confirm**, NOT an annotatable draft.
50661
+ - **UI design** (UI class only): an **annotatable** mock of the tool's UI \u2014 the human
50662
+ right-clicks elements to leave notes.
50663
+ Then run a review loop:
50664
+ - Ask the human to review (tell them how to comment). When they send comments, **restate the
50665
+ feedback** (which element, what change) so they can confirm you understood, then **build a
50666
+ NEW draft that incorporates the feedback and present it again**; keep iterating until
50667
+ they are satisfied. Comments mean "revise and re-show", NOT "start building".
50668
+ - Do NOT scaffold or write any file during the loop. Implement only once the human
50669
+ **explicitly approves** the design \u2014 and still ASK for that go-ahead yourself; never
50670
+ start building on your own initiative.
50671
+ Also surface genuinely different options (scope, layout, which tools) visually and let
50672
+ the human choose, instead of describing them in prose.
50673
+
50674
+ How to render a good widget is the widget middleware's own concern \u2014 follow it
50675
+ there; keep this prompt to plugin-design intent. These widgets are design aids
50676
+ only; the plugin's actual UI is still the MCP App HTML you author and test with
50677
+ \`manage_plugin\`.
50678
+
50194
50679
  Plan with persistent Tasks: MANDATORY until you load "task-tracking" with the
50195
50680
  \`skill\` tool, call neither \`manage_task\` nor any other task operation. Use
50196
50681
  persistent Tasks as the development planning surface and create one Task per
@@ -50198,7 +50683,16 @@ independently verifiable outcome (for example "manifest written", "handlers
50198
50683
  implemented", "UI added", "validated/tested via manage_plugin"). As you work,
50199
50684
  update each Task's status to reflect reality. Never use \`write_todos\`.
50200
50685
 
50201
- Author with the \`manage_plugin\` tool: validate, then create/update, then test.
50686
+ After the human confirms the design, author the plugin files directly with the filesystem tools, then verify with \`manage_plugin\`.
50687
+ 1. Scaffold once: \`manage_plugin { action: "create", type: "<type>", name }\` \u2014 it creates
50688
+ \`/root/.agents/plugins/<type>/\` with a stub \`plugin.json\`. It never carries content.
50689
+ 2. Write and edit every file yourself with \`write_file\` / \`edit_file\` (read back with
50690
+ \`read_file\`) under \`/root/.agents/plugins/<type>/\`: \`plugin.json\`, \`handlers/*.js\`, and
50691
+ (UI class) \`ui/*.html\`. Change one file at a time; edit in place instead of rewriting.
50692
+ 3. Verify in order: \`manage_plugin { action: "validate", type }\` (reads the files back from
50693
+ the sandbox) \u2192 \`manage_plugin { action: "reload", type }\` (the catalog caches, so a
50694
+ freshly written file is invisible to \`test\` until you reload) \u2192 \`manage_plugin
50695
+ { action: "test", type, target }\`.
50202
50696
  If a tool needs credentials, a human must configure the connection \u2014 use
50203
50697
  \`list_connections\` to check, and \`ask_user_to_clarify\` to ask the human to add
50204
50698
  the connector and give you its key. Only after a real \`connectionKey\` test
@@ -50207,14 +50701,15 @@ unverified (missing credentials), not as passing. Use \`shell_exec\` (the tool
50207
50701
  provided by the \`code_eval\` middleware) to run \`node handlers/x.js\` for manual
50208
50702
  checks when reproducing a failure.
50209
50703
 
50210
- After the plugin passes \`validate\` and its tests, deploy it as a resident
50211
- service: load "sandbox-plugins-deploy" and follow it. Choose \`onDemand\`
50704
+ Deployment is human-gated: do NOT deploy on your own after the tests pass.
50705
+ Only deploy when the human explicitly asks for it or approves it, then load
50706
+ "sandbox-plugins-deploy" and follow it. Choose \`onDemand\`
50212
50707
  (default) or \`alwaysOn\` for hot paths, call \`manage_plugin deploy\` with the
50213
50708
  chosen \`mode\`, then confirm with \`manage_plugin status\` (\`running\`). Only claim
50214
50709
  the plugin is deployed and serving once \`status\` reports \`running\`; if the
50215
50710
  environment reports no deployment service, say so and treat deployment as
50216
- unavailable \u2014 do NOT claim success. \`undeploy\` removes the service without
50217
- touching the plugin files.`;
50711
+ unavailable \u2014 do NOT claim success. Report the plugin as ready and let the human
50712
+ decide; \`undeploy\` removes the service without touching the plugin files.`;
50218
50713
 
50219
50714
  // src/middlewares/sandboxPluginsUi.ts
50220
50715
  var SANDBOX_PLUGINS_UI_RESOURCE = "ui://sandbox-plugins/app";
@@ -50390,6 +50885,7 @@ var SANDBOX_PLUGINS_APP_HTML = `<!doctype html>
50390
50885
  `;
50391
50886
 
50392
50887
  // src/middlewares/sandboxPluginsMiddleware.ts
50888
+ var PLUGINS_ROOT3 = "/root/.agents/plugins";
50393
50889
  var SANDBOX_PLUGINS_UI_REF = {
50394
50890
  kind: "plugin",
50395
50891
  pluginType: "sandbox-plugins",
@@ -50446,18 +50942,19 @@ var managePluginSchema = import_zod71.z.object({
50446
50942
  "get",
50447
50943
  "validate",
50448
50944
  "create",
50449
- "update",
50450
50945
  "delete",
50451
50946
  "reload",
50452
50947
  "test",
50948
+ "backup",
50949
+ "restore",
50453
50950
  "deploy",
50454
50951
  "undeploy",
50455
50952
  "status",
50456
50953
  "deployments"
50457
50954
  ]),
50458
50955
  type: import_zod71.z.string().optional(),
50459
- manifest: import_zod71.z.record(import_zod71.z.unknown()).optional(),
50460
- handlers: import_zod71.z.record(import_zod71.z.string()).optional(),
50956
+ name: import_zod71.z.string().optional(),
50957
+ description: import_zod71.z.string().optional(),
50461
50958
  mode: import_zod71.z.enum(["onDemand", "alwaysOn"]).optional(),
50462
50959
  target: import_zod71.z.object({
50463
50960
  kind: import_zod71.z.enum(["tool", "connection-test", "connection-discover"]),
@@ -50471,12 +50968,12 @@ var sandboxPluginsPlugin = {
50471
50968
  type: "sandbox-plugins",
50472
50969
  category: "execution",
50473
50970
  name: "Sandbox Plugins",
50474
- 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.",
50971
+ 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.",
50475
50972
  recommends: ["skill"],
50476
50973
  tools: [
50477
50974
  {
50478
50975
  name: "manage_plugin",
50479
- 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.",
50976
+ 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.",
50480
50977
  ui: { resource: SANDBOX_PLUGINS_UI_RESOURCE, displayMode: "inline" }
50481
50978
  }
50482
50979
  ],
@@ -50503,7 +51000,7 @@ var sandboxPluginsPlugin = {
50503
51000
  id: "sandbox-plugins",
50504
51001
  type: "sandbox-plugins",
50505
51002
  name: "Sandbox Plugins",
50506
- description: "Author and manage tenant plugins (list/validate/create/update/delete/reload/test/deploy/undeploy/status)",
51003
+ description: "Author and manage tenant plugins (list/validate/create/delete/reload/test/backup/restore/deploy/undeploy/status)",
50507
51004
  enabled: true,
50508
51005
  config: {}
50509
51006
  },
@@ -50523,6 +51020,14 @@ var sandboxPluginsPlugin = {
50523
51020
  enabled: true,
50524
51021
  config: {}
50525
51022
  },
51023
+ {
51024
+ id: "widget",
51025
+ type: "widget",
51026
+ name: "Widget",
51027
+ 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.",
51028
+ enabled: true,
51029
+ config: {}
51030
+ },
50526
51031
  {
50527
51032
  id: "ask_user_to_clarify",
50528
51033
  type: "ask_user_to_clarify",
@@ -50563,6 +51068,7 @@ var sandboxPluginsPlugin = {
50563
51068
  return JSON.stringify({
50564
51069
  plugins: loaded.plugins.map((p) => ({
50565
51070
  type: p.meta.type,
51071
+ path: `${PLUGINS_ROOT3}/${p.meta.type}`,
50566
51072
  name: p.meta.name,
50567
51073
  tools: p.meta.tools?.map((t) => t.name) ?? [],
50568
51074
  source: p.meta.source
@@ -50573,32 +51079,87 @@ var sandboxPluginsPlugin = {
50573
51079
  }
50574
51080
  case "get": {
50575
51081
  if (!input.type) return "type is required";
50576
- return await catalog2.readPluginFile(tenantId2, input.type, "plugin.json");
51082
+ const content = await catalog2.readPluginFile(tenantId2, input.type, "plugin.json");
51083
+ let manifest = content;
51084
+ try {
51085
+ manifest = JSON.parse(content);
51086
+ } catch {
51087
+ }
51088
+ return JSON.stringify({
51089
+ type: input.type,
51090
+ path: `${PLUGINS_ROOT3}/${input.type}`,
51091
+ manifest
51092
+ });
50577
51093
  }
50578
51094
  case "reload": {
50579
51095
  const loaded = await catalog2.reload(tenantId2);
50580
51096
  void publishPluginCatalogChanged(tenantId2);
50581
51097
  return JSON.stringify({ reloaded: true, plugins: loaded.plugins.length, diagnostics: loaded.diagnostics });
50582
51098
  }
50583
- case "create":
50584
- case "update": {
50585
- if (!input.type || !input.manifest) return "type and manifest are required";
50586
- const handlers = input.handlers ?? {};
51099
+ case "backup": {
51100
+ if (!input.type) return "type is required";
51101
+ try {
51102
+ const result = await catalog2.backupPlugin(tenantId2, input.type);
51103
+ if (result.files === 0) {
51104
+ return JSON.stringify({ backedUp: false, error: "nothing to back up (plugin has no files)" });
51105
+ }
51106
+ return JSON.stringify({ backedUp: true, files: result.files });
51107
+ } catch (err) {
51108
+ return JSON.stringify({ backedUp: false, error: errorText(err) });
51109
+ }
51110
+ }
51111
+ case "restore": {
51112
+ if (!input.type) return "type is required";
51113
+ let restored;
51114
+ let failure3;
51115
+ try {
51116
+ restored = await catalog2.restorePlugin(tenantId2, input.type);
51117
+ } catch (err) {
51118
+ failure3 = errorText(err);
51119
+ }
51120
+ try {
51121
+ await catalog2.reload(tenantId2);
51122
+ void publishPluginCatalogChanged(tenantId2);
51123
+ } catch {
51124
+ }
51125
+ if (failure3) return JSON.stringify({ restored: false, error: failure3 });
51126
+ return JSON.stringify({ restored: true, files: restored?.files ?? 0 });
51127
+ }
51128
+ case "create": {
51129
+ if (!input.type) return "type is required";
50587
51130
  const known = new Set(
50588
51131
  (catalog2.getCached(tenantId2)?.plugins ?? []).map((p) => p.meta.type).filter((t) => t !== input.type)
50589
51132
  );
51133
+ const stub = {
51134
+ schemaVersion: 1,
51135
+ type: input.type,
51136
+ name: input.name ?? input.type,
51137
+ description: input.description ?? "",
51138
+ version: "0.1.0",
51139
+ tools: []
51140
+ };
50590
51141
  const check = validateSandboxPluginManifest({
50591
- raw: input.manifest,
51142
+ raw: stub,
50592
51143
  dirName: input.type,
50593
51144
  reservedTypes: new Set(PluginRegistry.list()),
50594
51145
  knownTypes: known,
50595
- handlerExists: (rel) => rel in handlers
51146
+ handlerExists: () => true
50596
51147
  });
50597
- if (!check.manifest) return JSON.stringify({ written: false, diagnostics: check.diagnostics });
50598
- await catalog2.writePlugin(tenantId2, input.type, input.manifest, handlers);
51148
+ if (!check.manifest) {
51149
+ return JSON.stringify({ created: false, diagnostics: check.diagnostics });
51150
+ }
51151
+ const files = await catalog2.listPluginFiles(tenantId2, input.type);
51152
+ if (files.includes("plugin.json")) {
51153
+ return JSON.stringify({ created: false, error: "plugin already exists" });
51154
+ }
51155
+ await catalog2.writePlugin(tenantId2, input.type, check.manifest, {});
50599
51156
  const loaded = await catalog2.reload(tenantId2);
50600
51157
  void publishPluginCatalogChanged(tenantId2);
50601
- return JSON.stringify({ written: true, diagnostics: loaded.diagnostics, hash: loaded.hash });
51158
+ return JSON.stringify({
51159
+ created: true,
51160
+ path: `${PLUGINS_ROOT3}/${input.type}`,
51161
+ diagnostics: loaded.diagnostics
51162
+ });
50602
51163
  }
50603
51164
  case "delete": {
50604
51165
  if (!input.type) return "type is required";
@@ -50609,18 +51170,8 @@ var sandboxPluginsPlugin = {
50609
51170
  }
50610
51171
  case "validate": {
50611
51172
  if (!input.type) return "type is required";
50612
- const handlers = input.handlers ?? {};
50613
- const known = new Set(
50614
- (catalog2.getCached(tenantId2)?.plugins ?? []).map((p) => p.meta.type).filter((t) => t !== input.type)
50615
- );
50616
- const result = validateSandboxPluginManifest({
50617
- raw: input.manifest ?? {},
50618
- dirName: input.type,
50619
- reservedTypes: new Set(PluginRegistry.list()),
50620
- knownTypes: known,
50621
- handlerExists: (rel) => rel in handlers
50622
- });
50623
- return JSON.stringify({ diagnostics: result.diagnostics, valid: Boolean(result.manifest) });
51173
+ const result = await catalog2.validatePluginFromDisk(tenantId2, input.type);
51174
+ return JSON.stringify({ diagnostics: result.diagnostics, valid: result.valid });
50624
51175
  }
50625
51176
  case "test": {
50626
51177
  if (!input.type || !input.target) return "type and target are required";
@@ -50694,7 +51245,16 @@ var sandboxPluginsPlugin = {
50694
51245
  assistantId: typeof runCfg.assistant_id === "string" ? runCfg.assistant_id : void 0
50695
51246
  }
50696
51247
  });
50697
- return handlerResultText(redactSecretValues(result, secrets));
51248
+ const text = handlerResultText(redactSecretValues(result, secrets));
51249
+ const ui = toolDef.ui;
51250
+ if (!ui?.resource) return text;
51251
+ const uiRef = {
51252
+ kind: "plugin",
51253
+ pluginType: input.type,
51254
+ resource: ui.resource,
51255
+ displayMode: ui.displayMode ?? "inline"
51256
+ };
51257
+ return appendUiFence(text, uiRef);
50698
51258
  } catch (err) {
50699
51259
  return JSON.stringify(redactSecretValues({ ok: false, error: errorText(err) }, secrets));
50700
51260
  }
@@ -50766,7 +51326,7 @@ var sandboxPluginsPlugin = {
50766
51326
  },
50767
51327
  {
50768
51328
  name: "manage_plugin",
50769
- 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.",
51329
+ 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.",
50770
51330
  schema: managePluginSchema
50771
51331
  }
50772
51332
  )
@@ -51950,6 +52510,7 @@ registerBuiltinPlugins();
51950
52510
  buildTableName,
51951
52511
  buildTaskMutationFallback,
51952
52512
  buildTaskNormalizationFallback,
52513
+ canConvergeOrphan,
51953
52514
  checkEmptyContent,
51954
52515
  clearEncryptionKeyCache,
51955
52516
  clearEvalRunService,
@@ -51999,6 +52560,7 @@ registerBuiltinPlugins();
51999
52560
  createTaskLifecycleService,
52000
52561
  createTaskMiddleware,
52001
52562
  createTaskRunBridge,
52563
+ createTaskRunLiveness,
52002
52564
  createTeamMiddleware,
52003
52565
  createTeammateTools,
52004
52566
  createTrustedAgentMessageDispatcher,
@@ -52096,6 +52658,7 @@ registerBuiltinPlugins();
52096
52658
  isMcpConnectionError,
52097
52659
  isPluginSkillAuthorizedInSandbox,
52098
52660
  isPluginSkillManagedInSandbox,
52661
+ isTaskRunLeaseHeld,
52099
52662
  isUsingDefaultKey,
52100
52663
  isValidCronExpression,
52101
52664
  isValidSandboxName,