sec(door): closed by default, governor fails closed; self-authoring off by default
Three fail-open paths on the §15 door: no env at all meant allow-all; a
governor that could not be reached approved with a WARNING; and a reply
that never said DENY — empty, truncated, a refusal — approved, because the
rule was !contains("DENY"). On the two days the judge plan emptied every
outbound action was approved by nobody.
Now: governor_allows() needs an explicit ALLOW and no DENY; both judge()
implementations return false when unreachable; with no governor the door
opens only on CLAWMATES_DOOR_POLICY=allow. Open Agent Passport (arXiv
2603.20953): 74.6% social-engineering success under a permissive policy,
0 of 879 under a restrictive one. Local override gains the governor prod
already runs.
skill_self_authoring: default flipped to OFF. No agent-authored skill has
ever been delivered to a mission or scored; prod held zero proposals.
Enable with CLAWMATES_SKILL_SELF_AUTHORING=1 once promoted skills go
through the files arm and get a Skill-Use score.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz
This commit is contained in:
co-authored by
Claude Opus 5
parent
9b7680a605
commit
76ac3714f1
@@ -7,9 +7,10 @@
|
||||
//! (broker-executed tools reveal secrets only inside the broker), every action
|
||||
//! is journaled to the append-only audit log, and a central policy decides each
|
||||
//! call — but the **human approver is replaced by an automated policy/governor**
|
||||
//! ("agents control their destiny"). The default policy is allow-all, so agents
|
||||
//! are autonomous out of the gate; recipient allowlists, spend caps, taint
|
||||
//! blocks, or a governor agent plug into [`policy_decide`].
|
||||
//! ("agents control their destiny"). Recipient allowlists, spend caps, taint
|
||||
//! blocks, or a governor agent plug into [`policy_decide`]. Since 2026-09-20
|
||||
//! the door is **closed by default**: a call is approved only by a governor
|
||||
//! that answered ALLOW, or by an explicit `CLAWMATES_DOOR_POLICY=allow`.
|
||||
//!
|
||||
//! v1 exposes `email_send` (runtime-executed → `outbox`, observable, no external
|
||||
//! creds). Broker-backed tools (e.g. `slack_post`) are the next increment — they
|
||||
@@ -85,10 +86,14 @@ enum PolicyOutcome {
|
||||
/// 2. a per-workspace hourly rate cap (`CLAWMATES_DOOR_RATE_LIMIT`, counts
|
||||
/// executed door actions in the audit log);
|
||||
/// 3. an email recipient-domain allowlist (`CLAWMATES_DOOR_EMAIL_ALLOW`);
|
||||
/// 4. a governor hook (extension point) — a deterministic rule set or a
|
||||
/// governor agent can veto here.
|
||||
/// 4. a governor agent (`CLAWMATES_DOOR_GOVERNOR`) — must answer ALLOW;
|
||||
/// unreachable, silent, or off-contract means DENY;
|
||||
/// 5. with no governor, an explicit `CLAWMATES_DOOR_POLICY=allow`.
|
||||
///
|
||||
/// Default (no env set) = allow-all → agents fully autonomous.
|
||||
/// Default (no env set) = **deny**. This was allow-all until 2026-09-20, and
|
||||
/// the governor failed open on top of that, so with the judge plan emptied
|
||||
/// every outbound action was approved by nobody. See [`ungoverned_default`]
|
||||
/// and `cm_runtime::Runtime::judge` for the measurement behind the flip.
|
||||
async fn policy_decide(
|
||||
state: &AppState,
|
||||
workspace: cm_domain::WorkspaceId,
|
||||
@@ -140,9 +145,10 @@ async fn policy_decide(
|
||||
}
|
||||
|
||||
// 4. Governor agent: when CLAWMATES_DOOR_GOVERNOR is set, an LLM judges the
|
||||
// action and can veto — the "self-governing topology" path. Fail-open
|
||||
// (a governor outage doesn't halt agents); deterministic rules above are
|
||||
// the hard floor.
|
||||
// action — the "self-governing topology" path. Fail-CLOSED: a governor
|
||||
// that cannot be reached, or that does not say ALLOW, denies. The
|
||||
// deterministic rules above are the hard floor; this is the only
|
||||
// approver.
|
||||
if std::env::var("CLAWMATES_DOOR_GOVERNOR").is_ok() {
|
||||
let system = "You are a security governor for an autonomous agent's outbound actions. \
|
||||
Reply with exactly ALLOW or DENY on the first line, then one short reason. \
|
||||
@@ -166,24 +172,39 @@ async fn policy_decide(
|
||||
} else {
|
||||
state.runtime.judge(system, &request).await
|
||||
};
|
||||
// Fail-open is deliberate, but a governor that is failing open on EVERY
|
||||
// request is a security control that has quietly stopped existing —
|
||||
// and the caller drops `reason` whenever it allows, so nothing said so.
|
||||
// `judge()` returns this exact prefix when the provider never answered,
|
||||
// which a rate-limited or uncredited judge model does on every call.
|
||||
if allow && reason.starts_with("governor unreachable") {
|
||||
// Still logged loudly: a door that denies everything because its
|
||||
// governor is down is safe, and is also a platform with no outbound
|
||||
// actions until someone reads this line.
|
||||
if reason.starts_with("governor unreachable") {
|
||||
eprintln!(
|
||||
"mcp_door: WARNING — the door governor is FAILING OPEN for {mcp_tool} \
|
||||
({reason}). Every outbound action is being approved unjudged. Point \
|
||||
CLAWMATES_JUDGE_MODEL at a reachable model."
|
||||
"mcp_door: WARNING — the door governor is unreachable, DENYING {mcp_tool} \
|
||||
({reason}). Point CLAWMATES_JUDGE_MODEL at a reachable model."
|
||||
);
|
||||
}
|
||||
if !allow {
|
||||
return PolicyOutcome::Deny(format!("governor agent vetoed — {reason}"));
|
||||
}
|
||||
return PolicyOutcome::Approve;
|
||||
}
|
||||
|
||||
PolicyOutcome::Approve
|
||||
// 5. No governor. The door is closed unless the operator opened it.
|
||||
ungoverned_default(std::env::var("CLAWMATES_DOOR_POLICY").ok().as_deref())
|
||||
}
|
||||
|
||||
/// The posture with no governor configured. Only the literal `allow` opens
|
||||
/// the door; unset, empty, or anything else keeps it shut and says how to
|
||||
/// open it. `deny` is handled earlier as the kill switch and lands here too.
|
||||
fn ungoverned_default(policy: Option<&str>) -> PolicyOutcome {
|
||||
match policy.map(str::trim) {
|
||||
Some("allow") => PolicyOutcome::Approve,
|
||||
_ => PolicyOutcome::Deny(
|
||||
"the door has no governor and no allow policy — set CLAWMATES_DOOR_GOVERNOR=1 \
|
||||
or, to run ungoverned, CLAWMATES_DOOR_POLICY=allow"
|
||||
.into(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mint an auto-approved approval + single-use execution grant for a
|
||||
@@ -609,6 +630,18 @@ pub async fn mcp(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The default posture is closed. Before 2026-09-20 an unset policy
|
||||
/// meant allow-all.
|
||||
#[test]
|
||||
fn the_door_is_closed_unless_opened() {
|
||||
assert!(matches!(ungoverned_default(None), PolicyOutcome::Deny(_)));
|
||||
assert!(matches!(ungoverned_default(Some("")), PolicyOutcome::Deny(_)));
|
||||
assert!(matches!(ungoverned_default(Some("deny")), PolicyOutcome::Deny(_)));
|
||||
assert!(matches!(ungoverned_default(Some("yes")), PolicyOutcome::Deny(_)));
|
||||
assert!(matches!(ungoverned_default(Some("allow")), PolicyOutcome::Approve));
|
||||
assert!(matches!(ungoverned_default(Some(" allow ")), PolicyOutcome::Approve));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exposed_tool_name_maps_to_registry_name() {
|
||||
assert_eq!(internal_name("email_send"), Some("email.send"));
|
||||
|
||||
Reference in New Issue
Block a user