Files
clawmates/crates/cm-api/src/skill_self_authoring.rs
T
Omar SobhandClaude Opus 5 76ac3714f1
deploy / test (push) Successful in 5m15s
deploy / build (push) Successful in 5m28s
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
2026-09-20 22:07:39 -05:00

96 lines
3.8 KiB
Rust

//! Applies agents' own skill drafts, with no human decision.
//!
//! `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
//! ticking a checkbox in `LevelUpDrawer`. This worker removes the checkbox —
//! 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
//! workspace-scoped and versioned, cannot take the name of a hand-authored
//! skill, and lands with `approved_by = NULL` — so "an agent decided this" is
//! distinguishable from "a person decided this" forever after, which is the
//! property that makes the change reversible instead of merely fast.
//!
//! Only `skill_candidate` items apply here. `identity_refinement` and
//! `brain_consolidation` still wait for a human: they change what an agent IS
//! rather than adding a procedure it can consult.
use sqlx::{PgPool, Row};
use std::time::Duration;
/// How often to sweep for pending drafts.
///
/// Proposals arrive when someone runs a level-up, not continuously, so this is
/// slow on purpose — the work is bounded by how often an agent reflects, and
/// polling faster would only add load.
const SWEEP_INTERVAL: Duration = Duration::from_secs(120);
/// Start the sweep, unless self-authoring is switched off.
pub fn spawn(pool: PgPool) {
if !crate::level_up::self_authoring_enabled() {
eprintln!(
"skill_self_authoring: DISABLED (the default since 2026-09-20) — \
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;
}
eprintln!(
"skill_self_authoring: ENABLED — agents apply their own skill drafts \
without human approval. Writes are workspace-scoped, versioned, and \
cannot take a hand-authored skill's name; each lands with no approver \
recorded. Unset CLAWMATES_SKILL_SELF_AUTHORING to restore the gate."
);
tokio::spawn(async move {
loop {
if let Err(e) = sweep(&pool).await {
eprintln!("skill_self_authoring: sweep failed: {e}");
}
tokio::time::sleep(SWEEP_INTERVAL).await;
}
});
}
/// Apply every pending proposal's skill candidates. Returns how many skills landed.
pub async fn sweep(pool: &PgPool) -> Result<usize, String> {
// Bounded per pass: a backlog drains over several sweeps rather than
// holding the pool for as long as it takes to apply all of it.
let rows = sqlx::query(
"SELECT id, workspace_id FROM level_up_proposals
WHERE status = 'pending'
ORDER BY created_at
LIMIT 20",
)
.fetch_all(pool)
.await
.map_err(|e| format!("select pending proposals: {e}"))?;
let mut applied = 0usize;
for row in &rows {
let id: uuid::Uuid = row.get("id");
let workspace_id: uuid::Uuid = row.get("workspace_id");
match crate::level_up::apply_autonomous(
pool,
cm_domain::WorkspaceId::from(workspace_id),
id,
)
.await
{
Ok(items) if !items.is_empty() => {
applied += items.len();
eprintln!(
"skill_self_authoring: applied {} skill draft(s) from proposal {id} \
with no human approval",
items.len()
);
}
// A proposal with no skill candidates is left pending on purpose —
// its identity/memory items still belong to the human gate.
Ok(_) => {}
Err(e) => eprintln!("skill_self_authoring: proposal {id}: {e}"),
}
}
Ok(applied)
}