Skip to main content

sandbox:// Handoff Cards

When the agent has opened a browser session inside a sandbox, or produced a file, it needs a way to hand the user off to it. That is done by pointing an attachment card's defaultAction at the custom sandbox:// scheme.

The SDK intercepts it before the open-in-a-new-tab fallback, resolves it into a typed intent, and dispatches it to the matching handler. The raw sandbox:// URI is never passed to window.open().

sandbox:// Handoff Cards

When an attachment card's defaultAction points at the custom sandbox:// scheme, the SDK intercepts it before the open-in-new-tab fallback, resolves it into a typed intent, and dispatches it to the matching handler. Plain https:// links are untouched and open normally.

<Chatbot
  config={{ botProviderEndpoint: '...' }}
  customChannelId="my-channel"
  // intercepts sandbox://<name>/open-browser
  onSandboxOpenBrowser={(sandboxName) => { /* … */ }}
  // intercepts sandbox://<name>/open-file?absolute_path=…
  onSandboxOpenFile={(sandboxName, absolutePath) => { /* … */ }}
/>

Dispatched intents

No sandbox:// card clicked yet

Click either of the first two cards → the resolved intent is logged below. The third is a plain link and is never intercepted.

Loading chatbot...

URI Shape

sandbox://<sandboxName>/<action>?<query>

Two actions are supported today:

URIResolves to
sandbox://sbx-a/open-browser{ kind: 'open-browser', sandboxName: 'sbx-a' }
sandbox://sbx-a/open-file?absolute_path=%2Fhome%2Fuser%2Freport.md{ kind: 'open-file', sandboxName: 'sbx-a', absolutePath: '/home/user/report.md' }

absolute_path must be URL-encoded (absolute paths carry many /).

Taking Over the Handling

Both actions can be overridden by the consumer:

<Chatbot
config={{ botProviderEndpoint: "..." }}
customChannelId="my-channel"
// When set, the SDK defers to you; otherwise it fetches a one-time URL and opens a tab
onSandboxOpenBrowser={(sandboxName) => { /* … */ }}
// open-file usually lands in the File Explorer
onSandboxOpenFile={(sandboxName, absolutePath) => {
controller.requestFile(sandboxName, absolutePath);
}}
// Required if you really mean to take over open-file — see below
fileExplorer="off"
// Where the default handler opens the tab; defaults to '_blank'
sandboxBrowserOpenTarget="_blank"
/>
The two handlers do not behave the same way

onSandboxOpenBrowser is a genuine either/or: when set the SDK defers to you, and only falls back to its own handler when you omit it.

onSandboxOpenFile is not. It is additive, not a replacement — your handler runs, but the built-in File Explorer aside is still revealed as well, because that branch only checks the fileExplorer prop and never whether you supplied a callback. And fileExplorer defaults to 'builtin'.

The result is that your own controller and the Chatbot's internal one both receive the same requestFile. To actually take over, set fileExplorer to "off".

Resolving It Yourself

resolveSandboxUri can also be used standalone — for instance when you render the cards yourself:

import { resolveSandboxUri } from "@asgard-js/core";

const intent = resolveSandboxUri(uri);

if (intent === null) {
// Not sandbox://, unknown action, or missing absolute_path
// → treat it as a plain card, and never window.open() the raw URI
}

The return type:

type SandboxUriIntent =
| { kind: "open-browser"; sandboxName: string }
| { kind: "open-file"; sandboxName: string; absolutePath: string };
note

Parsing uses a hand-written regex rather than new URL(). Absolute paths carry many /, and a non-special scheme's host casing is unreliable under the WHATWG URL parser; the query is still handed to URLSearchParams, which decodes.

An https:// defaultAction behaves exactly as before — interception happens only for sandbox://.

See Also