@nexus-aethra/dsh-box-sandbox 0.1.0 → 0.2.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/README.md +43 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +169 -50
- package/dist/rpc.d.mts +9 -0
- package/dist/rpc.mjs +21 -4
- package/package.json +14 -10
- package/skills/dsh-box-tools/SKILL.md +80 -0
package/README.md
CHANGED
|
@@ -81,8 +81,48 @@ container.
|
|
|
81
81
|
|
|
82
82
|
pnpm install
|
|
83
83
|
pnpm typecheck
|
|
84
|
+
pnpm check:outputs # static: schema vs what the daemon returns
|
|
85
|
+
pnpm check:tools # live: every tool called against a running Box
|
|
84
86
|
pnpm build
|
|
85
|
-
node smoke.mjs # 10 checks against a running DSH Box
|
|
86
87
|
|
|
87
|
-
`
|
|
88
|
-
|
|
88
|
+
`check:tools` needs a running daemon: it imports the built plugin with a stub
|
|
89
|
+
host context, calls every tool's `execute` for real, and validates each answer
|
|
90
|
+
against that tool's own output schema — the same check the host applies, at the
|
|
91
|
+
moment the answer comes back. It exists because a tool whose schema disagrees
|
|
92
|
+
with its result does not fail at build time; it fails when someone presses the
|
|
93
|
+
button, and the only way to see that is to press it.
|
|
94
|
+
|
|
95
|
+
`check:tools` is a local check and does not run in CI, which has no daemon.
|
|
96
|
+
The tag pipeline runs `check:outputs` instead, because that one needs nothing
|
|
97
|
+
but the repository.
|
|
98
|
+
|
|
99
|
+
node smoke.mjs # transport-level: ping, stale-port failure, recovery
|
|
100
|
+
|
|
101
|
+
`smoke.mjs` reads the real discovery record and checks the wire contract.
|
|
102
|
+
|
|
103
|
+
## Releasing
|
|
104
|
+
|
|
105
|
+
This package's version is the DSH Box app's version. One tag, one release, and
|
|
106
|
+
`release.yml` checks all four manifests against it before anything is built —
|
|
107
|
+
bumping the app and forgetting this one fails the pipeline rather than
|
|
108
|
+
publishing a plugin whose version claims to match a build it was never part of.
|
|
109
|
+
|
|
110
|
+
To release:
|
|
111
|
+
|
|
112
|
+
1. Bump `version` in this `package.json`, the root `package.json`,
|
|
113
|
+
`src-tauri/tauri.conf.json` and `src-tauri/Cargo.toml` to the same number.
|
|
114
|
+
2. Run `pnpm check:tools` against a real Box. CI cannot — it has no daemon —
|
|
115
|
+
so this is the only place the live check happens.
|
|
116
|
+
3. Commit, tag `v<version>`, push the tag.
|
|
117
|
+
|
|
118
|
+
The `publish-plugin` job runs after the installers are built, so a release
|
|
119
|
+
whose app build failed does not put a version on npm. It typechecks, runs the
|
|
120
|
+
schema reconciliation, builds `dist/` (gitignored, so never restored from the
|
|
121
|
+
commit), prints the tarball contents, skips a version npm already has, refuses
|
|
122
|
+
to publish below what npm has, and publishes with provenance.
|
|
123
|
+
|
|
124
|
+
Auth is npm trusted publishing over OIDC — there is no long-lived token to
|
|
125
|
+
leak. One-time setup on npmjs.com, under the package's Settings → Trusted
|
|
126
|
+
Publishers: provider **GitHub Actions**, account `aethra-tech`, repository
|
|
127
|
+
`DSHBox`, workflow filename `release.yml`. Rename that file and npm has to be
|
|
128
|
+
told again.
|
package/dist/index.d.mts
CHANGED
|
@@ -4,9 +4,9 @@ import { Context } from "@deepseek-ai/cordis";
|
|
|
4
4
|
/** Cordis plugin name used by loader diagnostics and the patch overlay. */
|
|
5
5
|
declare const name = "dsh-box-sandbox";
|
|
6
6
|
/**
|
|
7
|
-
* Services this plugin depends on.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Services this plugin depends on. `tools` to register the box_* surface and
|
|
8
|
+
* `systemPrompt` for the cross-tool workflow section; the client itself is
|
|
9
|
+
* pure transport and needs nothing.
|
|
10
10
|
*/
|
|
11
11
|
declare const inject: string[];
|
|
12
12
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -165,11 +165,6 @@ async function withSession(id, method, params) {
|
|
|
165
165
|
});
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
|
-
function base64Of(value) {
|
|
169
|
-
const data = asRecord(value).data;
|
|
170
|
-
if (typeof data !== "string") throw new Error("screenshot result carried no image data");
|
|
171
|
-
return data;
|
|
172
|
-
}
|
|
173
168
|
/**
|
|
174
169
|
* Register one tool against the host registry.
|
|
175
170
|
*
|
|
@@ -211,6 +206,14 @@ function nextHint(next) {
|
|
|
211
206
|
return "\nnext: " + next;
|
|
212
207
|
}
|
|
213
208
|
/**
|
|
209
|
+
* A string that may legitimately be absent.
|
|
210
|
+
*
|
|
211
|
+
* The daemon reports these as ${Option<String>}: a miss is null, not
|
|
212
|
+
* an empty string, and a schema that says $string alone rejects it — so a
|
|
213
|
+
* click that worked is reported as a failed tool call.
|
|
214
|
+
*/
|
|
215
|
+
const nullable = { oneOf: [{ type: "string" }, { type: "null" }] };
|
|
216
|
+
/**
|
|
214
217
|
* Render a click, distinguishing the two outcomes an agent must not confuse.
|
|
215
218
|
*
|
|
216
219
|
* A dispatched event is not a delivered one. When something covers the target
|
|
@@ -236,7 +239,7 @@ function renderClick(value, target) {
|
|
|
236
239
|
function applyBoxTools(ctx) {
|
|
237
240
|
registerTool(ctx, {
|
|
238
241
|
name: "box_screenshot",
|
|
239
|
-
description: "Save what a running dshbox container currently renders as a PNG, and report where.
|
|
242
|
+
description: "Save what a running dshbox container currently renders as a PNG, and report where.You get the file path and the image size, not the picture: a tool result is assistant-sidecontent and the host sends text only on that side, so an image cannot arrive. Hand the pathto a person to look at, and use box_page_text to find out what is on the page. In practice:describe the page in your answer from box_page_text, and take a screenshot when the layoutor a rendering is what is worth showing rather than describing.",
|
|
240
243
|
parameters: {
|
|
241
244
|
type: "object",
|
|
242
245
|
properties: {
|
|
@@ -258,35 +261,38 @@ function applyBoxTools(ctx) {
|
|
|
258
261
|
additionalProperties: false,
|
|
259
262
|
properties: {
|
|
260
263
|
containerId: { type: "string" },
|
|
261
|
-
|
|
264
|
+
path: { type: "string" },
|
|
265
|
+
width: { type: "number" },
|
|
266
|
+
height: { type: "number" },
|
|
267
|
+
bytes: { type: "number" }
|
|
262
268
|
},
|
|
263
|
-
required: [
|
|
269
|
+
required: [
|
|
270
|
+
"containerId",
|
|
271
|
+
"path",
|
|
272
|
+
"width",
|
|
273
|
+
"height",
|
|
274
|
+
"bytes"
|
|
275
|
+
]
|
|
264
276
|
},
|
|
265
277
|
render: (_args, value) => {
|
|
266
278
|
const record = asRecord(value);
|
|
267
|
-
const shot = asRecord(record.attachment);
|
|
268
279
|
return [{
|
|
269
280
|
type: "text",
|
|
270
|
-
text: "Screenshot of container " + String(record.containerId) + "
|
|
271
|
-
}, {
|
|
272
|
-
type: "image",
|
|
273
|
-
attachment: record.attachment
|
|
281
|
+
text: "Screenshot of container " + String(record.containerId) + " (" + String(record.width) + "x" + String(record.height) + ", " + String(record.bytes) + " bytes) saved to " + String(record.path) + "."
|
|
274
282
|
}];
|
|
275
283
|
}
|
|
276
284
|
},
|
|
277
285
|
async execute(args) {
|
|
278
286
|
const containerId = containerIdOf(args);
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
});
|
|
283
|
-
const data = Buffer.from(base64Of(raw), "base64");
|
|
287
|
+
const result = await withSession(containerId, "debug_capture_screenshot", { fullPage: asRecord(args).fullPage === true });
|
|
288
|
+
const path = result.path;
|
|
289
|
+
if (typeof path !== "string" || path === "") throw new Error("screenshot result carried no file path");
|
|
284
290
|
return {
|
|
285
291
|
containerId,
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
292
|
+
path,
|
|
293
|
+
width: result.width,
|
|
294
|
+
height: result.height,
|
|
295
|
+
bytes: result.bytes
|
|
290
296
|
};
|
|
291
297
|
}
|
|
292
298
|
});
|
|
@@ -327,12 +333,12 @@ function applyBoxTools(ctx) {
|
|
|
327
333
|
properties: {
|
|
328
334
|
containerId: { type: "string" },
|
|
329
335
|
name: { type: "string" },
|
|
330
|
-
matched:
|
|
336
|
+
matched: nullable,
|
|
331
337
|
landed: { type: "boolean" },
|
|
332
|
-
hitTag:
|
|
333
|
-
hitText:
|
|
334
|
-
occludedBy:
|
|
335
|
-
selector:
|
|
338
|
+
hitTag: nullable,
|
|
339
|
+
hitText: nullable,
|
|
340
|
+
occludedBy: nullable,
|
|
341
|
+
selector: nullable,
|
|
336
342
|
x: { type: "number" },
|
|
337
343
|
y: { type: "number" },
|
|
338
344
|
tag: { type: "string" },
|
|
@@ -406,12 +412,17 @@ function applyBoxTools(ctx) {
|
|
|
406
412
|
properties: {
|
|
407
413
|
containerId: { type: "string" },
|
|
408
414
|
x: { type: "number" },
|
|
409
|
-
y: { type: "number" }
|
|
415
|
+
y: { type: "number" },
|
|
416
|
+
landed: { type: "boolean" },
|
|
417
|
+
hitTag: nullable,
|
|
418
|
+
hitText: nullable,
|
|
419
|
+
occludedBy: nullable
|
|
410
420
|
},
|
|
411
421
|
required: [
|
|
412
422
|
"containerId",
|
|
413
423
|
"x",
|
|
414
|
-
"y"
|
|
424
|
+
"y",
|
|
425
|
+
"landed"
|
|
415
426
|
]
|
|
416
427
|
},
|
|
417
428
|
render: (_args, value) => renderClick(value, "at (" + String(asRecord(value).x) + ", " + String(asRecord(value).y) + ")")
|
|
@@ -533,7 +544,9 @@ function applyBoxTools(ctx) {
|
|
|
533
544
|
scrollHeight: { type: "number" },
|
|
534
545
|
viewportHeight: { type: "number" },
|
|
535
546
|
screensBelow: { type: "number" },
|
|
536
|
-
moved: { type: "boolean" }
|
|
547
|
+
moved: { type: "boolean" },
|
|
548
|
+
scroller: nullable,
|
|
549
|
+
scrollerIsDocument: { type: "boolean" }
|
|
537
550
|
},
|
|
538
551
|
required: [
|
|
539
552
|
"containerId",
|
|
@@ -587,7 +600,7 @@ function applyBoxTools(ctx) {
|
|
|
587
600
|
properties: {
|
|
588
601
|
containerId: { type: "string" },
|
|
589
602
|
inserted: { type: "number" },
|
|
590
|
-
focused:
|
|
603
|
+
focused: nullable,
|
|
591
604
|
landed: { type: "boolean" }
|
|
592
605
|
},
|
|
593
606
|
required: [
|
|
@@ -608,9 +621,97 @@ function applyBoxTools(ctx) {
|
|
|
608
621
|
const containerId = containerIdOf(args);
|
|
609
622
|
const record = asRecord(args);
|
|
610
623
|
if (typeof record.text !== "string" || record.text === "") throw new Error("text is required and must be non-empty");
|
|
624
|
+
const result = await withSession(containerId, "debug_type_text", { text: record.text });
|
|
611
625
|
return {
|
|
612
626
|
containerId,
|
|
613
|
-
|
|
627
|
+
inserted: result.inserted,
|
|
628
|
+
focused: result.focused,
|
|
629
|
+
landed: result.focused !== null
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
registerTool(ctx, {
|
|
634
|
+
name: "box_console",
|
|
635
|
+
description: "Read what the page has logged, and clear the buffer — a second call reports only what arrived since the first. Uncaught errors are included whether or not any code caught them, which is the point: a client plugin that fails to register leaves a page that looks fine and says nothing, and this is the only tool that sees it. Repeated identical lines are collapsed into one entry with a count. The levels default to error and warning because log-level traffic is usually dozens of lines per page load; pass levels to ask for the rest when the failures are not the problem.",
|
|
636
|
+
parameters: {
|
|
637
|
+
type: "object",
|
|
638
|
+
properties: {
|
|
639
|
+
containerId: {
|
|
640
|
+
type: "string",
|
|
641
|
+
description: "Container whose console to read."
|
|
642
|
+
},
|
|
643
|
+
levels: {
|
|
644
|
+
type: "array",
|
|
645
|
+
items: {
|
|
646
|
+
type: "string",
|
|
647
|
+
enum: [
|
|
648
|
+
"log",
|
|
649
|
+
"info",
|
|
650
|
+
"debug",
|
|
651
|
+
"warning",
|
|
652
|
+
"error",
|
|
653
|
+
"assert"
|
|
654
|
+
]
|
|
655
|
+
},
|
|
656
|
+
description: "Which severities to return. Defaults to error and warning."
|
|
657
|
+
},
|
|
658
|
+
limit: {
|
|
659
|
+
type: "number",
|
|
660
|
+
description: "Most recent entries to return. Defaults to 50, capped at 500."
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
required: ["containerId"]
|
|
664
|
+
},
|
|
665
|
+
timeoutMs: WARM_BUDGET_MS,
|
|
666
|
+
output: {
|
|
667
|
+
schema: {
|
|
668
|
+
type: "object",
|
|
669
|
+
additionalProperties: false,
|
|
670
|
+
properties: {
|
|
671
|
+
containerId: { type: "string" },
|
|
672
|
+
entries: { type: "array" },
|
|
673
|
+
total: { type: "number" },
|
|
674
|
+
truncated: { type: "boolean" },
|
|
675
|
+
raw: { type: "number" },
|
|
676
|
+
methods: { type: "array" }
|
|
677
|
+
},
|
|
678
|
+
required: [
|
|
679
|
+
"containerId",
|
|
680
|
+
"entries",
|
|
681
|
+
"total",
|
|
682
|
+
"truncated",
|
|
683
|
+
"raw",
|
|
684
|
+
"methods"
|
|
685
|
+
]
|
|
686
|
+
},
|
|
687
|
+
render: (_args, value) => {
|
|
688
|
+
const record = asRecord(value);
|
|
689
|
+
const entries = Array.isArray(record.entries) ? record.entries : [];
|
|
690
|
+
if (entries.length === 0) return [{
|
|
691
|
+
type: "text",
|
|
692
|
+
text: "The page in container " + String(record.containerId) + " logged nothing at the levels asked for, and logged nothing since the last read." + nextHint("if the page was just loaded, try again after the failing step, or widen levels to log")
|
|
693
|
+
}];
|
|
694
|
+
const lines = entries.map((entry) => {
|
|
695
|
+
const item = asRecord(entry);
|
|
696
|
+
const where = item.source ? " (" + String(item.source) + ")" : "";
|
|
697
|
+
const times = Number(item.count ?? 1) > 1 ? " x" + String(item.count) : "";
|
|
698
|
+
return "[" + String(item.level) + "]" + where + " " + String(item.text) + times;
|
|
699
|
+
});
|
|
700
|
+
return [{
|
|
701
|
+
type: "text",
|
|
702
|
+
text: "Console of container " + String(record.containerId) + " — " + String(record.total) + " distinct message(s), oldest first" + (record.truncated === true ? " (truncated to the most recent)" : "") + ". Reading this cleared the buffer.\n" + lines.join("\n")
|
|
703
|
+
}];
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
async execute(args) {
|
|
707
|
+
const containerId = containerIdOf(args);
|
|
708
|
+
const record = asRecord(args);
|
|
709
|
+
const request = {};
|
|
710
|
+
if (Array.isArray(record.levels) && record.levels.length > 0) request.levels = record.levels;
|
|
711
|
+
if (typeof record.limit === "number") request.limit = record.limit;
|
|
712
|
+
return {
|
|
713
|
+
containerId,
|
|
714
|
+
...await withSession(containerId, "debug_console", request)
|
|
614
715
|
};
|
|
615
716
|
}
|
|
616
717
|
});
|
|
@@ -681,8 +782,8 @@ function applyBoxTools(ctx) {
|
|
|
681
782
|
kind: { type: "string" },
|
|
682
783
|
path: { type: "string" },
|
|
683
784
|
configured: { type: "boolean" },
|
|
684
|
-
configuredPath:
|
|
685
|
-
problem:
|
|
785
|
+
configuredPath: nullable,
|
|
786
|
+
problem: nullable
|
|
686
787
|
},
|
|
687
788
|
required: ["available"]
|
|
688
789
|
},
|
|
@@ -739,7 +840,7 @@ function applyBoxTools(ctx) {
|
|
|
739
840
|
const containerId = containerIdOf(args);
|
|
740
841
|
return {
|
|
741
842
|
containerId,
|
|
742
|
-
|
|
843
|
+
closed: (await getRpc().call("debug_close", { id: containerId })).closed === true
|
|
743
844
|
};
|
|
744
845
|
}
|
|
745
846
|
});
|
|
@@ -935,6 +1036,8 @@ function registerManageTools(ctx) {
|
|
|
935
1036
|
"inject",
|
|
936
1037
|
"rm",
|
|
937
1038
|
"rm-view",
|
|
1039
|
+
"view-on",
|
|
1040
|
+
"view-off",
|
|
938
1041
|
"prune",
|
|
939
1042
|
"types",
|
|
940
1043
|
"views"
|
|
@@ -965,7 +1068,7 @@ function registerManageTools(ctx) {
|
|
|
965
1068
|
},
|
|
966
1069
|
viewId: {
|
|
967
1070
|
type: "string",
|
|
968
|
-
description: "Which resource view to
|
|
1071
|
+
description: "Which resource view to act on: the type a Resources tab stands for. Required by rm-view, view-on and view-off."
|
|
969
1072
|
},
|
|
970
1073
|
name: {
|
|
971
1074
|
type: "string",
|
|
@@ -1083,8 +1186,20 @@ function registerManageTools(ctx) {
|
|
|
1083
1186
|
case "rm-view": return {
|
|
1084
1187
|
action,
|
|
1085
1188
|
containerId,
|
|
1086
|
-
result: asObject(await rpc.call("delete_resource_view", {
|
|
1189
|
+
result: asObject(await rpc.call("delete_resource_view", { viewId: required(record, "viewId") }))
|
|
1087
1190
|
};
|
|
1191
|
+
case "view-on":
|
|
1192
|
+
case "view-off": {
|
|
1193
|
+
const enabled = action === "view-on";
|
|
1194
|
+
return {
|
|
1195
|
+
action,
|
|
1196
|
+
containerId,
|
|
1197
|
+
result: asObject(await rpc.call("set_resource_view_enabled", {
|
|
1198
|
+
viewId: required(record, "viewId"),
|
|
1199
|
+
enabled
|
|
1200
|
+
}))
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1088
1203
|
case "prune": return {
|
|
1089
1204
|
action,
|
|
1090
1205
|
containerId,
|
|
@@ -1119,6 +1234,8 @@ function registerManageTools(ctx) {
|
|
|
1119
1234
|
"inject",
|
|
1120
1235
|
"rm",
|
|
1121
1236
|
"rm-view",
|
|
1237
|
+
"view-on",
|
|
1238
|
+
"view-off",
|
|
1122
1239
|
"prune",
|
|
1123
1240
|
"types",
|
|
1124
1241
|
"views"
|
|
@@ -1260,6 +1377,7 @@ function registerManageTools(ctx) {
|
|
|
1260
1377
|
};
|
|
1261
1378
|
const title = optional(record, "title");
|
|
1262
1379
|
if (title !== void 0) params.title = title;
|
|
1380
|
+
if (flag(record, "restart")) params.restart = true;
|
|
1263
1381
|
const added = await rpc.call("add_container_workspace", params);
|
|
1264
1382
|
return {
|
|
1265
1383
|
action,
|
|
@@ -1725,7 +1843,7 @@ function registerManageTools(ctx) {
|
|
|
1725
1843
|
"from-version",
|
|
1726
1844
|
"describe",
|
|
1727
1845
|
"url",
|
|
1728
|
-
"browse"
|
|
1846
|
+
"browse-paths"
|
|
1729
1847
|
]),
|
|
1730
1848
|
name: {
|
|
1731
1849
|
type: "string",
|
|
@@ -1741,7 +1859,7 @@ function registerManageTools(ctx) {
|
|
|
1741
1859
|
},
|
|
1742
1860
|
containerId: {
|
|
1743
1861
|
type: "string",
|
|
1744
|
-
description: "Which container, for describe, url and browse."
|
|
1862
|
+
description: "Which container, for describe, url and browse-paths."
|
|
1745
1863
|
},
|
|
1746
1864
|
profile: {
|
|
1747
1865
|
type: "string",
|
|
@@ -1815,6 +1933,7 @@ function registerManageTools(ctx) {
|
|
|
1815
1933
|
action,
|
|
1816
1934
|
result: asObject(await rpc.call("container_url", { id: needId() }))
|
|
1817
1935
|
};
|
|
1936
|
+
case "browse-paths":
|
|
1818
1937
|
case "browse": return {
|
|
1819
1938
|
action,
|
|
1820
1939
|
result: asObject(await rpc.call("browse_container_paths", { id: needId() }))
|
|
@@ -1824,7 +1943,7 @@ function registerManageTools(ctx) {
|
|
|
1824
1943
|
"from-version",
|
|
1825
1944
|
"describe",
|
|
1826
1945
|
"url",
|
|
1827
|
-
"browse"
|
|
1946
|
+
"browse-paths"
|
|
1828
1947
|
]);
|
|
1829
1948
|
}
|
|
1830
1949
|
}
|
|
@@ -2124,15 +2243,11 @@ function asArray(value) {
|
|
|
2124
2243
|
/** Cordis plugin name used by loader diagnostics and the patch overlay. */
|
|
2125
2244
|
const name = "dsh-box-sandbox";
|
|
2126
2245
|
/**
|
|
2127
|
-
* Services this plugin depends on.
|
|
2128
|
-
*
|
|
2129
|
-
*
|
|
2246
|
+
* Services this plugin depends on. `tools` to register the box_* surface and
|
|
2247
|
+
* `systemPrompt` for the cross-tool workflow section; the client itself is
|
|
2248
|
+
* pure transport and needs nothing.
|
|
2130
2249
|
*/
|
|
2131
|
-
const inject = [
|
|
2132
|
-
"tools",
|
|
2133
|
-
"attachments",
|
|
2134
|
-
"systemPrompt"
|
|
2135
|
-
];
|
|
2250
|
+
const inject = ["tools", "systemPrompt"];
|
|
2136
2251
|
/**
|
|
2137
2252
|
* Effective configuration after defaults.
|
|
2138
2253
|
*
|
|
@@ -2195,7 +2310,7 @@ async function status(config) {
|
|
|
2195
2310
|
* anything a tool already reports belongs in that tool.
|
|
2196
2311
|
*/
|
|
2197
2312
|
const WORKFLOW_PROMPT_SECTION = "dsh-box-sandbox:page-workflow";
|
|
2198
|
-
const WORKFLOW_PROMPT_ORDER =
|
|
2313
|
+
const WORKFLOW_PROMPT_ORDER = 3350;
|
|
2199
2314
|
function pageWorkflowPrompt() {
|
|
2200
2315
|
return [
|
|
2201
2316
|
"Debugging a page with the box_* tools:",
|
|
@@ -2211,7 +2326,11 @@ function pageWorkflowPrompt() {
|
|
|
2211
2326
|
"Before typing, click the field. box_type_text reports the focused element and inserts nothing",
|
|
2212
2327
|
"when no editable field holds focus.",
|
|
2213
2328
|
"If a page tool cannot find a browser, box_set_browser reports which one is in use and pins a",
|
|
2214
|
-
"different one."
|
|
2329
|
+
"different one.",
|
|
2330
|
+
"When a page looks right but behaves wrongly — a client bundle that never registered, a control",
|
|
2331
|
+
"that does nothing — read box_console. A page that failed to load its own assets says so only",
|
|
2332
|
+
"there; the rest of the surface reports a page that is present, which is exactly what a broken",
|
|
2333
|
+
"one also is."
|
|
2215
2334
|
].join("\n");
|
|
2216
2335
|
}
|
|
2217
2336
|
/** Register the plugin against a freshly mounted Cordis context. */
|
package/dist/rpc.d.mts
CHANGED
|
@@ -115,6 +115,15 @@ declare class DshboxRpc {
|
|
|
115
115
|
* never retried -- they are answers, not outages.
|
|
116
116
|
*/
|
|
117
117
|
call<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
|
|
118
|
+
/**
|
|
119
|
+
* The message for "discovery.json names a daemon that is not there".
|
|
120
|
+
*
|
|
121
|
+
* Staleness has two causes needing different fixes: the file may be
|
|
122
|
+
* missing (the daemon was never started) or it may name a pid that has
|
|
123
|
+
* exited (it crashed, or the machine rebooted). Both arrive here as a
|
|
124
|
+
* refused connection, so the record is read again to tell them apart.
|
|
125
|
+
*/
|
|
126
|
+
private unreachable;
|
|
118
127
|
/** Liveness probe. Resolves to the daemon's own status frame. */
|
|
119
128
|
ping(): Promise<{
|
|
120
129
|
pid: number;
|
package/dist/rpc.mjs
CHANGED
|
@@ -148,17 +148,34 @@ var DshboxRpc = class {
|
|
|
148
148
|
* never retried -- they are answers, not outages.
|
|
149
149
|
*/
|
|
150
150
|
async call(method, params) {
|
|
151
|
+
let lastStale = null;
|
|
151
152
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
152
153
|
const record = this.discovery();
|
|
153
154
|
try {
|
|
154
155
|
return await this.send(record, method, params);
|
|
155
156
|
} catch (error) {
|
|
156
|
-
if (
|
|
157
|
-
|
|
157
|
+
if (!isStaleEndpoint(error)) throw error;
|
|
158
|
+
lastStale = error;
|
|
159
|
+
if (attempt === 0) continue;
|
|
160
|
+
throw this.unreachable(method, lastStale);
|
|
158
161
|
}
|
|
159
162
|
}
|
|
160
|
-
/* c8 ignore next
|
|
161
|
-
throw
|
|
163
|
+
/* c8 ignore next */
|
|
164
|
+
throw this.unreachable(method, lastStale);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The message for "discovery.json names a daemon that is not there".
|
|
168
|
+
*
|
|
169
|
+
* Staleness has two causes needing different fixes: the file may be
|
|
170
|
+
* missing (the daemon was never started) or it may name a pid that has
|
|
171
|
+
* exited (it crashed, or the machine rebooted). Both arrive here as a
|
|
172
|
+
* refused connection, so the record is read again to tell them apart.
|
|
173
|
+
*/
|
|
174
|
+
unreachable(method, cause) {
|
|
175
|
+
const path = discoveryPath(this.configDir);
|
|
176
|
+
const record = readDiscovery(this.configDir);
|
|
177
|
+
if (record === null) return new DshboxUnavailableError("dshbox is not running, so " + method + " has nothing to talk to. Start it from the desktop app, or run `dshbox` once, then retry. No discovery record at " + path + ".", { cause });
|
|
178
|
+
return new DshboxUnavailableError("dshbox is not running, so " + method + " has nothing to talk to. The discovery record at " + path + " is stale: it names pid " + record.pid + " on port " + record.port + ", and nothing is listening there. Start the desktop app (or run `dshbox`) to bring up a new daemon; the stale record is replaced on startup.", { cause });
|
|
162
179
|
}
|
|
163
180
|
/** Liveness probe. Resolves to the daemon's own status frame. */
|
|
164
181
|
ping() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexus-aethra/dsh-box-sandbox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
"box",
|
|
11
11
|
"tools"
|
|
12
12
|
],
|
|
13
|
-
"homepage": "https://github.com/
|
|
13
|
+
"homepage": "https://github.com/Nexus-Aethra/DSHBox/tree/main/plugins/dsh-box-sandbox",
|
|
14
14
|
"bugs": {
|
|
15
|
-
"url": "https://github.com/
|
|
15
|
+
"url": "https://github.com/Nexus-Aethra/DSHBox/issues"
|
|
16
16
|
},
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/
|
|
19
|
+
"url": "git+https://github.com/Nexus-Aethra/DSHBox.git",
|
|
20
20
|
"directory": "plugins/dsh-box-sandbox"
|
|
21
21
|
},
|
|
22
22
|
"author": "Nexus Aethra",
|
|
@@ -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"
|
|
@@ -46,6 +47,14 @@
|
|
|
46
47
|
"patch": "./cordis.patch.yml"
|
|
47
48
|
}
|
|
48
49
|
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsdown",
|
|
52
|
+
"watch": "tsdown --watch",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"check:outputs": "node scripts/check-output-schemas.mjs",
|
|
55
|
+
"check:tools": "node scripts/check-tool-outputs.mjs",
|
|
56
|
+
"deploy": "node scripts/deploy.mjs"
|
|
57
|
+
},
|
|
49
58
|
"peerDependencies": {
|
|
50
59
|
"@deepseek-ai/cordis": "^4.0.1"
|
|
51
60
|
},
|
|
@@ -53,10 +62,5 @@
|
|
|
53
62
|
"@types/node": "^22.10.0",
|
|
54
63
|
"tsdown": "^0.22.2",
|
|
55
64
|
"typescript": "^5.6.0"
|
|
56
|
-
},
|
|
57
|
-
"scripts": {
|
|
58
|
-
"build": "tsdown",
|
|
59
|
-
"watch": "tsdown --watch",
|
|
60
|
-
"typecheck": "tsc --noEmit"
|
|
61
65
|
}
|
|
62
|
-
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
- **Anything that changes state through a task needs `box_task`.** Call it with
|
|
20
|
+
`action: 'status'` and the task id the action returned, or `action: 'list'` to
|
|
21
|
+
find what is already running. A task that has not finished has not happened.
|
|
22
|
+
- **A call that disappears is an outage, not an answer.** When the daemon is down
|
|
23
|
+
every tool fails with a message naming the discovery record it looked for; that
|
|
24
|
+
means "the box is not running", not "the thing does not exist".
|
|
25
|
+
|
|
26
|
+
## The tool surface
|
|
27
|
+
|
|
28
|
+
| Tool | What it does |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `box_overview` | Containers, templates, plugins. Start here. |
|
|
31
|
+
| `box_lifecycle` | start / stop / stop-now / restart / rebuild / remove a container |
|
|
32
|
+
| `box_create` | from-template, from-version, describe, url, browse-paths |
|
|
33
|
+
| `box_resources` | list, read, write, extract, inject, rm, rm-view, view-on, view-off, prune, types, views |
|
|
34
|
+
| `box_plugins` | list, installed, container, add, add-extension, copy, remove-plugin, import, export, export-installed, import-workspace, install-bundle, bundle, prune, graph |
|
|
35
|
+
| `box_templates` | list, show, pull, import, export, remove, prune, catalog |
|
|
36
|
+
| `box_settings` | toolchains, installed, catalog, refresh-catalog, data, references, template-info, template-list, save-mirrors, set-runtime-directory, uninstall-dsh |
|
|
37
|
+
| `box_task` | list, status, cancel, delete |
|
|
38
|
+
| `box_workspace` | list, add |
|
|
39
|
+
| `box_build` | build a template from a boxfile; apply a resource document |
|
|
40
|
+
|
|
41
|
+
## Rules that are easy to get wrong
|
|
42
|
+
|
|
43
|
+
1. **This container cannot be restarted from inside it.** `box_lifecycle` refuses
|
|
44
|
+
when the target is the container you are running in — stopping it kills the host
|
|
45
|
+
that is executing your call, so the result can never come back. Use `box_task`
|
|
46
|
+
to watch, and let a person or the CLI act on that container.
|
|
47
|
+
2. **`add`, `import` and `pull` are tasks.** They return a task id, not a finished
|
|
48
|
+
object. `box_task` with `action: 'status'` is how you learn whether it worked;
|
|
49
|
+
the task's `logPath` is where the daemon writes the step-by-step log.
|
|
50
|
+
3. **Closing a resource type is not deleting it.** `box_resources` with
|
|
51
|
+
`view-off` hides the type's tab and keeps its row and every copy taken under it;
|
|
52
|
+
`rm-view` removes the type itself and leaves the copies alone. Choose deliberately.
|
|
53
|
+
4. **Injection refuses rather than overwrites.** `inject` will not replace an
|
|
54
|
+
existing destination without `overwrite`, and will not touch a running container
|
|
55
|
+
without `restart`. That refusal is the safety property, not an error to work around.
|
|
56
|
+
5. **Three actions change the whole box and need `confirm: true`.**
|
|
57
|
+
`save-mirrors`, `set-runtime-directory`, `uninstall-dsh` affect every container
|
|
58
|
+
and every future session. The tool refuses without the flag.
|
|
59
|
+
|
|
60
|
+
## Debugging a page (separate tool family)
|
|
61
|
+
|
|
62
|
+
The same plugin also registers browser tools against a headless Chromium session:
|
|
63
|
+
`box_page_text`, `box_click_element`, `box_click_at`, `box_type_text`, `box_press_key`,
|
|
64
|
+
`box_scroll`, `box_screenshot`, `box_console`, `box_set_viewport`, `box_set_browser`,
|
|
65
|
+
`box_close`.
|
|
66
|
+
|
|
67
|
+
When using them, always start with the observation tool and act only on what it
|
|
68
|
+
returned. The tool descriptions carry the full workflow; follow them.
|
|
69
|
+
|
|
70
|
+
## Worked example: take a resource copy and check it landed
|
|
71
|
+
|
|
72
|
+
1. `box_overview` -> find the container id, e.g. `container-<timestamp>`.
|
|
73
|
+
2. `box_resources` with `action: 'list'` and that `containerId` -> see which kinds
|
|
74
|
+
exist and where.
|
|
75
|
+
3. `box_resources` with `action: 'extract'`, `kind: 'sessions'` -> returns a task id.
|
|
76
|
+
4. `box_task` with `action: 'status'`, that id -> `succeeded` means the copy exists.
|
|
77
|
+
5. `box_resources` with `action: 'stored'` -> confirm the new copy is listed.
|
|
78
|
+
|
|
79
|
+
If a step fails, the daemon's message names what it refused and why. Read it; do not
|
|
80
|
+
retry with a different flag to see if that works.
|