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.
URI Shape
sandbox://<sandboxName>/<action>?<query>
Two actions are supported today:
| URI | Resolves 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"
/>
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 };
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.
Plain Links Are Untouched
An https:// defaultAction behaves exactly as before — interception happens only
for sandbox://.
See Also
- File Explorer — where
open-filelands - Cold-start HUD — sandbox launch and
launchedSandboxes - Templates — the attachment card structure itself