kisenon
Agent-Safe Change Control

Guardrails

预算、策略层级、受限权限、风险检查和爆炸半径检查——在智能体伤到你之前阻止它的东西。

五道护栏环绕着一个 AI 智能体沙箱。其中两道是 智能体物理上无法逾越的硬限制(LEASHSCOPE),一道把 数据变成裁定(POLICY),还有两道产出供其权衡的数据 (LINTBLAST)。没有任何一道把 LLM 放进信任路径。

LEASH — per-sandbox budgets

阻止: 一个失控的智能体——无界地烧掉语句、计算或挂钟时间。

在你创建或运行沙箱时设定一个预算:

keon sandbox create \
  --budget-statements 500 \
  --budget-compute-seconds 120 \
  --budget-wall-seconds 900
# or the same three flags on: keon sandbox run …

一旦突破,沙箱会 自行终止:状态变为 discarded,且 budget_breached 指明哪个限额被突破——statementscompute_secondswall_clock。CLI 以错误码 sandbox_budget_breached 退出。在任何 预算的 80% 处会触发一次警告。

在传输层面,预算是一个 budgets 对象,每个预算一个条目:

"budgets": {
  "statements":      { "limit": 500, "used": 218, "warned": false },
  "compute_seconds": { "limit": 120, "used": 41,  "warned": false },
  "wall_clock":      { "used": 63 }
}

一个 缺失的 limit 意味着无限制 —— 没有会被误读的 哨兵值。

配置默认值和上限(owner/admin):

# Per-project defaults double as the create-time cap.
curl -X PATCH .../v1/projects/{id} \
  -d '{"sandbox_budget_defaults": {"statements": 500, "compute_seconds": 120}}'

默认值 就是 上限:在创建时请求超过项目 默认值的量会返回 422 sandbox_budget_exceeds_cap。每项目的默认值是 可配置的——这里没有固定的数字可引用。

两条不变量:

  • 在创建时冻结。 预算在沙箱创建时即固定;此后更改 项目默认值绝不会给一个活动沙箱重新套上束缚。
  • 智能体密钥只能收紧。 一个智能体能力密钥可以在创建时请求 更紧的 预算;它绝不能设定项目默认值。

POLICY — promote-policy tiers

阻止: 一次其变更比项目所允许的更危险的提升——即使 是一个智能体(或一个人)试图放行的那一次。

POLICY 是一个每项目的覆盖层,把一个 风险类别 映射到一个 操作, 叠加在基础的 promote_mode 之上。

配置(CLI):

keon projects promote-policy <projectId>                 # show effective policy
keon projects promote-policy <projectId> \
  --set destructive=block \
  --set unbounded=require_human \
  --on-conflicts require_human                           # merge-write
keon projects promote-policy <projectId> --clear         # drop the overlay

--set <class=action> 可重复,并合并进现有的覆盖层。

配置(API): GET / PUT / DELETE /v1/projects/{projectId}/promote-policy

{ "version": 1,
  "rules": { "destructive": "block", "unbounded": "require_human" },
  "on_conflicts": "require_human" }
字段合法取值
风险类别additivemutatingdestructiveunboundedrewrite
操作auto_promoterequire_humanblock
on_conflictsignorerequire_humanblock

ignore on_conflicts 合法,绝不作为类别操作。

基础姿态 来自 promote_mode,覆盖层按类别覆写:

promote_mode每个类别默认为
selfauto_promote
humanrequire_human

每条生效规则都会报告它来自哪里——source: overlaypromote_modedefault

智能体被阻止时看到什么: 一次被阻止的提升返回 409 promote_blocked_by_policy,并附带决策细节。

关键规则: 一次人工的 Approve 绝不能覆盖策略 block 策略 会在批准时重新评估,因此 block 顶住那次点击。策略 验证是 失败即关闭 / 严格 的——一个无法解析的策略拒绝,绝不 退化为放行。

SCOPE — scoped authority (lanes)

阻止: 任何在你所声明的模式和表之外的读或写——即智能体的 通道

配置(仅创建时):

keon sandbox create \
  --schemas app,analytics \
  --tables  app.users,app.orders
# omit both flags → unscoped (full-branch authority)

--tables 不带 --schemas 会在客户端被拒绝为 invalid_lane

在 fork 内部,智能体 物理上无法 在通道之外写入——一次 越出通道的写入会在 fork 内以 SQLSTATE 42501(权限不足) 或 3F000(无效模式名)失败。越出通道的 读取也被拒绝。并且 提升会以 409 sandbox_out_of_lane 拒绝任何越出通道的变更。

在传输层面,沙箱携带一个 scope 对象(缺失 = 未限定范围):

"scope": { "v": 1, "schemas": ["app", "analytics"], "tables": ["app.users", "app.orders"] }

keon sandbox diff 会增加一个 lane 对象,列出任何 violations[],每个带一个 原因——out_of_lane_schemaout_of_lane_relationunresolvable_targets

两条限制:

  • 创建后不可变。 拓宽一个通道意味着丢弃 + 重建—— 没有就地拓宽。
  • 仅直接目标。 一个通道 治理经由触发器或 SECURITY DEFINER 函数达成的传递性副作用。

LINT — static risk classification

产出: 重放集中每一条语句的静态风险类别——没有 执行、没有 LLM、没有行数。纯粹是文本层面的。

没有新命令。LINT 在你已经在读的三处地方浮现:

  • keon sandbox diff 中作为 risk_report
  • keon sandbox log 中逐操作地,
  • 在沙箱传输上作为 max_risk_class

各类别,由最低到最高严重度:

additive  <  mutating  <  rewrite  <  unbounded  <  destructive

一个空的重放集分类为 none

报告结构:

{ "statements": [
    { "seq": 4, "risk_class": "unbounded",
      "reasons": ["where_trivially_true: DELETE guarded by WHERE 1=1 matches every row"] }
  ],
  "rollup": { "max_class": "unbounded", "counts": { "additive": 2, "unbounded": 1 } } }

每个原因是 <snake_case_code>: <sentence>。例如,WHERE trueWHERE 1=1 会把该语句分类为 unbounded,原因为 where_trivially_true

两件要记住的事:

  • LINT 是数据,不是决策。分类;POLICY 执行。LINT 绝不自行阻止任何东西。
  • 它向上取。 在不确定时,LINT 赋予最坏的合理类别。 因为检测是文本层面的,实际 行数并非输入——一条 无 WHEREDELETE 无论表里有一行还是十亿行都读作 destructive

BLAST — blast-radius dry-run

产出: 关于一次提升 实际会做什么 的测量证据——真实的 锁级别、真实的持锁时长、真实的行数,以及自 fork 以来的任何分歧。

keon sandbox dry-run <id>

它将沙箱的确切语句集对父分支的 两个一次性 fork 进行 重放,测量真实成本,检测分歧,然后销毁两者。它 绝不触及 main,且智能体 绝不收到 fork 凭据。 BLAST 是 建议性的——它绝不阻断提升;冲突被暴露为 数据。

完整的字段参考、冲突语义和端点列表见 Sandboxes & promote → Blast-radius dry-run

How they fit together

  • LINTBLAST 产出 数据——一个静态,一个测量。
  • POLICY 把那份数据变成一个 决策,失败即关闭,当它说 block 时无法被一次人工点击覆盖。
  • LEASHSCOPE 是智能体完全无法逾越的硬 限制—— 在 fork 中执行,在创建时冻结。

参见 Common pitfalls 了解这五道被 惯常误读的方式。