cordis-plugin-sofagent-audit
变更机器审阅 + 验收硬门禁——24 规则 + git diff 硬证据 + Turn 停止验收判定(验收不过不放行,开关独立可关)(seam: tools/result + tools/pre-execute + fs/write-intent + agent/turn-stopping)——桥接 @sofagent/audit runRules——DSH(DeepSeek Harness)cordis plugin。sofagent 约束层在 DeepSeek Harness 生态的插件形态。
Install
npx skills add https://github.com/KongFangXun/sofagent/tree/main/engine/dsh-plugins/cordis-plugin-sofagent-audit
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install kongfangxun-sofagent@llmmart
git clone https://github.com/KongFangXun/sofagent.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole kongfangxun/sofagent collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
cordis-plugin-sofagent-audit
变更机器审阅 + 验收硬门禁——24 规则 + git diff 硬证据 + Turn 停止验收判定(验收不过不放行,开关独立可关)(seam: tools/result + tools/pre-execute + fs/write-intent + agent/turn-stopping)
用途
装上之后:工具调用前后各过一道审计——24 条 git diff 硬证据规则 + 危险操作黑名单;Agent 想收尾时先过机器验收判定,不过不放行(验收门禁有独立开关,settings 里可关)。什么时候用:需要「改了什么、谁改的、有没有越界」有硬证据可查,且交付标准明确、不想让 Agent 自述「做完了」就算完。
接入点(seam: tools/result + tools/pre-execute + fs/write-intent + agent/turn-stopping):桥接 @sofagent/audit,缺依赖时该能力静默跳过;接入形态(声明 / 实现)见 SEAMS.md。验收判定源 = define_acceptance / check_acceptance 两 MCP tool(v1.4.9 P2 吸收原 cordis-plugin-sofagent-gate 验收面,不另造判定源)。
本插件随 sofagent 主线版本发布(SkillHub 通道:skillhub install cordis-plugin-sofagent-audit 安装与检索;npm 通道未开通)。版本号与 sofagent 主线对齐。
相关链接
- sofagent 主仓:https://github.com/KongFangXun/sofagent
- 开发日志:docs/changelog/v1.4/v1.4.0.md(DSH 插件家族)
Files (sofagent)
-
src
-
index.test.ts 17.1 KB
// ============================================================ // cordis-plugin-sofagent-audit · 插件单测(v1.4.9 P2 合并批 · F3 验收吸收面) // ============================================================ // 断言面 = F3 的三条硬要求: // ① seam 四值声明——原三值 + agent/turn-stopping(吸收原 -gate 的验收门禁 seam) // ② settings 验收门禁独立开关——acceptanceGate 字段在场且默认 'true' // ③ 原 -gate 目录已删(合并完成的落位断言) // ============================================================ import { describe, it, expect, vi, beforeEach, afterEach, afterAll } from 'vitest'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import pluginDefault, { pluginMeta, capability, invoke } from './index'; const HERE = path.dirname(fileURLToPath(import.meta.url)); /** 必要的 schemastery 替身:settings.register 分支的 require 会拿它构造 shape——缺席走 catch 降级 */ vi.mock('@deepseek-ai/schemastery', () => ({ boolean: () => ({ __type: 'boolean' }), string: () => ({ __type: 'string' }), object: (shape: unknown) => ({ __type: 'object', shape }), })); // 章十:agent/turn-stopping 的续跑指令经宿主消息工厂构造(`@deepseek-ai/dsh-llm`)—— // 打桩成确定性替身,避免用例依赖宿主包是否装在当前环境(缺席时 handler 会 fail-open, // 那样就测不到真正的续跑路径)。 vi.mock('@deepseek-ai/dsh-llm', () => ({ createUserMessage: (input: Record<string, unknown>) => ({ __fakeUserMessage: true, ...input }), })); describe('cordis-plugin-sofagent-audit(F3 验收吸收)', () => { afterEach(() => { vi.restoreAllMocks(); }); it('插件元数据完整(id/version/description/seam 四值)', () => { const pkg = require('../package.json') as { version: string }; expect(pluginMeta.id).toBe('cordis-plugin-sofagent-audit'); expect(pluginMeta.version).toBe(pkg.version); // SSOT 对齐:版本跟随主版本(读 package.json) expect(pluginMeta.seam).toBe('tools/result + tools/pre-execute + fs/write-intent + agent/turn-stopping'); expect(pluginMeta.description).toContain('seam: tools/result + tools/pre-execute + fs/write-intent + agent/turn-stopping'); expect(pluginMeta.description).toContain('验收硬门禁'); // P2 新描述 }); it('seam 四值:与 plugins.json 清单逐字一致(三载体对账的门禁面)', () => { const manifest = require('../../plugins.json') as { plugins: Array<{ id: string; seam: string; capability: string }>; }; const entry = manifest.plugins.find((p) => p.id === 'cordis-plugin-sofagent-audit'); expect(entry).toBeDefined(); expect(pluginMeta.seam).toBe(entry!.seam); expect(capability).toBe(entry!.capability); // 四值逐段拆分断言(+ 号并列形态) const tokens = entry!.seam.split(' + ').map((s) => s.trim()); expect(tokens).toEqual(['tools/result', 'tools/pre-execute', 'fs/write-intent', 'agent/turn-stopping']); }); it('settings 验收门禁独立开关:register 收到 acceptanceGate 字段且 base 默认 true', () => { const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const register = vi.fn(() => ({ get: () => ({ enabled: true, rules: '24', acceptanceGate: 'true' }), watch: () => () => undefined })); const ctx = { provide: vi.fn(() => () => undefined), settings: { register }, }; (pluginDefault.apply as (c: unknown) => unknown)(ctx); expect(register).toHaveBeenCalledTimes(1); const [ns, , opts] = register.mock.calls[0] as [string, unknown, { base: Record<string, string> }]; expect(ns).toBe('sofagent-audit'); expect(opts.base.rules).toBe('24'); // 既有字段不回归 expect(opts.base.acceptanceGate).toBe('true'); // F3 新开关,默认开 errSpy.mockRestore(); }); it('能力说明非空', () => { expect(capability.length).toBeGreaterThan(10); }); it('invoke 可调用(成功或降级,不挂死)', async () => { const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const r = await invoke().catch((e) => e); expect(r).toBeDefined(); errSpy.mockRestore(); }); it('原 -gate 目录已删(合并完成的落位断言)', () => { const siblings = path.resolve(HERE, '..'); expect(fs.existsSync(path.join(siblings, 'cordis-plugin-sofagent-gate'))).toBe(false); }); }); // ───────────────────────────────────────────────────────────────────────────── // v1.5.0 章十 · seam 事件接线(seam 从声明到实现) // 断言面:① 订阅真实存在(ctx.on 的调用形态与事件名集合)② disposer 收进复合卸载契约 // ③ 拦截位的**真实判定**见效(判定源 = 引擎 checkDangerousCommand / checkAcceptance) // ④ 放行位不动作 ⑤ 判据不成立时不拦(fail-open) // ───────────────────────────────────────────────────────────────────────────── describe('章十 · seam 事件接线', () => { const MANIFEST = require('../../plugins.json') as { plugins: Array<{ id: string; seamHandlers?: string[] }>; }; const declared: string[] = MANIFEST.plugins.find((p) => p.id === 'cordis-plugin-sofagent-audit')?.seamHandlers ?? []; const FOUR = ['tools/result', 'tools/pre-execute', 'fs/write-intent', 'agent/turn-stopping']; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sofagent-audit-seam-')); // v1.5.1 第七章收口:意图通道会往 dataDir 落盘(`intent.jsonl` / `intent-skips.jsonl`)—— // 本 describe 显式把 SOFAGENT_DATA 钉在临时目录,**绝不写真实 `~/.sofagent`** // (dsh-plugins 未挂 tools/check/vitest-setup.mjs,隔离须在本文件自行声明; // 逐个用例内再覆盖该变量的用例仍以其自身取值为准)。 const seamData = path.join(tmp, 'data'); beforeEach(() => { process.env.SOFAGENT_DATA = seamData; // 逐用例复位(用例内自设取值的仍以其为准) }); afterEach(() => { vi.restoreAllMocks(); }); afterAll(() => { delete process.env.SOFAGENT_TASK_ID; delete process.env.SOFAGENT_PROJECT_ROOT; delete process.env.SOFAGENT_DATA; fs.rmSync(tmp, { recursive: true, force: true }); }); /** 宿主 ctx 替身:记录订阅名 + 保留 disposer(对齐 cordis `ctx.on(event, h) → disposer`) */ function hostCtx(extra: Record<string, unknown> = {}) { const subscribed = new Map<string, (...a: unknown[]) => unknown>(); const disposed: string[] = []; const on = vi.fn((event: string, handler: (...a: unknown[]) => unknown) => { subscribed.set(event, handler); return () => { disposed.push(event); }; }); return { ctx: { provide: vi.fn(() => () => undefined), on, ...extra }, subscribed, disposed }; } it('seamHandlers 与 plugins.json 声明同集合(三处对账的插件侧)', () => { expect(declared.slice().sort()).toEqual([...FOUR].sort()); }); it('apply 经 ctx.on 订阅四个事件位;复合 disposer 逐个反注册', async () => { const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const { ctx, subscribed, disposed } = hostCtx(); const disposer = (pluginDefault.apply as (c: unknown) => unknown)(ctx) as | (() => Promise<void>) | undefined; expect([...subscribed.keys()].sort()).toEqual([...FOUR].sort()); expect(typeof disposer).toBe('function'); await disposer!(); expect(disposed.slice().sort()).toEqual([...FOUR].sort()); errSpy.mockRestore(); }); it('tools/pre-execute:危险命令 → deny 决策(真判定源 checkDangerousCommand)', async () => { const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const { ctx, subscribed } = hostCtx(); (pluginDefault.apply as (c: unknown) => unknown)(ctx); const next = vi.fn(() => 'allow'); const verdict = await subscribed.get('tools/pre-execute')!( { name: 'bash', arguments: { command: 'rm -rf /' } }, next, ); expect(verdict).toMatchObject({ kind: 'deny' }); expect(String((verdict as { reason?: unknown }).reason)).toContain('sofagent 工具门禁'); expect(next).not.toHaveBeenCalled(); // 拦截 = 不调 next(瀑布流被否,工具不执行) errSpy.mockRestore(); }); it('tools/pre-execute:安全命令与非命令工具都放行(不扩大拦截面)', async () => { const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const { ctx, subscribed } = hostCtx(); (pluginDefault.apply as (c: unknown) => unknown)(ctx); const handler = subscribed.get('tools/pre-execute')!; const next1 = vi.fn(() => 'allow'); expect(await handler({ name: 'bash', arguments: { command: 'ls -la' } }, next1)).toBe('allow'); expect(next1).toHaveBeenCalledTimes(1); const next2 = vi.fn(() => 'allow'); expect(await handler({ name: 'read_file', arguments: { path: 'a.md' } }, next2)).toBe('allow'); expect(next2).toHaveBeenCalledTimes(1); errSpy.mockRestore(); }); it('fs/write-intent:放行接线(调 next 并原样返回其值)', async () => { const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const { ctx, subscribed } = hostCtx(); (pluginDefault.apply as (c: unknown) => unknown)(ctx); const next = vi.fn(() => 'write-intent'); const ret = await subscribed.get('fs/write-intent')!({ path: 'x.md' }, { id: 'actor' }, next); expect(ret).toBe('write-intent'); expect(next).toHaveBeenCalledTimes(1); errSpy.mockRestore(); }); it('agent/turn-stopping:无任务标识 → 不拦截(fail-open,无验收可判)', async () => { const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); delete process.env.SOFAGENT_TASK_ID; const { ctx, subscribed } = hostCtx(); (pluginDefault.apply as (c: unknown) => unknown)(ctx); const steer = vi.fn(); const agent = { steer }; await expect(subscribed.get('agent/turn-stopping')!({ agent, turn: 1 })).resolves.toBeUndefined(); expect(steer).not.toHaveBeenCalled(); errSpy.mockRestore(); }); it('agent/turn-stopping:验收未通过 → 续跑一次;同 turn 不重复;未定义验收不拦', async () => { const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); // 真实验收定义:grep-absent 命中标记文件 → 该条件不通过(无需跑 build/test,确定且快) // ⚠️ criterion.path 的语义是**相对 projectRoot**(引擎侧 `join(projectRoot, path)`), // 传绝对路径会被拼成 `<proj>/<abs>` → 目标不存在 → 引擎按「零命中」判**通过**(假绿)。 const proj = path.join(tmp, 'proj'); fs.mkdirSync(proj, { recursive: true }); fs.writeFileSync(path.join(proj, 'marker.txt'), 'SOFAGENT_ACCEPTANCE_MARKER\n'); const dataDir = path.join(tmp, 'data'); const orch = require('@sofagent/orchestrator') as { saveAcceptanceDefinition: (d: string, def: unknown) => string; }; orch.saveAcceptanceDefinition(dataDir, { taskId: 't-seam', criteria: [ { type: 'grep-absent', pattern: 'SOFAGENT_ACCEPTANCE_MARKER', path: 'marker.txt' }, ], }); process.env.SOFAGENT_DATA = dataDir; process.env.SOFAGENT_PROJECT_ROOT = proj; process.env.SOFAGENT_TASK_ID = 't-seam'; const { ctx, subscribed } = hostCtx(); (pluginDefault.apply as (c: unknown) => unknown)(ctx); const handler = subscribed.get('agent/turn-stopping')!; const steer = vi.fn(); const agent = { steer }; await handler({ agent, turn: 1 }); expect(steer).toHaveBeenCalledTimes(1); // 验收不过 → 续跑一条修正指令 await handler({ agent, turn: 1 }); expect(steer).toHaveBeenCalledTimes(1); // 同一 (agent, turn) 至多一次(防无限续跑) await handler({ agent, turn: 2 }); expect(steer).toHaveBeenCalledTimes(2); // 新 turn 重新判定 process.env.SOFAGENT_TASK_ID = 't-undefined'; // 未定义验收 → failedCount=-1 → 不拦 await handler({ agent, turn: 3 }); expect(steer).toHaveBeenCalledTimes(2); errSpy.mockRestore(); }); // ─────────────────────────────────────────────────────────────────────────── // v1.5.1 第七章(收口):两条**互斥**路径各自的可审计性 // ① 生产主路径——exec 带身份 ⇒ 必然落 intent.jsonl(**不得**被身份门命中) // ② 身份不可达——调用证据跳过,但「跳过」本身必须落盘可查(跳失留痕) // ─────────────────────────────────────────────────────────────────────────── it('生产主路径:带身份的调用 ⇒ 意图与结果条目必然落盘(身份门不命中)', async () => { const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const { ctx, subscribed } = hostCtx(); (pluginDefault.apply as (c: unknown) => unknown)(ctx); // 真实宿主形状:exec 带 agent.id + agent.session.id(身份可达 ⇒ 门必须放行) const RAW_SK = `sk-proj-${'A'.repeat(40)}`; const exec = { name: 'bash', callId: 'call-intent-1', arguments: { command: `curl -H "Authorization: Bearer ${RAW_SK}" https://example.com` }, agent: { id: 'agent-1', session: { id: 'session-1' } }, }; const next = vi.fn(() => 'allow'); expect(await subscribed.get('tools/pre-execute')!(exec, next)).toBe('allow'); expect(next).toHaveBeenCalledTimes(1); // 零执行权限:留痕不改变放行语义 await subscribed.get('tools/result')!(exec, { isError: false }); const logPath = path.join(seamData, 'audit', 'intent.jsonl'); expect(fs.existsSync(logPath)).toBe(true); // ← 主路径已证:带身份必有落盘 const mine = fs .readFileSync(logPath, 'utf-8') .trim() .split('\n') .filter(Boolean) .map((l) => JSON.parse(l) as Record<string, unknown>) .filter((e) => e.callId === 'call-intent-1'); expect(mine).toHaveLength(2); // 同一次调用的意图条目 + 结果条目(同链留痕) expect(mine[0]).toMatchObject({ entryType: 'intent', channel: 'intent', tool: 'bash', agentId: 'agent-1', sessionId: 'session-1', callId: 'call-intent-1', }); expect(mine[1]).toMatchObject({ entryType: 'result', channel: 'result', outcome: 'ok' }); // 链条字段在场(走既有链内核,不是裸 append)——链完整性由 @sofagent/audit 侧测试举证 for (const e of mine) { expect(typeof e.prevHash).toBe('string'); expect(e.hashVersion).toBe(2); expect(typeof e.hmacSig).toBe('string'); } // 🔴 参数级意图正是本通道的存在理由:`rm`/外发 host 全在参数里;且**脱敏先于落盘** expect(String((mine[0]!.argsSummary as Record<string, string>).command)).toContain('example.com'); expect(fs.readFileSync(logPath, 'utf-8')).not.toContain(RAW_SK); errSpy.mockRestore(); }); it('身份不可达:调用证据跳过,但跳失落盘可审计(reason/event/tool 齐备)', async () => { const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const { ctx, subscribed } = hostCtx(); (pluginDefault.apply as (c: unknown) => unknown)(ctx); const skipPath = path.join(seamData, 'audit', 'intent-skips.jsonl'); const count = (p: string) => fs.existsSync(p) ? fs.readFileSync(p, 'utf-8').trim().split('\n').filter(Boolean).length : 0; const before = count(skipPath); const exec = { name: 'orphan_tool', callId: 'call-skip-1', arguments: { command: 'ls -la' } }; const next = vi.fn(() => 'allow'); // 无 agent/session ⇒ 身份不可达(不猜测身份) expect(await subscribed.get('tools/pre-execute')!(exec, next)).toBe('allow'); expect(next).toHaveBeenCalledTimes(1); // 跳过留证同样不影响宿主(零执行权限) expect(count(skipPath)).toBe(before + 1); // ← 跳过必有据可查(不是黑洞) const rows = fs .readFileSync(skipPath, 'utf-8') .trim() .split('\n') .filter(Boolean) .map((l) => JSON.parse(l) as Record<string, unknown>); expect(rows[rows.length - 1]).toMatchObject({ reason: 'identity-unreachable', event: 'tools/pre-execute', tool: 'orphan_tool', }); // 调用证据面不受污染:该调用不进 intent.jsonl(「跳过」与「记录」是两件事) const logPath = path.join(seamData, 'audit', 'intent.jsonl'); const logText = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf-8') : ''; expect(logText).not.toContain('call-skip-1'); expect(logText).not.toContain('orphan_tool'); errSpy.mockRestore(); }); }); -
index.ts 18.3 KB
// cordis-plugin-sofagent-audit · DSH 反向插件(v1.5.2 P2 合并批:吸收原 -gate 验收门禁面) // seam 挂载:tools/result + tools/pre-execute + fs/write-intent + agent/turn-stopping // # 语义:工具结果留证 + 工具执行前拦截 + 文件写入意图拦截(放行) + Turn 停止验收判定 // (判定源 = checkDangerousCommand / check_acceptance——引擎包既有 @public,零改动) // v1.5.2 第七章:`tools/pre-execute` / `tools/result` 两个 seamHandler 追加**调用意图留痕** // (@sofagent/audit.createIntentChannel → <dataDir>/audit/intent.jsonl),审计输入面由此 // 从 git diff 单通道扩为双通道。该面**零执行权限**:只读消费宿主事件 + 落盘,不参与判定、 // 不碰 next、无 deny/allow 语义(拦截能力仍归上述判定源与宿主事件位,本插件未新增)。 // 身份不可达(exec 无 agent/session)时**调用证据跳过**,但「跳过」本身落 // `<dataDir>/audit/intent-skips.jsonl`(进盘不进链)——跳失可审计,不是黑洞。 // 清单生成源 = engine/dsh-plugins/plugins.json(生成 package.json 的 description/sofagent/dsh 段与 cordis.patch.yml);本文件的 seam 字面量由生成器 --check 与之对账。 // // v1.5.0 P2 吸收说明(F3):原 -gate 独立承载 agent/turn-stopping 验收门禁,但其 // 判定源(define_acceptance / check_acceptance 两 MCP tool)与 -audit 同族(机器可判定 // 审计),拆两包造成「审计归 audit、验收归 gate」的人为割裂。本插件把验收 seam 并入 // 四值声明,settings 补 acceptanceGate 独立开关(默认开;关档即不参与 Turn 停止判定—— // 与 F2 分档同语义:档位是 settings 字段,不另造判定源)。 import { createSofagentPlugin, seamHelpers, type SeamHandler, type SeamHelpers, } from '@sofagent/dsh-plugin-kit'; // ── 事件接线共用的环境解析与防御式取数 ──────────────────────────────────────── // 适配层红线:只做鸭子类型读取,宿主/引擎面缺席一律降级不抛——接线故障不得 // 升级成任务故障(handler 内所有失败路径都 fail-open + 可见日志)。 /** 项目根:显式环境变量优先,缺省取宿主进程工作目录(与 CLI/MCP 侧同口径) */ const projectRoot = (): string => process.env.SOFAGENT_PROJECT_ROOT ?? process.cwd(); /** 数据目录:交给 @sofagent/core 自己的解析(与 MCP/CLI 同源),不可达则交引擎兜底 */ async function resolveDataDir(helpers: SeamHelpers): Promise<string | undefined> { try { const dir = await helpers.call('@sofagent/core', 'getDataDir'); return typeof dir === 'string' && dir !== '' ? dir : undefined; } catch (err) { helpers.log(`数据目录解析不可用(交引擎默认值):${errMsg(err)}`); return undefined; } } /** 从宿主工具执行对象取命令字符串(仅命令类工具的入参带 `command`) */ function commandOf(exec: unknown): string | null { const args = (exec as { arguments?: unknown } | null | undefined)?.arguments; if (args === null || typeof args !== 'object') return null; const cmd = (args as Record<string, unknown>).command; return typeof cmd === 'string' && cmd.trim() !== '' ? cmd : null; } /** 留证所需的身份:从宿主 exec 上尽力取 (agentId, sessionId),取不到即放弃留证 */ function identityOf(exec: unknown): { agentId: string; sessionId: string } | null { const agent = (exec as { agent?: unknown } | null | undefined)?.agent as | { id?: unknown; session?: { id?: unknown; header?: { id?: unknown } } } | undefined; const agentId = typeof agent?.id === 'string' ? agent.id : ''; const rawSessionId = agent?.session?.id ?? agent?.session?.header?.id; const sessionId = typeof rawSessionId === 'string' ? rawSessionId : ''; return agentId !== '' && sessionId !== '' ? { agentId, sessionId } : null; } /** 一次性日志表——接线自证与降级提示只打一次,避免按事件刷屏 */ const loggedOnce = new Set<string>(); function logOnce(helpers: SeamHelpers, key: string, message: string): void { if (loggedOnce.has(key)) return; loggedOnce.add(key); helpers.log(message); } const errMsg = (err: unknown): string => (err instanceof Error ? err.message : String(err)); /** * Turn 停止判定的防重表:每 (agent, turn) 至多续跑一次。 * 必要性:宿主在每一步收尾都会再派发 `agent/turn-stopping`,若验收始终不过而 * 每次都续跑,Turn 会被拉成无限循环——本表把它约束成「每 turn 一次」。 */ const lastSteeredTurn = new WeakMap<object, unknown>(); /** * 跳失留证(v1.5.1 第七章收口)——**「跳过」本身必须可审计**。 * * 为什么不能只打一行日志:日志随进程消失,而「这个宿主上意图通道到底记了几次、 * 跳了几次、为什么跳」恰恰是**事后**(进程重启过 / 换过机器 / 翻旧账)才要问的 * 问题——只打日志等于把答案交给运气(日志没人收就没了)。故跳失落盘 * `<dataDir>/audit/intent-skips.jsonl`(**进盘不进链**:它是通道活性证据, * 不是调用证据,不做完整性声明),配 `@sofagent/audit.summarizeIntentChannel` * 即可**只读盘**回答上述问题。 * * 隔离纪律:本函数同样只在**身份不可达**时被调用(见 recordIntent 的门), * 故正常运行的生产宿主不会产生跳失记录;出现记录 = 宿主身份接线有问题。 * * 🔴 失败一律 fail-open + 可见日志:留证故障不得升级为宿主工具调用故障。 */ async function recordIntentSkipEvidence(helpers: SeamHelpers, event: string, exec: unknown): Promise<void> { logOnce( helpers, 'intent-no-identity', '意图留证:宿主 exec 未带 agent/session 标识——调用证据跳过(不猜测身份),跳失已落盘留证', ); const name = (exec as { name?: unknown } | null | undefined)?.name; try { await helpers.call('@sofagent/audit', 'recordIntentSkip', await resolveDataDir(helpers), { reason: 'identity-unreachable', event, ...(typeof name === 'string' && name !== '' ? { tool: name } : {}), }); } catch (err) { logOnce( helpers, 'intent-skip-evidence-failed', `意图跳失留证写入失败(本次跳过只剩日志,不阻断宿主):${errMsg(err)}`, ); } } /** * 调用意图留痕(v1.5.1 第七章·审计输入双通道)——**零执行权限**: * 只读消费宿主事件 + 落盘留痕,不参与任何判定、不碰 `next`、不产生 deny/allow 语义 * (拦截能力仍归上面两个判定源,本函数不拥有也未新增拦截面)。 * * 身份纪律与既有 `tools/result` 留证同款:exec 不带 agent/session ⇒ **跳过**(身份不可达, * 不猜测)——不可归因的意图条目对审计没有价值;该纪律同时保证无身份载荷(宿主替身 / * 尚未接入身份的宿主)不触发**调用证据**落盘。跳过的**事实**另落 `intent-skips.jsonl` * (`recordIntentSkipEvidence`),故「跳过」可审计、不是黑洞。 * * 通道实例按进程惰性创建一次(内建单例);**任何失败一律 fail-open** + 可见日志—— * 留痕故障不得升级为宿主工具调用故障(对齐本文件既有接线红线)。 */ type IntentChannelLike = { handle: (event: string, args: unknown[]) => void }; let intentChannel: IntentChannelLike | null | undefined; async function recordIntent( helpers: SeamHelpers, event: string, exec: unknown, result?: unknown, ): Promise<void> { if (identityOf(exec) === null) { await recordIntentSkipEvidence(helpers, event, exec); return; } try { if (intentChannel === undefined) { const dataDir = await resolveDataDir(helpers); const created = await helpers.call( '@sofagent/audit', 'createIntentChannel', dataDir === undefined ? {} : { dataDir }, ); const candidate = created as IntentChannelLike | null | undefined; intentChannel = candidate !== null && candidate !== undefined && typeof candidate.handle === 'function' ? candidate : null; if (intentChannel === null) { logOnce( helpers, 'intent-channel-missing', '意图通道不可用(@sofagent/audit 未导出 createIntentChannel)——意图留痕停用,不阻断宿主', ); } } intentChannel?.handle(event, [exec, result]); } catch (err) { logOnce(helpers, 'intent-record-failed', `意图留痕写入失败(不阻断宿主工具调用):${errMsg(err)}`); } } /** * seam 事件处理器(v1.5.0 章十)——四个事件位的**实现**(此前只有声明)。 * * 🔴 判定逻辑零改动:全部判定都来自引擎包既有 @public API * (`checkDangerousCommand` / `checkAcceptance`),插件层只负责接管子 + 转形状。 * 🔴 失败一律 fail-open:判定源不可达时放行 + 打可见日志,绝不把接线故障 * 变成工具执行失败或 Turn 中止。 */ const seamHandlers: Record<string, SeamHandler> = { /** * 工具执行前——宿主 waterfall(listener 约定 `(exec, next)`,不调 next 即否决)。 * 判定源 = `@sofagent/orchestrator.checkDangerousCommand`(与 sofagent 自家运行时 * `wrapToolsWithGate` 同源,故 DSH 侧与自家运行时的拦截口径一致)。 */ 'tools/pre-execute': async (...args: unknown[]) => { const helpers = seamHelpers(args); const [exec, next] = args; const cont = async (): Promise<unknown> => typeof next === 'function' ? await (next as () => unknown)() : undefined; // v1.5.1 第七章:意图留痕先于命令类判定(非命令类工具的意图同样在参数里: // 写文件落点 / 外发 host),且**先于拦截**——被拒的操作也留痕。零执行权限: // 本调用不改本 handler 的返回语义(下方放行/拦截分支逐字不变)。 await recordIntent(helpers, 'tools/pre-execute', exec); const command = commandOf(exec); if (command === null) return await cont(); // 非命令类工具不参与判定(不改变既有语义) try { const reason = await helpers.call('@sofagent/orchestrator', 'checkDangerousCommand', command); if (typeof reason === 'string' && reason !== '') { helpers.log(`工具执行前拦截:${reason}`); return { kind: 'deny', reason: `sofagent 工具门禁:${reason}` }; } } catch (err) { logOnce( helpers, 'pre-execute-unavailable', `工具执行前判定不可用(本次放行,不阻断宿主工具执行):${errMsg(err)}`, ); } return await cont(); }, /** * 工具结果留证——宿主 emit(listener 约定 `(exec, result)`,返回值无意义)。 * 留证口径:**只留失败结果**(被门禁拦下的调用、工具自身报错)——成功结果已由 * 宿主 session 日志承担,全量落库只会给决策日志灌入与判定无关的噪声。 */ 'tools/result': async (...args: unknown[]) => { const helpers = seamHelpers(args); const [exec, result] = args; // v1.5.1 第七章:结果入意图通道(与 pre-execute 的意图条目同链留痕、条目类型可区分) // ——先于下方「只留失败结果」的留证过滤:结果通道要的是**全部**调用的结果。 await recordIntent(helpers, 'tools/result', exec, result); if ((result as { isError?: unknown } | null | undefined)?.isError !== true) return; const id = identityOf(exec); if (id === null) { logOnce( helpers, 'result-no-identity', '工具结果留证:宿主 exec 未带 agent/session 标识——留证跳过(身份不可达,不猜测)', ); return; } const info = exec as { name?: unknown; callId?: unknown } | null | undefined; try { await helpers.call( '@sofagent/audit', 'emitDecision', { agentId: id.agentId, sessionId: id.sessionId, kind: 'TOOL_GATE', moment: 'ACT', category: 'skip', why: `工具结果留证:${typeof info?.name === 'string' && info.name !== '' ? info.name : '未知工具'} 返回错误`, artifactRef: typeof info?.callId === 'string' && info.callId !== '' ? `tool-call/${info.callId}` : undefined, }, await resolveDataDir(helpers), ); } catch (err) { logOnce(helpers, 'result-emit-failed', `工具结果留证写入失败(不阻断宿主):${errMsg(err)}`); } }, /** * 文件写入意图——宿主 waterfall(listener 约定 `(target, actor, next)`)。 * 🔴 本事件位当前是**放行接线**:sofagent 引擎包没有「按目标路径判定写意图」的 * @public 判定源,按马鞍铁律(只接线、不新造判定)不做触发式拦截—— * 订阅真实、disposer 真实、装载状态可自证,但不假装有判定。 */ 'fs/write-intent': async (...args: unknown[]) => { const helpers = seamHelpers(args); const next = args[2]; // waterfall 约定 (target, actor, next) logOnce( helpers, 'fs-write-intent-passthrough', '文件写入意图事件已接线(放行——引擎侧无「按路径判定写意图」的既有 API,不为接线新造判定)', ); return typeof next === 'function' ? await (next as () => unknown)() : undefined; }, /** * Turn 停止验收判定——宿主 serial(listener 约定 `(payload)`,payload 含 * `{ agent, turn, signal }`)。判定源 = `@sofagent/orchestrator.checkAcceptance`。 * * 「验收不过不放行」的机制:宿主在该事件上**不使用返回值**,真正能续跑 Turn 的 * 唯一手段是向 agent 送一条消息(宿主 Stop hook 同法:`agent.steer(...)`), * 宿主随后复查 inbox 决定是否继续。故本 handler 走 `agent.steer`。 * * 🔴 三重保险,避免把「接上验收」变成「Turn 打不住」: * ① 未定义验收(failedCount = -1)不拦——「没有验收定义」≠「验收不过」; * ② 每 (agent, turn) 至多续跑一次; * ③ 全程 fail-open:判定源 / 消息工厂 / steer 任一不可用只记日志,不抛。 */ 'agent/turn-stopping': async (...args: unknown[]) => { const helpers = seamHelpers(args); const [payload] = args; if (helpers.flags().acceptanceGate === false) return; // 关档:不参与 Turn 停止判定 const taskId = (process.env.SOFAGENT_TASK_ID ?? '').trim(); if (taskId === '') return; // 无任务标识 → 无验收可判(不凭空阻断收尾) let verdict: unknown; try { verdict = await helpers.call( '@sofagent/orchestrator', 'checkAcceptance', (await resolveDataDir(helpers)) ?? '', taskId, projectRoot(), ); } catch (err) { logOnce(helpers, 'turn-stopping-unavailable', `Turn 停止验收判定不可用(不拦截本次停止):${errMsg(err)}`); return; } const v = (verdict ?? {}) as { ok?: unknown; failedCount?: unknown }; if (v.ok !== false || typeof v.failedCount !== 'number' || v.failedCount <= 0) return; const p = (payload ?? {}) as { agent?: unknown; turn?: unknown }; const agent = p.agent as { steer?: unknown } | null | undefined; if (agent === null || agent === undefined || typeof agent.steer !== 'function') { logOnce( helpers, 'turn-stopping-no-steer', 'Turn 停止验收未通过,但宿主 agent 无 steer 面——本次无法续跑(记日志,不异常)', ); return; } if (lastSteeredTurn.get(agent as object) === p.turn) return; // ② 每 turn 至多一次 lastSteeredTurn.set(agent as object, p.turn); try { const message = await helpers.call('@deepseek-ai/dsh-llm', 'createUserMessage', { content: [ { type: 'text', text: `sofagent 验收未通过(${v.failedCount} 条未达)——先修完再收尾。` }, ], source: { kind: 'plugin', plugin: 'sofagent-audit' }, }); (agent.steer as (m: unknown) => unknown)(message); helpers.log( `Turn 停止验收未通过(${v.failedCount} 条)——已向宿主 agent 续跑一条修正指令(本 turn 内至多一次)`, ); } catch (err) { logOnce(helpers, 'turn-stopping-steer-failed', `验收续跑指令构造失败(不拦截本次停止):${errMsg(err)}`); } }, }; /** 插件声明(本文件唯一手写处;适配层红线由 kit 承担:ctx 鸭子类型 + 宿主 API 缺席降级不抛) */ const kit = createSofagentPlugin( { id: 'cordis-plugin-sofagent-audit', seam: 'tools/result + tools/pre-execute + fs/write-intent + agent/turn-stopping', seamSemantics: '工具结果留证 + 工具执行前拦截 + 文件写入意图拦截(放行) + Turn 停止验收判定(v1.4.9 P2 吸收原 -gate 验收门禁;判定源 = checkDangerousCommand/check_acceptance,不另造)', capability: '审计与验收(git diff 硬证据 + 24 规则 + 机器可判定验收)', bridgePkg: '@sofagent/audit', bridgeApi: 'runRules', description: '变更机器审阅 + 验收硬门禁——24 规则 + git diff 硬证据 + Turn 停止验收判定(验收不过不放行,开关独立可关)', // audit 专属 envelope(其余插件走 kit 默认值) purpose: 'sofagent 审计插件——24 规则 + git diff 硬证据 + 验收门禁', readyMessage: '审计与验收服务就绪(24 规则 + Turn 停止验收)', // v1.4.9 P2(F3):rules 为既有字段;acceptanceGate = 验收门禁独立开关(默认 'true' 开)—— // 关档即本插件的 agent/turn-stopping 面不参与 Turn 停止判定(审计三 seam 不受影响) settingsExtra: { rules: '24', acceptanceGate: 'true' }, seamHandlers, }, require('../package.json') as { version?: string }, ); export const pluginMeta = kit.pluginMeta; // 插件元数据(DSH profile/注册表消费) export const capability = kit.capability; // 依赖的 sofagent 能力说明(DSH skill 引导链展示) export const invoke = kit.invoke; // 桥接 @sofagent/* 公共 API(懒加载 + 降级不抛) export default kit.plugin; // DSH Cordis 插件契约(apply 三段式由 kit 提供)
-
-
cordis.patch.yml 905 B
# sofagent cordis-plugin-sofagent-audit bundle patch v1.5.2——注册为 DSH profile layer - insert: - id: sofagent-audit name: 'cordis-plugin-sofagent-audit' inject: [settings, dynamicCordisRunner] config: seam: "tools/result + tools/pre-execute + fs/write-intent + agent/turn-stopping" # 语义:工具结果留证 + 工具执行前拦截 + 文件写入意图拦截(放行) + Turn 停止验收判定(v1.4.9 P2 吸收原 -gate 验收门禁;判定源 = checkDangerousCommand/check_acceptance,不另造) description: "变更机器审阅 + 验收硬门禁——24 规则 + git diff 硬证据 + Turn 停止验收判定(验收不过不放行,开关独立可关)——桥接 @sofagent/audit runRules / @sofagent/audit emitDecision / @sofagent/orchestrator checkDangerousCommand / @sofagent/orchestrator checkAcceptance / @sofagent/core getDataDir" -
package.json 2.1 KB
{ "name": "cordis-plugin-sofagent-audit", "version": "1.5.2", "engines": { "node": ">=18" }, "description": "变更机器审阅 + 验收硬门禁——24 规则 + git diff 硬证据 + Turn 停止验收判定(验收不过不放行,开关独立可关)(seam: tools/result + tools/pre-execute + fs/write-intent + agent/turn-stopping)——桥接 @sofagent/audit runRules / @sofagent/audit emitDecision / @sofagent/orchestrator checkDangerousCommand / @sofagent/orchestrator checkAcceptance / @sofagent/core getDataDir", "license": "MIT", "author": "KongFangXun", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "build": "tsc", "test": "vitest run src/index.test.ts" }, "keywords": [ "sofagent", "dsh", "cordis-plugin" ], "sofagent": { "type": "dsh-plugin", "family": "cordis", "seam": "tools/result + tools/pre-execute + fs/write-intent + agent/turn-stopping", "seamSemantics": "工具结果留证 + 工具执行前拦截 + 文件写入意图拦截(放行) + Turn 停止验收判定(v1.4.9 P2 吸收原 -gate 验收门禁;判定源 = checkDangerousCommand/check_acceptance,不另造)", "seamHandlers": [ "tools/result", "tools/pre-execute", "fs/write-intent", "agent/turn-stopping" ] }, "//optionalDependencies": "v1.4.5 T6 (R4):src/index.ts 惰性 await import(懒加载 + 缺依赖降级不抛;v1.4.8 起该样板由 @sofagent/dsh-plugin-kit 统一封装;v1.4.9 P2 起厚插件多包 bridges 按序解析、部分可用即部分成功),此前未声明任何依赖——对齐 root package.json F-18 optionalDependencies 先例。", "optionalDependencies": { "@sofagent/audit": "1.5.2", "@sofagent/orchestrator": "1.5.2", "@sofagent/core": "1.5.2" }, "devDependencies": { "typescript": "^7.0.2", "vitest": "^5.0.0" }, "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }, "files": [ "dist/", "cordis.patch.yml", "SKILL.md", "!dist/**/*.js.map", "!dist/**/*.d.ts.map", "!dist/**/*.test.*" ], "dependencies": { "@sofagent/dsh-plugin-kit": "1.5.2" } } -
SKILL.md 1.9 KB
--- name: cordis-plugin-sofagent-audit slug: cordis-plugin-sofagent-audit version: 1.5.2 displayName: cordis-plugin-sofagent-audit description: > 变更机器审阅 + 验收硬门禁——24 规则 + git diff 硬证据 + Turn 停止验收判定(验收不过不放行,开关独立可关)(seam: tools/result + tools/pre-execute + fs/write-intent + agent/turn-stopping)——桥接 @sofagent/audit runRules——DSH(DeepSeek Harness)cordis plugin。sofagent 约束层在 DeepSeek Harness 生态的插件形态。 --- # cordis-plugin-sofagent-audit 变更机器审阅 + 验收硬门禁——24 规则 + git diff 硬证据 + Turn 停止验收判定(验收不过不放行,开关独立可关)(seam: tools/result + tools/pre-execute + fs/write-intent + agent/turn-stopping) ## 用途 **装上之后**:工具调用前后各过一道审计——24 条 git diff 硬证据规则 + 危险操作黑名单;Agent 想收尾时先过机器验收判定,不过不放行(验收门禁有独立开关,settings 里可关)。**什么时候用**:需要「改了什么、谁改的、有没有越界」有硬证据可查,且交付标准明确、不想让 Agent 自述「做完了」就算完。 **接入点**(seam: tools/result + tools/pre-execute + fs/write-intent + agent/turn-stopping):桥接 `@sofagent/audit`,缺依赖时该能力静默跳过;接入形态(声明 / 实现)见 [SEAMS.md](../SEAMS.md)。验收判定源 = define_acceptance / check_acceptance 两 MCP tool(v1.4.9 P2 吸收原 cordis-plugin-sofagent-gate 验收面,不另造判定源)。 本插件随 sofagent 主线版本发布(SkillHub 通道:`skillhub install cordis-plugin-sofagent-audit` 安装与检索;npm 通道未开通)。版本号与 sofagent 主线对齐。 ## 相关链接 - sofagent 主仓:https://github.com/KongFangXun/sofagent - 开发日志:docs/changelog/v1.4/v1.4.0.md(DSH 插件家族) -
tsconfig.json 185 B
{ "extends": "../tsconfig.base.json", "compilerOptions": { "rootDir": "src", "outDir": "dist" }, "include": [ "src" ], "exclude": [ "src/**/*.test.ts" ] }
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.