Skip to content

feat: executor tool invocation for js-exec#171

Open
cramforce wants to merge 4 commits intomainfrom
executor
Open

feat: executor tool invocation for js-exec#171
cramforce wants to merge 4 commits intomainfrom
executor

Conversation

@cramforce
Copy link
Copy Markdown
Contributor

@cramforce cramforce commented Mar 27, 2026

Add executor option to BashOptions that makes a tools proxy available to JavaScript code running in js-exec. Tool calls are synchronous from the QuickJS sandbox's perspective — they block via SharedArrayBuffer/Atomics while the host resolves them asynchronously.

Natively integrates with @executor/sdk — the setup callback receives the SDK instance for adding OpenAPI, GraphQL, and MCP sources that auto-discover tools. onToolApproval controls which tools are allowed.

Native SDK integration (OpenAPI, GraphQL, MCP)

import { Bash } from "just-bash";

const bash = new Bash({
  executor: {
    setup: async (sdk) => {
      await sdk.sources.add({
        kind: "openapi",
        endpoint: "https://petstore3.swagger.io/api/v3",
        specUrl: "https://petstore3.swagger.io/api/v3/openapi.json",
        name: "petstore",
      });

      await sdk.sources.add({
        kind: "graphql",
        endpoint: "https://countries.trevorblades.com/graphql",
        name: "countries",
      });

      await sdk.sources.add({
        kind: "mcp",
        endpoint: "https://mcp.example.com/sse",
        name: "internal",
        transport: "sse",
      });
    },

    onToolApproval: async (request) => {
      // Auto-approve reads, require confirmation for writes/deletes
      if (request.operationKind === "read") return { approved: true };
      const ok = await promptUser(
        `Allow ${request.toolPath} (${request.operationKind})?`
      );
      return ok ? { approved: true } : { approved: false, reason: "denied" };
    },
  },
});

await bash.exec(`js-exec -c '
  const pets = await tools.petstore.findPetsByStatus({ status: "available" });
  const country = await tools.countries.country({ code: "US" });
  const docs = await tools.internal.searchDocs({ query: "deploy" });
  console.log(pets.length, "pets,", country.name, ",", docs.hits.length, "docs");
'`);

Inline tools (no SDK needed)

const bash = new Bash({
  executor: {
    tools: {
      "math.add": {
        description: "Add two numbers",
        execute: (args) => ({ sum: args.a + args.b }),
      },
      "db.query": {
        execute: async (args) => {
          const rows = await pg.query(args.sql);
          return { rows };
        },
      },
    },
  },
});

await bash.exec(`js-exec -c '
  const sum = await tools.math.add({ a: 3, b: 4 });
  console.log(sum.sum);

  const data = await tools.db.query({ sql: "SELECT * FROM users" });
  for (const row of data.rows) console.log(row.name);
'`);

Both: inline tools + SDK sources

const bash = new Bash({
  executor: {
    tools: {
      "util.timestamp": {
        execute: () => ({ ts: Math.floor(Date.now() / 1000) }),
      },
    },
    setup: async (sdk) => {
      await sdk.sources.add({ kind: "openapi", endpoint: "...", specUrl: "...", name: "api" });
    },
    onToolApproval: "allow-all",
  },
});

Implementation

  • New INVOKE_TOOL (400) opcode in the SharedArrayBuffer bridge protocol
  • SyncBackend.invokeTool() — worker-side sync call via Atomics.wait
  • BridgeHandler accepts optional invokeTool callback, handles new opcode
  • Worker registers __invokeTool native function + tools Proxy when hasExecutorTools is set
  • Tool invoker threads from BashOptionsBashInterpreterOptionsInterpreterContextCommandContext → js-exec → BridgeHandler → worker
  • executor.setup lazily initializes @executor/sdk on first exec (dynamic import — SDK is only loaded when setup is provided)
  • executor.onToolApproval wired through to createExecutor() — controls approval for SDK-discovered tools (inline tools are always allowed)
  • SDK's CodeExecutor runtime delegates to js-exec's executeForExecutor
  • Full executor mode (log capture + result capture) available via executorMode flag for direct SDK integration

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
just-bash-website Ready Ready Preview, Comment Mar 28, 2026 5:44pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
just-bash Ignored Ignored Mar 28, 2026 5:44pm

