Skip to content

fix: AISandbox vm start routes through GUI app + per-VM spawn logging#6

Merged
DaxxSec merged 1 commit into
mainfrom
claude/cli-start-fixes
May 4, 2026
Merged

fix: AISandbox vm start routes through GUI app + per-VM spawn logging#6
DaxxSec merged 1 commit into
mainfrom
claude/cli-start-fixes

Conversation

@DaxxSec
Copy link
Copy Markdown
Owner

@DaxxSec DaxxSec commented May 4, 2026

Summary

Fixes the two CLI bugs surfaced by ai-mon (the "Issue C and D" punch list).

Issue C — foreground subprocess died silently

Before: secvf-cli vm start <name> (background mode) spawned a child running secvf-cli vm start <name> --foreground with stdio sent to /dev/null, then waited 1s and asked VMProcessManager.isVMRunning. When VZVirtualMachine.start() failed (entitlements gap, missing IPSW, malformed bundle, etc.), the user saw only `"VM failed to start"` with no diagnostic — the actual error was discarded into /dev/null.

After:

  • Subprocess stdout + stderr redirected to ~/.avf/logs/<name>-start-<timestamp>.log (mode 0600, append-only). Log path is printed on success and on failure so the user has somewhere to look.
  • Post-spawn check extended from a single 1s wait to a 5s/0.5s polling loop. Slow VZ bringup no longer reported as a failure.

Issue D — CLI's VMRunner can't produce the exec-bridge socket

Before: VMRunner.createAISandboxConfiguration adds the vsock socket device on the guest side, but the host-side listener (VsockExecBridgeManager) lives in the GUI app and is only started by AppDelegate.bootAISandboxSession(). So a CLI-spawned AISandbox VM never produces /tmp/secvf-exec-<id>.sock, and --wait always times out.

After: detect osType == \"AISandbox\" before the spawn branch and route via DistributedNotificationCenter to com.secvf.cli.start. The GUI app's existing handleCLIStartVM observer already calls bootAISandboxSession() for AISandbox VMs, which sets up the bridge. The CLI subprocess is skipped entirely on this path.

If SecVF.app isn't running, the CLI fails clearly with:

