Skip to content

Commit 532c2bc

Browse files
committed
fix: persist sparsekernel browser origins
1 parent 54504cd commit 532c2bc

5 files changed

Lines changed: 102 additions & 43 deletions

File tree

crates/sparsekernel-cli/src/lib.rs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ use serde::Deserialize;
55
use serde_json::{json, Value};
66
use sparsekernel_core::{
77
probe_browser_endpoint, probe_sandbox_backends, AppendTranscriptEventInput, ArtifactStore,
8-
AuditInput, BrowserBroker, CapabilityCheck, CompleteToolCallInput, CreateToolCallInput,
9-
EnqueueTaskInput, GrantCapabilityInput, LedgerToolBroker, ListBrowserObservationsInput,
10-
ListBrowserTargetsInput, LocalSandboxBroker, MockBrowserBroker, RecordBrowserObservationInput,
11-
RecordBrowserTargetInput, ResourceBudgetUpdateInput, SandboxBroker, SparseKernelDb,
12-
SparseKernelPaths, ToolBroker, UpsertSessionInput, SPARSEKERNEL_PROTOCOL_VERSION,
8+
AuditInput, BrowserBroker, BrowserContextAcquireInput, CapabilityCheck, CompleteToolCallInput,
9+
CreateToolCallInput, EnqueueTaskInput, GrantCapabilityInput, LedgerToolBroker,
10+
ListBrowserObservationsInput, ListBrowserTargetsInput, LocalSandboxBroker, MockBrowserBroker,
11+
RecordBrowserObservationInput, RecordBrowserTargetInput, ResourceBudgetUpdateInput,
12+
SandboxBroker, SparseKernelDb, SparseKernelPaths, ToolBroker, UpsertSessionInput,
13+
SPARSEKERNEL_PROTOCOL_VERSION,
1314
};
1415
use std::collections::HashMap;
1516
use std::error::Error;
@@ -317,6 +318,7 @@ struct AcquireBrowserContextRequest {
317318
trust_zone_id: String,
318319
max_contexts: Option<i64>,
319320
cdp_endpoint: Option<String>,
321+
allowed_origins: Option<Value>,
320322
}
321323

322324
#[derive(Debug, Deserialize)]
@@ -1497,12 +1499,15 @@ pub fn handle_api_request_with_daemon_state(
14971499
ApiReply {
14981500
status_code: 200,
14991501
body: serde_json::to_value(broker.acquire_context(
1500-
input.agent_id.as_deref(),
1501-
input.session_id.as_deref(),
1502-
input.task_id.as_deref(),
1503-
&input.trust_zone_id,
1504-
input.max_contexts.unwrap_or(2),
1505-
input.cdp_endpoint.as_deref(),
1502+
BrowserContextAcquireInput {
1503+
agent_id: input.agent_id.as_deref(),
1504+
session_id: input.session_id.as_deref(),
1505+
task_id: input.task_id.as_deref(),
1506+
trust_zone_id: &input.trust_zone_id,
1507+
max_contexts: input.max_contexts.unwrap_or(2),
1508+
cdp_endpoint: input.cdp_endpoint.as_deref(),
1509+
allowed_origins: input.allowed_origins.as_ref(),
1510+
},
15061511
)?)?,
15071512
}
15081513
}
@@ -2371,10 +2376,12 @@ mod tests {
23712376
"trust_zone_id": "public_web",
23722377
"max_contexts": 1,
23732378
"cdp_endpoint": "http://127.0.0.1:9222",
2379+
"allowed_origins": ["https://example.com"],
23742380
}),
23752381
);
23762382
let context_id = context["id"].as_str().unwrap().to_string();
23772383
assert_eq!(context["status"], "active");
2384+
assert_eq!(context["allowed_origins"][0], "https://example.com");
23782385

