mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(cli): detect Gemini managed-agent sandbox in detectAgentRuntime (#1294)
* feat(cli): detect Gemini managed-agent sandbox in detectAgentRuntime
Add `gemini_managed_agent` to the AgentRuntime union and a dedicated
isGeminiManagedAgent() detector. Empirical signal pair (from live-sandbox
introspection by gemini-agent, env_id b9db4e56, 2026-06-09):
existsSync('/.agents/AGENTS.md') AND isGVisor()
The conjunction is what makes the rule safe:
- `/.agents/AGENTS.md` excludes generic gVisor surfaces (GKE Sandbox,
Cloud Run gen2) that don't mount the managed-agent layout.
- The gVisor kernel check excludes a dev box that happens to have a
stray `/.agents/` directory.
Implementation notes:
- Filesystem-based check runs ahead of the env-var-only VENDOR_RULES
loop. VENDOR_RULES is documented as "Only checks for the EXISTENCE
of well-known env vars — never reads their values"; the Gemini
signal is filesystem + kernel, not env, so it gets a dedicated
branch rather than shoehorning into the rule list.
- GEMINI_API_KEY is deliberately NOT keyed on — it's user-settable on
any host. The filesystem + kernel pair is the actually-distinctive
signal.
- Reuses the existing isGVisor() helper for the kernel half of the
conjunction; no duplication.
Tests (4 new, vitest):
- Positive: /.agents/AGENTS.md + 4.19.0-gvisor → gemini_managed_agent
- Negative: gVisor alone (no /.agents/) → null (generic gVisor surface)
- Negative: /.agents/AGENTS.md alone (no gVisor) → null (dev box false-positive guard)
- Precedence: Gemini signal wins over a coincident CLAUDECODE env var
Empirical caveat: signal was gathered from a single sandbox. Re-confirming
across additional sandbox spins is a follow-up; the rule is conservative
enough (conjunction of two independent signals) that a single-spin
false-positive is unlikely, but a single-spin variance bug (e.g. some
sandbox flavors omitting one of the two markers) would surface as
under-detection rather than over-detection.
Source for signals: introspection write-up at
/tmp/gemini-sandbox-detection-signals.md (gemini-agent, 2026-06-09).
* docs(cli): reframe Gemini-managed-agent detection rationale (load-bearing vs guard)
gemini-agent's uniqueness analysis (FS-root + cgroup + netns + DMI + PID-1
introspection of env d59d6361, 2026-06-09) revealed the two signals are
NOT co-equal:
- /.agents/AGENTS.md is the uniqueness anchor — definitionally a
managed-agent artifact, injected per-run by the platform, mtime
tracks the interaction. Nothing in the generic Google-Cloud-on-gVisor
universe (Cloud Run gen2, GKE Sandbox, Fly.io) mounts /.agents/.
- isGVisor() is a guard, not a second uniqueness signal. gVisor itself
is shared with GKE Sandbox + Cloud Run gen2 — its real job here is
ruling out a stray user-created /.agents/AGENTS.md on a non-sandbox
host.
The original 3-spin work proved *stability* (signals consistent across
sandbox spins). This pass adds *uniqueness* — confirming the signals
discriminate Antigravity from the broader gVisor universe, not just
that they're reliably present. Stability ≠ uniqueness; both are
required for a correct detection rule.
Code unchanged (the AND-gate is sound). Docstring reframed so a future
reader doesn't mistake the conjunction for two independent uniqueness
signals. Also enumerated the markers NOT keyed on (with reasons), so
future contributors don't reach for them by naming inference.
Source: gemini-agent uniqueness analysis write-up.
* fix(cli): key Gemini managed-agent detection on /.agents/ mount, not optional AGENTS.md
The detector keyed on existsSync('/.agents/AGENTS.md'), but Google's Managed
Agents docs are explicit that AGENTS.md is OPTIONAL: an agent may declare its
instructions inline via system_instruction in agent.yaml and ship no AGENTS.md
file ("system_instruction and AGENTS.md are additive; both apply when present").
The platform auto-discovers the agent under the /.agents/ directory; skills
mount at /.agents/skills/ and AGENTS.md at /.agents/AGENTS.md only when shipped.
Keying on the file generalized only to templates that happen to bundle an
AGENTS.md (like HeyGen's own gemini-agent and Thor's reference). A managed agent
defined with inline instructions or a skills-only definition was a silent
false-negative. All three prior verification spins used our own AGENTS.md-bearing
template, so the gap was never exercised.
Broaden to the /.agents/ directory mount (still gVisor-guarded — false-positive
surface is unchanged) so skills-only and inline-instruction agents are detected.
Adds a regression test for the skills-but-no-AGENTS.md case. Documents the one
residual gap (pure inline-only, no skills/no AGENTS.md) that needs an empirical
spin to confirm.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(cli): tighten /.agents/ to a directory check + sync agent_runtime docs
Self-review follow-ups (no behavior change for real managed agents):
- isGeminiManagedAgent now requires statSync("/.agents").isDirectory() rather
than existsSync("/.agents"), matching the documented "directory mount"
contract. existsSync matched any entry (a stray file/symlink named /.agents),
widening the gVisor-gated false-positive surface beyond what the comment
claimed. Tests now mock statSync accordingly (and drop a dead /.agents/skills
mock clause the code never read).
- system.ts: the agent_runtime doc comment hard-coded the vendor list and said
"detected by env-var existence only" — both stale once a filesystem/kernel
detector (gemini_managed_agent) exists. Point at the AgentRuntime union and
note the filesystem-marker case instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
868c56fdbb
commit
0766eb8144
@@ -163,6 +163,121 @@ describe("detectAgentRuntime — Replit / Hermes / openclaw / Pi", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("detectAgentRuntime — Gemini managed agent", () => {
|
||||
// Gemini managed agent is detected via the `/.agents/` platform mount (a
|
||||
// DIRECTORY) and the gVisor kernel string, NOT env vars — so these tests
|
||||
// mock node:fs statSync and node:os rather than mutating process.env. We key
|
||||
// on the `/.agents/` directory (not the optional AGENTS.md file) so
|
||||
// skills-only and inline-instruction agents are still detected.
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
stripVendorEnv();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetModules();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
// Mock node:fs so statSync("/.agents") reports a directory; everything else
|
||||
// delegates to the real fs.
|
||||
const mockAgentsDir = () =>
|
||||
vi.doMock("node:fs", async () => {
|
||||
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
|
||||
return {
|
||||
...actual,
|
||||
statSync: (path: string) =>
|
||||
path === "/.agents"
|
||||
? ({ isDirectory: () => true } as unknown as import("node:fs").Stats)
|
||||
: actual.statSync(path),
|
||||
};
|
||||
});
|
||||
|
||||
it("reports gemini_managed_agent when /.agents/ is a directory AND the kernel is gVisor", async () => {
|
||||
vi.doMock("node:os", async () => {
|
||||
const actual = await vi.importActual<typeof import("node:os")>("node:os");
|
||||
return { ...actual, release: () => "4.19.0-gvisor", platform: () => "linux" };
|
||||
});
|
||||
mockAgentsDir();
|
||||
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
||||
expect(detectAgentRuntime()).toBe("gemini_managed_agent");
|
||||
});
|
||||
|
||||
it("detects a skills-only managed agent (no AGENTS.md) — the generalizability case", async () => {
|
||||
// AGENTS.md is OPTIONAL: an agent may use inline `system_instruction` or a
|
||||
// skills-only definition and ship no AGENTS.md. Keying on the `/.agents/`
|
||||
// directory mount (not the file) must still detect it — the mock makes
|
||||
// `/.agents` a directory with no AGENTS.md present.
|
||||
vi.doMock("node:os", async () => {
|
||||
const actual = await vi.importActual<typeof import("node:os")>("node:os");
|
||||
return { ...actual, release: () => "4.19.0-gvisor", platform: () => "linux" };
|
||||
});
|
||||
mockAgentsDir();
|
||||
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
||||
expect(detectAgentRuntime()).toBe("gemini_managed_agent");
|
||||
});
|
||||
|
||||
it("does NOT report gemini_managed_agent when /.agents/ is absent (even on gVisor)", async () => {
|
||||
// A generic gVisor surface (GKE Sandbox / Cloud Run gen2) that doesn't
|
||||
// mount the managed-agent layout must fall through to env-var rules.
|
||||
vi.doMock("node:os", async () => {
|
||||
const actual = await vi.importActual<typeof import("node:os")>("node:os");
|
||||
return { ...actual, release: () => "4.19.0-gvisor", platform: () => "linux" };
|
||||
});
|
||||
vi.doMock("node:fs", async () => {
|
||||
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
|
||||
return {
|
||||
...actual,
|
||||
statSync: (path: string) => {
|
||||
if (path === "/.agents") throw new Error("ENOENT: no such file or directory");
|
||||
return actual.statSync(path);
|
||||
},
|
||||
};
|
||||
});
|
||||
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
||||
expect(detectAgentRuntime()).toBeNull();
|
||||
});
|
||||
|
||||
it("does NOT report gemini_managed_agent when /.agents/ is a directory but the kernel is not gVisor", async () => {
|
||||
// A dev box that happens to have a stray /.agents/ must not false-positive
|
||||
// — the gVisor conjunction is what makes the signal safe.
|
||||
vi.doMock("node:os", async () => {
|
||||
const actual = await vi.importActual<typeof import("node:os")>("node:os");
|
||||
return { ...actual, release: () => "6.8.0-100-generic", platform: () => "linux" };
|
||||
});
|
||||
vi.doMock("node:fs", async () => {
|
||||
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
|
||||
return {
|
||||
...actual,
|
||||
statSync: (path: string) =>
|
||||
path === "/.agents"
|
||||
? ({ isDirectory: () => true } as unknown as import("node:fs").Stats)
|
||||
: actual.statSync(path),
|
||||
readFileSync: (path: string) =>
|
||||
path === "/proc/version"
|
||||
? "Linux version 6.8.0-100-generic (buildd@lcy01)"
|
||||
: actual.readFileSync(path),
|
||||
};
|
||||
});
|
||||
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
||||
expect(detectAgentRuntime()).toBeNull();
|
||||
});
|
||||
|
||||
it("returns gemini_managed_agent over an env-var rule when both signals match", async () => {
|
||||
// If a user happens to set CLAUDECODE=1 inside a Gemini sandbox (or any
|
||||
// odd config), the filesystem+kernel signal wins — Gemini is more
|
||||
// specific than a generic env-var marker.
|
||||
process.env["CLAUDECODE"] = "1";
|
||||
vi.doMock("node:os", async () => {
|
||||
const actual = await vi.importActual<typeof import("node:os")>("node:os");
|
||||
return { ...actual, release: () => "4.19.0-gvisor", platform: () => "linux" };
|
||||
});
|
||||
mockAgentsDir();
|
||||
const { detectAgentRuntime } = await import("./agent_runtime.js");
|
||||
expect(detectAgentRuntime()).toBe("gemini_managed_agent");
|
||||
});
|
||||
});
|
||||
|
||||
describe("detectSandboxRuntime — file-system path", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { existsSync, readFileSync, statSync } from "node:fs";
|
||||
import { platform, release } from "node:os";
|
||||
import { detectWSL } from "./platform.js";
|
||||
|
||||
@@ -30,6 +30,7 @@ export type AgentRuntime =
|
||||
| "hermes"
|
||||
| "openclaw"
|
||||
| "pi"
|
||||
| "gemini_managed_agent"
|
||||
| null;
|
||||
|
||||
interface VendorRule {
|
||||
@@ -144,10 +145,17 @@ export function detectSandboxRuntime(): SandboxRuntime {
|
||||
|
||||
/**
|
||||
* Identify the coding-agent vendor that spawned this process, if any.
|
||||
* Returns null on a regular interactive shell. Only checks for the
|
||||
* EXISTENCE of well-known env vars — never reads their values.
|
||||
* Returns null on a regular interactive shell. Most rules only check the
|
||||
* EXISTENCE of well-known env vars (never their values), but a few agents
|
||||
* are best identified by filesystem markers — those run via dedicated
|
||||
* detector functions ahead of the env-var rule loop.
|
||||
*/
|
||||
export function detectAgentRuntime(): AgentRuntime {
|
||||
// Gemini managed agent — keyed on the `/.agents/` platform mount with a
|
||||
// gVisor guard against false positives. See `isGeminiManagedAgent` for the
|
||||
// uniqueness-anchor-vs-guard split. Env vars alone are insufficient
|
||||
// (`GEMINI_API_KEY` is user-settable), so this runs ahead of VENDOR_RULES.
|
||||
if (isGeminiManagedAgent()) return "gemini_managed_agent";
|
||||
for (const rule of VENDOR_RULES) {
|
||||
if (rule.check(process.env)) return rule.name;
|
||||
}
|
||||
@@ -216,3 +224,67 @@ function isKVM(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini managed-agent sandbox — Google's Managed Agents runtime (the
|
||||
* Antigravity base agent). The platform auto-discovers the agent definition
|
||||
* under `/.agents/` and runs it inside a gVisor kernel.
|
||||
*
|
||||
* Signal hierarchy (the two checks are NOT co-equal):
|
||||
* - `/.agents/` is the *uniqueness anchor*: the platform's agent-definition
|
||||
* mount root. Per Google's Managed Agents docs the runtime scans `/.agents/`
|
||||
* for the agent's instructions (`/.agents/AGENTS.md`) and skills
|
||||
* (`/.agents/skills/<name>/SKILL.md`). Nothing in the generic
|
||||
* Google-Cloud-on-gVisor universe (Cloud Run gen2, GKE Sandbox, Fly.io)
|
||||
* mounts `/.agents/` at the filesystem root.
|
||||
*
|
||||
* We key on the `/.agents/` DIRECTORY, not `/.agents/AGENTS.md`: AGENTS.md
|
||||
* is OPTIONAL. Google's docs state "AGENTS.md is optional ... the
|
||||
* system_instruction and AGENTS.md are additive; both apply when present",
|
||||
* so an agent may declare its instructions inline via `system_instruction`
|
||||
* in agent.yaml and ship no AGENTS.md file. Keying on the file would
|
||||
* silently miss every managed agent that uses inline instructions or a
|
||||
* skills-only definition; the directory mount generalizes across all
|
||||
* managed agents that ship any definition.
|
||||
* - `isGVisor()` is a *guard*, not a co-uniqueness signal. gVisor itself
|
||||
* is shared with GKE Sandbox + Cloud Run gen2 — it does not discriminate
|
||||
* the managed-agent surface from those. Its job here is to rule out the
|
||||
* unlikely case of a human creating `/.agents/` on a non-sandbox host.
|
||||
*
|
||||
* Known coverage gap: an agent defined with ONLY inline `system_instruction`
|
||||
* (no skills, no AGENTS.md) may not materialize a `/.agents/` mount — that
|
||||
* tail can't be closed from the docs and needs an empirical spin to confirm.
|
||||
* The common case (skills and/or AGENTS.md present) is covered.
|
||||
*
|
||||
* Things deliberately NOT keyed on (each fails the uniqueness test —
|
||||
* shared across the broader Google-Cloud-on-gVisor universe or trivially
|
||||
* user-settable on any host):
|
||||
* - gVisor alone
|
||||
* - `Google Compute Engine` DMI (entire GCP reports this)
|
||||
* - `job` cgroup (Google-internal but broadly present)
|
||||
* - egress-proxy env / CA-cert env cluster (any MITM container sets these)
|
||||
* - `/workspace/` (the agent's data mount — generic, not unique)
|
||||
* - `GEMINI_API_KEY` (user-settable on any host)
|
||||
*
|
||||
* Source: Google Managed Agents docs (ai.google.dev/gemini-api/docs/custom-agents
|
||||
* + managed-agents-quickstart) for the `/.agents/` mount contract and AGENTS.md
|
||||
* optionality; empirical introspection of live managed-agent sandboxes by
|
||||
* gemini-agent (2026-06-09) for the gVisor pairing — present across 3
|
||||
* independent fresh sandbox spins (spike + `b9db4e56` + `d59d6361`).
|
||||
*/
|
||||
function isGeminiManagedAgent(): boolean {
|
||||
if (platform() !== "linux") return false;
|
||||
// The uniqueness anchor: the managed-agent definition mount root. We key on
|
||||
// the `/.agents/` directory (not the optional AGENTS.md file) so skills-only
|
||||
// and inline-instruction agents are still detected. Nothing else on gVisor
|
||||
// (Cloud Run, GKE Sandbox, Fly.io) creates this path. Require an actual
|
||||
// directory — a stray file or symlink named `/.agents` must not match.
|
||||
try {
|
||||
if (!statSync("/.agents").isDirectory()) return false;
|
||||
} catch {
|
||||
return false; // ENOENT / EACCES — no mount, not a managed agent.
|
||||
}
|
||||
// The guard: rule out a stray user-created `/.agents/` on a non-sandbox
|
||||
// host. Not a second uniqueness signal — gVisor isn't unique on its own.
|
||||
return isGVisor();
|
||||
}
|
||||
|
||||
@@ -39,10 +39,11 @@ export interface SystemMeta {
|
||||
*/
|
||||
sandbox_runtime: SandboxRuntime;
|
||||
/**
|
||||
* Coding-agent vendor that spawned this process, if any (claude_code,
|
||||
* codex, cursor, copilot_agent, replit, hermes, openclaw, pi).
|
||||
* Detected by env-var existence only — values are never read. Every rule
|
||||
* keys on a marker that has a public-source citation in agent_runtime.ts;
|
||||
* Coding-agent vendor that spawned this process, if any (see the
|
||||
* `AgentRuntime` union in agent_runtime.ts for the full, current set).
|
||||
* Most rules check env-var existence only — values are never read; a few
|
||||
* use filesystem/kernel markers (e.g. the Gemini managed-agent mount).
|
||||
* Every rule keys on a marker with a source citation in agent_runtime.ts;
|
||||
* unverified guesses are deliberately omitted (false-negative > guess).
|
||||
* null when no agent is detected.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user