rtk-for-dsh 0.0.0-stage → 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/README.md CHANGED
@@ -1,3 +1,94 @@
1
- # Temporary Holding Version
1
+ # rtk-for-dsh
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A connector that lets [DeepSeek Harness](../../cloned/deepseek-harness) run shell commands through [RTK](../../cloned/rtk-projects/rtk) (Rust Token Killer), so command output reaches the model filtered and compressed.
4
+
5
+ ## What it provides
6
+
7
+ `rtk-for-dsh` provides two bash executors for the DSH shell seam (`ctx.shell`). Each replaces the executor a composition mounts; the seam allows exactly one.
8
+
9
+ - `RtkBashExecutor` replaces `@deepseek-ai/dsh-bash-local`.
10
+ - `RtkSandboxBashExecutor` replaces `@deepseek-ai/dsh-bash-sandbox`. The DeepSeek Harness desktop profile mounts the sandbox variant (from `packages/bundle/base`), so use this one there.
11
+
12
+ For each command, the executor asks `rtk rewrite <command>` and acts on the exit code:
13
+
14
+ | `rtk rewrite` exit | Executor behaviour |
15
+ |---|---|
16
+ | `0` (allow) or `3` (ask, the default when no allow rules exist) | Runs the rewritten command, e.g. `git status` becomes `rtk git status`. |
17
+ | `1` (no rewrite) | Runs the command as written. |
18
+ | `2` (deny rule matched) | Does not run it. Returns exit `126` with `rtk: command refused by an RTK deny rule` on stderr. |
19
+ | missing binary, timeout, abort, other code | Runs the command as written. RTK is an output filter, so its absence never blocks the agent. |
20
+
21
+ Spawning, output caps, timeouts, cancellation and background jobs are inherited unchanged from `LocalBashExecutor`.
22
+
23
+ ## Install package
24
+
25
+ `npm pack` builds a tarball (`lib/` JS and types, `cordis.patch.yml`, README). Install it into a profile with:
26
+
27
+ ```bash
28
+ npm install ./rtk-for-dsh-dsh-rtk-bash-0.1.0.tgz
29
+ ```
30
+
31
+ The package declares its DeepSeek Harness packages (`dsh-bash-local`, `dsh-bash-sandbox`, `dsh-shell`, `cordis`, `schemastery`) as **optional peers**. They must be resolvable from the profile for the executor to load. npm has them only at `0.0.1-rc.1`, not at the app's runtime version, and their published peer metadata is broken, so npm does not install them for you.
32
+
33
+ - Entries: `rtk-for-dsh` (`RtkBashExecutor`, replaces `dsh-bash-local`) and `rtk-for-dsh/sandbox` (`RtkSandboxBashExecutor`, replaces `dsh-bash-sandbox`).
34
+ - `cordis.patch.yml` is the `dsh.bundle.patch` layer. It disables `bash-sandbox` and inserts `rtk-bash-sandbox`.
35
+
36
+ ```bash
37
+ npm run build # emits lib/; unexpected type errors fail
38
+ DSH_ROOT=/path/to/deepseek-harness npm run typecheck # checks src/ against the DSH sources
39
+ npm test
40
+ ```
41
+
42
+ ## Layout
43
+
44
+ ```
45
+ src/rewrite.ts rtk rewrite client (no DSH dependency; unit-tested)
46
+ src/executor.ts RtkBashExecutor (extends LocalBashExecutor) and RtkSandboxBashExecutor (extends SandboxBashExecutor)
47
+ src/index.ts exports
48
+ src/rewrite.test.ts tests: fake rtk binaries plus the real rtk
49
+ tests/ DSH-side specs (run inside a deepseek-harness workspace)
50
+ ```
51
+
52
+ ## Configuration
53
+
54
+ | Field | Default | Meaning |
55
+ |---|---|---|
56
+ | `rtkEnabled` | `true` | `false` makes the executor behave as `dsh-bash-local`. |
57
+ | `rtkBin` | `rtk` | RTK executable, resolved through PATH. |
58
+ | `rewriteTimeoutMs` | `2000` | Upper bound for one `rtk rewrite` call. |
59
+
60
+ The local bash fields (`cwd`, `timeoutMs`, `maxTimeoutMs`, `maxOutputBytes`, `maxSpillBytes`, `graceMs`) keep their `dsh-bash-local` meanings and defaults.
61
+
62
+ ## Wiring into a DSH composition
63
+
64
+ Replace the executor entry in your profile. Keep the environment registry and the bash tool as they are:
65
+
66
+ ```yaml
67
+ - name: 'rtk-for-dsh' # instead of '@deepseek-ai/dsh-bash-local'
68
+ config:
69
+ rtkEnabled: true
70
+ - name: '@deepseek-ai/dsh-shell-env'
71
+ - name: '@deepseek-ai/dsh-tool-bash'
72
+ ```
73
+
74
+ ## Testing
75
+
76
+ ```bash
77
+ npm test
78
+ ```
79
+
80
+ The suite runs `rtk` for real when it is on PATH, or at `RTK_BIN` when set. Other cases use fake binaries.
81
+
82
+ ## Status and limits
83
+
84
+ - Verified against deepseek-harness sources (commit `fcc0ad4621`, `pnpm install --frozen-lockfile`):
85
+ - `executor.ts` typechecks with `noImplicitOverride` and no errors in its own files or in `packages/shell/*`. The typecheck maps `@deepseek-ai/*` to the workspace sources, using a loose compiler configuration.
86
+ - 6 end-to-end cases in `tests/executor.dsh.spec.ts` pass under DSH's vitest setup. They cover: a rewrite replaces the command that runs; exit 1 runs the original; a denial is refused with exit 126 and the command does not run; a missing rtk falls back to the original; `rtkEnabled: false` skips rtk; a rewritten command's exit code is passed through. A seventh case runs `git status` through the real rtk 0.43.0 and gets rtk's compact output.
87
+ - Through the agent loop: `tests/session.dsh.spec.ts` mounts the executor with the real `dsh-tool-bash`, and a scripted mock model calls `bash`. Results: real rtk returns `git status` in its compact form; a plain `echo` runs normally; a deny verdict reaches the model as stderr `rtk: command refused by an RTK deny rule` with `[exit code: 126]`, and the command does not run. No DeepSeek API key is needed for this test.
88
+ - `tests/sandbox.dsh.spec.ts` runs the sandbox variant through the same mock-model harness: `git status` through real rtk, a plain `echo`, and the deny path all pass.
89
+ - Sandbox confinement is **not** verified. A probe that wrote a file outside the workspace succeeded with rtk disabled and with the stock `SandboxBashExecutor`, so this test harness does not enforce the sandbox. Check confinement in the real desktop app before relying on it.
90
+ - Not verified: a live DeepSeek model session (no API key in this environment), loading the executor from a DSH `cordis.yml` profile, a DSH typecheck after the sandbox-variant change, and DSH's strict `tsconfig.host.json` typecheck.
91
+ - Its `Config` schema repeats the base `LocalBashExecutor` fields because schemastery's object extension is not used here. If the base defaults change, update the copy.
92
+ - The `ask` verdict (exit 3) runs the rewritten command without a prompt, since DSH has no shell permission prompt of its own. Escalation prompts in DSH come from the sandbox path and are not affected.
93
+ - The rewrite runs under the plugin process's environment and rule set, not under the session's workdir. RTK reads its rules from the Claude settings it finds on the machine.
94
+ - Only `rtk rewrite` is used. The installed RTK here is 0.43.0; the checkout at `rtk-projects/rtk` is 0.49.0. Re-run the tests after upgrading.
@@ -0,0 +1,14 @@
1
+ # Patch layer for profiles built on dsh-base: route the model's bash commands
2
+ # through RTK, keeping the sandboxed executor (the one the desktop profile uses).
3
+ # Mounting this layer replaces dsh-bash-sandbox with the RTK-routed variant; the
4
+ # seam allows one executor, so the stock sandbox entry is disabled here.
5
+ - id: bash-sandbox
6
+ disabled: true
7
+
8
+ - insert:
9
+ - id: rtk-bash-sandbox
10
+ name: 'rtk-for-dsh/sandbox'
11
+ config:
12
+ rtkEnabled: true
13
+ rtkBin: rtk
14
+ rewriteTimeoutMs: 2000
@@ -0,0 +1,42 @@
1
+ /**
2
+ * DeepSeek Harness bash executors that route each command through RTK.
3
+ *
4
+ * Two variants share one routing step, so each keeps its base executor's
5
+ * behaviour:
6
+ * - {@link RtkBashExecutor} extends `LocalBashExecutor` (`dsh-bash-local`).
7
+ * - {@link RtkSandboxBashExecutor} extends `SandboxBashExecutor`
8
+ * (`dsh-bash-sandbox`), which the desktop profile mounts. Use this one where
9
+ * file confinement must stay in force.
10
+ *
11
+ * Before a command runs, `rtk rewrite` decides whether it runs as written, runs
12
+ * as an RTK-wrapped equivalent (for example `git status` to `rtk git status`),
13
+ * or is refused by an RTK deny rule. The base executor then runs the chosen
14
+ * command with its own deadline, output, and sandbox handling.
15
+ * @module rtk-for-dsh
16
+ */
17
+ import type { Volatile } from '@deepseek-ai/cordis';
18
+ import type { ShellExecSpec, ShellExecution } from '@deepseek-ai/dsh-shell';
19
+ import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local';
20
+ import type { Config as LocalBashConfig } from '@deepseek-ai/dsh-bash-local';
21
+ import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox';
22
+ /** Plugin settings added to the local bash executor's settings. */
23
+ export interface RtkConfig {
24
+ /** Route commands through `rtk rewrite`; when false the executor behaves as its base. */
25
+ rtkEnabled: Volatile<boolean>;
26
+ /** RTK executable (default `rtk`, resolved through PATH). */
27
+ rtkBin: Volatile<string>;
28
+ /** Upper bound for one `rtk rewrite` call; on expiry the command runs unchanged. */
29
+ rewriteTimeoutMs: Volatile<number>;
30
+ }
31
+ export type Config = LocalBashConfig & RtkConfig;
32
+ export declare class RtkBashExecutor extends LocalBashExecutor {
33
+ static Config: any;
34
+ readonly config: Config;
35
+ execute(spec: ShellExecSpec): Promise<ShellExecution>;
36
+ }
37
+ export declare class RtkSandboxBashExecutor extends SandboxBashExecutor {
38
+ static Config: any;
39
+ readonly config: Config;
40
+ execute(spec: ShellExecSpec): Promise<ShellExecution>;
41
+ }
42
+ export default RtkBashExecutor;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * DeepSeek Harness bash executors that route each command through RTK.
3
+ *
4
+ * Two variants share one routing step, so each keeps its base executor's
5
+ * behaviour:
6
+ * - {@link RtkBashExecutor} extends `LocalBashExecutor` (`dsh-bash-local`).
7
+ * - {@link RtkSandboxBashExecutor} extends `SandboxBashExecutor`
8
+ * (`dsh-bash-sandbox`), which the desktop profile mounts. Use this one where
9
+ * file confinement must stay in force.
10
+ *
11
+ * Before a command runs, `rtk rewrite` decides whether it runs as written, runs
12
+ * as an RTK-wrapped equivalent (for example `git status` to `rtk git status`),
13
+ * or is refused by an RTK deny rule. The base executor then runs the chosen
14
+ * command with its own deadline, output, and sandbox handling.
15
+ * @module rtk-for-dsh
16
+ */
17
+ import z from '@deepseek-ai/schemastery';
18
+ import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local';
19
+ import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox';
20
+ import { rewriteCommand } from './rewrite.js';
21
+ /** Script run in place of a command that an RTK deny rule refused. */
22
+ const DENY_SCRIPT = "printf '%s\\n' 'rtk: command refused by an RTK deny rule' >&2; exit 126";
23
+ /** Settings schema shared by both variants. Mirrors LocalBashExecutor.Config field for field. */
24
+ const configSchema = z.object({
25
+ cwd: z.string().volatile(),
26
+ timeoutMs: z.number().default(120_000).volatile(),
27
+ maxTimeoutMs: z.number().default(600_000).volatile(),
28
+ maxOutputBytes: z.number().default(64_000).volatile(),
29
+ maxSpillBytes: z.number().default(64 * 1024 * 1024).volatile(),
30
+ graceMs: z.number().default(3_000).volatile(),
31
+ rtkEnabled: z.boolean().default(true).volatile(),
32
+ rtkBin: z.string().default('rtk').volatile(),
33
+ rewriteTimeoutMs: z.number().default(2_000).volatile(),
34
+ });
35
+ /**
36
+ * Apply RTK's verdict to a resolved spec. Denied commands become a script that
37
+ * exits 126 without running the original command.
38
+ * @param config - the live RTK settings.
39
+ * @param spec - the resolved spec from the base executor's `resolve()`.
40
+ * @returns the spec to hand to the base executor's `execute()`.
41
+ */
42
+ async function routeSpec(config, spec) {
43
+ if (!config.rtkEnabled.get())
44
+ return spec;
45
+ const verdict = await rewriteCommand(spec.command, {
46
+ bin: config.rtkBin.get(),
47
+ timeoutMs: config.rewriteTimeoutMs.get(),
48
+ signal: spec.signal,
49
+ });
50
+ switch (verdict.kind) {
51
+ case 'rewritten': return { ...spec, command: verdict.command };
52
+ case 'denied': return { ...spec, command: DENY_SCRIPT };
53
+ case 'unchanged': return spec;
54
+ }
55
+ }
56
+ export class RtkBashExecutor extends LocalBashExecutor {
57
+ // schemastery has no object-extension helper, so the base fields are repeated in configSchema.
58
+ static Config = configSchema;
59
+ async execute(spec) {
60
+ return super.execute(await routeSpec(this.config, spec));
61
+ }
62
+ }
63
+ export class RtkSandboxBashExecutor extends SandboxBashExecutor {
64
+ static Config = configSchema;
65
+ async execute(spec) {
66
+ return super.execute(await routeSpec(this.config, spec));
67
+ }
68
+ }
69
+ export default RtkBashExecutor;
package/lib/index.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Package entry. The default export is {@link RtkBashExecutor}, which replaces
3
+ * `dsh-bash-local`. For the sandboxed executor, use `rtk-for-dsh/sandbox`.
4
+ * @module rtk-for-dsh
5
+ */
6
+ export { rewriteCommand } from './rewrite.ts';
7
+ export type { RewriteOptions, RewriteVerdict } from './rewrite.ts';
8
+ export { RtkBashExecutor, RtkSandboxBashExecutor, default } from './executor.ts';
9
+ export type { Config, RtkConfig } from './executor.ts';
package/lib/index.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Package entry. The default export is {@link RtkBashExecutor}, which replaces
3
+ * `dsh-bash-local`. For the sandboxed executor, use `rtk-for-dsh/sandbox`.
4
+ * @module rtk-for-dsh
5
+ */
6
+ export { rewriteCommand } from './rewrite.js';
7
+ export { RtkBashExecutor, RtkSandboxBashExecutor, default } from './executor.js';
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Client for `rtk rewrite`, the single rewrite registry RTK exposes to
3
+ * delegates. The exit code carries the policy verdict:
4
+ *
5
+ * | Exit | Stdout | Meaning |
6
+ * |------|-----------|------------------------------------------------------|
7
+ * | 0 | rewritten | Rewrite; the allow rules cover the command. |
8
+ * | 1 | (none) | No rewrite applies; run the command as written. |
9
+ * | 2 | (none) | A deny rule matched; do not run the command. |
10
+ * | 3 | rewritten | Rewrite; the host would normally ask (default case). |
11
+ *
12
+ * Any other outcome (missing binary, timeout, abort, unexpected exit code)
13
+ * resolves to `unchanged`, so the original command runs. RTK is an output
14
+ * filter; its absence must not block the agent.
15
+ */
16
+ export type RewriteVerdict = {
17
+ kind: 'unchanged';
18
+ } | {
19
+ kind: 'rewritten';
20
+ command: string;
21
+ } | {
22
+ kind: 'denied';
23
+ };
24
+ export interface RewriteOptions {
25
+ /** RTK executable, resolved through PATH when not absolute. */
26
+ bin: string;
27
+ /** Upper bound for the `rtk rewrite` process. */
28
+ timeoutMs: number;
29
+ /** Cancels the rewrite; an aborted rewrite resolves to `unchanged`. */
30
+ signal?: AbortSignal | undefined;
31
+ }
32
+ /**
33
+ * Ask RTK how to run `command`.
34
+ * @param command - the shell command exactly as the agent wrote it.
35
+ * @param options - binary, timeout and cancellation.
36
+ * @returns the verdict; never rejects.
37
+ */
38
+ export declare function rewriteCommand(command: string, options: RewriteOptions): Promise<RewriteVerdict>;
package/lib/rewrite.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Client for `rtk rewrite`, the single rewrite registry RTK exposes to
3
+ * delegates. The exit code carries the policy verdict:
4
+ *
5
+ * | Exit | Stdout | Meaning |
6
+ * |------|-----------|------------------------------------------------------|
7
+ * | 0 | rewritten | Rewrite; the allow rules cover the command. |
8
+ * | 1 | (none) | No rewrite applies; run the command as written. |
9
+ * | 2 | (none) | A deny rule matched; do not run the command. |
10
+ * | 3 | rewritten | Rewrite; the host would normally ask (default case). |
11
+ *
12
+ * Any other outcome (missing binary, timeout, abort, unexpected exit code)
13
+ * resolves to `unchanged`, so the original command runs. RTK is an output
14
+ * filter; its absence must not block the agent.
15
+ */
16
+ import { execFile } from 'node:child_process';
17
+ const EXIT_ALLOW = 0;
18
+ const EXIT_DENY = 2;
19
+ const EXIT_ASK = 3;
20
+ /**
21
+ * Ask RTK how to run `command`.
22
+ * @param command - the shell command exactly as the agent wrote it.
23
+ * @param options - binary, timeout and cancellation.
24
+ * @returns the verdict; never rejects.
25
+ */
26
+ export function rewriteCommand(command, options) {
27
+ return new Promise((resolve) => {
28
+ execFile(options.bin, ['rewrite', command], {
29
+ timeout: options.timeoutMs,
30
+ encoding: 'utf8',
31
+ windowsHide: true,
32
+ ...options.signal !== undefined ? { signal: options.signal } : {},
33
+ }, (error, stdout) => {
34
+ const code = exitCodeOf(error);
35
+ if (code === EXIT_DENY) {
36
+ resolve({ kind: 'denied' });
37
+ return;
38
+ }
39
+ if (code === EXIT_ALLOW || code === EXIT_ASK) {
40
+ const rewritten = stdout.replace(/\r?\n$/, '');
41
+ resolve(rewritten.length > 0 && rewritten !== command
42
+ ? { kind: 'rewritten', command: rewritten }
43
+ : { kind: 'unchanged' });
44
+ return;
45
+ }
46
+ // Exit 1 (no rewrite), or no numeric exit code (spawn failure, timeout, abort).
47
+ resolve({ kind: 'unchanged' });
48
+ });
49
+ });
50
+ }
51
+ /** The numeric exit code of a finished `execFile`, or `undefined` for spawn failures and signals. */
52
+ function exitCodeOf(error) {
53
+ if (error === null)
54
+ return EXIT_ALLOW;
55
+ const code = error.code;
56
+ return typeof code === 'number' ? code : undefined;
57
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Sandboxed entry point: the default export is {@link RtkSandboxBashExecutor},
3
+ * for profiles that mount `dsh-bash-sandbox` (the desktop profile does).
4
+ * @module rtk-for-dsh/sandbox
5
+ */
6
+ export { RtkSandboxBashExecutor, default } from './executor.ts';
7
+ export type { Config, RtkConfig } from './executor.ts';
package/lib/sandbox.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Sandboxed entry point: the default export is {@link RtkSandboxBashExecutor},
3
+ * for profiles that mount `dsh-bash-sandbox` (the desktop profile does).
4
+ * @module rtk-for-dsh/sandbox
5
+ */
6
+ export { RtkSandboxBashExecutor, default } from './executor.js';
package/package.json CHANGED
@@ -1,6 +1,69 @@
1
1
  {
2
2
  "name": "rtk-for-dsh",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "DeepSeek Harness bash executor that routes commands through RTK (rtk rewrite)",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./lib/index.d.ts",
10
+ "default": "./lib/index.js"
11
+ },
12
+ "./sandbox": {
13
+ "types": "./lib/sandbox.d.ts",
14
+ "default": "./lib/sandbox.js"
15
+ },
16
+ "./cordis.patch.yml": "./cordis.patch.yml",
17
+ "./package.json": "./package.json"
18
+ },
19
+ "scripts": {
20
+ "build": "node scripts/build.mjs",
21
+ "typecheck": "node scripts/typecheck-dsh.mjs",
22
+ "test": "node --test src/*.test.ts",
23
+ "prepack": "npm run build"
24
+ },
25
+ "engines": {
26
+ "node": ">=24"
27
+ },
28
+ "peerDependencies": {
29
+ "@deepseek-ai/cordis": "*",
30
+ "@deepseek-ai/dsh-bash-local": "*",
31
+ "@deepseek-ai/dsh-shell": "*",
32
+ "@deepseek-ai/schemastery": "*",
33
+ "@deepseek-ai/dsh-bash-sandbox": "*"
34
+ },
35
+ "license": "MIT",
36
+ "peerDependenciesMeta": {
37
+ "@deepseek-ai/cordis": {
38
+ "optional": true
39
+ },
40
+ "@deepseek-ai/dsh-bash-local": {
41
+ "optional": true
42
+ },
43
+ "@deepseek-ai/dsh-shell": {
44
+ "optional": true
45
+ },
46
+ "@deepseek-ai/schemastery": {
47
+ "optional": true
48
+ },
49
+ "@deepseek-ai/dsh-bash-sandbox": {
50
+ "optional": true
51
+ }
52
+ },
53
+ "types": "lib/index.d.ts",
54
+ "files": [
55
+ "lib/**/*.js",
56
+ "lib/**/*.d.ts",
57
+ "cordis.patch.yml",
58
+ "README.md"
59
+ ],
60
+ "dsh": {
61
+ "bundle": {
62
+ "patch": "./cordis.patch.yml"
63
+ }
64
+ },
65
+ "devDependencies": {
66
+ "@types/node": "^26.6.6",
67
+ "typescript": "^7.0.2"
68
+ }
69
+ }