Scopes & Inheritance

View as Markdown

When you create a workspace, you mount the resources it needs — AI credentials, vault secrets, and storage. Every runtime launched inside that workspace automatically inherits those mounts. This means you configure access once and every runtime just works.

How inheritance works

Workspace (mounts: ai, vault, storage)
├── Runtime "crm" ← inherits all mounts
├── Runtime "email" ← inherits all mounts
└── Runtime "research" ← inherits all mounts

You set the ceiling at the workspace level. Runtimes inherit everything by default. When needed, you can narrow scopes for a specific runtime or agent execution — but you can never widen beyond what the workspace allows.

AI credentials

AI credentials give runtimes access to LLM providers (OpenAI, Anthropic, Google, etc.) for agent-based automation like Stagehand and browser-use.

Managing credentials

1// Create a credential
2await bctrl.aiCredentials.create({
3 id: "openai-prod",
4 provider: "openai",
5 label: "Production OpenAI",
6 apiKey: "sk-...",
7 defaultModel: "gpt-4o",
8});
9
10// List all credentials
11const creds = await bctrl.aiCredentials.list();
12
13// Test connectivity
14await bctrl.aiCredentials.test("openai-prod");

Mounting to a workspace

1const workspace = await bctrl.workspaces.create({
2 name: "agent-workspace",
3 mounts: {
4 ai: {
5 allow: ["openai-prod", "anthropic-prod"],
6 defaults: {
7 openai: "openai-prod",
8 anthropic: "anthropic-prod",
9 },
10 },
11 },
12});
  • allow — which credential IDs this workspace can use
  • defaults — the default credential per provider (used by agents automatically)

Any runtime in this workspace can use Stagehand or browser-use agents, and they’ll automatically pick up the correct AI credentials.

Vault

The vault stores secrets like login credentials, API keys, and TOTP seeds. Runtimes use vault entries to authenticate into websites.

Managing secrets

1// Store a credential
2await bctrl.vault.set("prod/crm/salesforce", {
3 username: "[email protected]",
4 password: "...",
5 totp: "JBSWY3DPEHPK3PXP",
6 origins: ["https://login.salesforce.com"],
7});
8
9// Retrieve
10const cred = await bctrl.vault.get("prod/crm/salesforce");
11
12// Generate a TOTP code
13const code = await bctrl.vault.totp("prod/crm/salesforce");
14
15// List keys
16const keys = await bctrl.vault.list("prod/crm/");

Mounting to a workspace

1mounts: {
2 vault: {
3 allow: ["prod/crm/", "prod/email/"],
4 deny: ["prod/admin/"],
5 allowRawReads: true,
6 },
7}
  • allow — key prefixes the workspace can access
  • deny — key prefixes explicitly blocked (takes precedence over allow)
  • allowRawReads — whether plaintext secret values can be read directly

Runtimes in the workspace can only access secrets that match the allowed prefixes. An agent running in this workspace could auto-login to Salesforce using prod/crm/salesforce but couldn’t touch prod/admin/ secrets.

Storage

Storage gives runtimes a place to save files — screenshots, downloads, exports, CSV files. It’s scoped by workspace namespace.

Using storage

1// Upload a file
2await bctrl.storage("my-workspace").upload("exports/report.csv", csvBuffer);
3
4// Download
5const data = await bctrl.storage("my-workspace").get("exports/report.csv");
6
7// Browse files
8const files = await bctrl.storage("my-workspace").browse({ prefix: "exports/" });
9
10// Get a download URL
11const url = await bctrl.storage("my-workspace").getDownloadUrl("exports/report.csv");

Mounting to a workspace

1mounts: {
2 storage: {
3 workspace: "shared-exports",
4 },
5}

This connects the workspace to the shared-exports storage namespace. All runtimes in the workspace can read and write files there. Multiple workspaces can share the same storage namespace.

Narrowing scopes

Scopes can be narrowed at the runtime or execution level. This is useful when a workspace has broad access but a specific agent task should be restricted:

1// Workspace has access to all prod/ vault keys
2const workspace = await bctrl.workspaces.create({
3 name: "multi-agent",
4 mounts: {
5 vault: { allow: ["prod/"] },
6 ai: { allow: ["openai-prod", "anthropic-prod"] },
7 },
8});
9
10// This runtime can only use anthropic credentials
11// (narrowed from the workspace's full set)

The principle: workspaces define the ceiling, runtimes can lower it, nothing can raise it.

Putting it all together

1import { Bctrl } from "@bctrl/sdk";
2
3const bctrl = new Bctrl({ apiKey: process.env.BCTRL_API_KEY! });
4
5// 1. Set up credentials (one-time)
6await bctrl.aiCredentials.create({
7 id: "openai-prod",
8 provider: "openai",
9 label: "Production",
10 apiKey: process.env.OPENAI_API_KEY!,
11});
12
13await bctrl.vault.set("prod/crm/salesforce", {
14 username: "[email protected]",
15 password: process.env.SF_PASSWORD!,
16 totp: process.env.SF_TOTP_SEED!,
17 origins: ["https://login.salesforce.com"],
18});
19
20// 2. Create workspace with all mounts
21const workspace = await bctrl.workspaces.create({
22 name: "sales-pipeline",
23 mounts: {
24 ai: { allow: ["openai-prod"], defaults: { openai: "openai-prod" } },
25 vault: { allow: ["prod/crm/"] },
26 storage: { workspace: "sales-exports" },
27 },
28});
29
30// 3. Launch runtimes — they inherit everything
31const runtime = await workspace.runtimes
32 .browser("crm")
33 .stagehand({ mode: "profile", profileId: "sf-bot" });
34
35// 4. Agent can auto-login using vault creds and use OpenAI for reasoning
36await runtime.stagehand.act("Log in to Salesforce and export Q1 pipeline");