Add a task runtime adapter
createDefaultTaskExecutor({ adapters, customAdapters }) lets you keep the built-in task executor
while adding a controlled runtime adapter. Use this when a task must run through a host runtime that
NetScript does not ship by default.
Prerequisites
- A
TaskRuntimeAdapterLikeimplementation. - A task
typestring reserved for your adapter. - A clear sandbox story for the external runtime.
- A test task that exercises stdout, stderr, timeout, and failure.
Start from the real adapter contract
The executor consumes TaskRuntimeAdapterLike: id, runtime, supports(task), and
execute(task, options).
import type {
ResolvedTaskExecutionOptions,
TaskDefinition,
TaskResult,
TaskRuntimeAdapterLike,
} from '@netscript/plugin-workers-core/executor';
export const nodeAdapter: TaskRuntimeAdapterLike = {
id: 'node-runtime-adapter',
runtime: null,
supports(task: TaskDefinition): boolean {
return task.type === 'node';
},
async execute(
task: TaskDefinition,
options: ResolvedTaskExecutionOptions,
): Promise<TaskResult> {
const startedAt = new Date().toISOString();
const started = performance.now();
const command = new Deno.Command('node', {
args: [task.entrypoint ?? '', ...(options.args ?? [])],
cwd: options.cwd || undefined,
env: options.env,
stdout: 'piped',
stderr: 'piped',
signal: options.signal,
});
const output = await command.output();
const duration = Math.round(performance.now() - started);
const stdout = new TextDecoder().decode(output.stdout);
const stderr = new TextDecoder().decode(output.stderr);
options.onStdout?.(stdout);
options.onStderr?.(stderr);
return {
taskId: task.id,
status: output.success ? 'completed' : 'failed',
exitCode: output.code,
stdout,
stderr,
duration,
success: output.success,
error: output.success ? null : stderr || `node exited ${output.code}`,
result: null,
startedAt,
completedAt: new Date().toISOString(),
attempt: 1,
};
},
};
Register the adapter
Use customAdapters when your task type is not one of the built-in TaskType values. The default
adapter map still covers deno, python, dotnet, shell, powershell, cmd, and
executable.
import { createDefaultTaskExecutor } from '@netscript/plugin-workers-core/executor';
import { nodeAdapter } from './node-adapter.ts';
const executor = createDefaultTaskExecutor({
customAdapters: {
node: nodeAdapter,
},
defaults: {
cwd: Deno.cwd(),
timeout: 300_000,
},
});
const result = await executor.execute(
{
id: 'render-invoice',
type: 'node',
entrypoint: './tasks/render-invoice.mjs',
args: ['--invoice', 'inv_123'],
},
{
correlationId: 'invoice.inv_123',
onStdout: (line) => console.log(line),
onStderr: (line) => console.error(line),
},
);
if (!result.success) {
throw new Error(result.error ?? 'task failed');
}
Run it end to end
The adapter and its registration above, run against a real task file. Copy the two files, then run the command sequence from the workspace root:
// tasks/render-invoice.mjs — the external Node task the adapter spawns
const idx = process.argv.indexOf('--invoice');
const invoice = idx >= 0 ? process.argv[idx + 1] : 'unknown';
console.log(`rendered ${invoice}`);
// run-invoice.ts — register the adapter and execute one task
import { createDefaultTaskExecutor } from '@netscript/plugin-workers-core/executor';
import { nodeAdapter } from './node-adapter.ts';
const executor = createDefaultTaskExecutor({
customAdapters: { node: nodeAdapter },
defaults: { cwd: Deno.cwd(), timeout: 300_000 },
});
const result = await executor.execute(
{
id: 'render-invoice',
type: 'node',
entrypoint: './tasks/render-invoice.mjs',
args: ['--invoice', 'inv_123'],
},
{ correlationId: 'invoice.inv_123', onStdout: (line) => console.log(line) },
);
if (!result.success) throw new Error(result.error ?? 'task failed');
console.log('status', result.status, 'exit', result.exitCode);
# From the workspace root. --allow-run=node lets the adapter spawn the Node
# binary; --allow-read lets it read the task file. Node must be on PATH.
deno run --allow-run=node --allow-read run-invoice.ts
# rendered inv_123
# status completed exit 0
supports(task) matches task.type === 'node', so the executor routes render-invoice to your
adapter, which spawns node ./tasks/render-invoice.mjs --invoice inv_123, streams stdout through
onStdout, and returns a TaskResult with status: 'completed' and exitCode: 0.
Failure modes
supports(task)returnsfalse: the executor returns a failedTaskResultfor unsupported runtimes.- The host binary is missing: return a failed
TaskResultwith stderr or an error message. - Timeout handling is adapter-owned. Respect
options.timeoutandoptions.signalin your adapter. - OS Permission Sandbox Boundary: NetScript enforces fine-grained permission sandboxing (using
.permissions(...)mapped to Deno--allow-*flags) only for Deno tasks (runtime("deno")). Non-Deno task runtimes (Python, .NET, shell, PowerShell, cmd, executable, and custom subprocess adapters) execute with the full OS permissions of the host worker process. This boundary is drawn because OS-level sandboxing of arbitrary external binaries is outside the runtime's core scope. To run untrusted polyglot code safely, you must implement sandboxing at the adapter level (e.g., using container isolation or restricted OS users) or configure OS-level process restrictions.
Next steps
- Restrict Deno task permissions with Restrict worker task permissions.
- See built-in runtime behavior in Run a polyglot task.
- Look up the executor surface in workers reference.