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:
2026-09-11 12:04:06 +08:00
parent 429149e118
commit 4050827e5c
12 changed files with 455 additions and 92 deletions

View File

@ -0,0 +1,101 @@
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import React from 'react';
import PermissionChip, {
permissionModeHint,
ENFORCEMENT_LABEL,
} from '../../src/components/PermissionChip';
/**
* 权限档位徽标。
*
* 判据集中在「三个强制力必须可区分」——因为把它显示错的代价不对称:
* - 高估(partial 显示成 native):人以为档位是硬保证,把它当安全边界依赖
* - 低估(partial 显示成 advisory):人以为平台完全不拦,在能依赖的边界上过度保守
* 这张徽标是这两个事实唯一的对外呈现,所以点形与文案都必须区分。
*/
describe('PermissionChip 档位显示', () => {
it('档位为空或非法时不渲染(人→人的信、旧会话)', () => {
const { container: c1 } = render(React.createElement(PermissionChip, {}));
expect(c1.firstChild).toBeNull();
const { container: c2 } = render(React.createElement(PermissionChip, { mode: 'nonsense' }));
expect(c2.firstChild).toBeNull();
});
it('三个档位各有中文短标签', () => {
for (const [mode, label] of [['plan', '只读'], ['workspace', '目录内'], ['full', '全权']]) {
const { unmount } = render(
React.createElement(PermissionChip, { mode, enforcement: 'native' })
);
expect(screen.getByText(label)).toBeInTheDocument();
unmount();
}
});
});
describe('PermissionChip 强制力三态', () => {
it('native 用实心点,不带 partial/advisory 的标记', () => {
render(React.createElement(PermissionChip, { mode: 'plan', enforcement: 'native' }));
expect(screen.queryByTestId('chip-partial')).toBeNull();
expect(screen.queryByTestId('chip-advisory')).toBeNull();
});
it('partial 渲染为靶心(与实心、空心都不同形)', () => {
render(React.createElement(PermissionChip, { mode: 'plan', enforcement: 'partial' }));
expect(screen.getByTestId('chip-partial')).toBeInTheDocument();
expect(screen.queryByTestId('chip-advisory')).toBeNull();
});
it('advisory 渲染为空心点', () => {
render(React.createElement(PermissionChip, { mode: 'plan', enforcement: 'advisory' }));
expect(screen.getByTestId('chip-advisory')).toBeInTheDocument();
expect(screen.queryByTestId('chip-partial')).toBeNull();
});
it('认不出的强制力按 advisory 处理(保守方向,与后端 Normalize 同语义)', () => {
render(React.createElement(PermissionChip, { mode: 'plan', enforcement: 'PARTIAL' }));
expect(screen.getByTestId('chip-advisory')).toBeInTheDocument();
expect(screen.queryByTestId('chip-partial')).toBeNull();
});
it('tooltip 里给出强制力的中文标签', () => {
render(React.createElement(PermissionChip, { mode: 'workspace', enforcement: 'partial' }));
const chip = screen.getByText('目录内').closest('span');
expect(chip?.getAttribute('title')).toContain(ENFORCEMENT_LABEL.partial);
});
});
describe('permissionModeHint 三态文案', () => {
it('三种强制力的说明两两不同', () => {
for (const mode of ['plan', 'workspace']) {
const nat = permissionModeHint(mode, 'native');
const par = permissionModeHint(mode, 'partial');
const adv = permissionModeHint(mode, 'advisory');
expect(par).not.toBe(nat);
expect(par).not.toBe(adv);
}
});
it('partial 必须说明覆盖不完整,且不能说「平台不强制」', () => {
for (const mode of ['plan', 'workspace']) {
const par = permissionModeHint(mode, 'partial');
// 「不完整」或「缺口」必须出现:否则人无法知道拦截不可完全依赖
expect(par).toMatch(/不完整|缺口/);
// 「平台不强制」是 advisory 的说法,对 partial 是假话
expect(par).not.toMatch(/平台不强制/);
}
});
it('advisory 仍明说平台不强制', () => {
expect(permissionModeHint('plan', 'advisory')).toMatch(/平台不强制/);
expect(permissionModeHint('workspace', 'advisory')).toMatch(/平台不强制/);
});
it('full 档不提强制力差异(全权档本来就不需要授权)', () => {
for (const e of ['native', 'partial', 'advisory']) {
expect(permissionModeHint('full', e)).toMatch(/全权/);
}
});
});