# /iblai-vibe-virtual-machine

> Mirrored from [`iblai/vibe`](https://github.com/iblai/vibe) · [`skills/organizations/iblai-vibe-virtual-machine/SKILL.md`](https://github.com/iblai/vibe/blob/main/skills/organizations/iblai-vibe-virtual-machine/SKILL.md). This page is generated — edit it in the repository, not here.

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:port`
  destinations 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`.

![Virtual Machine — Network Policies](https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/skills/organizations/iblai-vibe-virtual-machine/iblai-vibe-virtual-machine.png)

![Virtual Machine — Secrets](https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/skills/organizations/iblai-vibe-virtual-machine/iblai-vibe-virtual-machine-secrets.png)

![New Network Policy dialog](https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/skills/organizations/iblai-vibe-virtual-machine/iblai-vibe-virtual-machine-new-policy.png)

![New VM Secret dialog — Value Source picks a stored value or an integration credential](https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/skills/organizations/iblai-vibe-virtual-machine/iblai-vibe-virtual-machine-new-secret.png)

![Network Policies — empty state](https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/skills/organizations/iblai-vibe-virtual-machine/iblai-vibe-virtual-machine-policies-empty.png)

![Secrets — empty state](https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/skills/organizations/iblai-vibe-virtual-machine/iblai-vibe-virtual-machine-secrets-empty.png)

> **Common setup (brand, conventions, env files, verification):** see [docs/skill-setup.md](https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/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/mcp` in `.mcp.json`)
- The viewer needs
  `Ibl.Mentor/VirtualMachineNetworkPolicies/{list,write,delete}` and
  `Ibl.Mentor/VirtualMachineSecrets/{list,write,delete}`. Organization
  admins hold them by default; they are not in the `learner` baseline.
  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):

```tsx
// 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:

```tsx
"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:port`
  entries, 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 `username` lets 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`, or
  `Credential {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` — the `host:port` chip
  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:

1. `pnpm build` -- must pass with zero errors
2. `pnpm test` -- vitest must pass
3. Touch test `/settings/virtual-machine` in the browser as `/iblai-vibe-ops-test` says: open it,
   use every control this skill added, read the console, check the phone width.

## Important Notes

- **Redux store**: Must include `mentorReducer` and `mentorMiddleware`
  — they register `virtualMachineApiSlice`, which these hooks live on.
- **`initializeDataLayer()`**: 5 args (v1.2+)
- **`@reduxjs/toolkit`**: Deduplicated via webpack aliases in `next.config.ts`
- **Peer deps**: `sonner` and `@iblai/iblai-web-mentor` must be installed
  (`pnpm add sonner @iblai/iblai-web-mentor`)
- **Records are keyed in the body.** PATCH and DELETE take `id`
  (policies) or `env_var` (secrets) in the request body, not in the
  path. These endpoints are newer than the pinned `@iblai/iblai-api`
  release, 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_var` is 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
  — `$1` per 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](https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/BRAND.md)
