diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a971fbac..899b21429 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Requires `a3s-sandbox` 0.2.1, whose `NativeSandbox` is shared across + threads and applies network grants through `&self`. - Workspace lexical FTS links published `a3s-vec` 0.1.8 (`a3s_vec_fts_v1`). On-disk `zvec_rust_fts_v1` generations stay incompatible and are rebuilt. - The fact log is the only coding control source. `send`, `stream`, attachment @@ -53,7 +55,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Meta Harness arbitrary assemble (`META-HARNESS2`).** Ordered `components: [...]` accepts stock parts and `host:` mounts via `HostHarnessRegistry` / `HostHarnessAssembler`. Node/Python expose - `Harness.host` + `components`. Builtin `intent_stamp` host component supports + `Harness.host` + `components`; Go exposes `SessionOptions.Harness` + (`HarnessOptions`, `HarnessHost`, stock part constants). SDK sessions that set + `harness` install `BuiltinHostHarnessRegistry`, so `host:` mounts resolve + from Node, Python, and Go instead of failing closed for lack of a registry. Builtin `intent_stamp` host component supports hermetic and Layer C proofs. F31 kernels (`meta_harness.rs`, `completion_attestor.rs`) join the ≥95% F-table gate. Live suite: `test_meta_harness_compose_live_e2e` (file/digest/gate oracles only). diff --git a/Cargo.lock b/Cargo.lock index 5664b7c8c..80a512a60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,6 +111,7 @@ name = "a3s-code-go-bridge" version = "9.0.0" dependencies = [ "a3s-code-core", + "a3s-effect", "anyhow", "async-trait", "base64 0.22.1", @@ -246,7 +247,8 @@ dependencies = [ [[package]] name = "a3s-sandbox" -version = "0.2.0" +version = "0.2.1" +source = "git+https://github.com/A3S-Lab/Sandbox.git?rev=2b8687892bbb4a5e3d6e74fca42949a3f186d236#2b8687892bbb4a5e3d6e74fca42949a3f186d236" dependencies = [ "anyhow", "async-trait", @@ -502,7 +504,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -513,7 +515,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2301,7 +2303,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2487,7 +2489,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3571,7 +3573,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.5", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -4519,7 +4521,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5317,7 +5319,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls 0.23.44", - "socket2 0.6.5", + "socket2 0.5.10", "thiserror 2.0.20", "tokio", "tracing", @@ -5355,9 +5357,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.5", + "socket2 0.5.10", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5906,7 +5908,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6424,7 +6426,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6664,10 +6666,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7635,7 +7637,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -8232,3 +8234,7 @@ dependencies = [ [[patch.unused]] name = "a3s-apofasi" version = "0.1.2" + +[[patch.unused]] +name = "a3s-sandbox" +version = "0.2.0" diff --git a/core/Cargo.toml b/core/Cargo.toml index 0a55729c7..139d98d89 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -66,7 +66,7 @@ a3s-search = { version = "=3.1.4", git = "https://github.com/A3S-Lab/Search.git" # Flow engine is optional: thin coding builds do not project named workflows. # Enabled by `dynamic-workflow` (and therefore `advanced-harness`). a3s-flow = { version = "=1.1.0", git = "https://github.com/A3S-Lab/Flow.git", rev = "2948ad51a1395177764766c3ddf7e44338f9e374", default-features = false, optional = true } -a3s-sandbox = { version = "=0.2.0", git = "https://github.com/A3S-Lab/Sandbox.git", rev = "d381943061b157fb0e63973372d138cb6f78ffde" } +a3s-sandbox = { version = "=0.2.1", git = "https://github.com/A3S-Lab/Sandbox.git", rev = "2b8687892bbb4a5e3d6e74fca42949a3f186d236" } # Typed System-1 decisions (Apofasi). Optional so thin coding builds stay # ML-free; lexical/router path only unless `apofasi-infer` is enabled. a3s-apofasi = { version = "=0.1.1", git = "https://github.com/A3S-Lab/Apofasi.git", rev = "0bc3961d3183f9d2692c0406967765866bc914d6", default-features = false, features = ["router"], optional = true } diff --git a/manual/META_HARNESS.md b/manual/META_HARNESS.md index c714526c4..f17652ba6 100644 --- a/manual/META_HARNESS.md +++ b/manual/META_HARNESS.md @@ -123,9 +123,27 @@ opts.harness = Harness.compose( ) ``` -Unknown part names and unknown `host:` values fail closed. `host:*` mounts -require `SessionOptions::with_host_harness_registry` (or the builtin registry in -tests). +```go +// Go +budget := uint32(4) +opts := &code.SessionOptions{ + Harness: &code.HarnessOptions{ + Components: []string{ + code.HarnessSystem, code.HarnessTools, code.HarnessHost("intent_stamp"), + code.HarnessBudget, code.HarnessInfer, + }, + ToolBudget: &budget, + System: []string{"You are a careful coding agent."}, + }, +} +``` + +Unknown part names fail closed when the session is created; unknown `host:` +values fail closed on the first run. Rust embedders supply +`SessionOptions::with_host_harness_registry`. Node, Python, and Go sessions that +set `harness` install `BuiltinHostHarnessRegistry`, so SDK `host:` mounts +resolve against Core's builtin components (currently `intent_stamp`); custom +host components need a Rust embedder. ## Verification diff --git a/scripts/sdk_api_alignment_check.mjs b/scripts/sdk_api_alignment_check.mjs index de2fe951c..d94953d34 100755 --- a/scripts/sdk_api_alignment_check.mjs +++ b/scripts/sdk_api_alignment_check.mjs @@ -466,6 +466,37 @@ function goStructFields(source, name) { ]; } +function goJsonTags(source, name) { + const match = source.match( + new RegExp(`^type\\s+${name}\\s+struct\\s*\\{([\\s\\S]*?)^\\}`, 'm'), + ); + assert.ok(match, `could not find Go struct ${name}`); + // `json:"-"` fields travel over the callback transport; key them by the + // snake_case field name so they still count as present. + return [...match[1].matchAll(/^\s*([A-Z][A-Za-z0-9]*)\s+[^`\n]*`json:"([a-z0-9_-]+)/gm)].map( + ([, field, tag]) => + tag === '-' ? field.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase() : tag, + ); +} + +// Core/Node option name -> Go JSON key (`top` or `top.nested`) where Go groups +// related options into one typed struct instead of flat fields. +const GO_SESSION_OPTION_ALIASES = new Map([ + ['role', 'prompt_slots.role'], + ['guidelines', 'prompt_slots.guidelines'], + ['response_style', 'prompt_slots.response_style'], + ['output_language', 'prompt_slots.output_language'], + ['extra', 'prompt_slots.extra'], + ['trajectory_path', 'trajectory.path'], + ['trajectory_mode', 'trajectory.mode'], + ['trajectory_max_text_bytes', 'trajectory.max_text_bytes'], + ['trajectory_include_messages', 'trajectory.include_messages'], + ['memory_store', 'file_memory_dir'], + ['session_store', 'file_session_store_dir'], + ['auto_parallel', 'auto_parallel_delegation'], + ['planning', 'planning_mode'], +]); + function expected(coreMethods, omissions, aliases) { return [ ...new Set( @@ -706,42 +737,43 @@ assertContainsAll('Go Session', goSession, [ 'OutcomeLedgerSnapshot', ]); assert.ok(!goSession.includes('ParallelTask'), 'Go Session must not expose ParallelTask (HARNESS-CONV4)'); -assertContainsAll('Go SessionOptions', goSessionOptions, [ - 'Model', - 'AgentDirs', - 'SkillDirs', - 'SearchConfig', - 'FileMemoryDir', - 'FileSessionStoreDir', - 'SecurityProvider', - 'SessionID', - 'TenantID', - 'Principal', - 'AgentTemplateID', - 'CorrelationID', - 'PlanningMode', - 'GoalTracking', - 'AutoSave', - 'ToolTimeoutMS', - 'LLMAPITimeoutMS', - 'AutoCompact', - 'MaxContextTokens', - 'Temperature', - 'ThinkingBudget', - 'MaxToolRounds', - 'MaxParallelTasks', - 'PromptSlots', - 'ImmutableContentAdapter', - 'CommandEnv', - 'CompletionWaivers', - 'EffectIsolation', - 'ExternalObservations', - 'OutcomeLedger', - 'PathRules', - 'PlanRun', - 'ReadOnlySession', - 'VerifierEnabled', +// Go must cover the same Core-derived surface as Node/Python. Every required +// SessionOptions name resolves to a Go JSON key, either top-level or inside the +// nested struct Go groups it under; a new Core field fails here until Go maps it. +const goSessionOptionKeys = new Set(goJsonTags(go, 'SessionOptions')); +const goNestedKeys = { + prompt_slots: new Set(goJsonTags(go, 'PromptSlots')), + trajectory: new Set(goJsonTags(go, 'TrajectoryConfig')), +}; +const missingGoSessionOptions = requiredSessionOptions.filter((name) => { + const alias = GO_SESSION_OPTION_ALIASES.get(name) ?? name; + const [top, nested] = alias.split('.'); + if (!goSessionOptionKeys.has(top)) return true; + return nested !== undefined && !goNestedKeys[top]?.has(nested); +}); +assert.deepEqual( + missingGoSessionOptions, + [], + `Go SessionOptions is missing Core options: ${missingGoSessionOptions.join(', ')}`, +); +const normalizeGo = (name) => name.replace(/_/g, '').toLowerCase(); +// Go idioms: the constructor is a package function, and context-taking calls +// block, so `*_async` variants collapse into the synchronous Go method. +const GO_METHOD_ALIASES = new Map([ + ['create', 'NewAgent'], + ['replace_session_async', 'ReplaceSession'], ]); +const goConstructors = [...go.matchAll(/^func\s+(New[A-Z][A-Za-z0-9]*)\s*\(/gm)].map((m) => m[1]); +for (const [label, goNames, required] of [ + ['Go Agent', [...goAgent, ...goConstructors], requiredAgent], + ['Go Session', goSession, requiredSession], +]) { + const present = new Set(goNames.map(normalizeGo)); + const missing = required.filter( + (name) => !present.has(normalizeGo(GO_METHOD_ALIASES.get(name) ?? name)), + ); + assert.deepEqual(missing, [], `${label} is missing Core methods: ${missing.join(', ')}`); +} assertContainsAll('Go StateGraphRuntime', goMethods(go, 'StateGraphRuntime'), [ 'BranchID', 'Version', diff --git a/sdk/go/README.md b/sdk/go/README.md index 82abce680..4e3b525dd 100644 --- a/sdk/go/README.md +++ b/sdk/go/README.md @@ -364,6 +364,39 @@ profile drift, and child runs inherit the parent profile without broadening it. provider through a typed object. The older `DefaultSecurity` boolean remains available for wire compatibility but is deprecated; do not set both options. +## Meta Harness composition + +Compose the harness from ordered components on the one fact log, the same +recipe Node's and Python's `Harness.compose` send: + +```go +budget := uint32(4) +session, err := agent.Session(ctx, ".", &code.SessionOptions{ + Harness: &code.HarnessOptions{ + Components: []string{ + code.HarnessSystem, + code.HarnessTools, + code.HarnessHost("intent_stamp"), + code.HarnessBudget, + code.HarnessInfer, + }, + ToolBudget: &budget, + System: []string{"You are a careful coding agent."}, + }, +}) +if err != nil { + return err +} +defer session.Close(context.Background()) +``` + +Leave `Harness` nil to keep the default `coding_actor` tree. `HarnessHost` +mounts one of Core's builtin host components (currently `intent_stamp`); +custom host components need a Rust embedder with its own `HostHarnessRegistry`. +Unknown stock parts are rejected when the session is created, and an +unregistered host id fails closed on the first run. Permission projection and +the completion gate stay Core-owned and cannot be disabled here. + ## Streaming Every event uses the shared, lossless `EventEnvelopeV1` shape. `Event.Type` is diff --git a/sdk/go/agent_test.go b/sdk/go/agent_test.go index 00e963fe2..a2042b413 100644 --- a/sdk/go/agent_test.go +++ b/sdk/go/agent_test.go @@ -228,6 +228,30 @@ func TestToolPresentationProfileUsesTypedSessionOption(t *testing.T) { } } +func TestHarnessOptionsMatchBridgeWire(t *testing.T) { + budget := uint32(4) + encoded, err := json.Marshal(SessionOptions{ + Harness: &HarnessOptions{ + Components: []string{ + HarnessSystem, HarnessTools, HarnessHost("intent_stamp"), HarnessBudget, HarnessInfer, + }, + ToolBudget: &budget, + System: []string{"careful coding agent"}, + }, + }) + if err != nil { + t.Fatal(err) + } + want := `{"harness":{"components":["system","tools","host:intent_stamp","budget","infer"],"tool_budget":4,"system":["careful coding agent"]}}` + if string(encoded) != want { + t.Fatalf("harness JSON = %s, want %s", encoded, want) + } + omitted, err := json.Marshal(SessionOptions{}) + if err != nil || string(omitted) != `{}` { + t.Fatalf("omitted harness must keep the default tree, got %s, %v", omitted, err) + } +} + func TestCreateSessionAndCloseWithInjectedRuntime(t *testing.T) { runtime := &fakeRuntime{ request: func( diff --git a/sdk/go/bridge/Cargo.toml b/sdk/go/bridge/Cargo.toml index 113e4e77c..424a2a636 100644 --- a/sdk/go/bridge/Cargo.toml +++ b/sdk/go/bridge/Cargo.toml @@ -34,6 +34,7 @@ tokio = { version = "1.35", features = [ tokio-util = "0.7" [dev-dependencies] +a3s-effect = "0.1.0" tempfile = "3.10" [features] diff --git a/sdk/go/bridge/src/lib.rs b/sdk/go/bridge/src/lib.rs index ba4868622..992a3d5e1 100644 --- a/sdk/go/bridge/src/lib.rs +++ b/sdk/go/bridge/src/lib.rs @@ -3255,6 +3255,35 @@ struct BridgeSessionOptions { read_only_session: Option, allow_process_host_sandbox: Option, verifier_enabled: Option, + harness: Option, +} + +/// Meta Harness compose recipe; mirrors Node/Python `HarnessComposeOptions`. +#[derive(Debug, Default, Deserialize)] +#[serde(default, deny_unknown_fields)] +struct BridgeHarnessCompose { + tool_budget: Option, + compact_after_chars: Option, + system: Vec, + parts: Vec, + components: Vec, +} + +impl BridgeHarnessCompose { + fn into_core(self) -> Result { + let list = if self.components.is_empty() { + self.parts + } else { + self.components + }; + a3s_code_core::HarnessComposeOptions::compose( + list, + self.tool_budget, + self.compact_after_chars, + self.system, + ) + .map_err(|error| BridgeFailure::new("INVALID_REQUEST", format!("harness: {error}"))) + } } #[derive(Debug, Default, Deserialize)] @@ -3503,6 +3532,15 @@ impl BridgeSessionOptions { if let Some(enabled) = self.verifier_enabled { options = options.with_verifier(enabled); } + if let Some(harness) = self.harness { + // SDK hosts cannot inject a Rust registry; `host:` resolves + // against Core's builtin components. + options = options + .with_harness(harness.into_core()?) + .with_host_harness_registry(std::sync::Arc::new( + a3s_code_core::BuiltinHostHarnessRegistry, + )); + } if let Some(value) = self.max_parse_retries { options = options.with_parse_retries(value); } @@ -4342,6 +4380,65 @@ mod tests { assert_eq!(slots.guidelines.as_deref(), Some("be precise")); } + #[test] + fn harness_components_map_to_core_compose() { + let bridge: BridgeSessionOptions = serde_json::from_value(json!({ + "harness": { + "components": ["system", "tools", "host:intent_stamp", "budget", "infer"], + "tool_budget": 4, + "system": ["careful coding agent"] + } + })) + .unwrap(); + let options = bridge.into_core(None).unwrap(); + let harness = options.harness.as_ref().expect("harness"); + assert_eq!( + harness.components, + vec!["system", "tools", "host:intent_stamp", "budget", "infer"] + ); + assert!( + harness.parts.is_empty(), + "host mounts use the components path" + ); + assert_eq!(harness.tool_budget, Some(4)); + assert_eq!(harness.system, vec!["careful coding agent".to_string()]); + + // The SDK installs the builtin registry, so the host mount admits. + let registry = options + .host_harness_registry + .as_deref() + .expect("SDK sessions resolve host mounts through the builtin registry"); + let config = a3s_effect::HarnessConfig::new(2, 100, 8, 1, vec![], vec![]).expect("config"); + let (_, policy) = + a3s_code_core::admit_from_compose_with_registry(Some(harness), Some(registry), config) + .expect("host:intent_stamp admits through the SDK registry"); + assert!(policy.permission_overlay && policy.completion_gate); + } + + #[test] + fn harness_stock_parts_and_unknown_components() { + let stock: BridgeSessionOptions = serde_json::from_value(json!({ + "harness": {"parts": ["system", "tools", "infer"]} + })) + .unwrap(); + let harness = stock.into_core(None).unwrap().harness.expect("harness"); + assert_eq!(harness.parts.len(), 3); + + let unknown: BridgeSessionOptions = serde_json::from_value(json!({ + "harness": {"components": ["system", "not-a-part"]} + })) + .unwrap(); + let error = unknown + .into_core(None) + .expect_err("unknown part fails closed"); + assert_eq!(error.code, "INVALID_REQUEST"); + + let typo = serde_json::from_value::(json!({ + "harness": {"component": ["system"]} + })); + assert!(typo.is_err(), "unknown harness keys are rejected"); + } + #[test] fn typed_security_provider_is_closed_and_legacy_flag_remains_compatible() { let typed: BridgeSessionOptions = serde_json::from_value(json!({ diff --git a/sdk/go/models.go b/sdk/go/models.go index fee63aaf5..ad65d8213 100644 --- a/sdk/go/models.go +++ b/sdk/go/models.go @@ -313,6 +313,37 @@ type SessionOptions struct { ReadOnlySession *bool `json:"read_only_session,omitempty"` AllowProcessHostSandbox *bool `json:"allow_process_host_sandbox,omitempty"` VerifierEnabled *bool `json:"verifier_enabled,omitempty"` + // Harness composes the Meta Harness. Nil keeps the default coding_actor tree. + Harness *HarnessOptions `json:"harness,omitempty"` +} + +// Stock Meta Harness parts for HarnessOptions.Components. +const ( + HarnessSystem = "system" + HarnessTools = "tools" + HarnessBudget = "budget" + HarnessCompact = "compact" + HarnessInfer = "infer" +) + +// HarnessHost returns the `host:` mount for a component registered in the +// Rust HostHarnessRegistry. Unknown ids fail when the session is created. +func HarnessHost(id string) string { + return "host:" + id +} + +// HarnessOptions is the Meta Harness compose recipe, matching Node/Python +// Harness.compose. Permission projection and the completion gate stay +// Core-owned and cannot be disabled from here. +type HarnessOptions struct { + // Components is the ordered assemble list of stock parts and HarnessHost + // mounts. When non-empty it takes precedence over Parts. + Components []string `json:"components,omitempty"` + // Parts is the legacy stock-only order. + Parts []string `json:"parts,omitempty"` + ToolBudget *uint32 `json:"tool_budget,omitempty"` + CompactAfterChars *uint `json:"compact_after_chars,omitempty"` + System []string `json:"system,omitempty"` } type CompletionWaiver struct { diff --git a/sdk/node/Cargo.lock b/sdk/node/Cargo.lock index a97a50ab4..6a2fbc790 100644 --- a/sdk/node/Cargo.lock +++ b/sdk/node/Cargo.lock @@ -189,8 +189,8 @@ dependencies = [ [[package]] name = "a3s-sandbox" -version = "0.2.0" -source = "git+https://github.com/A3S-Lab/Sandbox.git?rev=d381943061b157fb0e63973372d138cb6f78ffde#d381943061b157fb0e63973372d138cb6f78ffde" +version = "0.2.1" +source = "git+https://github.com/A3S-Lab/Sandbox.git?rev=2b8687892bbb4a5e3d6e74fca42949a3f186d236#2b8687892bbb4a5e3d6e74fca42949a3f186d236" dependencies = [ "anyhow", "async-trait", @@ -448,7 +448,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -459,7 +459,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1910,7 +1910,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3786,7 +3786,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4174,7 +4174,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4625,7 +4625,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4773,10 +4773,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.5", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5497,7 +5497,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/sdk/node/generated.d.ts b/sdk/node/generated.d.ts index 27fe6ddaa..5bd0922de 100644 --- a/sdk/node/generated.d.ts +++ b/sdk/node/generated.d.ts @@ -783,7 +783,7 @@ export interface SessionOptions { * ```js * agent.session('.', { * harness: Harness.compose({ - * parts: [Harness.system, Harness.tools, Harness.budget, Harness.compact, Harness.infer], + * components: [Harness.system(), Harness.tools(), Harness.host('intent_stamp'), Harness.infer()], * toolBudget: 4, * }), * }); @@ -1563,7 +1563,7 @@ export declare class Harness { static compact(): string /** Stock `infer` / scheduler part id. */ static infer(): string - /** Host Moore mount id (`host:`). Requires a Rust `HostHarnessRegistry`. */ + /** Host mount id (`host:`), resolved against Core's builtin host components. */ static host(id: string): string /** Validate and return a compose recipe for `SessionOptions.harness`. */ static compose(options: HarnessComposeOptions): HarnessComposeOptions diff --git a/sdk/node/package.json b/sdk/node/package.json index 8a6848849..5abb70779 100644 --- a/sdk/node/package.json +++ b/sdk/node/package.json @@ -49,7 +49,7 @@ "test:release-manifest": "node --test scripts/release-manifest.test.mjs scripts/patch-loader.test.mjs", "test:evaluation-protocol": "node ../../scripts/generate_evaluation_protocol_artifacts.mjs --check && node ../../scripts/check_evaluation_protocol_artifacts.mjs", "test:research-protocol": "node ../../scripts/generate_research_protocol_artifacts.mjs --check && node ../../scripts/check_research_protocol_artifacts.mjs", - "test:runtime": "node test.mjs && node test_budget_guard.mjs && node test_callback_safety.mjs && node test_confirmation_inheritance.mjs && node test_session_close.mjs && node test_model_generation_pool_health_fixture.mjs && node test_model_middleware_health_fixture.mjs && node test_sdk_capability_batch_fixture.mjs && node test_sdk_checkpoint_export_fixture.mjs && node test_sdk_immutable_content_fixture.mjs && node test_workspace_retrieval.mjs && node test-helpers.mjs", + "test:runtime": "node test.mjs && node test_budget_guard.mjs && node test_callback_safety.mjs && node test_confirmation_inheritance.mjs && node test_session_close.mjs && node test_meta_harness.mjs && node test_model_generation_pool_health_fixture.mjs && node test_model_middleware_health_fixture.mjs && node test_sdk_capability_batch_fixture.mjs && node test_sdk_checkpoint_export_fixture.mjs && node test_sdk_immutable_content_fixture.mjs && node test_workspace_retrieval.mjs && node test-helpers.mjs", "test:fixtures": "node test_workspace_retrieval_real_deepseek.mjs --validate-fixture", "test:types": "tsc --noEmit --module nodenext --moduleResolution nodenext --target es2022 --strict --skipLibCheck test-types.ts", "test:helpers": "node test-helpers.mjs" diff --git a/sdk/node/src/session_options.rs b/sdk/node/src/session_options.rs index b47778738..fc973c956 100644 --- a/sdk/node/src/session_options.rs +++ b/sdk/node/src/session_options.rs @@ -189,7 +189,7 @@ impl Harness { "infer".into() } - /// Host Moore mount id (`host:`). Requires a Rust `HostHarnessRegistry`. + /// Host mount id (`host:`), resolved against Core's builtin host components. #[napi] pub fn host(id: String) -> napi::Result { let formatted = a3s_code_core::host_component_id(&id); @@ -482,7 +482,7 @@ pub struct SessionOptions { /// ```js /// agent.session('.', { /// harness: Harness.compose({ - /// parts: [Harness.system, Harness.tools, Harness.budget, Harness.compact, Harness.infer], + /// components: [Harness.system(), Harness.tools(), Harness.host('intent_stamp'), Harness.infer()], /// toolBudget: 4, /// }), /// }); @@ -1355,7 +1355,13 @@ fn apply_host_contract_options( opts = opts.with_verifier(enabled); } if let Some(harness) = &options.harness { - opts = opts.with_harness(js_harness_to_core(harness)?); + // SDK hosts cannot inject a Rust registry; `host:` resolves against + // Core's builtin components. + opts = opts + .with_harness(js_harness_to_core(harness)?) + .with_host_harness_registry(std::sync::Arc::new( + a3s_code_core::BuiltinHostHarnessRegistry, + )); } Ok(opts) } diff --git a/sdk/node/src/tests.rs b/sdk/node/src/tests.rs index fef981aa3..aab2cd97c 100644 --- a/sdk/node/src/tests.rs +++ b/sdk/node/src/tests.rs @@ -653,6 +653,27 @@ fn harness_compose_maps_to_rust_session_options() { assert!(invalid.is_err()); } +#[test] +fn harness_host_mount_installs_builtin_registry() { + let opts = js_session_options_to_rust(Some(SessionOptions { + harness: Some(HarnessComposeOptions { + components: Some(vec![ + "system".into(), + "host:intent_stamp".into(), + "infer".into(), + ]), + ..Default::default() + }), + ..Default::default() + })) + .unwrap(); + assert!(opts.harness.is_some()); + assert!( + opts.host_harness_registry.is_some(), + "SDK sessions resolve host mounts through Core's builtin registry" + ); +} + #[test] fn artifact_store_limits_maps_to_rust_session_options() { let opts = js_session_options_to_rust(Some(SessionOptions { diff --git a/sdk/node/test_meta_harness.mjs b/sdk/node/test_meta_harness.mjs new file mode 100644 index 000000000..8f132e531 --- /dev/null +++ b/sdk/node/test_meta_harness.mjs @@ -0,0 +1,42 @@ +// Meta Harness composition through the Node SDK (no provider credentials needed). +import assert from 'node:assert/strict' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import mod from './index.js' + +const { Agent, Harness, LocalWorkspaceBackend } = mod +const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'a3s-node-harness-')) + +try { + const recipe = Harness.compose({ + components: [Harness.system(), Harness.tools(), Harness.host('intent_stamp'), Harness.budget(), Harness.infer()], + toolBudget: 4, + system: ['careful coding agent'], + }) + assert.deepEqual(recipe.components, ['system', 'tools', 'host:intent_stamp', 'budget', 'infer']) + assert.equal(recipe.toolBudget, 4) + + assert.throws(() => Harness.compose({ components: ['system', 'not-a-part'] }), /not-a-part|unknown/i) + + const agent = await Agent.create(` +default_model = "anthropic/claude-sonnet-4-20250514" + +providers "anthropic" { + api_key = "test-key" + models "claude-sonnet-4-20250514" { + name = "Claude Sonnet 4" + } +} +`.trim()) + const session = agent.session(workspace, { + harness: recipe, + permissionPolicy: { defaultDecision: 'allow' }, + workspaceBackend: new LocalWorkspaceBackend(workspace), + }) + assert.equal(session.isClosed(), false) + session.close() + console.log('meta harness ok') +} finally { + fs.rmSync(workspace, { recursive: true, force: true }) +} diff --git a/sdk/python/Cargo.lock b/sdk/python/Cargo.lock index 5218e8418..af8adbc37 100644 --- a/sdk/python/Cargo.lock +++ b/sdk/python/Cargo.lock @@ -188,8 +188,8 @@ dependencies = [ [[package]] name = "a3s-sandbox" -version = "0.2.0" -source = "git+https://github.com/A3S-Lab/Sandbox.git?rev=d381943061b157fb0e63973372d138cb6f78ffde#d381943061b157fb0e63973372d138cb6f78ffde" +version = "0.2.1" +source = "git+https://github.com/A3S-Lab/Sandbox.git?rev=2b8687892bbb4a5e3d6e74fca42949a3f186d236#2b8687892bbb4a5e3d6e74fca42949a3f186d236" dependencies = [ "anyhow", "async-trait", @@ -447,7 +447,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -458,7 +458,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1884,7 +1884,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3787,7 +3787,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4175,7 +4175,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4626,7 +4626,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4780,10 +4780,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.5", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5510,7 +5510,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/sdk/python/src/session_config.rs b/sdk/python/src/session_config.rs index dbded3a03..a26d0140d 100644 --- a/sdk/python/src/session_config.rs +++ b/sdk/python/src/session_config.rs @@ -168,7 +168,7 @@ impl PyHarness { "infer".into() } - /// Host Moore mount id (``host:``). Requires a Rust ``HostHarnessRegistry``. + /// Host mount id (``host:``), resolved against Core's builtin host components. #[staticmethod] pub(super) fn host(id: &str) -> PyResult { let formatted = a3s_code_core::host_component_id(id); diff --git a/sdk/python/src/session_options_conversion.rs b/sdk/python/src/session_options_conversion.rs index 83e877881..d8c34714f 100644 --- a/sdk/python/src/session_options_conversion.rs +++ b/sdk/python/src/session_options_conversion.rs @@ -486,7 +486,13 @@ fn apply_py_host_contract_options( options = options.with_verifier(enabled); } if let Some(harness) = &source.harness { - options = options.with_harness(harness.to_core()?); + // SDK hosts cannot inject a Rust registry; `host:` resolves against + // Core's builtin components. + options = options + .with_harness(harness.to_core()?) + .with_host_harness_registry(std::sync::Arc::new( + a3s_code_core::BuiltinHostHarnessRegistry, + )); } Ok(options) } diff --git a/sdk/python/src/tests.rs b/sdk/python/src/tests.rs index 2b1416c20..c429fc14f 100644 --- a/sdk/python/src/tests.rs +++ b/sdk/python/src/tests.rs @@ -840,11 +840,7 @@ fn session_options_map_harness_compose() { assert!(host_core.parts.is_empty()); assert_eq!( host_core.components, - vec![ - "system".into(), - "host:intent_stamp".into(), - "infer".into(), - ] + vec!["system".into(), "host:intent_stamp".into(), "infer".into(),] ); let invalid = PyHarnessComposeOptions { @@ -854,6 +850,24 @@ fn session_options_map_harness_compose() { assert!(invalid.to_core().is_err()); } +#[test] +fn session_options_host_mount_installs_builtin_registry() { + let mut session_options = PySessionOptions::new(); + session_options.harness = Some(PyHarnessComposeOptions { + tool_budget: None, + compact_after_chars: None, + system: Vec::new(), + parts: Vec::new(), + components: vec!["system".into(), "host:intent_stamp".into(), "infer".into()], + }); + let opts = build_rust_session_options(session_options).unwrap(); + assert!(opts.harness.is_some()); + assert!( + opts.host_harness_registry.is_some(), + "SDK sessions resolve host mounts through Core's builtin registry" + ); +} + #[test] fn session_options_reject_zero_model_context_window() { pyo3::prepare_freethreaded_python(); diff --git a/sdk/python/tests/test_session_options_harness.py b/sdk/python/tests/test_session_options_harness.py new file mode 100644 index 000000000..003100d41 --- /dev/null +++ b/sdk/python/tests/test_session_options_harness.py @@ -0,0 +1,49 @@ +"""Meta Harness composition through the Python SDK (no provider credentials needed).""" + +import pytest + +from a3s_code import Agent, Harness, SessionOptions + +INLINE_CONFIG = """ +default_model = "anthropic/claude-sonnet-4-20250514" + +providers "anthropic" { + api_key = "test-key" + models "claude-sonnet-4-20250514" { + name = "Claude Sonnet 4" + } +} +""".strip() + + +def test_compose_accepts_stock_and_host_components(): + recipe = Harness.compose( + components=[ + Harness.system(), + Harness.tools(), + Harness.host("intent_stamp"), + Harness.budget(), + Harness.infer(), + ], + tool_budget=4, + ) + assert recipe.components == ["system", "tools", "host:intent_stamp", "budget", "infer"] + assert recipe.tool_budget == 4 + + +def test_compose_rejects_unknown_part(): + with pytest.raises(ValueError): + Harness.compose(components=["system", "not-a-part"]) + + +def test_session_accepts_host_mount_recipe(tmp_path): + agent = Agent.create(INLINE_CONFIG) + options = SessionOptions() + options.harness = Harness.compose( + components=[Harness.system(), Harness.host("intent_stamp"), Harness.infer()] + ) + session = agent.session(str(tmp_path), options) + try: + assert options.harness.components == ["system", "host:intent_stamp", "infer"] + finally: + session.close()