@gpzhang2001/sharpkit-shell 0.2.1 → 0.2.2
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/lib/index.d.ts.map +1 -1
- package/lib/index.js +22 -4
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +45 -3
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.ts","names":["Schema"],"sources":["../src/index.d.ts"],"sourcesContent":["/**\n * `exec_command` / `write_stdin` model-facing shell tools over the pentest\n * sandbox — strix SDK ShellTool semantics (tools/shell/README.md + factory\n * wrapping): a fresh non-interactive login shell per call by default, PTY\n * processes for interactive work driven by `write_stdin` (chars=\"\" polls),\n * workdir restricted to the workspace, per-call timeout clamped by config,\n * and `run_in_background` mapped onto the dsh jobs runtime\n * (job_output/job_list/job_kill come free). Every exec passes an ask\n * approval gate (config-gated; the registry resolves `ask` through the\n * host's approval service and fails closed when none is composed).\n * @module @gpzhang2001/sharpkit-shell\n */\nimport type { Context } from '@deepseek-ai/cordis';\nimport type Schema from '@deepseek-ai/schemastery';\n/** strix's exact refusal text for write_stdin against a non-PTY process. */\nexport declare const STDIN_UNAVAILABLE = \"stdin is not available for this process. Start the command with 'tty=true' in 'exec_command' before using 'write_stdin'.\";\n/** One source tree Config entry (mutable twin of the sandbox spec for schemastery). */\nexport interface ConfigSource {\n workspaceSubdir: string;\n sourcePath: string;\n protectMetadata?: boolean;\n}\n/** Deployment-tunable configuration. */\nexport interface Config {\n /** Sandbox session cache key; the orchestrator pre-creates the real session under this id. */\n readonly scanId?: string;\n /** Source trees for the session this tool creates when none exists yet. */\n readonly sources?: ConfigSource[];\n
|
|
1
|
+
{"version":3,"file":"index.ts","names":["Schema"],"sources":["../src/index.d.ts"],"sourcesContent":["/**\n * `exec_command` / `write_stdin` model-facing shell tools over the pentest\n * sandbox — strix SDK ShellTool semantics (tools/shell/README.md + factory\n * wrapping): a fresh non-interactive login shell per call by default, PTY\n * processes for interactive work driven by `write_stdin` (chars=\"\" polls),\n * workdir restricted to the workspace, per-call timeout clamped by config,\n * and `run_in_background` mapped onto the dsh jobs runtime\n * (job_output/job_list/job_kill come free). Every exec passes an ask\n * approval gate (config-gated; the registry resolves `ask` through the\n * host's approval service and fails closed when none is composed), except\n * under the `'never'` approval policy — there the gate is skipped because\n * an ask can never be granted (the danger-full-access pre-trust stance).\n * @module @gpzhang2001/sharpkit-shell\n */\nimport type { Context } from '@deepseek-ai/cordis';\nimport type Schema from '@deepseek-ai/schemastery';\n/** strix's exact refusal text for write_stdin against a non-PTY process. */\nexport declare const STDIN_UNAVAILABLE = \"stdin is not available for this process. Start the command with 'tty=true' in 'exec_command' before using 'write_stdin'.\";\n/** One source tree Config entry (mutable twin of the sandbox spec for schemastery). */\nexport interface ConfigSource {\n workspaceSubdir: string;\n sourcePath: string;\n protectMetadata?: boolean;\n}\n/** Deployment-tunable configuration. */\nexport interface Config {\n /** Sandbox session cache key; the orchestrator pre-creates the real session under this id. */\n readonly scanId?: string;\n /** Source trees for the session this tool creates when none exists yet. */\n readonly sources?: ConfigSource[];\n /**\n * Ask-approval gate on every exec_command (fail-closed without an approval\n * service). Skipped automatically while the approval policy is `'never'` —\n * an ask there is a guaranteed auto-rejection, not an approval request.\n */\n readonly requireApproval?: boolean;\n /** Expose run_in_background (needs a jobs provider at runtime). */\n readonly enableRunInBackground?: boolean;\n /** Default per-call timeout; model values are clamped into [1, maxTimeoutMs]. */\n readonly defaultTimeoutMs?: number;\n /** Hard ceiling for the model's timeout_ms and the tool's own deadline. */\n readonly maxTimeoutMs?: number;\n /** Container workspace root (workdir validation). */\n readonly workspaceRoot?: string;\n /** Unpolled PTY output ceiling in bytes (approx; the tail is kept). */\n readonly maxTtyBufferBytes?: number;\n}\ndeclare module '@deepseek-ai/dsh-jobs' {\n interface JobKindMap {\n 'pentest-exec': 'pentest-exec';\n }\n}\nexport declare const name = \"pentest-tool-shell\";\nexport declare const inject: string[];\nexport declare const Config: Schema<Config>;\n/**\n * Validate a workdir against the workspace root (strix workdir contract),\n * segment by segment: no absolute escape, no `..`/`.`/empty segments, no\n * trailing slash.\n * @param cwd - the requested working directory.\n * @param workspaceRoot - container workspace root.\n * @returns null when valid, else strix's exact error text.\n */\nexport declare function workdirError(cwd: string | undefined, workspaceRoot: string): string | null;\n/** Clamp a requested timeout into the configured window. */\nexport declare function clampTimeout(requested: number | undefined, defaultTimeoutMs: number, maxTimeoutMs: number): number;\nexport declare function apply(ctx: Context, config?: Config): void;\n"],"mappings":";;;AAEA,IAAW,CAAC,qBAAqB;CAAC;OAAS,CAAC;CAAG,CAAC;AAAC;AACjD,IAAW,CAAC,gBAAgB;CAAC;OAAS,CAAC;CAAG;EAAC;EAAI;EAAI;CAAE;AAAC;AACtD,IAAW,CAAC,UAAU;CAAC;OAAS,CAAC,YAAY;CAAG;EAAC;EAAI;EAAI;EAAI;EAAI;EAAI;EAAI;EAAI;EAAI;CAAE;AAAC;AACpF,IAAI,CAAC,MAAM;CAAC;OAAS,CAAC;CAAG,CAAC,IAAI,EAAE;CAAG,WAAW,EAAE;AAAC;AACjD,IAAW,CAAC,QAAQ;CAAC;OAAS,CAAC;CAAG,CAAC;AAAC;AACpC,IAAW,CAAC,UAAU;CAAC;OAAS,CAAC;CAAG,CAAC;AAAC;AACtC,IAAW,CAAC,UAAU;CAAC;OAAS,CAAC,QAAQA,CAAM;CAAG,CAAC,IAAI,EAAE;AAAC;AAC1D,IAAW,CAAC,gBAAgB;CAAC;OAAS,CAAC;CAAG,CAAC,IAAI,EAAE;AAAC;AAClD,IAAW,CAAC,gBAAgB;CAAC;OAAS,CAAC;CAAG;EAAC;EAAI;EAAI;CAAE;AAAC;AACtD,IAAW,CAAC,SAAS;CAAC;OAAS,CAAC,SAAS,MAAM;CAAG;EAAC;EAAI;EAAI;EAAI;CAAE;AAAC"}
|
package/lib/index.js
CHANGED
|
@@ -36,6 +36,21 @@ function decodeCharsEscape(chars) {
|
|
|
36
36
|
//#region src/index.ts
|
|
37
37
|
/** strix's exact refusal text for write_stdin against a non-PTY process. */
|
|
38
38
|
const STDIN_UNAVAILABLE = "stdin is not available for this process. Start the command with 'tty=true' in 'exec_command' before using 'write_stdin'.";
|
|
39
|
+
/**
|
|
40
|
+
* Whether the session's approval policy is `'never'` — the deterministic
|
|
41
|
+
* stance where every ask resolves `rejected` without consulting anyone (set
|
|
42
|
+
* by the danger-full-access preset, i.e. a user who pre-trusted the session).
|
|
43
|
+
* An ask under that policy can never be granted — and its denial is even
|
|
44
|
+
* reported as "the user rejected tool" although no user was consulted — so
|
|
45
|
+
* gates treat it as pre-approval instead of asking: sandbox commands are a
|
|
46
|
+
* strict subset of the host bash the same preset already unleashed.
|
|
47
|
+
*/
|
|
48
|
+
function policyIsNever(ctx, session) {
|
|
49
|
+
if (session === void 0) return false;
|
|
50
|
+
const approval = ctx.get("approval");
|
|
51
|
+
if (approval === void 0) return false;
|
|
52
|
+
return (approval.overrideOf(session) ?? approval.config?.policy ?? "ask") === "never";
|
|
53
|
+
}
|
|
39
54
|
const name = "pentest-tool-shell";
|
|
40
55
|
const inject = ["tools", "pentestSandbox"];
|
|
41
56
|
const Config = z.object({
|
|
@@ -81,10 +96,13 @@ function apply(ctx, config = {}) {
|
|
|
81
96
|
})) } : {}
|
|
82
97
|
});
|
|
83
98
|
if (config.requireApproval !== false) ctx.on("tools/pre-execute", async (exec, next) => {
|
|
84
|
-
if (exec.name === "exec_command")
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
if (exec.name === "exec_command") {
|
|
100
|
+
if (policyIsNever(ctx, exec.agent?.session)) return next();
|
|
101
|
+
return {
|
|
102
|
+
kind: "ask",
|
|
103
|
+
reason: "pentest: run a command inside the attack sandbox"
|
|
104
|
+
};
|
|
105
|
+
}
|
|
88
106
|
return next();
|
|
89
107
|
});
|
|
90
108
|
const workspaceRoot = config.workspaceRoot ?? "/workspace";
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/chars.ts","../src/index.ts"],"sourcesContent":["/**\n * Escape decoding for `write_stdin` chars, ported verbatim from strix\n * factory.py `_decode_chars_escape` (\\\\uXXXX, \\\\xNN, and the C-style short\n * escapes; unknown escapes and backslash-free strings pass through).\n * @module @gpzhang2001/sharpkit-shell/chars\n */\n\nconst ESCAPE_RE = /\\\\(?:u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[0abtnvfr\\\\])/g\n\nconst SHORT_ESCAPES: Record<string, string> = {\n '\\\\\\\\': '\\\\',\n '\\\\n': '\\n',\n '\\\\t': '\\t',\n '\\\\r': '\\r',\n '\\\\0': '\\x00',\n '\\\\a': '\\x07',\n '\\\\b': '\\x08',\n '\\\\v': '\\x0b',\n '\\\\f': '\\x0c',\n}\n\n/**\n * Decode the model-facing escape forms in a `chars` argument.\n * @param chars - the raw argument value.\n * @returns the decoded characters to deliver to the process.\n */\nexport function decodeCharsEscape(chars: string): string {\n if (!chars.includes('\\\\')) return chars\n return chars.replace(ESCAPE_RE, token => {\n const short = SHORT_ESCAPES[token]\n if (short !== undefined) return short\n return String.fromCodePoint(Number.parseInt(token.slice(2), 16))\n })\n}\n","/**\n * `exec_command` / `write_stdin` model-facing shell tools over the pentest\n * sandbox — strix SDK ShellTool semantics (tools/shell/README.md + factory\n * wrapping): a fresh non-interactive login shell per call by default, PTY\n * processes for interactive work driven by `write_stdin` (chars=\"\" polls),\n * workdir restricted to the workspace, per-call timeout clamped by config,\n * and `run_in_background` mapped onto the dsh jobs runtime\n * (job_output/job_list/job_kill come free). Every exec passes an ask\n * approval gate (config-gated; the registry resolves `ask` through the\n * host's approval service and fails closed when none is composed).\n * @module @gpzhang2001/sharpkit-shell\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport type Schema from '@deepseek-ai/schemastery'\nimport z from '@deepseek-ai/schemastery'\nimport { defineTool } from '@deepseek-ai/dsh-tools'\nimport type { SandboxTtyProcess } from '@gpzhang2001/sharpkit-sandbox'\nimport { decodeCharsEscape } from './chars.ts'\n\n/** strix's exact refusal text for write_stdin against a non-PTY process. */\nexport const STDIN_UNAVAILABLE = \"stdin is not available for this process. Start the command with 'tty=true' in 'exec_command' before using 'write_stdin'.\"\n\n/** One source tree Config entry (mutable twin of the sandbox spec for schemastery). */\nexport interface ConfigSource {\n workspaceSubdir: string\n sourcePath: string\n protectMetadata?: boolean\n}\n\n/** Deployment-tunable configuration. */\nexport interface Config {\n /** Sandbox session cache key; the orchestrator pre-creates the real session under this id. */\n readonly scanId?: string\n /** Source trees for the session this tool creates when none exists yet. */\n readonly sources?: ConfigSource[]\n /** Ask-approval gate on every exec_command (fail-closed without an approval service). */\n readonly requireApproval?: boolean\n /** Expose run_in_background (needs a jobs provider at runtime). */\n readonly enableRunInBackground?: boolean\n /** Default per-call timeout; model values are clamped into [1, maxTimeoutMs]. */\n readonly defaultTimeoutMs?: number\n /** Hard ceiling for the model's timeout_ms and the tool's own deadline. */\n readonly maxTimeoutMs?: number\n /** Container workspace root (workdir validation). */\n readonly workspaceRoot?: string\n /** Unpolled PTY output ceiling in bytes (approx; the tail is kept). */\n readonly maxTtyBufferBytes?: number\n}\n\nimport type {} from '@deepseek-ai/dsh-jobs'\n\ndeclare module '@deepseek-ai/dsh-jobs' {\n interface JobKindMap {\n 'pentest-exec': 'pentest-exec'\n }\n}\n\n/** One tracked PTY process with a read cursor for polling. */\ninterface TrackedTty {\n readonly process: SandboxTtyProcess\n buffer: string\n cursor: number\n settled: { exitCode: number | null; signal: string | null } | undefined\n}\n\nexport const name = 'pentest-tool-shell'\n\nexport const inject = ['tools', 'pentestSandbox']\n\nexport const Config: Schema<Config> = z.object({\n scanId: z.string().default('pentest'),\n sources: z.array(z.object({\n workspaceSubdir: z.string(),\n sourcePath: z.string(),\n protectMetadata: z.boolean(),\n })),\n requireApproval: z.boolean().default(true),\n enableRunInBackground: z.boolean().default(true),\n defaultTimeoutMs: z.number().default(120_000),\n maxTimeoutMs: z.number().default(600_000),\n workspaceRoot: z.string().default('/workspace'),\n maxTtyBufferBytes: z.number().default(1_048_576),\n})\n\n/**\n * Validate a workdir against the workspace root (strix workdir contract),\n * segment by segment: no absolute escape, no `..`/`.`/empty segments, no\n * trailing slash.\n * @param cwd - the requested working directory.\n * @param workspaceRoot - container workspace root.\n * @returns null when valid, else strix's exact error text.\n */\nexport function workdirError(cwd: string | undefined, workspaceRoot: string): string | null {\n if (cwd === undefined) return null\n if (cwd === workspaceRoot) return null\n const prefix = `${workspaceRoot}/`\n const inside = cwd.startsWith(prefix)\n && cwd.slice(prefix.length).split('/').every(segment => segment !== '' && segment !== '.' && segment !== '..')\n return inside ? null : `exec_command: workdir must be a path inside ${workspaceRoot} (or omitted to use the turn's cwd). Got: '${cwd}'.`\n}\n\n/** Clamp a requested timeout into the configured window. */\nexport function clampTimeout(requested: number | undefined, defaultTimeoutMs: number, maxTimeoutMs: number): number {\n const value = requested ?? defaultTimeoutMs\n return Math.min(Math.max(1, value), maxTimeoutMs)\n}\n\nexport function apply(ctx: Context, config: Config = {}): void {\n const ttyProcesses = new Map<string, TrackedTty>()\n\n const session = () =>\n ctx.pentestSandbox.createSession({\n scanId: config.scanId ?? 'pentest',\n ...(config.sources !== undefined ? { sources: config.sources.map(source => ({\n workspaceSubdir: source.workspaceSubdir,\n sourcePath: source.sourcePath,\n ...(source.protectMetadata !== undefined ? { protectMetadata: source.protectMetadata } : {}),\n })) } : {}),\n })\n\n if (config.requireApproval !== false) {\n void ctx.on('tools/pre-execute', async (exec, next) => {\n if (exec.name === 'exec_command') return { kind: 'ask' as const, reason: 'pentest: run a command inside the attack sandbox' }\n return next()\n })\n }\n\n const workspaceRoot = config.workspaceRoot ?? '/workspace'\n const defaultTimeoutMs = config.defaultTimeoutMs ?? 120_000\n const maxTimeoutMs = config.maxTimeoutMs ?? 600_000\n const maxTtyBufferBytes = config.maxTtyBufferBytes ?? 1_048_576\n const backgroundEnabled = config.enableRunInBackground !== false\n\n ctx.tools.register(defineTool({\n name: 'exec_command',\n description: `Run a shell command inside the pentest sandbox (Kali container) in a fresh non-interactive bash login shell. Set tty=true for interactive programs (REPLs, ssh, msfconsole) and then drive them with write_stdin; run_in_background starts a job for long scans and returns a job id for job_output/job_kill.${backgroundEnabled ? '' : ' (run_in_background is disabled in this deployment.)'}`,\n parameters: {\n cmd: { type: 'string', required: true, description: 'The shell command line to execute.' },\n ...(backgroundEnabled ? { run_in_background: { type: 'boolean', description: 'Start as a background job; returns a job id (long scans).' } } : {}),\n cwd: { type: 'string', description: `Working directory inside ${workspaceRoot}.` },\n timeout_ms: { type: 'integer', description: 'Per-call timeout in milliseconds (clamped by deployment).' },\n tty: { type: 'boolean', description: 'Run under a PTY for interactive input via write_stdin.' },\n },\n output: {\n schema: {\n type: 'object',\n properties: {\n kind: { type: 'string', required: true, enum: ['foreground', 'tty', 'background'] },\n exit_code: { type: 'integer' },\n signal: { type: 'string' },\n timed_out: { type: 'boolean' },\n stdout: { type: 'string' },\n stderr: { type: 'string' },\n process_id: { type: 'string' },\n job_id: { type: 'string' },\n },\n additionalProperties: false,\n },\n render: (_args, value) => {\n if (value.kind === 'foreground') {\n return [{ type: 'text', text: `exit_code=${value.exit_code === null ? 'none' : String(value.exit_code)}${value.timed_out === true ? ' (timed out)' : ''}\\nstdout:\\n${value.stdout}\\nstderr:\\n${value.stderr}` }]\n }\n if (value.kind === 'tty') {\n return [{ type: 'text', text: `PTY process ${value.process_id} started. Poll with write_stdin(process_id=\"${value.process_id}\", chars=\"\"); send input via chars; deliver Ctrl-C with chars=\"\\\\x03\".` }]\n }\n return [{ type: 'text', text: `started background job ${value.job_id} (job_output to read, job_kill to stop)` }]\n },\n },\n timeoutMs: maxTimeoutMs,\n presentCall: args => ({ card: 'terminal', title: String(args.cmd ?? '') }),\n presentResult: (args, result) => {\n const text = result.content.filter(block => block.type === 'text').map(block => block.text).join('\\n')\n return { card: 'terminal', output: text === '' ? String(args.cmd ?? '') : text }\n },\n execute: async (args, exec) => {\n const cwdProblem = workdirError(args.cwd, workspaceRoot)\n if (cwdProblem !== null) throw new Error(cwdProblem)\n if (args.tty === true && args.run_in_background === true) {\n throw new Error('exec_command: tty=true and run_in_background cannot be combined — start a PTY process and drive it with write_stdin, or start a background job without a PTY.')\n }\n if (args.run_in_background === true && !backgroundEnabled) throw new Error('run_in_background is disabled for this deployment (enableRunInBackground: false)')\n const sandbox = await session()\n if (args.tty === true) {\n const process = await sandbox.execTty(args.cmd, { cwd: args.cwd })\n const tracked: TrackedTty = { process, buffer: '', cursor: 0, settled: undefined }\n process.subscribe(chunk => {\n tracked.buffer += chunk\n // Bound unpolled output; keep the tail and re-base the read cursor.\n if (tracked.buffer.length > maxTtyBufferBytes) {\n const excess = tracked.buffer.length - maxTtyBufferBytes\n tracked.buffer = tracked.buffer.slice(excess)\n tracked.cursor = Math.max(0, tracked.cursor - excess)\n }\n })\n void process.done.then(\n outcome => {\n tracked.settled = { exitCode: outcome.exitCode, signal: outcome.signal }\n },\n () => {\n tracked.settled = { exitCode: null, signal: null }\n },\n )\n ttyProcesses.set(process.id, tracked)\n return { kind: 'tty' as const, process_id: process.id }\n }\n if (args.run_in_background === true) {\n const jobs = ctx.get('jobs')\n if (jobs === undefined) throw new Error('run_in_background needs a jobs provider (load @deepseek-ai/dsh-jobs-local)')\n const job = sandbox.execJob(args.cmd, { cwd: args.cwd })\n let cancelled = false\n const jobId = jobs.start({\n kind: 'pentest-exec',\n label: args.cmd,\n ...exec.agent !== undefined ? { owner: exec.agent } : {},\n run: () => ({\n cancel: () => {\n cancelled = true\n job.terminate()\n },\n done: job.done.then(outcome =>\n // A killed docker-exec tree can still report exit code 0; the\n // cancellation flag is the authoritative kill signal.\n cancelled\n ? { status: 'killed' as const, detail: 'cancelled by request' }\n : {\n status: outcome.exitCode === 0 ? 'completed' as const : 'failed' as const,\n detail: `exit code: ${outcome.exitCode === null ? 'none' : String(outcome.exitCode)}`,\n }),\n readOutput: () => job.readOutput(),\n }),\n })\n return { kind: 'background' as const, job_id: jobId }\n }\n const result = await sandbox.exec(args.cmd, { cwd: args.cwd, timeoutMs: clampTimeout(args.timeout_ms, defaultTimeoutMs, maxTimeoutMs), signal: exec.signal })\n return {\n kind: 'foreground' as const,\n ...(result.exitCode !== null ? { exit_code: result.exitCode } : {}),\n ...(result.signal !== null ? { signal: result.signal } : {}),\n timed_out: result.timedOut,\n stdout: result.stdout,\n stderr: result.stderr,\n }\n },\n }))\n\n ctx.tools.register(defineTool({\n name: 'write_stdin',\n description: 'Send characters to a PTY-backed process started by exec_command(tty=true). chars=\"\" polls for new output without sending anything. Escapes (\\\\n, \\\\t, \\\\x03 for Ctrl-C, \\\\uXXXX) are decoded.',\n parameters: {\n process_id: { type: 'string', required: true, description: 'The process id returned by exec_command(tty=true).' },\n chars: { type: 'string', required: true, description: 'Characters to write (\"\" to poll for output only).' },\n },\n output: {\n schema: {\n type: 'object',\n properties: {\n output: { type: 'string', required: true },\n running: { type: 'boolean', required: true },\n exit_code: { type: 'integer' },\n },\n additionalProperties: false,\n },\n render: (_args, value) => [{\n type: 'text',\n text: `${value.running ? '(running)' : `(exited${value.exit_code === undefined ? '' : ` exit_code=${String(value.exit_code)}`})`}\\n${value.output}`,\n }],\n },\n execute: async args => {\n const tracked = ttyProcesses.get(args.process_id)\n if (tracked === undefined) throw new Error(STDIN_UNAVAILABLE)\n const chars = decodeCharsEscape(args.chars)\n if (chars !== '') await tracked.process.write(chars)\n const delta = tracked.buffer.slice(tracked.cursor)\n tracked.cursor = tracked.buffer.length\n if (tracked.settled !== undefined) ttyProcesses.delete(args.process_id)\n return {\n output: delta,\n running: tracked.settled === undefined,\n ...(tracked.settled !== undefined && tracked.settled.exitCode !== null ? { exit_code: tracked.settled.exitCode } : {}),\n }\n },\n }))\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/chars.ts","../src/index.ts"],"sourcesContent":["/**\n * Escape decoding for `write_stdin` chars, ported verbatim from strix\n * factory.py `_decode_chars_escape` (\\\\uXXXX, \\\\xNN, and the C-style short\n * escapes; unknown escapes and backslash-free strings pass through).\n * @module @gpzhang2001/sharpkit-shell/chars\n */\n\nconst ESCAPE_RE = /\\\\(?:u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[0abtnvfr\\\\])/g\n\nconst SHORT_ESCAPES: Record<string, string> = {\n '\\\\\\\\': '\\\\',\n '\\\\n': '\\n',\n '\\\\t': '\\t',\n '\\\\r': '\\r',\n '\\\\0': '\\x00',\n '\\\\a': '\\x07',\n '\\\\b': '\\x08',\n '\\\\v': '\\x0b',\n '\\\\f': '\\x0c',\n}\n\n/**\n * Decode the model-facing escape forms in a `chars` argument.\n * @param chars - the raw argument value.\n * @returns the decoded characters to deliver to the process.\n */\nexport function decodeCharsEscape(chars: string): string {\n if (!chars.includes('\\\\')) return chars\n return chars.replace(ESCAPE_RE, token => {\n const short = SHORT_ESCAPES[token]\n if (short !== undefined) return short\n return String.fromCodePoint(Number.parseInt(token.slice(2), 16))\n })\n}\n","/**\n * `exec_command` / `write_stdin` model-facing shell tools over the pentest\n * sandbox — strix SDK ShellTool semantics (tools/shell/README.md + factory\n * wrapping): a fresh non-interactive login shell per call by default, PTY\n * processes for interactive work driven by `write_stdin` (chars=\"\" polls),\n * workdir restricted to the workspace, per-call timeout clamped by config,\n * and `run_in_background` mapped onto the dsh jobs runtime\n * (job_output/job_list/job_kill come free). Every exec passes an ask\n * approval gate (config-gated; the registry resolves `ask` through the\n * host's approval service and fails closed when none is composed), except\n * under the `'never'` approval policy — there the gate is skipped because\n * an ask can never be granted (the danger-full-access pre-trust stance).\n * @module @gpzhang2001/sharpkit-shell\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport type Schema from '@deepseek-ai/schemastery'\nimport z from '@deepseek-ai/schemastery'\nimport { defineTool } from '@deepseek-ai/dsh-tools'\nimport type { SandboxTtyProcess } from '@gpzhang2001/sharpkit-sandbox'\nimport { decodeCharsEscape } from './chars.ts'\n\n/** strix's exact refusal text for write_stdin against a non-PTY process. */\nexport const STDIN_UNAVAILABLE = \"stdin is not available for this process. Start the command with 'tty=true' in 'exec_command' before using 'write_stdin'.\"\n\n/** One source tree Config entry (mutable twin of the sandbox spec for schemastery). */\nexport interface ConfigSource {\n workspaceSubdir: string\n sourcePath: string\n protectMetadata?: boolean\n}\n\n/**\n * Structural view of the host approval service (`ctx.approval`) — reads the\n * session's effective approval policy without a hard dependency on\n * `@deepseek-ai/dsh-user-approval`, using only stock public API members\n * (`overrideOf` + the plugin's configured default; same pattern as the\n * preset lookup in tool-proxy).\n */\ninterface ApprovalPolicySource {\n overrideOf: (session: object) => 'ask' | 'never' | undefined\n config?: { policy?: 'ask' | 'never' }\n}\n\n/**\n * Whether the session's approval policy is `'never'` — the deterministic\n * stance where every ask resolves `rejected` without consulting anyone (set\n * by the danger-full-access preset, i.e. a user who pre-trusted the session).\n * An ask under that policy can never be granted — and its denial is even\n * reported as \"the user rejected tool\" although no user was consulted — so\n * gates treat it as pre-approval instead of asking: sandbox commands are a\n * strict subset of the host bash the same preset already unleashed.\n */\nfunction policyIsNever(ctx: Context, session: object | undefined): boolean {\n if (session === undefined) return false\n const approval = ctx.get('approval') as ApprovalPolicySource | undefined\n if (approval === undefined) return false\n return (approval.overrideOf(session) ?? approval.config?.policy ?? 'ask') === 'never'\n}\n\n/** Deployment-tunable configuration. */\nexport interface Config {\n /** Sandbox session cache key; the orchestrator pre-creates the real session under this id. */\n readonly scanId?: string\n /** Source trees for the session this tool creates when none exists yet. */\n readonly sources?: ConfigSource[]\n /**\n * Ask-approval gate on every exec_command (fail-closed without an approval\n * service). Skipped automatically while the approval policy is `'never'` —\n * an ask there is a guaranteed auto-rejection, not an approval request.\n */\n readonly requireApproval?: boolean\n /** Expose run_in_background (needs a jobs provider at runtime). */\n readonly enableRunInBackground?: boolean\n /** Default per-call timeout; model values are clamped into [1, maxTimeoutMs]. */\n readonly defaultTimeoutMs?: number\n /** Hard ceiling for the model's timeout_ms and the tool's own deadline. */\n readonly maxTimeoutMs?: number\n /** Container workspace root (workdir validation). */\n readonly workspaceRoot?: string\n /** Unpolled PTY output ceiling in bytes (approx; the tail is kept). */\n readonly maxTtyBufferBytes?: number\n}\n\nimport type {} from '@deepseek-ai/dsh-jobs'\n\ndeclare module '@deepseek-ai/dsh-jobs' {\n interface JobKindMap {\n 'pentest-exec': 'pentest-exec'\n }\n}\n\n/** One tracked PTY process with a read cursor for polling. */\ninterface TrackedTty {\n readonly process: SandboxTtyProcess\n buffer: string\n cursor: number\n settled: { exitCode: number | null; signal: string | null } | undefined\n}\n\nexport const name = 'pentest-tool-shell'\n\nexport const inject = ['tools', 'pentestSandbox']\n\nexport const Config: Schema<Config> = z.object({\n scanId: z.string().default('pentest'),\n sources: z.array(z.object({\n workspaceSubdir: z.string(),\n sourcePath: z.string(),\n protectMetadata: z.boolean(),\n })),\n requireApproval: z.boolean().default(true),\n enableRunInBackground: z.boolean().default(true),\n defaultTimeoutMs: z.number().default(120_000),\n maxTimeoutMs: z.number().default(600_000),\n workspaceRoot: z.string().default('/workspace'),\n maxTtyBufferBytes: z.number().default(1_048_576),\n})\n\n/**\n * Validate a workdir against the workspace root (strix workdir contract),\n * segment by segment: no absolute escape, no `..`/`.`/empty segments, no\n * trailing slash.\n * @param cwd - the requested working directory.\n * @param workspaceRoot - container workspace root.\n * @returns null when valid, else strix's exact error text.\n */\nexport function workdirError(cwd: string | undefined, workspaceRoot: string): string | null {\n if (cwd === undefined) return null\n if (cwd === workspaceRoot) return null\n const prefix = `${workspaceRoot}/`\n const inside = cwd.startsWith(prefix)\n && cwd.slice(prefix.length).split('/').every(segment => segment !== '' && segment !== '.' && segment !== '..')\n return inside ? null : `exec_command: workdir must be a path inside ${workspaceRoot} (or omitted to use the turn's cwd). Got: '${cwd}'.`\n}\n\n/** Clamp a requested timeout into the configured window. */\nexport function clampTimeout(requested: number | undefined, defaultTimeoutMs: number, maxTimeoutMs: number): number {\n const value = requested ?? defaultTimeoutMs\n return Math.min(Math.max(1, value), maxTimeoutMs)\n}\n\nexport function apply(ctx: Context, config: Config = {}): void {\n const ttyProcesses = new Map<string, TrackedTty>()\n\n const session = () =>\n ctx.pentestSandbox.createSession({\n scanId: config.scanId ?? 'pentest',\n ...(config.sources !== undefined ? { sources: config.sources.map(source => ({\n workspaceSubdir: source.workspaceSubdir,\n sourcePath: source.sourcePath,\n ...(source.protectMetadata !== undefined ? { protectMetadata: source.protectMetadata } : {}),\n })) } : {}),\n })\n\n if (config.requireApproval !== false) {\n void ctx.on('tools/pre-execute', async (exec, next) => {\n if (exec.name === 'exec_command') {\n // Asking under the 'never' policy is a guaranteed rejection (no user\n // is ever consulted, yet the denial is worded as a user rejection) —\n // that single collision once killed the whole sandbox tool line\n // (exec_command, caido_api, agent-browser) for the entire session.\n // Honor the pre-trust instead.\n if (policyIsNever(ctx, exec.agent?.session)) return next()\n return { kind: 'ask' as const, reason: 'pentest: run a command inside the attack sandbox' }\n }\n return next()\n })\n }\n\n const workspaceRoot = config.workspaceRoot ?? '/workspace'\n const defaultTimeoutMs = config.defaultTimeoutMs ?? 120_000\n const maxTimeoutMs = config.maxTimeoutMs ?? 600_000\n const maxTtyBufferBytes = config.maxTtyBufferBytes ?? 1_048_576\n const backgroundEnabled = config.enableRunInBackground !== false\n\n ctx.tools.register(defineTool({\n name: 'exec_command',\n description: `Run a shell command inside the pentest sandbox (Kali container) in a fresh non-interactive bash login shell. Set tty=true for interactive programs (REPLs, ssh, msfconsole) and then drive them with write_stdin; run_in_background starts a job for long scans and returns a job id for job_output/job_kill.${backgroundEnabled ? '' : ' (run_in_background is disabled in this deployment.)'}`,\n parameters: {\n cmd: { type: 'string', required: true, description: 'The shell command line to execute.' },\n ...(backgroundEnabled ? { run_in_background: { type: 'boolean', description: 'Start as a background job; returns a job id (long scans).' } } : {}),\n cwd: { type: 'string', description: `Working directory inside ${workspaceRoot}.` },\n timeout_ms: { type: 'integer', description: 'Per-call timeout in milliseconds (clamped by deployment).' },\n tty: { type: 'boolean', description: 'Run under a PTY for interactive input via write_stdin.' },\n },\n output: {\n schema: {\n type: 'object',\n properties: {\n kind: { type: 'string', required: true, enum: ['foreground', 'tty', 'background'] },\n exit_code: { type: 'integer' },\n signal: { type: 'string' },\n timed_out: { type: 'boolean' },\n stdout: { type: 'string' },\n stderr: { type: 'string' },\n process_id: { type: 'string' },\n job_id: { type: 'string' },\n },\n additionalProperties: false,\n },\n render: (_args, value) => {\n if (value.kind === 'foreground') {\n return [{ type: 'text', text: `exit_code=${value.exit_code === null ? 'none' : String(value.exit_code)}${value.timed_out === true ? ' (timed out)' : ''}\\nstdout:\\n${value.stdout}\\nstderr:\\n${value.stderr}` }]\n }\n if (value.kind === 'tty') {\n return [{ type: 'text', text: `PTY process ${value.process_id} started. Poll with write_stdin(process_id=\"${value.process_id}\", chars=\"\"); send input via chars; deliver Ctrl-C with chars=\"\\\\x03\".` }]\n }\n return [{ type: 'text', text: `started background job ${value.job_id} (job_output to read, job_kill to stop)` }]\n },\n },\n timeoutMs: maxTimeoutMs,\n presentCall: args => ({ card: 'terminal', title: String(args.cmd ?? '') }),\n presentResult: (args, result) => {\n const text = result.content.filter(block => block.type === 'text').map(block => block.text).join('\\n')\n return { card: 'terminal', output: text === '' ? String(args.cmd ?? '') : text }\n },\n execute: async (args, exec) => {\n const cwdProblem = workdirError(args.cwd, workspaceRoot)\n if (cwdProblem !== null) throw new Error(cwdProblem)\n if (args.tty === true && args.run_in_background === true) {\n throw new Error('exec_command: tty=true and run_in_background cannot be combined — start a PTY process and drive it with write_stdin, or start a background job without a PTY.')\n }\n if (args.run_in_background === true && !backgroundEnabled) throw new Error('run_in_background is disabled for this deployment (enableRunInBackground: false)')\n const sandbox = await session()\n if (args.tty === true) {\n const process = await sandbox.execTty(args.cmd, { cwd: args.cwd })\n const tracked: TrackedTty = { process, buffer: '', cursor: 0, settled: undefined }\n process.subscribe(chunk => {\n tracked.buffer += chunk\n // Bound unpolled output; keep the tail and re-base the read cursor.\n if (tracked.buffer.length > maxTtyBufferBytes) {\n const excess = tracked.buffer.length - maxTtyBufferBytes\n tracked.buffer = tracked.buffer.slice(excess)\n tracked.cursor = Math.max(0, tracked.cursor - excess)\n }\n })\n void process.done.then(\n outcome => {\n tracked.settled = { exitCode: outcome.exitCode, signal: outcome.signal }\n },\n () => {\n tracked.settled = { exitCode: null, signal: null }\n },\n )\n ttyProcesses.set(process.id, tracked)\n return { kind: 'tty' as const, process_id: process.id }\n }\n if (args.run_in_background === true) {\n const jobs = ctx.get('jobs')\n if (jobs === undefined) throw new Error('run_in_background needs a jobs provider (load @deepseek-ai/dsh-jobs-local)')\n const job = sandbox.execJob(args.cmd, { cwd: args.cwd })\n let cancelled = false\n const jobId = jobs.start({\n kind: 'pentest-exec',\n label: args.cmd,\n ...exec.agent !== undefined ? { owner: exec.agent } : {},\n run: () => ({\n cancel: () => {\n cancelled = true\n job.terminate()\n },\n done: job.done.then(outcome =>\n // A killed docker-exec tree can still report exit code 0; the\n // cancellation flag is the authoritative kill signal.\n cancelled\n ? { status: 'killed' as const, detail: 'cancelled by request' }\n : {\n status: outcome.exitCode === 0 ? 'completed' as const : 'failed' as const,\n detail: `exit code: ${outcome.exitCode === null ? 'none' : String(outcome.exitCode)}`,\n }),\n readOutput: () => job.readOutput(),\n }),\n })\n return { kind: 'background' as const, job_id: jobId }\n }\n const result = await sandbox.exec(args.cmd, { cwd: args.cwd, timeoutMs: clampTimeout(args.timeout_ms, defaultTimeoutMs, maxTimeoutMs), signal: exec.signal })\n return {\n kind: 'foreground' as const,\n ...(result.exitCode !== null ? { exit_code: result.exitCode } : {}),\n ...(result.signal !== null ? { signal: result.signal } : {}),\n timed_out: result.timedOut,\n stdout: result.stdout,\n stderr: result.stderr,\n }\n },\n }))\n\n ctx.tools.register(defineTool({\n name: 'write_stdin',\n description: 'Send characters to a PTY-backed process started by exec_command(tty=true). chars=\"\" polls for new output without sending anything. Escapes (\\\\n, \\\\t, \\\\x03 for Ctrl-C, \\\\uXXXX) are decoded.',\n parameters: {\n process_id: { type: 'string', required: true, description: 'The process id returned by exec_command(tty=true).' },\n chars: { type: 'string', required: true, description: 'Characters to write (\"\" to poll for output only).' },\n },\n output: {\n schema: {\n type: 'object',\n properties: {\n output: { type: 'string', required: true },\n running: { type: 'boolean', required: true },\n exit_code: { type: 'integer' },\n },\n additionalProperties: false,\n },\n render: (_args, value) => [{\n type: 'text',\n text: `${value.running ? '(running)' : `(exited${value.exit_code === undefined ? '' : ` exit_code=${String(value.exit_code)}`})`}\\n${value.output}`,\n }],\n },\n execute: async args => {\n const tracked = ttyProcesses.get(args.process_id)\n if (tracked === undefined) throw new Error(STDIN_UNAVAILABLE)\n const chars = decodeCharsEscape(args.chars)\n if (chars !== '') await tracked.process.write(chars)\n const delta = tracked.buffer.slice(tracked.cursor)\n tracked.cursor = tracked.buffer.length\n if (tracked.settled !== undefined) ttyProcesses.delete(args.process_id)\n return {\n output: delta,\n running: tracked.settled === undefined,\n ...(tracked.settled !== undefined && tracked.settled.exitCode !== null ? { exit_code: tracked.settled.exitCode } : {}),\n }\n },\n }))\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gpzhang2001/sharpkit-shell",
|
|
3
3
|
"description": "exec_command / write_stdin model-facing shell tools over the sharpkit sandbox",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@deepseek-ai/schemastery": "3.18.2",
|
|
31
|
-
"@gpzhang2001/sharpkit-sandbox": "^0.2.
|
|
31
|
+
"@gpzhang2001/sharpkit-sandbox": "^0.2.2"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@deepseek-ai/cordis": "^4.0.2",
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* and `run_in_background` mapped onto the dsh jobs runtime
|
|
8
8
|
* (job_output/job_list/job_kill come free). Every exec passes an ask
|
|
9
9
|
* approval gate (config-gated; the registry resolves `ask` through the
|
|
10
|
-
* host's approval service and fails closed when none is composed)
|
|
10
|
+
* host's approval service and fails closed when none is composed), except
|
|
11
|
+
* under the `'never'` approval policy — there the gate is skipped because
|
|
12
|
+
* an ask can never be granted (the danger-full-access pre-trust stance).
|
|
11
13
|
* @module @gpzhang2001/sharpkit-shell
|
|
12
14
|
*/
|
|
13
15
|
|
|
@@ -28,13 +30,45 @@ export interface ConfigSource {
|
|
|
28
30
|
protectMetadata?: boolean
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Structural view of the host approval service (`ctx.approval`) — reads the
|
|
35
|
+
* session's effective approval policy without a hard dependency on
|
|
36
|
+
* `@deepseek-ai/dsh-user-approval`, using only stock public API members
|
|
37
|
+
* (`overrideOf` + the plugin's configured default; same pattern as the
|
|
38
|
+
* preset lookup in tool-proxy).
|
|
39
|
+
*/
|
|
40
|
+
interface ApprovalPolicySource {
|
|
41
|
+
overrideOf: (session: object) => 'ask' | 'never' | undefined
|
|
42
|
+
config?: { policy?: 'ask' | 'never' }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether the session's approval policy is `'never'` — the deterministic
|
|
47
|
+
* stance where every ask resolves `rejected` without consulting anyone (set
|
|
48
|
+
* by the danger-full-access preset, i.e. a user who pre-trusted the session).
|
|
49
|
+
* An ask under that policy can never be granted — and its denial is even
|
|
50
|
+
* reported as "the user rejected tool" although no user was consulted — so
|
|
51
|
+
* gates treat it as pre-approval instead of asking: sandbox commands are a
|
|
52
|
+
* strict subset of the host bash the same preset already unleashed.
|
|
53
|
+
*/
|
|
54
|
+
function policyIsNever(ctx: Context, session: object | undefined): boolean {
|
|
55
|
+
if (session === undefined) return false
|
|
56
|
+
const approval = ctx.get('approval') as ApprovalPolicySource | undefined
|
|
57
|
+
if (approval === undefined) return false
|
|
58
|
+
return (approval.overrideOf(session) ?? approval.config?.policy ?? 'ask') === 'never'
|
|
59
|
+
}
|
|
60
|
+
|
|
31
61
|
/** Deployment-tunable configuration. */
|
|
32
62
|
export interface Config {
|
|
33
63
|
/** Sandbox session cache key; the orchestrator pre-creates the real session under this id. */
|
|
34
64
|
readonly scanId?: string
|
|
35
65
|
/** Source trees for the session this tool creates when none exists yet. */
|
|
36
66
|
readonly sources?: ConfigSource[]
|
|
37
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Ask-approval gate on every exec_command (fail-closed without an approval
|
|
69
|
+
* service). Skipped automatically while the approval policy is `'never'` —
|
|
70
|
+
* an ask there is a guaranteed auto-rejection, not an approval request.
|
|
71
|
+
*/
|
|
38
72
|
readonly requireApproval?: boolean
|
|
39
73
|
/** Expose run_in_background (needs a jobs provider at runtime). */
|
|
40
74
|
readonly enableRunInBackground?: boolean
|
|
@@ -121,7 +155,15 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
121
155
|
|
|
122
156
|
if (config.requireApproval !== false) {
|
|
123
157
|
void ctx.on('tools/pre-execute', async (exec, next) => {
|
|
124
|
-
if (exec.name === 'exec_command')
|
|
158
|
+
if (exec.name === 'exec_command') {
|
|
159
|
+
// Asking under the 'never' policy is a guaranteed rejection (no user
|
|
160
|
+
// is ever consulted, yet the denial is worded as a user rejection) —
|
|
161
|
+
// that single collision once killed the whole sandbox tool line
|
|
162
|
+
// (exec_command, caido_api, agent-browser) for the entire session.
|
|
163
|
+
// Honor the pre-trust instead.
|
|
164
|
+
if (policyIsNever(ctx, exec.agent?.session)) return next()
|
|
165
|
+
return { kind: 'ask' as const, reason: 'pentest: run a command inside the attack sandbox' }
|
|
166
|
+
}
|
|
125
167
|
return next()
|
|
126
168
|
})
|
|
127
169
|
}
|