@axiom-lattice/core 7.0.2 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,
@@ -40404,7 +40618,11 @@ var TaskLifecycleService = class {
40404
40618
  expectedStatuses: ["completed", "cancelled"],
40405
40619
  updates: { status: "in_progress", failureReason: null, result: null },
40406
40620
  summary: "Task reopened",
40407
- detail: { type: "reopen" }
40621
+ detail: { type: "reopen" },
40622
+ // Preserve an existing thread by default (the project runtime reuses the
40623
+ // canonical Task Thread); a dispatch that allocated a fresh run thread
40624
+ // opts in to replacing it.
40625
+ replaceThread: input.replaceThread ?? false
40408
40626
  });
40409
40627
  }
40410
40628
  /**
@@ -42265,6 +42483,39 @@ function createTaskRunBridge(deps) {
42265
42483
  transitioned: true
42266
42484
  };
42267
42485
  }
42486
+ if (task.status === "completed" || task.status === "cancelled") {
42487
+ if (task.ownerType !== "agent") {
42488
+ return {
42489
+ success: false,
42490
+ code: "AGENT_TASK_REQUIRED",
42491
+ error: "Lifecycle transitions only support agent tasks."
42492
+ };
42493
+ }
42494
+ const reopened = await deps.lifecycle.reopenTask({
42495
+ tenantId: tenantId2,
42496
+ taskId: task.id,
42497
+ actor,
42498
+ ...threadId !== void 0 ? { threadId } : {},
42499
+ // The caller allocated a fresh run thread for this dispatch.
42500
+ replaceThread: true
42501
+ });
42502
+ if (!reopened.success) {
42503
+ return {
42504
+ success: false,
42505
+ code: reopened.code,
42506
+ error: reopened.error,
42507
+ ...reopened.hint !== void 0 ? { hint: reopened.hint } : {},
42508
+ ...reopened.retryable !== void 0 ? { retryable: reopened.retryable } : {}
42509
+ };
42510
+ }
42511
+ return {
42512
+ success: true,
42513
+ task: reopened.task,
42514
+ warnings: reopened.warnings ?? [],
42515
+ mutated: true,
42516
+ transitioned: true
42517
+ };
42518
+ }
42268
42519
  return { success: true, task, warnings, mutated: false, transitioned: false };
42269
42520
  },
42270
42521
  async settleRun(input) {
@@ -42437,6 +42688,45 @@ async function tryAcquireTaskRun(input) {
42437
42688
  };
42438
42689
  }
42439
42690
 
42691
+ // src/services/taskLiveness.ts
42692
+ function registeredLeaseStore() {
42693
+ try {
42694
+ return storeLatticeManager.getStoreLattice("default", "lease").store;
42695
+ } catch {
42696
+ return void 0;
42697
+ }
42698
+ }
42699
+ async function isTaskRunLeaseHeld(task) {
42700
+ const store = registeredLeaseStore();
42701
+ if (!store) return false;
42702
+ try {
42703
+ if (await store.get(taskRunLeaseKey(task.id))) return true;
42704
+ const threadId = typeof task.context?.thread_id === "string" ? task.context.thread_id : void 0;
42705
+ if (threadId && task.ownerId && await store.get(threadRunLeaseKey(task.tenantId, task.ownerId, threadId))) {
42706
+ return true;
42707
+ }
42708
+ return false;
42709
+ } catch {
42710
+ return true;
42711
+ }
42712
+ }
42713
+ function createTaskRunLiveness() {
42714
+ return async (task) => taskRunRegistry.isLive(task.id) || isTaskRunLeaseHeld(task);
42715
+ }
42716
+ function isMultiReplicaDeclared() {
42717
+ const id = process.env.AXIOM_REPLICA_ID;
42718
+ return typeof id === "string" && id.trim() !== "";
42719
+ }
42720
+ function isSharedLeaseStore() {
42721
+ const store = registeredLeaseStore();
42722
+ const name = store?.constructor?.name;
42723
+ return typeof name === "string" && !name.startsWith("InMemory");
42724
+ }
42725
+ function canConvergeOrphan() {
42726
+ if (!isMultiReplicaDeclared()) return true;
42727
+ return isSharedLeaseStore();
42728
+ }
42729
+
42440
42730
  // src/services/ExecutionAuthorityRegistry.ts
42441
42731
  var ManagedTaskStartError = class extends Error {
42442
42732
  /**
@@ -42520,7 +42810,10 @@ async function prepareTaskDispatch(deps, input) {
42520
42810
  }
42521
42811
  };
42522
42812
  }
42523
- if (task.status === "completed" || task.status === "cancelled") {
42813
+ const managedAuthority = executionAuthorityRegistry.resolve(task);
42814
+ const terminal = task.status === "completed" || task.status === "cancelled";
42815
+ const reopenableTerminal = terminal && task.ownerType === "agent" && !managedAuthority;
42816
+ if (terminal && !reopenableTerminal) {
42524
42817
  return {
42525
42818
  kind: "failure",
42526
42819
  failure: {
@@ -42544,19 +42837,18 @@ async function prepareTaskDispatch(deps, input) {
42544
42837
  }
42545
42838
  };
42546
42839
  }
42547
- if (task.status !== "pending" && task.status !== "interrupted" && task.status !== "failed") {
42840
+ if (task.status !== "pending" && task.status !== "interrupted" && task.status !== "failed" && !reopenableTerminal) {
42548
42841
  return {
42549
42842
  kind: "failure",
42550
42843
  failure: {
42551
42844
  success: false,
42552
42845
  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.`,
42846
+ error: "Only a `pending`, `interrupted`, `failed`, or reopenable terminal task can be dispatched.",
42847
+ 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
42848
  taskId: task.id
42556
42849
  }
42557
42850
  };
42558
42851
  }
42559
- const managedAuthority = executionAuthorityRegistry.resolve(task);
42560
42852
  if (managedAuthority) {
42561
42853
  if (managedAuthority.onManagedStart) {
42562
42854
  try {
@@ -42726,7 +43018,8 @@ function persistedExecutionObservation(item, attempted) {
42726
43018
  }
42727
43019
  function buildWorkerMessage(task, parent, dependencies) {
42728
43020
  const sections2 = [
42729
- "Execute the following persistent task. Return your result as an observation; do not assume the task is complete.",
43021
+ `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.`,
43022
+ "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
43023
  `Persisted Task ID: \`${task.id}\``,
42731
43024
  `# Task: ${task.title}`,
42732
43025
  task.description?.trim() || "No task description was provided."
@@ -42758,9 +43051,17 @@ function createTaskExecutionService(deps) {
42758
43051
  taskStore: deps.taskStore,
42759
43052
  workItemStore: deps.workItemStore
42760
43053
  });
43054
+ const isTaskHeldByLiveRun = deps.isTaskHeldByLiveRun ?? createTaskRunLiveness();
42761
43055
  const run = async (plan) => {
42762
43056
  const { tenantId: tenantId2, agentId, actor, parent, completedDependencies } = plan;
42763
43057
  let task = plan.task;
43058
+ const readTaskStatus = async () => {
43059
+ try {
43060
+ return (await deps.taskStore.getById(tenantId2, task.id))?.status;
43061
+ } catch {
43062
+ return void 0;
43063
+ }
43064
+ };
42764
43065
  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
43066
  const storedRunId = typeof task.context?.run_id === "string" && task.context.run_id.trim() !== "" ? task.context.run_id : void 0;
42766
43067
  let reuseCurrentRun = false;
@@ -42819,7 +43120,7 @@ function createTaskExecutionService(deps) {
42819
43120
  task = transfer.task;
42820
43121
  }
42821
43122
  const warnings = [];
42822
- if (task.status === "pending" || task.status === "interrupted" || task.status === "failed") {
43123
+ if (task.status === "pending" || task.status === "interrupted" || task.status === "failed" || task.status === "completed" || task.status === "cancelled") {
42823
43124
  const begun = await bridge.beginRun({
42824
43125
  tenantId: tenantId2,
42825
43126
  task,
@@ -42946,6 +43247,7 @@ function createTaskExecutionService(deps) {
42946
43247
  ...warnings.length > 0 ? { warnings } : {}
42947
43248
  };
42948
43249
  }
43250
+ const settledStatus = await readTaskStatus();
42949
43251
  return {
42950
43252
  success: false,
42951
43253
  code: failureCode,
@@ -42954,6 +43256,7 @@ function createTaskExecutionService(deps) {
42954
43256
  agentId,
42955
43257
  threadId,
42956
43258
  executionResultId: eventKey,
43259
+ ...settledStatus !== void 0 ? { taskStatus: settledStatus } : {},
42957
43260
  ...warnings.length > 0 ? { warnings } : {}
42958
43261
  };
42959
43262
  } catch (persistenceError) {
@@ -43002,6 +43305,7 @@ function createTaskExecutionService(deps) {
43002
43305
  ...warnings.length > 0 ? { warnings } : {}
43003
43306
  };
43004
43307
  }
43308
+ const taskStatus = await readTaskStatus();
43005
43309
  return {
43006
43310
  success: true,
43007
43311
  taskId: task.id,
@@ -43009,6 +43313,7 @@ function createTaskExecutionService(deps) {
43009
43313
  threadId,
43010
43314
  observation: validated.observation,
43011
43315
  executionResultId: eventKey,
43316
+ ...taskStatus !== void 0 ? { taskStatus } : {},
43012
43317
  ...warnings.length > 0 ? { warnings } : {}
43013
43318
  };
43014
43319
  } catch (error) {
@@ -43029,6 +43334,23 @@ function createTaskExecutionService(deps) {
43029
43334
  };
43030
43335
  return {
43031
43336
  start: async (input) => {
43337
+ let current;
43338
+ try {
43339
+ current = await deps.taskStore.getById(input.tenantId, input.taskId);
43340
+ } catch {
43341
+ current = null;
43342
+ }
43343
+ if (current && current.status === "in_progress" && current.ownerType === "agent" && canConvergeOrphan() && !executionAuthorityRegistry.resolve(current) && !await isTaskHeldByLiveRun(current)) {
43344
+ try {
43345
+ await lifecycle.failTask({
43346
+ tenantId: input.tenantId,
43347
+ taskId: current.id,
43348
+ actor: input.actor,
43349
+ failureReason: "Run is no longer executing this task (no_executor)"
43350
+ });
43351
+ } catch {
43352
+ }
43353
+ }
43032
43354
  const prepared = await prepareTaskDispatch(deps, input);
43033
43355
  if (prepared.kind === "managed") return prepared.result;
43034
43356
  if (prepared.kind === "failure") return prepared.failure;
@@ -45173,9 +45495,85 @@ Use \`imagine_html\`. Wrap the entire thing in a single raised card. All content
45173
45495
  </div>
45174
45496
  </div>
45175
45497
  \`\`\``;
45498
+ var ANNOTATABLE_MOCKUP = `## Annotatable drafts (reviewable mockups)
45499
+
45500
+ 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.)
45501
+
45502
+ - 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\`.
45503
+ - 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.
45504
+ - 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).
45505
+ - Handle \`contextmenu\` with event delegation; \`preventDefault()\` + \`stopPropagation()\` (this replaces the native browser menu, which is fine on a draft).
45506
+ - 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.
45507
+ - 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.
45508
+
45509
+ \`\`\`html
45510
+ <style>
45511
+ .anno{position:relative}
45512
+ .anno:hover{outline:1px dashed var(--color-border-info);outline-offset:-2px;border-radius:var(--border-radius-md)}
45513
+ .anno.marked{outline:1.5px solid var(--color-text-info);outline-offset:-2px}
45514
+ .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}
45515
+ .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}
45516
+ .modal .mt{font-size:14px;font-weight:500;margin-bottom:2px}
45517
+ .modal .ms{font-size:11px;color:var(--color-text-tertiary);margin-bottom:10px}
45518
+ .modal textarea{width:100%;box-sizing:border-box;height:auto;min-height:72px;padding:8px 10px;resize:vertical}
45519
+ .modal .mrow{display:flex;gap:8px;justify-content:flex-end;margin-top:10px}
45520
+ .primary{background:var(--color-text-info);color:var(--color-background-primary);border-color:var(--color-text-info)}
45521
+ .primary:hover{background:var(--color-text-info);opacity:.9}
45522
+ </style>
45523
+ <div class="panel" style="position:relative">
45524
+ <div style="font-size:12px;color:var(--color-text-tertiary);margin-bottom:8px">Right-click an element to comment</div>
45525
+ <div class="anno" data-el="Title" data-val="Q4 revenue">Q4 revenue</div>
45526
+ <div class="anno" data-el="Primary action" data-val="Export">Export</div>
45527
+ </div>
45528
+ <script>
45529
+ var panel = document.querySelector('.panel');
45530
+ var current = null;
45531
+ function closeBox(){
45532
+ if (current) current.classList.remove('marked');
45533
+ var ov = panel.querySelector('.overlay');
45534
+ if (ov) ov.remove();
45535
+ current = null;
45536
+ }
45537
+ function openBox(el){
45538
+ closeBox();
45539
+ current = el;
45540
+ el.classList.add('marked');
45541
+ var ov = document.createElement('div');
45542
+ ov.className = 'overlay';
45543
+ ov.innerHTML = '<div class="modal">' +
45544
+ '<div class="mt">Note: ' + el.dataset.el + '</div>' +
45545
+ (el.dataset.val ? '<div class="ms">Current: ' + el.dataset.val + '</div>' : '') +
45546
+ '<textarea id="cmt" placeholder="What should change? e.g. bolder bar / expand by default / add a year filter"></textarea>' +
45547
+ '<div class="mrow"><button id="cancel">Cancel</button><button class="primary" id="send">Send note \u2197</button></div>' +
45548
+ '</div>';
45549
+ panel.appendChild(ov);
45550
+ ov.querySelector('#cmt').focus();
45551
+ ov.querySelector('#send').onclick = function(){
45552
+ var text = ov.querySelector('#cmt').value.trim();
45553
+ if (text && typeof sendPrompt === 'function') {
45554
+ sendPrompt('[design note] ' + el.dataset.el + (el.dataset.val ? ' (current: ' + el.dataset.val + ')' : '') + ' \u2014 change: ' + text);
45555
+ }
45556
+ closeBox();
45557
+ };
45558
+ ov.querySelector('#cancel').onclick = closeBox;
45559
+ ov.addEventListener('click', function(e){ if (e.target === ov) closeBox(); });
45560
+ }
45561
+ document.addEventListener('keydown', function(e){ if (e.key === 'Escape') closeBox(); });
45562
+ panel.addEventListener('contextmenu', function(e){
45563
+ var el = e.target.closest('.anno');
45564
+ if (!el) return;
45565
+ e.preventDefault(); e.stopPropagation();
45566
+ openBox(el);
45567
+ });
45568
+ </script>
45569
+ \`\`\`
45570
+
45571
+ - 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\`).
45572
+
45573
+ Keep it lightweight: annotate only meaningful elements, reuse one overlay, no libraries, and put \`<script>\` last so the draft streams in top-down.`;
45176
45574
  var MODULE_SECTIONS = {
45177
45575
  art: [SVG_SETUP, ART_AND_ILLUSTRATION],
45178
- mockup: [UI_COMPONENTS, COLOR_PALETTE],
45576
+ mockup: [UI_COMPONENTS, COLOR_PALETTE, ANNOTATABLE_MOCKUP],
45179
45577
  interactive: [UI_COMPONENTS, COLOR_PALETTE],
45180
45578
  chart: [UI_COMPONENTS, COLOR_PALETTE, CHARTS_CHART_JS],
45181
45579
  diagram: [COLOR_PALETTE, SVG_SETUP, DIAGRAM_TYPES]
@@ -45677,7 +46075,7 @@ function renderTaskBinding(taskId) {
45677
46075
 
45678
46076
  ## Bound Task
45679
46077
 
45680
- You are executing task \`${taskId}\`. Read its contract with \`manage_task get\`, update it as you work, and complete it when done.
46078
+ 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
46079
  `;
45682
46080
  }
45683
46081
  function createBoundTaskLifecycle() {
@@ -46196,7 +46594,7 @@ var manageTaskSchema = import_zod68.z.object({
46196
46594
  title: import_zod68.z.string().optional().describe("Task title (required for create)"),
46197
46595
  description: import_zod68.z.string().optional().describe("Task description in Markdown"),
46198
46596
  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"),
46597
+ 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
46598
  dueDate: import_zod68.z.string().optional().describe("Due date (ISO 8601 format)"),
46201
46599
  metadata: import_zod68.z.record(import_zod68.z.unknown()).optional().describe("Structured metadata (e.g. projectId, module)"),
46202
46600
  metadataFilter: import_zod68.z.record(
@@ -47787,8 +48185,8 @@ ${TASK_CONVERGENCE_GUIDANCE}
47787
48185
  - update: Update a task's content fields (title/description/priority/dueDate/metadata/parentId/sourceId/context/ownerId/result/failureReason/requireReview/dependencies/files)
47788
48186
  Status changes can also be done here, but use set_status for a simpler dedicated status update.
47789
48187
  - 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)
48188
+ 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.
48189
+ 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
48190
  To mark failed: set_status with status='failed', failureReason='...'
47793
48191
  To pause for input: set_status with status='interrupted', summary='what is needed'
47794
48192
  Valid status values: pending, in_progress, review, failed, interrupted, completed, cancelled.
@@ -47805,16 +48203,16 @@ Report existing keys directly; set 'isNew: true' to add a key the owner does not
47805
48203
  track yet. Belief is evidence metadata: it never blocks a completion, and an
47806
48204
  unmatched or malformed entry is recorded without changing the owner's table.
47807
48205
  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
48206
+ execution is dispatched. Set \`in_progress\` yourself ONLY for a task THIS run
48207
+ executes; a task you delegate stays \`pending\` and is claimed automatically by
48208
+ \`task\` dispatch. Use \`task { taskId, agentId }\` to delegate a subtask
47811
48209
  to an expert \u2014 never re-dispatch the task you are processing.`,
47812
48210
  schema: manageTaskSchema
47813
48211
  }
47814
48212
  ),
47815
48213
  (0, import_langchain83.tool)(handleStartTask, {
47816
48214
  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.`,
48215
+ 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
48216
  schema: startTaskSchema
47819
48217
  })
47820
48218
  ]
@@ -49512,7 +49910,7 @@ A plugin is the SET of classes it needs, e.g. \`connection + tools + skills\` or
49512
49910
  | Tool shows an interactive panel | \`sandbox-plugins-ui\` |
49513
49911
  | Tool orchestrates platform tools/agents | \`sandbox-plugins-consume-open\` |
49514
49912
  | Tool callable by external MCP clients | \`sandbox-plugins-expose\` |
49515
- | Run as a long-lived resident service | \`sandbox-plugins-deploy\` |
49913
+ | Run as a long-lived resident service (only on the human's explicit request) | \`sandbox-plugins-deploy\` |
49516
49914
  | Validate / test / debug | \`sandbox-plugins-testing\` |
49517
49915
 
49518
49916
  ## Agents (not a supported authoring path)
@@ -49554,8 +49952,8 @@ chart, table, viewer\u2026). It can also call the plugin's OWN tools back from t
49554
49952
  - It is NOT the tool's JSON \`schema\` (that is the input contract for the model).
49555
49953
  - It is NOT the tool \`description\`.
49556
49954
  - 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\`.
49955
+ - Saying "add a UI" means: add a \`ui\` field to the tool in \`plugin.json\` and write
49956
+ \`ui/<tool>.html\` (with \`write_file\`). Details in \`sandbox-plugins-ui\`.
49559
49957
 
49560
49958
  ## Decide first \u2014 clarify these with the user before writing files
49561
49959
  Ask for anything not already stated; do not guess silently:
@@ -49570,7 +49968,7 @@ Ask for anything not already stated; do not guess silently:
49570
49968
  multi-select? connection test needed? \u2192 read \`sandbox-plugins-connection\`.
49571
49969
  - **UI**: should the tool show an interactive panel instead of only text? what
49572
49970
  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
49971
+ \`sandbox-plugins-ui\` and add \`ui\` + \`ui/<tool>.html\` (step 6 below). If the
49574
49972
  user says no UI, skip it \u2014 text-only is fine.
49575
49973
  - **Skills**: procedure or knowledge? who consumes it? trigger phrase? tied to a
49576
49974
  specific tool?
@@ -49603,13 +50001,34 @@ Ask for anything not already stated; do not guess silently:
49603
50001
  \`\`\`
49604
50002
 
49605
50003
  ## 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\`
50004
+ 1. **Design first, and iterate \u2014 then get explicit approval before you build (non-trivial plugins).**
50005
+ Before writing any file, produce the design and review it with the human. It has two parts,
50006
+ with different treatments:
50007
+ - **Functional design** (always): an **interactive** component map of what you intend to
50008
+ build \u2014 the tools, their inputs/outputs, whether a connection is needed, whether a UI
50009
+ is needed, and how the agent calls them. Make it click-to-explore (e.g. click a tool to
50010
+ reveal its handler / IO / connection); it is a map to **confirm**, not to annotate.
50011
+ - **UI design** (only when the UI class applies): an **annotatable** mock of the tool's UI \u2014
50012
+ the human right-clicks elements to leave notes (see \`sandbox-plugins-ui\` \u2192
50013
+ "Reviewable drafts").
50014
+ Show both drafts with the \`widget\` middleware, then run the review loop:
50015
+ - Ask the human to review (say how to comment). On each round of comments, **restate the
50016
+ feedback** (element + requested change), then **regenerate the draft with the feedback and
50017
+ present it again** \u2014 comments are "revise and re-show", never "start building".
50018
+ - **Do NOT scaffold or write any file during the loop.** Implement only after the human
50019
+ **explicitly approves**, and ask for that go-ahead yourself before you start.
50020
+ Skip the drafts only for a trivial single-tool plugin whose shape is already agreed \u2014 but
50021
+ still confirm the goal.
50022
+ 2. Scaffold with \`manage_plugin { action: "create", type, name }\` \u2014 creates
50023
+ \`/root/.agents/plugins/<type>/\` with a stub \`plugin.json\`. It carries no content.
50024
+ 3. Read \`sandbox-plugins-manifest\` and write \`plugin.json\` with \`write_file\`.
50025
+ 4. Read \`sandbox-plugins-handlers\` and write each handler with \`write_file\`.
50026
+ 5. If the plugin needs credentials: read \`sandbox-plugins-connection\`.
50027
+ 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\`.
50028
+ 7. If a tool must be callable by external MCP clients: read \`sandbox-plugins-expose\`
49611
50029
  and add \`expose: true\` (plus \`readOnly\`/\`destructive\` accuracy hints).
49612
- 6. Read \`sandbox-plugins-testing\` and iterate using \`manage_plugin\`.
50030
+ 8. Read \`sandbox-plugins-testing\` and iterate: edit files with \`edit_file\`, then
50031
+ \`validate\` \u2192 \`reload\` \u2192 \`test\`.
49613
50032
 
49614
50033
  Templates: \`templates/plugin.json\`, \`templates/handler.js\`, \`templates/test.js\`, \`templates/discover.js\`, \`templates/ui.html\`.
49615
50034
  `,
@@ -49671,6 +50090,20 @@ description: Add an interactive HTML UI to a tenant plugin tool (rendered as a s
49671
50090
  A tool can declare an HTML file that Axiom renders as a sandboxed MCP App when
49672
50091
  the tool runs. The app can call the plugin's OWN tools back.
49673
50092
 
50093
+ ## Reviewable drafts (before you build the real UI)
50094
+ When you show a **UI mock with \`show_widget\`** for the user to react to (the design
50095
+ step, before writing \`ui/<tool>.html\`), make it **annotatable**: the user right-clicks
50096
+ any element to leave a comment that comes back to you as a message. Turn it on by
50097
+ default for review drafts and include a small visible hint (e.g. "\u53F3\u952E\u4EFB\u610F\u5143\u7D20\u6279\u6CE8");
50098
+ the \`widget\` mockup guidelines have the pattern. Omit it only when the draft is meant
50099
+ to be purely experienced (a live demo with no review) \u2014 then leave out the hint and
50100
+ the handlers. This is only for the **mock**: the real \`ui/<tool>.html\` is the plugin's
50101
+ product and is never annotatable.
50102
+ A round of annotations is **NOT approval**: restate the comments you received, regenerate
50103
+ the mock from them, and show it again, iterating until the human is satisfied. Build
50104
+ \`ui/<tool>.html\` only after the human **explicitly approves** \u2014 and ask for that go-ahead
50105
+ yourself.
50106
+
49674
50107
  ## Declare it
49675
50108
  \`\`\`
49676
50109
  /root/.agents/plugins/<type>/
@@ -49703,11 +50136,10 @@ the tool runs. The app can call the plugin's OWN tools back.
49703
50136
  - Omit \`csp\` for the restrictive default (\`default-src 'none'\`). WebAssembly is allowed by default.
49704
50137
 
49705
50138
  ## 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\`.
50139
+ Write the HTML to \`/root/.agents/plugins/<type>/ui/<tool>.html\` with \`write_file\`,
50140
+ then call \`manage_plugin { action: "reload", type }\` so the host serves the new HTML.
50141
+ The file must be declared by a tool's \`ui.resource\` in \`plugin.json\`, otherwise it is
50142
+ treated as missing (\`TOOL_UI\`).
49711
50143
 
49712
50144
  ## How it behaves at runtime
49713
50145
  - 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 +50235,7 @@ grants apply to the UI too). See \`sandbox-plugins-expose\`.
49803
50235
  ## Gotchas
49804
50236
  - \`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
50237
  - \`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.
50238
+ - 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
50239
 
49808
50240
  Start from \`templates/ui.html\`.
49809
50241
  `,
@@ -50067,21 +50499,23 @@ description: Validate, test, and reload a tenant sandbox plugin using the manage
50067
50499
 
50068
50500
  Use the \`manage_plugin\` tool. The loop:
50069
50501
 
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.
50502
+ 1. Write/edit the plugin files with the filesystem tools (\`write_file\`/\`edit_file\`) under
50503
+ \`/root/.agents/plugins/<type>/\`; \`create\` only scaffolds the directory + stub.
50504
+ 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
50505
  3. If the plugin has \`connection.fields\` and a tool reads credentials: a **human**
50073
50506
  must configure a connection of this plugin type. You cannot create credentials.
50074
50507
  Call \`list_connections { type }\` first \u2014 if none exist, use \`ask_user_to_clarify\`
50075
50508
  to state explicitly that only the human can configure the connector, ask them to
50076
50509
  add it (Console \u2192 Connections) and to give you the connection key. Do not claim
50077
50510
  the tool works until a real \`connectionKey\` test passes.
50078
- 4. \`action: "test"\` to exercise a handler without an LLM:
50511
+ 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).
50512
+ 5. \`action: "test"\` to exercise a handler without an LLM:
50079
50513
  - Tool: \`{ type, target: { kind: "tool", name, input, connectionKey? } }\`
50080
50514
  - Connection: \`{ type, target: { kind: "connection-test" } }\` or \`{ kind: "connection-discover" }\`
50081
50515
  Returns \`{ ok, result, exitCode, durationMs, stdout, stderr }\`.
50082
50516
  For credentialed tools, pass the real \`connectionKey\`. Without one, the handler
50083
50517
  receives an empty connection \u2014 that only checks plumbing, NOT functional success.
50084
- 5. \`action: "reload"\` if you edited files directly.
50518
+ 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
50519
 
50086
50520
  ## Secrets
50087
50521
  - \`list_connections\` returns identifiers only (\`key\`/\`name\`/\`description\`), never config or secrets.
@@ -50096,10 +50530,10 @@ Use the \`manage_plugin\` tool. The loop:
50096
50530
  | HANDLER_MISSING | handler path wrong or file not written |
50097
50531
  | TOOL_SCHEMA | unsupported JSON Schema keyword |
50098
50532
  | non-zero exit | handler threw; read stdout/stderr from \`test\` |
50099
- | old behaviour after edit | version/hash unchanged \u2014 bump \`version\` or call \`reload\` |
50533
+ | old behaviour after edit | the catalog caches by content hash/TTL \u2014 call \`reload\` |
50100
50534
  | empty connection in handler | no \`connectionKey\` passed, or no connection configured for this type |
50101
50535
 
50102
- Fall back to \`execute_code\`/\`shell\` to run \`node handlers/x.js\` manually with
50536
+ Fall back to \`shell_exec\` to run \`node handlers/x.js\` manually with
50103
50537
  \`PLUGIN_INPUT\` and a synthetic \`PLUGIN_CONNECTION_FILE\` to reproduce logic bugs.
50104
50538
  A synthetic fixture only verifies plumbing \u2014 a real upstream call requires a real
50105
50539
  \`connectionKey\` configured by a human.
@@ -50110,7 +50544,7 @@ A synthetic fixture only verifies plumbing \u2014 a real upstream call requires
50110
50544
  version: "1.0.0",
50111
50545
  content: `---
50112
50546
  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.
50547
+ 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
50548
  ---
50115
50549
 
50116
50550
  # Deploying a Plugin as a Resident Service
@@ -50120,10 +50554,13 @@ Optionally, you can **deploy** the plugin as a **resident service**: one
50120
50554
  long-lived sandbox per plugin that serves every call. This is faster for
50121
50555
  frequent or heavy calls, but costs a running sandbox.
50122
50556
 
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.
50557
+ ## Precondition: human request, then tests must pass first
50558
+ Deployment is **opt-in and human-gated**: NEVER deploy on your own after tests
50559
+ pass. Wait until the human explicitly asks for it or approves it. A passing test
50560
+ is necessary but NOT sufficient to deploy.
50561
+ Then confirm the plugin is validated AND its tests pass:
50562
+ 1. Files written with the filesystem tools and \`reload\` run.
50563
+ 2. \`manage_plugin validate\` \u2014 no diagnostics.
50127
50564
  3. \`manage_plugin test\` \u2014 every tool/connection test returns \`ok: true\`
50128
50565
  (a real \`connectionKey\` test for credentialed tools).
50129
50566
  Never deploy a plugin you have not tested: a resident service wraps the same
@@ -50191,6 +50628,30 @@ load "sandbox-plugins-expose" and follow it (\`expose: true\` + key grants).
50191
50628
  If a tool must call the platform's own tools/agents from its handler, load
50192
50629
  "sandbox-plugins-consume-open".
50193
50630
 
50631
+ Design first, and iterate on it before you build \u2014 the design is a conversation, not
50632
+ a one-shot. Use the \`widget\` middleware to present it (a decision surface, not a showcase):
50633
+ - **Functional design** (always): an **interactive** component map of the shape you
50634
+ intend to build \u2014 which tools, their inputs/outputs, whether a connection or a UI
50635
+ is needed, how the agent calls them. Make it click-to-explore (and prefer it over
50636
+ an SVG flowchart); it is a map to **confirm**, NOT an annotatable draft.
50637
+ - **UI design** (UI class only): an **annotatable** mock of the tool's UI \u2014 the human
50638
+ right-clicks elements to leave notes.
50639
+ Then run a review loop:
50640
+ - Ask the human to review (tell them how to comment). When they send comments, **restate the
50641
+ feedback** (which element, what change) so they can confirm you understood, then **build a
50642
+ NEW draft that incorporates the feedback and present it again**; keep iterating until
50643
+ they are satisfied. Comments mean "revise and re-show", NOT "start building".
50644
+ - Do NOT scaffold or write any file during the loop. Implement only once the human
50645
+ **explicitly approves** the design \u2014 and still ASK for that go-ahead yourself; never
50646
+ start building on your own initiative.
50647
+ Also surface genuinely different options (scope, layout, which tools) visually and let
50648
+ the human choose, instead of describing them in prose.
50649
+
50650
+ How to render a good widget is the widget middleware's own concern \u2014 follow it
50651
+ there; keep this prompt to plugin-design intent. These widgets are design aids
50652
+ only; the plugin's actual UI is still the MCP App HTML you author and test with
50653
+ \`manage_plugin\`.
50654
+
50194
50655
  Plan with persistent Tasks: MANDATORY until you load "task-tracking" with the
50195
50656
  \`skill\` tool, call neither \`manage_task\` nor any other task operation. Use
50196
50657
  persistent Tasks as the development planning surface and create one Task per
@@ -50198,7 +50659,16 @@ independently verifiable outcome (for example "manifest written", "handlers
50198
50659
  implemented", "UI added", "validated/tested via manage_plugin"). As you work,
50199
50660
  update each Task's status to reflect reality. Never use \`write_todos\`.
50200
50661
 
50201
- Author with the \`manage_plugin\` tool: validate, then create/update, then test.
50662
+ After the human confirms the design, author the plugin files directly with the filesystem tools, then verify with \`manage_plugin\`.
50663
+ 1. Scaffold once: \`manage_plugin { action: "create", type: "<type>", name }\` \u2014 it creates
50664
+ \`/root/.agents/plugins/<type>/\` with a stub \`plugin.json\`. It never carries content.
50665
+ 2. Write and edit every file yourself with \`write_file\` / \`edit_file\` (read back with
50666
+ \`read_file\`) under \`/root/.agents/plugins/<type>/\`: \`plugin.json\`, \`handlers/*.js\`, and
50667
+ (UI class) \`ui/*.html\`. Change one file at a time; edit in place instead of rewriting.
50668
+ 3. Verify in order: \`manage_plugin { action: "validate", type }\` (reads the files back from
50669
+ the sandbox) \u2192 \`manage_plugin { action: "reload", type }\` (the catalog caches, so a
50670
+ freshly written file is invisible to \`test\` until you reload) \u2192 \`manage_plugin
50671
+ { action: "test", type, target }\`.
50202
50672
  If a tool needs credentials, a human must configure the connection \u2014 use
50203
50673
  \`list_connections\` to check, and \`ask_user_to_clarify\` to ask the human to add
50204
50674
  the connector and give you its key. Only after a real \`connectionKey\` test
@@ -50207,14 +50677,15 @@ unverified (missing credentials), not as passing. Use \`shell_exec\` (the tool
50207
50677
  provided by the \`code_eval\` middleware) to run \`node handlers/x.js\` for manual
50208
50678
  checks when reproducing a failure.
50209
50679
 
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\`
50680
+ Deployment is human-gated: do NOT deploy on your own after the tests pass.
50681
+ Only deploy when the human explicitly asks for it or approves it, then load
50682
+ "sandbox-plugins-deploy" and follow it. Choose \`onDemand\`
50212
50683
  (default) or \`alwaysOn\` for hot paths, call \`manage_plugin deploy\` with the
50213
50684
  chosen \`mode\`, then confirm with \`manage_plugin status\` (\`running\`). Only claim
50214
50685
  the plugin is deployed and serving once \`status\` reports \`running\`; if the
50215
50686
  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.`;
50687
+ unavailable \u2014 do NOT claim success. Report the plugin as ready and let the human
50688
+ decide; \`undeploy\` removes the service without touching the plugin files.`;
50218
50689
 
50219
50690
  // src/middlewares/sandboxPluginsUi.ts
50220
50691
  var SANDBOX_PLUGINS_UI_RESOURCE = "ui://sandbox-plugins/app";
@@ -50390,6 +50861,7 @@ var SANDBOX_PLUGINS_APP_HTML = `<!doctype html>
50390
50861
  `;
50391
50862
 
50392
50863
  // src/middlewares/sandboxPluginsMiddleware.ts
50864
+ var PLUGINS_ROOT3 = "/root/.agents/plugins";
50393
50865
  var SANDBOX_PLUGINS_UI_REF = {
50394
50866
  kind: "plugin",
50395
50867
  pluginType: "sandbox-plugins",
@@ -50446,18 +50918,19 @@ var managePluginSchema = import_zod71.z.object({
50446
50918
  "get",
50447
50919
  "validate",
50448
50920
  "create",
50449
- "update",
50450
50921
  "delete",
50451
50922
  "reload",
50452
50923
  "test",
50924
+ "backup",
50925
+ "restore",
50453
50926
  "deploy",
50454
50927
  "undeploy",
50455
50928
  "status",
50456
50929
  "deployments"
50457
50930
  ]),
50458
50931
  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(),
50932
+ name: import_zod71.z.string().optional(),
50933
+ description: import_zod71.z.string().optional(),
50461
50934
  mode: import_zod71.z.enum(["onDemand", "alwaysOn"]).optional(),
50462
50935
  target: import_zod71.z.object({
50463
50936
  kind: import_zod71.z.enum(["tool", "connection-test", "connection-discover"]),
@@ -50471,12 +50944,12 @@ var sandboxPluginsPlugin = {
50471
50944
  type: "sandbox-plugins",
50472
50945
  category: "execution",
50473
50946
  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.",
50947
+ 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
50948
  recommends: ["skill"],
50476
50949
  tools: [
50477
50950
  {
50478
50951
  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.",
50952
+ 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
50953
  ui: { resource: SANDBOX_PLUGINS_UI_RESOURCE, displayMode: "inline" }
50481
50954
  }
50482
50955
  ],
@@ -50503,7 +50976,7 @@ var sandboxPluginsPlugin = {
50503
50976
  id: "sandbox-plugins",
50504
50977
  type: "sandbox-plugins",
50505
50978
  name: "Sandbox Plugins",
50506
- description: "Author and manage tenant plugins (list/validate/create/update/delete/reload/test/deploy/undeploy/status)",
50979
+ description: "Author and manage tenant plugins (list/validate/create/delete/reload/test/backup/restore/deploy/undeploy/status)",
50507
50980
  enabled: true,
50508
50981
  config: {}
50509
50982
  },
@@ -50523,6 +50996,14 @@ var sandboxPluginsPlugin = {
50523
50996
  enabled: true,
50524
50997
  config: {}
50525
50998
  },
50999
+ {
51000
+ id: "widget",
51001
+ type: "widget",
51002
+ name: "Widget",
51003
+ 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.",
51004
+ enabled: true,
51005
+ config: {}
51006
+ },
50526
51007
  {
50527
51008
  id: "ask_user_to_clarify",
50528
51009
  type: "ask_user_to_clarify",
@@ -50563,6 +51044,7 @@ var sandboxPluginsPlugin = {
50563
51044
  return JSON.stringify({
50564
51045
  plugins: loaded.plugins.map((p) => ({
50565
51046
  type: p.meta.type,
51047
+ path: `${PLUGINS_ROOT3}/${p.meta.type}`,
50566
51048
  name: p.meta.name,
50567
51049
  tools: p.meta.tools?.map((t) => t.name) ?? [],
50568
51050
  source: p.meta.source
@@ -50573,32 +51055,87 @@ var sandboxPluginsPlugin = {
50573
51055
  }
50574
51056
  case "get": {
50575
51057
  if (!input.type) return "type is required";
50576
- return await catalog2.readPluginFile(tenantId2, input.type, "plugin.json");
51058
+ const content = await catalog2.readPluginFile(tenantId2, input.type, "plugin.json");
51059
+ let manifest = content;
51060
+ try {
51061
+ manifest = JSON.parse(content);
51062
+ } catch {
51063
+ }
51064
+ return JSON.stringify({
51065
+ type: input.type,
51066
+ path: `${PLUGINS_ROOT3}/${input.type}`,
51067
+ manifest
51068
+ });
50577
51069
  }
50578
51070
  case "reload": {
50579
51071
  const loaded = await catalog2.reload(tenantId2);
50580
51072
  void publishPluginCatalogChanged(tenantId2);
50581
51073
  return JSON.stringify({ reloaded: true, plugins: loaded.plugins.length, diagnostics: loaded.diagnostics });
50582
51074
  }
50583
- case "create":
50584
- case "update": {
50585
- if (!input.type || !input.manifest) return "type and manifest are required";
50586
- const handlers = input.handlers ?? {};
51075
+ case "backup": {
51076
+ if (!input.type) return "type is required";
51077
+ try {
51078
+ const result = await catalog2.backupPlugin(tenantId2, input.type);
51079
+ if (result.files === 0) {
51080
+ return JSON.stringify({ backedUp: false, error: "nothing to back up (plugin has no files)" });
51081
+ }
51082
+ return JSON.stringify({ backedUp: true, files: result.files });
51083
+ } catch (err) {
51084
+ return JSON.stringify({ backedUp: false, error: errorText(err) });
51085
+ }
51086
+ }
51087
+ case "restore": {
51088
+ if (!input.type) return "type is required";
51089
+ let restored;
51090
+ let failure3;
51091
+ try {
51092
+ restored = await catalog2.restorePlugin(tenantId2, input.type);
51093
+ } catch (err) {
51094
+ failure3 = errorText(err);
51095
+ }
51096
+ try {
51097
+ await catalog2.reload(tenantId2);
51098
+ void publishPluginCatalogChanged(tenantId2);
51099
+ } catch {
51100
+ }
51101
+ if (failure3) return JSON.stringify({ restored: false, error: failure3 });
51102
+ return JSON.stringify({ restored: true, files: restored?.files ?? 0 });
51103
+ }
51104
+ case "create": {
51105
+ if (!input.type) return "type is required";
50587
51106
  const known = new Set(
50588
51107
  (catalog2.getCached(tenantId2)?.plugins ?? []).map((p) => p.meta.type).filter((t) => t !== input.type)
50589
51108
  );
51109
+ const stub = {
51110
+ schemaVersion: 1,
51111
+ type: input.type,
51112
+ name: input.name ?? input.type,
51113
+ description: input.description ?? "",
51114
+ version: "0.1.0",
51115
+ tools: []
51116
+ };
50590
51117
  const check = validateSandboxPluginManifest({
50591
- raw: input.manifest,
51118
+ raw: stub,
50592
51119
  dirName: input.type,
50593
51120
  reservedTypes: new Set(PluginRegistry.list()),
50594
51121
  knownTypes: known,
50595
- handlerExists: (rel) => rel in handlers
51122
+ handlerExists: () => true
50596
51123
  });
50597
- if (!check.manifest) return JSON.stringify({ written: false, diagnostics: check.diagnostics });
50598
- await catalog2.writePlugin(tenantId2, input.type, input.manifest, handlers);
51124
+ if (!check.manifest) {
51125
+ return JSON.stringify({ created: false, diagnostics: check.diagnostics });
51126
+ }
51127
+ const files = await catalog2.listPluginFiles(tenantId2, input.type);
51128
+ if (files.includes("plugin.json")) {
51129
+ return JSON.stringify({ created: false, error: "plugin already exists" });
51130
+ }
51131
+ await catalog2.writePlugin(tenantId2, input.type, check.manifest, {});
50599
51132
  const loaded = await catalog2.reload(tenantId2);
50600
51133
  void publishPluginCatalogChanged(tenantId2);
50601
- return JSON.stringify({ written: true, diagnostics: loaded.diagnostics, hash: loaded.hash });
51134
+ return JSON.stringify({
51135
+ created: true,
51136
+ path: `${PLUGINS_ROOT3}/${input.type}`,
51137
+ diagnostics: loaded.diagnostics
51138
+ });
50602
51139
  }
50603
51140
  case "delete": {
50604
51141
  if (!input.type) return "type is required";
@@ -50609,18 +51146,8 @@ var sandboxPluginsPlugin = {
50609
51146
  }
50610
51147
  case "validate": {
50611
51148
  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) });
51149
+ const result = await catalog2.validatePluginFromDisk(tenantId2, input.type);
51150
+ return JSON.stringify({ diagnostics: result.diagnostics, valid: result.valid });
50624
51151
  }
50625
51152
  case "test": {
50626
51153
  if (!input.type || !input.target) return "type and target are required";
@@ -50694,7 +51221,16 @@ var sandboxPluginsPlugin = {
50694
51221
  assistantId: typeof runCfg.assistant_id === "string" ? runCfg.assistant_id : void 0
50695
51222
  }
50696
51223
  });
50697
- return handlerResultText(redactSecretValues(result, secrets));
51224
+ const text = handlerResultText(redactSecretValues(result, secrets));
51225
+ const ui = toolDef.ui;
51226
+ if (!ui?.resource) return text;
51227
+ const uiRef = {
51228
+ kind: "plugin",
51229
+ pluginType: input.type,
51230
+ resource: ui.resource,
51231
+ displayMode: ui.displayMode ?? "inline"
51232
+ };
51233
+ return appendUiFence(text, uiRef);
50698
51234
  } catch (err) {
50699
51235
  return JSON.stringify(redactSecretValues({ ok: false, error: errorText(err) }, secrets));
50700
51236
  }
@@ -50766,7 +51302,7 @@ var sandboxPluginsPlugin = {
50766
51302
  },
50767
51303
  {
50768
51304
  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.",
51305
+ 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
51306
  schema: managePluginSchema
50771
51307
  }
50772
51308
  )
@@ -51950,6 +52486,7 @@ registerBuiltinPlugins();
51950
52486
  buildTableName,
51951
52487
  buildTaskMutationFallback,
51952
52488
  buildTaskNormalizationFallback,
52489
+ canConvergeOrphan,
51953
52490
  checkEmptyContent,
51954
52491
  clearEncryptionKeyCache,
51955
52492
  clearEvalRunService,
@@ -51999,6 +52536,7 @@ registerBuiltinPlugins();
51999
52536
  createTaskLifecycleService,
52000
52537
  createTaskMiddleware,
52001
52538
  createTaskRunBridge,
52539
+ createTaskRunLiveness,
52002
52540
  createTeamMiddleware,
52003
52541
  createTeammateTools,
52004
52542
  createTrustedAgentMessageDispatcher,
@@ -52096,6 +52634,7 @@ registerBuiltinPlugins();
52096
52634
  isMcpConnectionError,
52097
52635
  isPluginSkillAuthorizedInSandbox,
52098
52636
  isPluginSkillManagedInSandbox,
52637
+ isTaskRunLeaseHeld,
52099
52638
  isUsingDefaultKey,
52100
52639
  isValidCronExpression,
52101
52640
  isValidSandboxName,