wip inspector

This commit is contained in:
Nathan Flurry 2026-01-27 19:26:13 -08:00
parent 7a5bb2b8b0
commit f67b6fc4b1
8 changed files with 198 additions and 152 deletions

View file

@ -51,10 +51,24 @@ const getDefaultEndpoint = () => {
return origin; return origin;
}; };
const getInitialConnection = () => {
if (typeof window === "undefined") {
return { endpoint: "http://127.0.0.1:2468", token: "" };
}
const params = new URLSearchParams(window.location.search);
const urlParam = params.get("url")?.trim();
const tokenParam = params.get("token") ?? "";
return {
endpoint: urlParam && urlParam.length > 0 ? urlParam : getDefaultEndpoint(),
token: tokenParam
};
};
export default function App() { export default function App() {
const issueTrackerUrl = "https://github.com/rivet-dev/sandbox-agent/issues/new"; const issueTrackerUrl = "https://github.com/rivet-dev/sandbox-agent/issues/new";
const [endpoint, setEndpoint] = useState(getDefaultEndpoint); const initialConnectionRef = useRef(getInitialConnection());
const [token, setToken] = useState(""); const [endpoint, setEndpoint] = useState(initialConnectionRef.current.endpoint);
const [token, setToken] = useState(initialConnectionRef.current.token);
const [connected, setConnected] = useState(false); const [connected, setConnected] = useState(false);
const [connecting, setConnecting] = useState(false); const [connecting, setConnecting] = useState(false);
const [connectError, setConnectError] = useState<string | null>(null); const [connectError, setConnectError] = useState<string | null>(null);

View file

@ -5,6 +5,7 @@ import {
Brain, Brain,
Download, Download,
FileDiff, FileDiff,
Gauge,
GitBranch, GitBranch,
HelpCircle, HelpCircle,
Image, Image,
@ -30,6 +31,7 @@ const badges = [
{ key: "sessionLifecycle", label: "Lifecycle", icon: PlayCircle }, { key: "sessionLifecycle", label: "Lifecycle", icon: PlayCircle },
{ key: "errorEvents", label: "Errors", icon: AlertTriangle }, { key: "errorEvents", label: "Errors", icon: AlertTriangle },
{ key: "reasoning", label: "Reasoning", icon: Brain }, { key: "reasoning", label: "Reasoning", icon: Brain },
{ key: "status", label: "Status", icon: Gauge },
{ key: "commandExecution", label: "Commands", icon: Terminal }, { key: "commandExecution", label: "Commands", icon: Terminal },
{ key: "fileChanges", label: "File Changes", icon: FileDiff }, { key: "fileChanges", label: "File Changes", icon: FileDiff },
{ key: "mcpTools", label: "MCP", icon: Plug }, { key: "mcpTools", label: "MCP", icon: Plug },

View file

@ -8,6 +8,7 @@ export type AgentCapabilitiesView = AgentCapabilities & {
sessionLifecycle?: boolean; sessionLifecycle?: boolean;
errorEvents?: boolean; errorEvents?: boolean;
reasoning?: boolean; reasoning?: boolean;
status?: boolean;
commandExecution?: boolean; commandExecution?: boolean;
fileChanges?: boolean; fileChanges?: boolean;
mcpTools?: boolean; mcpTools?: boolean;
@ -26,6 +27,7 @@ export const emptyCapabilities: AgentCapabilitiesView = {
sessionLifecycle: false, sessionLifecycle: false,
errorEvents: false, errorEvents: false,
reasoning: false, reasoning: false,
status: false,
commandExecution: false, commandExecution: false,
fileChanges: false, fileChanges: false,
mcpTools: false, mcpTools: false,

View file

@ -1,7 +1,7 @@
{ {
"name": "sandbox-agent", "name": "sandbox-agent",
"version": "0.1.0", "version": "0.1.0",
"description": "TypeScript SDK for sandbox-agent", "description": "Universal API for automatic coding agents in sandboxes. Supprots Claude Code, Codex, OpenCode, and Amp.",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -67,6 +67,7 @@ export interface components {
sessionLifecycle: boolean; sessionLifecycle: boolean;
/** @description Whether this agent uses a shared long-running server process (vs per-turn subprocess) */ /** @description Whether this agent uses a shared long-running server process (vs per-turn subprocess) */
sharedProcess: boolean; sharedProcess: boolean;
status: boolean;
streamingDeltas: boolean; streamingDeltas: boolean;
textMessages: boolean; textMessages: boolean;
toolCalls: boolean; toolCalls: boolean;

View file

@ -76,6 +76,8 @@ To keep snapshots deterministic:
- Permission flow snapshots are truncated after the permission request (or first assistant) event. - Permission flow snapshots are truncated after the permission request (or first assistant) event.
- Unknown events are preserved as `kind: unknown` (raw payload in universal schema). - Unknown events are preserved as `kind: unknown` (raw payload in universal schema).
- Prefer snapshot-based event skeleton assertions over manual event-order assertions in tests. - Prefer snapshot-based event skeleton assertions over manual event-order assertions in tests.
- **Never update snapshots based on any agent that is not the mock agent.** The mock agent is the source of truth for snapshots; other agents must be compared against the mock snapshots without regenerating them.
- Agent-specific endpoints keep per-agent snapshots; any session-related snapshots must use the mock baseline as the single source of truth.
## Typical commands ## Typical commands

View file

@ -1,4 +1,4 @@
use std::collections::BTreeMap; use std::collections::{BTreeMap, HashMap};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use axum::body::{Body, Bytes}; use axum::body::{Body, Bytes};
@ -12,7 +12,7 @@ use tempfile::TempDir;
use sandbox_agent_agent_management::agents::{AgentId, AgentManager}; use sandbox_agent_agent_management::agents::{AgentId, AgentManager};
use sandbox_agent_agent_management::testing::{test_agents_from_env, TestAgentConfig}; use sandbox_agent_agent_management::testing::{test_agents_from_env, TestAgentConfig};
use sandbox_agent_agent_credentials::ExtractedCredentials; use sandbox_agent_agent_credentials::ExtractedCredentials;
use sandbox_agent::router::{build_router, AppState, AuthConfig}; use sandbox_agent::router::{build_router, AgentCapabilities, AgentListResponse, AppState, AuthConfig};
use tower::util::ServiceExt; use tower::util::ServiceExt;
use tower_http::cors::CorsLayer; use tower_http::cors::CorsLayer;
@ -455,6 +455,12 @@ fn normalize_event(event: &Value, seq: usize) -> Value {
if let Some(event_type) = event.get("type").and_then(Value::as_str) { if let Some(event_type) = event.get("type").and_then(Value::as_str) {
map.insert("type".to_string(), Value::String(event_type.to_string())); map.insert("type".to_string(), Value::String(event_type.to_string()));
} }
if let Some(source) = event.get("source").and_then(Value::as_str) {
map.insert("source".to_string(), Value::String(source.to_string()));
}
if let Some(synthetic) = event.get("synthetic").and_then(Value::as_bool) {
map.insert("synthetic".to_string(), Value::Bool(synthetic));
}
let data = event.get("data").unwrap_or(&Value::Null); let data = event.get("data").unwrap_or(&Value::Null);
match event.get("type").and_then(Value::as_str).unwrap_or("") { match event.get("type").and_then(Value::as_str).unwrap_or("") {
"session.started" => { "session.started" => {
@ -668,6 +674,17 @@ fn normalize_health(value: &Value) -> Value {
Value::Object(map) Value::Object(map)
} }
async fn fetch_capabilities(app: &Router) -> HashMap<String, AgentCapabilities> {
let (status, payload) = send_json(app, Method::GET, "/v1/agents", None).await;
assert_eq!(status, StatusCode::OK, "list agents");
let response: AgentListResponse = serde_json::from_value(payload).expect("agents payload");
response
.agents
.into_iter()
.map(|agent| (agent.id, agent.capabilities))
.collect()
}
fn snapshot_status(status: StatusCode) -> Value { fn snapshot_status(status: StatusCode) -> Value {
json!({ "status": status.as_u16() }) json!({ "status": status.as_u16() })
} }
@ -1077,16 +1094,21 @@ async fn api_endpoints_snapshots() {
async fn approval_flow_snapshots() { async fn approval_flow_snapshots() {
let configs = test_agents_from_env().expect("configure SANDBOX_TEST_AGENTS or install agents"); let configs = test_agents_from_env().expect("configure SANDBOX_TEST_AGENTS or install agents");
let app = TestApp::new(); let app = TestApp::new();
let capabilities = fetch_capabilities(&app.app).await;
for config in &configs { for config in &configs {
// OpenCode doesn't support "plan" permission mode required for approval flows // OpenCode doesn't support "plan" permission mode required for approval flows
if config.agent == AgentId::Opencode { if config.agent == AgentId::Opencode {
continue; continue;
} }
let caps = capabilities
.get(config.agent.as_str())
.expect("capabilities missing");
let _guard = apply_credentials(&config.credentials); let _guard = apply_credentials(&config.credentials);
install_agent(&app.app, config.agent).await; install_agent(&app.app, config.agent).await;
if caps.plan_mode && caps.permissions {
let permission_session = format!("perm-{}", config.agent.as_str()); let permission_session = format!("perm-{}", config.agent.as_str());
create_session(&app.app, config.agent, &permission_session, "plan").await; create_session(&app.app, config.agent, &permission_session, "plan").await;
let status = send_status( let status = send_status(
@ -1148,7 +1170,9 @@ async fn approval_flow_snapshots() {
})); }));
}); });
} }
}
if caps.questions {
let question_reply_session = format!("question-reply-{}", config.agent.as_str()); let question_reply_session = format!("question-reply-{}", config.agent.as_str());
create_session(&app.app, config.agent, &question_reply_session, "plan").await; create_session(&app.app, config.agent, &question_reply_session, "plan").await;
let status = send_status( let status = send_status(
@ -1274,6 +1298,7 @@ async fn approval_flow_snapshots() {
} }
} }
} }
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn http_events_snapshots() { async fn http_events_snapshots() {