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:
@ -5,6 +5,7 @@ export const MODES: string[];
|
||||
export const DEFAULT_MODE: string;
|
||||
|
||||
export const ENFORCE_NATIVE: 'native';
|
||||
export const ENFORCE_PARTIAL: 'partial';
|
||||
export const ENFORCE_ADVISORY: 'advisory';
|
||||
|
||||
export function normalizeMode(mode: unknown): string;
|
||||
|
||||
@ -34,8 +34,16 @@ export const MODES = [MODE_PLAN, MODE_WORKSPACE, MODE_FULL];
|
||||
/** 没有显式指定时的档位。与 Gateway 的 DefaultPermissionMode 必须一致。 */
|
||||
export const DEFAULT_MODE = MODE_WORKSPACE;
|
||||
|
||||
/** 平台有原生拦截点,档位被真正执行。 */
|
||||
/** 平台有原生拦截点,档位被完整执行。 */
|
||||
export const ENFORCE_NATIVE = 'native';
|
||||
/**
|
||||
* 平台有原生拦截点,但覆盖不完整(有已知缺口)。
|
||||
*
|
||||
* 实测例子:DSH 的 Landlock 沙箱受内核 ABI 版本限制,能拦下大部分写入与命令
|
||||
* 执行,但并非全部路径。只给 native / advisory 两个取值会逼出一个假陈述:
|
||||
* 标 native 是高估(人会当成硬保证),标 advisory 是低估(它确实在拦)。
|
||||
*/
|
||||
export const ENFORCE_PARTIAL = 'partial';
|
||||
/** 平台没有拦截点,档位只写进提示词。 */
|
||||
export const ENFORCE_ADVISORY = 'advisory';
|
||||
|
||||
@ -58,11 +66,16 @@ export function normalizeMode(mode) {
|
||||
* 保守方向是 advisory 而不是 native:不能替一个没自报过的平台宣称
|
||||
* 「档位在这里是被强制的」。
|
||||
*
|
||||
* 但**显式自报的值一律原样保留**(含 partial):那是平台自己的事实陈述,
|
||||
* 把它降级到任一极端都是在替它说假话。
|
||||
*
|
||||
* @param {unknown} e
|
||||
* @returns {string}
|
||||
*/
|
||||
export function normalizeEnforcement(e) {
|
||||
return e === ENFORCE_NATIVE || e === ENFORCE_ADVISORY ? e : ENFORCE_ADVISORY;
|
||||
return e === ENFORCE_NATIVE || e === ENFORCE_PARTIAL || e === ENFORCE_ADVISORY
|
||||
? e
|
||||
: ENFORCE_ADVISORY;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -232,16 +245,19 @@ export function piBlocksOutright(mode) {
|
||||
/**
|
||||
* 给模型看的档位说明,放进提示词。
|
||||
*
|
||||
* 为什么 advisory 时措辞完全不同:那种平台(homeagent)没有任何机制阻止
|
||||
* 模型动手,所以只能把约束说成「请你遵守」而不是「你做不到」。
|
||||
* 假装它是强制的更危险 —— 模型会以为越界会被拦,于是不必自己小心。
|
||||
* 三种强制力必须说三种话:
|
||||
* - native :「会被拦下」—— 模型可以依赖它
|
||||
* - partial:「大部分会被拦下,但有已知缺口」—— 不能依赖它
|
||||
* - advisory:「平台不拦,靠你自己遵守」
|
||||
* 把 partial 当成 native 会让模型以为越界一定被拦,于是不必自己小心;
|
||||
* 当成 advisory 又会让它以为平台完全没有拦截机制,在本可以依赖的边界上过度保守。
|
||||
*
|
||||
* @param {{mode: string, enforcement: string, workspace?: string}} ctx
|
||||
* @returns {string}
|
||||
*/
|
||||
export function modeBriefing({ mode, enforcement, workspace }) {
|
||||
const m = normalizeMode(mode);
|
||||
const enforced = normalizeEnforcement(enforcement) === ENFORCE_NATIVE;
|
||||
const e = normalizeEnforcement(enforcement);
|
||||
const dir = workspace ? `\`${workspace}\`` : '本任务的工作目录';
|
||||
|
||||
if (m === MODE_FULL) {
|
||||
@ -249,26 +265,42 @@ export function modeBriefing({ mode, enforcement, workspace }) {
|
||||
}
|
||||
|
||||
if (m === MODE_PLAN) {
|
||||
return enforced
|
||||
? [
|
||||
if (e === ENFORCE_NATIVE) {
|
||||
return [
|
||||
'本任务权限档位:plan(只读)。',
|
||||
'写文件、改文件、执行命令都会被平台拦下 —— 这一档只用来查与想。',
|
||||
'请把结论、方案、需要人工执行的步骤写在回信里。需要动手请让发件人把档位改成 workspace。',
|
||||
].join('\n')
|
||||
: [
|
||||
'本任务权限档位:plan(只读)。',
|
||||
'**这个平台无法强制这一档**,所以约束靠你自己遵守:请不要写文件、改文件或执行命令。',
|
||||
'请把结论、方案、需要人工执行的步骤写在回信里。需要动手请让发件人把档位改成 workspace。',
|
||||
].join('\n');
|
||||
}
|
||||
if (e === ENFORCE_PARTIAL) {
|
||||
return [
|
||||
'本任务权限档位:plan(只读)。',
|
||||
'平台会拦截写文件、改文件与执行命令,但**拦截覆盖不完整**(沙箱能力受平台/内核限制,有已知缺口)。',
|
||||
'因此不要把「会被拦下」当成保证:请主动只查与想,把结论、方案与需要人工执行的步骤写在回信里。',
|
||||
'需要动手请让发件人把档位改成 workspace。',
|
||||
].join('\n');
|
||||
}
|
||||
return [
|
||||
'本任务权限档位:plan(只读)。',
|
||||
'**这个平台无法强制这一档**,所以约束靠你自己遵守:请不要写文件、改文件或执行命令。',
|
||||
'请把结论、方案、需要人工执行的步骤写在回信里。需要动手请让发件人把档位改成 workspace。',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
return enforced
|
||||
? [
|
||||
if (e === ENFORCE_NATIVE) {
|
||||
return [
|
||||
`本任务权限档位:workspace。可以在 ${dir} 内读写,越出该目录的写入与命令执行会先向人类请求授权。`,
|
||||
'授权可能需要等待,也可能被拒绝 —— 被拒绝时请换一条不需要越界的做法,或在回信里说明需要人工执行哪一步。',
|
||||
].join('\n')
|
||||
: [
|
||||
`本任务权限档位:workspace。请把改动限制在 ${dir} 内。`,
|
||||
'**这个平台无法强制这一档**,所以边界靠你自己遵守:需要改该目录之外的东西时,不要直接动手,先在回信里说明。',
|
||||
].join('\n');
|
||||
}
|
||||
if (e === ENFORCE_PARTIAL) {
|
||||
return [
|
||||
`本任务权限档位:workspace。请把改动限制在 ${dir} 内;越界写入与命令执行会向人类请求授权。`,
|
||||
'但**沙箱覆盖不完整**(有已知缺口),不要依赖「越界一定被拦」:请主动守住边界,需要改该目录之外的东西时先在回信里说明。',
|
||||
].join('\n');
|
||||
}
|
||||
return [
|
||||
`本任务权限档位:workspace。请把改动限制在 ${dir} 内。`,
|
||||
'**这个平台无法强制这一档**,所以边界靠你自己遵守:需要改该目录之外的东西时,不要直接动手,先在回信里说明。',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
@ -443,9 +443,15 @@ export function apply(ctx: any, config: PluginConfig): void {
|
||||
|
||||
async function beat(): Promise<void> {
|
||||
const body: Record<string, unknown> = {};
|
||||
// DSH 有真沙箱(read-only / workspace-write / danger-full-access),
|
||||
// 档位在这里是被强制执行的,不是 advisory。
|
||||
body.mode_enforcement = 'native';
|
||||
// DSH 有真沙箱(read-only / workspace-write / danger-full-access),但它对
|
||||
// 档位的拦截**覆盖不完整**:Landlock 受内核 ABI 版本限制,部分路径与系统
|
||||
// 调用未被约束(PLAN.md L5 实测结论:dsh = Landlock partial)。
|
||||
//
|
||||
// 因此报 partial 而不是 native。两个极端都是在说假话:
|
||||
// - native:人会以为 plan 档是硬保证,把它当安全边界依赖;
|
||||
// - advisory:又低估了它(确实在拦),而「平台无法强制」会让模型
|
||||
// 在本可依赖的边界上过度保守。
|
||||
body.mode_enforcement = 'partial';
|
||||
const [entries, models] = await Promise.all([collectSessions(), collectModels()]);
|
||||
if (entries) {
|
||||
body.platform_sessions = snapshotDshSessions(entries, (id) => mailDrivenSessions.has(id));
|
||||
|
||||
@ -10,7 +10,7 @@ import assert from 'node:assert/strict';
|
||||
|
||||
import {
|
||||
MODE_PLAN, MODE_WORKSPACE, MODE_FULL, MODES, DEFAULT_MODE,
|
||||
ENFORCE_NATIVE, ENFORCE_ADVISORY,
|
||||
ENFORCE_NATIVE, ENFORCE_PARTIAL, ENFORCE_ADVISORY,
|
||||
normalizeMode, normalizeEnforcement, modeAtMost, modeNeedsHuman,
|
||||
opencodePermissions, dshSandboxMode, dshApprovalPolicy,
|
||||
piGuardedTools, piBlocksOutright, modeBriefing,
|
||||
@ -30,8 +30,9 @@ test('非法档位 fail-closed 到默认档,不是 full', () => {
|
||||
});
|
||||
|
||||
test('强制力保守方向是 advisory', () => {
|
||||
assert.equal(normalizeEnforcement(ENFORCE_NATIVE), ENFORCE_NATIVE);
|
||||
assert.equal(normalizeEnforcement(ENFORCE_ADVISORY), ENFORCE_ADVISORY);
|
||||
for (const ok of [ENFORCE_NATIVE, ENFORCE_PARTIAL, ENFORCE_ADVISORY]) {
|
||||
assert.equal(normalizeEnforcement(ok), ok, `${ok} 是平台自报的事实,必须原样保留`);
|
||||
}
|
||||
for (const bad of ['', 'NATIVE', 'enforced', null, undefined]) {
|
||||
assert.equal(normalizeEnforcement(bad), ENFORCE_ADVISORY);
|
||||
}
|
||||
@ -244,3 +245,39 @@ test('脏输入不炸且按默认档', () => {
|
||||
assert.match(s, /workspace/);
|
||||
assert.match(s, /无法强制/, '脏强制力按 advisory 处理');
|
||||
});
|
||||
|
||||
// ─── partial:有拦截点但覆盖不完整 ───
|
||||
//
|
||||
// 这个取值的全部意义就是「不许说假话」。因此判据不是「措辞好看」,
|
||||
// 而是它与两个极端的说法**都不同**,且明确交代「不要依赖会被拦」。
|
||||
test('partial 三档措辞两两不同(不能与任一极端混同)', () => {
|
||||
for (const mode of [MODE_PLAN, MODE_WORKSPACE]) {
|
||||
const nat = modeBriefing({ mode, enforcement: ENFORCE_NATIVE });
|
||||
const par = modeBriefing({ mode, enforcement: ENFORCE_PARTIAL });
|
||||
const adv = modeBriefing({ mode, enforcement: ENFORCE_ADVISORY });
|
||||
assert.notEqual(par, nat, `${mode}: partial 不能与 native 同措辞(那是高估)`);
|
||||
assert.notEqual(par, adv, `${mode}: partial 不能与 advisory 同措辞(那是低估)`);
|
||||
}
|
||||
});
|
||||
|
||||
test('partial 必须交代「覆盖不完整」且不得说「无法强制」', () => {
|
||||
for (const mode of [MODE_PLAN, MODE_WORKSPACE]) {
|
||||
const par = modeBriefing({ mode, enforcement: ENFORCE_PARTIAL });
|
||||
assert.match(par, /不完整|缺口/, `${mode}: 必须说清覆盖不完整`);
|
||||
assert.equal(/无法强制/.test(par), false,
|
||||
`${mode}: partial 平台确实在拦,「无法强制」是错的`);
|
||||
}
|
||||
});
|
||||
|
||||
test('partial 不能把「会被拦下」当成保证', () => {
|
||||
const plan = modeBriefing({ mode: MODE_PLAN, enforcement: ENFORCE_PARTIAL });
|
||||
// native 版说「都会被平台拦下」,partial 版必须收回这个承诺
|
||||
assert.equal(/都会被平台拦下/.test(plan), false,
|
||||
'partial 下承诺「都会拦下」会让模型不必自己小心 —— 那是 native 才成立的话');
|
||||
assert.match(plan, /不要依赖|主动/, '必须给出「主动自律」的指引');
|
||||
});
|
||||
|
||||
test('partial 与 native 一样要求把方案写在回信里(出路不能消失)', () => {
|
||||
const s = modeBriefing({ mode: MODE_PLAN, enforcement: ENFORCE_PARTIAL });
|
||||
assert.match(s, /回信/);
|
||||
});
|
||||
|
||||
@ -34,8 +34,16 @@ export const MODES = [MODE_PLAN, MODE_WORKSPACE, MODE_FULL];
|
||||
/** 没有显式指定时的档位。与 Gateway 的 DefaultPermissionMode 必须一致。 */
|
||||
export const DEFAULT_MODE = MODE_WORKSPACE;
|
||||
|
||||
/** 平台有原生拦截点,档位被真正执行。 */
|
||||
/** 平台有原生拦截点,档位被完整执行。 */
|
||||
export const ENFORCE_NATIVE = 'native';
|
||||
/**
|
||||
* 平台有原生拦截点,但覆盖不完整(有已知缺口)。
|
||||
*
|
||||
* 实测例子:DSH 的 Landlock 沙箱受内核 ABI 版本限制,能拦下大部分写入与命令
|
||||
* 执行,但并非全部路径。只给 native / advisory 两个取值会逼出一个假陈述:
|
||||
* 标 native 是高估(人会当成硬保证),标 advisory 是低估(它确实在拦)。
|
||||
*/
|
||||
export const ENFORCE_PARTIAL = 'partial';
|
||||
/** 平台没有拦截点,档位只写进提示词。 */
|
||||
export const ENFORCE_ADVISORY = 'advisory';
|
||||
|
||||
@ -58,11 +66,16 @@ export function normalizeMode(mode) {
|
||||
* 保守方向是 advisory 而不是 native:不能替一个没自报过的平台宣称
|
||||
* 「档位在这里是被强制的」。
|
||||
*
|
||||
* 但**显式自报的值一律原样保留**(含 partial):那是平台自己的事实陈述,
|
||||
* 把它降级到任一极端都是在替它说假话。
|
||||
*
|
||||
* @param {unknown} e
|
||||
* @returns {string}
|
||||
*/
|
||||
export function normalizeEnforcement(e) {
|
||||
return e === ENFORCE_NATIVE || e === ENFORCE_ADVISORY ? e : ENFORCE_ADVISORY;
|
||||
return e === ENFORCE_NATIVE || e === ENFORCE_PARTIAL || e === ENFORCE_ADVISORY
|
||||
? e
|
||||
: ENFORCE_ADVISORY;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -232,16 +245,19 @@ export function piBlocksOutright(mode) {
|
||||
/**
|
||||
* 给模型看的档位说明,放进提示词。
|
||||
*
|
||||
* 为什么 advisory 时措辞完全不同:那种平台(homeagent)没有任何机制阻止
|
||||
* 模型动手,所以只能把约束说成「请你遵守」而不是「你做不到」。
|
||||
* 假装它是强制的更危险 —— 模型会以为越界会被拦,于是不必自己小心。
|
||||
* 三种强制力必须说三种话:
|
||||
* - native :「会被拦下」—— 模型可以依赖它
|
||||
* - partial:「大部分会被拦下,但有已知缺口」—— 不能依赖它
|
||||
* - advisory:「平台不拦,靠你自己遵守」
|
||||
* 把 partial 当成 native 会让模型以为越界一定被拦,于是不必自己小心;
|
||||
* 当成 advisory 又会让它以为平台完全没有拦截机制,在本可以依赖的边界上过度保守。
|
||||
*
|
||||
* @param {{mode: string, enforcement: string, workspace?: string}} ctx
|
||||
* @returns {string}
|
||||
*/
|
||||
export function modeBriefing({ mode, enforcement, workspace }) {
|
||||
const m = normalizeMode(mode);
|
||||
const enforced = normalizeEnforcement(enforcement) === ENFORCE_NATIVE;
|
||||
const e = normalizeEnforcement(enforcement);
|
||||
const dir = workspace ? `\`${workspace}\`` : '本任务的工作目录';
|
||||
|
||||
if (m === MODE_FULL) {
|
||||
@ -249,26 +265,42 @@ export function modeBriefing({ mode, enforcement, workspace }) {
|
||||
}
|
||||
|
||||
if (m === MODE_PLAN) {
|
||||
return enforced
|
||||
? [
|
||||
if (e === ENFORCE_NATIVE) {
|
||||
return [
|
||||
'本任务权限档位:plan(只读)。',
|
||||
'写文件、改文件、执行命令都会被平台拦下 —— 这一档只用来查与想。',
|
||||
'请把结论、方案、需要人工执行的步骤写在回信里。需要动手请让发件人把档位改成 workspace。',
|
||||
].join('\n')
|
||||
: [
|
||||
'本任务权限档位:plan(只读)。',
|
||||
'**这个平台无法强制这一档**,所以约束靠你自己遵守:请不要写文件、改文件或执行命令。',
|
||||
'请把结论、方案、需要人工执行的步骤写在回信里。需要动手请让发件人把档位改成 workspace。',
|
||||
].join('\n');
|
||||
}
|
||||
if (e === ENFORCE_PARTIAL) {
|
||||
return [
|
||||
'本任务权限档位:plan(只读)。',
|
||||
'平台会拦截写文件、改文件与执行命令,但**拦截覆盖不完整**(沙箱能力受平台/内核限制,有已知缺口)。',
|
||||
'因此不要把「会被拦下」当成保证:请主动只查与想,把结论、方案与需要人工执行的步骤写在回信里。',
|
||||
'需要动手请让发件人把档位改成 workspace。',
|
||||
].join('\n');
|
||||
}
|
||||
return [
|
||||
'本任务权限档位:plan(只读)。',
|
||||
'**这个平台无法强制这一档**,所以约束靠你自己遵守:请不要写文件、改文件或执行命令。',
|
||||
'请把结论、方案、需要人工执行的步骤写在回信里。需要动手请让发件人把档位改成 workspace。',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
return enforced
|
||||
? [
|
||||
if (e === ENFORCE_NATIVE) {
|
||||
return [
|
||||
`本任务权限档位:workspace。可以在 ${dir} 内读写,越出该目录的写入与命令执行会先向人类请求授权。`,
|
||||
'授权可能需要等待,也可能被拒绝 —— 被拒绝时请换一条不需要越界的做法,或在回信里说明需要人工执行哪一步。',
|
||||
].join('\n')
|
||||
: [
|
||||
`本任务权限档位:workspace。请把改动限制在 ${dir} 内。`,
|
||||
'**这个平台无法强制这一档**,所以边界靠你自己遵守:需要改该目录之外的东西时,不要直接动手,先在回信里说明。',
|
||||
].join('\n');
|
||||
}
|
||||
if (e === ENFORCE_PARTIAL) {
|
||||
return [
|
||||
`本任务权限档位:workspace。请把改动限制在 ${dir} 内;越界写入与命令执行会向人类请求授权。`,
|
||||
'但**沙箱覆盖不完整**(有已知缺口),不要依赖「越界一定被拦」:请主动守住边界,需要改该目录之外的东西时先在回信里说明。',
|
||||
].join('\n');
|
||||
}
|
||||
return [
|
||||
`本任务权限档位:workspace。请把改动限制在 ${dir} 内。`,
|
||||
'**这个平台无法强制这一档**,所以边界靠你自己遵守:需要改该目录之外的东西时,不要直接动手,先在回信里说明。',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import assert from 'node:assert/strict';
|
||||
|
||||
import {
|
||||
MODE_PLAN, MODE_WORKSPACE, MODE_FULL, MODES, DEFAULT_MODE,
|
||||
ENFORCE_NATIVE, ENFORCE_ADVISORY,
|
||||
ENFORCE_NATIVE, ENFORCE_PARTIAL, ENFORCE_ADVISORY,
|
||||
normalizeMode, normalizeEnforcement, modeAtMost, modeNeedsHuman,
|
||||
opencodePermissions, dshSandboxMode, dshApprovalPolicy,
|
||||
piGuardedTools, piBlocksOutright, modeBriefing,
|
||||
@ -30,8 +30,9 @@ test('非法档位 fail-closed 到默认档,不是 full', () => {
|
||||
});
|
||||
|
||||
test('强制力保守方向是 advisory', () => {
|
||||
assert.equal(normalizeEnforcement(ENFORCE_NATIVE), ENFORCE_NATIVE);
|
||||
assert.equal(normalizeEnforcement(ENFORCE_ADVISORY), ENFORCE_ADVISORY);
|
||||
for (const ok of [ENFORCE_NATIVE, ENFORCE_PARTIAL, ENFORCE_ADVISORY]) {
|
||||
assert.equal(normalizeEnforcement(ok), ok, `${ok} 是平台自报的事实,必须原样保留`);
|
||||
}
|
||||
for (const bad of ['', 'NATIVE', 'enforced', null, undefined]) {
|
||||
assert.equal(normalizeEnforcement(bad), ENFORCE_ADVISORY);
|
||||
}
|
||||
@ -244,3 +245,39 @@ test('脏输入不炸且按默认档', () => {
|
||||
assert.match(s, /workspace/);
|
||||
assert.match(s, /无法强制/, '脏强制力按 advisory 处理');
|
||||
});
|
||||
|
||||
// ─── partial:有拦截点但覆盖不完整 ───
|
||||
//
|
||||
// 这个取值的全部意义就是「不许说假话」。因此判据不是「措辞好看」,
|
||||
// 而是它与两个极端的说法**都不同**,且明确交代「不要依赖会被拦」。
|
||||
test('partial 三档措辞两两不同(不能与任一极端混同)', () => {
|
||||
for (const mode of [MODE_PLAN, MODE_WORKSPACE]) {
|
||||
const nat = modeBriefing({ mode, enforcement: ENFORCE_NATIVE });
|
||||
const par = modeBriefing({ mode, enforcement: ENFORCE_PARTIAL });
|
||||
const adv = modeBriefing({ mode, enforcement: ENFORCE_ADVISORY });
|
||||
assert.notEqual(par, nat, `${mode}: partial 不能与 native 同措辞(那是高估)`);
|
||||
assert.notEqual(par, adv, `${mode}: partial 不能与 advisory 同措辞(那是低估)`);
|
||||
}
|
||||
});
|
||||
|
||||
test('partial 必须交代「覆盖不完整」且不得说「无法强制」', () => {
|
||||
for (const mode of [MODE_PLAN, MODE_WORKSPACE]) {
|
||||
const par = modeBriefing({ mode, enforcement: ENFORCE_PARTIAL });
|
||||
assert.match(par, /不完整|缺口/, `${mode}: 必须说清覆盖不完整`);
|
||||
assert.equal(/无法强制/.test(par), false,
|
||||
`${mode}: partial 平台确实在拦,「无法强制」是错的`);
|
||||
}
|
||||
});
|
||||
|
||||
test('partial 不能把「会被拦下」当成保证', () => {
|
||||
const plan = modeBriefing({ mode: MODE_PLAN, enforcement: ENFORCE_PARTIAL });
|
||||
// native 版说「都会被平台拦下」,partial 版必须收回这个承诺
|
||||
assert.equal(/都会被平台拦下/.test(plan), false,
|
||||
'partial 下承诺「都会拦下」会让模型不必自己小心 —— 那是 native 才成立的话');
|
||||
assert.match(plan, /不要依赖|主动/, '必须给出「主动自律」的指引');
|
||||
});
|
||||
|
||||
test('partial 与 native 一样要求把方案写在回信里(出路不能消失)', () => {
|
||||
const s = modeBriefing({ mode: MODE_PLAN, enforcement: ENFORCE_PARTIAL });
|
||||
assert.match(s, /回信/);
|
||||
});
|
||||
|
||||
@ -34,8 +34,16 @@ export const MODES = [MODE_PLAN, MODE_WORKSPACE, MODE_FULL];
|
||||
/** 没有显式指定时的档位。与 Gateway 的 DefaultPermissionMode 必须一致。 */
|
||||
export const DEFAULT_MODE = MODE_WORKSPACE;
|
||||
|
||||
/** 平台有原生拦截点,档位被真正执行。 */
|
||||
/** 平台有原生拦截点,档位被完整执行。 */
|
||||
export const ENFORCE_NATIVE = 'native';
|
||||
/**
|
||||
* 平台有原生拦截点,但覆盖不完整(有已知缺口)。
|
||||
*
|
||||
* 实测例子:DSH 的 Landlock 沙箱受内核 ABI 版本限制,能拦下大部分写入与命令
|
||||
* 执行,但并非全部路径。只给 native / advisory 两个取值会逼出一个假陈述:
|
||||
* 标 native 是高估(人会当成硬保证),标 advisory 是低估(它确实在拦)。
|
||||
*/
|
||||
export const ENFORCE_PARTIAL = 'partial';
|
||||
/** 平台没有拦截点,档位只写进提示词。 */
|
||||
export const ENFORCE_ADVISORY = 'advisory';
|
||||
|
||||
@ -58,11 +66,16 @@ export function normalizeMode(mode) {
|
||||
* 保守方向是 advisory 而不是 native:不能替一个没自报过的平台宣称
|
||||
* 「档位在这里是被强制的」。
|
||||
*
|
||||
* 但**显式自报的值一律原样保留**(含 partial):那是平台自己的事实陈述,
|
||||
* 把它降级到任一极端都是在替它说假话。
|
||||
*
|
||||
* @param {unknown} e
|
||||
* @returns {string}
|
||||
*/
|
||||
export function normalizeEnforcement(e) {
|
||||
return e === ENFORCE_NATIVE || e === ENFORCE_ADVISORY ? e : ENFORCE_ADVISORY;
|
||||
return e === ENFORCE_NATIVE || e === ENFORCE_PARTIAL || e === ENFORCE_ADVISORY
|
||||
? e
|
||||
: ENFORCE_ADVISORY;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -232,16 +245,19 @@ export function piBlocksOutright(mode) {
|
||||
/**
|
||||
* 给模型看的档位说明,放进提示词。
|
||||
*
|
||||
* 为什么 advisory 时措辞完全不同:那种平台(homeagent)没有任何机制阻止
|
||||
* 模型动手,所以只能把约束说成「请你遵守」而不是「你做不到」。
|
||||
* 假装它是强制的更危险 —— 模型会以为越界会被拦,于是不必自己小心。
|
||||
* 三种强制力必须说三种话:
|
||||
* - native :「会被拦下」—— 模型可以依赖它
|
||||
* - partial:「大部分会被拦下,但有已知缺口」—— 不能依赖它
|
||||
* - advisory:「平台不拦,靠你自己遵守」
|
||||
* 把 partial 当成 native 会让模型以为越界一定被拦,于是不必自己小心;
|
||||
* 当成 advisory 又会让它以为平台完全没有拦截机制,在本可以依赖的边界上过度保守。
|
||||
*
|
||||
* @param {{mode: string, enforcement: string, workspace?: string}} ctx
|
||||
* @returns {string}
|
||||
*/
|
||||
export function modeBriefing({ mode, enforcement, workspace }) {
|
||||
const m = normalizeMode(mode);
|
||||
const enforced = normalizeEnforcement(enforcement) === ENFORCE_NATIVE;
|
||||
const e = normalizeEnforcement(enforcement);
|
||||
const dir = workspace ? `\`${workspace}\`` : '本任务的工作目录';
|
||||
|
||||
if (m === MODE_FULL) {
|
||||
@ -249,26 +265,42 @@ export function modeBriefing({ mode, enforcement, workspace }) {
|
||||
}
|
||||
|
||||
if (m === MODE_PLAN) {
|
||||
return enforced
|
||||
? [
|
||||
if (e === ENFORCE_NATIVE) {
|
||||
return [
|
||||
'本任务权限档位:plan(只读)。',
|
||||
'写文件、改文件、执行命令都会被平台拦下 —— 这一档只用来查与想。',
|
||||
'请把结论、方案、需要人工执行的步骤写在回信里。需要动手请让发件人把档位改成 workspace。',
|
||||
].join('\n')
|
||||
: [
|
||||
'本任务权限档位:plan(只读)。',
|
||||
'**这个平台无法强制这一档**,所以约束靠你自己遵守:请不要写文件、改文件或执行命令。',
|
||||
'请把结论、方案、需要人工执行的步骤写在回信里。需要动手请让发件人把档位改成 workspace。',
|
||||
].join('\n');
|
||||
}
|
||||
if (e === ENFORCE_PARTIAL) {
|
||||
return [
|
||||
'本任务权限档位:plan(只读)。',
|
||||
'平台会拦截写文件、改文件与执行命令,但**拦截覆盖不完整**(沙箱能力受平台/内核限制,有已知缺口)。',
|
||||
'因此不要把「会被拦下」当成保证:请主动只查与想,把结论、方案与需要人工执行的步骤写在回信里。',
|
||||
'需要动手请让发件人把档位改成 workspace。',
|
||||
].join('\n');
|
||||
}
|
||||
return [
|
||||
'本任务权限档位:plan(只读)。',
|
||||
'**这个平台无法强制这一档**,所以约束靠你自己遵守:请不要写文件、改文件或执行命令。',
|
||||
'请把结论、方案、需要人工执行的步骤写在回信里。需要动手请让发件人把档位改成 workspace。',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
return enforced
|
||||
? [
|
||||
if (e === ENFORCE_NATIVE) {
|
||||
return [
|
||||
`本任务权限档位:workspace。可以在 ${dir} 内读写,越出该目录的写入与命令执行会先向人类请求授权。`,
|
||||
'授权可能需要等待,也可能被拒绝 —— 被拒绝时请换一条不需要越界的做法,或在回信里说明需要人工执行哪一步。',
|
||||
].join('\n')
|
||||
: [
|
||||
`本任务权限档位:workspace。请把改动限制在 ${dir} 内。`,
|
||||
'**这个平台无法强制这一档**,所以边界靠你自己遵守:需要改该目录之外的东西时,不要直接动手,先在回信里说明。',
|
||||
].join('\n');
|
||||
}
|
||||
if (e === ENFORCE_PARTIAL) {
|
||||
return [
|
||||
`本任务权限档位:workspace。请把改动限制在 ${dir} 内;越界写入与命令执行会向人类请求授权。`,
|
||||
'但**沙箱覆盖不完整**(有已知缺口),不要依赖「越界一定被拦」:请主动守住边界,需要改该目录之外的东西时先在回信里说明。',
|
||||
].join('\n');
|
||||
}
|
||||
return [
|
||||
`本任务权限档位:workspace。请把改动限制在 ${dir} 内。`,
|
||||
'**这个平台无法强制这一档**,所以边界靠你自己遵守:需要改该目录之外的东西时,不要直接动手,先在回信里说明。',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import assert from 'node:assert/strict';
|
||||
|
||||
import {
|
||||
MODE_PLAN, MODE_WORKSPACE, MODE_FULL, MODES, DEFAULT_MODE,
|
||||
ENFORCE_NATIVE, ENFORCE_ADVISORY,
|
||||
ENFORCE_NATIVE, ENFORCE_PARTIAL, ENFORCE_ADVISORY,
|
||||
normalizeMode, normalizeEnforcement, modeAtMost, modeNeedsHuman,
|
||||
opencodePermissions, dshSandboxMode, dshApprovalPolicy,
|
||||
piGuardedTools, piBlocksOutright, modeBriefing,
|
||||
@ -30,8 +30,9 @@ test('非法档位 fail-closed 到默认档,不是 full', () => {
|
||||
});
|
||||
|
||||
test('强制力保守方向是 advisory', () => {
|
||||
assert.equal(normalizeEnforcement(ENFORCE_NATIVE), ENFORCE_NATIVE);
|
||||
assert.equal(normalizeEnforcement(ENFORCE_ADVISORY), ENFORCE_ADVISORY);
|
||||
for (const ok of [ENFORCE_NATIVE, ENFORCE_PARTIAL, ENFORCE_ADVISORY]) {
|
||||
assert.equal(normalizeEnforcement(ok), ok, `${ok} 是平台自报的事实,必须原样保留`);
|
||||
}
|
||||
for (const bad of ['', 'NATIVE', 'enforced', null, undefined]) {
|
||||
assert.equal(normalizeEnforcement(bad), ENFORCE_ADVISORY);
|
||||
}
|
||||
@ -244,3 +245,39 @@ test('脏输入不炸且按默认档', () => {
|
||||
assert.match(s, /workspace/);
|
||||
assert.match(s, /无法强制/, '脏强制力按 advisory 处理');
|
||||
});
|
||||
|
||||
// ─── partial:有拦截点但覆盖不完整 ───
|
||||
//
|
||||
// 这个取值的全部意义就是「不许说假话」。因此判据不是「措辞好看」,
|
||||
// 而是它与两个极端的说法**都不同**,且明确交代「不要依赖会被拦」。
|
||||
test('partial 三档措辞两两不同(不能与任一极端混同)', () => {
|
||||
for (const mode of [MODE_PLAN, MODE_WORKSPACE]) {
|
||||
const nat = modeBriefing({ mode, enforcement: ENFORCE_NATIVE });
|
||||
const par = modeBriefing({ mode, enforcement: ENFORCE_PARTIAL });
|
||||
const adv = modeBriefing({ mode, enforcement: ENFORCE_ADVISORY });
|
||||
assert.notEqual(par, nat, `${mode}: partial 不能与 native 同措辞(那是高估)`);
|
||||
assert.notEqual(par, adv, `${mode}: partial 不能与 advisory 同措辞(那是低估)`);
|
||||
}
|
||||
});
|
||||
|
||||
test('partial 必须交代「覆盖不完整」且不得说「无法强制」', () => {
|
||||
for (const mode of [MODE_PLAN, MODE_WORKSPACE]) {
|
||||
const par = modeBriefing({ mode, enforcement: ENFORCE_PARTIAL });
|
||||
assert.match(par, /不完整|缺口/, `${mode}: 必须说清覆盖不完整`);
|
||||
assert.equal(/无法强制/.test(par), false,
|
||||
`${mode}: partial 平台确实在拦,「无法强制」是错的`);
|
||||
}
|
||||
});
|
||||
|
||||
test('partial 不能把「会被拦下」当成保证', () => {
|
||||
const plan = modeBriefing({ mode: MODE_PLAN, enforcement: ENFORCE_PARTIAL });
|
||||
// native 版说「都会被平台拦下」,partial 版必须收回这个承诺
|
||||
assert.equal(/都会被平台拦下/.test(plan), false,
|
||||
'partial 下承诺「都会拦下」会让模型不必自己小心 —— 那是 native 才成立的话');
|
||||
assert.match(plan, /不要依赖|主动/, '必须给出「主动自律」的指引');
|
||||
});
|
||||
|
||||
test('partial 与 native 一样要求把方案写在回信里(出路不能消失)', () => {
|
||||
const s = modeBriefing({ mode: MODE_PLAN, enforcement: ENFORCE_PARTIAL });
|
||||
assert.match(s, /回信/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user