@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.
@@ -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 /** 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}\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"}
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") return {
85
- kind: "ask",
86
- reason: "pentest: run a command inside the attack sandbox"
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.1",
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.1"
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
- /** Ask-approval gate on every exec_command (fail-closed without an approval service). */
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') return { kind: 'ask' as const, reason: 'pentest: run a command inside the attack sandbox' }
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
  }