Claude Cursor Skill

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 生态的插件形态。

LLM Mart · 0 points · 1 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download KongFangXun-sofagent-engine_dsh-plugins_cordis-plugin-sofagent-audit-b8c9032.zip · 16 KB
Part of kongfangxun/sofagent — 15 skills

Install

skills CLI npx skills add https://github.com/KongFangXun/sofagent/tree/main/engine/dsh-plugins/cordis-plugin-sofagent-audit
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install kongfangxun-sofagent@llmmart
Git 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.

No comments yet.

Reviews (0)

No reviews yet.

Related