-
Notifications
You must be signed in to change notification settings - Fork 163
feat(miosa): interactive PTY, live resource metrics, agent-browser env #1212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ross0x01
merged 1 commit into
hackerai-tech:codex/miosa-sandbox-rollout
from
robertohluna:feat/miosa-pty-metrics-parity
Aug 29, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| /** | ||
| * Tests for sampleMiosaMetrics — live CPU/memory/disk sampling inside a MIOSA | ||
| * sandbox, used by the pre-command health check. | ||
| */ | ||
|
|
||
| import { sampleMiosaMetrics } from "../miosa-metrics"; | ||
| import type { MiosaSandbox } from "../miosa-sandbox"; | ||
|
|
||
| type RunResult = { stdout: string; stderr: string; exitCode: number }; | ||
|
|
||
| function sandboxReturning(result: RunResult | Error): MiosaSandbox { | ||
| return { | ||
| commands: { | ||
| run: async () => { | ||
| if (result instanceof Error) throw result; | ||
| return result; | ||
| }, | ||
| }, | ||
| } as unknown as MiosaSandbox; | ||
| } | ||
|
|
||
| /** Two /proc/stat lines whose delta is 100 busy jiffies out of 200 total. */ | ||
| const CPU_50_PCT = [ | ||
| "CPU_A:cpu 100 0 100 800 0 0 0 0 0 0", | ||
| "CPU_B:cpu 150 0 150 900 0 0 0 0 0 0", | ||
| ].join("\n"); | ||
|
|
||
| function probeOutput(overrides: Partial<Record<string, string>> = {}): string { | ||
| const base: Record<string, string> = { | ||
| cpu: CPU_50_PCT, | ||
| memTotal: "MEM_TOTAL:1000000", | ||
| memAvail: "MEM_AVAIL:250000", | ||
| diskUsed: "DISK_USED:2000", | ||
| diskTotal: "DISK_TOTAL:10000", | ||
| }; | ||
| const merged = { ...base, ...overrides }; | ||
| return [ | ||
| merged.cpu, | ||
| merged.memTotal, | ||
| merged.memAvail, | ||
| merged.diskUsed, | ||
| merged.diskTotal, | ||
| ].join("\n"); | ||
| } | ||
|
|
||
| describe("sampleMiosaMetrics", () => { | ||
| it("derives cpu, memory and disk percentages from the guest probe", async () => { | ||
| const sandbox = sandboxReturning({ | ||
| stdout: probeOutput(), | ||
| stderr: "", | ||
| exitCode: 0, | ||
| }); | ||
|
|
||
| const metrics = await sampleMiosaMetrics(sandbox); | ||
|
|
||
| // busy delta 100 of 200 total jiffies | ||
| expect(metrics?.cpuPct).toBeCloseTo(50, 5); | ||
| // 750000 of 1000000 kB in use | ||
| expect(metrics?.memPct).toBeCloseTo(75, 5); | ||
| // 2000 of 10000 kB used | ||
| expect(metrics?.diskPct).toBeCloseTo(20, 5); | ||
| }); | ||
|
|
||
| it("reports 0% CPU for an idle guest rather than NaN", async () => { | ||
| const idle = [ | ||
| "CPU_A:cpu 100 0 100 800 0 0 0 0 0 0", | ||
| "CPU_B:cpu 100 0 100 800 0 0 0 0 0 0", | ||
| ].join("\n"); | ||
|
|
||
| const metrics = await sampleMiosaMetrics( | ||
| sandboxReturning({ | ||
| stdout: probeOutput({ cpu: idle }), | ||
| stderr: "", | ||
| exitCode: 0, | ||
| }), | ||
| ); | ||
|
|
||
| expect(metrics?.cpuPct).toBe(0); | ||
| }); | ||
|
|
||
| it("returns null when the probe exits non-zero", async () => { | ||
| const metrics = await sampleMiosaMetrics( | ||
| sandboxReturning({ stdout: "", stderr: "boom", exitCode: 1 }), | ||
| ); | ||
| expect(metrics).toBeNull(); | ||
| }); | ||
|
|
||
| it("returns null instead of throwing when the command itself fails", async () => { | ||
| const metrics = await sampleMiosaMetrics( | ||
| sandboxReturning(new Error("sandbox unreachable")), | ||
| ); | ||
| expect(metrics).toBeNull(); | ||
| }); | ||
|
|
||
| it("returns null on unparseable output rather than reporting zeroes", async () => { | ||
| // A sandbox that answers but without the expected fields must not be | ||
| // reported as a perfectly idle machine — that would mask real pressure. | ||
| const metrics = await sampleMiosaMetrics( | ||
| sandboxReturning({ stdout: "unexpected", stderr: "", exitCode: 0 }), | ||
| ); | ||
| expect(metrics).toBeNull(); | ||
| }); | ||
|
|
||
| it("tolerates a guest that reports no disk figures", async () => { | ||
| const metrics = await sampleMiosaMetrics( | ||
| sandboxReturning({ | ||
| stdout: [CPU_50_PCT, "MEM_TOTAL:1000000", "MEM_AVAIL:250000"].join( | ||
| "\n", | ||
| ), | ||
| stderr: "", | ||
| exitCode: 0, | ||
| }), | ||
| ); | ||
|
|
||
| expect(metrics).not.toBeNull(); | ||
| expect(metrics?.diskPct).toBe(0); | ||
| expect(metrics?.memPct).toBeCloseTo(75, 5); | ||
| }); | ||
|
|
||
| it("clamps percentages into 0-100", async () => { | ||
| // MemAvailable can briefly exceed MemTotal on some kernels; a negative | ||
| // percentage would render as "-3%" in a warning string. | ||
| const metrics = await sampleMiosaMetrics( | ||
| sandboxReturning({ | ||
| stdout: probeOutput({ | ||
| memTotal: "MEM_TOTAL:1000", | ||
| memAvail: "MEM_AVAIL:1200", | ||
| }), | ||
| stderr: "", | ||
| exitCode: 0, | ||
| }), | ||
| ); | ||
|
|
||
| expect(metrics?.memPct).toBe(0); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| /** | ||
| * MIOSA sandbox resource metrics. | ||
| * | ||
| * MIOSA's `GET /api/v1/sandboxes/:id/metrics` currently reports the sandbox's | ||
| * *configured* shape (cpu_count / memory_mb / disk_size_mb) and an empty time | ||
| * series — it does not yet publish live utilisation. Wiring the health check to | ||
| * it would return numbers that never move, which is worse than none: the agent | ||
| * would read "0% CPU" while the box is pegged. | ||
| * | ||
| * So sample the guest directly instead. One short exec reads /proc and df, and | ||
| * the CPU figure comes from two /proc/stat snapshots 200ms apart, because a | ||
| * single snapshot only gives cumulative jiffies since boot, not current load. | ||
| * | ||
| * Swap this for the API once MIOSA publishes a real series. | ||
| */ | ||
|
|
||
| import type { SandboxResourceMetrics } from "@/types"; | ||
| import type { MiosaSandbox } from "./miosa-sandbox"; | ||
|
|
||
| /** Gap between the two /proc/stat reads used to derive CPU utilisation. */ | ||
| const CPU_SAMPLE_INTERVAL_MS = 200; | ||
|
|
||
| /** Keep the probe well under any caller timeout — metrics must never block a command. */ | ||
| const PROBE_TIMEOUT_MS = 5_000; | ||
|
|
||
| const PROBE = ` | ||
| a=$(head -n1 /proc/stat) | ||
| sleep ${CPU_SAMPLE_INTERVAL_MS / 1000} | ||
| b=$(head -n1 /proc/stat) | ||
| echo "CPU_A:$a" | ||
| echo "CPU_B:$b" | ||
| echo "MEM_TOTAL:$(awk '/^MemTotal:/{print $2}' /proc/meminfo)" | ||
| echo "MEM_AVAIL:$(awk '/^MemAvailable:/{print $2}' /proc/meminfo)" | ||
| df -k / | awk 'NR==2{print "DISK_USED:"$3; print "DISK_TOTAL:"($3+$4)}' | ||
| `.trim(); | ||
|
|
||
| function field(lines: string[], prefix: string): string | null { | ||
| const hit = lines.find((l) => l.startsWith(prefix)); | ||
| return hit ? hit.slice(prefix.length) : null; | ||
| } | ||
|
|
||
| /** | ||
| * CPU utilisation between two `/proc/stat` cpu lines. | ||
| * | ||
| * Field 4 is idle and field 5 is iowait; everything else counts as busy. | ||
| * Returns 0 when the two samples are identical (an idle box), never NaN. | ||
| */ | ||
| function cpuPctFromStat(a: string, b: string): number { | ||
| const parse = (line: string): number[] => | ||
| line | ||
| .replace(/^cpu\s+/, "") | ||
| .trim() | ||
| .split(/\s+/) | ||
| .map((n) => Number.parseInt(n, 10) || 0); | ||
|
|
||
| const first = parse(a); | ||
| const second = parse(b); | ||
| if (first.length < 5 || second.length < 5) return 0; | ||
|
|
||
| const totalOf = (v: number[]) => v.reduce((sum, n) => sum + n, 0); | ||
| const idleOf = (v: number[]) => (v[3] ?? 0) + (v[4] ?? 0); | ||
|
|
||
| const totalDelta = totalOf(second) - totalOf(first); | ||
| const idleDelta = idleOf(second) - idleOf(first); | ||
| if (totalDelta <= 0) return 0; | ||
|
|
||
| const pct = ((totalDelta - idleDelta) / totalDelta) * 100; | ||
| return Math.min(100, Math.max(0, pct)); | ||
| } | ||
|
|
||
| /** | ||
| * Sample live CPU, memory and disk usage from inside a MIOSA sandbox. | ||
| * | ||
| * Returns `null` — never throws — when the probe fails or the guest returns | ||
| * something unparseable. A metrics failure must not fail the health check that | ||
| * called it, let alone the command behind it. | ||
| */ | ||
| export async function sampleMiosaMetrics( | ||
| sandbox: MiosaSandbox, | ||
| ): Promise<SandboxResourceMetrics | null> { | ||
| try { | ||
| const result = await sandbox.commands.run(PROBE, { | ||
| timeoutMs: PROBE_TIMEOUT_MS, | ||
| }); | ||
| if (result.exitCode !== 0) return null; | ||
|
|
||
| const lines = result.stdout.split("\n").map((l) => l.trim()); | ||
|
|
||
| const cpuA = field(lines, "CPU_A:"); | ||
| const cpuB = field(lines, "CPU_B:"); | ||
| const memTotalKb = Number(field(lines, "MEM_TOTAL:")); | ||
| const memAvailKb = Number(field(lines, "MEM_AVAIL:")); | ||
| const diskUsedKb = Number(field(lines, "DISK_USED:")); | ||
| const diskTotalKb = Number(field(lines, "DISK_TOTAL:")); | ||
|
|
||
| if (!cpuA || !cpuB) return null; | ||
| if (!Number.isFinite(memTotalKb) || memTotalKb <= 0) return null; | ||
|
|
||
| const cpuPct = cpuPctFromStat(cpuA, cpuB); | ||
| const memPct = ((memTotalKb - memAvailKb) / memTotalKb) * 100; | ||
| const diskPct = | ||
| Number.isFinite(diskTotalKb) && diskTotalKb > 0 | ||
| ? (diskUsedKb / diskTotalKb) * 100 | ||
| : 0; | ||
|
|
||
| return { | ||
| cpuPct, | ||
| memPct: Math.min(100, Math.max(0, memPct)), | ||
| diskPct: Math.min(100, Math.max(0, diskPct)), | ||
| }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reject missing
MEM_AVAILoutput.Number(null)andNumber("")return0. A probe withoutMEM_AVAILtherefore reports 100% memory usage and can create a false warning. Validate that the raw field exists and that its numeric value is finite before calculatingmemPct.Proposed fix
🤖 Prompt for AI Agents