@nexus-aethra/dsh-box-sandbox 0.1.9 → 0.2.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -109,7 +109,19 @@ async function waitForTask(id, options = {}) {
109
109
  * it is an agent that will not.
110
110
  */
111
111
  async function runTask(method, params, options = {}) {
112
- const { finished, task } = await waitForTask((await getRpc().call(method, params)).id, options);
112
+ return followTask(await getRpc().call(method, params), options);
113
+ }
114
+ /**
115
+ * Follow an already-enqueued task record to its end, with runTask's rules.
116
+ *
117
+ * Exists for the methods that only sometimes enqueue: the daemon answers them
118
+ * in line when no task was needed and with a task record when one was, and the
119
+ * caller cannot know which until the reply is in hand. A record without an id
120
+ * is not a task and is handed back untouched — deciding that shape is the
121
+ * caller's, since only it knows what the synchronous answer looks like.
122
+ */
123
+ async function followTask(enqueued, options = {}) {
124
+ const { finished, task } = await waitForTask(enqueued.id, options);
113
125
  if (!finished) throw new Error(`the ${task.kind} task did not finish within the budget; it is still ${task.status} at ${task.progress}%. Watch it with box_task, or read ${task.logPath ?? "its log"}.`);
114
126
  if (failed(task)) throw new Error(`the ${task.kind} task ended ${task.status}` + (task.error ? `: ${task.error}` : "") + (task.rollbackError ? ` (rollback also failed: ${task.rollbackError})` : "") + (task.logPath ? `. Full detail in ${task.logPath}` : "."));
115
127
  return task;
@@ -264,21 +276,24 @@ function applyBoxTools(ctx) {
264
276
  path: { type: "string" },
265
277
  width: { type: "number" },
266
278
  height: { type: "number" },
267
- bytes: { type: "number" }
279
+ bytes: { type: "number" },
280
+ url: { type: "string" },
281
+ title: nullable
268
282
  },
269
283
  required: [
270
284
  "containerId",
271
285
  "path",
272
286
  "width",
273
287
  "height",
274
- "bytes"
288
+ "bytes",
289
+ "url"
275
290
  ]
276
291
  },
277
292
  render: (_args, value) => {
278
293
  const record = asRecord(value);
279
294
  return [{
280
295
  type: "text",
281
- text: "Screenshot of container " + String(record.containerId) + " (" + String(record.width) + "x" + String(record.height) + ", " + String(record.bytes) + " bytes) saved to " + String(record.path) + "."
296
+ text: "Screenshot of container " + String(record.containerId) + " (" + String(record.width) + "x" + String(record.height) + ", " + String(record.bytes) + " bytes) saved to " + String(record.path) + ". Page: " + String(record.url) + (record.title ? " — " + String(record.title) : "") + "."
282
297
  }];
283
298
  }
284
299
  },
@@ -292,7 +307,9 @@ function applyBoxTools(ctx) {
292
307
  path,
293
308
  width: result.width,
294
309
  height: result.height,
295
- bytes: result.bytes
310
+ bytes: result.bytes,
311
+ url: result.url,
312
+ title: result.title
296
313
  };
297
314
  }
298
315
  });
@@ -440,6 +457,108 @@ function applyBoxTools(ctx) {
440
457
  };
441
458
  }
442
459
  });
