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.
The host half runs in a sandbox, separate from Oda's own code. Three properties shape everything you write:
await works; type annotations and the rest of TypeScript syntax do not.process is deliberately absent, so a typeof process probe does not throw. The encoding helpers btoa, atob, TextEncoder and TextDecoder are available, and console output is written through to the app log.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.
Node APIs are not available directly. When code reaches for one, the sandbox refuses it and points to where the work belongs:
require is refused: require is not available in the dynamic package sandbox — Node modules are unavailable. The rest of the message says to use the services on ctx instead: inject: ['fs'] for files, ['web'] for HTTP and ['bash'] for processes, and to query the service list first for exact names.setTimeout, setInterval, setImmediate and their clear functions are refused: Node timers are unavailable. The message points to the timer service instead: declare inject: ['timer'] on your plugin and call ctx. timeout or ctx. interval; those calls are cleaned up automatically when the package stops.fetch is refused (fetch is not available in the dynamic package sandbox) and says network access goes through the web service: declare inject: ['web'] and call ctx. web, and query the service list for its methods first.<name> is not available in the dynamic package sandbox, followed by 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.
A host half defines tools with harness.; a registered tool becomes callable by the agent. The definition and its schema are checked, and each refusal names the rule that failed:
harness. defineTool <path> must contain only own enumerable string keys, harness. defineTool <path>. <key> is not supported by the unified schema DSL.required lists only declared properties; oneOf holds at least two schemas; enum is a non-empty array; and additionalProperties must be explicitly true or false (at the implicit parameter root, true or omitted is accepted, because that root is open).harness. defineTool output must declare { schema, render, presentationMeta? }, output. render must be a function, execute must be a function.dynamic tool registration must use a tool returned by harness. defineTool(. . . ).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/
The browser half reaches the host half with host., and the call lands in a method you register with harness.. Two checks apply:
harness. handle(method, fn) needs a non-empty string method name and a handler function as its second argument: harness. handle(method, fn) needs a non-empty string method name, harness. handle("<method>") needs a handler function as its second argument.null, and your answer must be JSON: return null when there is nothing to report.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:
sandbox ctx is read-only; cannot assign "<name>".service "<name>" is not injected. Declare it: inject: ['<name>', …] on your plugin. Declaring a service also parks the package when its provider is away, instead of failing a later call.The definition around your half is checked when it is created and when it starts:
idPrefix must be 3 to 6 lowercase English letters.plugin "<id>" has no successful version yet; start "<packageId>" with mode "run", package "<packageId>" is already current; use mode "run", package "<packageId>" differs from current "<current>"; use mode "update". Mode run starts the current version; mode update replaces it with a different one.no dynamic plugin "<id>" in this process — it may have been removed or lost when DataMind Oda restarted. Definitions do not survive a restart; create the package and start it again.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.
Node timers do not run in the sandbox. Declare inject: ['timer'] and call ctx. or ctx.; those calls are cleaned up automatically when the package stops.
Network access goes through the web service: declare inject: ['web'] and call ctx.. The refusal for fetch points there and says to query the service list for its methods first.
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.
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.
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.
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.
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.
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.
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.