23792386
let observed = json_call(
23802387
&mut db,
@@ -2438,6 +2445,7 @@ mod tests {
24382445
.unwrap()
24392446
.body;
24402447
assert_eq!(contexts.as_array().unwrap().len(), 1);
2448+
assert_eq!(contexts[0]["allowed_origins"][0], "https://example.com");
24412449

24422450
let pools = handle_api_request(&mut db, "GET", "/browser/pools", &[])
24432451
.unwrap()

crates/sparsekernel-core/src/lib.rs

Lines changed: 74 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ pub struct BrowserContextRecord {
397397
pub session_id: Option<String>,
398398
pub task_id: Option<String>,
399399
pub profile_mode: String,
400+
pub allowed_origins: Option<Value>,
400401
pub status: String,
401402
pub created_at: String,
402403
}
@@ -1736,7 +1737,7 @@ impl SparseKernelDb {
17361737

17371738
pub fn list_browser_contexts(&self, limit: i64) -> Result<Vec<BrowserContextRecord>> {
17381739
let mut stmt = self.conn.prepare(
1739-
"SELECT id, pool_id, agent_id, session_id, task_id, profile_mode, status, created_at
1740+
"SELECT id, pool_id, agent_id, session_id, task_id, profile_mode, allowed_origins_json, status, created_at
17401741
FROM browser_contexts ORDER BY created_at DESC LIMIT ?",
17411742
)?;
17421743
let rows = stmt.query_map(params![limit.max(0)], browser_context_from_row)?;
@@ -2108,8 +2109,9 @@ fn browser_context_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<Browser
21082109
session_id: row.get(3)?,
21092110
task_id: row.get(4)?,
21102111
profile_mode: row.get(5)?,
2111-
status: row.get(6)?,
2112-
created_at: row.get(7)?,
2112+
allowed_origins: parse_json(row.get(6)?),
2113+
status: row.get(7)?,
2114+
created_at: row.get(8)?,
21132115
})
21142116
}
21152117

@@ -2715,30 +2717,39 @@ impl ToolBroker for LedgerToolBroker<'_> {
27152717
pub trait BrowserBroker {
27162718
fn acquire_context(
27172719
&self,
2718-
agent_id: Option<&str>,
2719-
session_id: Option<&str>,
2720-
task_id: Option<&str>,
2721-
trust_zone_id: &str,
2722-
max_contexts: i64,
2723-
cdp_endpoint: Option<&str>,
2720+
input: BrowserContextAcquireInput<'_>,
27242721
) -> Result<BrowserContextRecord>;
27252722
fn release_context(&self, context_id: &str) -> Result<bool>;
27262723
}
27272724

2725+
pub struct BrowserContextAcquireInput<'a> {
2726+
pub agent_id: Option<&'a str>,
2727+
pub session_id: Option<&'a str>,
2728+
pub task_id: Option<&'a str>,
2729+
pub trust_zone_id: &'a str,
2730+
pub max_contexts: i64,
2731+
pub cdp_endpoint: Option<&'a str>,
2732+
pub allowed_origins: Option<&'a Value>,
2733+
}
2734+
27282735
pub struct MockBrowserBroker<'a> {
27292736
pub db: &'a SparseKernelDb,
27302737
}
27312738