Add `executor` option to `BashOptions` that makes a `tools` proxy available
to JavaScript code running in js-exec. Tool calls are synchronous from the
QuickJS sandbox's perspective — they block via SharedArrayBuffer/Atomics
while the host resolves them asynchronously.

Natively integrates with `@executor/sdk` — the `setup` callback receives
the SDK instance for adding OpenAPI, GraphQL, and MCP sources that
auto-discover tools. `onToolApproval` controls which tools are allowed.

## Native SDK integration (OpenAPI, GraphQL, MCP)

```ts
import { Bash } from "just-bash";

const bash = new Bash({
  executor: {
    setup: async (sdk) => {
      await sdk.sources.add({
        kind: "openapi",
        endpoint: "https://petstore3.swagger.io/api/v3",
        specUrl: "https://petstore3.swagger.io/api/v3/openapi.json",
        name: "petstore",
      });

      await sdk.sources.add({
        kind: "graphql",
        endpoint: "https://countries.trevorblades.com/graphql",
        name: "countries",
      });

      await sdk.sources.add({
        kind: "mcp",
        endpoint: "https://mcp.example.com/sse",
        name: "internal",
        transport: "sse",
      });
    },

    onToolApproval: async (request) => {
      // Auto-approve reads, require confirmation for writes/deletes
      if (request.operationKind === "read") return { approved: true };
      const ok = await promptUser(
        `Allow ${request.toolPath} (${request.operationKind})?`
      );
      return ok ? { approved: true } : { approved: false, reason: "denied" };
    },
  },
});

await bash.exec(`js-exec -c '
  const pets = await tools.petstore.findPetsByStatus({ status: "available" });
  const country = await tools.countries.country({ code: "US" });
  const docs = await tools.internal.searchDocs({ query: "deploy" });
  console.log(pets.length, "pets,", country.name, ",", docs.hits.length, "docs");
'`);
```

## Inline tools (no SDK needed)

```ts
const bash = new Bash({
  executor: {
    tools: {
      "math.add": {
        description: "Add two numbers",
        execute: (args) => ({ sum: args.a + args.b }),
      },
      "db.query": {
        execute: async (args) => {
          const rows = await pg.query(args.sql);
          return { rows };
        },
      },
    },
  },
});

await bash.exec(`js-exec -c '
  const sum = await tools.math.add({ a: 3, b: 4 });
  console.log(sum.sum);

  const data = await tools.db.query({ sql: "SELECT * FROM users" });
  for (const row of data.rows) console.log(row.name);
'`);
```

## Both: inline tools + SDK sources

```ts
const bash = new Bash({
  executor: {
    tools: {
      "util.timestamp": {
        execute: () => ({ ts: Math.floor(Date.now() / 1000) }),
      },
    },
    setup: async (sdk) => {
      await sdk.sources.add({ kind: "openapi", endpoint: "...", specUrl: "...", name: "api" });
    },
    onToolApproval: "allow-all",
  },
});
```

## Implementation

- New INVOKE_TOOL (400) opcode in the SharedArrayBuffer bridge protocol
- SyncBackend.invokeTool() — worker-side sync call via Atomics.wait
- BridgeHandler accepts optional invokeTool callback, handles new opcode
- Worker registers __invokeTool native function + tools Proxy when
  hasExecutorTools is set
- Tool invoker threads from BashOptions → Bash → InterpreterOptions →
  InterpreterContext → CommandContext → js-exec → BridgeHandler → worker
- executor.setup lazily initializes @executor/sdk on first exec
  (dynamic import — SDK is only loaded when setup is provided)
- executor.onToolApproval wired through to createExecutor() — controls
  approval for SDK-discovered tools (inline tools are always allowed)
- SDK's CodeExecutor runtime delegates to js-exec's executeForExecutor
- Full executor mode (log capture + result capture) available via
  executorMode flag for direct SDK integration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant