Skip to main content

Module configfile

Module configfile 

Source
Expand description

§3 “The Single Config File” (docs/composable-harness/COMPOSABLE-HARNESS-DESIGN.md) — P1 of the composable-harness migration (design §5.2, phase P1).

HarnessConfig is the one schema described in §3.1: schema_version + extends + [core] (+ its subtables) + [capabilities.*] + [experimental]. It is a plain serde struct with no format-specific logic, so it parses identically from TOML (HarnessConfig::from_toml_str, the CLI’s format) or JSON (HarnessConfig::from_json_str, the SDK mirror §3.0 describes as superseding the old 9-field ConfigProfile).

P1 scope (design §5.2’s exact wording): the config surface, not the module runtime. Fields with no Config runtime home yet are captured typed-but-unconsumed with a P3/P4: doc-comment rather than inventing behavior ahead of the phase that consumes them. extends is parsed but not resolved — preset resolution (§3.5, the preset table itself in §4) is P2. [capabilities.*] module settings are likewise parsed but not consumed — module runtime wiring is P3 (design’s explicit framing: “consumed later phases”).

Naming note (P1 judgment call). crates/harness/src/config.rs already defines a small ConfigFile { profiles: HashMap<String, ConfigProfile> } — a named-profile table (the SDK’s --profile/from_profile_file mechanism). That shape is not what §3.1 describes (one resolved harness, not a table of named alternatives), so this module introduces the new type under a distinct name, HarnessConfig, rather than repurposing or renaming the existing ConfigFile. This is the least-breaking path: zero changes to Config::from_profile_file or its existing test (crates/harness/tests/agent_loop.rs:640-652).

[core.model] schema conflict (P1 judgment call). §3.1 literally shows a scalar core.model (the model id string, line 582) and a table [core.model] a few lines later (allow_switch, line 611-612). Those are not simultaneously representable in one TOML document — a table cannot redefine a key already set as a string in the same parent table (verified empirically: both tomllib and the toml crate reject it as “cannot overwrite a value”). Rather than silently working around a spec bug, this is exposed under a distinct table name, [core.model_switch] (CoreModelSwitchConfig), until the design doc is corrected upstream.

Structs§

BashToolConfig
[core.tools.bash] — maps onto a real crate::config::ToolOverride (enabled/description/schema_tier/timeout_secs) via HarnessConfig::to_config_profile (P4e closes the timeout_secs gap S14 flagged — BashTool’s timeout is consumed via tools::ToolContext::bash_timeout_secs, threaded from agent::build_tool_context, not the ToolOverride struct directly, since Tool::execute only sees a ToolContext, not the resolved Config/ToolOverride map — see that field’s doc comment).
CapabilityConfig
[capabilities.<name>] (§2 modules). Every module table carries enabled plus module-specific settings. P1 captures the settings as an untyped catch-all: the modules themselves are P3+ (“consumed later phases” per design §5.2’s P1 description) — this struct is the config surface for them, not their runtime.
CoreCompactionConfig
[core.compaction] (obligation 5). after_messages maps to the real Config.compact_after_messages, resolved in P1; the rest are P4 NEW pressure-trigger fields.
CoreModelSwitchConfig
[core.model_switch] (design’s [core.model]; see the naming-conflict doc comment above).
CoreOutputConfig
[core.output] (obligation 9). P3/P4: Config.event_sink is code-only (“Callbacks/handlers are code-only”, config.rs); this is its declarative equivalent, not yet wired to anything.
CoreRetryConfig
[core.retry]. P4: consumed by a request-retry loop that doesn’t exist as a Config field yet (obligation 1; pi§3 naming).
CoreSection
[core] (§3.1 lines 581-609 + the named subtables that follow).
CoreSessionConfig
[core.session] (obligation 6). P3/P4: entirely NEW — no Config field represents a session store location/policy today.
CoreSkillsConfig
[core.skills] (obligation 4, D-7). P3/P4: a NEW subsystem extending Config.prompts; not yet consumed.
CoreSteeringConfig
[core.steering] (obligation 7; pi§3 semantics). P4: NEW, no Config field yet.
CoreToolsConfig
[core.tools] — registry shaping (§3.1 line 619; replaces with_builtins() hardcoding, tools/mod.rs:179-192, in P3).
EditFileToolConfig
[core.tools.edit_file]. P3/P4: require_read_before_edit/ notebook_aware have no ToolOverride home yet (S6/S12 catalog rows 32, 40 — ToolContext state). schema_tier DOES resolve (P2 addition, mirroring [core.tools.bash].schema_tier’s existing P1 handling in HarnessConfig::to_config_profile) — needed for token-saver’s own C9 resolution (§2.2: “per-tool Full override survives a global minimal”, design §4.5) to actually materialize into the resolved Config rather than silently parsing-and-dropping the one field the preset relies on.
HarnessConfig
The top-level schema (§3.1): one TOML/JSON document that fully determines the harness’s shape (§3.0: “Everything the harness does is a function of the resolved file”).
ReadFileToolConfig
[core.tools.read_file]. P3/P4: multimodal has no ToolOverride home yet (catalog §4a small).
ResolveOptions
Options controlling resolve’s step 5 validation strictness.
Resolved
§3.5’s resolver output: one materialized Config (step 7), the folded HarnessConfig it came from (defaults < preset layer < user file < sanitized project file, step 4), every named module’s activation state (step 7’s “module-activation set”), the resolved preset chain (root-first, informational), and any non-fatal warnings collected along the way (lenient-mode unknown keys, D-7/D-9 fallbacks, C1/C3/C4/C6, sanitizer/clamp notices from a project layer).

