mirror of
https://github.com/wirenboard/agent-vm.git
synced 2026-07-10 00:14:30 +00:00
post-review cleanup: drop broken observability, tighten chrome MCP, finish placeholder rename
Folds in fourteen findings from the code review of the previous two
commits. Most are 'the commit advertised something it didn't actually
deliver' — straightforward to make honest.
**Observability commit was a no-op — revert it.**
- `main.rs` filter went back to the simple `warn` default. The previous
`microsandbox_network::proxy=info,microsandbox_network::tls::proxy=info`
targeted modules that (a) only emit `tracing::debug!` (so info
filtered them out) and (b) run in the `msb sandbox` subprocess whose
own tracing init reads no env at all. Net behaviour: zero proxy
logs surfaced; the doc-comment claim was wishful thinking.
- `run.rs` post-mortem hint trimmed to one accurate line. Dropped the
pointers to `msb.stderr.log` and `msb-exit.log` (vendor `spawn.rs`
inherits stderr instead of teeing; `handle.rs::wait` writes no exit
log). What remains: a single `(see <runtime.log> for the in-VM tail)`
appended via `with_context`. Avoids the duplicated error output the
old code produced — anyhow's main termination prints the chain once
rather than the previous "eprintln before return then re-print on
exit" combo.
- `runtime_log_path()` replaces `msb_sandbox_log_dir`; comment no
longer claims to "mirror" upstream, and the final fallback matches
`microsandbox_utils::resolve_home()` (`./.microsandbox`) rather than
the previous incorrect `/var/lib/microsandbox`. Hint now also fires
in the non-TTY branch's `with_context` and `anyhow::bail!` paths,
not just TTY — CI users are the ones most likely to need the
pointer.
**Chrome MCP cleanups.**
- `certutil` trust string `-t TC` → `-t C,,`. The previous form set
the SSL column to `TC` (T = trusted issuer of *client* certs, C =
trusted issuer of server certs). We only want server-cert trust;
the T silently broadened scope. `C,,` is the textbook idiom.
- Wrapper's `--preserve-env` allow-list expanded to forward the env
vars chrome-devtools-mcp itself documents
(`CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS`, `CI`, `DEBUG`), agentd-
set proxy config (`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` plus
lowercase variants), and basic locale (`TZ`, `LANG`, `LC_ALL`,
`TMPDIR`). Without these, user MCP-config `env:` blocks and
documented opt-outs were silently stripped by sudo's `env_reset`.
- Wrapper now `cd /home/chrome` before exec'ing. Inherited cwd was
`/workspace` (the project bind-mount), which the chrome user (UID
9999) cannot write — any MCP tool emitting a relative path
(`./trace.json`, `./screenshot.png`, `--logFile=./debug.log`)
EACCES'd silently. `/home/chrome` is chrome-owned and writable.
- Pre-warm `chrome-devtools-mcp@latest` into chrome's npm cache at
image build time (`/home/chrome/.npm/_npx/...`). Previously every
first MCP call paid a multi-second `npx -y` fetch and broke
entirely when registry.npmjs.org was unreachable. The image gains
~21 MiB; cold launches drop the fetch.
- Bash prelude's certutil block is now gated on `AGENT_VM_NO_CHROME_MCP`
not being set. Previously the env-var opt-out only suppressed the
MCP entry in `claude.json`; the sudo→chrome→certutil round-trip
ran on every launch regardless, exposing the opt-out user to the
exact sudoers/NSS failure modes they were trying to avoid.
**Sticky opt-out fixed.** `write_default_claude_root_state` now
always owns the `chrome-devtools` key: `AGENT_VM_NO_CHROME_MCP=1`
explicitly `mcp.remove("chrome-devtools")`s a previously-written
entry instead of leaving it sticky in the on-disk merged JSON.
**OPENCODE placeholder rename completed.** `OPENCODE_OPENAI_ACCESS_PLACEHOLDER`
JWT signature is now `msb-opencode-placeholder-a-v2` (was
`MSB_OPENCODE_v1`), matching the rename rationale spelled out on
`OPENAI_ID_PLACEHOLDER`: non-token-shaped sentinels so Anthropic
doesn't flag transcripts that contain them.
**Stale literals swept.** `ARCHITECTURE.md` reads the new names at
all five reference sites. Six tests in `intercept_hook.rs` that
hardcoded `MSB_PLACEHOLDER_GH_TOKEN_v1[_xxxxxxxx]` as a standalone
fixture now use `secrets::GH_TOKEN_PLACEHOLDER` directly — so a
future placeholder shape change is exercised by the existing
regression assertions instead of slipping past them.
End-to-end verified inside a fresh sandbox:
- `claude mcp list` → ✓ Connected
- `navigate_page` https://example.com + `take_snapshot` returns the
real "Example Domain" content
- NSS DB lists only the microsandbox CA with trust `C,,` (not `CT,,`)
- `chrome-devtools-mcp` package present in /home/chrome/.npm/_npx/
out of the box (no per-launch fetch)
- `AGENT_VM_NO_CHROME_MCP=1`: claude.json mcpServers is `{}` (entry
removed, not just absent on new files); NSS DB is empty (certutil
prelude skipped)
- Wrapper preserves CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS, CI,
DEBUG, HTTP_PROXY, TZ, NODE_EXTRA_CA_CERTS across the sudo hop
- 73 cargo unit tests pass (incl. the intercept_hook tests now using
the constant)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f40e6df2bc
commit
ce745ece6d
6 changed files with 134 additions and 93 deletions
|
|
@ -310,8 +310,10 @@ Real tokens never enter the VM. The dance per provider:
|
|||
`~/.codex/auth.json` for Codex) at every launch. It extracts the
|
||||
access token, keeps it in a short-lived Rust `String`, and registers
|
||||
it as a microsandbox secret with a stable placeholder string
|
||||
(`MSB_PLACEHOLDER_ANTHROPIC_ACCESS_TOKEN_v1` and
|
||||
`MSB_PLACEHOLDER_OPENAI_ACCESS_TOKEN_v1`).
|
||||
(`msb-anthropic-placeholder-a-v2` and
|
||||
`msb-openai-placeholder-a-v2`). The placeholder *constants* live in
|
||||
`crates/agent-vm/src/secrets.rs` (`ANTHROPIC_ACCESS_PLACEHOLDER` etc.) —
|
||||
prefer the constant over the literal so a future rename doesn't drift.
|
||||
2. **Guest side.** agent-vm writes a "placeholder credentials" JSON
|
||||
into the per-project state dir
|
||||
(`<state>/claude/.credentials.json`,
|
||||
|
|
@ -324,7 +326,7 @@ Real tokens never enter the VM. The dance per provider:
|
|||
sandbox's HTTPS traffic. When the agent makes a request to any
|
||||
allowed host (`api.anthropic.com`, `platform.claude.com`,
|
||||
`api.openai.com`, `chatgpt.com`, `auth.openai.com`), the proxy
|
||||
sees `Authorization: Bearer MSB_PLACEHOLDER_...` in the outgoing
|
||||
sees `Authorization: Bearer msb-…-placeholder-…` in the outgoing
|
||||
request, splices in the real token from the secret config, then
|
||||
forwards.
|
||||
|
||||
|
|
@ -402,7 +404,7 @@ End-to-end verified on a nested-VM test host (cwd
|
|||
- `cat /root/.claude/.credentials.json` inside the guest shows the
|
||||
placeholder, not the real token. ✓
|
||||
- `cat /proc/1/environ | tr '\0' '\n' | grep -i token` finds only
|
||||
`MSB_AGENT_VM_ANTHROPIC_UNUSED=MSB_PLACEHOLDER_…`. ✓
|
||||
`MSB_AGENT_VM_ANTHROPIC_UNUSED=msb-…-placeholder-…`. ✓
|
||||
- TLS-intercepted curl to `https://api.anthropic.com` sees the
|
||||
microsandbox CA on the server cert (`CN=microsandbox CA`),
|
||||
confirming requests go through the substitution proxy. ✓
|
||||
|
|
@ -596,8 +598,8 @@ Inside the guest:
|
|||
|
||||
Response:
|
||||
HTTP 200 application/json
|
||||
{"access_token":"MSB_PLACEHOLDER_ANTHROPIC_ACCESS_TOKEN_v1",
|
||||
"refresh_token":"MSB_PLACEHOLDER_ANTHROPIC_REFRESH_TOKEN_v1",
|
||||
{"access_token":"msb-anthropic-placeholder-a-v2",
|
||||
"refresh_token":"msb-anthropic-placeholder-r-v2",
|
||||
"expires_in":3499, "token_type":"Bearer",
|
||||
"scope":["user:file_upload","user:inference",…]}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1199,14 +1199,17 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn strip_auth_removes_the_header_keeps_body() {
|
||||
let r = b"POST /repos/x/y/issues HTTP/1.1\r\n\
|
||||
Host: api.github.com\r\n\
|
||||
Authorization: token MSB_PLACEHOLDER_GH_TOKEN_v1\r\n\
|
||||
Content-Type: application/json\r\n\
|
||||
Content-Length: 11\r\n\
|
||||
\r\n\
|
||||
{\"title\":1}";
|
||||
let out = strip_authorization_from_request(r);
|
||||
let r = format!(
|
||||
"POST /repos/x/y/issues HTTP/1.1\r\n\
|
||||
Host: api.github.com\r\n\
|
||||
Authorization: token {placeholder}\r\n\
|
||||
Content-Type: application/json\r\n\
|
||||
Content-Length: 11\r\n\
|
||||
\r\n\
|
||||
{{\"title\":1}}",
|
||||
placeholder = secrets::GH_TOKEN_PLACEHOLDER,
|
||||
);
|
||||
let out = strip_authorization_from_request(r.as_bytes());
|
||||
let s = std::str::from_utf8(&out).unwrap();
|
||||
// Authorization line gone.
|
||||
assert!(!s.to_ascii_lowercase().contains("authorization:"));
|
||||
|
|
@ -1217,7 +1220,7 @@ mod tests {
|
|||
// Body preserved verbatim.
|
||||
assert!(s.ends_with("\r\n\r\n{\"title\":1}"));
|
||||
// Placeholder absent at any layer.
|
||||
assert!(!s.contains("MSB_PLACEHOLDER_GH_TOKEN_v1"));
|
||||
assert!(!s.contains(secrets::GH_TOKEN_PLACEHOLDER));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1470,7 +1473,7 @@ mod tests {
|
|||
use base64::Engine as _;
|
||||
use microsandbox_network::secrets::handler::SecretsHandler;
|
||||
|
||||
let placeholder = "MSB_PLACEHOLDER_GH_TOKEN_v1_xxxxxxxx";
|
||||
let placeholder = secrets::GH_TOKEN_PLACEHOLDER;
|
||||
let real_token = "REAL_TOKEN_KEEPALIVE_DEFENSE_CANARY";
|
||||
|
||||
let config = build_github_secrets_config(placeholder, real_token);
|
||||
|
|
@ -1523,7 +1526,7 @@ mod tests {
|
|||
use base64::Engine as _;
|
||||
use microsandbox_network::secrets::handler::SecretsHandler;
|
||||
|
||||
let placeholder = "MSB_PLACEHOLDER_GH_TOKEN_v1_xxxxxxxx";
|
||||
let placeholder = secrets::GH_TOKEN_PLACEHOLDER;
|
||||
let real_token = "REAL_TOKEN_ALLOWED_REPO";
|
||||
|
||||
let config = build_github_secrets_config(placeholder, real_token);
|
||||
|
|
@ -1627,7 +1630,7 @@ mod tests {
|
|||
// Sentinel values: if either string shows up in the final
|
||||
// upstream bytes, the test fails — we'd be leaking a real
|
||||
// token to the network.
|
||||
let placeholder = "MSB_PLACEHOLDER_GH_TOKEN_v1_xxxxxxxx";
|
||||
let placeholder = secrets::GH_TOKEN_PLACEHOLDER;
|
||||
let real_token = "REAL_TOKEN_MUST_NEVER_REACH_UPSTREAM_42";
|
||||
|
||||
let config = build_github_secrets_config(placeholder, real_token);
|
||||
|
|
@ -1718,7 +1721,7 @@ mod tests {
|
|||
use base64::Engine as _;
|
||||
use microsandbox_network::secrets::handler::SecretsHandler;
|
||||
|
||||
let placeholder = "MSB_PLACEHOLDER_GH_TOKEN_v1_xxxxxxxx";
|
||||
let placeholder = secrets::GH_TOKEN_PLACEHOLDER;
|
||||
let real_token = "REAL_TOKEN_KEEPALIVE_LEAK_CANARY";
|
||||
|
||||
let config = build_github_secrets_config(placeholder, real_token);
|
||||
|
|
@ -1795,7 +1798,7 @@ mod tests {
|
|||
use base64::Engine as _;
|
||||
use microsandbox_network::secrets::handler::SecretsHandler;
|
||||
|
||||
let placeholder = "MSB_PLACEHOLDER_GH_TOKEN_v1_xxxxxxxx";
|
||||
let placeholder = secrets::GH_TOKEN_PLACEHOLDER;
|
||||
let real_token = "REAL_TOKEN_FOR_ALLOWED_REPO";
|
||||
|
||||
let config = build_github_secrets_config(placeholder, real_token);
|
||||
|
|
|
|||
|
|
@ -84,20 +84,11 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
|
||||
/// Wire `tracing` so `RUST_LOG=agent_vm=debug,microsandbox=info` works.
|
||||
/// Default level is `warn` for most crates, but the network proxy
|
||||
/// modules default to `info` so per-connection lifecycle records
|
||||
/// (duration, bytes, close reason) show up out of the box — these
|
||||
/// are the breadcrumbs that distinguish "Claude API hung" from
|
||||
/// "Claude's pooled HTTP/2 conn died after hours of idle". They land
|
||||
/// in `~/.microsandbox/sandboxes/<name>/logs/msb.stderr.log` thanks
|
||||
/// to the spawn-side tee.
|
||||
/// Default level is `warn` — keeps normal output clean, but anything from
|
||||
/// the microsandbox stack surfaces when you ask for it.
|
||||
fn init_tracing() {
|
||||
use tracing_subscriber::{EnvFilter, fmt};
|
||||
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
EnvFilter::new(
|
||||
"warn,microsandbox_network::proxy=info,microsandbox_network::tls::proxy=info",
|
||||
)
|
||||
});
|
||||
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn"));
|
||||
fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_writer(std::io::stderr)
|
||||
|
|
|
|||
|
|
@ -605,23 +605,37 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
|
|||
// `/usr/local/share/ca-certificates/microsandbox-ca.crt` is
|
||||
// written into the guest by agentd at boot, so this can't be
|
||||
// baked into the image — the per-boot CA is what we have to
|
||||
// inject. `certutil -A` of an already-present nickname is a
|
||||
// no-op (we re-run on every launch but the cost is one fork +
|
||||
// one sqlite open), so we don't bother gating on "did we
|
||||
// already do this?". `|| true` keeps a missing chrome user
|
||||
// (AGENT_VM_NO_CHROME_MCP build, or a future image variant
|
||||
// without the MCP) from breaking the prelude.
|
||||
// inject. Trust string is `-t C,,` (trusted issuer of *server*
|
||||
// certs only; the leading `T` would also mark it as a trusted
|
||||
// issuer of client certs, which we don't want). Only injected
|
||||
// when the chrome MCP is enabled: gating both here and in
|
||||
// `secrets::write_default_claude_root_state` on the same env
|
||||
// var keeps the opt-out actually opt-out — no sudo, no
|
||||
// certutil fork.
|
||||
let project_guest_path_escaped = shell_escape(&project_guest_path);
|
||||
let chrome_mcp_prelude = if std::env::var_os("AGENT_VM_NO_CHROME_MCP").is_some() {
|
||||
String::new()
|
||||
} else {
|
||||
// Image-fresh NSS DB starts empty on every launch (the rootfs
|
||||
// upper layer is regenerated by `Sandbox::builder.replace()`),
|
||||
// so `certutil -A` always runs against the same baseline; no
|
||||
// chance of accumulating duplicate trust entries across boots.
|
||||
// `2>/dev/null || true` keeps a transient missing chrome user
|
||||
// or unreadable CA file from breaking the prelude.
|
||||
String::from(
|
||||
"if [ -f /usr/local/share/ca-certificates/microsandbox-ca.crt ] \\\n\
|
||||
&& [ -d /home/chrome/.pki/nssdb ]; then\n\
|
||||
\tsudo -u chrome -n -- certutil -d sql:/home/chrome/.pki/nssdb -A \\\n\
|
||||
\t\t-t C,, -n microsandbox \\\n\
|
||||
\t\t-i /usr/local/share/ca-certificates/microsandbox-ca.crt \\\n\
|
||||
\t\t2>/dev/null || true\n\
|
||||
fi\n",
|
||||
)
|
||||
};
|
||||
let prelude = format!(
|
||||
"sed -i '/^nameserver .*:/d' /etc/resolv.conf 2>/dev/null || true\n\
|
||||
[ -t 0 ] || exec < /dev/null\n\
|
||||
if [ -f /usr/local/share/ca-certificates/microsandbox-ca.crt ] \\\n\
|
||||
&& [ -d /home/chrome/.pki/nssdb ]; then\n\
|
||||
\tsudo -u chrome -n -- certutil -d sql:/home/chrome/.pki/nssdb -A \\\n\
|
||||
\t\t-t TC -n microsandbox \\\n\
|
||||
\t\t-i /usr/local/share/ca-certificates/microsandbox-ca.crt \\\n\
|
||||
\t\t2>/dev/null || true\n\
|
||||
fi\n\
|
||||
{chrome_mcp_prelude}\
|
||||
_hook={path}/.agent-vm.runtime.sh\n\
|
||||
if [ -f \"$_hook\" ]; then\n\
|
||||
\techo \"==> sourcing $_hook\" >&2\n\
|
||||
|
|
@ -643,26 +657,10 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
|
|||
let t_run = Instant::now();
|
||||
let exit = if std::io::stdin().is_terminal() {
|
||||
eprintln!("==> Attaching to {inner_cmd}");
|
||||
match sandbox.attach(cmd, agent_args).await {
|
||||
Ok(code) => code,
|
||||
Err(err) => {
|
||||
// Print a focused pointer to the post-mortem logs the
|
||||
// runtime now writes (spawn.rs tees msb's stderr;
|
||||
// handle.rs appends to msb-exit.log on reap). The
|
||||
// raw-mode scopeguard inside attach_inner has already
|
||||
// restored the terminal by the time we see this Err.
|
||||
eprintln!("==> attach failed: {err:#}");
|
||||
eprintln!(
|
||||
"==> VM post-mortem (if a death happened, the cause is usually in one of these):"
|
||||
);
|
||||
let log_dir = msb_sandbox_log_dir(&session.sandbox_name);
|
||||
eprintln!(" {}/runtime.log (msb tracing / panic)", log_dir.display());
|
||||
eprintln!(" {}/msb.stderr.log (libkrun / kernel printk / OOM)", log_dir.display());
|
||||
eprintln!(" {}/msb-exit.log (exit status of each boot)", log_dir.display());
|
||||
eprintln!(" dmesg -T | tail (host-side OOM-kill of the msb process)");
|
||||
return Err(err).with_context(|| format!("attaching to {inner_cmd}"));
|
||||
}
|
||||
}
|
||||
sandbox
|
||||
.attach(cmd, agent_args)
|
||||
.await
|
||||
.with_context(|| format!("attaching to {inner_cmd} (see {} for the in-VM tail)", runtime_log_path(&session.sandbox_name).display()))?
|
||||
} else {
|
||||
// No host TTY (piped, redirected, smoke-tested under `sg`/`sudo` etc.).
|
||||
// attach() needs a real /dev/tty for raw-mode stdin, so use the
|
||||
|
|
@ -679,7 +677,12 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
|
|||
e.args(agent_args).cwd(project_guest_path.clone())
|
||||
})
|
||||
.await
|
||||
.with_context(|| format!("running {inner_cmd} in sandbox"))?;
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"running {inner_cmd} in sandbox (see {} for the in-VM tail)",
|
||||
runtime_log_path(&session.sandbox_name).display()
|
||||
)
|
||||
})?;
|
||||
let mut stdout = tokio::io::stdout();
|
||||
let mut stderr = tokio::io::stderr();
|
||||
// Track whether we actually saw an Exited event. The previous
|
||||
|
|
@ -704,7 +707,10 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
|
|||
break;
|
||||
}
|
||||
ExecEvent::Failed(payload) => {
|
||||
anyhow::bail!("exec session failed: {payload:?}");
|
||||
anyhow::bail!(
|
||||
"exec session failed: {payload:?} (see {} for the in-VM tail)",
|
||||
runtime_log_path(&session.sandbox_name).display()
|
||||
);
|
||||
}
|
||||
ExecEvent::Started { .. } | ExecEvent::StdinError(_) => {}
|
||||
}
|
||||
|
|
@ -712,7 +718,8 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
|
|||
match exit_code {
|
||||
Some(c) => c,
|
||||
None => anyhow::bail!(
|
||||
"exec session event stream ended without Exited (agentd disconnect or microsandbox bug; partial output above)"
|
||||
"exec session event stream ended without Exited (agentd disconnect or microsandbox bug; partial output above; see {} for the in-VM tail)",
|
||||
runtime_log_path(&session.sandbox_name).display()
|
||||
),
|
||||
}
|
||||
};
|
||||
|
|
@ -740,18 +747,22 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
|
|||
Ok(exit)
|
||||
}
|
||||
|
||||
/// Best-effort guess at where msb writes per-sandbox runtime logs.
|
||||
/// Mirrors `microsandbox_utils::resolve_home()` / `sandboxes_dir()`:
|
||||
/// `$MSB_HOME` or `~/.microsandbox`, then `sandboxes/<name>/logs`.
|
||||
/// Used only for human-readable post-mortem hints, so a stale guess
|
||||
/// (e.g. caller overrode the home via config) just makes the hint
|
||||
/// slightly off rather than breaking anything.
|
||||
fn msb_sandbox_log_dir(sandbox_name: &str) -> PathBuf {
|
||||
/// Best-effort hint at where msb writes a sandbox's `runtime.log` (the
|
||||
/// only post-mortem file vendor actually creates today — `vm.rs`'s
|
||||
/// `setup_log_capture` redirects the subprocess stderr there). Used
|
||||
/// solely in the `with_context` of attach errors, so a wrong guess just
|
||||
/// dilutes the hint message rather than breaking anything. Matches
|
||||
/// upstream's `microsandbox_utils::resolve_home()` fallback order
|
||||
/// (`$MSB_HOME` → `$HOME/.microsandbox` → `./.microsandbox`).
|
||||
fn runtime_log_path(sandbox_name: &str) -> PathBuf {
|
||||
let home = env::var_os("MSB_HOME")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| env::var_os("HOME").map(|h| PathBuf::from(h).join(".microsandbox")))
|
||||
.unwrap_or_else(|| PathBuf::from("/var/lib/microsandbox"));
|
||||
home.join("sandboxes").join(sandbox_name).join("logs")
|
||||
.unwrap_or_else(|| PathBuf::from("./.microsandbox"));
|
||||
home.join("sandboxes")
|
||||
.join(sandbox_name)
|
||||
.join("logs")
|
||||
.join("runtime.log")
|
||||
}
|
||||
|
||||
/// One `--mount HOST[:GUEST]` argument resolved into separate paths.
|
||||
|
|
|
|||
|
|
@ -61,14 +61,16 @@ pub const OPENAI_ID_PLACEHOLDER: &str = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ
|
|||
/// header = base64url('{"alg":"none","typ":"JWT"}')
|
||||
/// payload = base64url('{"exp":9999999999,
|
||||
/// "chatgpt_account_id":"00000000-0000-0000-0000-000000000000"}')
|
||||
/// sig = "MSB_OPENCODE_v1"
|
||||
/// sig = "msb-opencode-placeholder-a-v2" (non-token-shaped to
|
||||
/// match the rest of the placeholder family; see the warning
|
||||
/// on `OPENAI_ID_PLACEHOLDER`)
|
||||
///
|
||||
/// **Kept short on purpose:** an earlier ~480-char payload (with
|
||||
/// iss/aud/scp/email/sub claims) triggered upstream issue #8 — long
|
||||
/// placeholders fail sandbox boot with `handshake read id_offset:
|
||||
/// timed out before relay sent bytes`. Add fields here only if
|
||||
/// OpenCode actually parses them and chokes on absence.
|
||||
pub const OPENCODE_OPENAI_ACCESS_PLACEHOLDER: &str = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOjk5OTk5OTk5OTksImNoYXRncHRfYWNjb3VudF9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDAwMCJ9.MSB_OPENCODE_v1";
|
||||
pub const OPENCODE_OPENAI_ACCESS_PLACEHOLDER: &str = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOjk5OTk5OTk5OTksImNoYXRncHRfYWNjb3VudF9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDAwMCJ9.msb-opencode-placeholder-a-v2";
|
||||
pub const OPENCODE_OPENAI_REFRESH_PLACEHOLDER: &str = "msb-opencode-placeholder-r-v2";
|
||||
/// Placeholder for the host's `gh auth token`. The in-guest `gh` /
|
||||
/// git credential helper sees this string; the proxy substitutes the
|
||||
|
|
@ -673,12 +675,19 @@ fn write_default_claude_root_state(path: &Path, project_guest_path: &str) -> Res
|
|||
// (`/home/chrome/.pki/nssdb/`, populated by the launcher's bash
|
||||
// prelude at boot). With that, no `--acceptInsecureCerts` (which
|
||||
// would accept *any* untrusted cert) — only our CA is trusted.
|
||||
if std::env::var("AGENT_VM_NO_CHROME_MCP").is_err() {
|
||||
let mcp = obj
|
||||
.entry("mcpServers".to_string())
|
||||
.or_insert_with(|| serde_json::json!({}))
|
||||
.as_object_mut()
|
||||
.context("~/.claude.json mcpServers is not an object")?;
|
||||
// The map is created (or reused) regardless so the opt-out can
|
||||
// also REMOVE a previously-written entry — without this, setting
|
||||
// AGENT_VM_NO_CHROME_MCP after a launch without it would leave
|
||||
// the stale entry in the on-disk claude.json and the MCP would
|
||||
// keep spawning. We always own this key.
|
||||
let mcp = obj
|
||||
.entry("mcpServers".to_string())
|
||||
.or_insert_with(|| serde_json::json!({}))
|
||||
.as_object_mut()
|
||||
.context("~/.claude.json mcpServers is not an object")?;
|
||||
if std::env::var_os("AGENT_VM_NO_CHROME_MCP").is_some() {
|
||||
mcp.remove("chrome-devtools");
|
||||
} else {
|
||||
mcp.insert(
|
||||
"chrome-devtools".into(),
|
||||
serde_json::json!({
|
||||
|
|
|
|||
|
|
@ -82,16 +82,32 @@ RUN echo 'chrome:x:9999:9999:Chrome MCP:/home/chrome:/bin/bash' >> /etc/passwd \
|
|||
'# Args (npx + chrome-devtools-mcp + flags) come from the MCP' \
|
||||
'# config in ~/.claude.json; we just switch UID.' \
|
||||
'#' \
|
||||
'# We preserve a small allow-list of env vars across the sudo' \
|
||||
'# call: agentd boot-sets NODE_EXTRA_CA_CERTS / SSL_CERT_FILE /' \
|
||||
'# CURL_CA_BUNDLE / REQUESTS_CA_BUNDLE so any HTTPS client in' \
|
||||
'# the guest trusts the microsandbox MITM CA. Without these' \
|
||||
'# preserved, node fails `npx -y` with SELF_SIGNED_CERT_IN_CHAIN' \
|
||||
'# fetching from registry.npmjs.org and the MCP exits before' \
|
||||
'# the JSON-RPC handshake. PATH is needed so npx is findable.' \
|
||||
'# Preserve list (passed through sudo'\''s env_reset):' \
|
||||
'# - NODE_EXTRA_CA_CERTS / SSL_CERT_FILE / CURL_CA_BUNDLE /' \
|
||||
'# REQUESTS_CA_BUNDLE: agentd boot-sets these so any HTTPS' \
|
||||
'# client trusts the microsandbox MITM CA. Without them' \
|
||||
'# node fails `npx -y` with SELF_SIGNED_CERT_IN_CHAIN.' \
|
||||
'# - PATH: needed so npx is findable (sudo'\''s secure_path' \
|
||||
'# would otherwise replace it).' \
|
||||
'# - HTTP_PROXY / HTTPS_PROXY / NO_PROXY (both cases):' \
|
||||
'# forward agentd-set proxy config so chrome routes traffic' \
|
||||
'# the same way as the rest of the guest.' \
|
||||
'# - CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS / CI / DEBUG:' \
|
||||
'# user/MCP-config opt-outs and debugging knobs that' \
|
||||
'# chrome-devtools-mcp documents.' \
|
||||
'# - TZ / LANG / LC_ALL / TMPDIR: locale + sane temp paths.' \
|
||||
'#' \
|
||||
'# `cd /home/chrome` keeps chromium'\''s default CWD writable' \
|
||||
'# (the agent runs as root in /workspace, where chrome (UID' \
|
||||
'# 9999) cannot write; tools that emit relative paths' \
|
||||
'# (./trace.json, ./screenshot.png) would otherwise EACCES).' \
|
||||
'set -e' \
|
||||
'cd /home/chrome' \
|
||||
'exec sudo -u chrome -H -n \' \
|
||||
' --preserve-env=NODE_EXTRA_CA_CERTS,SSL_CERT_FILE,CURL_CA_BUNDLE,REQUESTS_CA_BUNDLE,PATH \' \
|
||||
' --preserve-env=NODE_EXTRA_CA_CERTS,SSL_CERT_FILE,CURL_CA_BUNDLE,REQUESTS_CA_BUNDLE \' \
|
||||
' --preserve-env=PATH,HTTP_PROXY,HTTPS_PROXY,NO_PROXY,http_proxy,https_proxy,no_proxy \' \
|
||||
' --preserve-env=CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS,CI,DEBUG \' \
|
||||
' --preserve-env=TZ,LANG,LC_ALL,TMPDIR \' \
|
||||
' -- "$@"' \
|
||||
> /usr/local/bin/agent-vm-chrome-mcp \
|
||||
&& chmod 0755 /usr/local/bin/agent-vm-chrome-mcp
|
||||
|
|
@ -112,6 +128,15 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
|||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Pre-warm chrome-devtools-mcp's npm cache as the chrome user so the
|
||||
# first browser tool call in any sandbox doesn't pay the multi-second
|
||||
# `npx -y` fetch cost (and so cold launches don't fail entirely when
|
||||
# registry.npmjs.org is briefly unreachable). `--help` exits cleanly
|
||||
# after install/version-check; it's the cheapest exercise of the full
|
||||
# install path. Must come AFTER the nodejs RUN above — npx didn't
|
||||
# exist before it.
|
||||
RUN sudo -u chrome -H npx -y chrome-devtools-mcp@latest --help >/dev/null
|
||||
|
||||
# Claude Code official installer.
|
||||
RUN curl -fsSL https://claude.ai/install.sh | bash
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue