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
96 lines
3.8 KiB
Rust
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)
|
|
}
|