Enums§

HarnessConfigError
F7 fix: schema_version previously parsed any u32 silently — a future (or simply typo’d) version number would be interpreted under TODAY’s field meanings with no warning at all, exactly the kind of silent misinterpretation §3.5 step 5’s “fail SAFE” precedent exists to prevent elsewhere in this migration. Only 1 is understood in P1.
ResolveError
Everything that can fail §3.5 resolution.

Constants§

MODULE_NAMES
The 31 top-level [capabilities.<name>] table names (§2’s 35 modules, minus the 4 that nest as sub-tables of a family: permissions.rules, permissions.sandbox, permissions.protected_paths nest under permissions; mcp.server is the capabilities.mcp.serve bool, not a separate top-level table).
NESTED_MODULE_NAMES
The 3 nested sub-modules under capabilities.permissions (module family 10-13, §2 table) — dotted paths module_enabled understands.

Functions§

cache_plan_str
P4d: read capabilities.cache.plan — same extraction rationale as deferred_tools_core.
clamp_project_permissions
§3.3’s monotonic clamp, applied specifically at the project-layer merge (not the general HarnessConfig::overlay, which is also used for the preset chain and the user layer — a user’s OWN config extending a preset and then setting a looser value is fine; only the UNTRUSTED project layer is clamped). Mirrors userconfig.rs’s clamp_sandbox/clamp_approval (F2/F3 fix precedent): even a project value that survived sanitize_for_project (because it isn’t the single GLOBAL loosest value) must still be no looser than the base layer’s OWN effective posture — e.g. a project setting workspace_write when the base layer has read_only is a real widening and must be clamped back.
deferred_tools_core
P4d (design §5.2 P1 CLI-adapter follow-up): read capabilities.deferred_tools.core (module 24’s eagerly-advertised allowlist) — the S-sized read materialize_config inlined, extracted so the CLI’s own build_config can share it without re-deriving the same JSON-array walk, same pattern as permissions_rules_patterns. Caller is responsible for the cap.enabled == Some(true) gate (both call sites already fetch the capability that way). None when the core key is absent — leaves the caller’s existing value untouched, matching ConfigProfile::tool_advertising_core’s “only overridden if the profile sets it” contract.
expand_env_vars
P4 (design §5.2 “P4”, §1.8: “env substitution in values”): expand ${VAR} references in s against the process environment. Applied at HarnessConfig::to_config_profile to the string-valued [core] fields that plausibly vary per deployment — base_url, system_prompt, additional_dirs, extra_headers values, and extra_body string values (judgment call, §1.8’s “in values” wording names no exhaustive field list; api_key_env/api_key_cmd are deliberately EXCLUDED — the former is already an env var NAME not a value, the latter is a shell command the shell itself expands when it runs, see the call site’s comment).
merge_permissions_capability
Merge a project-layer capabilities.permissions table onto the trusted (user/global) layer’s — the single canonical merge BOTH the CLI route (crates/cli/src/userconfig.rs::overlay_project) and this core resolver (resolve_top, below) call, so the two routes cannot diverge the way the independent Fable-5 review of P4a found (proven attacks, both against the hard approval floor Config::needs_approval gives rules.deny — true even under ApprovalPolicy::Never):
merge_reduction_capability
Merge the project-layer reduction module without allowing an untrusted repository to widen an explicit trusted disable. Reduction is the one capability a project may enable when the trusted layer is silent, but an explicit false on either the module master switch or a documented pass gate is narrowing and therefore dominates true from the other layer. Settings still deep-merge so a sibling project key cannot discard trusted gates that it did not mention.
module_enabled
Whether a module (a top-level name, or a dotted top.sub path for the NESTED_MODULE_NAMES) is enabled in a resolved HarnessConfig.
permissions_network_policy
P5-1 (design §2 module 12 carry-forward, §3.1 capabilities.permissions.sandbox.network.*): give the crate::tools::NetworkPolicy enforcement point (ToolContext::check_network, wired since P4c) its real config source. Reads the network sub-table of capabilities.permissions.sandbox — note this is nested under permissions, not a separate permissions.sandbox capability entry (see module_enabled’s doc comment on the dotted-name convention: nested modules 11-13 all live in permissions’s own settings, never as separate BTreeMap keys). None when capabilities.permissions.sandbox (the TABLE form; the bare-string tier shorthand has no network to read) is absent entirely — byte-identical to today’s no-policy-configured gap. Present-but-network-absent still yields Some(NetworkPolicy::default()) (enabled: false), which is a harmless no-op — see NetworkPolicy’s own doc comment (crate::tools) on enabled: false behaving exactly like None on the context.
permissions_protected_paths
P5-1 (design §2 module 13, §3.1 capabilities.permissions.protected_paths.paths): read the protected- paths glob list — unconditional-on-cap like auto_approved_tools/ permissions_rules_patterns above (not gated on permissions.protected_paths.enabled, same sibling-field precedent); crate::permissions::rules::protected_path_deny_rules is what expands this list into the engine’s actual deny tier at the gate.
permissions_rules_ask_patterns
P5-1 (design §2 module 11, §3.1 capabilities.permissions.rules.ask): the ask sibling of permissions_rules_patterns’s deny/allow — kept as its own function (rather than folded into that one) since only the P5-1 engine consults ask at all; Config::needs_approval (the legacy gate) has no ask concept, so permissions_rules_patterns staying deny/allow-only keeps its existing callers (including the CLI’s build_config) untouched.
permissions_rules_patterns
P4 (design §5.2 “P4”): read capabilities.permissions.rules.deny/ .allow (module 11’s two pattern arrays) into (deny, allow) glob pattern lists — the S-sized generalization of auto_approved_tools this phase lands, NOT the full P5 deny→ask→allow priority engine. cap is the already-fetched capabilities.permissions table (both this resolver’s materialize_config and the CLI’s own build_config fetch it themselves first, since each has a different container type to fetch it FROM — a HarnessConfig vs a BTreeMap on FileConfig). Empty Vecs when the table or either key is absent — the default, byte-identical-to-today shape.
permissions_sandbox_env_policy
P5-10 (§2 module 12, §3.1 capabilities.permissions.sandbox.env_policy): same parse-or-fail-safe-to-Default treatment as permissions_sandbox_escalation — an absent or unrecognized value falls back to crate::sandbox::SandboxEnvPolicy::Inherit (today’s behavior), never silently to the stricter None (that would be a surprising, unrequested behavior CHANGE, not a safe fail-closed default — env_policy narrows what a subprocess sees, it isn’t a security gate the way escalation’s fail-closed direction is).
permissions_sandbox_escalation
P5-10 (§2 module 12, §3.1 capabilities.permissions.sandbox.escalation): parses via crate::sandbox::SandboxEscalation::parse (the alias- normalizing parser every sandbox-adjacent string in this crate uses); an absent or unrecognized value fails safe to crate::sandbox::SandboxEscalation::Deny (the type’s own Default), never silently to Allow.
permissions_sandbox_os_enabled
P5-10 (§2 module 12, §3.1 capabilities.permissions.sandbox.enabled): the OS-level backstop’s own master gate — see crate::sandbox:: os_sandbox_active’s doc comment for why None (the TABLE form’s enabled key absent, OR the bare-string sandbox = "<tier>" shorthand used instead, which has no enabled key to read at all) preserves the pre-P5-10 tier-driven trigger rather than defaulting to Some(false).
resolve
§3.5’s resolver entry point: top_toml is the file being resolved (e.g. the user’s config) — it may set extends (steps 1-3). project_toml is an optional second, untrusted layer (§3.3) — ALWAYS sanitized and clamped before merge (step 4), regardless of what it sets. opts controls step 5’s strictness. Steps 6-7 (module validation, Config materialization) run last, over the fully-folded result.
resolve_harness
P3 CLI-wiring entry point (design §5.2 P3, “CLI load path resolves config through the P2 resolver”): resolve an already-typed HarnessConfig — e.g. one assembled by the CLI from its own FileConfig’s forward-compatible extends/capabilities/experimental fields, which are ALREADY sanitized/merged by userconfig.rs’s own project-layer handling (sanitized_for_project/overlay_project) before this ever sees them — so there is no second untrusted text layer to merge here, unlike resolve. top.extends is still chased (steps 1-3) exactly as resolve does; when top.extends is None, callers that want “no config file ⇒ supercode-default semantics” (design §4 intro: “supercode with no config file resolves to this preset”) must set top.extends = Some("supercode-default".to_string()) themselves before calling this — this function does not silently default it, since a SILENT default would be exactly the kind of implicit behavior the supercode-default preset exists to name instead of hide.
sanitize_for_project
§3.3’s monotonic-tightening rule for a project-layer HarnessConfig: strip/narrow everything an untrusted repo must not control, recording what it touched. Mirrors userconfig.rs::sanitized_for_project’s contract on the new unified schema (see the module note above).
server_bind
P5-8 (§2 module 31 server, D8 “remote attach”): read capabilities.server.bind — the HTTP listen address serve/--output- format rpc --http-class transports use. None (unset) means the LOOPBACK DEFAULT the runtime itself picks (127.0.0.1, OS-assigned ephemeral port) — this fn only surfaces an EXPLICIT override, so the runtime can tell “the operator opted into a specific bind” (which may warrant the non-loopback-exposure warning) from “nothing configured (safe default)”.
server_token
P5-8: read capabilities.server.token — the bearer token a remote HTTP client must present (§ security posture: stdio transports are parent- process-trusted and need no token; HTTP does). None (unset) means the runtime mints a random per-session token instead of trusting a operator-chosen fixed value.
subagent_definitions
P5-3 (design §2 module 9, §3.1 capabilities.subagents.agents.<name>, D3 “named-defs”): parse the named-subagent-definition sub-table into crate::subagents::NamedAgentDefinitions, keyed by name. Missing or malformed fields degrade gracefully (an entry with no system_prompt gets an empty one — the caller falls back to the parent’s own system prompt, see Agent::run_spawn_subagent) rather than erroring the whole resolve — a config-shape mistake here is a weaker agent definition, not a security-relevant silent-allow (unlike the permissions-layer case-sensitivity carry-forward elsewhere in this file).