The host half of a dynamic package

A dynamic package is an extension Oda writes and runs in a chat. A package comes in up to two halves: a browser half that adds UI to DataMind Oda, and a host half that runs inside Oda and does the work behind it. This page covers the host half: the sandbox it runs in, the tools it defines, the handlers it answers and the rules around returning and replacing it. The other side is on The browser half of a dynamic package.

How the sandbox runs your code

The host half runs in a sandbox, separate from Oda's own code. Three properties shape everything you write:

The sandbox keeps a host half away from Oda's internals, but it is not a security boundary. Treat a dynamic package with the same trust as commands you run in the terminal. Dynamic packages covers the trust model.

What the sandbox withholds

Node APIs are not available directly. When code reaches for one, the sandbox refuses it and points to where the work belongs:

If the code does not parse, the failure reads dynamic package "<half>" failed to parse, shows where parsing stopped, and teaches the format: The sandbox runs plain JavaScript, not TypeScript. A second note warns about bracket balance: the code runs as the body of an async function, so a trailing }); can close a call that was never opened.

Define a tool

A host half defines tools with harness.defineTool; a registered tool becomes callable by the agent. The definition and its schema are checked, and each refusal names the rule that failed:

An execute result must be lossless JSON. The refusal reads: must be lossless JSON data (objects, arrays, strings, numbers, booleans, null) — not a class instance, function, Map/Set, Date, or undefined. Return a plain object built from the values you need, or `return null` when the caller needs no value back.

Answer calls from the browser half

The browser half reaches the host half with host.call, and the call lands in a method you register with harness.handle. Two checks apply:

Return the plugin

Every host half must end by returning its plugin. A half that falls through fails with the question the Host half returned `undefined` — did you forget `return`?, and the returned value must be a plugin function or an object with an apply(ctx) method: the Host half must return a Plugin function or an object with apply(ctx).

The context the plugin receives is read-only, and it carries only what the runtime intends plus the services you declare:

The definition and its versions

The definition around your half is checked when it is created and when it starts:

Questions, answered

Why is require not available in the sandbox?

The sandbox runs package code without direct Node access, and the refusal tells you where the work belongs: declare the services you need instead, with inject: ['fs'] for files, ['web'] for HTTP and ['bash'] for processes as the examples. Query the service list first so you declare exact names: Inspect and manage extensions covers the list and query calls.

How does a host half wait for a timer?

Node timers do not run in the sandbox. Declare inject: ['timer'] and call ctx.timeout or ctx.interval; those calls are cleaned up automatically when the package stops.

How does a host half make a network call?

Network access goes through the web service: declare inject: ['web'] and call ctx.web. The refusal for fetch points there and says to query the service list for its methods first.

Why did my package fail to parse?

The sandbox runs plain JavaScript as the body of an async function, and TypeScript syntax is refused. Remove type annotations; the message shows where parsing stopped, and a bracket-balance note explains that a trailing }); can close a call that was never opened.

Why was my tool definition refused?

The refusal names the rule that failed. The common ones: a key outside the unified schema DSL, a value without a supported type, a required entry that names an undeclared property, a oneOf with fewer than two schemas, an empty enum, and an additionalProperties that is not explicitly true or false. Fix the named rule and define the tool again.

What can an execute function return?

Lossless JSON only: objects, arrays, strings, numbers, booleans and null. Not a class instance, function, Map, Set, Date or undefined. Build a plain object in the shape you need, or return null when the caller needs no value back.

Why did my host half fail with "returned undefined"?

The half ran to the end without returning its plugin. End the code with a return, and return either a plugin function or an object with an apply(ctx) method.

Why is a service "not injected"?

Declare it on the plugin you return: inject: ['<name>', …]. A declared service also parks the package when its provider is away, instead of failing when the service is used.

Why does starting a package say to use mode "run" or "update"?

Mode run starts the package's current version; mode update replaces the current version with a different one. If the package has no successful version yet, start it with run; if it is already current, run starts it again. Running and approvals are on Dynamic packages.

What does "no dynamic plugin" mean?

The id is not in the running app: it was removed, or Oda restarted since the package was created. Definitions do not survive a restart, so create the package and start it again.