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
@@ -227,20 +227,57 @@ pub async fn apply(
|
|||||||
|
|
||||||
/// Is autonomous skill authoring on?
|
/// Is autonomous skill authoring on?
|
||||||
///
|
///
|
||||||
/// Default ON, by operator decision. Stated at boot rather than assumed: this
|
/// Default OFF since 2026-09-20, by operator decision. It shipped default ON,
|
||||||
/// flips a human approval gate that has existed since the feature shipped, and
|
/// and in the months since no agent-authored skill was ever delivered to a
|
||||||
/// a safety gate that changes state silently is how nobody notices it changed.
|
/// mission or scored by the Skill-Use scorer — prod's `level_up_proposals`
|
||||||
|
/// held zero rows on the day of the flip. An auto-apply loop whose output has
|
||||||
|
/// never been measured is a supply chain of our own making (the shape Cisco
|
||||||
|
/// found in OpenClaw's third-party skills), so it waits for a human until
|
||||||
|
/// `promoted_from_brain` skills go through the `files` delivery arm and get
|
||||||
|
/// a Trigger/Compliance score like the hand-authored ones. Stated at boot
|
||||||
|
/// either way: a safety gate that changes state silently is how nobody
|
||||||
|
/// notices it changed.
|
||||||
pub fn self_authoring_enabled() -> bool {
|
pub fn self_authoring_enabled() -> bool {
|
||||||
!matches!(
|
matches!(
|
||||||
std::env::var("CLAWMATES_SKILL_SELF_AUTHORING")
|
std::env::var("CLAWMATES_SKILL_SELF_AUTHORING")
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.trim()
|
.trim()
|
||||||
.to_ascii_lowercase()
|
.to_ascii_lowercase()
|
||||||
.as_str(),
|
.as_str(),
|
||||||
"0" | "off" | "false"
|
"1" | "on" | "true"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod self_authoring_flag_tests {
|
||||||
|
/// Serialised through one env var; each case restores the prior state.
|
||||||
|
fn with(value: Option<&str>, f: impl FnOnce()) {
|
||||||
|
let key = "CLAWMATES_SKILL_SELF_AUTHORING";
|
||||||
|
let prior = std::env::var(key).ok();
|
||||||
|
match value {
|
||||||
|
Some(v) => std::env::set_var(key, v),
|
||||||
|
None => std::env::remove_var(key),
|
||||||
|
}
|
||||||
|
f();
|
||||||
|
match prior {
|
||||||
|
Some(v) => std::env::set_var(key, v),
|
||||||
|
None => std::env::remove_var(key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Off unless switched on. The previous default was the reverse.
|
||||||
|
#[test]
|
||||||
|
fn off_by_default_on_by_explicit_opt_in() {
|
||||||
|
with(None, || assert!(!super::self_authoring_enabled()));
|
||||||
|
with(Some(""), || assert!(!super::self_authoring_enabled()));
|
||||||
|
with(Some("0"), || assert!(!super::self_authoring_enabled()));
|
||||||
|
with(Some("yes"), || assert!(!super::self_authoring_enabled()));
|
||||||
|
with(Some("1"), || assert!(super::self_authoring_enabled()));
|
||||||
|
with(Some("on"), || assert!(super::self_authoring_enabled()));
|
||||||
|
with(Some("TRUE"), || assert!(super::self_authoring_enabled()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply a pending proposal's `skill_candidate` items with no human decision.
|
/// Apply a pending proposal's `skill_candidate` items with no human decision.
|
||||||
///
|
///
|
||||||
/// ONLY `skill_candidate`. The other item kinds are deliberately left to the
|
/// ONLY `skill_candidate`. The other item kinds are deliberately left to the
|
||||||
|
|||||||
@@ -7,9 +7,10 @@
|
|||||||
//! (broker-executed tools reveal secrets only inside the broker), every action
|
//! (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
|
//! 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**
|
//! call — but the **human approver is replaced by an automated policy/governor**
|
||||||
//! ("agents control their destiny"). The default policy is allow-all, so agents
|
//! ("agents control their destiny"). Recipient allowlists, spend caps, taint
|
||||||
//! are autonomous out of the gate; recipient allowlists, spend caps, taint
|
//! blocks, or a governor agent plug into [`policy_decide`]. Since 2026-09-20
|
||||||
//! blocks, or a governor agent plug into [`policy_decide`].
|
//! 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
|
//! v1 exposes `email_send` (runtime-executed → `outbox`, observable, no external
|
||||||
//! creds). Broker-backed tools (e.g. `slack_post`) are the next increment — they
|
//! 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
|
/// 2. a per-workspace hourly rate cap (`CLAWMATES_DOOR_RATE_LIMIT`, counts
|
||||||
/// executed door actions in the audit log);
|
/// executed door actions in the audit log);
|
||||||
/// 3. an email recipient-domain allowlist (`CLAWMATES_DOOR_EMAIL_ALLOW`);
|
/// 3. an email recipient-domain allowlist (`CLAWMATES_DOOR_EMAIL_ALLOW`);
|
||||||
/// 4. a governor hook (extension point) — a deterministic rule set or a
|
/// 4. a governor agent (`CLAWMATES_DOOR_GOVERNOR`) — must answer ALLOW;
|
||||||
/// governor agent can veto here.
|
/// 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(
|
async fn policy_decide(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
workspace: cm_domain::WorkspaceId,
|
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
|
// 4. Governor agent: when CLAWMATES_DOOR_GOVERNOR is set, an LLM judges the
|
||||||
// action and can veto — the "self-governing topology" path. Fail-open
|
// action — the "self-governing topology" path. Fail-CLOSED: a governor
|
||||||
// (a governor outage doesn't halt agents); deterministic rules above are
|
// that cannot be reached, or that does not say ALLOW, denies. The
|
||||||
// the hard floor.
|
// deterministic rules above are the hard floor; this is the only
|
||||||
|
// approver.
|
||||||
if std::env::var("CLAWMATES_DOOR_GOVERNOR").is_ok() {
|
if std::env::var("CLAWMATES_DOOR_GOVERNOR").is_ok() {
|
||||||
let system = "You are a security governor for an autonomous agent's outbound actions. \
|
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. \
|
Reply with exactly ALLOW or DENY on the first line, then one short reason. \
|
||||||
@@ -166,24 +172,39 @@ async fn policy_decide(
|
|||||||
} else {
|
} else {
|
||||||
state.runtime.judge(system, &request).await
|
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,
|
// `judge()` returns this exact prefix when the provider never answered,
|
||||||
// which a rate-limited or uncredited judge model does on every call.
|
// 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!(
|
eprintln!(
|
||||||
"mcp_door: WARNING — the door governor is FAILING OPEN for {mcp_tool} \
|
"mcp_door: WARNING — the door governor is unreachable, DENYING {mcp_tool} \
|
||||||
({reason}). Every outbound action is being approved unjudged. Point \
|
({reason}). Point CLAWMATES_JUDGE_MODEL at a reachable model."
|
||||||
CLAWMATES_JUDGE_MODEL at a reachable model."
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if !allow {
|
if !allow {
|
||||||
return PolicyOutcome::Deny(format!("governor agent vetoed — {reason}"));
|
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
|
/// Mint an auto-approved approval + single-use execution grant for a
|
||||||
@@ -609,6 +630,18 @@ pub async fn mcp(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn exposed_tool_name_maps_to_registry_name() {
|
fn exposed_tool_name_maps_to_registry_name() {
|
||||||
assert_eq!(internal_name("email_send"), Some("email.send"));
|
assert_eq!(internal_name("email_send"), Some("email.send"));
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
//!
|
//!
|
||||||
//! `level_up` has generated complete skill drafts from a model since it
|
//! `level_up` has generated complete skill drafts from a model since it
|
||||||
//! shipped; the only thing between a draft and the catalogue was an operator
|
//! shipped; the only thing between a draft and the catalogue was an operator
|
||||||
//! ticking a checkbox in `LevelUpDrawer`. This worker removes the checkbox, by
|
//! ticking a checkbox in `LevelUpDrawer`. This worker removes the checkbox —
|
||||||
//! operator decision.
|
//! when switched on. It is OFF by default since 2026-09-20; see
|
||||||
|
//! `level_up::self_authoring_enabled` for why.
|
||||||
//!
|
//!
|
||||||
//! What is deliberately NOT removed is the record. Every write stays
|
//! What is deliberately NOT removed is the record. Every write stays
|
||||||
//! workspace-scoped and versioned, cannot take the name of a hand-authored
|
//! workspace-scoped and versioned, cannot take the name of a hand-authored
|
||||||
@@ -29,8 +30,9 @@ const SWEEP_INTERVAL: Duration = Duration::from_secs(120);
|
|||||||
pub fn spawn(pool: PgPool) {
|
pub fn spawn(pool: PgPool) {
|
||||||
if !crate::level_up::self_authoring_enabled() {
|
if !crate::level_up::self_authoring_enabled() {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"skill_self_authoring: DISABLED (CLAWMATES_SKILL_SELF_AUTHORING) — \
|
"skill_self_authoring: DISABLED (the default since 2026-09-20) — \
|
||||||
agent skill drafts wait for a human in the level-up drawer"
|
agent skill drafts wait for a human in the level-up drawer. \
|
||||||
|
Set CLAWMATES_SKILL_SELF_AUTHORING=1 to let agents apply their own."
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -38,7 +40,7 @@ pub fn spawn(pool: PgPool) {
|
|||||||
"skill_self_authoring: ENABLED — agents apply their own skill drafts \
|
"skill_self_authoring: ENABLED — agents apply their own skill drafts \
|
||||||
without human approval. Writes are workspace-scoped, versioned, and \
|
without human approval. Writes are workspace-scoped, versioned, and \
|
||||||
cannot take a hand-authored skill's name; each lands with no approver \
|
cannot take a hand-authored skill's name; each lands with no approver \
|
||||||
recorded. Set CLAWMATES_SKILL_SELF_AUTHORING=0 to restore the gate."
|
recorded. Unset CLAWMATES_SKILL_SELF_AUTHORING to restore the gate."
|
||||||
);
|
);
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
|
|||||||
@@ -605,19 +605,19 @@ impl ZeroClawDriveExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Use a runtime agent as a governance judge: drive `alias` with the judge
|
/// Use a runtime agent as a governance judge: drive `alias` with the judge
|
||||||
/// prompt and parse the verdict (`DENY` anywhere ⇒ deny, else allow). This
|
/// prompt and parse the verdict with [`cm_runtime::governor_allows`]. This
|
||||||
/// lets a **subscription-only** model (e.g. Kimi via `kimi_cli`) be the judge
|
/// lets a **subscription-only** model (e.g. Kimi via `kimi_cli`) be the judge
|
||||||
/// with no platform API key — the registry/SDK path GLM and Kimi can't take.
|
/// with no platform API key — the registry/SDK path GLM and Kimi can't take.
|
||||||
/// Fail-open (returns `(true, …)`) so a judge outage never halts agents.
|
/// Fail-closed: an unreachable judge denies, for the reason given on
|
||||||
|
/// [`cm_runtime::Runtime::judge`].
|
||||||
pub async fn judge(&self, alias: &str, system: &str, user: &str) -> (bool, String) {
|
pub async fn judge(&self, alias: &str, system: &str, user: &str) -> (bool, String) {
|
||||||
let prompt = format!("{system}\n\n{user}");
|
let prompt = format!("{system}\n\n{user}");
|
||||||
match self.drive(alias, &prompt).await {
|
match self.drive(alias, &prompt).await {
|
||||||
Ok(outcome) => {
|
Ok(outcome) => {
|
||||||
let text = outcome.output.trim().to_string();
|
let text = outcome.output.trim().to_string();
|
||||||
let allow = !text.to_uppercase().contains("DENY");
|
(cm_runtime::governor_allows(&text), text)
|
||||||
(allow, text)
|
|
||||||
}
|
}
|
||||||
Err(e) => (true, format!("governor unreachable (fail-open): {e}")),
|
Err(e) => (false, format!("governor unreachable (fail-closed): {e}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ mod tools;
|
|||||||
pub use events::{RunEventBody, RunEventEnvelope};
|
pub use events::{RunEventBody, RunEventEnvelope};
|
||||||
pub use outbox::{drain_once, spawn_drainer, EmailSender, LettreSender, SmtpConfig};
|
pub use outbox::{drain_once, spawn_drainer, EmailSender, LettreSender, SmtpConfig};
|
||||||
pub use runtime::{
|
pub use runtime::{
|
||||||
judge_model, ProviderRegistry, Runtime, RuntimeConfig, RuntimeError, StartedRun,
|
governor_allows, judge_model, ProviderRegistry, Runtime, RuntimeConfig, RuntimeError,
|
||||||
|
StartedRun,
|
||||||
};
|
};
|
||||||
pub use sandboxes::{NodeDriverProvider, SandboxManager};
|
pub use sandboxes::{NodeDriverProvider, SandboxManager};
|
||||||
pub use terminals::{DriveConfig, TerminalManager};
|
pub use terminals::{DriveConfig, TerminalManager};
|
||||||
|
|||||||
@@ -30,6 +30,16 @@ const APPROVAL_TTL: time::Duration = time::Duration::hours(24);
|
|||||||
/// comparison scorer). Judges use the strongest model — default
|
/// comparison scorer). Judges use the strongest model — default
|
||||||
/// `claude-opus-5` — while everything else runs on the configured default
|
/// `claude-opus-5` — while everything else runs on the configured default
|
||||||
/// model (`claude-sonnet-4-6`). Override with `CLAWMATES_JUDGE_MODEL`.
|
/// model (`claude-sonnet-4-6`). Override with `CLAWMATES_JUDGE_MODEL`.
|
||||||
|
/// Read a governor's verdict. Only an explicit `ALLOW` with no `DENY`
|
||||||
|
/// anywhere approves; an empty reply, a reply that never says either, or one
|
||||||
|
/// that says both is a denial. The previous rule was `!contains("DENY")`,
|
||||||
|
/// under which a model that answered nothing at all — a truncated stream, a
|
||||||
|
/// refusal, a reply in the wrong shape — approved the action.
|
||||||
|
pub fn governor_allows(reply: &str) -> bool {
|
||||||
|
let upper = reply.to_ascii_uppercase();
|
||||||
|
upper.contains("ALLOW") && !upper.contains("DENY")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn judge_model() -> String {
|
pub fn judge_model() -> String {
|
||||||
std::env::var("CLAWMATES_JUDGE_MODEL").unwrap_or_else(|_| "claude-opus-5".to_string())
|
std::env::var("CLAWMATES_JUDGE_MODEL").unwrap_or_else(|_| "claude-opus-5".to_string())
|
||||||
}
|
}
|
||||||
@@ -298,13 +308,18 @@ impl Runtime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Governor agent: ask the **judge model** to judge an action. Returns
|
/// Governor agent: ask the **judge model** to judge an action. Returns
|
||||||
/// `(allow, reason)`. The verdict is the first token (`ALLOW`/`DENY`) of the
|
/// `(allow, reason)`, parsed by [`governor_allows`]: only an explicit
|
||||||
/// model's reply. Best-effort and **fail-open** — if the model is
|
/// `ALLOW` with no `DENY` approves.
|
||||||
/// unreachable it returns `(true, …)` so a governor outage doesn't halt
|
///
|
||||||
/// autonomous agents (the governor is an extra soft check atop deterministic
|
/// **Fail-closed** since 2026-09-20. This returned `(true, …)` when the
|
||||||
/// policy, not the only gate). Judges use the strongest model
|
/// model was unreachable so that a governor outage would not halt
|
||||||
/// ([`judge_model`], default `claude-opus-4-8`); everything else runs on the
|
/// autonomous agents — which meant that on the day the judge plan emptied
|
||||||
/// configured default model.
|
/// (2026-08-29, 2026-09-09) every outbound action was approved unjudged,
|
||||||
|
/// with nothing but a log line saying so. Open Agent Passport (arXiv
|
||||||
|
/// 2603.20953) measured the difference a restrictive default makes:
|
||||||
|
/// social-engineered actions succeeded 74.6% of the time under a
|
||||||
|
/// permissive policy and 0 of 879 under a restrictive one. A door that
|
||||||
|
/// cannot reach its governor now says so and denies.
|
||||||
pub async fn judge(&self, system: &str, user: &str) -> (bool, String) {
|
pub async fn judge(&self, system: &str, user: &str) -> (bool, String) {
|
||||||
let (provider, model) = self.resolve_provider(&judge_model());
|
let (provider, model) = self.resolve_provider(&judge_model());
|
||||||
let request = ChatRequest {
|
let request = ChatRequest {
|
||||||
@@ -327,10 +342,10 @@ impl Runtime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => return (true, format!("governor unreachable (fail-open): {e}")),
|
Err(e) => return (false, format!("governor unreachable (fail-closed): {e}")),
|
||||||
}
|
}
|
||||||
let allow = !text.to_uppercase().contains("DENY");
|
let text = text.trim().to_string();
|
||||||
(allow, text.trim().to_string())
|
(governor_allows(&text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One-shot completion: send `system`+`user` to `model`, collect the full
|
/// One-shot completion: send `system`+`user` to `model`, collect the full
|
||||||
@@ -1077,3 +1092,21 @@ fn chat_messages(history: &[MessageWithSteps], user_text: &str) -> Vec<ChatMessa
|
|||||||
});
|
});
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod governor_verdict_tests {
|
||||||
|
use super::governor_allows;
|
||||||
|
|
||||||
|
/// Only an explicit ALLOW approves. The old rule, `!contains("DENY")`,
|
||||||
|
/// approved every reply in the first three rows.
|
||||||
|
#[test]
|
||||||
|
fn silence_and_off_contract_replies_deny() {
|
||||||
|
assert!(!governor_allows(""));
|
||||||
|
assert!(!governor_allows("Sure, that looks reasonable to me."));
|
||||||
|
assert!(!governor_allows("I cannot evaluate this request."));
|
||||||
|
assert!(!governor_allows("DENY\nrecipient is external"));
|
||||||
|
assert!(!governor_allows("ALLOW? No — DENY, the payload holds a token"));
|
||||||
|
assert!(governor_allows("ALLOW\nroutine status update to a known channel"));
|
||||||
|
assert!(governor_allows("allow"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -124,6 +124,10 @@ services:
|
|||||||
# max_tokens was raised alongside this.
|
# max_tokens was raised alongside this.
|
||||||
CLAWMATES_VALIDATOR_MODEL: glm:glm-5.3
|
CLAWMATES_VALIDATOR_MODEL: glm:glm-5.3
|
||||||
CLAWMATES_JUDGE_MODEL: glm:glm-5.3
|
CLAWMATES_JUDGE_MODEL: glm:glm-5.3
|
||||||
|
# The §15 door is closed by default since 2026-09-20: with no governor
|
||||||
|
# and no CLAWMATES_DOOR_POLICY=allow every outbound action is denied.
|
||||||
|
# Same governor as prod, judged by the model above.
|
||||||
|
CLAWMATES_DOOR_GOVERNOR: "1"
|
||||||
# Read by the `glm` provider's api_key_env in clawmates.toml.
|
# Read by the `glm` provider's api_key_env in clawmates.toml.
|
||||||
ZAI_API_KEY: ${ZAI_API_KEY:?set in .env}
|
ZAI_API_KEY: ${ZAI_API_KEY:?set in .env}
|
||||||
# ElevenLabs GenFM, for rendering the Continuous Research episode.
|
# ElevenLabs GenFM, for rendering the Continuous Research episode.
|
||||||
|
|||||||
@@ -53,8 +53,10 @@ document before now.
|
|||||||
|
|
||||||
`identity_refinement` and `brain_consolidation` still wait for a human: they
|
`identity_refinement` and `brain_consolidation` still wait for a human: they
|
||||||
change what an agent *is* rather than adding a procedure it can consult.
|
change what an agent *is* rather than adding a procedure it can consult.
|
||||||
Disable with `CLAWMATES_SKILL_SELF_AUTHORING=0`; the state is announced at
|
**Off by default since 2026-09-20** — no agent-authored skill had ever been
|
||||||
boot either way.
|
delivered to a mission or scored, and prod held zero proposals. Enable with
|
||||||
|
`CLAWMATES_SKILL_SELF_AUTHORING=1`; the state is announced at boot either
|
||||||
|
way.
|
||||||
- **No external registry feeds the catalogue.** ClawBrainHub trades `.brain`
|
- **No external registry feeds the catalogue.** ClawBrainHub trades `.brain`
|
||||||
files and never touches the `skills` table. `cm_db::repo::skills::create` —
|
files and never touches the `skills` table. `cm_db::repo::skills::create` —
|
||||||
the only path that would produce `source_kind='hand_authored'` — has no
|
the only path that would produce `source_kind='hand_authored'` — has no
|
||||||
|
|||||||
Reference in New Issue
Block a user