Files
MailUI4Agents/client/electron/test/components/PermissionChip.test.tsx
JianFeeeee 4050827e5c 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 两两不同,且不含「无法强制」
2026-09-11 12:04:06 +08:00

102 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(/全权/);
}
});
});