@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 +305 -25
- package/package.json +2 -1
- package/skills/dsh-box-tools/SKILL.md +85 -0
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
|
-
|
|
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: "
|
|
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
|
|
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", {
|
|
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
|
|
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
|
-
*
|
|
2182
|
-
*
|
|
2183
|
-
*
|
|
2184
|
-
*
|
|
2185
|
-
*
|
|
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
|
-
*
|
|
2190
|
-
*
|
|
2191
|
-
*
|
|
2192
|
-
*
|
|
2193
|
-
*
|
|
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
|
|
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
|
|
2466
|
+
return {
|
|
2467
|
+
role: "external",
|
|
2468
|
+
containerId: null
|
|
2469
|
+
};
|
|
2201
2470
|
}
|
|
2202
|
-
|
|
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.
|
|
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.
|