@nexus-aethra/dsh-box-sandbox 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nexus Aethra
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # @nexus-aethra/dsh-box-sandbox
2
+
3
+ Host-side bridge from a DSH session to the DSH Box daemon.
4
+
5
+ ## Why it exists
6
+
7
+ Developing a DSH plugin *inside* DSH is a crash story: a bad edit takes
8
+ down the host that is running it. DSH Box stays outside that loop, so the
9
+ development sandbox is the one process the agent is allowed to break. This
10
+ plugin makes that sandbox reachable over a typed transport instead of by
11
+ shelling out to the `dshbox` CLI and scraping stdout.
12
+
13
+ ## Wire contract
14
+
15
+ Verified against `src-tauri/crates/dshboxd/src/main.rs`. The details below
16
+ are the ones that are easy to get wrong:
17
+
18
+ - **`POST /rpc` is matched exactly.** `path != "//rpc"` is a 404, so the
19
+ path must carry no query string.
20
+ - **The token travels in the JSON body**, not in an `Authorization`
21
+ header. `box-client` also sends `Bearer`, but the daemon never reads it.
22
+ - **Params are flattened** into the top-level request object alongside
23
+ `token` and `method` — there is no nested `params` field.
24
+ - **Replies**: `{ ok: true, result }` for sync methods,
25
+ `{ ok: true, task }` for async ones, `{ ok: false, error }` otherwise.
26
+ The daemon closes the connection after each frame.
27
+ - **`GET /events?token=...`** is the SSE stream; that endpoint takes the
28
+ token from the query string.
29
+
30
+ ## Discovery
31
+
32
+ The daemon binds a **dynamic** loopback port and mints a token per launch,
33
+ then publishes both atomically:
34
+
35
+ <config-dir>/server/discovery.json
36
+
37
+ where `<config-dir>` is `DSHBOX_CONFIG_DIR` if set, otherwise `~/.dsh-box` —
38
+ the same precedence `box_foundation::config_path` applies on the Rust side.
39
+ The record is `{ token, pid, startedAt, port }` and is removed on shutdown.
40
+
41
+ Ports are never hardcoded. The record is re-read on every call, and a
42
+ transport failure is retried exactly once against a fresh read, so a daemon
43
+ restart (new port, new token) is transparent to callers.
44
+
45
+ ## Usage
46
+
47
+ import { getRpc, status } from '@nexus-aethra/dsh-box-sandbox'
48
+
49
+ const pong = await getRpc().ping()
50
+ const containers = await getRpc().call('list_containers')
51
+
52
+ // Probe without throwing — for prompt surfaces and diagnostics.
53
+ const s = await status()
54
+ if (s.problem) console.log('dshbox unavailable:', s.problem)
55
+
56
+ `status()` never throws: it returns the discovery path, the endpoint when
57
+ reachable, and a `problem` string otherwise. A failed mount is never
58
+ fatal — a missing daemon is a normal first-run state, not a load error.
59
+
60
+ ## Config
61
+
62
+ - insert:
63
+ - id: dsh-box-sandbox
64
+ name: '@nexus-aethra/dsh-box-sandbox'
65
+ config: {}
66
+
67
+ | Field | Default | Meaning |
68
+ |---|---|---|
69
+ | `configDir` | `''` | Empty means resolve like the daemon does. |
70
+ | `timeoutMs` | `30000` | Per-attempt request timeout. |
71
+
72
+ ## Security
73
+
74
+ The discovery token is full control of the daemon: create and delete
75
+ containers, run builds, install plugins. Anything that can read the record
76
+ can drive dshbox. That is the intent here, but it is a deliberate trust
77
+ boundary — do not mount the host profile directory into an untrusted
78
+ container.
79
+
80
+ ## Development
81
+
82
+ pnpm install
83
+ pnpm typecheck
84
+ pnpm build
85
+ node smoke.mjs # 10 checks against a running DSH Box
86
+
87
+ `smoke.mjs` needs a live daemon: it reads the real discovery record, pings
88
+ it, forces a stale-port failure, and verifies recovery.
@@ -0,0 +1,11 @@
1
+ # @nexus-aethra/dsh-box-sandbox: mounts the host-side development sandbox.
2
+ #
3
+ # Every field is optional. With no config the client honours
4
+ # DSHBOX_CONFIG_DIR and falls back to ~/.dsh-box, which is where DSH Box
5
+ # publishes its discovery record. Overriding configDir is only needed to
6
+ # point at a non-default profile.
7
+
8
+ - insert:
9
+ - id: dsh-box-sandbox
10
+ name: '@nexus-aethra/dsh-box-sandbox'
11
+ config: {}
@@ -0,0 +1,47 @@
1
+ import { Discovery, DshboxRpc, DshboxUnavailableError, discoveryPath, getRpc, resetRpc } from "./rpc.mjs";
2
+ import { Context } from "@deepseek-ai/cordis";
3
+ //#region src/index.d.ts
4
+ /** Cordis plugin name used by loader diagnostics and the patch overlay. */
5
+ declare const name = "dsh-box-sandbox";
6
+ /**
7
+ * Services this plugin depends on. Empty for now: the client is pure
8
+ * transport and needs no host service. Tools built on top of it will
9
+ * declare their own.
10
+ */
11
+ declare const inject: string[];
12
+ /**
13
+ * Plugin config. Both fields carry defaults, so the mount works with an
14
+ * empty config block; an empty configDir means "resolve it the way the
15
+ * daemon does" rather than "the current directory".
16
+ */
17
+ interface Config {
18
+ /**
19
+ * Directory holding server/discovery.json. The default empty string
20
+ * means: honour DSHBOX_CONFIG_DIR, then fall back to ~/.dsh-box.
21
+ */
22
+ configDir: string;
23
+ /** Per-attempt request timeout in milliseconds. */
24
+ timeoutMs: number;
25
+ }
26
+ /** Snapshot of reachability, for diagnostics and prompt surfaces. */
27
+ interface Status {
28
+ /** Path the discovery record was looked for at. */
29
+ discoveryPath: string;
30
+ /** Endpoint details when the daemon is up, else null. */
31
+ endpoint: Discovery | null;
32
+ /** Populated when endpoint is null. */
33
+ problem: string | null;
34
+ }
35
+ /**
36
+ * Probe the daemon without throwing.
37
+ *
38
+ * Returns the discovery record when the daemon answers, or the reason it
39
+ * could not be reached. A ping failure is reported rather than raised so
40
+ * that a status surface can render "dshbox is not running" without every
41
+ * caller having to catch.
42
+ */
43
+ declare function status(config?: Partial<Config>): Promise<Status>;
44
+ /** Register the plugin against a freshly mounted Cordis context. */
45
+ declare function apply(ctx: Context, config?: Partial<Config>): void;
46
+ //#endregion
47
+ export { Config, type Discovery, DshboxRpc, DshboxUnavailableError, Status, apply, discoveryPath, getRpc, inject, name, resetRpc, status };