460
+ registerTool(ctx, {
461
+ name: "box_hover",
462
+ description: "Park the pointer on a control and report what the hover revealed. Use it when a control exists only while hovered — row actions in this UI are display:none until :hover, so box_page_text does not list them and box_click_element cannot find them: hover the row first, then list or click. Target it the same way as box_click_element (role and name, or x,y when there is no name). The pointer stays where this leaves it until the next move or click, and the reply names what ended up under it, so a hover that landed on something else is visible immediately.",
463
+ parameters: {
464
+ type: "object",
465
+ properties: {
466
+ containerId: {
467
+ type: "string",
468
+ description: "Container whose page to hover."
469
+ },
470
+ name: {
471
+ type: "string",
472
+ description: "Accessible name of the control to hover, matched exactly, e.g. the row name box_page_text reported."
473
+ },
474
+ role: {
475
+ type: "string",
476
+ description: "Role to require alongside the name, e.g. row or button."
477
+ },
478
+ nth: {
479
+ type: "number",
480
+ description: "Zero-based index among controls sharing the name. Omit for the first."
481
+ },
482
+ within: {
483
+ type: "string",
484
+ description: "Container name to scope the search to, when the same name appears in more than one place."
485
+ },
486
+ x: {
487
+ type: "number",
488
+ description: "Horizontal viewport coordinate, from the left edge. Use with y instead of name."
489
+ },
490
+ y: {
491
+ type: "number",
492
+ description: "Vertical viewport coordinate, from the top edge. Use with x instead of name."
493
+ }
494
+ },
495
+ required: ["containerId"]
496
+ },
497
+ timeoutMs: WARM_BUDGET_MS,
498
+ output: {
499
+ schema: {
500
+ type: "object",
501
+ additionalProperties: false,
502
+ properties: {
503
+ containerId: { type: "string" },
504
+ url: { type: "string" },
505
+ x: { type: "number" },
506
+ y: { type: "number" },
507
+ landed: { type: "boolean" },
508
+ hitTag: nullable,
509
+ hitText: nullable,
510
+ matched: nullable,
511
+ occludedBy: nullable
512
+ },
513
+ required: [
514
+ "containerId",
515
+ "url",
516
+ "x",
517
+ "y",
518
+ "landed"
519
+ ]
520
+ },
521
+ render: (_args, value) => {
522
+ const record = asRecord(value);
523
+ const at = "at (" + String(record.x) + ", " + String(record.y) + ")";
524
+ if (record.landed === false) {
525
+ const cover = record.occludedBy ? " it was covered by <" + String(record.occludedBy) + ">" + (record.hitText ? " " + JSON.stringify(record.hitText) : "") : " nothing is under that point";
526
+ return [{
527
+ type: "text",
528
+ text: "The hover " + at + " in container " + String(record.containerId) + " did NOT land:" + cover + "." + nextHint("hover the row or control by name, then call box_page_text to list what appeared")
529
+ }];
530
+ }
531
+ const under = record.hitTag ? " over <" + String(record.hitTag) + ">" : "";
532
+ return [{
533
+ type: "text",
534
+ text: "Pointer parked " + at + " in container " + String(record.containerId) + under + "." + (record.matched ? " Target: " + JSON.stringify(record.matched) + "." : "") + nextHint("call box_page_text again to list what the hover revealed, or box_click_element to act on it")
535
+ }];
536
+ }
537
+ },
538
+ async execute(args) {
539
+ const containerId = containerIdOf(args);
540
+ const record = asRecord(args);
541
+ const hasName = typeof record.name === "string" && record.name.trim() !== "";
542
+ const hasPoint = typeof record.x === "number" && typeof record.y === "number";
543
+ if (!hasName && !hasPoint) throw new Error("give a name (with optional role, nth, within) or both x and y");
544
+ if (hasName) return {
545
+ containerId,
546
+ ...await withSession(containerId, "debug_hover", {
547
+ name: record.name,
548
+ role: typeof record.role === "string" ? record.role : void 0,
549
+ nth: typeof record.nth === "number" ? record.nth : void 0,
550
+ within: typeof record.within === "string" ? record.within : void 0
551
+ })
552
+ };
553
+ return {
554
+ containerId,
555
+ ...await withSession(containerId, "debug_hover", {
556
+ x: record.x,
557
+ y: record.y
558
+ })
559
+ };
560
+ }
561
+ });
443
562
  registerTool(ctx, {
444
563
  name: "box_page_text",
445
564
  description: "CALL THIS FIRST when you need to know what is on a page. It renders the whole page as text: every control and region with its role, name, on-screen position, and whether it is currently visible, and whether it is disabled. It needs no selector, and it is the only listing: there is no second way to ask what is on a page. Then hand the role+name it returns straight back to box_click_element. Anything marked BELOW FOLD needs box_scroll before you can act on it.",
@@ -456,7 +575,7 @@ function applyBoxTools(ctx) {
456
575
  },
457
576
  within: {
458
577
  type: "string",
459
- description: "NaNname is ambiguous: a settings dialog and the chat screen behind it canboth offer the same one, and acting on the wrong element still looks likeit worked."
578
+ description: "Name of a container to scope the listing to, such as a dialog. Use it when a name is ambiguous: a settings dialog and the chat screen behind it can both offer the same one, and acting on the wrong element still looks like it worked."
460
579
  },
461
580
  limit: {
462
581
  type: "number",
@@ -475,6 +594,8 @@ function applyBoxTools(ctx) {
475
594
  count: { type: "number" },
476
595
  truncated: { type: "boolean" },
477
596
  belowFold: { type: "number" },
597
+ url: { type: "string" },
598
+ title: nullable,
478
599
  elements: {
479
600
  type: "array",
480
601
  items: { type: "object" }
@@ -485,6 +606,7 @@ function applyBoxTools(ctx) {
485
606
  "count",
486
607
  "truncated",
487
608
  "belowFold",
609
+ "url",
488
610
  "elements"
489
611
  ]
490
612
  },
@@ -493,7 +615,7 @@ function applyBoxTools(ctx) {
493
615
  const elements = Array.isArray(record.elements) ? record.elements : [];
494
616
  const below = Number(record.belowFold ?? 0);
495
617
  const lines = [];
496
- lines.push("Page of container " + String(record.containerId) + ": " + String(record.count) + " entr" + (record.count === 1 ? "y" : "ies") + (record.truncated === true ? " (truncated at the limit)." : "."));
618
+ lines.push("Page of container " + String(record.containerId) + " at " + String(record.url) + (record.title ? " — " + String(record.title) : "") + ": " + String(record.count) + " entr" + (record.count === 1 ? "y" : "ies") + (record.truncated === true ? " (truncated at the limit)." : "."));
497
619
  if (below > 0) lines.push(below + " entr" + (below === 1 ? "y is" : "ies are") + " below the fold; call box_scroll to reach them.");
498
620
  lines.push(renderPageElements(elements));
499
621
  lines.push(nextHint("act on an entry with box_click_element (role+name) or box_click_at (x,y)"));
@@ -761,6 +883,111 @@ function applyBoxTools(ctx) {
761
883
  };
762
884
  }
763
885
  });
886
+ registerTool(ctx, {
887
+ name: "box_navigate",
888
+ description: "Point the debugged page at a URL, or bring it back to the container app. With no url it navigates to the container's own live address — this is the recovery after a container restart: the host binds a fresh port on every start, so a page opened before the restart can never answer again, and every page tool against it fails or hangs. The reply reports where the page actually ended up (the app may redirect), not where it was asked to go.",
889
+ parameters: {
890
+ type: "object",
891
+ properties: {
892
+ containerId: {
893
+ type: "string",
894
+ description: "Container whose page to move."
895
+ },
896
+ url: {
897
+ type: "string",
898
+ description: "Absolute URL to open. Omit to go back to the container app itself."
899
+ }
900
+ },
901
+ required: ["containerId"]
902
+ },
903
+ timeoutMs: WARM_BUDGET_MS,
904
+ output: {
905
+ schema: {
906
+ type: "object",
907
+ additionalProperties: false,
908
+ properties: {
909
+ containerId: { type: "string" },
910
+ url: { type: "string" },
911
+ title: nullable,
912
+ navigatedTo: { type: "string" }
913
+ },
914
+ required: [
915
+ "containerId",
916
+ "url",
917
+ "navigatedTo"
918
+ ]
919
+ },
920
+ render: (_args, value) => {
921
+ const record = asRecord(value);
922
+ return [{
923
+ type: "text",
924
+ text: "Page of container " + String(record.containerId) + " now at " + String(record.url) + (record.title ? " — " + String(record.title) : "") + " (asked for " + String(record.navigatedTo) + ")." + nextHint("call box_page_text to see what the page shows now")
925
+ }];
926
+ }
927
+ },
928
+ async execute(args) {
929
+ const containerId = containerIdOf(args);
930
+ const record = asRecord(args);
931
+ const result = await withSession(containerId, "debug_navigate", { url: typeof record.url === "string" && record.url.trim() !== "" ? record.url.trim() : void 0 });
932
+ return {
933
+ containerId,
934
+ url: result.url,
935
+ title: result.title,
936
+ navigatedTo: result.navigatedTo
937
+ };
938
+ }
939
+ });
940
+ registerTool(ctx, {
941
+ name: "box_evaluate",
942
+ description: "Run one JavaScript expression in the debugged page and return its value. Use it for facts the other tools do not answer for: an attribute, a computed style, whether a selector matches, a value React keeps out of the accessibility tree. Non-string values come back JSON-encoded, so objects and arrays stay readable. The expression runs inside the page with the page's own scope — it can do anything the page's own scripts could, and no more.",
943
+ parameters: {
944
+ type: "object",
945
+ properties: {
946
+ containerId: {
947
+ type: "string",
948
+ description: "Container whose page to evaluate in."
949
+ },
950
+ expression: {
951
+ type: "string",
952
+ description: "JavaScript to evaluate, e.g. document.title or Boolean(document.querySelector(\".row-actions\"))."
953
+ }
954
+ },
955
+ required: ["containerId", "expression"]
956
+ },
957
+ timeoutMs: WARM_BUDGET_MS,
958
+ output: {
959
+ schema: {
960
+ type: "object",
961
+ additionalProperties: false,
962
+ properties: {
963
+ containerId: { type: "string" },
964
+ url: { type: "string" },
965
+ result: { type: "string" }
966
+ },
967
+ required: [
968
+ "containerId",
969
+ "url",
970
+ "result"
971
+ ]
972
+ },
973
+ render: (_args, value) => {
974
+ const record = asRecord(value);
975
+ return [{
976
+ type: "text",
977
+ text: "Evaluated in container " + String(record.containerId) + " at " + String(record.url) + ":\n" + String(record.result)
978
+ }];
979
+ }
980
+ },
981
+ async execute(args) {
982
+ const containerId = containerIdOf(args);
983
+ const record = asRecord(args);
984
+ if (typeof record.expression !== "string" || record.expression.trim() === "") throw new Error("expression is required and must be non-empty");
985
+ return {
986
+ containerId,
987
+ ...await withSession(containerId, "debug_evaluate", { expression: record.expression })
988
+ };
989
+ }
990
+ });
764
991
  registerTool(ctx, {
765
992
  name: "box_set_browser",
766
993
  description: "Choose which browser the other box_* tools drive, and report the one in use. Call it when a page tool fails to launch a browser, or when auto-detection picks a browser you would rather not use. Pass an absolute path to a Chrome or Edge executable to pin it; pass an empty string to go back to auto-detection. The path is validated before it is saved, so a typo is rejected here rather than surfacing later as a failed launch.",
@@ -987,14 +1214,29 @@ function registerManageTools(ctx) {
987
1214
  plugins: {
988
1215
  type: "array",
989
1216
  items: { type: "object" }
1217
+ },
1218
+ self: {
1219
+ type: "object",
1220
+ additionalProperties: false,
1221
+ properties: {
1222
+ role: {
1223
+ type: "string",
1224
+ enum: ["container", "external"]
1225
+ },
1226
+ containerId: { oneOf: [{ type: "string" }, { type: "null" }] }
1227
+ },
1228
+ required: ["role", "containerId"]
990
1229
  }
991
1230
  }, [
992
1231
  "box",
993
1232
  "containers",
994
1233
  "templates",
995
- "plugins"
1234
+ "plugins",
1235
+ "self"
996
1236
  ], (value) => {
997
1237
  const lines = [];
1238
+ const self = value.self ?? {};
1239
+ lines.push(`Self: ${self.role === "container" ? `inside container ${String(self.containerId)} — lifecycle verbs on it are refused` : "external (not inside a Box container)"}.`);
998
1240
  const box = value.box ?? {};
999
1241
  const containers = value.containers ?? [];
1000
1242
  const templates = value.templates ?? [];
@@ -1013,7 +1255,8 @@ function registerManageTools(ctx) {
1013
1255
  box: {},
1014
1256
  containers: [],
1015
1257
  templates: [],
1016
- plugins: []
1258
+ plugins: [],
1259
+ self: selfIdentity()
1017
1260
  };
1018
1261
  if (want.includes("box")) result.box = asObject(await rpc.call("get_info"));
1019
1262
  if (want.includes("containers")) result.containers = asArray(await rpc.call("list_containers"));
@@ -1036,6 +1279,8 @@ function registerManageTools(ctx) {
1036
1279
  "inject",
1037
1280
  "rm",
1038
1281
  "rm-view",
1282
+ "view-on",
1283
+ "view-off",
1039
1284
  "prune",
1040
1285
  "types",
1041
1286
  "views"
@@ -1066,7 +1311,7 @@ function registerManageTools(ctx) {
1066
1311
  },
1067
1312
  viewId: {
1068
1313
  type: "string",
1069
- description: "Which resource view to remove."
1314
+ description: "Which resource view to act on: the type a Resources tab stands for. Required by rm-view, view-on and view-off."
1070
1315
  },
1071
1316
  name: {
1072
1317
  type: "string",
@@ -1184,8 +1429,20 @@ function registerManageTools(ctx) {
1184
1429
  case "rm-view": return {
1185
1430
  action,
1186
1431
  containerId,
1187
- result: asObject(await rpc.call("delete_resource_view", { id: required(record, "viewId") }))
1432
+ result: asObject(await rpc.call("delete_resource_view", { viewId: required(record, "viewId") }))
1188
1433
  };
1434
+ case "view-on":
1435
+ case "view-off": {
1436
+ const enabled = action === "view-on";
1437
+ return {
1438
+ action,
1439
+ containerId,
1440
+ result: asObject(await rpc.call("set_resource_view_enabled", {
1441
+ viewId: required(record, "viewId"),
1442
+ enabled
1443
+ }))
1444
+ };
1445
+ }
1189
1446
  case "prune": return {
1190
1447
  action,
1191
1448
  containerId,
@@ -1220,6 +1477,8 @@ function registerManageTools(ctx) {
1220
1477
  "inject",
1221
1478
  "rm",
1222
1479
  "rm-view",
1480
+ "view-on",
1481
+ "view-off",
1223
1482
  "prune",
1224
1483
  "types",
1225
1484
  "views"
@@ -1363,11 +1622,12 @@ function registerManageTools(ctx) {
1363
1622
  if (title !== void 0) params.title = title;
1364
1623
  if (flag(record, "restart")) params.restart = true;
1365
1624
  const added = await rpc.call("add_container_workspace", params);
1625
+ if (added !== null && typeof added === "object" && typeof added.id === "string" && typeof added.status === "string") await followTask(added, { timeoutMs: 84e4 });
1366
1626
  return {
1367
1627
  action,
1368
1628
  containerId,
1369
1629
  workspaces: (await rpc.call("list_container_workspaces", { id: containerId })).workspaces ?? [],
1370
- created: added?.created === true
1630
+ created: added !== null && !("status" in added) && added.created === true
1371
1631
  };
1372
1632
  }
1373
1633
  badAction("workspace", action, ["list", "add"]);
@@ -2178,28 +2438,48 @@ function asObject(value) {
2178
2438
  return { value };
2179
2439
  }
2180
2440
  /**
2181
- * True when this plugin is running inside the container it was asked about.
2182
- *
2183
- * Nothing hands a plugin its own container id -- the host process is told which
2184
- * container it is, but not the plugins it mounts -- so it is read back out of
2185
- * where the plugin is installed, which is always
2441
+ * The daemon stamps `DSHBOX_CONTAINER_ID` onto the host process it spawns, so
2442
+ * a mounted plugin can name its own container without guessing. The stamp is
2443
+ * the authority; the install-path parse below is the fallback for hosts
2444
+ * started by older daemons, before the stamp existed — there, nothing hands a
2445
+ * plugin its own container id, so it is read back out of where the plugin is
2446
+ * installed, which is always
2186
2447
  * `<instance>/profile/profiles/<p>/node_modules/...`: the container id is a
2187
2448
  * directory name on the way to it.
2188
2449
  *
2189
- * Comparing path segments to the id that was passed in, rather than testing the
2190
- * id's shape, is the point. An earlier version matched a UUID 8-4-4-4-12 and
2191
- * silently never matched anything: real ids are `container-<10 digits>-<uuid>`,
2192
- * with a unix timestamp in front. A guard that quietly does not fire is worse
2193
- * than no guard, because the code reads as though the case is handled.
2450
+ * The fallback matches the segment's prefix, not the id's full shape. An
2451
+ * earlier version matched a UUID 8-4-4-4-12 and silently never matched
2452
+ * anything: ids are `container-<unix timestamp>...`, and a guard that quietly
2453
+ * does not fire is worse than no guard, because the code reads as though the
2454
+ * case is handled.
2194
2455
  */
2195
- function runsInside(containerId) {
2456
+ function selfIdentity() {
2457
+ const stamped = process.env.DSHBOX_CONTAINER_ID?.trim();
2458
+ if (stamped) return {
2459
+ role: "container",
2460
+ containerId: stamped
2461
+ };
2196
2462
  let here;
2197
2463
  try {
2198
2464
  here = dirname(fileURLToPath(import.meta.url));
2199
2465
  } catch {
2200
- return false;
2466
+ return {
2467
+ role: "external",
2468
+ containerId: null
2469
+ };
2201
2470
  }
2202
- return here.split(/[\\/]/).includes(containerId);
2471
+ const own = here.split(/[\\/]/).find((part) => /^container-\d/.test(part));
2472
+ return own ? {
2473
+ role: "container",
2474
+ containerId: own
2475
+ } : {
2476
+ role: "external",
2477
+ containerId: null
2478
+ };
2479
+ }
2480
+ /** True when this plugin is running inside the container it was asked about. */
2481
+ function runsInside(containerId) {
2482
+ return selfIdentity().containerId === containerId;
2203
2483
  }
2204
2484
  /** Read an optional finite number, treating blank and NaN as absent. */
2205
2485
  function number(args, field) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexus-aethra/dsh-box-sandbox",
3
- "version": "0.1.9",
3
+ "version": "0.2.1-alpha.1",
4
4
  "description": "Host-side bridge from a DSH session to the DSH Box daemon: typed box_* tools an agent can call without a shell.",
5
5
  "keywords": [
6
6
  "dsh",
@@ -37,6 +37,7 @@
37
37
  },
38
38
  "files": [
39
39
  "dist/",
40
+ "skills/",
40
41
  "cordis.patch.yml",
41
42
  "README.md",
42
43
  "LICENSE"
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: dsh-box-tools
3
+ description: Use when you need to inspect, start, or change DSH Box containers, templates, plugins or resources from inside a container session — cover box_overview, box_lifecycle, box_create, box_resources, box_plugins, box_templates, box_settings, box_task and the browser page tools.
4
+ ---
5
+
6
+ # Driving DSH Box with the `box_*` tools
7
+
8
+ You run inside a DSH Box container, and this profile mounts the sandbox plugin
9
+ (`@nexus-aethra/dsh-box-sandbox`) that bridges to the `dshboxd` daemon on the host.
10
+ It registers a typed `box_*` tool surface, so you never have to shell out to `dshbox`
11
+ or parse its stdout. Every tool is one JSON call; every answer is the daemon's own
12
+ result, not a summary.
13
+
14
+ ## Read this first
15
+
16
+ - **`box_overview` before anything else.** It is the only listing tool: containers,
17
+ templates, and the plugins the box knows. Acting on an id you guessed is the
18
+ failure this tool removes.
19
+ - **Check `self` in the same `box_overview` answer.** It says where you sit:
20
+ `role: container` with the container id you are running in, or `external` when
21
+ you watch the box from outside (a desktop or web DSH). A `container` agent
22
+ must not stop, restart, rebuild or remove its own container — `box_lifecycle`
23
+ refuses, because the call could never report back.
24
+ - **Anything that changes state through a task needs `box_task`.** Call it with
25
+ `action: 'status'` and the task id the action returned, or `action: 'list'` to
26
+ find what is already running. A task that has not finished has not happened.
27
+ - **A call that disappears is an outage, not an answer.** When the daemon is down
28
+ every tool fails with a message naming the discovery record it looked for; that
29
+ means "the box is not running", not "the thing does not exist".
30
+
31
+ ## The tool surface
32
+
33
+ | Tool | What it does |
34
+ |---|---|
35
+ | `box_overview` | Containers, templates, plugins, and where you sit (`self`). Start here. |
36
+ | `box_lifecycle` | start / stop / stop-now / restart / rebuild / remove a container |
37
+ | `box_create` | from-template, from-version, describe, url, browse-paths |
38
+ | `box_resources` | list, read, write, extract, inject, rm, rm-view, view-on, view-off, prune, types, views |
39
+ | `box_plugins` | list, installed, container, add, add-extension, copy, remove-plugin, import, export, export-installed, import-workspace, install-bundle, bundle, prune, graph |
40
+ | `box_templates` | list, show, pull, import, export, remove, prune, catalog |
41
+ | `box_settings` | toolchains, installed, catalog, refresh-catalog, data, references, template-info, template-list, save-mirrors, set-runtime-directory, uninstall-dsh |
42
+ | `box_task` | list, status, cancel, delete |
43
+ | `box_workspace` | list, add (restart: true stops the container, adds, starts it again) |
44
+ | `box_build` | build a template from a boxfile; apply a resource document |
45
+
46
+ ## Rules that are easy to get wrong
47
+
48
+ 1. **This container cannot be restarted from inside it.** `box_lifecycle` refuses
49
+ when the target is the container you are running in — stopping it kills the host
50
+ that is executing your call, so the result can never come back. Use `box_task`
51
+ to watch, and let a person or the CLI act on that container.
52
+ 2. **`add`, `import` and `pull` are tasks.** They return a task id, not a finished
53
+ object. `box_task` with `action: 'status'` is how you learn whether it worked;
54
+ the task's `logPath` is where the daemon writes the step-by-step log.
55
+ 3. **Closing a resource type is not deleting it.** `box_resources` with
56
+ `view-off` hides the type's tab and keeps its row and every copy taken under it;
57
+ `rm-view` removes the type itself and leaves the copies alone. Choose deliberately.
58
+ 4. **Injection refuses rather than overwrites.** `inject` will not replace an
59
+ existing destination without `overwrite`, and will not touch a running container
60
+ without `restart`. That refusal is the safety property, not an error to work around.
61
+ 5. **Three actions change the whole box and need `confirm: true`.**
62
+ `save-mirrors`, `set-runtime-directory`, `uninstall-dsh` affect every container
63
+ and every future session. The tool refuses without the flag.
64
+
65
+ ## Debugging a page (separate tool family)
66
+
67
+ The same plugin also registers browser tools against a headless Chromium session:
68
+ `box_page_text`, `box_click_element`, `box_click_at`, `box_hover`, `box_type_text`, `box_press_key`,
69
+ `box_scroll`, `box_navigate`, `box_evaluate`, `box_screenshot`, `box_console`, `box_set_viewport`,
70
+ `box_set_browser`, `box_close`.
71
+
72
+ When using them, always start with the observation tool and act only on what it
73
+ returned. The tool descriptions carry the full workflow; follow them.
74
+
75
+ ## Worked example: take a resource copy and check it landed
76
+
77
+ 1. `box_overview` -> find the container id, e.g. `container-<timestamp>`.
78
+ 2. `box_resources` with `action: 'list'` and that `containerId` -> see which kinds
79
+ exist and where.
80
+ 3. `box_resources` with `action: 'extract'`, `kind: 'sessions'` -> returns a task id.
81
+ 4. `box_task` with `action: 'status'`, that id -> `succeeded` means the copy exists.
82
+ 5. `box_resources` with `action: 'stored'` -> confirm the new copy is listed.
83
+
84
+ If a step fails, the daemon's message names what it refused and why. Read it; do not
85
+ retry with a different flag to see if that works.