// The one `{ id, kind, config }` shape every sandbox connection is written as: the config arm per kind, the union over // them, and what the Capabilities page reads (status, probe, connect/rename/sign-in inputs). import { z } from "zod"; import { ExitConfigSchema } from "./exit.js"; import { entryId } from "./internal.js"; import { NetdiskConfigSchema } from "./netdisk.js"; import { VpnConfigSchema } from "./vpn.js"; // One-way: the browser extension bundles webext.js alone, so nothing there may reach back into this file. import { WebExtConfigSchema } from "./webext.js"; import { PhoneConfigSchema } from "./phone.js"; // Everything a user adds is a capability with an idempotent apply plus a status check; the manifest is the source of // truth for what's active, and `mcp`-kind entries also feed the agent's MCP servers each turn. export const CapabilityKindSchema = z.enum([ "devops", "monorepo", "mcp", "cli", "plugin", "extension", "ssh", "vpn", "exit", "netdisk", "docker", "browser", "identity", "device", "webext", "phone", "agent", "endpoint", "localmodel", "wallet", "fleet", "link", ]); export type CapabilityKind = z.infer; export const CapabilityStateSchema = z.enum(["active", "pending", "error", "inactive"]); export type CapabilityState = z.infer; // Per-kind config. Secrets (an mcp token) live here and are denylisted like tools.json. export const McpConfigSchema = z.object({ url: z.url().describe("Where the tool server answers."), token: z.string().optional().describe("The credential it needs, if any. Stored, never echoed back."), }); // Gives the agent an authenticated CLI tool: credential plus any non-secret URL, injected into the agent's env each // turn, taught via an .agents/skills/ cheatsheet. Provider fields are data in an extension's // `contributes.capabilities`, validated at add-time, not by this schema. export const CliConfigSchema = z .object({ provider: z .string() .min(1) .describe( "Which tool to give the agent. The rest of the fields are whatever that tool's own card declares it needs, and are checked against it when you connect.", ), }) .catchall(z.string()); // A Claude Code plugin from a git repo; the daemon only owns the checkout, the Agent SDK's plugin loader reads its // internals. `path` is a subdirectory for a plugin inside a larger checkout; `token` is https auth for a private repo. export const PluginConfigSchema = z.object({ url: z.url().describe("The repository to take the plugin from."), // Branch / tag / commit sha to install from; absent = the default branch's HEAD. Either way `commit` pins it. ref: z .string() .min(1) .optional() .describe( "A branch, tag or commit to install from. Leave it out for the default branch. The install pins the commit it names at that moment.", ), path: z .string() .min(1) .refine((value) => !value.split("/").includes(".."), { message: "path must stay inside the checkout" }) .optional() .describe("Where inside the repository the plugin lives, for one that sits in a larger checkout."), token: z.string().min(1).optional().describe("A credential for a private repository. Stored, never echoed back."), // Set by the sandbox at install, never by the form: re-applying a stored entry (a rotated token, a migration) // checks this out again, and only adding the plugin anew moves it. commit: z .string() .regex(/^[0-9a-f]{40}$/, "commit must be a full 40-character commit sha") .optional() .describe( "The exact commit installed, in full, resolved from the branch, tag or commit when the plugin was added. Its hooks run inside the agent's session, so it stays at this commit until it is added again.", ), }); // An intentic extension from a git repo (UI bundle + agent contributions + processes). Unlike `plugin`, `ref` must be a // full commit sha, since extension code runs trusted in the owner's browser and every update is a deliberate re-add at // a new sha. export const ExtensionConfigSchema = z.object({ url: z.url().describe("The repository to take the extension from."), ref: z .string() .regex(/^[0-9a-f]{40}$/, "ref must be a full 40-character commit sha") .describe( "The exact commit to install, in full. Required rather than optional because extension code runs with your browser's trust: the owner approves precisely the code that runs, and an update is a deliberate re-install at a new commit.", ), path: z .string() .min(1) .refine((value) => !value.split("/").includes(".."), { message: "path must stay inside the checkout" }) .optional() .describe("Where inside the repository the extension lives, for one that sits in a larger checkout."), token: z.string().min(1).optional().describe("A credential for a private repository. Stored, never echoed back."), // What update checks and advisories are read against; absent falls back to the official registry. registry: z .url() .optional() .describe( "Which registry this install came from, which is what update checks and security advisories are read against. Absent falls back to the official one.", ), }); // A remote machine the agent reaches over SSH; one capability per machine, the id is its ssh-config Host alias. Writes // a config block and a 0600 key/password file, unlike `cli`, so nothing is injected into the agent's env and machines // never collide. export const SshConfigSchema = z.discriminatedUnion("auth", [ z.object({ auth: z.literal("key").describe("Sign in with a key."), host: z.string().min(1).describe("The machine's address."), port: z.coerce.number().default(22).describe("Which port it listens on."), user: z.string().min(1).describe("Which user to connect as."), privateKey: z.string().min(1).describe("The private key, whole. Stored with tight permissions and never echoed back."), }), // The sandbox made this key itself (capabilities.sshKey), so its private half never crossed the wire; stored and // written exactly like a pasted one. z.object({ auth: z.literal("generated").describe("Sign in with a key the sandbox generated. Its private half never left the sandbox."), host: z.string().min(1).describe("The machine's address."), port: z.coerce.number().default(22).describe("Which port it listens on."), user: z.string().min(1).describe("Which user to connect as."), privateKey: z.string().min(1).describe("The private half of the generated key. Stored with tight permissions and never echoed back."), }), z.object({ auth: z.literal("password").describe("Sign in with a password."), host: z.string().min(1).describe("The machine's address."), port: z.coerce.number().default(22).describe("Which port it listens on."), user: z.string().min(1).describe("Which user to connect as."), password: z.string().min(1).describe("The password. Stored, never echoed back."), }), ]); // What a form sends where a generated key's private half goes: the token capabilities.sshKey answered with, which the // add swaps for the key the daemon kept. VAULTED's sibling (policy/capability-secrets.ts) for a value not stored yet; a // marker with nothing held behind it is refused, never written. const STASHED_PREFIX = "__intentic_stashed__:"; export const stashedMarker = (token: string): string => `${STASHED_PREFIX}${token}`; // The token a marker names; undefined for anything else (a real key, VAULTED, a blank). export const stashedToken = (value: unknown): string | undefined => typeof value === "string" && value.startsWith(STASHED_PREFIX) && value.length > STASHED_PREFIX.length ? value.slice(STASHED_PREFIX.length) : undefined; // What is optional about the in-sandbox Docker Engine: `gpu` is an IMAGE option (rides the Dockerfile overlay, needs a // rebuild), everything else is an ENGINE option (rewrites daemon.json, restarts dockerd, no rebuild but stops running // containers). Flat strings, not nested booleans, to match the manifest's own two-state convention. export const DockerConfigSchema = z.object({ gpu: z.enum(["on", "off"]).default("off"), // Pull-through cache/mirror; the nested engine starts with an empty image store, so the first compose up pulls // everything. registryMirror: z.url().optional(), // Registries reachable over plain http or a self-signed cert (a LAN or homelab registry); space- or // comma-separated. insecureRegistries: z.string().optional(), // One CIDR the nested engine carves container networks from; docker's default commonly collides with a corporate // VPN or LAN, silently routing some hosts into the bridge instead of the tunnel. addressPool: z.string().optional(), }); // A logged-in browser session the agent drives via Playwright MCP, one capability per account (not per platform): the // id keys the profile, login, passkey and tool prefix. `identity` shares another capability's browser and cookies (so // its own `exit`/credentials are ignored); `purpose`/`openedAt` record why and when the agent opened it, since a site // card declares no fields to carry that otherwise. export const BrowserConfigSchema = z .object({ platform: z.string().min(1), username: z.string().optional(), password: z.string().optional(), identity: z.string().optional(), purpose: z.string().optional(), // ISO-8601 date, stamped when the agent opens the account, absent for one connected by hand. openedAt: z.string().optional(), // The `exit` capability this profile browses from; ignored for an account born from an `identity`, since the // exit belongs to whoever owns the shared profile. exit: z.string().optional(), }) .catchall(z.string()); // One email identity the sandbox acts as online: one persisted Chromium profile, signed in once by the owner, that // every browser account naming it in `identity` shares. A capability, not a persona, since it holds live secrets (email // password, profile identity) that must never be committed to git. `mailbox` feeds the code-reading tool; // `openAccounts` is the explicit, off-by-default consent to sign up unattended. export const IdentityConfigSchema = z.object({ email: z.string().min(3), password: z.string().optional(), mailbox: z.string().optional(), loginUrl: z.url().optional(), openAccounts: z.enum(["on", "off"]).default("off"), // Sets browsing location for the whole shared profile at once; the only coherent place to set it, since every // account born from this identity shares one browser. exit: z.string().optional(), }); export type IdentityConfig = z.infer; // A connected device of the user's own, the inverse of `ssh`: it dials this daemon over one outbound WebSocket and // serves an MCP tool surface the daemon tunnels, never implements. Scopes are the grant and are enforced on the // machine, never here, so a compromised sandbox can't exceed what the owner ticked. // on/off, not boolean: capability configs arrive from the form as strings, and a select renders an enum. const hostScope = z.enum(["on", "off"]); export const DeviceScopesSchema = z.object({ // Run commands in a real shell (PowerShell on Windows, the login shell on Linux). Off ⇒ files/screen only. shell: hostScope.default("on"), // Create, modify and trash files under `roots`. Reads are always allowed within them; this is the write half. write: hostScope.default("off"), // Capture the screen. Off ⇒ screenshot refuses, and the agent is told so rather than getting a black frame. screen: hostScope.default("on"), // Its own switch, not part of `screen`: looking and touching are different permissions. Default off, like `write`. control: hostScope.default("off"), // Narrower than `shell`: named fleet operations — start, stop, update, reshape, remove — with no shell at all. sandboxes: hostScope.default("off"), // Its own switch under `shell`, default off while `shell` defaults on: a laptop has no disposable image to recreate // from. destructive: hostScope.default("off"), // Its own switch, default off: receiving a program this sandbox built (`devices push`) and running it under the // agent's supervision (app_start). Narrower than `shell` in what it reaches and wider in what it trusts: the program // is the sandbox's, not one already on the machine. programs: hostScope.default("off"), // One directory per line. Empty ⇒ the machine's home directory, which is what the agent reports at connect. roots: z.string().optional(), }); export type DeviceScopes = z.infer; export const DeviceConfigSchema = DeviceScopesSchema.extend({ platform: z.string().min(1) }); // An ACP agent served as a chat provider: the daemon spawns `command` as a long-lived JSON-RPC subprocess, and the // capability id becomes the provider id. `env` is a pasted KEY=VALUE block, the whole secret field; `loginCommand` runs // an interactive device-code login in a visible terminal. export const AcpAgentConfigSchema = z.object({ command: z.string().min(1), name: z.string().min(1).optional(), env: z.string().optional(), loginCommand: z.string().min(1).optional(), }); // A model API the user pointed us at: one shape for any server serving models over HTTP, local or remote, since they // differ only in the URL. `protocol` is the real fork, about the wire, not location: // openai: re-served through the bundled translator, since the harness speaks only Anthropic Messages // anthropic: the harness is pointed straight at it with the user's own key // `headers` is a pasted block for non-credential routing metadata (a tenant id); not the secret field, `apiKey` is. export const EndpointProtocolSchema = z.enum(["openai", "anthropic"]); export type EndpointProtocol = z.infer; export const EndpointConfigSchema = z.object({ // Taken verbatim, version segment included: guessing the suffix a server wants is how a working URL 404s. baseUrl: z.url(), protocol: EndpointProtocolSchema.default("openai"), apiKey: z.string().optional(), headers: z.string().optional(), }); // A model the sandbox runs itself, the managed counterpart of `endpoint`: the daemon downloads the weights, serves them // on a loopback llama-server, and registers an `endpoint/` provider like a user-added one. `model` is a Hugging // Face path (or "custom" for a direct GGUF `url`); `context`/`contextTokens` pick the served window, resolved to one // token count in exactly one place so the server's flag and the card's promise never disagree. export const LOCAL_MODEL_WINDOWS = ["16384", "32768", "65536", "131072"] as const; export type LocalModelWindow = (typeof LOCAL_MODEL_WINDOWS)[number]; // 65,536: the smallest rung a full agent turn's own tool-schema overhead actually fits in (agent/context-budget.ts); // the previous 32,768 default refused a model's first real message outright. export const LOCAL_MODEL_WINDOW_DEFAULT: LocalModelWindow = "65536"; // Bounds against a typo, not a preference: below the floor no room is left after the loop's own overhead, above the // ceiling no shipped GGUF was trained for it. export const LOCAL_MODEL_WINDOW_MIN = 2048; export const LOCAL_MODEL_WINDOW_MAX = 1_048_576; // q8_0 KV cost per token, the rate every window price derives from: 64 KiB, so the rungs come out at exactly 1, 2, 4 // and 8 GiB. One rate rather than a per-model one, because the true cost varies ~2x across the list (50–60 KB/token // measured) and a per-row figure is arithmetic somebody redoes by hand on every model added. Rounded UP into that band // rather than down: over-reserving costs a rung, under-reserving costs an allocation failure a card had promised // against. export const LOCAL_MODEL_KV_BYTES_PER_TOKEN = 65_536; // What a rung honestly serves. `instant` is the one the connect view prefetches — it downloads in under a minute and // cannot drive a full agent turn, so its catalog row is helper-only (Model.helperOnly): the chat picker does not offer // it and the daemon refuses a turn on it. `work` is what that view recommends for real use. export type LocalModelTier = "instant" | "work"; export interface LocalModelChoice { // Hugging Face owner/repo/file.gguf, exactly as LocalModelConfig.model carries it. readonly id: string; readonly label: string; // The published file's own size, not a rounded guess: the card's label, the fit arithmetic and the download // estimate all read this, so a model cannot be priced two ways. readonly weightsBytes: number; readonly tier: LocalModelTier; // The repository commit the file is fetched at, a sha and never a branch: `main` moves, and a re-upload under the // same name would change what a card promised without the card changing. readonly revision: string; // The file's own sha256 at that commit (its LFS oid). A download that does not hash to it is deleted, not served. readonly sha256: string; } // The curated list, smallest first. ponytail: sizes, revisions and digests are the Hugging Face API's answer for each // file (`/api/models//revision/main` for the sha, `/tree/` for the size and LFS oid), read 2026-09-29 — the // hand-written labels that predated them overstated gemma-4 12B by 2x and Qwen3.8 27B by a third, which is the class of // drift a machine-readable size exists to end. Bump a row's three together: each belongs to its revision. export const LOCAL_MODELS: readonly LocalModelChoice[] = [ { id: "unsloth/Qwen3.5-2B-GGUF/Qwen3.5-2B-Q4_K_M.gguf", label: "Qwen3.5 2B", weightsBytes: 1_280_835_840, tier: "instant", revision: "f6d5376be1edb4d416d56da11e5397a961aca8ae", sha256: "aaf42c8b7c3cab2bf3d69c355048d4a0ee9973d48f16c731c0520ee914699223", }, { id: "unsloth/Phi-4-mini-instruct-GGUF/Phi-4-mini-instruct-Q4_K_M.gguf", label: "Phi-4-mini 3.8B", weightsBytes: 2_491_874_272, tier: "work", revision: "78eb92a46fc37e6b524df991ed9aca9bc6aa7b80", sha256: "88c00229914083cd112853aab84ed51b87bdf6b9ce42f532d8c85c7c63b1730a", }, { id: "unsloth/Qwen3.5-9B-GGUF/Qwen3.5-9B-Q4_K_M.gguf", label: "Qwen3.5 9B", weightsBytes: 5_680_522_464, tier: "work", revision: "3885219b6810b007914f3a7950a8d1b469d598a5", sha256: "03b74727a860a56338e042c4420bb3f04b2fec5734175f4cb9fa853daf52b7e8", }, { id: "unsloth/gemma-4-12b-it-GGUF/gemma-4-12b-it-Q4_K_M.gguf", label: "Gemma 4 12B", weightsBytes: 7_121_861_440, tier: "work", revision: "fc034cfff751157913579611efad8462ac1be606", sha256: "0a270ec9fe6b34f4a0d33992b6135117b484ebc4766ab76b51d4ae8c457e4c42", }, { id: "unsloth/Qwen3.8-27B-GGUF/Qwen3.8-27B-UD-Q4_K_M.gguf", label: "Qwen3.8 27B", weightsBytes: 16_464_440_224, tier: "work", revision: "4ca720788d1e01f1bff70c033e0d0028fd02e502", sha256: "322e194ff79741c7baa497c240f677f54b201b0efab44ca8e50f122b39123482", }, ]; export const localModelChoice = (id: string): LocalModelChoice | undefined => LOCAL_MODELS.find((choice) => choice.id === id); // Exactly one row carries the instant tier; the fit module and the prefetch route take it from here rather than // repeating its id, so the prefetched bytes and the offered option cannot name different files. export const LOCAL_MODEL_INSTANT: LocalModelChoice = LOCAL_MODELS.find((choice) => choice.tier === "instant")!; export const LocalModelConfigSchema = z.object({ model: z.string().min(1), url: z.url().optional(), context: z.union([z.enum(LOCAL_MODEL_WINDOWS), z.literal("custom")]).default(LOCAL_MODEL_WINDOW_DEFAULT), // Coerced from a text field's string, like the ssh card's `port`; read only when `context` is "custom". contextTokens: z.coerce.number().int().min(LOCAL_MODEL_WINDOW_MIN).max(LOCAL_MODEL_WINDOW_MAX).optional(), }); export type LocalModelConfig = z.infer; // The sandbox wallet: a USDC balance under owner policy, spent on x402-payable endpoints. The signing key lives with // the platform, never the container; `address` is the wallet's public address, written back by apply, never typed. // Policy defaults are conservative (every payment carded); amounts are decimal strings, never floats, since the daemon // does arithmetic in atomic units. const usdAmount = z.string().regex(/^\d+(\.\d{1,6})?$/, "a USD amount like 0.50 (up to six decimals: USDC's own precision)"); export const WalletNetworkSchema = z.enum(["eip155:8453", "eip155:84532"]); export type WalletNetwork = z.infer; export const WalletConfigSchema = z.object({ // CAIP-2 chain id; Base mainnet, or Base Sepolia for a zero-real-money test mode. network: WalletNetworkSchema.default("eip155:8453"), // The wallet's public address, the platform's answer at apply time, never a form field. address: z.string().optional(), // Hard per-payment ceiling: over it the route refuses without raising a card. perPaymentMaxUsd: usdAmount.default("1.00"), // Payments at or under this settle without a card, inside the daily cap. "0" = every payment is carded. autoApproveUnderUsd: usdAmount.default("0"), // The UTC-day ceiling across all payments, carded or not. dailyCapUsd: usdAmount.default("5.00"), allow: z.string().optional(), deny: z.string().optional(), }); export type WalletConfig = z.infer; // The owner's Intentic account, reachable from inside this sandbox: what lets it create sandboxes on that account // instead of asking a person to fetch a setup code from the browser. The token is minted at Settings ▸ Tokens and is // account-scoped, so it is held by the daemon and never injected into the agent's env — the approval card on every // create is only an approval while the agent cannot mint a claim behind it. export const FleetConfigSchema = z.object({ token: z.string().min(1).describe("A provisioning token from Settings ▸ Tokens. Stored, never echoed back."), }); export type FleetConfig = z.infer; // Another of the owner's sandboxes this one may hand tasks to (schemas/links.ts). The token is an inbound link that // sandbox's owner minted, held by this daemon and never put in the agent's env, like the fleet's: the agent gets the // `delegate` tool, not the credential. What a task may do over there is that sandbox's terms, never this card's. export const LinkConfigSchema = z.object({ url: z.url().describe("The other sandbox's own address, the one its editor reaches it at."), token: z.string().min(1).describe("The link token that sandbox minted for this one. Stored, never echoed back."), name: z.string().max(100).optional().describe("What the other sandbox is called, as its owner named it."), sandboxId: z.string().max(100).optional().describe("The other sandbox's id on the owner's account."), }); export type LinkConfig = z.infer; export type McpConfig = z.infer; export type CliConfig = z.infer; export type PluginConfig = z.infer; export type ExtensionConfig = z.infer; export type SshConfig = z.infer; export type DockerConfig = z.infer; export type BrowserConfig = z.infer; export type DeviceConfig = z.infer; export type AcpAgentConfig = z.infer; export type EndpointConfig = z.infer; export const CapabilitySchema = z.discriminatedUnion("kind", [ z.object({ id: entryId, kind: z.literal("devops"), config: z.object({}) }), // A pnpm+turbo monorepo the user scaffolds as its own repo, named by `id`; apps are added afterwards from its // operator panel. z.object({ id: entryId, kind: z.literal("monorepo"), config: z.object({}) }), z.object({ id: entryId, kind: z.literal("mcp"), config: McpConfigSchema }), z.object({ id: entryId, kind: z.literal("cli"), config: CliConfigSchema }), z.object({ id: entryId, kind: z.literal("plugin"), config: PluginConfigSchema }), z.object({ id: entryId, kind: z.literal("extension"), config: ExtensionConfigSchema }), z.object({ id: entryId, kind: z.literal("ssh"), config: SshConfigSchema }), // No IFNAMSIZ cap on the id: the tunnel interface name is derived, not the id itself, so a descriptive name is // free. z.object({ id: entryId, kind: z.literal("vpn"), config: VpnConfigSchema }), // A geo exit; deliberately not a vpn arm, since it routes nothing into the main table and carries no full-tunnel // warning. z.object({ id: entryId, kind: z.literal("exit"), config: ExitConfigSchema }), // A network disk mounted under /mnt/netdisk/; its live state is on the /netdisk routes, like a vpn's. z.object({ id: entryId, kind: z.literal("netdisk"), config: NetdiskConfigSchema }), // The in-sandbox Docker Engine, baked into the image, dormant by default. No remove: de-privileging it silently // would be more destructive than useful given what runs on it. z.object({ id: entryId, kind: z.literal("docker"), config: DockerConfigSchema }), z.object({ id: entryId, kind: z.literal("browser"), config: BrowserConfigSchema }), // One email identity browser accounts are born from; they join it via their own `identity` field. z.object({ id: entryId, kind: z.literal("identity"), config: IdentityConfigSchema }), z.object({ id: entryId, kind: z.literal("device"), config: DeviceConfigSchema }), // The user's own browser, through their installed extension; `host`'s sibling, one capability per browser. Distinct // from `browser`, the sandbox's own Chromium profile: this one is the person's, already signed into everything, // only ever borrowed. z.object({ id: entryId, kind: z.literal("webext"), config: WebExtConfigSchema }), // The user's own phone, through the app they installed on it; one capability per phone. Its own kind rather than a // `device` platform: a phone has no shell and no home folder, and what it grants (screen, apps, picked folders, // notifications) is a different set of switches. z.object({ id: entryId, kind: z.literal("phone"), config: PhoneConfigSchema }), z.object({ id: entryId, kind: z.literal("agent"), config: AcpAgentConfigSchema }), // The id becomes `endpoint/` in the chat picker, the `agent` kind's precedent, since these two are the only // kinds that mint providers. z.object({ id: entryId, kind: z.literal("endpoint"), config: EndpointConfigSchema }), // Deliberately mints the same `endpoint/` ids as `endpoint`: to every consumer this is just an endpoint the // daemon happens to operate. z.object({ id: entryId, kind: z.literal("localmodel"), config: LocalModelConfigSchema }), // The sandbox's USDC wallet (WalletConfigSchema), one per sandbox; the key never enters the container. z.object({ id: entryId, kind: z.literal("wallet"), config: WalletConfigSchema }), // The owner's account, one per sandbox: the box holding this may provision sandboxes on it. z.object({ id: entryId, kind: z.literal("fleet"), config: FleetConfigSchema }), // Another of the owner's sandboxes, one card per sandbox: the id is the agent's name for it (`delegate`'s `to`). z.object({ id: entryId, kind: z.literal("link"), config: LinkConfigSchema }), ]); export type Capability = z.infer; // A credential the owner must type elsewhere (a WhatsApp pairing code); rendered large and copyable, not buried in // `detail`'s sentence. export const CapabilityStatusSchema = z.object({ state: CapabilityStateSchema.describe("Whether it is live, still coming up, broken, or switched off."), detail: z.string().optional().describe("What is wrong, in words a person can act on."), code: z.string().optional().describe("A short marker for that reason, for anything deciding what to do about it."), settling: z .boolean() .optional() .describe( "True while something under way will move this on its own: a start, a download, a pairing code waiting to be typed. Absent when only a person can move it, or a pushed change will say when it moved.", ), }); export type CapabilityStatus = z.infer; // The list row: manifest entry plus live status, secrets never returned. `secrets` names which config keys hold a // credential without carrying it, since `config` alone can't distinguish a blank field from one already holding a value // the browser isn't shown. export const CapabilitySummarySchema = z.object({ id: z.string().describe("The connection's id."), kind: CapabilityKindSchema.describe("What sort of thing it is."), status: CapabilityStatusSchema.describe("Whether it is working."), config: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).describe("Its settings, minus anything secret."), // Defaulted for the daemon-older-than-browser seam: a required field would fail the whole list parse on an older // sandbox. secrets: z .array(z.string()) .default([]) .describe("Which credentials it holds, by name. The values are on one route only, and it is not this one."), }); export type CapabilitySummary = z.infer; // A capability the workspace asks for but the manifest doesn't carry, derived from what's checked out under /work, not // from configuration; prevents illegible failures like a missing docker socket with no pointer back to the fix. Keyed // by catalog card, not kind, since several cards share one kind. Re-derived on every read, so it drops out once its // evidence moves; `prefill` is never a secret, even one sitting in a checked-in file. export const CapabilityRecommendationSchema = z.object({ entry: z.string().describe("Which catalog entry is being suggested."), evidence: z .string() .describe("What was seen that prompted it: a file, a remote, printed verbatim so the claim can be checked rather than believed."), reason: z.string().describe("The same claim in words, without repeating the evidence into it."), prefill: z .record(z.string(), z.string()) .describe( "Settings the scan could read, to fill the form so you supply only the credential. Never a secret, even when one is sitting in a checked-in file: the suggestion points at such a file, it does not absorb what is in it.", ), }); export type CapabilityRecommendation = z.infer; export const CapabilitiesListSchema = z.object({ capabilities: z.array(CapabilitySummarySchema).describe("What this sandbox is connected to."), // Defaulted for the daemon-older-than-browser seam, so an older sandbox's parse doesn't fail just to hide a badge. recommendations: z .array(CapabilityRecommendationSchema) .default([]) .describe( "Things worth connecting, worked out from what is actually in the workspace rather than from anything you configured. Re-derived on every read, so one whose evidence has moved simply stops being suggested.", ), }); export const CapabilityIdParamSchema = z.object({ id: z.string().describe("Which connection.") }); // One capability's config verbatim, secrets included, for capabilities.connection; never served to a browser, since its // handler accepts only daemon header grants, not a member identity. export const CapabilityConnectionSchema = z.object({ id: z.string().describe("The connection's id."), kind: z.string().describe("What sort of thing it is."), config: z .record(z.string(), z.string()) .describe("Its settings exactly as stored, credentials included. The field names are its own kind's, which the caller already knows."), }); export type CapabilityConnection = z.infer; // DELETE /capabilities/recommendations/{entry}: the declined evidence is recorded daemon-side, so the suggestion returns // once the workspace changes under it. export const CapabilityEntryParamSchema = z.object({ entry: z.string().describe("Which suggestion to stop making.") }); // POST /capabilities/{id}/secret: replaces just the per-kind secret field and re-runs the capability's idempotent // apply. export const CapabilitySecretInputSchema = z.object({ id: z.string().describe("Which connection."), value: z.string().min(1).describe("The new credential. Its other settings are left alone."), }); // POST /capabilities/{id}/oauth-token: a delegated sign-in's refresh token, stored on the card that declares // `refreshToken` in its manifest. Write-only: the caller proves it holds the card's current token (empty before the // first sign-in) and names the settings it signed in against, so a card edited meanwhile, or another card, is refused. export const CapabilityRefreshTokenInputSchema = z.object({ id: z.string().describe("Which connection."), binding: z .record(z.string(), z.string()) .describe("The card's settings the sign-in was made against, by field key: those its manifest's `refreshToken.boundTo` names."), previous: z.string().max(32768).describe("The token the caller holds for this card, empty before its first sign-in."), refreshToken: z.string().min(1).max(32768).describe("The new refresh token. Never echoed back."), login: z.boolean().optional().describe("A fresh interactive sign-in rather than a rotation: restarts the card's listener."), }); // POST /capabilities/{id}/rename: a capability's id is the agent's handle for it (skill file, tool prefix, env suffix, // ssh alias), so renaming is a migration, not a label edit. Which kinds may be renamed is the handler's own answer, not // this schema's. export const CapabilityRenameSchema = z.object({ id: z.string(), to: z .string() .min(1) .max(60) .regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/), }); // POST /capabilities/{id}/login: the tmux session running `loginCommand`, surfaced in the terminal panel to complete // sign-in. export const CapabilityLoginSchema = z.object({ session: z.string().describe("The terminal the sign-in is happening in. Attach to it to type.") }); // GET /capabilities/{id}/otp: a freshly minted TOTP code; the seed never crosses, `secondsRemaining` cues a re-mint // rather than a stale submit. export const CapabilityOtpSchema = z.object({ code: z.string().describe("The code."), secondsRemaining: z .number() .describe("How long it lasts. Its expiring is what makes handing one to an agent safe, since the seed behind it is never revealed."), }); // POST /capabilities/probe: did these settings actually reach the thing, asked before saving, so a form's reader gets // the service's own confirmation or refusal in place, not a later "not connected" card. `ok: false` is a reported // failure; `checked: false` means no test exists at all, a different thing that must never render as one. export const CapabilityProbeSchema = z.object({ checked: z.boolean().describe("Whether this connection can be tested from here at all. False is not a failure: it is 'no test exists'."), ok: z.boolean().describe("Whether the service answered as itself."), message: z .string() .describe("What happened, in the words a person standing in front of the form needs: the service's own answer, or its refusal."), // The account the credential turned out to be: a login, an org slug, a project name. Only when the card declares // where to read it and the service answered with one; a form offers it as the connection's name. who: z.string().optional().describe("Who the service said the credential belongs to, when it said."), }); export type CapabilityProbe = z.infer; // POST /capabilities/ssh-key: a key pair made inside the sandbox for a connection being set up. The public half is the // whole answer; the private half stays in the daemon until an add sends `stashedMarker(token)` where it goes. export const SshKeySchema = z.object({ publicKey: z.string().describe("The public half, as the one line a server's authorized_keys holds: `ssh-ed25519 AAAA… intentic-`."), token: z .string() .describe( "Stands for the private half, which never leaves the sandbox. Sent wrapped as a marker in the private key's place, it installs that key once, and it lapses after thirty minutes.", ), }); export type SshKey = z.infer;