Skip to content

Add grok as a guest agent - #473

Open
alisaifee wants to merge 6 commits into
trailofbits:mainfrom
alisaifee:main
Open

alisaifee wants to merge 6 commits into
trailofbits:mainfrom
alisaifee:main

Conversation

@alisaifee

@alisaifee alisaifee commented Sep 11, 2026

Copy link
Copy Markdown

Problem

coop can launch Claude Code and Codex in the guest. Grok Build has to be
installed and configured by hand.

Description

coop grok launches Grok Build in the guest. coop setup installs it from
https://x.ai/cli/install.sh as the guest user. The image ships home skel
files as root, so setup chown -Rs the guest home first. The launcher
SSHes in with --always-approve, --trust, and --cwd /workspace.
--ask adds --permission-mode default. login and logout keep
--trust and --cwd /workspace.

Changes

  • Golden image: official installer, ~/.grok/bin on PATH,
    /usr/local/bin/grok, grok-yolo. The guest installers share one
    shell; Grok deletes its temp installer after it runs.
  • [grok]: api_key, env_forward, config_dir, marketplaces,
    plugins, Model Context Protocol servers.
  • Bootstrap: XAI_API_KEY goes in over SSH SendEnv. Allowlisted host
    files overlay into ~/.grok. Restart overwrites files still present
    on the host; guest-only files and config_dir = false keep prior
    copies. Directory symlinks, hidden directories, and bare git repos
    stay on the host. Host config.toml is merged except [plugins];
    guest [plugins] is kept. /workspace is recorded in
    trusted_folders.toml. A copied auth.json is mode 0600.
    Configured plugins install on first boot.
  • coop agent update --grok runs grok update. The guest user can run
    grok update directly. --check prints Claude Code, Codex, and
    Grok Build.
  • Lima: growing an instance disk updates lima.yaml disk: and
    restores the original yaml if the grow fails.
  • Docs: docs/grok-integration.md, commands, getting started, trust
    model, changelog. --no-agents names Grok Build.
  • Integration: binary, symlink, PATH, grok-yolo, settings merge after
    restart, --check names all three. The suite sets
    [grok] config_dir = false. The merge phase points config_dir at a
    fixture directory.

Existing images: coop setup --rebuild. Existing VMs:
coop restore <vm> --image <image> --reprovision (or destroy and
recreate).

Validation

  • Lima guest suite
  • Firecracker guest suite

@CLAassistant

CLAassistant commented Sep 11, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Add the official installer, put grok and agent on the guest PATH, and
require the binary in the golden image. Marketplace baking comes later.
Parse api_key, env_forward, config_dir, marketplaces, plugins, and
Model Context Protocol servers the same way [claude] and [codex] work.
Add coop grok, forward XAI_API_KEY, and copy host allowlisted files
without following directory symlinks. Write managed settings and folder
trust, drop the host [plugins] table, set a copied auth.json to 0600,
and install configured plugins. coop agent update --grok runs grok update.
Add the integration guide and update the command, config, trust-model,
and getting-started docs so the third agent is listed with Claude and Codex.
A developer ~/.grok/skills can hold gigabytes of git lore and
venvs. Copying that into every VM start filled the Lima disk
and left read-only packs that a second scp could not overwrite.
The restart remove quoted ~ so it never expanded.

Skip hidden and bare-git directories, leave ~/ unquoted in the
guest remove, and isolate the integration suite from host ~/.grok.
Growing a disk with truncate left lima.yaml at the old size, so
the next start looked like a shrink and Lima refused to boot.
Update disk: with the grow, and revert the yaml if truncate fails.
@alisaifee
alisaifee marked this pull request as ready for review September 11, 2026 15:52
Comment thread src/backend.rs
// A previous boot may have copied read-only files. scp cannot
// overwrite those; replace the dest directory first.
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
target.exec(remove_guest_staged_dir(guest_subdir, name))?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please preserve guest-only files when refreshing these directories. This shared helper also copies Claude rules/ and commands/ and Codex prompts/. If the host supplies one of those directories, restarting now deletes files created only in the guest; the previous recursive copy was additive. Removing the directory before transfer also leaves it missing or partially restored if SCP fails. Could the read-only-file handling preserve existing entries and avoid deleting the destination before a successful transfer?

Comment thread src/backend.rs
toml::Value::String("always-approve".to_string()),
);

if root.remove("plugins").is_some() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removes guest plugin activation as well as imported host settings. bootstrap_grok calls this merge on every boot, even with config_dir = false, but installs plugins only on first boot and only for the delta missing from the image. In the inspected upstream implementation, installation writes [plugins].enabled, and discovery disables user/marketplace plugins absent from that list. Consequently, this loses activation on restart, or on first boot for Lima-baked plugins. Please preserve guest plugin state separately from the host table being imported. The upstream behavior is visible in installation/config handling and discovery; I did not verify an installed Grok artifact against that source revision.

Comment thread src/lima.rs
let disk_value = format!("\"{}GiB\"", new_size.as_u32());
let edited = set_yaml_scalar(&original, "disk", &disk_value)
.with_context(|| format!("No top-level 'disk' key in {}", yaml_path.display()))?;
crate::fs_util::atomic_write_with_mode(&yaml_path, &edited, 0o644)?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please restore the original YAML when truncate cannot be spawned, too. This write happens before .status().context(...) ?, which returns immediately if the executable is missing or cannot run. The restoration below only handles a successfully spawned process returning a nonzero status, so a spawn failure reports an unsuccessful resize while leaving the enlarged disk setting persisted. Both failure paths should restore the original configuration.

Comment thread tests/integration.sh
if coop_exec /home/ubuntu/.grok/bin/grok --version >/dev/null; then
pass "grok binary invocable via full path"
else
skip "grok --version" "binary exists but --version returned non-zero"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fail this check when an installed Grok executable cannot report its version. Provisioning already checks test -x, but that does not establish that the loader/runtime works. Skipping a failed --version allows an executable but unusable installation to pass this phase. The earlier missing-binary skip can remain for deliberately supported older images; this branch should distinguish an installed binary that fails to run.

Comment thread scripts/guest/grok.sh
if curl -fsSL -o "$INSTALLER" https://x.ai/cli/install.sh 2>/tmp/grok-curl-err; then
break
fi
CURL_EXIT=$?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$? here is the status of the completed if, not the failed curl. An if with a false condition and no else returns zero, so these retry and final-error messages always report curl exit code 0. Please capture the failure status inside an else branch. An isolated Bash reproduction with a child exiting 22 returned 0 after the if, versus 22 inside else.

@hbrodin

hbrodin commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Thanks for contributing Grok Build support! The main changes I'd like to see are preserving guest-created config files and Grok plugin activation across restarts, restoring Lima's configuration on every resize failure path, failing the version check when an installed Grok binary cannot run, and reporting curl's actual failure status. I've left the details inline.

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.

3 participants