Error: SecVF.app is not running. Open the app first, then re-run \`secvf-cli vm start <name>\`.

App-running detection uses NSWorkspace.shared.runningApplications against {com.ItzDaxxy.SecVF, com.DaxxSec.SecVF} (same legacy/intended bundle-id pair ISOCacheManager.verifyCallerIsMainApp historically accepted).

Plumbing

Test plan

  • swift build in SecVF/cli/ — clean (only pre-existing Swift 6 warning in VMRunner.swift)
  • xcodebuild build for the main app — clean
  • Manual: secvf-cli vm start <linux-vm> with intentionally broken bundle — confirm log path is reported and contains the actual VZ error
  • Manual: secvf-cli vm start <ai-sandbox-base> --wait with SecVF.app running — confirm the bridge socket appears and --wait returns success
  • Manual: secvf-cli vm start <ai-sandbox-base> with SecVF.app NOT running — confirm clear error message

🤖 Generated with Claude Code

Two bugs surfaced from ai-mon ("Issue C and D" punch list):

Issue C — foreground subprocess died silently
  - vm start (background mode) spawned `secvf-cli vm start <name> --foreground`
    with stdio sent to /dev/null, then waited 1s and asked
    VMProcessManager.isVMRunning. When VZVirtualMachine.start() failed
    (entitlements gap, missing IPSW, malformed bundle), the user only saw
    "VM failed to start" with no diagnostic — the subprocess's error was
    in /dev/null.
  - Fix: redirect the foreground subprocess's stdout+stderr to a per-VM
    log under ~/.avf/logs/<name>-start-<timestamp>.log, opened via
    open(2) with O_WRONLY|O_CREAT|O_APPEND and 0600 perms. Path is
    printed on success and on failure so the user has somewhere to look.
    Also extended the post-spawn check from a single 1s wait to a
    5s/0.5s polling loop so slow VZ bringup isn't reported as failure.

Issue D — CLI's VMRunner can't produce the exec-bridge socket
  - VMRunner.createAISandboxConfiguration adds the vsock socket DEVICE on
    the guest side, but the host-side LISTENER (VsockExecBridgeManager)
    lives in the GUI app and is only started by AppDelegate.bootAISandboxSession().
    A CLI-spawned VM has no /tmp/secvf-exec-<id>.sock, so --wait always
    times out for AISandbox VMs.
  - Fix: detect osType == "AISandbox" before the spawn branch and route
    via DistributedNotificationCenter to com.secvf.cli.start. The GUI
    app's existing handleCLIStartVM observer already calls
    bootAISandboxSession() for AISandbox VMs, which sets up the bridge.
    The CLI subprocess is skipped entirely on this path.
  - If SecVF.app isn't running, fail clearly:
    "SecVF.app is not running. Open the app first, then re-run …"
  - Detection uses NSWorkspace.shared.runningApplications against
    {com.ItzDaxxy.SecVF, com.DaxxSec.SecVF} (same legacy/intended ID
    pair ISOCacheManager already accepts).

Plumbing:
  - Added isSecVFAppRunning() and vmStartLogPath(for:) helpers at file
    scope in VMCommand.swift.
  - Imported AppKit (NSWorkspace) — available on macOS 14+; no Package.swift
    change needed.

Build: clean. CLI swift build + Xcode build both succeed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@DaxxSec DaxxSec merged commit db95f4c into main May 4, 2026
DaxxSec pushed a commit that referenced this pull request May 13, 2026
Works through every item from the review's suggestions table.

#1 SecVFError: extract `tokenizeHomePath(_:home:)` static helper from
   logToAudit so the home-redaction logic is independently testable.
   Test now exercises the production helper directly (was previously
   re-implementing the replacement and asserting on stdlib API).
   Added 2 extra cases: leaves non-home paths alone, replaces every
   occurrence in a single line.

#2 VMLibraryWindowController.deleteVM: remove the deleted VM's UUID
   from `runningFilterIDs` so the focus filter doesn't ghost a
   phantom entry. Reset to nil if the set goes empty.

#3 TacticalTableRowView.drawSeparator: comment claimed a "skip last
   row" behavior that wasn't actually implemented. Updated the
   comment to reflect reality (the host NSTableView controls whether
   drawSeparator is called for the last row via gridStyleMask /
   intercellSpacing).

#4 PacketFilterPresets: add `populateMenu(_:target:action:)` overload
   that appends preset items onto an existing menu. buildMenu now
   delegates to it. PacketAnalysisWindowController now calls
   populateMenu directly on the popup's existing menu, preserving
   the title item without the allocate-then-copy-every-item dance.

#5 VMConnectionOverlayView.draw: documented the latent horizontal-
   scroller caveat. `bounds.maxX` tracks content width (overlay is a
   table subview); when the table has no horizontal scroller (today)
   visible == content so this is fine. Comment now points future
   maintainers at `clipView.visibleRect.maxX` if/when that changes.

#6 AppDelegate.handleFocusVMConsole: switched from Swift string
   interpolation inside NSLog's format string to printf-style %@
   formatting matching the rest of the file (and avoiding format-
   string concerns).

#7 VMManager.networkPeers: added a perf comment documenting the
   O(routers × n) cost in refreshConnectionOverlay's loop and the
   `[routerVMId: [guests]]` index hint if the library ever grows
   into the hundreds of VMs.

#8 AppColorsTests.testCyanAliasesPointToODGreen: replaced NSColor
   instance equality with a component-by-component comparison via
   a new `assertSameColor` helper. Test now still passes if a
   future refactor expresses the alias as two literal NSColors with
   matching components — what matters is the *visible* contract,
   not Swift-level reference equality. Tolerance: 0.001 per channel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant