kisenon
Agent-Safe Change Control

常见陷阱

悄悄击溃缩容到零和智能体安全的反模式——以及如何避免它们。

这些模式不会报错。它们通过 CI,在 diff 里看起来合情合理,却 悄悄取消掉一项你正在为之付费的保证——缩容到零的节省或 智能体安全。下面的每一条:发生了什么,为什么它有害,以及修复。

Keep-alive code defeats scale-to-zero

一个智能体"帮忙"添加了按定时器触碰数据库的代码。每一次 周期性查询都算作活动并重置挂起计时器,因此一个你设计成 缩容到零的端点永远不会休眠——而空闲也不再免费。

它以几种形态出现。一个前端或后端心跳:

// runs forever, holds the compute awake forever
setInterval(async () => {
  await pool.query("SELECT 1");   // "health check"
}, 30_000);

一个指向某个其处理器恰好查询数据库的端点的外部 运行时间监控——因此每一次探测都唤醒计算:

app.get("/health", async (_req, res) => {
  await pool.query("SELECT 1");   // monitored every 60s → never idle
  res.send("ok");
});

或者那些用验证查询保持一个活动连接的连接池设置—— 一个大于零的 min,或一个无服务器"预热器":

const pool = new Pool({
  min: 2,                         // keeps ≥2 connections open + validated
  // a validation/keep-alive query on each idle connection resets the timer
});

修复: 审计智能体写的 diff 是否有周期性的数据库访问。把存活性 检查保持在 HTTP 层——探测应用,而非数据库——并让连接池 排空到零(min: 0)。控制台的计算视图会显示一个端点 为什么 没有 缩容到零,这能很快浮现一个游离的心跳。

Handing the agent main credentials short-circuits ASCC

把 main 分支的连接字符串交给智能体——或让它以你自己的 用户身份登录运行 keon——绕过 ASCC 强制的一切:沙箱 隔离、LEASH 预算、LINT 和 BLAST 检查、提升闸门、归因, 以及撤销锚点。智能体现在正在直接写入生产环境,而那个 使智能体变更安全的循环一步也没有运行。

智能体绝不应持有能写入 main 的凭据。提升按设计是 服务端的——cp 是生产环境的唯一写入者。

修复:Settings → API keys 铸造一个 智能体范围的 API 密钥 (capability = agent,scope = 某个项目,短过期),只交给智能体沙箱 连接字符串,并从其环境中移除更强的凭据(没有完整的 DATABASE_URL,没有 ~/.pgpass,同一 shell 中没有已登录的 keon 会话)。参见 Hand an agent a scoped credentialGuardrails 的 SCOPE 部分。

Retry storms around wake-up

挂起之后的第一次连接必须唤醒计算,因此它比一个 温热的更慢。一个看到单次缓慢连接的智能体可能会"修复"它——用 激进的客户端超时和重试循环去猛击端点,或者通过 添加一个 keep-alive(陷阱 #1)使它根本不必唤醒。二者都为掩盖一个 一次性成本而把节省交换掉了。

修复: 把客户端 connect_timeout 设在 10 秒左右,并在第一次 连接时保持耐心。一旦温热路径就位,冷启动在同区域内是亚秒级的——一次 稍慢的连接是预期之内的,而非一个需要工程绕过的故障。

postgresql://…?connect_timeout=10

Credentials committed to code

智能体把连接字符串和 API 密钥硬编码进源码和 .env 文件, 它们随后进入 git。泄露的密钥在进入一个公开(或后来公开)的 仓库后几小时内就被采集——这个窗口以小时计,而非天。

修复: 通过环境注入密钥,绝不提交它们。把 .env 文件排除在 git 之外。一旦泄露,立即从 Settings → API keys 轮换 ——吊销暴露的密钥并铸造一个新的。

Direct DDL on main bypasses the promote loop

直接对 main 运行迁移会跳过那个使变更可逆且可审计的 捕获。你会失去发出的 up/down 迁移(EMIT)、 签名的账本条目(LEDGER),以及撤销锚点(UNDO)。模式 变了,但没有平台可以重放或回滚的记录。

修复: 沙箱 → 提升,即使是一个"琐碎"的模式变更。提升循环 正是产出迁移、账本条目和锚点的东西;一次直接的 ALTER 一个都不产出。

Writes inside a rolled-back transaction still land

捕获记录智能体运行的语句——它没有 事务边界意识。在一个 BEGIN … ROLLBACK 内执行的语句 仍会被捕获,并且如果那个沙箱被提升,它们会被重放并 应用。沙箱中的一次 ROLLBACK 不会把它们取消捕获。

BEGIN;
UPDATE users SET plan = 'pro';   -- captured
ROLLBACK;                        -- does NOT remove it from the capture

修复: 不要依赖沙箱内的 ROLLBACK 来"撤销"探索性工作。 如果你想让工作消失,丢弃沙箱——那才是捕获所尊重的处置 路径。

Parking work in a sandbox past its budget or TTL

沙箱是短暂的。当一个沙箱耗尽其 LEASH 预算时,它会被 自动丢弃(status=discarded),其中任何未提升的工作都会丢失。 把一个沙箱当作一个你会"稍后回来"的长期分支,正是一天的 变更消失的方式。

修复: 提升它,或在一个全新的沙箱中重新捕获该工作。默认把沙箱 当作可丢弃的——耐久的制品是提升,而非沙箱。

Misreading the guarantees

关于这些检查实际承诺什么的三个具体陷阱:

  • 爆炸半径试运行是建议性的。 它测量一次提升 做 什么——真实的锁、时长、行数——但它绝不给提升设闸门。一次 干净的试运行是信息,而非一盏绿灯;而一次试运行在提升前是否 被要求,是 POLICY 的决定,而非试运行的。

  • Approve 无法覆盖策略 block。 一次人工的 Approve 绝不能放行 一个策略 block 或一条越出通道的语句。绕过一个 block 的 唯一途径是一次经过审计的策略更改——按设计,没有手动 覆盖开关。

  • 一个脱敏 fork 脱敏的是值,而非形态。 在一个脱敏 fork 上, 是 脱敏的,因此智能体针对那些脱敏值写的依赖数据的 DML 在重放时 不会匹配父分支上的真实行。模式通道的工作是 与值无关的,会忠实重放;针对脱敏数据开发的依赖值的 DML 则不会。