Merge nucleic/olive-iris-panda-dfdi into dev
This commit is contained in:
@@ -772,6 +772,18 @@ public actor MCPApprovalServer {
|
||||
/// directly (the handler is the sole gate); never routed through Claude's permission path,
|
||||
/// so `auto` mode can't auto-approve it. Only advertised when a handler is registered.
|
||||
public static let qualifiedHostExecToolName = "mcp__nucleic__host_exec"
|
||||
/// Appended to EVERY macOS-VM tool description. Concurrent macOS guests are hard-capped by macOS
|
||||
/// itself (``MacVMSettings/maxConcurrentVMs``), so one agent parking an idle VM starves every
|
||||
/// other agent's boot — that cap is why this notice rides on the tools that *use* the VM, not
|
||||
/// just on `mac_vm_control`. Linux VMs carry no such cap and deliberately omit it.
|
||||
public static let macVMFreeWhenIdleNotice =
|
||||
"MANDATORY — concurrent macOS VMs are hard-capped across ALL agents, and one you leave "
|
||||
+ "running blocks every other agent (and your own later work) from booting one. The moment "
|
||||
+ "you are no longer ACTIVELY using this VM you MUST free it with `mac_vm_control` — even "
|
||||
+ "mid-turn, between two steps of the same task, before you read files, think, edit code, or "
|
||||
+ "run container work: `suspend` when you'll need it again (it saves all state to disk and "
|
||||
+ "frees the slot), `stop` when the in-VM state is disposable, `kill` when you're done with "
|
||||
+ "it entirely. Never leave an idle macOS VM running."
|
||||
public static let macVMExecToolName = "mac_vm_exec"
|
||||
/// Fully-qualified name — pre-allowed like `host_exec` so the call reaches our handler directly
|
||||
/// (the handler is the sole gate). Only advertised when a handler is registered.
|
||||
@@ -1492,12 +1504,22 @@ public actor MCPApprovalServer {
|
||||
}
|
||||
|
||||
/// The `tools/list` entry for a VM lifecycle tool (`mac_vm_control` / `linux_vm_control`) — the two
|
||||
/// differ only by name and the guest-OS word in the prose, so they share one schema builder.
|
||||
/// `sibling` is the exec tool that boots the VM, named so the agent knows what it's controlling.
|
||||
/// differ only by name, the guest-OS word in the prose, and whether the guest is capped, so they
|
||||
/// share one schema builder. `sibling` is the exec tool that boots the VM, named so the agent
|
||||
/// knows what it's controlling. `capped` is true only for macOS: macOS itself limits concurrent
|
||||
/// guests (``MacVMSettings/maxConcurrentVMs``), so an idle macOS VM starves other agents and MUST
|
||||
/// be freed. Linux VMs have no such ceiling, so their entry asks only for ordinary RAM courtesy —
|
||||
/// claiming a cap there would be false and would push needless suspend/resume churn.
|
||||
private static func vmControlToolSchema(
|
||||
name: String, osLabel: String, sibling: String
|
||||
name: String, osLabel: String, sibling: String, capped: Bool
|
||||
) -> JSONValue {
|
||||
.object([
|
||||
let lifecycleDuty = capped
|
||||
? macVMFreeWhenIdleNotice + " It's also torn down for you at session end."
|
||||
: "\(osLabel) VMs are NOT capped the way macOS VMs are, so there is no slot to race for "
|
||||
+ "— you may leave this one running across a task. Still, `suspend` or `stop` one "
|
||||
+ "you're done with to give the host its RAM back. It's also torn down for you at "
|
||||
+ "session end."
|
||||
return .object([
|
||||
"name": .string(name),
|
||||
"description": .string(
|
||||
"Manage the lifecycle of THIS session's isolated \(osLabel) VM — the one `\(sibling)` "
|
||||
@@ -1511,11 +1533,7 @@ public actor MCPApprovalServer {
|
||||
+ "VM holds); `kill` powers it off AND deletes its disk clone (the next command "
|
||||
+ "boots a fresh one — you lose any state you built up inside it). These ops act only "
|
||||
+ "on your own disposable VM, so they need NO approval. Returns {ok, state, message}. "
|
||||
+ "Concurrent VMs are hard-capped across ALL agents, so free your slot the moment "
|
||||
+ "the VM goes idle — even mid-turn, between two steps of the same task: `suspend` "
|
||||
+ "whenever you'll need it again (it keeps all state, however long you stay away), "
|
||||
+ "`stop` only if the in-VM state is disposable — don't leave a VM running idle and "
|
||||
+ "blocking other agents. It's also torn down for you at session end."),
|
||||
+ lifecycleDuty),
|
||||
"inputSchema": .object([
|
||||
"type": .string("object"),
|
||||
"properties": .object([
|
||||
@@ -2063,7 +2081,8 @@ public actor MCPApprovalServer {
|
||||
+ "single shell `command` (combine build + run, e.g. \"xcodebuild "
|
||||
+ "-scheme App test\"), plus `reason`: why this needs the macOS VM "
|
||||
+ "specifically. Returns {exit_code, stdout, stderr}. Every call needs "
|
||||
+ "explicit user approval and is NEVER auto-approved."),
|
||||
+ "explicit user approval and is NEVER auto-approved. "
|
||||
+ Self.macVMFreeWhenIdleNotice),
|
||||
"inputSchema": .object([
|
||||
"type": .string("object"),
|
||||
"properties": .object([
|
||||
@@ -2093,7 +2112,8 @@ public actor MCPApprovalServer {
|
||||
+ "notifications clear immediately and it relaunches clean. Takes no "
|
||||
+ "arguments. Acts only on your own disposable VM, so it needs NO approval. "
|
||||
+ "Returns {exit_code, stdout, stderr}. After calling it, take a fresh "
|
||||
+ "`mac_vm_computer` screenshot to confirm the view is clear."),
|
||||
+ "`mac_vm_computer` screenshot to confirm the view is clear. "
|
||||
+ Self.macVMFreeWhenIdleNotice),
|
||||
"inputSchema": .object([
|
||||
"type": .string("object"),
|
||||
"properties": .object([:]),
|
||||
@@ -2149,7 +2169,7 @@ public actor MCPApprovalServer {
|
||||
+ "don't hold back or ask permission for changes inside it. Prefer "
|
||||
+ "`mac_vm_exec` for headless "
|
||||
+ "build/test commands; use this when you need to look at or operate the "
|
||||
+ "screen."),
|
||||
+ "screen. " + Self.macVMFreeWhenIdleNotice),
|
||||
"inputSchema": .object([
|
||||
"type": .string("object"),
|
||||
"properties": .object([
|
||||
@@ -2219,7 +2239,8 @@ public actor MCPApprovalServer {
|
||||
+ "order. Like `mac_vm_computer` this runs in the VM sandbox and is NOT "
|
||||
+ "per-action approval-gated. Prefer the single-action `mac_vm_computer` "
|
||||
+ "when you need to SEE the result before deciding the next action; use "
|
||||
+ "this when the plan is already known."),
|
||||
+ "this when the plan is already known. "
|
||||
+ Self.macVMFreeWhenIdleNotice),
|
||||
"inputSchema": Self.computerBatchInputSchema(
|
||||
actionEnumDescription:
|
||||
"One of: screenshot, left_click, right_click, double_click, mouse_move, "
|
||||
@@ -2255,7 +2276,8 @@ public actor MCPApprovalServer {
|
||||
+ "rather than you doing it. Returns {completed, note}: `completed` is "
|
||||
+ "whether the user says they did it, `note` is any message they left — "
|
||||
+ "read both, then continue (re-screenshot the VM to confirm state). Every "
|
||||
+ "call needs explicit user consent and is NEVER auto-approved."),
|
||||
+ "call needs explicit user consent and is NEVER auto-approved. "
|
||||
+ Self.macVMFreeWhenIdleNotice),
|
||||
"inputSchema": .object([
|
||||
"type": .string("object"),
|
||||
"properties": .object([
|
||||
@@ -2784,13 +2806,13 @@ public actor MCPApprovalServer {
|
||||
if macVMControlHandlers[token] != nil {
|
||||
tools.append(Self.vmControlToolSchema(
|
||||
name: Self.macVMControlToolName, osLabel: "macOS",
|
||||
sibling: Self.macVMExecToolName))
|
||||
sibling: Self.macVMExecToolName, capped: true))
|
||||
}
|
||||
// Only advertise linux_vm_control when this session can use the Linux VM.
|
||||
if linuxVMControlHandlers[token] != nil {
|
||||
tools.append(Self.vmControlToolSchema(
|
||||
name: Self.linuxVMControlToolName, osLabel: "Linux",
|
||||
sibling: Self.linuxVMExecToolName))
|
||||
sibling: Self.linuxVMExecToolName, capped: false))
|
||||
}
|
||||
return rpcResult(id: id, result: .object(["tools": .array(tools)]))
|
||||
|
||||
|
||||
@@ -382,7 +382,10 @@ public actor SessionController {
|
||||
that never exits is killed after a timeout and you get its partial output plus a note back \
|
||||
(rather than an infinite hang). Like host_exec, calling the tool \
|
||||
IS how you request approval; pass a `reason` explaining why the task needs the macOS \
|
||||
toolchain. The first call boots the VM (a minute or two); later calls reuse it.
|
||||
toolchain. The first call boots the VM (a minute or two); later calls reuse it. \
|
||||
MANDATORY: the moment you are no longer ACTIVELY using the VM you MUST free it with \
|
||||
`mac_vm_control` (`suspend` to keep its state, `stop`/`kill` if you don't need it) — \
|
||||
macOS VMs are hard-capped across all agents, so never leave one idle.
|
||||
""" : ""
|
||||
let computer = allowMacVMComputer ? "\n\n" + """
|
||||
You can also SEE and OPERATE the macOS VM's screen with the `mac_vm_computer` tool — use \
|
||||
@@ -423,7 +426,9 @@ public actor SessionController {
|
||||
slides in and obstructs part of the screen — covering a control you need or hiding what \
|
||||
you're reading — call `mac_vm_clear_notifications` to dismiss the on-screen notifications \
|
||||
(it kills the guest's NotificationCenter, which relaunches clean); it takes no arguments \
|
||||
and needs no approval, then re-screenshot.
|
||||
and needs no approval, then re-screenshot. And the same MANDATORY rule applies to \
|
||||
computer-use: the moment you stop ACTIVELY driving the screen, free the VM with \
|
||||
`mac_vm_control` rather than leaving it idle.
|
||||
""" : ""
|
||||
// macOS-VM lifecycle: exposed whenever the macOS VM is usable (exec or computer-use), so the
|
||||
// agent can free resources or reset the VM it booted without ending the session. VMs are a
|
||||
@@ -436,12 +441,14 @@ public actor SessionController {
|
||||
**frees its slot** so another VM can boot in its place, and `resume` restores it from that \
|
||||
saved state (the next VM command auto-resumes too); `restart` reboots it in place to reclaim \
|
||||
RAM; `kill` deletes its disk clone so the next VM command boots a fresh one. These act \
|
||||
only on your own disposable VM, so they need NO approval. IMPORTANT — VMs are a scarce, \
|
||||
hard-capped resource: at most \(maxConcurrentVMs) can run concurrently across ALL agents, \
|
||||
so a VM you leave running idle blocks other agents (and your own later work) from booting \
|
||||
one, and a fresh boot fails outright once the cap is hit. NEVER leave a VM running that \
|
||||
you are not actively using — `suspend` it the moment it goes idle, even if that is in \
|
||||
the middle of the very turn you booted it in. Do not hold it "just in case" while you \
|
||||
only on your own disposable VM, so they need NO approval. IMPORTANT — macOS VMs are a \
|
||||
scarce, hard-capped resource: at most \(maxConcurrentVMs) can run concurrently across ALL \
|
||||
agents, so a VM you leave running idle blocks other agents (and your own later work) from \
|
||||
booting one, and a fresh boot fails outright once the cap is hit. You MUST free your VM \
|
||||
the moment you are no longer ACTIVELY using it — `suspend` it as soon as it goes idle, \
|
||||
even if that is in \
|
||||
the middle of the very turn you booted it in, and `kill` it once you're finished with it \
|
||||
for good. Do not hold it "just in case" while you \
|
||||
read files, think, edit code, or run container work: those steps don't touch the Mac, so \
|
||||
free the slot before them and resume after. Suspend is the default: it saves the VM's full \
|
||||
state to disk, keeps all of it, and — unlike a plain freeze — frees the slot so another VM \
|
||||
@@ -501,19 +508,19 @@ public actor SessionController {
|
||||
control per-step screenshots, settle delay, and failure handling).
|
||||
""" : ""
|
||||
// Linux-VM lifecycle: the Linux sibling of `mac_vm_control`, exposed whenever the Linux VM is
|
||||
// usable (exec or computer-use).
|
||||
// usable (exec or computer-use). Deliberately NOT under the macOS scarcity rule —
|
||||
// `maxConcurrentVMs` is a macOS-guest ceiling (`MacVMSettings`, see docs/MACOS_VM.md §10.1)
|
||||
// that Linux guests don't share, so telling the agent to race for a slot here would be false
|
||||
// and would buy nothing but suspend/resume churn.
|
||||
let linuxVMControl = (allowLinuxVMExec || allowLinuxVMComputer) ? "\n\n" + """
|
||||
You can manage that Linux VM's lifecycle with `linux_vm_control` — the sibling of \
|
||||
`mac_vm_control`, same ops (`status`, `stop`, `suspend`, `resume`, `restart`, `kill`) \
|
||||
acting on your own disposable Linux VM, no approval needed. The same scarcity rule \
|
||||
applies: at most \(maxConcurrentVMs) VMs run concurrently across all agents, so NEVER \
|
||||
leave one running that you aren't actively using. `suspend` it the moment it goes idle — \
|
||||
even mid-turn, between two steps of the same task, while you read files or run container \
|
||||
work — rather than holding it "just in case". That's the default: it saves the VM's state \
|
||||
to disk and frees the slot for another VM, keeping everything. Then `resume` it (or just \
|
||||
issue any VM command, which auto-resumes) when you come back — it restores from disk, a bit \
|
||||
slower than a plain thaw but lossless. Reach for `stop` only when you're done for a long while and want the \
|
||||
RAM back. It's torn down for you at session end; `restart`/`kill` reset it mid-session.
|
||||
acting on your own disposable Linux VM, no approval needed. Unlike macOS VMs, Linux VMs \
|
||||
are NOT capped across agents, so there is no slot to free and you can leave this one \
|
||||
running across a task without starving anyone. Still, when you're done with it for a \
|
||||
while, `suspend` it (saves its full state to disk; `resume`, or any VM command, brings it \
|
||||
back exactly where it left off) or `stop` it to give the host its RAM back. It's torn \
|
||||
down for you at session end; `restart`/`kill` reset it mid-session.
|
||||
""" : ""
|
||||
// The escalation ladder — a short policy that ties the rungs together, naming only the tools
|
||||
// actually exposed this run so it never points the agent at an unavailable tool.
|
||||
|
||||
@@ -380,6 +380,50 @@ import Testing
|
||||
]))
|
||||
}
|
||||
|
||||
@Test func everyMacVMToolTellsTheAgentToFreeTheVMWhenIdle() async throws {
|
||||
// macOS guests are hard-capped (`MacVMSettings.maxConcurrentVMs`), so an agent parking an
|
||||
// idle VM starves every other agent's boot. The duty therefore rides on EVERY macOS-VM tool
|
||||
// description — the agent must see it wherever it touches the VM, not only if it happens to
|
||||
// read `mac_vm_control`. Linux VMs have no such cap, so their tools must NOT carry it.
|
||||
let server = MCPApprovalServer()
|
||||
let port = try await server.start()
|
||||
defer { Task { await server.stop() } }
|
||||
|
||||
let runtime = ClaudeCodeBackend(
|
||||
containerManager: ContainerManager(), macVMManager: MacVMManager())
|
||||
let run = RunSpec(
|
||||
sessionID: SessionID(rawValue: "vm-idle-duty"), worktree: "/repo",
|
||||
prompt: AgentInput(parts: []), allowHostExec: true, allowMacVMExec: true,
|
||||
allowMacVMComputer: true, allowLinuxVMExec: true, allowLinuxVMComputer: true,
|
||||
allowAgentContainers: true)
|
||||
await runtime.registerPlatformTools(on: server, token: "idle-duty", run: run)
|
||||
|
||||
let listed = try await post(
|
||||
["jsonrpc": "2.0", "id": 1, "method": "tools/list"],
|
||||
port: port, token: "idle-duty")
|
||||
var descriptions: [String: String] = [:]
|
||||
for tool in listed.body?["result"]?["tools"]?.arrayValue ?? [] {
|
||||
guard let name = tool["name"]?.stringValue,
|
||||
let description = tool["description"]?.stringValue
|
||||
else { continue }
|
||||
descriptions[name] = description
|
||||
}
|
||||
let notice = MCPApprovalServer.macVMFreeWhenIdleNotice
|
||||
for name in [
|
||||
"mac_vm_exec", "mac_vm_computer", "mac_vm_computer_batch", "mac_vm_control",
|
||||
"mac_vm_clear_notifications", "mac_vm_request_operator",
|
||||
] {
|
||||
#expect(descriptions[name]?.contains(notice) == true, "\(name) omits the free-when-idle duty")
|
||||
}
|
||||
for name in [
|
||||
"linux_vm_exec", "linux_vm_computer", "linux_vm_computer_batch", "linux_vm_control",
|
||||
] {
|
||||
#expect(descriptions[name]?.contains(notice) == false, "\(name) borrowed the macOS cap prose")
|
||||
}
|
||||
// The Linux lifecycle tool says the opposite out loud, so the agent doesn't infer the cap.
|
||||
#expect(descriptions["linux_vm_control"]?.contains("NOT capped the way macOS VMs are") == true)
|
||||
}
|
||||
|
||||
@Test func advertisesAndServesHostExecOnlyWhenRegistered() async throws {
|
||||
let server = MCPApprovalServer()
|
||||
let port = try await server.start()
|
||||
|
||||
@@ -901,28 +901,46 @@ import UniformTypeIdentifiers
|
||||
}
|
||||
|
||||
@Test func vmLifecycleGuidanceInstructsProactiveFreeingUnderTheCap() {
|
||||
// Both control blocks must push the agent to free its own VM the moment it goes idle — VMs
|
||||
// are hard-capped across agents, so an idle VM starves the pool. "Idle" includes mid-turn
|
||||
// gaps (reading files, container work), not just the end of a stretch of VM work, so both
|
||||
// blocks say so explicitly. The cap number is threaded in, not hardcoded, so the prose stays
|
||||
// truthful when the user overrides `maxConcurrentVMs`.
|
||||
// The macOS control block must push the agent to free its own VM the moment it stops using
|
||||
// it — macOS guests are hard-capped across agents, so an idle one starves the pool. "Idle"
|
||||
// includes mid-turn gaps (reading files, container work), not just the end of a stretch of
|
||||
// VM work, so the block says so explicitly. The cap number is threaded in, not hardcoded, so
|
||||
// the prose stays truthful when the user overrides `maxConcurrentVMs`.
|
||||
let mac = SessionController.sandboxBuildGuidance(
|
||||
allowHostExec: true, allowMacVMExec: true, maxConcurrentVMs: 2)
|
||||
#expect(mac.contains("at most 2 can run concurrently"))
|
||||
#expect(mac.contains("NEVER leave a VM running that you are not actively using"))
|
||||
#expect(mac.contains("`suspend` it the moment it goes idle"))
|
||||
#expect(mac.contains("You MUST free your VM the moment you are no longer ACTIVELY using it"))
|
||||
#expect(mac.contains("even if that is in the middle of the very turn you booted it in"))
|
||||
#expect(mac.contains("free the slot before them and resume after"))
|
||||
// A non-default cap flows through verbatim.
|
||||
let macFour = SessionController.sandboxBuildGuidance(
|
||||
allowHostExec: true, allowMacVMExec: true, maxConcurrentVMs: 4)
|
||||
#expect(macFour.contains("at most 4 can run concurrently"))
|
||||
}
|
||||
|
||||
@Test func macVMUseGuidanceCarriesTheFreeWhenIdleDuty() {
|
||||
// The duty rides on the exec and computer-use blocks too, not just the lifecycle block — the
|
||||
// agent decides to *use* the VM there, and that's where it needs to know it owns freeing it.
|
||||
let exec = SessionController.sandboxBuildGuidance(
|
||||
allowHostExec: true, allowMacVMExec: true, allowMacVMComputer: false)
|
||||
#expect(exec.contains(
|
||||
"the moment you are no longer ACTIVELY using the VM you MUST free it with "
|
||||
+ "`mac_vm_control`"))
|
||||
let computer = SessionController.sandboxBuildGuidance(
|
||||
allowHostExec: true, allowMacVMExec: false, allowMacVMComputer: true)
|
||||
#expect(computer.contains("the moment you stop ACTIVELY driving the screen, free the VM"))
|
||||
}
|
||||
|
||||
@Test func linuxVMLifecycleGuidanceClaimsNoConcurrencyCap() {
|
||||
// `maxConcurrentVMs` is a macOS-guest ceiling; Linux guests don't share it. The Linux block
|
||||
// must not borrow the macOS scarcity prose — that would be false and would buy nothing but
|
||||
// suspend/resume churn — while still asking for ordinary RAM courtesy.
|
||||
let linux = SessionController.sandboxBuildGuidance(
|
||||
allowHostExec: true, allowLinuxVMExec: true, maxConcurrentVMs: 2)
|
||||
#expect(linux.contains("at most 2 VMs run concurrently"))
|
||||
#expect(linux.contains("NEVER leave one running that you aren't actively using"))
|
||||
#expect(linux.contains("`suspend` it the moment it goes idle"))
|
||||
#expect(linux.contains("even mid-turn, between two steps of the same task"))
|
||||
#expect(linux.contains("Unlike macOS VMs, Linux VMs are NOT capped across agents"))
|
||||
#expect(!linux.contains("at most 2 VMs run concurrently"))
|
||||
#expect(!linux.contains("NEVER leave one running"))
|
||||
#expect(linux.contains("give the host its RAM back"))
|
||||
}
|
||||
|
||||
// MARK: - Computer-use command builders
|
||||
|
||||
@@ -167,6 +167,14 @@ session's own disposable Linux VM. Advertised (and pre-allowed) whenever the Lin
|
||||
`MCPApprovalServer.VMControlCall`/`VMControlReply` and `ClaudeCodeBackend.performVMControl` with the
|
||||
macOS tool — see [MACOS_VM.md §8.3](MACOS_VM.md) for the full op table and rationale.
|
||||
|
||||
One deliberate divergence: the prose. `maxConcurrentVMs` is a **macOS-guest** ceiling (macOS itself
|
||||
limits simultaneous macOS guests — [MACOS_VM.md §10.1](MACOS_VM.md)), and Linux guests don't share
|
||||
it. So the macOS tools carry `MCPApprovalServer.macVMFreeWhenIdleNotice` — a MANDATORY "free the VM
|
||||
the moment you stop actively using it" duty, since one agent parking an idle macOS VM blocks every
|
||||
other agent's boot — while `linux_vm_control` says the opposite out loud ("NOT capped the way macOS
|
||||
VMs are"). Borrowing the macOS scarcity prose here would be false and would buy nothing but
|
||||
suspend/resume churn.
|
||||
|
||||
## Settings
|
||||
|
||||
`MacVMSettings` (in `Project.swift`), all off/pinned by default:
|
||||
|
||||
+14
-2
@@ -427,8 +427,8 @@ manager already owns. One `op`:
|
||||
| --- | --- |
|
||||
| `status` | Read-only: reports `running` / `suspended` / `stopped` (disk clone present) / `absent`. |
|
||||
| `stop` | Power off, keep the disk clone (frees host RAM; the next VM command reboots it). |
|
||||
| `suspend` | Pause/freeze in place (`VZVirtualMachine.pause` — keeps RAM, halts CPU). |
|
||||
| `resume` | Thaw a suspended guest (`VZVirtualMachine.resume`). Reuse (`ensureRunning`) also auto-resumes, so `suspend` is transparent to a later exec. |
|
||||
| `suspend` | Pause, save the runtime state to disk, power off — frees the guest's RAM **and its slot** (falls back to a RAM-pause if the save can't be made). |
|
||||
| `resume` | Restore a suspended guest from its saved state. Reuse (`ensureRunning`) also auto-resumes, so `suspend` is transparent to a later exec. |
|
||||
| `restart` | Stop → reboot in place (reclaims the RAM a long-running VM holds). |
|
||||
| `kill` | Stop **and** delete the disk clone (the next VM command boots a fresh one). |
|
||||
|
||||
@@ -440,6 +440,18 @@ advertised (and pre-allowed) whenever the session can use the macOS VM at all, i
|
||||
[LINUX_VM.md](LINUX_VM.md)); both share `MCPApprovalServer.VMControlCall`/`VMControlReply` and
|
||||
`ClaudeCodeBackend.performVMControl`.
|
||||
|
||||
**The free-when-idle duty.** Because concurrent macOS guests are hard-capped (`maxConcurrentVMs`,
|
||||
§10.1), an agent that parks an idle VM blocks every *other* agent's boot — so the duty to free it is
|
||||
stated as MANDATORY, and `MCPApprovalServer.macVMFreeWhenIdleNotice` is appended to **every** macOS-VM
|
||||
tool description (`mac_vm_exec`, `mac_vm_computer`, `mac_vm_computer_batch`,
|
||||
`mac_vm_clear_notifications`, `mac_vm_request_operator`, `mac_vm_control`), not just the lifecycle
|
||||
tool: the agent has to see it wherever it touches the VM, not only if it happens to read
|
||||
`mac_vm_control`. It says: the moment you stop *actively* using the VM — including mid-turn, before
|
||||
reading files or running container work — `suspend` (need it again), `stop` (state is disposable), or
|
||||
`kill` (done with it). `SessionController.sandboxBuildGuidance` repeats it in the system prompt. The
|
||||
Linux tools deliberately carry the *opposite* wording, since Linux guests are uncapped
|
||||
([LINUX_VM.md](LINUX_VM.md) §Lifecycle control).
|
||||
|
||||
---
|
||||
|
||||
## 9. Settings
|
||||
|
||||
Reference in New Issue
Block a user