Skip to main content
0.0.x

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 TaskRuntimeAdapterLike implementation.
  • A task type string 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) returns false: the executor returns a failed TaskResult for unsupported runtimes.
  • The host binary is missing: return a failed TaskResult with stderr or an error message.
  • Timeout handling is adapter-owned. Respect options.timeout and options.signal in 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