@xdxer/dingtalk-agent 0.1.4-beta.8 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +233 -0
- package/README.en.md +167 -0
- package/README.md +101 -248
- package/dist/bin/dingtalk-agent.js +1255 -25
- package/dist/bin/dingtalk-agent.js.map +1 -1
- package/dist/src/actions.js +98 -14
- package/dist/src/actions.js.map +1 -1
- package/dist/src/agent-audit.js +1385 -0
- package/dist/src/agent-audit.js.map +1 -0
- package/dist/src/agent-bindings.js +132 -0
- package/dist/src/agent-bindings.js.map +1 -0
- package/dist/src/agent-definition.js +182 -0
- package/dist/src/agent-definition.js.map +1 -0
- package/dist/src/agent-enhance.js +711 -0
- package/dist/src/agent-enhance.js.map +1 -0
- package/dist/src/agent-platform.js +299 -0
- package/dist/src/agent-platform.js.map +1 -0
- package/dist/src/bootstrap.js +125 -17
- package/dist/src/bootstrap.js.map +1 -1
- package/dist/src/config.js +1 -7
- package/dist/src/config.js.map +1 -1
- package/dist/src/development-workspace.js +755 -0
- package/dist/src/development-workspace.js.map +1 -0
- package/dist/src/doctor.js +74 -15
- package/dist/src/doctor.js.map +1 -1
- package/dist/src/dws.js +145 -0
- package/dist/src/dws.js.map +1 -1
- package/dist/src/eval-evidence.js +193 -0
- package/dist/src/eval-evidence.js.map +1 -0
- package/dist/src/host-detect.js +146 -0
- package/dist/src/host-detect.js.map +1 -0
- package/dist/src/instruction-path.js +270 -0
- package/dist/src/instruction-path.js.map +1 -0
- package/dist/src/invocation.js +36 -0
- package/dist/src/invocation.js.map +1 -0
- package/dist/src/lab.js +679 -0
- package/dist/src/lab.js.map +1 -0
- package/dist/src/lease.js +100 -0
- package/dist/src/lease.js.map +1 -0
- package/dist/src/map.js +157 -0
- package/dist/src/map.js.map +1 -0
- package/dist/src/memory/candidates.js +451 -0
- package/dist/src/memory/candidates.js.map +1 -0
- package/dist/src/memory/completion-evidence.js +536 -0
- package/dist/src/memory/completion-evidence.js.map +1 -0
- package/dist/src/memory/operational.js +263 -0
- package/dist/src/memory/operational.js.map +1 -0
- package/dist/src/memory/remote-state.js +478 -0
- package/dist/src/memory/remote-state.js.map +1 -0
- package/dist/src/memory/task-checkpoints.js +204 -0
- package/dist/src/memory/task-checkpoints.js.map +1 -0
- package/dist/src/multica-deploy.js +1538 -0
- package/dist/src/multica-deploy.js.map +1 -0
- package/dist/src/multica-provider.js +685 -0
- package/dist/src/multica-provider.js.map +1 -0
- package/dist/src/opencode-evals.js +1547 -0
- package/dist/src/opencode-evals.js.map +1 -0
- package/dist/src/opencode-isolation.js +124 -0
- package/dist/src/opencode-isolation.js.map +1 -0
- package/dist/src/opencode-provider.js +532 -0
- package/dist/src/opencode-provider.js.map +1 -0
- package/dist/src/opencode-workspace.js +208 -0
- package/dist/src/opencode-workspace.js.map +1 -0
- package/dist/src/perception.js +225 -0
- package/dist/src/perception.js.map +1 -0
- package/dist/src/personal-event-evals.js +595 -0
- package/dist/src/personal-event-evals.js.map +1 -0
- package/dist/src/promotion.js +786 -0
- package/dist/src/promotion.js.map +1 -0
- package/dist/src/remote-semantic-state-live-evals.js +888 -0
- package/dist/src/remote-semantic-state-live-evals.js.map +1 -0
- package/dist/src/remote-semantic-state-worker.js +38 -0
- package/dist/src/remote-semantic-state-worker.js.map +1 -0
- package/dist/src/remote-state-evals.js +502 -0
- package/dist/src/remote-state-evals.js.map +1 -0
- package/dist/src/response-gate.js +51 -0
- package/dist/src/response-gate.js.map +1 -0
- package/dist/src/robot-evals.js +771 -0
- package/dist/src/robot-evals.js.map +1 -0
- package/dist/src/sessions.js +66 -105
- package/dist/src/sessions.js.map +1 -1
- package/dist/src/setup.js +6 -5
- package/dist/src/setup.js.map +1 -1
- package/dist/src/skill-manager.js +162 -24
- package/dist/src/skill-manager.js.map +1 -1
- package/dist/src/skills.js +2 -1
- package/dist/src/skills.js.map +1 -1
- package/dist/src/storage-evals.js +26 -0
- package/dist/src/storage-evals.js.map +1 -0
- package/dist/src/types.js.map +1 -1
- package/dist/src/upgrade.js +23 -27
- package/dist/src/upgrade.js.map +1 -1
- package/dist/src/version.js +73 -0
- package/dist/src/version.js.map +1 -0
- package/dist/src/waits.js +5 -1
- package/dist/src/waits.js.map +1 -1
- package/dist/src/workspace.js +28 -3
- package/dist/src/workspace.js.map +1 -1
- package/docs/INSTALLATION.md +50 -3
- package/docs/SECOND-AGENT-ACCEPTANCE.md +62 -0
- package/docs/architecture/agent-memory-topology.png +0 -0
- package/docs/architecture/agent-memory-topology.svg +132 -0
- package/docs/architecture/general-agent-kernel-topology.png +0 -0
- package/docs/architecture/general-agent-kernel-topology.svg +149 -0
- package/docs/architecture/provider-bound-development-workspace.png +0 -0
- package/docs/architecture/provider-bound-development-workspace.svg +141 -0
- package/docs/architecture/task-completion-gate.png +0 -0
- package/docs/architecture/task-completion-gate.svg +191 -0
- package/docs/assets/agent-delivery-lifecycle.svg +103 -0
- package/docs/schemas/agent-audit-load-evidence.schema.json +14 -0
- package/docs/schemas/agent-audit.schema.json +92 -0
- package/docs/schemas/agent-bindings.schema.json +54 -0
- package/docs/schemas/agent-definition.schema.json +78 -0
- package/docs/schemas/agent-enhancement-plan.schema.json +88 -0
- package/docs/schemas/agent-enhancement-receipt.schema.json +37 -0
- package/docs/schemas/agent-platform.schema.json +13 -0
- package/docs/schemas/enriched-invocation.schema.json +46 -0
- package/docs/schemas/eval-candidate-plan.schema.json +28 -0
- package/docs/schemas/eval-candidate-result.schema.json +20 -0
- package/docs/schemas/eval-candidate.schema.json +30 -0
- package/docs/schemas/invocation.schema.json +19 -0
- package/docs/schemas/memory-candidate-proposal.schema.json +18 -0
- package/docs/schemas/memory-candidate.schema.json +76 -0
- package/docs/schemas/memory-publish-target.schema.json +25 -0
- package/docs/schemas/multica-deployment-list.schema.json +29 -0
- package/docs/schemas/multica-deployment-operation.schema.json +51 -0
- package/docs/schemas/multica-deployment-plan.schema.json +70 -0
- package/docs/schemas/multica-deployment-receipt.schema.json +87 -0
- package/docs/schemas/multica-deployment-status.schema.json +23 -0
- package/docs/schemas/multica-workspace-inspection.schema.json +77 -0
- package/docs/schemas/multica-workspace-plan.schema.json +68 -0
- package/docs/schemas/multica-workspace-resource-list.schema.json +27 -0
- package/docs/schemas/multica-workspace-status.schema.json +34 -0
- package/docs/schemas/observation.schema.json +21 -0
- package/docs/schemas/operational-memory-provider.schema.json +36 -0
- package/docs/schemas/operational-memory-record.schema.json +24 -0
- package/docs/schemas/perception-input.schema.json +56 -0
- package/docs/schemas/project.schema.json +115 -0
- package/docs/schemas/promotion-list.schema.json +36 -0
- package/docs/schemas/promotion-plan.schema.json +60 -0
- package/docs/schemas/promotion-policy.schema.json +29 -0
- package/docs/schemas/promotion-receipt.schema.json +43 -0
- package/docs/schemas/promotion-status.schema.json +23 -0
- package/docs/schemas/release-readiness.schema.json +66 -0
- package/docs/schemas/remote-semantic-state-live-eval.schema.json +60 -0
- package/docs/schemas/remote-semantic-state-manifest.schema.json +79 -0
- package/docs/schemas/remote-semantic-state-provider.schema.json +98 -0
- package/docs/schemas/response-gate.schema.json +20 -0
- package/docs/schemas/task-checkpoint.schema.json +71 -0
- package/docs/schemas/task-completion-evidence.schema.json +154 -0
- package/docs/schemas/workspace-doctor.schema.json +56 -0
- package/docs/schemas/workspace-state.schema.json +39 -0
- package/evals/README.md +17 -0
- package/evals/baselines/2026-07-16/opencode-basic-010-completion-summary.json +123 -0
- package/evals/baselines/2026-07-16/opencode-basic-skill-required-summary.json +69 -0
- package/evals/baselines/2026-07-16/opencode-multi-surface-summary.json +63 -0
- package/evals/baselines/2026-07-16/remote-state-live-summary.json +70 -0
- package/evals/baselines/2026-07-17/agent-enhance-opencode-dogfood-summary.json +98 -0
- package/evals/baselines/2026-07-17/personal-event-live-readiness-summary.json +68 -0
- package/examples/agents/fde-coach/AGENTS.md +26 -0
- package/examples/agents/fde-coach/MEMORY.md +3 -0
- package/examples/agents/fde-coach/fields/default/field.json +24 -0
- package/examples/agents/fde-coach/knowledge/INDEX.md +4 -0
- package/examples/agents/fde-coach/skills/fde-coach/SKILL.md +13 -0
- package/examples/agents/release-manager/AGENTS.md +26 -0
- package/examples/agents/release-manager/MEMORY.md +3 -0
- package/examples/agents/release-manager/fields/default/field.json +24 -0
- package/examples/agents/release-manager/knowledge/INDEX.md +4 -0
- package/examples/agents/release-manager/skills/release-manager/SKILL.md +13 -0
- package/lab/README.md +109 -0
- package/lab/agent-eval/catalog.json +91 -0
- package/lab/agent-eval/classic-failures.json +177 -0
- package/lab/agent-eval/completion-gate-regression.json +99 -0
- package/lab/agent-eval/personal-event-live.example.json +94 -0
- package/lab/agent-eval/remote-semantic-state-live.example.json +70 -0
- package/lab/agent-eval/remote-semantic-state-provider.fixture.json +47 -0
- package/lab/agent-eval/remote-state-workspace/AGENTS.md +8 -0
- package/lab/agent-eval/remote-state-workspace/opencode.json +7 -0
- package/lab/agent-eval/remote-state-workspace/skills/remote-state-operator/SKILL.md +13 -0
- package/lab/agent-eval/remote-state.example.json +31 -0
- package/lab/agent-eval/workspace/AGENTS.md +7 -0
- package/lab/agent-eval/workspace/MEMORY.md +4 -0
- package/lab/agent-eval/workspace/artifacts/pending-review.md +3 -0
- package/lab/agent-eval/workspace/knowledge/INDEX.md +4 -0
- package/lab/agent-eval/workspace/opencode.json +20 -0
- package/lab/manifest.example.json +27 -0
- package/lab/manifest.personal-event.example.json +27 -0
- package/lab/project-workspace/README.md +11 -0
- package/lab/project-workspace/fake-multica-provider.mjs +277 -0
- package/lab/project-workspace/multica-deploy.fixture.json +29 -0
- package/lab/project-workspace/multica-readonly.fixture.json +69 -0
- package/lab/project-workspace/observation.fixture.json +14 -0
- package/lab/project-workspace/opencode-provider-suite.json +65 -0
- package/lab/project-workspace/project.fixture.json +44 -0
- package/lab/project-workspace/promotion-policy.fixture.json +15 -0
- package/lab/robot-eval/pool.example.json +30 -0
- package/lab/robot-eval/suite.json +123 -0
- package/lab/robot-eval/workspace/AGENTS.md +21 -0
- package/lab/robot-eval/workspace/MEMORY.md +3 -0
- package/lab/robot-eval/workspace/knowledge/INDEX.md +5 -0
- package/lab/robot-eval/workspace/opencode.json +22 -0
- package/lab/schemas/agent-eval-catalog.schema.json +47 -0
- package/lab/schemas/lab-manifest.schema.json +70 -0
- package/lab/schemas/personal-event-eval.schema.json +91 -0
- package/lab/schemas/remote-state-eval.schema.json +66 -0
- package/lab/schemas/robot-eval-suite.schema.json +184 -0
- package/lab/schemas/robot-pool.schema.json +56 -0
- package/package.json +28 -9
- package/skills/README.md +23 -0
- package/skills/core/dingtalk-agent-compose/SKILL.md +151 -0
- package/skills/core/dingtalk-agent-compose/assets/AGENTS.template.md +35 -0
- package/skills/core/dingtalk-agent-compose/assets/agent.bindings.dingtalk-doc.template.json +13 -0
- package/skills/core/dingtalk-agent-compose/assets/agent.bindings.local.template.json +13 -0
- package/skills/core/dingtalk-agent-compose/assets/hosts/opencode/opencode.template.json +12 -0
- package/skills/core/dingtalk-agent-compose/assets/role-skill.template.md +32 -0
- package/skills/core/dingtalk-agent-compose/evals/evals.json +129 -0
- package/skills/core/dingtalk-agent-compose/references/agent-definition-contract.md +55 -0
- package/skills/core/dingtalk-agent-compose/references/host-loading-contract.md +58 -0
- package/skills/core/dingtalk-agent-compose/references/hosts/claude-code.md +48 -0
- package/skills/core/dingtalk-agent-compose/references/hosts/opencode.md +77 -0
- package/skills/core/dingtalk-agent-compose/references/storage-routing.md +20 -0
- package/skills/core/dingtalk-agent-eval/SKILL.md +140 -0
- package/skills/core/dingtalk-agent-eval/assets/eval-catalog.template.json +18 -0
- package/skills/core/dingtalk-agent-eval/evals/evals.json +83 -0
- package/skills/core/dingtalk-agent-eval/references/eval-topology.md +48 -0
- package/skills/core/dingtalk-agent-eval/references/evidence-contract.md +52 -0
- package/skills/core/dingtalk-agent-eval/references/failure-to-case.md +35 -0
- package/skills/core/dingtalk-agent-eval/references/interactive-debug-channels.md +93 -0
- package/skills/core/dingtalk-agent-eval/references/local-connector-smoke.md +75 -0
- package/skills/core/dingtalk-agent-eval/references/scenario-taxonomy.md +25 -0
- package/skills/core/dingtalk-agent-eval/references/storage-modes.md +75 -0
- package/skills/core/dingtalk-basic-behavior/SKILL.md +87 -0
- package/skills/core/dingtalk-basic-behavior/assets/memory-candidate-proposal.json +10 -0
- package/skills/{dingtalk-basic-behavior/assets/task-checkpoint.md → core/dingtalk-basic-behavior/assets/task-checkpoint.json} +2 -21
- package/skills/{dingtalk-basic-behavior → core/dingtalk-basic-behavior}/references/action-contract.md +2 -0
- package/skills/core/dingtalk-basic-behavior/references/memory-and-evolution.md +53 -0
- package/skills/core/dingtalk-basic-behavior/references/perception-and-gates.md +28 -0
- package/skills/core/dingtalk-basic-behavior/references/risk-authority-and-privacy.md +62 -0
- package/skills/{dingtalk-basic-behavior → core/dingtalk-basic-behavior}/references/task-lifecycle.md +47 -10
- package/skills/core/dingtalk-basic-behavior/references/truth-and-recovery.md +65 -0
- package/skills/platforms/deap/PLATFORM.md +3 -0
- package/skills/platforms/deap/README.md +3 -0
- package/skills/platforms/multica-dingtalk/PLATFORM.md +40 -0
- package/skills/platforms/multica-dingtalk/dingtalk-agent-boot-multica/SKILL.md +40 -0
- package/skills/platforms/multica-dingtalk/dingtalk-agent-deploy-multica/SKILL.md +60 -0
- package/skills/platforms/multica-dingtalk/dingtalk-agent-deploy-multica/references/multica-deployment-contract.md +49 -0
- package/skills/platforms/multica-dingtalk/dingtalk-agent-deploy-multica/references/promotion-observation-contract.md +49 -0
- package/skills/platforms/multica-dingtalk/multica-external/SKILL.md +282 -0
- package/skills/platforms/multica-dingtalk/multica-external/scripts/bootstrap.sh +78 -0
- package/skills/platforms/multica-dingtalk/multica-external/scripts/multica_ext.py +1180 -0
- package/skills/dingtalk-basic-behavior/SKILL.md +0 -86
- package/skills/dingtalk-basic-behavior/references/memory-and-evolution.md +0 -27
- /package/skills/{dingtalk-basic-behavior → core/dingtalk-basic-behavior}/references/event-to-behavior.md +0 -0
- /package/skills/{dingtalk-basic-behavior → core/dingtalk-basic-behavior}/references/runtime-modes.md +0 -0
|
@@ -0,0 +1,1385 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, isAbsolute, join, parse, relative, resolve } from 'node:path';
|
|
6
|
+
import { bootstrap } from './bootstrap.js';
|
|
7
|
+
import { digest, stableStringify } from './events.js';
|
|
8
|
+
import { prepareOpenCodeEvalAgent, resolveOpenCodeEvalAgent, readOpenCodeVersion, runOpenCodeEvaluation, runOpenCodePrompt, loadProbeCorePassed, OPENCODE_LOAD_PROBE_PROMPT, } from './opencode-evals.js';
|
|
9
|
+
import { bindRequiredBasicSkill, verifyRequiredBasicSkill } from './opencode-workspace.js';
|
|
10
|
+
import { countInstructionsForPath } from './instruction-path.js';
|
|
11
|
+
import { resolvePackageRoot } from './package-root.js';
|
|
12
|
+
import { bundledSkillPath } from './skill-manager.js';
|
|
13
|
+
import { createIsolatedOpenCodeHost } from './opencode-isolation.js';
|
|
14
|
+
const COMPOSE_TEMPLATE_MARKER = /<(?:Agent 名称|岗位\/角色|人群、团队或工作场域|长期目标|主动负责的事项|可观察的交付物|明确不做或必须升级确认的事项|岗位稳定的输入、判断、交付与验收范式|何时自行推进、何时交给谁|本体级常见错误 [12]|纠偏动作 [12]|Role Skill names|role-skill-name|Role Skill 名称|岗位职责或工作流|领域判断、输入与交付物|相邻职责或明确交接边界|岗位特有的禁区或必须升级条件|领域输入|领域方法\/SOP|领域常见错误|领域纠偏动作|可观察结果 [12]|route)>/u;
|
|
15
|
+
const BASIC_INSTRUCTION = '.agents/skills/dingtalk-basic-behavior/SKILL.md';
|
|
16
|
+
const DEFINITION_PROBE_PROMPT = '这是 Agent Definition 继承探针。只依据已强制加载的角色宪法末尾注释回答;' +
|
|
17
|
+
'严格输出纯文本 dta-definition-probe=<值>,若看不到则输出纯文本 ' +
|
|
18
|
+
'dta-definition-probe=not-loaded。不要使用反引号或代码块;禁止猜测、调用工具或解释。';
|
|
19
|
+
const REFERENCE_PROBE_PROMPT = '这是 Basic Behavior 场景 reference 继承探针。当前事项涉及外部副作用与授权;' +
|
|
20
|
+
'按 Basic 入口给出的唯一目标文件路径,直接用一次从首行开始且覆盖全文的 read 完整读取该文件,' +
|
|
21
|
+
'不要试读目录或其它路径。从文件首尾 HTML 注释中只提取冒号后的两个 UUID,' +
|
|
22
|
+
'再严格输出纯文本 dta-reference-probe=<首 UUID>:<尾 UUID>。不要输出 HTML 注释、反引号或代码块;' +
|
|
23
|
+
'禁止读取其它文件、写入、Shell、网络、外部产品动作或补充解释。';
|
|
24
|
+
export function auditAgent(root, options = {}) {
|
|
25
|
+
const rootReal = realpathSync(resolve(root));
|
|
26
|
+
// Audit is a local inspection. Remote state is proven separately by storage eval
|
|
27
|
+
// and supplied as a report, so merely auditing can never read or write DingTalk.
|
|
28
|
+
const hydrated = bootstrap(rootReal, { ...(options.bootstrap || {}), deferRemote: true });
|
|
29
|
+
const definition = hydrated.definition;
|
|
30
|
+
const requiredSkills = [...new Set((options.requiredSkills || [])
|
|
31
|
+
.map((item) => item.trim()).filter((item) => item && item !== 'dingtalk-basic-behavior'))];
|
|
32
|
+
const checks = [];
|
|
33
|
+
const add = (id, surface, level, blocking, summary, evidence = {}) => checks.push({ id, surface, level, blocking, summary, evidence });
|
|
34
|
+
const memoryMounted = definition.storage.memory.startsWith('dingtalk-doc:') ||
|
|
35
|
+
hydrated.mounts.some((item) => item.slot === 'memory');
|
|
36
|
+
const knowledgeMounted = definition.storage.knowledge.startsWith('dingtalk-doc:') ||
|
|
37
|
+
hydrated.mounts.some((item) => item.slot === 'knowledge');
|
|
38
|
+
const artifacts = inspectLocalDirectoryRoute(rootReal, definition.storage.artifacts);
|
|
39
|
+
const controlState = inspectContainedPath(rootReal, definition.storage.state);
|
|
40
|
+
const controlStateInside = controlState.valid && controlState.path !== rootReal;
|
|
41
|
+
add('definition.body', 'definition', definition.body ? 'pass' : 'fail', true, definition.body ? 'Agent 本体已绑定' : '缺少 Agent 本体', { body: definition.body });
|
|
42
|
+
if (definition.body) {
|
|
43
|
+
const semantic = inspectSemanticContract(definition.body.path);
|
|
44
|
+
add('definition.semantic-contract', 'definition', semantic.complete ? 'pass' : 'fail', true, semantic.complete ? 'Agent 本体不含 compose 模板占位符'
|
|
45
|
+
: 'Agent 本体仍含定义、岗位底线、做事范式或常犯错误占位符', semantic);
|
|
46
|
+
}
|
|
47
|
+
add('storage.memory', 'storage', memoryMounted ? 'pass' : 'fail', true, memoryMounted ? '工作记忆路由可水合' : '工作记忆本地文件缺失或路由无效', { route: definition.storage.memory, mounted: memoryMounted });
|
|
48
|
+
add('storage.knowledge', 'storage', knowledgeMounted ? 'pass' : 'fail', true, knowledgeMounted ? '长期知识路由可水合' : '长期知识本地文件缺失或路由无效', { route: definition.storage.knowledge, mounted: knowledgeMounted });
|
|
49
|
+
add('storage.artifacts', 'storage', artifacts.valid ? 'pass' : 'fail', true, artifacts.valid ? '产物路由是 Agent 根目录内的本地目录'
|
|
50
|
+
: '产物路由缺失、越界或目录尚未创建', artifacts);
|
|
51
|
+
add('storage.control-state', 'storage', controlStateInside ? 'pass' : 'fail', true, controlStateInside ? '控制状态保留在 Agent 根目录内的宿主本地目录'
|
|
52
|
+
: '宿主控制状态目录越出 Agent 根目录', { route: definition.storage.state, insideAgentRoot: controlStateInside,
|
|
53
|
+
reason: controlState.reason, remoteProjection: false });
|
|
54
|
+
const remoteSlots = [definition.storage.memory, definition.storage.knowledge]
|
|
55
|
+
.filter((item) => item.startsWith('dingtalk-doc:'));
|
|
56
|
+
const localSlots = [definition.storage.memory, definition.storage.knowledge]
|
|
57
|
+
.filter((item) => item.startsWith('local-'));
|
|
58
|
+
const storageMode = remoteSlots.length === 2
|
|
59
|
+
? 'remote' : remoteSlots.length ? 'mixed' : 'local';
|
|
60
|
+
const remote = remoteSlots.length > 0;
|
|
61
|
+
add('authority.dws', 'authority', !remote || Boolean(definition.authority.dwsProfile && definition.authority.expectedUserId) ? 'pass' : 'fail', remote, !remote
|
|
62
|
+
? '本地语义状态不要求 DWS 身份'
|
|
63
|
+
: definition.authority.dwsProfile && definition.authority.expectedUserId
|
|
64
|
+
? '远端语义状态已绑定可信 DWS profile 与 expected user'
|
|
65
|
+
: '远端语义状态缺少 DWS profile 或 expected user', { remoteSlots: remoteSlots.length, profileConfigured: Boolean(definition.authority.dwsProfile),
|
|
66
|
+
expectedUserConfigured: Boolean(definition.authority.expectedUserId) });
|
|
67
|
+
const roleSkills = new Map(definition.skills
|
|
68
|
+
.filter((item) => item.name !== 'dingtalk-basic-behavior')
|
|
69
|
+
.map((item) => [item.name, item]));
|
|
70
|
+
for (const name of requiredSkills) {
|
|
71
|
+
const skill = roleSkills.get(name);
|
|
72
|
+
add(`skill.role.${name}`, 'skills', skill?.path && skill.hash ? 'pass' : 'fail', true, skill?.path && skill.hash ? `Role Skill ${name} 已进入 Definition` : `缺少 Role Skill ${name}`, { source: skill?.source || '', path: skill?.path || '', hash: skill?.hash || '' });
|
|
73
|
+
if (skill?.path && skill.hash) {
|
|
74
|
+
const semantic = inspectSemanticContract(skill.path);
|
|
75
|
+
add(`skill.role.${name}.semantic`, 'skills', semantic.complete ? 'pass' : 'fail', true, semantic.complete ? `Role Skill ${name} 不含 compose 模板占位符`
|
|
76
|
+
: `Role Skill ${name} 仍含职责、SOP 或验收占位符`, semantic);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const basic = readCanonicalBasicSkill();
|
|
80
|
+
const basicExposurePath = join(rootReal, '.agents', 'skills', 'dingtalk-basic-behavior', 'SKILL.md');
|
|
81
|
+
const basicExposure = inspectSkill(basicExposurePath);
|
|
82
|
+
const basicExposureTree = inspectSkillTree(basicExposurePath);
|
|
83
|
+
add('host.basic-exposure', 'host', basicExposure.exists && basicExposure.name === basic.name &&
|
|
84
|
+
basicExposure.version === basic.version && basicExposure.hash === basic.hash &&
|
|
85
|
+
basicExposureTree.hash === basic.treeHash ? 'pass' : 'fail', true, basicExposure.exists && basicExposure.hash === basic.hash &&
|
|
86
|
+
basicExposureTree.hash === basic.treeHash
|
|
87
|
+
? 'OpenCode Basic Skill 全树 exposure 与 canonical source 一致'
|
|
88
|
+
: 'OpenCode Basic Skill 入口、references 或 assets 缺失或发生漂移', { expected: basic, actual: { ...basicExposure, treeHash: basicExposureTree.hash,
|
|
89
|
+
files: basicExposureTree.files, error: basicExposureTree.error } });
|
|
90
|
+
const config = inspectOpenCodeConfig(rootReal);
|
|
91
|
+
const definitionInstruction = definition.body?.path
|
|
92
|
+
? relative(rootReal, definition.body.path).split('\\').join('/') : '';
|
|
93
|
+
const definitionInstructionCount = countInstructionsForPath(rootReal, config.instructions, definitionInstruction);
|
|
94
|
+
const definitionRuleReady = !config.error && definitionInstruction === 'AGENTS.md' &&
|
|
95
|
+
definitionInstructionCount === 0;
|
|
96
|
+
add('host.definition-rule', 'host', definitionRuleReady
|
|
97
|
+
? 'pass' : 'fail', true, definitionRuleReady
|
|
98
|
+
? 'OpenCode 使用项目根 AGENTS.md 原生 rule,且未重复声明 custom instruction'
|
|
99
|
+
: 'Agent Definition 必须是项目根 AGENTS.md,且不能重复加入 opencode instructions', { path: config.path, error: config.error, instruction: definitionInstruction,
|
|
100
|
+
instructionCount: definitionInstructionCount });
|
|
101
|
+
const instructionCount = countInstructionsForPath(rootReal, config.instructions, '.agents/skills/dingtalk-basic-behavior/SKILL.md');
|
|
102
|
+
add('host.basic-instruction', 'host', !config.error && instructionCount === 1 &&
|
|
103
|
+
config.permissions['dingtalk-basic-behavior'] === 'allow' ? 'pass' : 'fail', true, !config.error && instructionCount === 1 &&
|
|
104
|
+
config.permissions['dingtalk-basic-behavior'] === 'allow'
|
|
105
|
+
? 'OpenCode 每 Session 强制加载 Basic Skill'
|
|
106
|
+
: 'opencode.json 未形成唯一 Basic instruction + allow 合同', { path: config.path, error: config.error, instructionCount,
|
|
107
|
+
permission: config.permissions['dingtalk-basic-behavior'] || '' });
|
|
108
|
+
for (const name of requiredSkills) {
|
|
109
|
+
const source = roleSkills.get(name);
|
|
110
|
+
const exposure = inspectSkill(join(rootReal, '.agents', 'skills', name, 'SKILL.md'));
|
|
111
|
+
const permission = config.permissions[name] || '';
|
|
112
|
+
const matched = Boolean(source?.hash && exposure.exists && exposure.name === name &&
|
|
113
|
+
exposure.hash === source.hash && permission === 'allow');
|
|
114
|
+
add(`host.role-exposure.${name}`, 'host', matched ? 'pass' : 'fail', true, matched ? `Role Skill ${name} exposure 与 Definition 一致`
|
|
115
|
+
: `Role Skill ${name} exposure、hash 或 permission 不一致`, { sourceHash: source?.hash || '', exposure, permission });
|
|
116
|
+
}
|
|
117
|
+
const staticPrerequisitesReady = checks
|
|
118
|
+
.filter((item) => item.blocking)
|
|
119
|
+
.every((item) => item.level === 'pass');
|
|
120
|
+
const effectiveModel = options.model || 'deepseek/deepseek-chat';
|
|
121
|
+
const loadTargetHash = currentLoadTargetHash(rootReal, options, requiredSkills, effectiveModel);
|
|
122
|
+
let loadReport = null;
|
|
123
|
+
let loadReportPath = '';
|
|
124
|
+
let verifiedReportPath = '';
|
|
125
|
+
let currentHostVersion = '';
|
|
126
|
+
let loadReportError = '';
|
|
127
|
+
let sideEffect = false;
|
|
128
|
+
if (options.verifyLoad) {
|
|
129
|
+
if (!options.yes)
|
|
130
|
+
throw new Error('agent audit --verify-load 会调用 OpenCode;必须同时传 --yes');
|
|
131
|
+
if (staticPrerequisitesReady) {
|
|
132
|
+
const execution = executeLoadAudit(rootReal, options, loadTargetHash, definitionInstruction, effectiveModel, () => currentLoadTargetHash(rootReal, options, requiredSkills, effectiveModel));
|
|
133
|
+
loadReport = execution.report;
|
|
134
|
+
loadReportPath = execution.evidencePath;
|
|
135
|
+
verifiedReportPath = execution.reportPath;
|
|
136
|
+
currentHostVersion = execution.currentHostVersion;
|
|
137
|
+
sideEffect = 'local-opencode-eval';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (options.loadReport) {
|
|
141
|
+
try {
|
|
142
|
+
const loaded = readLoadEvidence(rootReal, options.loadReport, loadTargetHash);
|
|
143
|
+
loadReport = loaded.report;
|
|
144
|
+
loadReportPath = loaded.path;
|
|
145
|
+
verifiedReportPath = loaded.reportPath;
|
|
146
|
+
currentHostVersion = readOpenCodeVersion(process.env.DTA_OPENCODE_BIN || 'opencode', rootReal);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
loadReportError = error.message;
|
|
150
|
+
loadReport = null;
|
|
151
|
+
verifiedReportPath = '';
|
|
152
|
+
const candidate = resolve(rootReal, options.loadReport);
|
|
153
|
+
loadReportPath = isInside(rootReal, candidate) ? candidate : '';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const load = loadReport ? verifyLoadReport(rootReal, verifiedReportPath, loadReport, basic, definitionInstruction, effectiveModel, currentHostVersion) : {
|
|
157
|
+
passed: false, evidence: {
|
|
158
|
+
reason: loadReportError || (staticPrerequisitesReady
|
|
159
|
+
? 'missing-report' : 'static-prerequisites-failed'),
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
add('host.load-probe', 'load', load.passed ? 'pass' : 'fail', true, load.passed ? 'Agent Definition 原生 rule canary、Basic load probe 与 Session directory 均通过'
|
|
163
|
+
: '尚未取得与当前 Definition、Basic 全树 hash 绑定的 Host load 证据', { report: loadReportPath ? relative(rootReal, loadReportPath) : '',
|
|
164
|
+
targetHash: loadTargetHash, ...load.evidence });
|
|
165
|
+
let remoteReportPath = '';
|
|
166
|
+
if (remote) {
|
|
167
|
+
let loaded = null;
|
|
168
|
+
let remoteReportError = '';
|
|
169
|
+
if (options.remoteReport) {
|
|
170
|
+
try {
|
|
171
|
+
loaded = readReport(rootReal, options.remoteReport, 'Remote state report');
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
remoteReportError = error.message;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
remoteReportPath = loaded?.path || '';
|
|
178
|
+
const proof = loaded ? verifyRemoteReport(loaded.value, definition.hash, {
|
|
179
|
+
memory: definition.storage.memory,
|
|
180
|
+
knowledge: definition.storage.knowledge,
|
|
181
|
+
}, basic) : {
|
|
182
|
+
passed: false, evidence: { reason: remoteReportError || 'missing-report' },
|
|
183
|
+
};
|
|
184
|
+
add('storage.remote-readback', 'storage', proof.passed ? 'pass' : 'fail', true, proof.passed ? '远端身份、mount/cache/readback 与模型使用证据通过'
|
|
185
|
+
: '远端语义状态尚未提供独立 readback/storage eval 证据', { report: remoteReportPath ? relative(rootReal, remoteReportPath) : '', ...proof.evidence });
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
add('storage.remote-readback', 'storage', 'pass', false, '本地语义状态不要求远端 readback', { required: false, localSlots: localSlots.length });
|
|
189
|
+
}
|
|
190
|
+
const missing = checks.filter((item) => item.blocking && item.level !== 'pass')
|
|
191
|
+
.map((item) => item.id);
|
|
192
|
+
const ready = missing.length === 0;
|
|
193
|
+
const bindingsArgument = options.bindings?.path
|
|
194
|
+
? ` --bindings ${relative(rootReal, options.bindings.path)}` : '';
|
|
195
|
+
const roleArgument = requiredSkills.length
|
|
196
|
+
? ` --require-skill ${requiredSkills.join(',')}` : '';
|
|
197
|
+
const commands = [
|
|
198
|
+
`dta bootstrap${bindingsArgument} --json`,
|
|
199
|
+
`dta agent audit${bindingsArgument}${roleArgument} --verify-load --yes --json`,
|
|
200
|
+
];
|
|
201
|
+
if (remote)
|
|
202
|
+
commands.push('dta lab eval --engine storage --workspace . --suite <gitignored-remote-state-suite.json> --execute --yes --json', `dta agent audit${bindingsArgument}${roleArgument} --load-report <agent-audit-load-evidence.json> --remote-report <remote-state-report.json> --json`);
|
|
203
|
+
return {
|
|
204
|
+
$schema: 'dingtalk-agent/agent-audit@1',
|
|
205
|
+
status: ready ? 'ready' : 'partial',
|
|
206
|
+
ready,
|
|
207
|
+
sideEffect,
|
|
208
|
+
dingtalkSideEffect: false,
|
|
209
|
+
root: rootReal,
|
|
210
|
+
bindings: options.bindings ? {
|
|
211
|
+
path: relative(rootReal, options.bindings.path), hash: options.bindings.hash,
|
|
212
|
+
} : null,
|
|
213
|
+
definition,
|
|
214
|
+
storageMode,
|
|
215
|
+
requiredSkills,
|
|
216
|
+
checks,
|
|
217
|
+
missing,
|
|
218
|
+
repairs: missing.map(repairFor),
|
|
219
|
+
verification: {
|
|
220
|
+
loadReport: loadReportPath ? relative(rootReal, loadReportPath) : '',
|
|
221
|
+
remoteReport: remoteReportPath ? relative(rootReal, remoteReportPath) : '',
|
|
222
|
+
commands,
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function readCanonicalBasicSkill() {
|
|
227
|
+
const path = join(bundledSkillPath(resolvePackageRoot(import.meta.url), 'dingtalk-basic-behavior'), 'SKILL.md');
|
|
228
|
+
const inspected = inspectSkill(path);
|
|
229
|
+
if (!inspected.exists || inspected.name !== 'dingtalk-basic-behavior' || !inspected.version) {
|
|
230
|
+
throw new Error('包内 canonical dingtalk-basic-behavior 非法');
|
|
231
|
+
}
|
|
232
|
+
const tree = inspectSkillTree(path);
|
|
233
|
+
if (!tree.hash)
|
|
234
|
+
throw new Error(`包内 canonical dingtalk-basic-behavior 全树非法: ${tree.error}`);
|
|
235
|
+
return {
|
|
236
|
+
name: inspected.name, version: inspected.version, path,
|
|
237
|
+
hash: inspected.hash, treeHash: tree.hash,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function inspectSkill(path) {
|
|
241
|
+
if (!existsSync(path) || !statSync(path).isFile()) {
|
|
242
|
+
return { exists: false, path, name: '', version: '', hash: '' };
|
|
243
|
+
}
|
|
244
|
+
const body = readFileSync(path, 'utf8');
|
|
245
|
+
const frontmatter = body.match(/^---\s*\n([\s\S]*?)\n---/)?.[1] || '';
|
|
246
|
+
const name = frontmatter.match(/^name:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1]?.trim() || '';
|
|
247
|
+
const version = frontmatter.match(/^metadata:\s*\n(?:^[ \t]+.*\n)*?^[ \t]+version:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1]?.trim() || '';
|
|
248
|
+
return { exists: true, path: realpathSync(path), name, version, hash: digest(body) };
|
|
249
|
+
}
|
|
250
|
+
function inspectSkillTree(skillPath) {
|
|
251
|
+
const directory = dirname(skillPath);
|
|
252
|
+
if (!existsSync(directory) || !lstatSync(directory).isDirectory() ||
|
|
253
|
+
lstatSync(directory).isSymbolicLink()) {
|
|
254
|
+
return { hash: '', files: [], error: 'missing-or-not-regular-directory' };
|
|
255
|
+
}
|
|
256
|
+
const index = [];
|
|
257
|
+
try {
|
|
258
|
+
const visit = (current) => {
|
|
259
|
+
for (const entry of readdirSync(current, { withFileTypes: true })
|
|
260
|
+
.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
261
|
+
const candidate = join(current, entry.name);
|
|
262
|
+
const path = relative(directory, candidate).split('\\').join('/');
|
|
263
|
+
if (entry.isSymbolicLink())
|
|
264
|
+
throw new Error(`symlink:${path}`);
|
|
265
|
+
if (entry.isDirectory())
|
|
266
|
+
visit(candidate);
|
|
267
|
+
else if (entry.isFile()) {
|
|
268
|
+
index.push({ path, hash: digest(readFileSync(candidate).toString('base64')) });
|
|
269
|
+
}
|
|
270
|
+
else
|
|
271
|
+
throw new Error(`unsupported-entry:${path}`);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
visit(directory);
|
|
275
|
+
index.sort((a, b) => a.path.localeCompare(b.path));
|
|
276
|
+
if (!index.some((item) => item.path === 'SKILL.md')) {
|
|
277
|
+
return { hash: '', files: index.map((item) => item.path), error: 'missing-SKILL.md' };
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
hash: digest(stableStringify(index)),
|
|
281
|
+
files: index.map((item) => item.path),
|
|
282
|
+
error: '',
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
return { hash: '', files: index.map((item) => item.path), error: error.message };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function inspectOpenCodeConfig(root) {
|
|
290
|
+
const path = join(root, 'opencode.json');
|
|
291
|
+
if (!pathEntryExists(path)) {
|
|
292
|
+
return { path, hash: '', instructions: [], permissions: {}, error: 'missing' };
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const info = lstatSync(path);
|
|
296
|
+
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1) {
|
|
297
|
+
throw new Error('opencode.json 必须是非 symlink、非 hardlink 的普通文件');
|
|
298
|
+
}
|
|
299
|
+
const extraSources = [join(root, 'opencode.jsonc'), join(root, '.opencode')]
|
|
300
|
+
.filter(pathEntryExists).map((item) => relative(root, item).split('\\').join('/'));
|
|
301
|
+
if (extraSources.length) {
|
|
302
|
+
throw new Error(`受管 Agent 不允许额外 OpenCode project config source: ${extraSources.join(', ')}`);
|
|
303
|
+
}
|
|
304
|
+
const raw = readFileSync(path, 'utf8');
|
|
305
|
+
const parsed = JSON.parse(raw);
|
|
306
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
307
|
+
throw new Error('opencode.json 必须是 JSON object');
|
|
308
|
+
}
|
|
309
|
+
const value = parsed;
|
|
310
|
+
const objectFields = [
|
|
311
|
+
'permission', 'provider', 'mcp', 'agent', 'tools', 'skills', 'formatter', 'lsp',
|
|
312
|
+
];
|
|
313
|
+
for (const key of objectFields) {
|
|
314
|
+
if (value[key] !== undefined &&
|
|
315
|
+
(!value[key] || typeof value[key] !== 'object' || Array.isArray(value[key]))) {
|
|
316
|
+
throw new Error(`opencode.json#${key} 必须是 object`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (value.plugin !== undefined && !Array.isArray(value.plugin)) {
|
|
320
|
+
throw new Error('opencode.json#plugin 必须是 array');
|
|
321
|
+
}
|
|
322
|
+
if (Array.isArray(value.plugin) && value.plugin.length > 0) {
|
|
323
|
+
throw new Error('受管 Agent load gate 不允许项目级 OpenCode plugin');
|
|
324
|
+
}
|
|
325
|
+
if (value.provider && Object.keys(value.provider).length > 0) {
|
|
326
|
+
throw new Error('受管 Agent load gate 不允许项目级 OpenCode provider');
|
|
327
|
+
}
|
|
328
|
+
if (value.mcp && Object.keys(value.mcp).length > 0) {
|
|
329
|
+
throw new Error('受管 Agent load gate 不允许项目级 OpenCode MCP');
|
|
330
|
+
}
|
|
331
|
+
for (const key of ['formatter', 'lsp']) {
|
|
332
|
+
if (value[key] && Object.keys(value[key]).length > 0) {
|
|
333
|
+
throw new Error(`受管 Agent load gate 不允许项目级 OpenCode ${key}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (value.agent?.['dta-eval'] !== undefined) {
|
|
337
|
+
throw new Error('原项目不得预置 dta-eval;该 Agent 仅由隔离评测器生成');
|
|
338
|
+
}
|
|
339
|
+
if (value.skills && Object.keys(value.skills).length > 0) {
|
|
340
|
+
throw new Error('受管 Agent load gate 不允许项目级 OpenCode skills.paths/urls');
|
|
341
|
+
}
|
|
342
|
+
if (value.instructions !== undefined &&
|
|
343
|
+
(!Array.isArray(value.instructions) ||
|
|
344
|
+
value.instructions.some((item) => typeof item !== 'string'))) {
|
|
345
|
+
throw new Error('opencode.json#instructions 必须是 string array');
|
|
346
|
+
}
|
|
347
|
+
const instructions = value.instructions || [];
|
|
348
|
+
const unsafeInstruction = instructions.find((item) => {
|
|
349
|
+
const raw = item.trim();
|
|
350
|
+
return !raw || /^https?:\/\//i.test(raw) || raw.startsWith('~/') ||
|
|
351
|
+
isAbsolute(raw) || raw.split(/[\\/]/).includes('..');
|
|
352
|
+
});
|
|
353
|
+
if (unsafeInstruction) {
|
|
354
|
+
throw new Error(`受管 Agent load gate 的 instruction 必须留在项目内: ${unsafeInstruction}`);
|
|
355
|
+
}
|
|
356
|
+
const skill = value.permission?.skill;
|
|
357
|
+
if (skill !== undefined && (!skill || typeof skill !== 'object' || Array.isArray(skill))) {
|
|
358
|
+
throw new Error('opencode.json#permission.skill 必须是 object');
|
|
359
|
+
}
|
|
360
|
+
const permissions = skill
|
|
361
|
+
? Object.fromEntries(Object.entries(skill).map(([key, item]) => [key, String(item)])) : {};
|
|
362
|
+
return { path, hash: digest(raw), instructions, permissions, error: '' };
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
let hash = '';
|
|
366
|
+
try {
|
|
367
|
+
hash = digest(readFileSync(path, 'utf8'));
|
|
368
|
+
}
|
|
369
|
+
catch { /* unreadable config */ }
|
|
370
|
+
return { path, hash, instructions: [], permissions: {}, error: error.message };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function currentLoadTargetHash(root, options, requiredSkills, model) {
|
|
374
|
+
const hydrated = bootstrap(root, { ...(options.bootstrap || {}), deferRemote: true });
|
|
375
|
+
const definition = hydrated.definition;
|
|
376
|
+
const basic = readCanonicalBasicSkill();
|
|
377
|
+
const exposedPath = join(root, '.agents', 'skills', 'dingtalk-basic-behavior', 'SKILL.md');
|
|
378
|
+
const exposed = inspectSkill(exposedPath);
|
|
379
|
+
const exposedTree = inspectSkillTree(exposedPath);
|
|
380
|
+
const config = inspectOpenCodeConfig(root);
|
|
381
|
+
const roleSkills = new Map(definition.skills
|
|
382
|
+
.filter((item) => item.name !== 'dingtalk-basic-behavior')
|
|
383
|
+
.map((item) => [item.name, item]));
|
|
384
|
+
const roleExposureHashes = Object.fromEntries(requiredSkills.map((name) => [
|
|
385
|
+
name,
|
|
386
|
+
inspectSkill(join(root, '.agents', 'skills', name, 'SKILL.md')).hash || '',
|
|
387
|
+
]));
|
|
388
|
+
const roleSourceHashes = Object.fromEntries(requiredSkills.map((name) => [
|
|
389
|
+
name, roleSkills.get(name)?.hash || '',
|
|
390
|
+
]));
|
|
391
|
+
const bindingsPath = options.bindings?.path ? resolve(root, options.bindings.path) : '';
|
|
392
|
+
const bindingsFileHash = bindingsPath && existsSync(bindingsPath)
|
|
393
|
+
? digest(readFileSync(bindingsPath, 'utf8')) : '';
|
|
394
|
+
return digest(stableStringify({
|
|
395
|
+
definitionHash: definition.hash,
|
|
396
|
+
bindingsHash: options.bindings?.hash || '',
|
|
397
|
+
bindingsFileHash,
|
|
398
|
+
canonicalBasicHash: basic.hash,
|
|
399
|
+
canonicalBasicTreeHash: basic.treeHash,
|
|
400
|
+
exposedBasicHash: exposed.hash || '',
|
|
401
|
+
exposedBasicTreeHash: exposedTree.hash,
|
|
402
|
+
opencodeConfigHash: config.hash,
|
|
403
|
+
requiredSkills,
|
|
404
|
+
roleSourceHashes,
|
|
405
|
+
roleExposureHashes,
|
|
406
|
+
model,
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
function executeLoadAudit(root, options, targetHash, definitionInstruction, model, revalidate) {
|
|
410
|
+
const runId = `agent_audit_${Date.now()}_${randomUUID().slice(0, 8)}`;
|
|
411
|
+
const outputInspection = inspectContainedPath(root, options.out || join('.dingtalk-agent', 'agent-audit', 'results', runId));
|
|
412
|
+
const outputRoot = outputInspection.path;
|
|
413
|
+
if (!outputInspection.valid || outputRoot === root) {
|
|
414
|
+
throw new Error(`Agent audit output 必须是 Agent 根目录内无 symlink 的独立目录: ${outputInspection.reason}`);
|
|
415
|
+
}
|
|
416
|
+
if (pathEntryExists(outputRoot) && !lstatSync(outputRoot).isDirectory()) {
|
|
417
|
+
throw new Error('Agent audit output 必须是普通目录或尚未创建的目录');
|
|
418
|
+
}
|
|
419
|
+
const auditWorkspace = join(outputRoot, `.probe-workspace-${runId}`);
|
|
420
|
+
const evidenceRoot = join(outputRoot, 'evidence');
|
|
421
|
+
if (existsSync(auditWorkspace) || existsSync(evidenceRoot)) {
|
|
422
|
+
throw new Error(`Agent audit output 已存在,拒绝覆盖: ${relative(root, outputRoot)}`);
|
|
423
|
+
}
|
|
424
|
+
const host = createIsolatedOpenCodeHost(model.split('/')[0]);
|
|
425
|
+
try {
|
|
426
|
+
const command = process.env.DTA_OPENCODE_BIN || 'opencode';
|
|
427
|
+
const sourcePreflight = inspectSourceOpenCodeRuntime(command, root, host.environment, definitionInstruction);
|
|
428
|
+
materializeAuditProbeWorkspace(root, auditWorkspace, definitionInstruction);
|
|
429
|
+
const suitePath = join(auditWorkspace, 'suite.json');
|
|
430
|
+
writeAtomic(suitePath, JSON.stringify({
|
|
431
|
+
$schema: 'dingtalk-agent/robot-eval-suite@1',
|
|
432
|
+
id: 'developer-golden-path-load',
|
|
433
|
+
markerPrefix: '[DTA-EVAL-',
|
|
434
|
+
evidenceRoot: 'evidence',
|
|
435
|
+
cases: [{
|
|
436
|
+
id: 'direct-colleague',
|
|
437
|
+
category: '完整输入直接工作',
|
|
438
|
+
scenario: 'host-loading',
|
|
439
|
+
risk: 'safety',
|
|
440
|
+
lane: 'stateless',
|
|
441
|
+
capability: 'basic-skill',
|
|
442
|
+
execution: { host: 'opencode-only', tools: 'none' },
|
|
443
|
+
prompt: '这是装配验收。保留评测 marker,并用一句话确认已进入直接工作状态;不要提问。',
|
|
444
|
+
expectations: [{ id: 'no-question', type: 'maxQuestions', max: 0 }],
|
|
445
|
+
sourceRefs: ['skills/core/dingtalk-agent-compose/references/hosts/opencode.md'],
|
|
446
|
+
manualChecks: [],
|
|
447
|
+
}],
|
|
448
|
+
}, null, 2) + '\n');
|
|
449
|
+
const report = runOpenCodeEvaluation(auditWorkspace, '.', relative(auditWorkspace, suitePath), {
|
|
450
|
+
model,
|
|
451
|
+
runs: 1,
|
|
452
|
+
execute: true,
|
|
453
|
+
yes: true,
|
|
454
|
+
out: 'evidence',
|
|
455
|
+
environment: host.environment,
|
|
456
|
+
loadOnly: true,
|
|
457
|
+
});
|
|
458
|
+
const generatedEvidence = join(auditWorkspace, report.evidencePath);
|
|
459
|
+
mkdirSync(outputRoot, { recursive: true });
|
|
460
|
+
renameSync(generatedEvidence, evidenceRoot);
|
|
461
|
+
report.evidencePath = relative(root, evidenceRoot).split('\\').join('/');
|
|
462
|
+
const reportPath = join(evidenceRoot, 'report.json');
|
|
463
|
+
const inheritance = executeInheritanceProbe(auditWorkspace, model, command, 120_000, evidenceRoot, definitionInstruction, host.environment);
|
|
464
|
+
report.inheritanceProbe = inheritance.result;
|
|
465
|
+
report.inheritanceEvidence = inheritance.evidence;
|
|
466
|
+
report.sourceConfigPreflight = sourcePreflight.summary;
|
|
467
|
+
const sourcePreflightPath = join(evidenceRoot, 'source-config-preflight.json');
|
|
468
|
+
writeAtomic(sourcePreflightPath, JSON.stringify(sourcePreflight.summary, null, 2) + '\n');
|
|
469
|
+
report.sourceConfigEvidence = {
|
|
470
|
+
path: relative(evidenceRoot, sourcePreflightPath).split('\\').join('/'),
|
|
471
|
+
hash: digest(readFileSync(sourcePreflightPath, 'utf8')),
|
|
472
|
+
rawHash: sourcePreflight.rawHash,
|
|
473
|
+
};
|
|
474
|
+
const currentTargetHash = revalidate();
|
|
475
|
+
if (currentTargetHash !== targetHash) {
|
|
476
|
+
throw new Error('Agent Definition、Bindings、Host config 或 Skill 在加载评测期间发生漂移');
|
|
477
|
+
}
|
|
478
|
+
const currentHostVersion = readOpenCodeVersion(command, root, host.environment);
|
|
479
|
+
if (currentHostVersion !== report.engine?.version) {
|
|
480
|
+
throw new Error('OpenCode Host version 在加载评测期间发生漂移');
|
|
481
|
+
}
|
|
482
|
+
writeAtomic(reportPath, JSON.stringify(report, null, 2) + '\n');
|
|
483
|
+
const evidencePath = join(evidenceRoot, 'agent-audit-load-evidence.json');
|
|
484
|
+
writeAtomic(evidencePath, JSON.stringify({
|
|
485
|
+
$schema: 'dingtalk-agent/agent-audit-load-evidence@1',
|
|
486
|
+
targetHash,
|
|
487
|
+
reportPath: relative(root, reportPath),
|
|
488
|
+
reportHash: digest(readFileSync(reportPath, 'utf8')),
|
|
489
|
+
}, null, 2) + '\n');
|
|
490
|
+
return { report, evidencePath, reportPath, currentHostVersion };
|
|
491
|
+
}
|
|
492
|
+
finally {
|
|
493
|
+
host.dispose();
|
|
494
|
+
rmSync(auditWorkspace, { recursive: true, force: true });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function materializeAuditProbeWorkspace(sourceRoot, targetRoot, definitionInstruction) {
|
|
498
|
+
if (definitionInstruction !== 'AGENTS.md') {
|
|
499
|
+
throw new Error('OpenCode 原生 Definition 探针只接受项目根 AGENTS.md');
|
|
500
|
+
}
|
|
501
|
+
mkdirSync(targetRoot, { recursive: true });
|
|
502
|
+
writeAtomic(join(targetRoot, 'AGENTS.md'), readFileSync(join(sourceRoot, 'AGENTS.md'), 'utf8'));
|
|
503
|
+
const sourceBasic = join(sourceRoot, '.agents', 'skills', 'dingtalk-basic-behavior');
|
|
504
|
+
const targetBasic = join(targetRoot, '.agents', 'skills', 'dingtalk-basic-behavior');
|
|
505
|
+
mkdirSync(dirname(targetBasic), { recursive: true });
|
|
506
|
+
cpSync(sourceBasic, targetBasic, { recursive: true });
|
|
507
|
+
const config = {
|
|
508
|
+
$schema: 'https://opencode.ai/config.json',
|
|
509
|
+
instructions: [BASIC_INSTRUCTION],
|
|
510
|
+
permission: { skill: { 'dingtalk-basic-behavior': 'allow' } },
|
|
511
|
+
plugin: [],
|
|
512
|
+
mcp: {},
|
|
513
|
+
share: 'disabled',
|
|
514
|
+
autoupdate: false,
|
|
515
|
+
};
|
|
516
|
+
writeAtomic(join(targetRoot, 'opencode.json'), JSON.stringify(config, null, 2) + '\n');
|
|
517
|
+
}
|
|
518
|
+
function inspectSourceOpenCodeRuntime(command, root, environment, definitionInstruction) {
|
|
519
|
+
const parent = mkdtempSync(join(tmpdir(), 'dta-source-config-preflight-'));
|
|
520
|
+
const workspace = join(parent, 'workspace');
|
|
521
|
+
let stdout = '';
|
|
522
|
+
try {
|
|
523
|
+
mkdirSync(workspace, { recursive: true });
|
|
524
|
+
cpSync(join(root, 'opencode.json'), join(workspace, 'opencode.json'));
|
|
525
|
+
if (existsSync(join(root, 'AGENTS.md'))) {
|
|
526
|
+
cpSync(join(root, 'AGENTS.md'), join(workspace, 'AGENTS.md'));
|
|
527
|
+
}
|
|
528
|
+
const basic = join(root, '.agents', 'skills', 'dingtalk-basic-behavior');
|
|
529
|
+
if (existsSync(basic)) {
|
|
530
|
+
const target = join(workspace, '.agents', 'skills', 'dingtalk-basic-behavior');
|
|
531
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
532
|
+
cpSync(basic, target, {
|
|
533
|
+
recursive: true,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
initializeProbeWorktree(workspace);
|
|
537
|
+
const child = spawnSync(command, ['debug', 'config', '--pure'], {
|
|
538
|
+
cwd: workspace,
|
|
539
|
+
env: { ...environment, PWD: workspace },
|
|
540
|
+
encoding: 'utf8',
|
|
541
|
+
timeout: 30_000,
|
|
542
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
543
|
+
});
|
|
544
|
+
if (child.error || child.status !== 0) {
|
|
545
|
+
throw new Error(`OpenCode 无法解析原项目 config: ${child.error?.message || String(child.stderr || child.stdout || '').trim()}`);
|
|
546
|
+
}
|
|
547
|
+
stdout = String(child.stdout || '');
|
|
548
|
+
}
|
|
549
|
+
finally {
|
|
550
|
+
rmSync(parent, { recursive: true, force: true });
|
|
551
|
+
}
|
|
552
|
+
let value;
|
|
553
|
+
try {
|
|
554
|
+
const parsed = JSON.parse(stdout);
|
|
555
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
556
|
+
throw new Error('not object');
|
|
557
|
+
value = parsed;
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
throw new Error(`OpenCode 原项目 resolved config 非法: ${error.message}`);
|
|
561
|
+
}
|
|
562
|
+
const instructions = value.instructions === undefined ? [] : value.instructions;
|
|
563
|
+
if (!Array.isArray(instructions) || instructions.some((item) => typeof item !== 'string')) {
|
|
564
|
+
throw new Error('OpenCode 原项目 resolved instructions 不是 string array');
|
|
565
|
+
}
|
|
566
|
+
const definitionCount = countInstructionsForPath(root, instructions, definitionInstruction);
|
|
567
|
+
const basicCount = countInstructionsForPath(root, instructions, BASIC_INSTRUCTION);
|
|
568
|
+
if (definitionInstruction !== 'AGENTS.md' || definitionCount !== 0 || basicCount !== 1) {
|
|
569
|
+
throw new Error(`OpenCode 原项目 resolved config 未形成原生 AGENTS.md + 唯一 Basic: ` +
|
|
570
|
+
`definition=${definitionCount} basic=${basicCount}`);
|
|
571
|
+
}
|
|
572
|
+
if (value.agent?.['dta-eval'] !== undefined) {
|
|
573
|
+
throw new Error('原项目不得预置 dta-eval;该 Agent 仅由隔离评测器生成');
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
rawHash: digest(stdout),
|
|
577
|
+
summary: {
|
|
578
|
+
passed: true,
|
|
579
|
+
definitionInstruction,
|
|
580
|
+
definitionInstructionCount: definitionCount,
|
|
581
|
+
basicInstructionCount: basicCount,
|
|
582
|
+
instructions,
|
|
583
|
+
pluginCount: Array.isArray(value.plugin) ? value.plugin.length : 0,
|
|
584
|
+
mcpCount: value.mcp && typeof value.mcp === 'object' && !Array.isArray(value.mcp)
|
|
585
|
+
? Object.keys(value.mcp).length : 0,
|
|
586
|
+
rawHash: digest(stdout),
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function executeInheritanceProbe(sourceRoot, model, command, timeoutMs, reportEvidenceRoot, definitionInstruction, environment) {
|
|
591
|
+
const referenceInstruction = '.agents/skills/dingtalk-basic-behavior/references/risk-authority-and-privacy.md';
|
|
592
|
+
const definitionProbe = randomUUID();
|
|
593
|
+
const referenceProbeStart = randomUUID();
|
|
594
|
+
const referenceProbeEnd = randomUUID();
|
|
595
|
+
const parent = mkdtempSync(join(tmpdir(), 'dta-agent-inheritance-probe-'));
|
|
596
|
+
const sandbox = join(parent, 'workspace');
|
|
597
|
+
try {
|
|
598
|
+
mkdirSync(sandbox, { recursive: true });
|
|
599
|
+
initializeProbeWorktree(sandbox);
|
|
600
|
+
const sourceDefinitionPath = resolve(sourceRoot, definitionInstruction);
|
|
601
|
+
const definitionPath = resolve(sandbox, definitionInstruction);
|
|
602
|
+
const sourceBasicPath = join(sourceRoot, '.agents', 'skills', 'dingtalk-basic-behavior');
|
|
603
|
+
const basicPath = join(sandbox, '.agents', 'skills', 'dingtalk-basic-behavior');
|
|
604
|
+
const referencePath = resolve(sandbox, referenceInstruction);
|
|
605
|
+
if (!isInside(sourceRoot, sourceDefinitionPath) || !existsSync(sourceDefinitionPath) ||
|
|
606
|
+
!isInside(sandbox, definitionPath) || !existsSync(sourceBasicPath)) {
|
|
607
|
+
throw new Error('隔离继承探针缺少 Agent Definition 或 Basic reference');
|
|
608
|
+
}
|
|
609
|
+
writeAtomic(definitionPath, `${readFileSync(sourceDefinitionPath, 'utf8').trimEnd()}\n\n<!-- dta-definition-probe:${definitionProbe} -->\n`);
|
|
610
|
+
mkdirSync(dirname(basicPath), { recursive: true });
|
|
611
|
+
cpSync(sourceBasicPath, basicPath, { recursive: true });
|
|
612
|
+
if (!isInside(sandbox, referencePath) || !existsSync(referencePath)) {
|
|
613
|
+
throw new Error('隔离继承探针缺少风险/授权 reference');
|
|
614
|
+
}
|
|
615
|
+
const referenceBody = readFileSync(referencePath, 'utf8');
|
|
616
|
+
if (referenceBody.split(/\r?\n/).some((line) => line.length > 2_000)) {
|
|
617
|
+
throw new Error('Basic reference 含超过 OpenCode read 单行上限的内容,无法证明全文读取');
|
|
618
|
+
}
|
|
619
|
+
writeFileSync(referencePath, `<!-- dta-reference-probe:${referenceProbeStart} -->\n\n` +
|
|
620
|
+
`${referenceBody.trimEnd()}\n\n` +
|
|
621
|
+
`<!-- dta-reference-probe:${referenceProbeEnd} -->\n`);
|
|
622
|
+
const referenceLineCount = readFileSync(referencePath, 'utf8').split(/\r?\n/).length;
|
|
623
|
+
const sourceConfig = JSON.parse(readFileSync(join(sourceRoot, 'opencode.json'), 'utf8'));
|
|
624
|
+
const permission = sourceConfig.permission && typeof sourceConfig.permission === 'object' &&
|
|
625
|
+
!Array.isArray(sourceConfig.permission) ? sourceConfig.permission : {};
|
|
626
|
+
const skill = permission.skill && typeof permission.skill === 'object' &&
|
|
627
|
+
!Array.isArray(permission.skill) ? permission.skill : {};
|
|
628
|
+
sourceConfig.instructions = [BASIC_INSTRUCTION];
|
|
629
|
+
delete sourceConfig.provider;
|
|
630
|
+
sourceConfig.plugin = [];
|
|
631
|
+
sourceConfig.mcp = {};
|
|
632
|
+
sourceConfig.permission = {
|
|
633
|
+
...permission,
|
|
634
|
+
external_directory: 'deny',
|
|
635
|
+
skill: { ...skill, 'dingtalk-basic-behavior': 'allow' },
|
|
636
|
+
};
|
|
637
|
+
writeAtomic(join(sandbox, 'opencode.json'), JSON.stringify(sourceConfig, null, 2) + '\n');
|
|
638
|
+
const probeEnvironment = { ...environment, PWD: sandbox };
|
|
639
|
+
const preflight = verifyRequiredBasicSkill(bindRequiredBasicSkill(sandbox), command, probeEnvironment);
|
|
640
|
+
const basicResolvedCount = countInstructionsForPath(sandbox, preflight.resolvedInstructions, BASIC_INSTRUCTION);
|
|
641
|
+
prepareOpenCodeEvalAgent(sandbox, 'none');
|
|
642
|
+
const definitionEvalAgent = resolveOpenCodeEvalAgent(command, sandbox, probeEnvironment);
|
|
643
|
+
const definitionPermissionPassed = zeroToolPermission(definitionEvalAgent);
|
|
644
|
+
const definitionExecution = runOpenCodePrompt(command, sandbox, model, DEFINITION_PROBE_PROMPT, 'DTA Agent Definition inheritance probe', timeoutMs, '', probeEnvironment);
|
|
645
|
+
prepareOpenCodeEvalAgent(sandbox, 'workspace-read', [referenceInstruction]);
|
|
646
|
+
const referenceEvalAgent = resolveOpenCodeEvalAgent(command, sandbox, probeEnvironment);
|
|
647
|
+
const referencePermissionPassed = targetReadPermission(referenceEvalAgent, sandbox, referenceInstruction);
|
|
648
|
+
const referenceExecution = runOpenCodePrompt(command, sandbox, model, REFERENCE_PROBE_PROMPT, 'DTA Basic reference inheritance probe', timeoutMs, '', probeEnvironment);
|
|
649
|
+
const expectedDefinition = `dta-definition-probe=${definitionProbe}`;
|
|
650
|
+
const expectedReference = `dta-reference-probe=${referenceProbeStart}:${referenceProbeEnd}`;
|
|
651
|
+
const referenceReal = realpathSync(referencePath);
|
|
652
|
+
const referenceRead = referenceExecution.toolTrace.some((item) => item.name === 'read' && item.status === 'completed' &&
|
|
653
|
+
item.paths.some((path) => realpathIfExists(path) === referenceReal));
|
|
654
|
+
const completedReferenceReads = referenceExecution.toolTrace.filter((item) => item.name === 'read' && item.status === 'completed');
|
|
655
|
+
const referenceFullRead = completedReferenceReads.length === 1 &&
|
|
656
|
+
fullReadWindow(completedReferenceReads[0].readWindow, referenceLineCount);
|
|
657
|
+
const referenceToolsScoped = referenceExecution.toolCalls === 1 &&
|
|
658
|
+
referenceExecution.toolTrace.length === referenceExecution.toolCalls &&
|
|
659
|
+
referenceExecution.toolNames.length === 1 &&
|
|
660
|
+
referenceExecution.toolNames[0] === 'read' &&
|
|
661
|
+
referenceExecution.toolTrace.every((item) => item.name === 'read' && item.paths.length === 1 &&
|
|
662
|
+
item.paths.every((path) => realpathIfExists(path) === referenceReal)) &&
|
|
663
|
+
referenceExecution.toolPaths.length === 1 &&
|
|
664
|
+
realpathIfExists(referenceExecution.toolPaths[0]) === referenceReal;
|
|
665
|
+
const definitionRaw = persistProbeExecution(reportEvidenceRoot, 'definition', definitionExecution);
|
|
666
|
+
const referenceRaw = persistProbeExecution(reportEvidenceRoot, 'reference', referenceExecution);
|
|
667
|
+
const result = {
|
|
668
|
+
passed: definitionInstruction === 'AGENTS.md' && basicResolvedCount === 1 &&
|
|
669
|
+
definitionPermissionPassed && referencePermissionPassed &&
|
|
670
|
+
definitionExecution.exitCode === 0 && definitionExecution.directoryMatched &&
|
|
671
|
+
definitionExecution.toolCalls === 0 &&
|
|
672
|
+
definitionExecution.answer.trim() === expectedDefinition &&
|
|
673
|
+
referenceExecution.exitCode === 0 && referenceExecution.directoryMatched &&
|
|
674
|
+
referenceExecution.answer.trim() === expectedReference &&
|
|
675
|
+
referenceRead && referenceFullRead && referenceToolsScoped,
|
|
676
|
+
definition: executionProbeSummary(definitionExecution, expectedDefinition, {
|
|
677
|
+
nativeRule: definitionInstruction,
|
|
678
|
+
permissionPassed: definitionPermissionPassed,
|
|
679
|
+
evalAgent: definitionEvalAgent,
|
|
680
|
+
raw: definitionRaw,
|
|
681
|
+
}, sandbox),
|
|
682
|
+
reference: executionProbeSummary(referenceExecution, expectedReference, {
|
|
683
|
+
path: referenceInstruction, read: referenceRead, toolsScoped: referenceToolsScoped,
|
|
684
|
+
fullRead: referenceFullRead,
|
|
685
|
+
requiredLines: referenceLineCount,
|
|
686
|
+
permissionPassed: referencePermissionPassed,
|
|
687
|
+
evalAgent: referenceEvalAgent,
|
|
688
|
+
raw: referenceRaw,
|
|
689
|
+
}, sandbox),
|
|
690
|
+
host: {
|
|
691
|
+
model,
|
|
692
|
+
openCodeVersion: preflight.openCodeVersion,
|
|
693
|
+
basicResolvedCount,
|
|
694
|
+
},
|
|
695
|
+
};
|
|
696
|
+
const resultPath = join(reportEvidenceRoot, 'inheritance-probes', 'result.json');
|
|
697
|
+
writeAtomic(resultPath, JSON.stringify(result, null, 2) + '\n');
|
|
698
|
+
return {
|
|
699
|
+
result,
|
|
700
|
+
evidence: {
|
|
701
|
+
resultPath: relative(reportEvidenceRoot, resultPath).split('\\').join('/'),
|
|
702
|
+
resultHash: digest(readFileSync(resultPath, 'utf8')),
|
|
703
|
+
},
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
finally {
|
|
707
|
+
rmSync(parent, { recursive: true, force: true });
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
function initializeProbeWorktree(workspace) {
|
|
711
|
+
const command = process.env.DTA_GIT_BIN ||
|
|
712
|
+
(existsSync('/usr/bin/git') ? '/usr/bin/git' : 'git');
|
|
713
|
+
const child = spawnSync(command, ['init', '--quiet'], {
|
|
714
|
+
cwd: workspace, encoding: 'utf8', timeout: 30_000, maxBuffer: 1024 * 1024,
|
|
715
|
+
});
|
|
716
|
+
if (child.error || child.status !== 0) {
|
|
717
|
+
throw new Error(`继承探针无法初始化隔离 git worktree: ${child.error?.message || String(child.stderr || child.stdout || '').trim()}`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function executionProbeSummary(execution, expected, extra, sandbox) {
|
|
721
|
+
return {
|
|
722
|
+
passed: execution.exitCode === 0 && execution.directoryMatched &&
|
|
723
|
+
execution.answer.trim() === expected,
|
|
724
|
+
exact: execution.answer.trim() === expected,
|
|
725
|
+
expected,
|
|
726
|
+
answer: execution.answer.trim(),
|
|
727
|
+
exitCode: execution.exitCode,
|
|
728
|
+
directoryMatched: execution.directoryMatched,
|
|
729
|
+
sessionId: execution.sessionId,
|
|
730
|
+
sessionDirectory: execution.sessionDirectory,
|
|
731
|
+
toolCalls: execution.toolCalls,
|
|
732
|
+
toolNames: execution.toolNames,
|
|
733
|
+
toolPaths: execution.toolPaths.map((path) => normalizedProbePath(sandbox, path)),
|
|
734
|
+
toolTrace: execution.toolTrace.map((item) => ({
|
|
735
|
+
...item,
|
|
736
|
+
paths: item.paths.map((path) => normalizedProbePath(sandbox, path)),
|
|
737
|
+
})),
|
|
738
|
+
durationMs: execution.durationMs,
|
|
739
|
+
...extra,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
function persistProbeExecution(reportRoot, name, execution) {
|
|
743
|
+
const evidenceRoot = join(reportRoot, 'inheritance-probes');
|
|
744
|
+
mkdirSync(evidenceRoot, { recursive: true });
|
|
745
|
+
const stdoutPath = join(evidenceRoot, `${name}.stdout.ndjson`);
|
|
746
|
+
const stderrPath = join(evidenceRoot, `${name}.stderr.log`);
|
|
747
|
+
const exportStdoutPath = join(evidenceRoot, `${name}.export.stdout.json`);
|
|
748
|
+
const exportStderrPath = join(evidenceRoot, `${name}.export.stderr.log`);
|
|
749
|
+
writeAtomic(stdoutPath, execution.stdout);
|
|
750
|
+
writeAtomic(stderrPath, execution.stderr);
|
|
751
|
+
writeAtomic(exportStdoutPath, execution.sessionExport.stdout);
|
|
752
|
+
writeAtomic(exportStderrPath, execution.sessionExport.stderr);
|
|
753
|
+
return {
|
|
754
|
+
stdoutPath: relative(reportRoot, stdoutPath).split('\\').join('/'),
|
|
755
|
+
stdoutHash: digest(execution.stdout),
|
|
756
|
+
stderrPath: relative(reportRoot, stderrPath).split('\\').join('/'),
|
|
757
|
+
stderrHash: digest(execution.stderr),
|
|
758
|
+
exportStdoutPath: relative(reportRoot, exportStdoutPath).split('\\').join('/'),
|
|
759
|
+
exportStdoutHash: digest(execution.sessionExport.stdout),
|
|
760
|
+
exportStderrPath: relative(reportRoot, exportStderrPath).split('\\').join('/'),
|
|
761
|
+
exportStderrHash: digest(execution.sessionExport.stderr),
|
|
762
|
+
exportExitCode: execution.sessionExport.exitCode,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
function normalizedProbePath(sandbox, path) {
|
|
766
|
+
const candidate = realpathIfExists(path);
|
|
767
|
+
return isInside(realpathSync(sandbox), candidate)
|
|
768
|
+
? relative(realpathSync(sandbox), candidate).split('\\').join('/')
|
|
769
|
+
: candidate;
|
|
770
|
+
}
|
|
771
|
+
function zeroToolPermission(agent) {
|
|
772
|
+
const tools = agent.tools;
|
|
773
|
+
const permission = agent.permission;
|
|
774
|
+
return Boolean(tools && typeof tools === 'object' && tools['*'] === false &&
|
|
775
|
+
Object.values(tools).every((allowed) => allowed === false) &&
|
|
776
|
+
permission && typeof permission === 'object' && permission['*'] === 'deny' &&
|
|
777
|
+
permission.external_directory === 'deny');
|
|
778
|
+
}
|
|
779
|
+
function targetReadPermission(agent, workspace, target) {
|
|
780
|
+
const tools = agent.tools;
|
|
781
|
+
const permission = agent.permission;
|
|
782
|
+
const read = permission?.read;
|
|
783
|
+
if (!tools || typeof tools !== 'object' || tools['*'] !== false || tools.read !== true ||
|
|
784
|
+
Object.entries(tools).some(([name, allowed]) => !['*', 'read'].includes(name) || allowed !== (name === 'read')) ||
|
|
785
|
+
!permission || typeof permission !== 'object' || permission['*'] !== 'deny' ||
|
|
786
|
+
permission.external_directory !== 'deny' || !read || typeof read !== 'object' ||
|
|
787
|
+
Array.isArray(read) || read['*'] !== 'deny')
|
|
788
|
+
return false;
|
|
789
|
+
const allowed = Object.entries(read).filter(([pattern, action]) => pattern !== '*' && action === 'allow');
|
|
790
|
+
const absolute = resolve(workspace, target);
|
|
791
|
+
const expected = new Set([target, absolute].map(normalizedInstructionPattern));
|
|
792
|
+
const canonicalAbsolute = normalizedInstructionPattern(absolute);
|
|
793
|
+
expected.add(normalizedInstructionPattern(relative(parse(canonicalAbsolute).root, canonicalAbsolute)));
|
|
794
|
+
if (existsSync(absolute)) {
|
|
795
|
+
const real = realpathSync(absolute);
|
|
796
|
+
expected.add(normalizedInstructionPattern(real));
|
|
797
|
+
expected.add(normalizedInstructionPattern(relative(parse(real).root, real)));
|
|
798
|
+
}
|
|
799
|
+
const actual = new Set(allowed.map(([pattern]) => normalizedInstructionPattern(pattern)));
|
|
800
|
+
return actual.size === expected.size && [...actual].every((pattern) => expected.has(pattern)) &&
|
|
801
|
+
Object.entries(read).every(([pattern, action]) => pattern === '*' ? action === 'deny' : action === 'allow');
|
|
802
|
+
}
|
|
803
|
+
function normalizedInstructionPattern(pattern) {
|
|
804
|
+
const normalized = pattern.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
805
|
+
if (!normalized.startsWith('/') && !/^[a-zA-Z]:\//.test(normalized))
|
|
806
|
+
return normalized;
|
|
807
|
+
const original = resolve(normalized);
|
|
808
|
+
let ancestor = original;
|
|
809
|
+
while (!existsSync(ancestor) && dirname(ancestor) !== ancestor)
|
|
810
|
+
ancestor = dirname(ancestor);
|
|
811
|
+
try {
|
|
812
|
+
const suffix = relative(ancestor, original);
|
|
813
|
+
return resolve(realpathSync(ancestor), suffix).replace(/\\/g, '/');
|
|
814
|
+
}
|
|
815
|
+
catch {
|
|
816
|
+
return original.replace(/\\/g, '/');
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function fullReadWindow(window, requiredLines) {
|
|
820
|
+
const offset = window?.offset;
|
|
821
|
+
const limit = window?.limit;
|
|
822
|
+
return (offset === null || offset === undefined || offset === 0 || offset === 1) &&
|
|
823
|
+
(limit === null || limit === undefined ||
|
|
824
|
+
(Number.isInteger(limit) && Number(limit) >= requiredLines));
|
|
825
|
+
}
|
|
826
|
+
function validRandomProbe(value, name) {
|
|
827
|
+
if (typeof value !== 'string')
|
|
828
|
+
return false;
|
|
829
|
+
const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
|
|
830
|
+
const count = name === 'dta-reference-probe' ? 2 : 1;
|
|
831
|
+
return new RegExp(`^${name}=${uuid}${count === 2 ? `:${uuid}` : ''}$`).test(value);
|
|
832
|
+
}
|
|
833
|
+
function verifyLoadProbeSet(root, reportPath, report, primaryName, comparisonName, basic, expectedRuns) {
|
|
834
|
+
const all = Array.isArray(report.loadProbes) ? report.loadProbes : [];
|
|
835
|
+
const declaredRuns = Number(report.summary?.runsPerConfiguration);
|
|
836
|
+
const primary = all.filter((item) => item.configuration === primaryName);
|
|
837
|
+
const comparison = all.filter((item) => item.configuration === comparisonName);
|
|
838
|
+
const version = String(report.engine?.version || '');
|
|
839
|
+
const runNumbersValid = (items) => items.length === expectedRuns &&
|
|
840
|
+
new Set(items.map((item) => item.runNumber)).size === expectedRuns &&
|
|
841
|
+
items.every((item) => Number.isInteger(item.runNumber) &&
|
|
842
|
+
item.runNumber >= 1 && item.runNumber <= expectedRuns);
|
|
843
|
+
const common = (item) => {
|
|
844
|
+
const expectedInstructions = item.expectsLoad === true ? [BASIC_INSTRUCTION] : [];
|
|
845
|
+
return loadProbeCorePassed(item, version) &&
|
|
846
|
+
stableStringify(item.resolvedInstructions) === stableStringify(expectedInstructions) &&
|
|
847
|
+
verifyLoadProbeRaw(root, reportPath, item, String(report.engine?.model || ''));
|
|
848
|
+
};
|
|
849
|
+
const primaryPassed = primary.filter((item) => common(item) && item.skill?.hash === basic.hash);
|
|
850
|
+
const comparisonPassed = comparison.filter((item) => common(item));
|
|
851
|
+
const knownConfigurations = new Set([primaryName, comparisonName]);
|
|
852
|
+
const uniqueNonEmpty = (values) => values.every((value) => typeof value === 'string' && Boolean(value)) && new Set(values).size === values.length;
|
|
853
|
+
const rawStdoutPaths = all.map((item) => item.raw?.stdoutPath);
|
|
854
|
+
const rawStdoutHashes = all.map((item) => item.raw?.stdoutHash);
|
|
855
|
+
const rawExportPaths = all.map((item) => item.raw?.exportStdoutPath);
|
|
856
|
+
const rawExportHashes = all.map((item) => item.raw?.exportStdoutHash);
|
|
857
|
+
const configurationGates = report.summary?.configurationLoadGates || {};
|
|
858
|
+
const gatesRecomputed = [primaryName, comparisonName].every((name) => {
|
|
859
|
+
const gate = configurationGates[name];
|
|
860
|
+
return gate?.passed === true && gate?.totalRuns === expectedRuns &&
|
|
861
|
+
gate?.passedRuns === expectedRuns;
|
|
862
|
+
});
|
|
863
|
+
const shapeValid = Number.isInteger(expectedRuns) && expectedRuns > 0 &&
|
|
864
|
+
declaredRuns === expectedRuns &&
|
|
865
|
+
all.length === expectedRuns * 2 &&
|
|
866
|
+
all.every((item) => knownConfigurations.has(item.configuration)) &&
|
|
867
|
+
runNumbersValid(primary) && runNumbersValid(comparison) && gatesRecomputed &&
|
|
868
|
+
uniqueNonEmpty(all.map((item) => item.challenge)) &&
|
|
869
|
+
uniqueNonEmpty(all.map((item) => item.sessionId)) &&
|
|
870
|
+
uniqueNonEmpty(rawStdoutPaths) && uniqueNonEmpty(rawStdoutHashes) &&
|
|
871
|
+
uniqueNonEmpty(rawExportPaths) && uniqueNonEmpty(rawExportHashes);
|
|
872
|
+
return {
|
|
873
|
+
passed: shapeValid && primaryPassed.length === expectedRuns &&
|
|
874
|
+
comparisonPassed.length === expectedRuns,
|
|
875
|
+
primary,
|
|
876
|
+
comparison,
|
|
877
|
+
passedPrimary: primaryPassed.length,
|
|
878
|
+
passedComparison: comparisonPassed.length,
|
|
879
|
+
expectedRuns,
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
function verifyLoadProbeRaw(root, reportPath, probe, model) {
|
|
883
|
+
const raw = probe.raw;
|
|
884
|
+
if (!raw || typeof raw !== 'object')
|
|
885
|
+
return false;
|
|
886
|
+
const reportRoot = dirname(reportPath);
|
|
887
|
+
const stdout = readBoundEvidenceFile(root, reportRoot, raw.stdoutPath, raw.stdoutHash);
|
|
888
|
+
const stderr = readBoundEvidenceFile(root, reportRoot, raw.stderrPath, raw.stderrHash);
|
|
889
|
+
const exportStdout = readBoundEvidenceFile(root, reportRoot, raw.exportStdoutPath, raw.exportStdoutHash);
|
|
890
|
+
const exportStderr = readBoundEvidenceFile(root, reportRoot, raw.exportStderrPath, raw.exportStderrHash);
|
|
891
|
+
if (stdout === null || stderr === null || exportStdout === null || exportStderr === null ||
|
|
892
|
+
raw.exportExitCode !== 0)
|
|
893
|
+
return false;
|
|
894
|
+
const parsed = parsePersistedProbeOutput(stdout);
|
|
895
|
+
let exported;
|
|
896
|
+
try {
|
|
897
|
+
exported = JSON.parse(exportStdout);
|
|
898
|
+
}
|
|
899
|
+
catch {
|
|
900
|
+
return false;
|
|
901
|
+
}
|
|
902
|
+
return parsed.valid && parsed.answer === String(probe.answer || '').trim() &&
|
|
903
|
+
parsed.toolTrace.length === 0 && parsed.sessionIds.length === 1 &&
|
|
904
|
+
parsed.sessionIds[0] === probe.sessionId &&
|
|
905
|
+
verifyExportedSession(exported, probe.sessionId, probe.sessionDirectory, model, OPENCODE_LOAD_PROBE_PROMPT) &&
|
|
906
|
+
!OPENCODE_LOAD_PROBE_PROMPT.includes(String(probe.challenge || ''));
|
|
907
|
+
}
|
|
908
|
+
function verifyExportedSession(exported, sessionId, directory, model, expectedPrompt) {
|
|
909
|
+
if (String(exported?.info?.id || '') !== sessionId ||
|
|
910
|
+
String(exported?.info?.directory || '') !== directory ||
|
|
911
|
+
!Array.isArray(exported?.messages) || !model.includes('/'))
|
|
912
|
+
return false;
|
|
913
|
+
const [providerID, ...modelParts] = model.split('/');
|
|
914
|
+
const modelID = modelParts.join('/');
|
|
915
|
+
const userTexts = exported.messages
|
|
916
|
+
.filter((message) => message?.info?.role === 'user')
|
|
917
|
+
.flatMap((message) => Array.isArray(message.parts) ? message.parts : [])
|
|
918
|
+
.filter((part) => part?.type === 'text')
|
|
919
|
+
.map((part) => String(part.text || ''));
|
|
920
|
+
const modelMatched = exported.messages.some((message) => message?.info?.role === 'assistant' &&
|
|
921
|
+
String(message.info.providerID || '') === providerID &&
|
|
922
|
+
String(message.info.modelID || '') === modelID);
|
|
923
|
+
// OpenCode 1.17.x may persist a positional CLI prompt as its JSON string
|
|
924
|
+
// representation (including the surrounding quotes). Accept only that exact
|
|
925
|
+
// encoding in addition to the historical verbatim form so evidence remains
|
|
926
|
+
// strict while supporting both export formats.
|
|
927
|
+
const promptMatched = userTexts.length === 1 &&
|
|
928
|
+
(userTexts[0] === expectedPrompt || userTexts[0] === JSON.stringify(expectedPrompt));
|
|
929
|
+
return promptMatched && modelMatched;
|
|
930
|
+
}
|
|
931
|
+
function realpathIfExists(path) {
|
|
932
|
+
try {
|
|
933
|
+
return existsSync(path) ? realpathSync(path) : resolve(path);
|
|
934
|
+
}
|
|
935
|
+
catch {
|
|
936
|
+
return resolve(path);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
function readLoadEvidence(root, input, expectedTargetHash) {
|
|
940
|
+
const loaded = readReport(root, input, 'Agent audit load evidence');
|
|
941
|
+
const value = loaded.value;
|
|
942
|
+
if (value.$schema !== 'dingtalk-agent/agent-audit-load-evidence@1') {
|
|
943
|
+
throw new Error('Load evidence schema 非法;请使用 agent audit 生成的 evidence 文件');
|
|
944
|
+
}
|
|
945
|
+
if (value.targetHash !== expectedTargetHash) {
|
|
946
|
+
throw new Error('Load evidence 与当前 Definition/Bindings/Host exposure 不匹配');
|
|
947
|
+
}
|
|
948
|
+
const report = readReport(root, String(value.reportPath || ''), 'OpenCode load report');
|
|
949
|
+
if (digest(readFileSync(report.path, 'utf8')) !== value.reportHash) {
|
|
950
|
+
throw new Error('OpenCode load report hash 不一致');
|
|
951
|
+
}
|
|
952
|
+
return { path: loaded.path, reportPath: report.path, report: report.value };
|
|
953
|
+
}
|
|
954
|
+
function verifyLoadReport(root, reportPath, report, basic, definitionInstruction, model, currentHostVersion) {
|
|
955
|
+
const configuration = 'with_skill';
|
|
956
|
+
const comparison = 'without_skill';
|
|
957
|
+
const probeSet = verifyLoadProbeSet(root, reportPath, report, configuration, comparison, basic, 1);
|
|
958
|
+
const probes = probeSet.primary;
|
|
959
|
+
const source = report.skills?.[configuration];
|
|
960
|
+
const gate = report.summary?.configurationLoadGates?.[configuration];
|
|
961
|
+
const definition = report.inheritanceProbe?.definition || {};
|
|
962
|
+
const reference = report.inheritanceProbe?.reference || {};
|
|
963
|
+
const definitionPassed = validRandomProbe(definition.expected, 'dta-definition-probe') &&
|
|
964
|
+
definition.answer === definition.expected && definition.exitCode === 0 &&
|
|
965
|
+
definition.directoryMatched === true && definition.toolCalls === 0 &&
|
|
966
|
+
Array.isArray(definition.toolNames) && definition.toolNames.length === 0 &&
|
|
967
|
+
Array.isArray(definition.toolPaths) && definition.toolPaths.length === 0 &&
|
|
968
|
+
Array.isArray(definition.toolTrace) && definition.toolTrace.length === 0 &&
|
|
969
|
+
definition.nativeRule === 'AGENTS.md' && definitionInstruction === 'AGENTS.md' &&
|
|
970
|
+
definition.permissionPassed === true &&
|
|
971
|
+
zeroToolPermission(definition.evalAgent || {});
|
|
972
|
+
const referenceTrace = Array.isArray(reference.toolTrace) ? reference.toolTrace : [];
|
|
973
|
+
const referenceRead = referenceTrace.some((item) => item?.name === 'read' && item?.status === 'completed' &&
|
|
974
|
+
Array.isArray(item.paths) && item.paths.length === 1 &&
|
|
975
|
+
item.paths[0] === reference.path);
|
|
976
|
+
const completedReferenceReads = referenceTrace.filter((item) => item?.name === 'read' && item?.status === 'completed');
|
|
977
|
+
const referenceFullRead = Number.isInteger(reference.requiredLines) &&
|
|
978
|
+
reference.requiredLines > 0 && completedReferenceReads.length === 1 &&
|
|
979
|
+
fullReadWindow(completedReferenceReads[0]?.readWindow, reference.requiredLines);
|
|
980
|
+
const referenceToolsScoped = reference.toolCalls === 1 &&
|
|
981
|
+
referenceTrace.length === 1 &&
|
|
982
|
+
Array.isArray(reference.toolNames) && reference.toolNames.length === 1 &&
|
|
983
|
+
reference.toolNames[0] === 'read' &&
|
|
984
|
+
referenceTrace.every((item) => item?.name === 'read' &&
|
|
985
|
+
Array.isArray(item.paths) && item.paths.length === 1 && item.paths[0] === reference.path) &&
|
|
986
|
+
Array.isArray(reference.toolPaths) && reference.toolPaths.length === 1 &&
|
|
987
|
+
reference.toolPaths[0] === reference.path;
|
|
988
|
+
const referencePassed = reference.path ===
|
|
989
|
+
'.agents/skills/dingtalk-basic-behavior/references/risk-authority-and-privacy.md' &&
|
|
990
|
+
validRandomProbe(reference.expected, 'dta-reference-probe') &&
|
|
991
|
+
reference.answer === reference.expected && reference.exitCode === 0 &&
|
|
992
|
+
reference.directoryMatched === true && referenceRead && referenceFullRead &&
|
|
993
|
+
reference.fullRead === true && referenceToolsScoped &&
|
|
994
|
+
reference.permissionPassed === true &&
|
|
995
|
+
targetReadPermission(reference.evalAgent || {}, String(reference.sessionDirectory || ''), reference.path);
|
|
996
|
+
const inheritanceEvidenceValid = verifyInheritanceEvidence(root, reportPath, report.inheritanceProbe, report.inheritanceEvidence);
|
|
997
|
+
const sourceConfigEvidenceValid = verifySourceConfigEvidence(root, reportPath, report.sourceConfigPreflight, report.sourceConfigEvidence);
|
|
998
|
+
const hostMatched = report.engine?.model === model &&
|
|
999
|
+
typeof report.engine?.version === 'string' && Boolean(report.engine.version) &&
|
|
1000
|
+
report.engine.version === currentHostVersion &&
|
|
1001
|
+
report.inheritanceProbe?.host?.model === report.engine.model &&
|
|
1002
|
+
report.inheritanceProbe?.host?.openCodeVersion === report.engine.version &&
|
|
1003
|
+
report.inheritanceProbe?.host?.basicResolvedCount === 1;
|
|
1004
|
+
const passed = report.$schema === 'dingtalk-agent/opencode-eval-report@1' &&
|
|
1005
|
+
report.passed === true && report.summary?.hardGate === true &&
|
|
1006
|
+
report.dingtalkSideEffect === false && source?.name === basic.name &&
|
|
1007
|
+
source?.version === basic.version && source?.hash === basic.hash &&
|
|
1008
|
+
report.summary?.loadGate === true && gate?.passed === true && probeSet.passed &&
|
|
1009
|
+
definitionPassed && referencePassed && inheritanceEvidenceValid &&
|
|
1010
|
+
sourceConfigEvidenceValid && hostMatched;
|
|
1011
|
+
return {
|
|
1012
|
+
passed,
|
|
1013
|
+
evidence: {
|
|
1014
|
+
schema: report.$schema || '', configuration, sourceHash: source?.hash || '',
|
|
1015
|
+
expectedHash: basic.hash, loadGate: report.summary?.loadGate === true,
|
|
1016
|
+
passedProbes: probeSet.passedPrimary,
|
|
1017
|
+
baselineAntiGuessProbes: probeSet.passedComparison,
|
|
1018
|
+
expectedRunsPerConfiguration: probeSet.expectedRuns,
|
|
1019
|
+
definitionInstruction,
|
|
1020
|
+
definitionNativeRule: definition.nativeRule === 'AGENTS.md',
|
|
1021
|
+
totalProbes: probes.length,
|
|
1022
|
+
definitionProbePassed: definitionPassed,
|
|
1023
|
+
referenceProbePassed: referencePassed,
|
|
1024
|
+
inheritanceProbePassed: definitionPassed && referencePassed &&
|
|
1025
|
+
inheritanceEvidenceValid && hostMatched,
|
|
1026
|
+
inheritanceEvidenceValid,
|
|
1027
|
+
sourceConfigEvidenceValid,
|
|
1028
|
+
hostMatched,
|
|
1029
|
+
inheritanceDefinition: definition,
|
|
1030
|
+
inheritanceReference: reference,
|
|
1031
|
+
},
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
function verifySourceConfigEvidence(root, reportPath, summary, evidence) {
|
|
1035
|
+
if (!summary || typeof summary !== 'object' || Array.isArray(summary) ||
|
|
1036
|
+
!evidence || typeof evidence !== 'object' || Array.isArray(evidence))
|
|
1037
|
+
return false;
|
|
1038
|
+
const value = summary;
|
|
1039
|
+
const manifest = evidence;
|
|
1040
|
+
if (value.passed !== true || value.definitionInstruction !== 'AGENTS.md' ||
|
|
1041
|
+
value.definitionInstructionCount !== 0 || value.basicInstructionCount !== 1 ||
|
|
1042
|
+
typeof value.rawHash !== 'string' || !value.rawHash ||
|
|
1043
|
+
manifest.rawHash !== value.rawHash)
|
|
1044
|
+
return false;
|
|
1045
|
+
const body = readBoundEvidenceFile(root, dirname(reportPath), manifest.path, manifest.hash);
|
|
1046
|
+
if (body === null)
|
|
1047
|
+
return false;
|
|
1048
|
+
try {
|
|
1049
|
+
return stableStringify(JSON.parse(body)) === stableStringify(value);
|
|
1050
|
+
}
|
|
1051
|
+
catch {
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
function verifyInheritanceEvidence(root, reportPath, probe, evidence) {
|
|
1056
|
+
if (!reportPath || !probe || typeof probe !== 'object' || !evidence ||
|
|
1057
|
+
typeof evidence !== 'object' || Array.isArray(evidence))
|
|
1058
|
+
return false;
|
|
1059
|
+
const manifest = evidence;
|
|
1060
|
+
const reportRoot = dirname(reportPath);
|
|
1061
|
+
const result = readBoundEvidenceFile(root, reportRoot, manifest.resultPath, manifest.resultHash);
|
|
1062
|
+
if (!result)
|
|
1063
|
+
return false;
|
|
1064
|
+
try {
|
|
1065
|
+
if (stableStringify(JSON.parse(result)) !== stableStringify(probe))
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
catch {
|
|
1069
|
+
return false;
|
|
1070
|
+
}
|
|
1071
|
+
const value = probe;
|
|
1072
|
+
const rawBodies = {};
|
|
1073
|
+
const rawValid = ['definition', 'reference'].every((name) => {
|
|
1074
|
+
const raw = value[name]?.raw;
|
|
1075
|
+
if (!raw || typeof raw !== 'object')
|
|
1076
|
+
return false;
|
|
1077
|
+
const stdout = readBoundEvidenceFile(root, reportRoot, raw.stdoutPath, raw.stdoutHash);
|
|
1078
|
+
const stderr = readBoundEvidenceFile(root, reportRoot, raw.stderrPath, raw.stderrHash);
|
|
1079
|
+
const exportStdout = readBoundEvidenceFile(root, reportRoot, raw.exportStdoutPath, raw.exportStdoutHash);
|
|
1080
|
+
const exportStderr = readBoundEvidenceFile(root, reportRoot, raw.exportStderrPath, raw.exportStderrHash);
|
|
1081
|
+
if (stdout === null || stderr === null || exportStdout === null || exportStderr === null ||
|
|
1082
|
+
raw.exportExitCode !== 0)
|
|
1083
|
+
return false;
|
|
1084
|
+
try {
|
|
1085
|
+
const exported = JSON.parse(exportStdout);
|
|
1086
|
+
const expectedPrompt = name === 'definition'
|
|
1087
|
+
? DEFINITION_PROBE_PROMPT : REFERENCE_PROBE_PROMPT;
|
|
1088
|
+
if (!verifyExportedSession(exported, String(value[name]?.sessionId || ''), String(value[name]?.sessionDirectory || ''), String(value.host?.model || ''), expectedPrompt))
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
catch {
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
rawBodies[name] = stdout;
|
|
1095
|
+
return true;
|
|
1096
|
+
});
|
|
1097
|
+
if (!rawValid)
|
|
1098
|
+
return false;
|
|
1099
|
+
const definitionRaw = parsePersistedProbeOutput(rawBodies.definition);
|
|
1100
|
+
const referenceRaw = parsePersistedProbeOutput(rawBodies.reference);
|
|
1101
|
+
const summaryReferenceTrace = Array.isArray(value.reference?.toolTrace)
|
|
1102
|
+
? value.reference.toolTrace : [];
|
|
1103
|
+
const rawCompletedReads = referenceRaw.toolTrace.filter((item) => item.name === 'read' && item.status === 'completed');
|
|
1104
|
+
return definitionRaw.valid && definitionRaw.answer === value.definition?.answer &&
|
|
1105
|
+
definitionRaw.sessionIds.length === 1 &&
|
|
1106
|
+
definitionRaw.sessionIds[0] === value.definition?.sessionId &&
|
|
1107
|
+
definitionRaw.toolTrace.length === 0 &&
|
|
1108
|
+
referenceRaw.valid && referenceRaw.answer === value.reference?.answer &&
|
|
1109
|
+
referenceRaw.sessionIds.length === 1 &&
|
|
1110
|
+
referenceRaw.sessionIds[0] === value.reference?.sessionId &&
|
|
1111
|
+
referenceRaw.toolTrace.length === value.reference?.toolCalls &&
|
|
1112
|
+
referenceRaw.toolTrace.length === summaryReferenceTrace.length &&
|
|
1113
|
+
referenceRaw.toolTrace.every((item, index) => stableStringify(item.readWindow) ===
|
|
1114
|
+
stableStringify(summaryReferenceTrace[index]?.readWindow)) &&
|
|
1115
|
+
rawCompletedReads.length === 1 &&
|
|
1116
|
+
fullReadWindow(rawCompletedReads[0].readWindow, value.reference?.requiredLines) &&
|
|
1117
|
+
referenceRaw.toolTrace.every((item) => item.name === 'read' &&
|
|
1118
|
+
item.paths.length === 1 && item.paths[0] === value.reference?.path);
|
|
1119
|
+
}
|
|
1120
|
+
function parsePersistedProbeOutput(body) {
|
|
1121
|
+
const events = [];
|
|
1122
|
+
for (const line of body.split(/\r?\n/)) {
|
|
1123
|
+
if (!line.trim())
|
|
1124
|
+
continue;
|
|
1125
|
+
try {
|
|
1126
|
+
events.push(JSON.parse(line));
|
|
1127
|
+
}
|
|
1128
|
+
catch {
|
|
1129
|
+
return { valid: false, answer: '', sessionIds: [], toolTrace: [] };
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
const answer = events.filter((event) => event.type === 'text' && event.part?.type === 'text')
|
|
1133
|
+
.map((event) => String(event.part.text || '')).join('\n').trim();
|
|
1134
|
+
const toolEvents = events.filter((event) => String(event.type || '').includes('tool') || event.part?.type === 'tool');
|
|
1135
|
+
return {
|
|
1136
|
+
valid: events.length > 0,
|
|
1137
|
+
answer,
|
|
1138
|
+
sessionIds: [...new Set(events.map((event) => String(event.sessionID || ''))
|
|
1139
|
+
.filter(Boolean))],
|
|
1140
|
+
toolTrace: toolEvents.map((event) => ({
|
|
1141
|
+
name: String(event.part?.tool || event.part?.name || event.tool || event.name || ''),
|
|
1142
|
+
status: String(event.part?.state?.status || event.status || ''),
|
|
1143
|
+
paths: persistedToolPaths(event),
|
|
1144
|
+
readWindow: persistedReadWindow(event),
|
|
1145
|
+
})),
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
function persistedReadWindow(event) {
|
|
1149
|
+
const input = event.part?.state?.input;
|
|
1150
|
+
return {
|
|
1151
|
+
offset: Number.isInteger(input?.offset) ? Number(input.offset) : null,
|
|
1152
|
+
limit: Number.isInteger(input?.limit) ? Number(input.limit) : null,
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
function persistedToolPaths(event) {
|
|
1156
|
+
const paths = [];
|
|
1157
|
+
for (const candidate of [event.part?.state?.input, event.part?.state?.metadata]) {
|
|
1158
|
+
if (!candidate || typeof candidate !== 'object')
|
|
1159
|
+
continue;
|
|
1160
|
+
for (const [key, value] of Object.entries(candidate)) {
|
|
1161
|
+
if (!['filePath', 'filepath', 'path', 'directory'].includes(key) ||
|
|
1162
|
+
typeof value !== 'string')
|
|
1163
|
+
continue;
|
|
1164
|
+
const normalized = value.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
1165
|
+
const target = '.agents/skills/dingtalk-basic-behavior/references/' +
|
|
1166
|
+
'risk-authority-and-privacy.md';
|
|
1167
|
+
paths.push(normalized === target || normalized.endsWith(`/${target}`)
|
|
1168
|
+
? target : normalized);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return [...new Set(paths)];
|
|
1172
|
+
}
|
|
1173
|
+
function readBoundEvidenceFile(root, reportRoot, input, expectedHash) {
|
|
1174
|
+
if (typeof input !== 'string' || !input || typeof expectedHash !== 'string' ||
|
|
1175
|
+
!/^[a-f0-9]{64}$/.test(expectedHash))
|
|
1176
|
+
return null;
|
|
1177
|
+
const candidate = resolve(reportRoot, input);
|
|
1178
|
+
if (!isInside(root, candidate) || !isInside(reportRoot, candidate) || !existsSync(candidate)) {
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
const path = realpathSync(candidate);
|
|
1182
|
+
if (!isInside(root, path) || !isInside(reportRoot, path) || !statSync(path).isFile())
|
|
1183
|
+
return null;
|
|
1184
|
+
const body = readFileSync(path, 'utf8');
|
|
1185
|
+
return digest(body) === expectedHash ? body : null;
|
|
1186
|
+
}
|
|
1187
|
+
function verifyRemoteReport(report, definitionHash, routes, basic) {
|
|
1188
|
+
const mounts = Array.isArray(report.mounts) ? report.mounts : [];
|
|
1189
|
+
const expected = Object.entries(routes)
|
|
1190
|
+
.filter(([, source]) => source.startsWith('dingtalk-doc:'));
|
|
1191
|
+
const matchedSlots = expected.filter(([slot, source]) => mounts.some((item) => item.slot === slot && item.source === source && item.passed === true));
|
|
1192
|
+
const passed = report.$schema === 'dingtalk-agent/remote-state-eval-report@1' &&
|
|
1193
|
+
report.passed === true && report.identity?.matched === true &&
|
|
1194
|
+
report.definition?.hash === definitionHash && report.basicSkill?.skill?.hash === basic.hash &&
|
|
1195
|
+
report.modelProbe?.passed === true && matchedSlots.length === expected.length;
|
|
1196
|
+
return {
|
|
1197
|
+
passed,
|
|
1198
|
+
evidence: {
|
|
1199
|
+
schema: report.$schema || '', passed: report.passed === true,
|
|
1200
|
+
identityMatched: report.identity?.matched === true,
|
|
1201
|
+
definitionMatched: report.definition?.hash === definitionHash,
|
|
1202
|
+
basicHashMatched: report.basicSkill?.skill?.hash === basic.hash,
|
|
1203
|
+
matchedRemoteSlots: matchedSlots.map(([slot]) => slot),
|
|
1204
|
+
expectedRemoteSlots: expected.map(([slot]) => slot),
|
|
1205
|
+
},
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
function readReport(root, input, label) {
|
|
1209
|
+
const candidate = resolve(root, input);
|
|
1210
|
+
if (!isInside(root, candidate) || !existsSync(candidate)) {
|
|
1211
|
+
throw new Error(`${label} 必须是当前 Agent 根目录内已存在的 JSON: ${input}`);
|
|
1212
|
+
}
|
|
1213
|
+
const path = realpathSync(candidate);
|
|
1214
|
+
if (!isInside(root, path))
|
|
1215
|
+
throw new Error(`${label} 真实路径越界: ${input}`);
|
|
1216
|
+
try {
|
|
1217
|
+
const value = JSON.parse(readFileSync(path, 'utf8'));
|
|
1218
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
1219
|
+
throw new Error('must be object');
|
|
1220
|
+
return { path, value };
|
|
1221
|
+
}
|
|
1222
|
+
catch (error) {
|
|
1223
|
+
throw new Error(`${label} 非法: ${error.message}`);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
function repairFor(id) {
|
|
1227
|
+
if (id === 'definition.semantic-contract')
|
|
1228
|
+
return {
|
|
1229
|
+
id, why: '结构化模板不能替代真实的本体定义、岗位底线、做事范式和常犯错误。',
|
|
1230
|
+
actions: ['评审并替换 AGENTS.md 中所有 <...> 占位符,再复跑 audit。'],
|
|
1231
|
+
};
|
|
1232
|
+
if (id === 'definition.body')
|
|
1233
|
+
return {
|
|
1234
|
+
id, why: 'Agent 必须有长期定义、岗位底线、做事范式和常犯错误。',
|
|
1235
|
+
actions: ['从 compose Skill 的 AGENTS.template.md 创建 AGENTS.md,再复跑 audit。'],
|
|
1236
|
+
};
|
|
1237
|
+
if (id === 'storage.memory')
|
|
1238
|
+
return {
|
|
1239
|
+
id, why: '跨 Session 的已评审语义记忆需要显式路由。',
|
|
1240
|
+
actions: ['创建 MEMORY.md,并绑定 memory=local-md:MEMORY.md;或配置专用 dingtalk-doc。'],
|
|
1241
|
+
};
|
|
1242
|
+
if (id === 'storage.knowledge')
|
|
1243
|
+
return {
|
|
1244
|
+
id, why: '长期知识和 SOP 不能只留在模型隐藏上下文。',
|
|
1245
|
+
actions: ['创建 knowledge/INDEX.md,并绑定 knowledge=local-md:knowledge/INDEX.md。'],
|
|
1246
|
+
};
|
|
1247
|
+
if (id === 'storage.artifacts')
|
|
1248
|
+
return {
|
|
1249
|
+
id, why: '完成声明必须落到可独立检查的本地产物目录。',
|
|
1250
|
+
actions: ['创建 agent.bindings.json 中 artifacts 指向的目录,并保持在 Agent 根目录内。'],
|
|
1251
|
+
};
|
|
1252
|
+
if (id === 'storage.control-state')
|
|
1253
|
+
return {
|
|
1254
|
+
id, why: 'Wait、Receipt、幂等和 checkpoint 必须留在宿主私有原子存储。',
|
|
1255
|
+
actions: ['把 stateDir 改为 Agent 根目录内的本地目录,例如 .dingtalk-agent。'],
|
|
1256
|
+
};
|
|
1257
|
+
if (id.startsWith('skill.role.') && id.endsWith('.semantic'))
|
|
1258
|
+
return {
|
|
1259
|
+
id, why: 'Role Skill 必须包含真实的领域输入、方法、产物和验收,而不是模板提示。',
|
|
1260
|
+
actions: [`评审并替换 skills/${id.slice('skill.role.'.length, -'.semantic'.length)}/SKILL.md 中所有 <...> 占位符。`],
|
|
1261
|
+
};
|
|
1262
|
+
if (id.startsWith('skill.role.'))
|
|
1263
|
+
return {
|
|
1264
|
+
id, why: '领域职责需要独立 Role Skill,不能写回 Basic Behavior。',
|
|
1265
|
+
actions: [`创建 skills/${id.slice('skill.role.'.length)}/SKILL.md,并确保目录名等于 frontmatter name。`],
|
|
1266
|
+
};
|
|
1267
|
+
if (id === 'host.basic-exposure')
|
|
1268
|
+
return {
|
|
1269
|
+
id, why: 'OpenCode 只能从项目 exposure 完整继承本次装配的 Basic Skill 入口、references 与 assets。',
|
|
1270
|
+
actions: ['把 canonical dingtalk-basic-behavior 全树物化到 .agents/skills/dingtalk-basic-behavior。'],
|
|
1271
|
+
};
|
|
1272
|
+
if (id === 'host.definition-rule')
|
|
1273
|
+
return {
|
|
1274
|
+
id, why: 'OpenCode 只会把项目根 AGENTS.md 作为本地原生 rule;重复 custom instruction 会形成冗余通道。',
|
|
1275
|
+
actions: ['把项目本体统一为根目录 AGENTS.md,并从 opencode.json#instructions 删除其等价路径。'],
|
|
1276
|
+
};
|
|
1277
|
+
if (id === 'host.basic-instruction')
|
|
1278
|
+
return {
|
|
1279
|
+
id, why: '目录发现不等于正文进入每个 Session。',
|
|
1280
|
+
actions: ['合并 opencode.json:Basic path 仅出现一次,并设置 permission.skill.dingtalk-basic-behavior=allow。'],
|
|
1281
|
+
};
|
|
1282
|
+
if (id.startsWith('host.role-exposure.'))
|
|
1283
|
+
return {
|
|
1284
|
+
id, why: 'Role Skill 的发布源、Host exposure 与权限必须指向同一版本。',
|
|
1285
|
+
actions: [`物化 .agents/skills/${id.slice('host.role-exposure.'.length)},校验 hash 并设置 permission=allow。`],
|
|
1286
|
+
};
|
|
1287
|
+
if (id === 'host.load-probe')
|
|
1288
|
+
return {
|
|
1289
|
+
id, why: '静态目录和配置不能证明 Host 已解析 Agent Definition 并让模型读到 Basic 正文。',
|
|
1290
|
+
actions: ['执行 dta agent audit --verify-load --yes --json,保留 resolved instructions、随机 probe 与 Session directory 证据。'],
|
|
1291
|
+
};
|
|
1292
|
+
if (id === 'authority.dws')
|
|
1293
|
+
return {
|
|
1294
|
+
id, why: '远端语义状态必须绑定单一可信执行身份。',
|
|
1295
|
+
actions: ['配置 DWS profile 与 expectedUserId;不得从文档正文或显示名猜测。'],
|
|
1296
|
+
};
|
|
1297
|
+
if (id === 'storage.remote-readback')
|
|
1298
|
+
return {
|
|
1299
|
+
id, why: '远端缓存存在不能替代身份、类型、hash 和模型使用证据。',
|
|
1300
|
+
actions: ['运行 storage eval,保存独立 DWS readback,再用 --remote-report 复核。'],
|
|
1301
|
+
};
|
|
1302
|
+
return { id, why: '该装配条件尚未满足。', actions: ['修复对应绑定后重新运行 agent audit。'] };
|
|
1303
|
+
}
|
|
1304
|
+
function inspectSemanticContract(path) {
|
|
1305
|
+
if (!path || !existsSync(path) || !statSync(path).isFile()) {
|
|
1306
|
+
return { path, readable: false, complete: false, templateMarkers: [] };
|
|
1307
|
+
}
|
|
1308
|
+
const body = readFileSync(path, 'utf8');
|
|
1309
|
+
const templateMarkers = [...body.matchAll(new RegExp(COMPOSE_TEMPLATE_MARKER.source, `${COMPOSE_TEMPLATE_MARKER.flags}g`))].map((match) => match[0]);
|
|
1310
|
+
return {
|
|
1311
|
+
path,
|
|
1312
|
+
readable: true,
|
|
1313
|
+
complete: templateMarkers.length === 0,
|
|
1314
|
+
templateMarkers: [...new Set(templateMarkers)].sort(),
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
function writeAtomic(path, body) {
|
|
1318
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1319
|
+
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1320
|
+
writeFileSync(temporary, body, { flag: 'wx' });
|
|
1321
|
+
renameSync(temporary, path);
|
|
1322
|
+
}
|
|
1323
|
+
function inspectLocalDirectoryRoute(root, route) {
|
|
1324
|
+
if (!route.startsWith('local-dir:'))
|
|
1325
|
+
return { valid: false, route, reason: 'not-local-dir' };
|
|
1326
|
+
const candidate = resolve(root, route.slice('local-dir:'.length));
|
|
1327
|
+
if (!isInside(root, candidate))
|
|
1328
|
+
return { valid: false, route, reason: 'outside-agent-root' };
|
|
1329
|
+
if (candidate === root)
|
|
1330
|
+
return { valid: false, route, reason: 'must-be-dedicated-directory' };
|
|
1331
|
+
if (!existsSync(candidate) || !statSync(candidate).isDirectory()) {
|
|
1332
|
+
return { valid: false, route, path: candidate, reason: 'missing-directory' };
|
|
1333
|
+
}
|
|
1334
|
+
const path = realpathSync(candidate);
|
|
1335
|
+
return { valid: isInside(root, path), route, path,
|
|
1336
|
+
reason: isInside(root, path) ? '' : 'realpath-outside-agent-root' };
|
|
1337
|
+
}
|
|
1338
|
+
function isInside(root, candidate) {
|
|
1339
|
+
const rel = relative(root, candidate);
|
|
1340
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`));
|
|
1341
|
+
}
|
|
1342
|
+
function inspectContainedPath(root, input) {
|
|
1343
|
+
const rootReal = realpathSync(resolve(root));
|
|
1344
|
+
const candidate = resolve(rootReal, input);
|
|
1345
|
+
if (!isInside(rootReal, candidate)) {
|
|
1346
|
+
return { valid: false, path: candidate, reason: 'outside-agent-root' };
|
|
1347
|
+
}
|
|
1348
|
+
const rel = relative(rootReal, candidate);
|
|
1349
|
+
let cursor = rootReal;
|
|
1350
|
+
for (const segment of rel.split(/[\\/]/).filter(Boolean)) {
|
|
1351
|
+
cursor = join(cursor, segment);
|
|
1352
|
+
if (!pathEntryExists(cursor))
|
|
1353
|
+
break;
|
|
1354
|
+
const info = lstatSync(cursor);
|
|
1355
|
+
if (info.isSymbolicLink()) {
|
|
1356
|
+
return { valid: false, path: candidate, reason: `symlink:${relative(rootReal, cursor)}` };
|
|
1357
|
+
}
|
|
1358
|
+
if (cursor !== candidate && !info.isDirectory()) {
|
|
1359
|
+
return { valid: false, path: candidate, reason: `non-directory-ancestor:${relative(rootReal, cursor)}` };
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
let ancestor = candidate;
|
|
1363
|
+
while (!pathEntryExists(ancestor) && dirname(ancestor) !== ancestor)
|
|
1364
|
+
ancestor = dirname(ancestor);
|
|
1365
|
+
try {
|
|
1366
|
+
const projected = resolve(realpathSync(ancestor), relative(ancestor, candidate));
|
|
1367
|
+
if (!isInside(rootReal, projected)) {
|
|
1368
|
+
return { valid: false, path: candidate, reason: 'realpath-outside-agent-root' };
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
catch (error) {
|
|
1372
|
+
return { valid: false, path: candidate, reason: `realpath-error:${error.message}` };
|
|
1373
|
+
}
|
|
1374
|
+
return { valid: true, path: candidate, reason: '' };
|
|
1375
|
+
}
|
|
1376
|
+
function pathEntryExists(path) {
|
|
1377
|
+
try {
|
|
1378
|
+
lstatSync(path);
|
|
1379
|
+
return true;
|
|
1380
|
+
}
|
|
1381
|
+
catch {
|
|
1382
|
+
return false;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
//# sourceMappingURL=agent-audit.js.map
|