27322739
impl BrowserBroker for MockBrowserBroker<'_> {
27332740
fn acquire_context(
27342741
&self,
2735-
agent_id: Option<&str>,
2736-
session_id: Option<&str>,
2737-
task_id: Option<&str>,
2738-
trust_zone_id: &str,
2739-
max_contexts: i64,
2740-
cdp_endpoint: Option<&str>,
2742+
input: BrowserContextAcquireInput<'_>,
27412743
) -> Result<BrowserContextRecord> {
2744+
let BrowserContextAcquireInput {
2745+
agent_id,
2746+
session_id,
2747+
task_id,
2748+
trust_zone_id,
2749+
max_contexts,
2750+
cdp_endpoint,
2751+
allowed_origins,
2752+
} = input;
27422753
if let Some(endpoint) = cdp_endpoint {
27432754
validate_browser_cdp_endpoint(endpoint)?;
27442755
}
@@ -2815,9 +2826,17 @@ impl BrowserBroker for MockBrowserBroker<'_> {
28152826
}
28162827
let context_id = format!("browser_ctx_{}", Uuid::new_v4());
28172828
self.db.conn.execute(
2818-
"INSERT INTO browser_contexts(id, pool_id, agent_id, session_id, task_id, profile_mode, status, created_at)
2819-
VALUES(?, ?, ?, ?, ?, 'ephemeral', 'active', ?)",
2820-
params![context_id, pool_id, agent_id, session_id, task_id, now],
2829+
"INSERT INTO browser_contexts(id, pool_id, agent_id, session_id, task_id, profile_mode, allowed_origins_json, status, created_at)
2830+
VALUES(?, ?, ?, ?, ?, 'ephemeral', ?, 'active', ?)",
2831+
params![
2832+
context_id,
2833+
pool_id,
2834+
agent_id,
2835+
session_id,
2836+
task_id,
2837+
json_text(allowed_origins),
2838+
now
2839+
],
28212840
)?;
28222841
self.db.conn.execute(
28232842
"INSERT INTO resource_leases(id, resource_type, resource_id, owner_task_id, owner_agent_id, trust_zone_id, status, created_at, updated_at)
@@ -2843,6 +2862,7 @@ impl BrowserBroker for MockBrowserBroker<'_> {
28432862
session_id: session_id.map(str::to_string),
28442863
task_id: task_id.map(str::to_string),
28452864
profile_mode: "ephemeral".to_string(),
2865+
allowed_origins: allowed_origins.cloned(),
28462866
status: "active".to_string(),
28472867
created_at: now,
28482868
})
@@ -3526,10 +3546,26 @@ mod tests {
35263546
.unwrap();
35273547
let broker = MockBrowserBroker { db: &db };
35283548
assert!(broker
3529-
.acquire_context(Some("agent-a"), None, None, "public_web", 10, None)
3549+
.acquire_context(BrowserContextAcquireInput {
3550+
agent_id: Some("agent-a"),
3551+
session_id: None,
3552+
task_id: None,
3553+
trust_zone_id: "public_web",
3554+
max_contexts: 10,
3555+
cdp_endpoint: None,
3556+
allowed_origins: None,
3557+
})
35303558
.is_ok());
35313559
let denied = broker
3532-
.acquire_context(Some("agent-a"), None, None, "authenticated_web", 10, None)
3560+
.acquire_context(BrowserContextAcquireInput {
3561+
agent_id: Some("agent-a"),
3562+
session_id: None,
3563+
task_id: None,
3564+
trust_zone_id: "authenticated_web",
3565+
max_contexts: 10,
3566+
cdp_endpoint: None,
3567+
allowed_origins: None,
3568+
})
35333569
.unwrap_err()
35343570
.to_string();
35353571
assert!(denied.contains("browser context budget exhausted"));
@@ -3768,14 +3804,15 @@ mod tests {
37683804
.unwrap();
37693805
let browser = MockBrowserBroker { db: &db };
37703806
let context = browser
3771-
.acquire_context(
3772-
Some("main"),
3773-
None,
3774-
None,
3775-
"public_web",
3776-
1,
3777-
Some("http://127.0.0.1:9222"),
3778-
)
3807+
.acquire_context(BrowserContextAcquireInput {
3808+
agent_id: Some("main"),
3809+
session_id: None,
3810+
task_id: None,
3811+
trust_zone_id: "public_web",
3812+
max_contexts: 1,
3813+
cdp_endpoint: Some("http://127.0.0.1:9222"),
3814+
allowed_origins: None,
3815+
})
37793816
.unwrap();
37803817
let pools = db.list_browser_pools().unwrap();
37813818
assert_eq!(pools[0].browser_kind, "cdp");
@@ -3784,7 +3821,15 @@ mod tests {
37843821
Some("http://127.0.0.1:9222")
37853822
);
37863823
assert!(browser
3787-
.acquire_context(Some("main"), None, None, "public_web", 1, None)
3824+
.acquire_context(BrowserContextAcquireInput {
3825+
agent_id: Some("main"),
3826+
session_id: None,
3827+
task_id: None,
3828+
trust_zone_id: "public_web",
3829+
max_contexts: 1,
3830+
cdp_endpoint: None,
3831+
allowed_origins: None,
3832+
})
37883833
.is_err());
37893834
assert!(browser.release_context(&context.id).unwrap());
37903835
let sandbox = LocalSandboxBroker { db: &db };

docs/reference/test.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ title: "Tests"
1414
- `OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed`: explicit broad changed test run. Use it when a test harness/config/package edit should fall back to Vitest's broader changed-test behavior.
1515
- `pnpm changed:lanes`: shows the architectural lanes triggered by the diff against `origin/main`.
1616
- `pnpm check:changed`: runs the smart changed check gate for the diff against `origin/main`. It runs typecheck, lint, and guard commands for the affected architectural lanes, but does not run Vitest tests. Use `pnpm test:changed` or explicit `pnpm test <target>` for test proof.
17-
- Rust workspace changes route through active-toolchain `cargo fmt --all --check`, `cargo package -p sparsekernel-core --locked`, `cargo clippy --workspace --all-targets --locked -- -D warnings`, and `cargo test --workspace --locked` locally; CI runs the same Rust lane with Rust 1.80.0 to enforce the workspace MSRV.
17+
- Rust workspace changes route through active-toolchain `cargo fmt --all --check`, `cargo package -p sparsekernel-core --locked --allow-dirty`, `cargo clippy --workspace --all-targets --locked -- -D warnings`, and `cargo test --workspace --locked` locally; CI runs the clean package equivalent with Rust 1.80.0 to enforce the workspace MSRV.
1818
- `pnpm test`: routes explicit file/directory targets through scoped Vitest lanes. Untargeted runs use fixed shard groups and expand to leaf configs for local parallel execution; the extension group always expands to the per-extension shard configs instead of one giant root-project process.
1919
- Test wrapper runs end with a short `[test] passed|failed|skipped ... in ...` summary. Vitest's own duration line stays the per-shard detail.
2020
- Full, extension, and include-pattern shard runs update local timing data in `.artifacts/vitest-shard-timings.json`; later whole-config runs use those timings to balance slow and fast shards. Include-pattern CI shards append the shard name to the timing key, which keeps filtered shard timings visible without replacing whole-config timing data. Set `OPENCLAW_TEST_PROJECTS_TIMINGS=0` to ignore the local timing artifact.

scripts/check-changed.mjs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,13 @@ export function createChangedCheckPlan(result, options = {}) {
126126
}
127127
if (lanes.rust) {
128128
addCommand("rust format", "cargo", ["fmt", "--all", "--check"]);
129-
addCommand("rust package", "cargo", ["package", "-p", "sparsekernel-core", "--locked"]);
129+
addCommand("rust package", "cargo", [
130+
"package",
131+
"-p",
132+
"sparsekernel-core",
133+
"--locked",
134+
"--allow-dirty",
135+
]);
130136
addCommand("rust lint", "cargo", [
131137
"clippy",
132138
"--workspace",

test/scripts/changed-lanes.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ describe("scripts/changed-lanes", () => {
256256
});
257257
expect(plan.commands.find((command) => command.name === "rust package")).toMatchObject({
258258
bin: "cargo",
259-
args: ["package", "-p", "sparsekernel-core", "--locked"],
259+
args: ["package", "-p", "sparsekernel-core", "--locked", "--allow-dirty"],
260260
});
261261
expect(plan.commands.find((command) => command.name === "rust tests")).toMatchObject({
262262
bin: "cargo",

0 commit comments

Comments
 (0)