feat(permission): 新增第三种强制力 partial —— DSH 如实自报,不再冒充 native
# 问题
审计发现 DSH 自报 mode_enforcement=native,而实测它的 Landlock 沙箱受内核 ABI
版本限制、拦截覆盖不完整(PLAN.md L5 自己写的就是 dsh = Landlock partial)。
只有 native / advisory 两个取值时,这个平台无论标哪个都是在说假话:
- 标 native → 人会以为 plan 档是硬保证,把它当安全边界依赖;
- 标 advisory → 又低估了它(确实在拦),而「平台无法强制」会让模型
在本可依赖的边界上过度保守。
多一个取值比多说一句假话便宜。
# 改动
- Go models:EnforcementPartial = "partial",ValidEnforcement 接受它;
NormalizeEnforcement 对显式自报值一律原样保留(partial 降级到任一极端都是假话),
未知值仍然 fail-closed 到 advisory。
- 三桥共用 lib/permission-mode.js(逐字节同源):ENFORCE_PARTIAL +
modeBriefing 三态措辞。partial 版必须同时做到两件事:
说清「覆盖不完整」,并收回 native 那句「都会被平台拦下」的承诺
—— 否则模型会以为越界一定被拦,于是不必自己小心。
- 前端 PermissionChip:三个点形区分(实心 / 靶心 / 空心)+ 三套 tooltip 文案;
认不出的强制力按 advisory(与后端同方向)。
- DSH 插件心跳改报 partial。
- 顺带修正活跃 DSH 会话的历史快照:那批 native 是插件当时的**误报**,
不是能力变化,因此把 status<>'archived' 的 dsh 会话改为 partial;
归档会话按设计保留(不重写已结束的历史)。改前已 sqlite3 .backup 备份。
# 验证
- agents.mode_enforcement:dsh 由 native 变为 partial(心跳生效)
- Go 全量、三桥插件 320/362/409、前端 196 全绿(新增 PermissionChip 11 例)
- 三桥共用模块同源校验通过
- 关键判据:partial 的措辞与 native/advisory 两两不同,且不含「无法强制」
This commit is contained in:
@ -3,17 +3,22 @@
|
||||
*
|
||||
* 两个字段必须成对显示:
|
||||
* permission_mode 档位(plan / workspace / full)——「要求什么」
|
||||
* permission_enforcement 强制力(native / advisory)——「平台实际做到了什么」
|
||||
* permission_enforcement 强制力(native / partial / advisory)——「平台实际做到了什么」
|
||||
*
|
||||
* 为什么强制力也要上界面:只显示档位会让人以为 plan 档管住了 homeagent,
|
||||
* 而 homeagent 没有工具拦截点、档位只是提示词建议(advisory)。
|
||||
* 差异可见才符合 I-5(失败必须当场可见)。
|
||||
*
|
||||
* 为什么必须有三态而不是两态:DSH 有真沙箱,但 Landlock 受内核 ABI 限制、
|
||||
* 拦截覆盖不完整。把这种平台标成 native 是**高估**(人会把它当硬保证),
|
||||
* 标成 advisory 是**低估**(它确实在拦,「平台不强制」是假话)。
|
||||
* 三个点形(实心 / 靶心 / 空心)让差异在列表里一眼可见。
|
||||
*/
|
||||
|
||||
export interface PermissionChipProps {
|
||||
/** 档位:plan / workspace / full */
|
||||
mode?: string;
|
||||
/** 实际强制力:native / advisory */
|
||||
/** 实际强制力:native / partial / advisory */
|
||||
enforcement?: string;
|
||||
/** 紧凑模式(卡片上用);默认常规(详情页用) */
|
||||
compact?: boolean;
|
||||
@ -25,21 +30,39 @@ const MODE_LABEL: Record<string, string> = {
|
||||
full: '全权',
|
||||
};
|
||||
|
||||
/** 档位 → 文字说明(tooltip 用) */
|
||||
/** 强制力取值 → 中文短标签(tooltip 用) */
|
||||
export const ENFORCEMENT_LABEL: Record<string, string> = {
|
||||
native: '平台强制',
|
||||
partial: '平台强制(覆盖不完整)',
|
||||
advisory: '仅提示',
|
||||
};
|
||||
|
||||
/** 归一化强制力:认不出的值按 advisory(保守方向,与后端 Normalize 同语义) */
|
||||
function normalizeEnforcement(e?: string): string {
|
||||
return e === 'native' || e === 'partial' || e === 'advisory' ? e : 'advisory';
|
||||
}
|
||||
|
||||
/** 档位 → 文字说明(tooltip 用)。三种强制力必须给出三种话。 */
|
||||
export function permissionModeHint(mode?: string, enforcement?: string): string {
|
||||
const enforced = enforcement === 'native';
|
||||
const e = normalizeEnforcement(enforcement);
|
||||
switch (mode) {
|
||||
case 'plan':
|
||||
return enforced
|
||||
? 'plan 档:只读。写/改/执行会被平台强制拦下,本档只用来查与想。'
|
||||
: 'plan 档:只读(advisory,平台不强制)。请把结论写在回信里。';
|
||||
if (e === 'native') return 'plan 档:只读。写/改/执行会被平台强制拦下,本档只用来查与想。';
|
||||
if (e === 'partial') {
|
||||
return 'plan 档:只读。平台会拦截写/改/执行,但覆盖不完整(沙箱能力受限,有已知缺口)——'
|
||||
+ '不要把「会被拦下」当保证,请把结论写在回信里。';
|
||||
}
|
||||
return 'plan 档:只读(advisory,平台不强制)。请把结论写在回信里。';
|
||||
case 'full':
|
||||
return 'full 档:全权。工具调用不需额外授权。';
|
||||
case 'workspace':
|
||||
default:
|
||||
return enforced
|
||||
? 'workspace 档:目录内可动,越界需经授权。'
|
||||
: 'workspace 档(advisory,平台不强制)。请把改动限制在工作目录内。';
|
||||
if (e === 'native') return 'workspace 档:目录内可动,越界需经授权。';
|
||||
if (e === 'partial') {
|
||||
return 'workspace 档:目录内可动,越界会请求授权 —— 但沙箱覆盖不完整(有已知缺口),'
|
||||
+ '请主动把改动限制在工作目录内。';
|
||||
}
|
||||
return 'workspace 档(advisory,平台不强制)。请把改动限制在工作目录内。';
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,9 +71,9 @@ export default function PermissionChip({ mode, enforcement, compact }: Permissio
|
||||
const normalized = mode || '';
|
||||
if (!['plan', 'workspace', 'full'].includes(normalized)) return null;
|
||||
|
||||
const enforced = enforcement === 'native';
|
||||
const e = normalizeEnforcement(enforcement);
|
||||
|
||||
// 配色按档位:plan 用蓝(只读),workspace 用黄(有边界的动),full 用红/橙(全权)
|
||||
// 配色按档位:plan 用蓝(只读),workspace 用绿(有边界的动),full 用橙(全权)
|
||||
const color = normalized === 'plan'
|
||||
? 'bg-blue-50 text-blue-700 border-blue-200'
|
||||
: normalized === 'full'
|
||||
@ -62,18 +85,26 @@ export default function PermissionChip({ mode, enforcement, compact }: Permissio
|
||||
|
||||
return (
|
||||
<span
|
||||
title={hint + (enforcement ? `(强制力:${enforcement === 'native' ? '平台强制' : '仅提示' })` : '')}
|
||||
title={`${hint}(强制力:${ENFORCEMENT_LABEL[e]})`}
|
||||
className={`inline-flex items-center gap-1 rounded border font-medium ${color} ${
|
||||
compact ? 'px-1 text-[10px]' : 'px-1.5 text-xs'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
{enforced ? (
|
||||
// native:平台强制 —— 实心圆点
|
||||
{e === 'native' ? (
|
||||
// native:完整强制 —— 实心圆点
|
||||
<span className="inline-block w-1.5 h-1.5 rounded-full bg-current" />
|
||||
) : e === 'partial' ? (
|
||||
// partial:有拦截但覆盖不完整 —— 环内实心(靶心),与实心/空心都不同形
|
||||
<span
|
||||
data-testid="chip-partial"
|
||||
className="inline-flex items-center justify-center w-1.5 h-1.5 rounded-full border border-current"
|
||||
>
|
||||
<span className="block w-[3px] h-[3px] rounded-full bg-current" />
|
||||
</span>
|
||||
) : (
|
||||
// advisory:仅提示 —— 空心圆点
|
||||
<span className="inline-block w-1.5 h-1.5 rounded-full border border-current" />
|
||||
<span data-testid="chip-advisory" className="inline-block w-1.5 h-1.5 rounded-full border border-current" />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user