Add the organization settings Virtual Machine tab -- "Manage the network policies and secrets available to agents' virtual machines." Agents on the Virtual Machine Shell sandbox kind run each chat in an isolated Linux VM with no network access by default. This surface holds the two organization-wide records that open it up, in two sub-tabs:
- Network Policies — reusable named lists of the
host:portdestinations a VM may reach (up to 100 entries each). - Secrets — keys the VM can use but never read. Inside the machine the environment variable holds a placeholder; the real value is substituted only on TLS requests to the secret's allowed hosts.
Both are shared by every agent in the organization. Picking them for
one agent happens in its Sandbox settings — see
/iblai-vibe-agent-virtual-machine.






Common setup (brand, conventions, env files, verification): see docs/skill-setup.md.
Related surfaces
/iblai-vibe-agent-virtual-machine— the agent's Sandbox → Network Access section: the egress profile, the policy picker and the secrets multi-select that consume these records. Its Manage Policies / Manage Secrets buttons open this exact surface in a popup./iblai-vibe-agent-sandbox— the Sandbox Type card that turns Virtual Machine Shell on in the first place./iblai-vibe-account— the Account settings page hosts this surface as its Virtual Machine tab (see Step 2)./iblai-vibe-credential— the organization's integration credentials; a secret can read its value from one field of a saved credential instead of storing its own.
Prerequisites
- Auth must be set up first (
/iblai-vibe-auth) - MCP server + skills configured (
@iblai/mcpin.mcp.json) - The viewer needs
Ibl.Mentor/VirtualMachineNetworkPolicies/{list,write,delete}andIbl.Mentor/VirtualMachineSecrets/{list,write,delete}. Organization admins hold them by default; they are not in thelearnerbaseline. Each section hides on its own list 403, and when both lists are forbidden the tab renders a single permission notice. - Only agents on the Virtual Machine Shell kind consume these records. Nothing breaks without one, but nothing uses them either.
Step 1: Check Environment
Before proceeding, check for an iblai.env in the project root. Look for
PLATFORM, DOMAIN, and TOKEN variables. If the file does not exist or
is missing these variables, tell the user:
"You need an iblai.env with your platform configuration. Download the
template and fill in your values:
curl -o iblai.env https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/iblai.env"
Step 2: Mount it
Two ways, both supported.
As the Account page's Virtual Machine tab (how the SPAs ship it):
// app/(app)/settings/virtual-machine/page.tsx
"use client";
import { Account } from "@iblai/iblai-js/web-containers/next";
export default function OrgVirtualMachinePage() {
return (
<div className="flex h-full flex-col bg-white">
{/* accountProps = your usual <Account> wiring */}
<Account {...accountProps} targetTab="virtualMachine" />
</div>
);
}
See /iblai-vibe-account Step 2 for the full <Account> prop wiring.
Standalone — unlike the Memory admin tab, this one is exported:
"use client";
import { VirtualMachineAdminTab } from "@iblai/iblai-js/web-containers";
export default function VirtualMachinePage({
tenantKey,
username,
}: {
tenantKey: string;
username: string;
}) {
return (
<VirtualMachineAdminTab
tenantKey={tenantKey}
username={username}
defaultTab="network-policies"
/>
);
}
Step 3: Use MCP Tools for Customization
get_component_info("VirtualMachineAdminTab")
get_component_info("HostPortChipInput")
get_hook_info("useListVirtualMachineSecretsQuery")
Component Props
<VirtualMachineAdminTab> — @iblai/iblai-js/web-containers
| Prop | Type | Required | Description |
|---|---|---|---|
tenantKey |
string |
Yes | Organization key whose policies and secrets are managed |
username |
string |
No | Signed-in admin. When present, a policy delete refused because agents still use it lists those agents by name instead of by id |
defaultTab |
'network-policies' | 'secrets' |
No | Which sub-tab opens first; falls back to the other one when that list is forbidden |
Component map (web-containers source)
Everything lives under packages/web-containers/src/components/ in
the SDK. Only VirtualMachineAdminTab and HostPortChipInput are
public exports; the rest are listed as the reference for custom builds.
| Component | Path | Role |
|---|---|---|
VirtualMachineAdminTab |
profile/virtual-machine-admin/index.tsx |
The surface: info box, permission gate, Network Policies / Secrets sub-tabs |
NetworkPoliciesSection |
profile/virtual-machine-admin/network-policies-section.tsx |
Policies table + New / Edit / Delete, including the "still in use" refusal |
VmSecretsSection |
profile/virtual-machine-admin/vm-secrets-section.tsx |
Secrets table + New / Edit / Delete, and the Source column |
NetworkPolicyDialog |
virtual-machine/network-policy-dialog.tsx |
Create / edit a policy. Also opened straight from the agent's Create Policy button |
VmSecretDialog |
virtual-machine/vm-secret-dialog.tsx |
Create / edit a secret, including the Value Source choice |
HostPortChipInput |
virtual-machine/host-port-chip-input.tsx |
The host:port chip input both dialogs use (Enter / comma / space / paste, per-entry validation, keyboard-removable chips) |
toIntegrationCredentialOptions |
virtual-machine/credential-options.ts |
Normalises the integration-credential list into { id, name, fields } — only ids, names and the keys of the masked value |
What each sub-tab renders
Network Policies
- New Policy opens the dialog; each row has pencil (edit) and trash (delete) actions.
- Columns: Name, Allowed Hosts (first 3 as chips, then
"+N more"), Description (
—when blank), Updated (time-ago), Actions. - Empty: "No network policies yet. Create one to let an agent's VM reach specific hosts."
- New / Edit Network Policy dialog: Name (unique in the
organization, ≤ 255 chars), Allowed Hosts (1–100
host:portentries, IP addresses allowed), Description (optional). Editing sends only the fields that changed, and removing a host raises an amber warning that a bound secret needing it will stop working — the backend allows the edit anyway. - Delete asks to confirm. If an agent still uses the policy the
backend refuses: the dialog then lists the blocking agents (by name
when
usernamelets it resolve them, else by id) and hides the Delete button until you move those agents off it.
Secrets
- New Secret opens the dialog; pencil and trash per row.
- Columns: Name, Variable (the env var as a code chip),
Allowed Hosts, Source (
Stored Value, orCredential {name} · {field}), Updated, Actions. - Empty: "No VM secrets yet. Create one to let an agent call an API with your organization's key without ever seeing it."
- New / Edit VM Secret dialog:
- Name — display name, ≤ 255 chars.
- Environment Variable — e.g.
ACME_KEY; letters, digits and underscores, cannot start with a digit. Read-only once created — delete and recreate to rename. - Allowed Hosts — hostnames only, no IPs. These are the only hosts the real value is ever sent to, so keep them narrow.
- Value Source — Enter a Value (stored encrypted, never shown again, at least 8 characters) or Use an Integration Credential (pick a saved credential and one of its fields; the secret follows rotations automatically).
- On edit the value field is never prefilled — blank keeps the current value, and typing a new one replaces it (dropping any credential link).
- Delete warns that the secret is also removed from every agent that uses it, then deletes.
REST API
For custom UI. Both records live under
${dmUrl}/api/ai-account/orgs/{org}/ where dmUrl is
NEXT_PUBLIC_API_BASE_URL. Auth: Authorization: Token <token>.
Records are keyed in the body, never in the URL.
Network policies — virtual-machine-network-policy/
| Method | Body / notes |
|---|---|
| GET | [{ id, name, allowed_hosts, description, created_at, updated_at }] |
| POST | { "name": "Acme APIs", "allowed_hosts": ["api.acme.com:443"], "description": "" } → 201 |
| PATCH | { "id": 12, … } — any of name, allowed_hosts (replaces the whole list), description |
| DELETE | { "id": 12 } → 204; 400 while an agent still uses it, and the message names the agents |
Secrets — virtual-machine-secret/
| Method | Body / notes |
|---|---|
| GET | [{ id, name, env_var, allow_hosts, source_credential_id, source_field, created_at, updated_at }] — never a value |
| POST | { "name", "env_var": "ACME_KEY", "allow_hosts": ["api.acme.com:443"], "value": "…" } or { …, "source_credential_id": 7, "source_field": "api_key" } — exactly one form |
| PATCH | keyed by { "env_var": "ACME_KEY" }; sending value or a source_credential_id switches the form. env_var itself cannot change |
| DELETE | { "env_var": "ACME_KEY" } → 204; also unbinds it from every agent |
Integration credentials for the second form come from
GET /api/ai-account/orgs/{org}/integration-credential/, which
returns [{ id, name, value: { api_key: "sk-****", … } }]. Only the
ids, names and the keys of value are used — masked values are
never shown.
Validation the UI mirrors
| Rule | Constant |
|---|---|
host:port, no scheme, path or wildcard; port 1–65535 |
VIRTUAL_MACHINE_HOST_PORT_PATTERN |
localhost, metadata*, instance-data* and .local / .internal / .localhost suffixes refused; reserved IP ranges refused |
VIRTUAL_MACHINE_REJECTED_HOSTS, VIRTUAL_MACHINE_REJECTED_HOST_SUFFIXES |
| Policies accept IP addresses, secrets do not | HostPortChipInput's allowIpAddresses |
| ≤ 100 hosts per policy | VIRTUAL_MACHINE_MAX_POLICY_HOSTS |
| ≤ 20 secrets per agent | VIRTUAL_MACHINE_MAX_SECRETS_PER_MENTOR |
Stored value ≥ 8 characters, and a masked-looking value (sk-****) is refused |
VIRTUAL_MACHINE_SECRET_VALUE_MIN_LENGTH |
env_var matches ^[A-Za-z_][A-Za-z0-9_]*$ |
VIRTUAL_MACHINE_ENV_VAR_PATTERN |
Every 400 comes back as { error } — either a plain message or
{ field: ["…"] }.
Related Exports
From @iblai/iblai-js/web-containers:
VirtualMachineAdminTab,VirtualMachineAdminTabProps— this surface.HostPortChipInput,HostPortChipInputProps— thehost:portchip input, reusable for any allowlist field.VirtualMachineNetworkSection— the per-agent binding section (/iblai-vibe-agent-virtual-machine).
From @iblai/iblai-js/web-containers/next:
Account— the settings page hosting this as its Virtual Machine tab (targetTab="virtualMachine").
From @iblai/iblai-js/data-layer:
- Policies:
useListVirtualMachineNetworkPoliciesQuery,useLazyListVirtualMachineNetworkPoliciesQuery,useCreateVirtualMachineNetworkPolicyMutation,useUpdateVirtualMachineNetworkPolicyMutation,useDeleteVirtualMachineNetworkPolicyMutation - Secrets:
useListVirtualMachineSecretsQuery,useLazyListVirtualMachineSecretsQuery,useCreateVirtualMachineSecretMutation,useUpdateVirtualMachineSecretMutation,useDeleteVirtualMachineSecretMutation - Credentials for the Value Source picker:
useGetIntegrationCredentialsQuery - Helpers:
validateHostPort,isValidHostPort,splitHostPortInput,normalizeHostPort,getVirtualMachineApiError,getVirtualMachineErrorStatus,extractBoundMentorIds - Constants:
VIRTUAL_MACHINE_ENDPOINTS,VIRTUAL_MACHINE_PERMISSIONS,VIRTUAL_MACHINE_EGRESS_PROFILES,VIRTUAL_MACHINE_MAX_POLICY_HOSTS,VIRTUAL_MACHINE_MAX_SECRETS_PER_MENTOR,VIRTUAL_MACHINE_SECRET_VALUE_MIN_LENGTH,VIRTUAL_MACHINE_ENV_VAR_PATTERN,VIRTUAL_MACHINE_HOST_PORT_PATTERN,VIRTUAL_MACHINE_REJECTED_HOSTS,VIRTUAL_MACHINE_REJECTED_HOST_SUFFIXES - Types:
VirtualMachineNetworkPolicy,VirtualMachineSecret,VirtualMachineApiError,VirtualMachineHostPortIssue
SDK source: packages/web-containers/src/components/profile/virtual-machine-admin/,
components/virtual-machine/, and
packages/data-layer/src/features/virtual-machine/.
Step 4: Verify
Run /iblai-vibe-ops-test before telling the user the work is ready:
pnpm build-- must pass with zero errorspnpm test-- vitest must pass- Start dev server and touch test:
pnpm dev & npx playwright screenshot http://localhost:3000/settings/virtual-machine /tmp/org-vm.png
Important Notes
- Redux store: Must include
mentorReducerandmentorMiddleware— they registervirtualMachineApiSlice, which these hooks live on. initializeDataLayer(): 5 args (v1.2+)@reduxjs/toolkit: Deduplicated via webpack aliases innext.config.ts- Peer deps:
sonnerand@iblai/iblai-web-mentormust be installed (pnpm add sonner @iblai/iblai-web-mentor) - Records are keyed in the body. PATCH and DELETE take
id(policies) orenv_var(secrets) in the request body, not in the path. These endpoints are newer than the pinned@iblai/iblai-apirelease, so the slice is hand-written. - A value is never returned. No endpoint hands back a secret's value, the table only shows its source, and the edit form starts blank. Never build UI that tries to display one.
env_varis immutable. Deleting and recreating is the rename path — and the delete unbinds the secret from every agent first.- Per-section 403. The two lists gate independently: an admin may see policies but not secrets. Both forbidden → one permission notice for the whole tab.
- Deleting a policy an agent uses is refused, deliberately, so an agent on the Custom profile never points at a policy that vanished. Move those agents to another policy or profile first.
- Scope is one organization. Policies and secrets are never shared across organizations.
- Billing: VM lifetime is charged to the chatting user's credits
—
$1per 10 minutes by default, prorated per second and overridable per organization. The agent-side section states this next to the controls. - Brand guidelines: BRAND.md