@xdxer/dingtalk-agent 0.1.4-beta.0 → 0.1.4-beta.10

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.
Files changed (273) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.en.md +397 -0
  3. package/README.md +497 -46
  4. package/dist/bin/dingtalk-agent.js +1162 -340
  5. package/dist/bin/dingtalk-agent.js.map +1 -1
  6. package/dist/src/actions.js +98 -14
  7. package/dist/src/actions.js.map +1 -1
  8. package/dist/src/agent-audit.js +460 -0
  9. package/dist/src/agent-audit.js.map +1 -0
  10. package/dist/src/agent-bindings.js +132 -0
  11. package/dist/src/agent-bindings.js.map +1 -0
  12. package/dist/src/agent-definition.js +182 -0
  13. package/dist/src/agent-definition.js.map +1 -0
  14. package/dist/src/agent-enhance.js +678 -0
  15. package/dist/src/agent-enhance.js.map +1 -0
  16. package/dist/src/bootstrap.js +125 -17
  17. package/dist/src/bootstrap.js.map +1 -1
  18. package/dist/src/config.js +1 -7
  19. package/dist/src/config.js.map +1 -1
  20. package/dist/src/development-workspace.js +729 -0
  21. package/dist/src/development-workspace.js.map +1 -0
  22. package/dist/src/doctor.js +20 -9
  23. package/dist/src/doctor.js.map +1 -1
  24. package/dist/src/dws.js +145 -0
  25. package/dist/src/dws.js.map +1 -1
  26. package/dist/src/eval-evidence.js +193 -0
  27. package/dist/src/eval-evidence.js.map +1 -0
  28. package/dist/src/init.js +1 -1
  29. package/dist/src/init.js.map +1 -1
  30. package/dist/src/invocation.js +36 -0
  31. package/dist/src/invocation.js.map +1 -0
  32. package/dist/src/lab.js +679 -0
  33. package/dist/src/lab.js.map +1 -0
  34. package/dist/src/lease.js +100 -0
  35. package/dist/src/lease.js.map +1 -0
  36. package/dist/src/memory/candidates.js +451 -0
  37. package/dist/src/memory/candidates.js.map +1 -0
  38. package/dist/src/memory/completion-evidence.js +536 -0
  39. package/dist/src/memory/completion-evidence.js.map +1 -0
  40. package/dist/src/memory/operational.js +263 -0
  41. package/dist/src/memory/operational.js.map +1 -0
  42. package/dist/src/memory/remote-state.js +478 -0
  43. package/dist/src/memory/remote-state.js.map +1 -0
  44. package/dist/src/memory/task-checkpoints.js +204 -0
  45. package/dist/src/memory/task-checkpoints.js.map +1 -0
  46. package/dist/src/multica-deploy.js +1480 -0
  47. package/dist/src/multica-deploy.js.map +1 -0
  48. package/dist/src/multica-provider.js +685 -0
  49. package/dist/src/multica-provider.js.map +1 -0
  50. package/dist/src/opencode-evals.js +1062 -0
  51. package/dist/src/opencode-evals.js.map +1 -0
  52. package/dist/src/opencode-provider.js +531 -0
  53. package/dist/src/opencode-provider.js.map +1 -0
  54. package/dist/src/opencode-workspace.js +197 -0
  55. package/dist/src/opencode-workspace.js.map +1 -0
  56. package/dist/src/perception.js +225 -0
  57. package/dist/src/perception.js.map +1 -0
  58. package/dist/src/personal-event-evals.js +595 -0
  59. package/dist/src/personal-event-evals.js.map +1 -0
  60. package/dist/src/promotion.js +786 -0
  61. package/dist/src/promotion.js.map +1 -0
  62. package/dist/src/remote-semantic-state-live-evals.js +888 -0
  63. package/dist/src/remote-semantic-state-live-evals.js.map +1 -0
  64. package/dist/src/remote-semantic-state-worker.js +38 -0
  65. package/dist/src/remote-semantic-state-worker.js.map +1 -0
  66. package/dist/src/remote-state-evals.js +501 -0
  67. package/dist/src/remote-state-evals.js.map +1 -0
  68. package/dist/src/response-gate.js +51 -0
  69. package/dist/src/response-gate.js.map +1 -0
  70. package/dist/src/robot-evals.js +770 -0
  71. package/dist/src/robot-evals.js.map +1 -0
  72. package/dist/src/sessions.js +66 -105
  73. package/dist/src/sessions.js.map +1 -1
  74. package/dist/src/setup.js +10 -9
  75. package/dist/src/setup.js.map +1 -1
  76. package/dist/src/skill-manager.js +102 -203
  77. package/dist/src/skill-manager.js.map +1 -1
  78. package/dist/src/skills.js.map +1 -1
  79. package/dist/src/storage-evals.js +26 -0
  80. package/dist/src/storage-evals.js.map +1 -0
  81. package/dist/src/types.js.map +1 -1
  82. package/dist/src/upgrade.js +137 -0
  83. package/dist/src/upgrade.js.map +1 -0
  84. package/dist/src/waits.js +5 -1
  85. package/dist/src/waits.js.map +1 -1
  86. package/dist/src/workspace.js +28 -3
  87. package/dist/src/workspace.js.map +1 -1
  88. package/docs/INSTALLATION.md +118 -13
  89. package/docs/SECOND-AGENT-ACCEPTANCE.md +62 -0
  90. package/docs/architecture/agent-memory-topology.png +0 -0
  91. package/docs/architecture/agent-memory-topology.svg +132 -0
  92. package/docs/architecture/general-agent-kernel-topology.png +0 -0
  93. package/docs/architecture/general-agent-kernel-topology.svg +149 -0
  94. package/docs/architecture/provider-bound-development-workspace.png +0 -0
  95. package/docs/architecture/provider-bound-development-workspace.svg +141 -0
  96. package/docs/architecture/task-completion-gate.png +0 -0
  97. package/docs/architecture/task-completion-gate.svg +191 -0
  98. package/docs/schemas/agent-audit-load-evidence.schema.json +14 -0
  99. package/docs/schemas/agent-audit.schema.json +92 -0
  100. package/docs/schemas/agent-bindings.schema.json +54 -0
  101. package/docs/schemas/agent-definition.schema.json +78 -0
  102. package/docs/schemas/agent-enhancement-plan.schema.json +88 -0
  103. package/docs/schemas/agent-enhancement-receipt.schema.json +37 -0
  104. package/docs/schemas/enriched-invocation.schema.json +46 -0
  105. package/docs/schemas/eval-candidate-plan.schema.json +28 -0
  106. package/docs/schemas/eval-candidate-result.schema.json +20 -0
  107. package/docs/schemas/eval-candidate.schema.json +30 -0
  108. package/docs/schemas/invocation.schema.json +19 -0
  109. package/docs/schemas/memory-candidate-proposal.schema.json +18 -0
  110. package/docs/schemas/memory-candidate.schema.json +76 -0
  111. package/docs/schemas/memory-publish-target.schema.json +25 -0
  112. package/docs/schemas/multica-deployment-list.schema.json +29 -0
  113. package/docs/schemas/multica-deployment-operation.schema.json +51 -0
  114. package/docs/schemas/multica-deployment-plan.schema.json +70 -0
  115. package/docs/schemas/multica-deployment-receipt.schema.json +87 -0
  116. package/docs/schemas/multica-deployment-status.schema.json +23 -0
  117. package/docs/schemas/multica-workspace-inspection.schema.json +77 -0
  118. package/docs/schemas/multica-workspace-plan.schema.json +68 -0
  119. package/docs/schemas/multica-workspace-resource-list.schema.json +27 -0
  120. package/docs/schemas/multica-workspace-status.schema.json +34 -0
  121. package/docs/schemas/observation.schema.json +21 -0
  122. package/docs/schemas/operational-memory-provider.schema.json +36 -0
  123. package/docs/schemas/operational-memory-record.schema.json +24 -0
  124. package/docs/schemas/perception-input.schema.json +56 -0
  125. package/docs/schemas/project.schema.json +112 -0
  126. package/docs/schemas/promotion-list.schema.json +36 -0
  127. package/docs/schemas/promotion-plan.schema.json +60 -0
  128. package/docs/schemas/promotion-policy.schema.json +29 -0
  129. package/docs/schemas/promotion-receipt.schema.json +43 -0
  130. package/docs/schemas/promotion-status.schema.json +23 -0
  131. package/docs/schemas/release-readiness.schema.json +65 -0
  132. package/docs/schemas/remote-semantic-state-live-eval.schema.json +60 -0
  133. package/docs/schemas/remote-semantic-state-manifest.schema.json +79 -0
  134. package/docs/schemas/remote-semantic-state-provider.schema.json +98 -0
  135. package/docs/schemas/response-gate.schema.json +20 -0
  136. package/docs/schemas/task-checkpoint.schema.json +71 -0
  137. package/docs/schemas/task-completion-evidence.schema.json +154 -0
  138. package/docs/schemas/workspace-doctor.schema.json +56 -0
  139. package/docs/schemas/workspace-state.schema.json +39 -0
  140. package/evals/baselines/2026-07-16/opencode-basic-010-completion-summary.json +123 -0
  141. package/evals/baselines/2026-07-16/opencode-basic-skill-required-summary.json +69 -0
  142. package/evals/baselines/2026-07-16/opencode-multi-surface-summary.json +63 -0
  143. package/evals/baselines/2026-07-16/remote-state-live-summary.json +70 -0
  144. package/evals/baselines/2026-07-17/agent-enhance-opencode-dogfood-summary.json +98 -0
  145. package/evals/baselines/2026-07-17/personal-event-live-readiness-summary.json +68 -0
  146. package/examples/agents/fde-coach/AGENTS.md +26 -0
  147. package/examples/agents/fde-coach/MEMORY.md +3 -0
  148. package/examples/agents/fde-coach/fields/default/field.json +24 -0
  149. package/examples/agents/fde-coach/knowledge/INDEX.md +4 -0
  150. package/examples/agents/fde-coach/skills/fde-coach/SKILL.md +13 -0
  151. package/examples/agents/release-manager/AGENTS.md +26 -0
  152. package/examples/agents/release-manager/MEMORY.md +3 -0
  153. package/examples/agents/release-manager/fields/default/field.json +24 -0
  154. package/examples/agents/release-manager/knowledge/INDEX.md +4 -0
  155. package/examples/agents/release-manager/skills/release-manager/SKILL.md +13 -0
  156. package/lab/README.md +109 -0
  157. package/lab/agent-eval/catalog.json +91 -0
  158. package/lab/agent-eval/classic-failures.json +177 -0
  159. package/lab/agent-eval/completion-gate-regression.json +99 -0
  160. package/lab/agent-eval/personal-event-live.example.json +94 -0
  161. package/lab/agent-eval/remote-semantic-state-live.example.json +70 -0
  162. package/lab/agent-eval/remote-semantic-state-provider.fixture.json +47 -0
  163. package/lab/agent-eval/remote-state-workspace/AGENTS.md +8 -0
  164. package/lab/agent-eval/remote-state-workspace/opencode.json +7 -0
  165. package/lab/agent-eval/remote-state-workspace/skills/remote-state-operator/SKILL.md +13 -0
  166. package/lab/agent-eval/remote-state.example.json +31 -0
  167. package/lab/agent-eval/workspace/AGENTS.md +7 -0
  168. package/lab/agent-eval/workspace/MEMORY.md +4 -0
  169. package/lab/agent-eval/workspace/artifacts/pending-review.md +3 -0
  170. package/lab/agent-eval/workspace/knowledge/INDEX.md +4 -0
  171. package/lab/agent-eval/workspace/opencode.json +20 -0
  172. package/lab/manifest.example.json +27 -0
  173. package/lab/manifest.personal-event.example.json +27 -0
  174. package/lab/project-workspace/README.md +11 -0
  175. package/lab/project-workspace/fake-multica-provider.mjs +266 -0
  176. package/lab/project-workspace/multica-deploy.fixture.json +29 -0
  177. package/lab/project-workspace/multica-readonly.fixture.json +69 -0
  178. package/lab/project-workspace/observation.fixture.json +14 -0
  179. package/lab/project-workspace/opencode-provider-suite.json +65 -0
  180. package/lab/project-workspace/project.fixture.json +44 -0
  181. package/lab/project-workspace/promotion-policy.fixture.json +15 -0
  182. package/lab/robot-eval/pool.example.json +30 -0
  183. package/lab/robot-eval/suite.json +123 -0
  184. package/lab/robot-eval/workspace/AGENTS.md +21 -0
  185. package/lab/robot-eval/workspace/MEMORY.md +3 -0
  186. package/lab/robot-eval/workspace/knowledge/INDEX.md +5 -0
  187. package/lab/robot-eval/workspace/opencode.json +22 -0
  188. package/lab/schemas/agent-eval-catalog.schema.json +47 -0
  189. package/lab/schemas/lab-manifest.schema.json +70 -0
  190. package/lab/schemas/personal-event-eval.schema.json +91 -0
  191. package/lab/schemas/remote-state-eval.schema.json +66 -0
  192. package/lab/schemas/robot-eval-suite.schema.json +184 -0
  193. package/lab/schemas/robot-pool.schema.json +56 -0
  194. package/package.json +26 -14
  195. package/skills/dingtalk-agent-boot-multica/SKILL.md +40 -0
  196. package/skills/dingtalk-agent-compose/SKILL.md +110 -0
  197. package/skills/dingtalk-agent-compose/assets/AGENTS.template.md +26 -0
  198. package/skills/dingtalk-agent-compose/assets/agent.bindings.dingtalk-doc.template.json +13 -0
  199. package/skills/dingtalk-agent-compose/assets/agent.bindings.local.template.json +13 -0
  200. package/skills/dingtalk-agent-compose/assets/opencode.template.json +12 -0
  201. package/skills/dingtalk-agent-compose/assets/role-skill.template.md +24 -0
  202. package/skills/dingtalk-agent-compose/evals/evals.json +94 -0
  203. package/skills/dingtalk-agent-compose/references/agent-definition-contract.md +55 -0
  204. package/skills/dingtalk-agent-compose/references/opencode-host-contract.md +65 -0
  205. package/skills/dingtalk-agent-compose/references/storage-routing.md +20 -0
  206. package/skills/dingtalk-agent-deploy/SKILL.md +60 -0
  207. package/skills/dingtalk-agent-deploy/references/multica-deployment-contract.md +49 -0
  208. package/skills/dingtalk-agent-deploy/references/promotion-observation-contract.md +49 -0
  209. package/skills/dingtalk-agent-eval/SKILL.md +116 -0
  210. package/skills/dingtalk-agent-eval/assets/eval-catalog.template.json +18 -0
  211. package/skills/dingtalk-agent-eval/evals/evals.json +61 -0
  212. package/skills/dingtalk-agent-eval/references/eval-topology.md +34 -0
  213. package/skills/dingtalk-agent-eval/references/evidence-contract.md +31 -0
  214. package/skills/dingtalk-agent-eval/references/scenario-taxonomy.md +25 -0
  215. package/skills/dingtalk-agent-eval/references/storage-modes.md +75 -0
  216. package/skills/dingtalk-basic-behavior/SKILL.md +63 -3
  217. package/skills/dingtalk-basic-behavior/assets/memory-candidate-proposal.json +10 -0
  218. package/skills/dingtalk-basic-behavior/assets/{task-checkpoint.md → task-checkpoint.json} +2 -21
  219. package/skills/dingtalk-basic-behavior/references/action-contract.md +2 -0
  220. package/skills/dingtalk-basic-behavior/references/memory-and-evolution.md +15 -1
  221. package/skills/dingtalk-basic-behavior/references/perception-and-gates.md +28 -0
  222. package/skills/dingtalk-basic-behavior/references/task-lifecycle.md +32 -7
  223. package/dist/src/boot.js +0 -70
  224. package/dist/src/boot.js.map +0 -1
  225. package/dist/src/duty.js +0 -74
  226. package/dist/src/duty.js.map +0 -1
  227. package/dist/src/kb.js +0 -240
  228. package/dist/src/kb.js.map +0 -1
  229. package/dist/src/runs.js +0 -79
  230. package/dist/src/runs.js.map +0 -1
  231. package/docs/ARCHITECTURE.md +0 -217
  232. package/docs/MINIMAL-WORKSPACE-V1.md +0 -172
  233. package/docs/OPEN-SOURCE-REFERENCES.md +0 -107
  234. package/docs/SELF-TEST.md +0 -252
  235. package/docs//345/206/205/347/275/221/345/256/236/347/233/270.md +0 -77
  236. package/evals/baselines/2026-07-14/behavior-summary.json +0 -28
  237. package/evals/baselines/2026-07-14/contract-summary.json +0 -18
  238. package/evals/baselines/2026-07-14/live-canary-summary.json +0 -25
  239. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/SKILL.md +0 -72
  240. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/references/action-contract.md +0 -31
  241. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/references/event-to-behavior.md +0 -22
  242. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/references/memory-and-evolution.md +0 -25
  243. package/evals/baselines/2026-07-15/dingtalk-basic-behavior-0.3.0/references/runtime-modes.md +0 -34
  244. package/evals/baselines/2026-07-15/task-lifecycle-summary.json +0 -50
  245. package/evals/evals.json +0 -316
  246. package/evals/fixtures/dm-ambiguous-send.json +0 -4
  247. package/evals/fixtures/dm-blocked.json +0 -4
  248. package/evals/fixtures/dm-clear.json +0 -4
  249. package/evals/fixtures/dm-discussion.json +0 -4
  250. package/evals/fixtures/dm-doc-write-no-tool.json +0 -4
  251. package/evals/fixtures/dm-long-task-ack.json +0 -4
  252. package/evals/fixtures/dm-nonblocking-gap.json +0 -4
  253. package/evals/fixtures/dm-structured-task.json +0 -4
  254. package/evals/fixtures/group.json +0 -10
  255. package/evals/fixtures/mentioned.json +0 -3
  256. package/evals/run-contract-evals.mjs +0 -1106
  257. package/evals/run-shadow-evals.mjs +0 -267
  258. package/evals/runners/README.md +0 -66
  259. package/evals/runners/claude-shadow.mjs +0 -533
  260. package/evals/schemas/action-request.schema.json +0 -77
  261. package/evals/shadow-evals.json +0 -133
  262. package/skills/AGENTS.md +0 -104
  263. package/skills//344/273/273/345/212/241.md +0 -48
  264. package/skills//345/237/272/347/241/200/350/241/214/344/270/272.md +0 -44
  265. package/skills//345/277/203/350/267/263.md +0 -79
  266. package/skills//346/266/210/346/201/257.md +0 -50
  267. package/skills//347/237/245/350/257/206.md +0 -55
  268. package/skills//350/257/204/346/265/213.md +0 -62
  269. package/skills//351/222/211/351/222/211.md +0 -64
  270. package/templates/ontology/index.md +0 -24
  271. package/templates/ontology/self/access.md +0 -38
  272. package/templates/ontology/self/role-spec.md +0 -34
  273. package/templates/ontology/self/workspace.md +0 -41
@@ -0,0 +1,786 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, writeFileSync, } from 'node:fs';
3
+ import { dirname, join, relative, resolve } from 'node:path';
4
+ import { inspectAgentProject, showDevelopmentWorkspace, } from './development-workspace.js';
5
+ import { digest, stableStringify } from './events.js';
6
+ import { applyMulticaDeployment, planMulticaDeployment, statusMulticaDeployment, } from './multica-deploy.js';
7
+ export const PROMOTION_POLICY_SCHEMA = 'dingtalk-agent/promotion-policy@1';
8
+ export const PROMOTION_PLAN_SCHEMA = 'dingtalk-agent/promotion-plan@1';
9
+ export const PROMOTION_RECEIPT_SCHEMA = 'dingtalk-agent/promotion-receipt@1';
10
+ export const OBSERVATION_SCHEMA = 'dingtalk-agent/observation@1';
11
+ export const EVAL_CANDIDATE_SCHEMA = 'dingtalk-agent/eval-candidate@1';
12
+ const PROMOTION_ROOT = '.dingtalk-agent/development-workspace-evidence/promotions';
13
+ const CANDIDATE_ROOT = '.dingtalk-agent/eval-candidates';
14
+ const HASH = /^[a-f0-9]{64}$/;
15
+ const SAFE_ID = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/;
16
+ const STAGE_RANK = { dev: 0, test: 1, staging: 2, prod: 3 };
17
+ const SURFACES = new Set(['response', 'filesystem', 'workspace', 'artifact']);
18
+ export function planPromotion(start, policyRef, routeId, options = {}) {
19
+ const info = inspectAgentProject(start, options.cliVersion || '');
20
+ const { policy, policyPath, policyHash } = loadPolicy(info.root, policyRef);
21
+ const route = policy.routes.find((item) => item.id === routeId);
22
+ if (!route)
23
+ throw new Error(`Promotion route 不存在: ${routeId}`);
24
+ const source = showDevelopmentWorkspace(info.root, route.fromWorkspace, options.cliVersion || '');
25
+ const target = showDevelopmentWorkspace(info.root, route.toWorkspace, options.cliVersion || '');
26
+ const blocking = [];
27
+ if (source.legacy || source.provider.kind !== 'opencode')
28
+ blocking.push('source.provider-not-opencode');
29
+ if (target.legacy || target.provider.kind !== 'multica')
30
+ blocking.push('target.provider-not-multica');
31
+ if (target.stage === 'prod')
32
+ blocking.push('target.prod-requires-separate-live-gate');
33
+ if (STAGE_RANK[target.stage] < STAGE_RANK[source.stage])
34
+ blocking.push('target.stage-downgrade');
35
+ if (source.sourceHash !== target.sourceHash)
36
+ blocking.push('project.source-hash-mismatch');
37
+ let evalEvidence = null;
38
+ try {
39
+ evalEvidence = loadEvalGate(info.root, source, route);
40
+ }
41
+ catch (error) {
42
+ blocking.push(`eval.${errorCategory(error)}`);
43
+ }
44
+ let deployment;
45
+ try {
46
+ deployment = planMulticaDeployment(info.root, target.name, {
47
+ env: options.env, cliVersion: options.cliVersion,
48
+ });
49
+ blocking.push(...deployment.blocking.map((item) => `deployment.${item}`));
50
+ }
51
+ catch (error) {
52
+ blocking.push(`deployment.${errorCategory(error)}`);
53
+ deployment = emptyDeploymentPlan(target);
54
+ }
55
+ const sourceSkills = deployment.expected.skills.map((item) => ({ name: item.name, hash: item.hash }));
56
+ const sourceBinding = {
57
+ workspace: source.name,
58
+ stage: source.stage,
59
+ sourceHash: source.sourceHash,
60
+ desiredHash: source.desiredHash,
61
+ observedHash: source.observedHash,
62
+ definitionHash: deployment.expected.definitionHash || emptyHash(),
63
+ skills: sourceSkills,
64
+ };
65
+ const targetBinding = {
66
+ workspace: target.name,
67
+ stage: target.stage,
68
+ desiredHash: target.desiredHash,
69
+ deploymentHash: deployment.deploymentHash || emptyHash(),
70
+ deploymentPlanId: deployment.planId,
71
+ };
72
+ const resolvedEval = evalEvidence || emptyEvalEvidence(route);
73
+ const uniqueBlocking = [...new Set(blocking)].sort();
74
+ const planId = `promotion_plan_${digest(stableStringify({
75
+ routeId, policyHash, source: sourceBinding, eval: resolvedEval,
76
+ target: targetBinding, deploymentPlanId: deployment.planId, blocking: uniqueBlocking,
77
+ })).slice(0, 24)}`;
78
+ return {
79
+ $schema: PROMOTION_PLAN_SCHEMA,
80
+ planId,
81
+ routeId,
82
+ policyPath,
83
+ policyHash,
84
+ source: sourceBinding,
85
+ eval: resolvedEval,
86
+ target: targetBinding,
87
+ deployment,
88
+ blocking: uniqueBlocking,
89
+ requiresPlanId: true,
90
+ requiresExplicitYes: true,
91
+ remoteRead: false,
92
+ remoteWrite: false,
93
+ localWrite: false,
94
+ triggerWrite: false,
95
+ sideEffect: false,
96
+ dingtalkSideEffect: false,
97
+ };
98
+ }
99
+ export function applyPromotion(start, policyRef, routeId, options = {}) {
100
+ if (!options.yes || !options.planId) {
101
+ throw new Error('promote 会调用 Multica deploy;必须显式传当前 --plan-id 和 --yes');
102
+ }
103
+ const info = inspectAgentProject(start, options.cliVersion || '');
104
+ const plan = planPromotion(info.root, policyRef, routeId, {
105
+ env: options.env, cliVersion: options.cliVersion,
106
+ });
107
+ if (plan.planId !== options.planId) {
108
+ throw new Error(`promotion plan 已失效;期望当前 planId ${plan.planId}`);
109
+ }
110
+ if (plan.blocking.length)
111
+ throw new Error(`promotion 前置条件不完整: ${plan.blocking.join(', ')}`);
112
+ const deployment = applyMulticaDeployment(info.root, plan.target.workspace, {
113
+ ...options,
114
+ planId: plan.deployment.planId,
115
+ yes: true,
116
+ });
117
+ const deploymentReceiptPath = safeProjectPath(info.root, deployment.evidencePath, true);
118
+ const deploymentReceiptHash = sha256(readFileSync(deploymentReceiptPath));
119
+ const promoted = deployment.status === 'ready' && deployment.passed && deployment.providerReady &&
120
+ deployment.deploymentHash === plan.target.deploymentHash && Boolean(deployment.observedHash);
121
+ const status = promoted ? 'promoted'
122
+ : ['verifying', 'reconciling'].includes(deployment.status) ? 'verifying' : 'failed';
123
+ const target = {
124
+ ...plan.target,
125
+ deploymentReceiptId: deployment.receiptId,
126
+ deploymentOperationId: deployment.operationId,
127
+ deploymentReceiptPath: deployment.evidencePath,
128
+ deploymentReceiptHash,
129
+ observedHash: deployment.observedHash,
130
+ };
131
+ const linkHash = promotionLinkHash(plan.source, plan.eval, target);
132
+ const promotionId = `promotion_${digest(stableStringify({
133
+ routeId, planId: plan.planId, deploymentReceiptId: deployment.receiptId, linkHash,
134
+ })).slice(0, 24)}`;
135
+ const receiptId = `promotion_receipt_${digest(stableStringify({ promotionId, status, linkHash })).slice(0, 24)}`;
136
+ const receipt = {
137
+ $schema: PROMOTION_RECEIPT_SCHEMA,
138
+ promotionId,
139
+ receiptId,
140
+ planId: plan.planId,
141
+ routeId,
142
+ policyHash: plan.policyHash,
143
+ status,
144
+ passed: promoted,
145
+ idempotent: deployment.idempotent,
146
+ source: plan.source,
147
+ eval: plan.eval,
148
+ target,
149
+ linkHash,
150
+ failures: promoted ? [] : [...deployment.failures],
151
+ evidencePath: promotionReceiptPath(receiptId),
152
+ remoteRead: deployment.remoteRead,
153
+ remoteWrite: deployment.remoteWrite,
154
+ localWrite: true,
155
+ triggerWrite: false,
156
+ sideEffect: deployment.sideEffect,
157
+ dingtalkSideEffect: false,
158
+ };
159
+ persistPromotion(info.root, receipt);
160
+ return receipt;
161
+ }
162
+ export function statusPromotion(start, promotionId = '') {
163
+ const info = inspectAgentProject(start);
164
+ const loaded = loadPromotion(info.root, promotionId);
165
+ if (!loaded)
166
+ return {
167
+ $schema: 'dingtalk-agent/promotion-status@1', available: false,
168
+ promotionId: '', status: 'missing', passed: false, integrity: true, receipt: null,
169
+ remoteRead: false, remoteWrite: false, localWrite: false, triggerWrite: false,
170
+ sideEffect: false, dingtalkSideEffect: false,
171
+ };
172
+ verifyPromotionDependencies(info.root, loaded.receipt, loaded.receiptHash);
173
+ return {
174
+ $schema: 'dingtalk-agent/promotion-status@1', available: true,
175
+ promotionId: loaded.receipt.promotionId,
176
+ status: loaded.receipt.status,
177
+ passed: loaded.receipt.passed,
178
+ integrity: true,
179
+ receipt: loaded.receipt,
180
+ remoteRead: false, remoteWrite: false, localWrite: false, triggerWrite: false,
181
+ sideEffect: false, dingtalkSideEffect: false,
182
+ };
183
+ }
184
+ export function listPromotions(start) {
185
+ const info = inspectAgentProject(start);
186
+ const directory = safeProjectPath(info.root, join(PROMOTION_ROOT, 'bindings'), false);
187
+ const promotions = [];
188
+ if (existsSync(directory)) {
189
+ for (const entry of readdirSync(directory, { withFileTypes: true })
190
+ .filter((item) => item.isFile() && item.name.endsWith('.json'))
191
+ .sort((a, b) => a.name.localeCompare(b.name))) {
192
+ const promotionId = entry.name.slice(0, -5);
193
+ const status = statusPromotion(info.root, promotionId);
194
+ if (!status.receipt)
195
+ continue;
196
+ promotions.push({
197
+ promotionId, receiptId: status.receipt.receiptId, routeId: status.receipt.routeId,
198
+ status: status.receipt.status, passed: status.receipt.passed,
199
+ sourceHash: status.receipt.source.sourceHash,
200
+ evalEvidenceHash: status.receipt.eval.evidenceHash,
201
+ deploymentHash: status.receipt.target.deploymentHash,
202
+ observedHash: status.receipt.target.observedHash,
203
+ });
204
+ }
205
+ }
206
+ return {
207
+ $schema: 'dingtalk-agent/promotion-list@1', promotions,
208
+ remoteRead: false, remoteWrite: false, localWrite: false, triggerWrite: false,
209
+ sideEffect: false, dingtalkSideEffect: false,
210
+ };
211
+ }
212
+ export function planObservation(start, promotionId, inputRef) {
213
+ const info = inspectAgentProject(start);
214
+ const loaded = loadPromotion(info.root, promotionId);
215
+ if (!loaded)
216
+ throw new Error(`Promotion 不存在: ${promotionId}`);
217
+ verifyPromotionDependencies(info.root, loaded.receipt, loaded.receiptHash);
218
+ if (!loaded.receipt.passed || loaded.receipt.status !== 'promoted') {
219
+ throw new Error('只有已 promoted 且完整性通过的 Receipt 可以生成 Eval candidate');
220
+ }
221
+ const observation = loadObservation(info.root, inputRef);
222
+ if (observation.observedHash !== loaded.receipt.target.observedHash) {
223
+ throw new Error('Observation observedHash 与 Promotion 远端 observedHash 不一致');
224
+ }
225
+ const observationHash = digest(stableStringify(observation));
226
+ const candidateId = `eval_candidate_${digest(stableStringify({
227
+ promotionId, receiptHash: loaded.receiptHash, observationHash,
228
+ })).slice(0, 24)}`;
229
+ return {
230
+ $schema: 'dingtalk-agent/eval-candidate-plan@1',
231
+ candidateId,
232
+ promotionId,
233
+ observedHash: observation.observedHash,
234
+ observationHash,
235
+ candidatePath: candidatePath(candidateId),
236
+ writeBudget: { local: 1 },
237
+ reviewRequired: true,
238
+ eligibleForSuite: false,
239
+ remoteRead: false, remoteWrite: false, localWrite: false, triggerWrite: false,
240
+ sideEffect: false, dingtalkSideEffect: false,
241
+ };
242
+ }
243
+ export function applyObservation(start, promotionId, inputRef, options = {}) {
244
+ if (!options.yes)
245
+ throw new Error('observe 会写入本地 Eval candidate;必须显式传 --yes');
246
+ const info = inspectAgentProject(start);
247
+ const plan = planObservation(info.root, promotionId, inputRef);
248
+ const loaded = loadPromotion(info.root, promotionId);
249
+ if (!loaded)
250
+ throw new Error(`Promotion 不存在: ${promotionId}`);
251
+ const observation = loadObservation(info.root, inputRef);
252
+ const candidateBase = {
253
+ $schema: EVAL_CANDIDATE_SCHEMA,
254
+ candidateId: plan.candidateId,
255
+ status: 'proposed',
256
+ eligibleForSuite: false,
257
+ reviewRequired: true,
258
+ published: false,
259
+ promotion: {
260
+ promotionId,
261
+ receiptId: loaded.receipt.receiptId,
262
+ receiptHash: loaded.receiptHash,
263
+ linkHash: loaded.receipt.linkHash,
264
+ },
265
+ source: loaded.receipt.source,
266
+ eval: {
267
+ runId: loaded.receipt.eval.runId,
268
+ evidenceHash: loaded.receipt.eval.evidenceHash,
269
+ suiteId: loaded.receipt.eval.suiteId,
270
+ },
271
+ target: {
272
+ workspace: loaded.receipt.target.workspace,
273
+ deploymentReceiptId: loaded.receipt.target.deploymentReceiptId,
274
+ deploymentHash: loaded.receipt.target.deploymentHash,
275
+ observedHash: loaded.receipt.target.observedHash,
276
+ },
277
+ observation,
278
+ observationHash: plan.observationHash,
279
+ evidencePath: plan.candidatePath,
280
+ remoteRead: false,
281
+ remoteWrite: false,
282
+ localWrite: true,
283
+ triggerWrite: false,
284
+ sideEffect: 'local-eval-candidate',
285
+ dingtalkSideEffect: false,
286
+ };
287
+ const candidateHash = digest(stableStringify(candidateBase));
288
+ const candidate = { ...candidateBase, candidateHash };
289
+ const path = safeProjectPath(info.root, plan.candidatePath, false);
290
+ if (existsSync(path)) {
291
+ const previous = JSON.parse(readFileSync(path, 'utf8'));
292
+ if (stableStringify(previous) !== stableStringify(candidate)) {
293
+ throw new Error('Eval candidate ID 已存在但内容不一致');
294
+ }
295
+ return {
296
+ $schema: 'dingtalk-agent/eval-candidate-result@1', applied: false, idempotent: true,
297
+ candidate, remoteRead: false, remoteWrite: false, localWrite: false,
298
+ triggerWrite: false, sideEffect: false, dingtalkSideEffect: false,
299
+ };
300
+ }
301
+ atomicJson(path, candidate);
302
+ const readback = JSON.parse(readFileSync(path, 'utf8'));
303
+ if (stableStringify(readback) !== stableStringify(candidate)) {
304
+ throw new Error('Eval candidate 写后回读不一致');
305
+ }
306
+ return {
307
+ $schema: 'dingtalk-agent/eval-candidate-result@1', applied: true, idempotent: false,
308
+ candidate, remoteRead: false, remoteWrite: false, localWrite: true,
309
+ triggerWrite: false, sideEffect: 'local-eval-candidate', dingtalkSideEffect: false,
310
+ };
311
+ }
312
+ function loadEvalGate(root, source, route) {
313
+ if (!source.state || source.state.status !== 'ready')
314
+ throw new Error('source-not-ready');
315
+ const binding = source.state.binding;
316
+ for (const key of [
317
+ 'managedPath', 'sourceHash', 'evalRunId', 'evalEvidencePath', 'evalPlanHash',
318
+ 'evalSuiteHash', 'evalReportHash', 'evalSuiteId', 'evalModel', 'evalSourceHash',
319
+ 'evalDesiredHash', 'evalObservedHash',
320
+ ])
321
+ if (!binding[key])
322
+ throw new Error(`binding-${key}-missing`);
323
+ if (binding.sourceHash !== source.sourceHash || binding.evalSourceHash !== source.sourceHash ||
324
+ binding.evalDesiredHash !== source.desiredHash || source.state.desiredHash !== source.desiredHash) {
325
+ throw new Error('source-hash-drift');
326
+ }
327
+ const managed = safeProjectPath(root, binding.managedPath, true);
328
+ const actualObservedHash = hashTree(managed);
329
+ if (actualObservedHash !== source.observedHash || actualObservedHash !== binding.evalObservedHash ||
330
+ source.state.observedHash !== actualObservedHash)
331
+ throw new Error('observed-hash-drift');
332
+ const policySuitePath = safeProjectPath(root, route.suite, true);
333
+ const policySuite = readJson(policySuitePath, 'Promotion policy suite');
334
+ const policySuiteHash = digest(stableStringify(policySuite));
335
+ if (policySuiteHash !== binding.evalSuiteHash)
336
+ throw new Error('designated-suite-drift');
337
+ const evidenceRoot = safeProjectPath(root, binding.evalEvidencePath, true);
338
+ if (!lstatSync(evidenceRoot).isDirectory() || lstatSync(evidenceRoot).isSymbolicLink()) {
339
+ throw new Error('evidence-root-invalid');
340
+ }
341
+ const planPath = safeProjectPath(root, join(binding.evalEvidencePath, 'plan.json'), true);
342
+ const suitePath = safeProjectPath(root, join(binding.evalEvidencePath, 'suite.json'), true);
343
+ const reportPath = safeProjectPath(root, join(binding.evalEvidencePath, 'report.json'), true);
344
+ if (sha256(readFileSync(planPath)) !== binding.evalPlanHash ||
345
+ sha256(readFileSync(reportPath)) !== binding.evalReportHash)
346
+ throw new Error('evidence-byte-drift');
347
+ const plan = readJson(planPath, 'OpenCode Eval plan');
348
+ const suite = readJson(suitePath, 'OpenCode Eval suite');
349
+ const report = readJson(reportPath, 'OpenCode Eval report');
350
+ if (digest(stableStringify(suite)) !== binding.evalSuiteHash ||
351
+ stableStringify(suite) !== stableStringify(policySuite))
352
+ throw new Error('suite-evidence-drift');
353
+ if (plan.$schema !== 'dingtalk-agent/opencode-eval-plan@1' ||
354
+ report.$schema !== 'dingtalk-agent/opencode-eval-report@1' ||
355
+ report.runId !== binding.evalRunId || report.evidencePath !== binding.evalEvidencePath ||
356
+ plan.suite !== suite.id || suite.id !== binding.evalSuiteId ||
357
+ report.engine?.model !== source.provider.model || report.engine?.model !== binding.evalModel) {
358
+ throw new Error('evidence-binding-mismatch');
359
+ }
360
+ const runs = Number(plan.runsPerConfiguration);
361
+ const configurations = stringList(plan.configurations, 'Eval plan configurations');
362
+ const planCases = Array.isArray(plan.cases) ? plan.cases : [];
363
+ const planCaseIds = planCases.map((item) => String(item?.id || ''));
364
+ if (!Number.isInteger(runs) || runs < route.minimumRuns || runs > 10 ||
365
+ configurations.length !== 2 || new Set(configurations).size !== configurations.length) {
366
+ throw new Error('runs-or-configurations');
367
+ }
368
+ if (!route.requiredCases.every((id) => planCaseIds.includes(id)))
369
+ throw new Error('required-case-missing');
370
+ if (report.passed !== true || report.summary?.loadGate !== true ||
371
+ report.summary?.hardGate !== true || report.summary?.falseCompletionGate !== true ||
372
+ report.summary?.behaviorScoresValid !== true ||
373
+ (route.requireBehaviorGate && report.summary?.behaviorGate !== true)) {
374
+ throw new Error('hard-gate-failed');
375
+ }
376
+ for (const configuration of configurations) {
377
+ const gate = report.summary?.configurationLoadGates?.[configuration];
378
+ if (!gate?.passed || gate.passedRuns !== runs || gate.totalRuns !== runs) {
379
+ throw new Error('configuration-load-gate-failed');
380
+ }
381
+ }
382
+ const results = Array.isArray(report.results) ? report.results : [];
383
+ const expectedResults = planCaseIds.length * configurations.length * runs;
384
+ if (results.length !== expectedResults || results.some((item) => !item.hardGatePassed || !item.safetyPassed || item.falseCompletion)) {
385
+ throw new Error('result-hard-gate-failed');
386
+ }
387
+ for (const caseId of route.requiredCases) {
388
+ for (const configuration of configurations) {
389
+ const selected = results.filter((item) => item.id === caseId && item.configuration === configuration);
390
+ if (selected.length !== runs)
391
+ throw new Error('required-case-result-count');
392
+ for (const surface of route.requiredSurfaces) {
393
+ if (!selected.every((item) => item.surfaceResults?.[surface]?.declared === true &&
394
+ item.surfaceResults?.[surface]?.passed === true))
395
+ throw new Error(`surface-${surface}-failed`);
396
+ }
397
+ }
398
+ }
399
+ const evidenceHash = digest(stableStringify({
400
+ runId: binding.evalRunId,
401
+ evidencePath: binding.evalEvidencePath,
402
+ planHash: binding.evalPlanHash,
403
+ suiteHash: binding.evalSuiteHash,
404
+ reportHash: binding.evalReportHash,
405
+ sourceHash: binding.evalSourceHash,
406
+ desiredHash: binding.evalDesiredHash,
407
+ observedHash: binding.evalObservedHash,
408
+ }));
409
+ return {
410
+ runId: binding.evalRunId,
411
+ evidencePath: binding.evalEvidencePath,
412
+ evidenceHash,
413
+ planHash: binding.evalPlanHash,
414
+ suiteHash: binding.evalSuiteHash,
415
+ reportHash: binding.evalReportHash,
416
+ suiteId: binding.evalSuiteId,
417
+ model: binding.evalModel,
418
+ runs,
419
+ configurations,
420
+ caseIds: [...route.requiredCases],
421
+ requiredSurfaces: [...route.requiredSurfaces],
422
+ loadGate: true,
423
+ hardGate: true,
424
+ falseCompletionGate: true,
425
+ behaviorGate: report.summary.behaviorGate === true,
426
+ };
427
+ }
428
+ function loadPolicy(root, ref) {
429
+ const path = safeProjectPath(root, ref, true);
430
+ if (lstatSync(path).isSymbolicLink() || !lstatSync(path).isFile()) {
431
+ throw new Error('Promotion policy 必须是 Project 内真实文件');
432
+ }
433
+ const input = readJson(path, 'Promotion policy');
434
+ exactKeys(input, ['$schema', 'routes'], 'Promotion policy');
435
+ if (input.$schema !== PROMOTION_POLICY_SCHEMA || !Array.isArray(input.routes) ||
436
+ input.routes.length < 1 || input.routes.length > 32)
437
+ throw new Error('Promotion policy schema/routes 非法');
438
+ const ids = new Set();
439
+ const routes = input.routes.map((value, index) => {
440
+ const item = objectValue(value, `Promotion route ${index}`);
441
+ exactKeys(item, [
442
+ 'id', 'fromWorkspace', 'toWorkspace', 'suite', 'minimumRuns',
443
+ 'requiredCases', 'requiredSurfaces', 'requireBehaviorGate',
444
+ ], `Promotion route ${index}`);
445
+ const id = safeId(item.id, 'Promotion route id');
446
+ if (ids.has(id))
447
+ throw new Error(`Promotion route 重复: ${id}`);
448
+ ids.add(id);
449
+ const requiredCases = uniqueSafeIds(item.requiredCases, 'Promotion requiredCases', 30);
450
+ const requiredSurfaces = stringList(item.requiredSurfaces, 'Promotion requiredSurfaces');
451
+ if (!requiredSurfaces.length || requiredSurfaces.some((surface) => !SURFACES.has(surface)) ||
452
+ new Set(requiredSurfaces).size !== requiredSurfaces.length)
453
+ throw new Error('Promotion requiredSurfaces 非法');
454
+ if (!Number.isInteger(item.minimumRuns) || item.minimumRuns < 1 || item.minimumRuns > 10 ||
455
+ typeof item.requireBehaviorGate !== 'boolean')
456
+ throw new Error('Promotion route gate 配置非法');
457
+ const suite = stringField(item.suite, 'Promotion suite');
458
+ safeProjectPath(root, suite, true);
459
+ return {
460
+ id,
461
+ fromWorkspace: safeId(item.fromWorkspace, 'Promotion fromWorkspace'),
462
+ toWorkspace: safeId(item.toWorkspace, 'Promotion toWorkspace'),
463
+ suite,
464
+ minimumRuns: item.minimumRuns,
465
+ requiredCases,
466
+ requiredSurfaces: requiredSurfaces,
467
+ requireBehaviorGate: item.requireBehaviorGate,
468
+ };
469
+ });
470
+ const policy = { $schema: PROMOTION_POLICY_SCHEMA, routes };
471
+ return {
472
+ policy,
473
+ policyPath: relative(realpathSync(root), path),
474
+ policyHash: digest(stableStringify(policy)),
475
+ };
476
+ }
477
+ function loadObservation(root, ref) {
478
+ const path = safeProjectPath(root, ref, true);
479
+ if (lstatSync(path).isSymbolicLink() || !lstatSync(path).isFile()) {
480
+ throw new Error('Observation input 必须是 Project 内真实文件');
481
+ }
482
+ const input = readJson(path, 'Observation input');
483
+ exactKeys(input, [
484
+ '$schema', 'observedAt', 'observedHash', 'category', 'risk', 'capability',
485
+ 'outcome', 'summary', 'expected', 'actual', 'sourceRefs', 'redacted',
486
+ ], 'Observation input');
487
+ if (input.$schema !== OBSERVATION_SCHEMA || input.redacted !== true ||
488
+ !['behavior', 'safety', 'reliability', 'quality'].includes(String(input.category)) ||
489
+ !['low', 'medium', 'high', 'critical'].includes(String(input.risk)) ||
490
+ !['pass', 'fail', 'mixed', 'unknown'].includes(String(input.outcome))) {
491
+ throw new Error('Observation schema/category/risk/outcome/redacted 非法');
492
+ }
493
+ const observedAt = stringField(input.observedAt, 'Observation observedAt');
494
+ if (Number.isNaN(Date.parse(observedAt)) || new Date(observedAt).toISOString() !== observedAt) {
495
+ throw new Error('Observation observedAt 必须是规范 ISO 时间');
496
+ }
497
+ const observedHash = hashField(input.observedHash, 'Observation observedHash');
498
+ const capability = boundedString(input.capability, 'Observation capability', 100);
499
+ const summary = boundedString(input.summary, 'Observation summary', 500);
500
+ const expected = boundedString(input.expected, 'Observation expected', 1000);
501
+ const actual = boundedString(input.actual, 'Observation actual', 1000);
502
+ const sourceRefs = stringList(input.sourceRefs, 'Observation sourceRefs');
503
+ if (sourceRefs.length > 10 || sourceRefs.some((item) => item.length > 200)) {
504
+ throw new Error('Observation sourceRefs 超出预算');
505
+ }
506
+ assertRedactedObservation([capability, summary, expected, actual, ...sourceRefs]);
507
+ return {
508
+ $schema: OBSERVATION_SCHEMA,
509
+ observedAt,
510
+ observedHash,
511
+ category: input.category,
512
+ risk: input.risk,
513
+ capability,
514
+ outcome: input.outcome,
515
+ summary,
516
+ expected,
517
+ actual,
518
+ sourceRefs,
519
+ redacted: true,
520
+ };
521
+ }
522
+ function persistPromotion(root, receipt) {
523
+ const receiptPath = safeProjectPath(root, receipt.evidencePath, false);
524
+ atomicJson(receiptPath, receipt);
525
+ const receiptHash = sha256(readFileSync(receiptPath));
526
+ const pointer = {
527
+ $schema: 'dingtalk-agent/promotion-pointer@1',
528
+ promotionId: receipt.promotionId,
529
+ receiptId: receipt.receiptId,
530
+ receiptPath: receipt.evidencePath,
531
+ receiptHash,
532
+ };
533
+ atomicJson(safeProjectPath(root, promotionBindingPath(receipt.promotionId), false), pointer);
534
+ atomicJson(safeProjectPath(root, join(PROMOTION_ROOT, 'latest.json'), false), pointer);
535
+ }
536
+ function loadPromotion(root, promotionId = '') {
537
+ const pointerPath = safeProjectPath(root, promotionId
538
+ ? promotionBindingPath(safeResourceId(promotionId, 'promotionId'))
539
+ : join(PROMOTION_ROOT, 'latest.json'), false);
540
+ if (!existsSync(pointerPath))
541
+ return null;
542
+ const pointer = readJson(pointerPath, 'Promotion pointer');
543
+ exactKeys(pointer, ['$schema', 'promotionId', 'receiptId', 'receiptPath', 'receiptHash'], 'Promotion pointer');
544
+ if (pointer.$schema !== 'dingtalk-agent/promotion-pointer@1')
545
+ throw new Error('Promotion pointer schema 非法');
546
+ const expectedPromotionId = safeResourceId(pointer.promotionId, 'pointer promotionId');
547
+ if (promotionId && expectedPromotionId !== promotionId)
548
+ throw new Error('Promotion pointer ID 不一致');
549
+ const receiptPath = safeProjectPath(root, stringField(pointer.receiptPath, 'pointer receiptPath'), true);
550
+ const receiptHash = sha256(readFileSync(receiptPath));
551
+ if (receiptHash !== hashField(pointer.receiptHash, 'pointer receiptHash')) {
552
+ throw new Error('Promotion Receipt hash 不一致');
553
+ }
554
+ const receipt = readJson(receiptPath, 'Promotion Receipt');
555
+ if (receipt.$schema !== PROMOTION_RECEIPT_SCHEMA || receipt.promotionId !== expectedPromotionId ||
556
+ receipt.receiptId !== pointer.receiptId || receipt.evidencePath !== pointer.receiptPath) {
557
+ throw new Error('Promotion Receipt 绑定不一致');
558
+ }
559
+ return { receipt, receiptHash };
560
+ }
561
+ function verifyPromotionDependencies(root, receipt, receiptHash) {
562
+ if (receipt.linkHash !== promotionLinkHash(receipt.source, receipt.eval, receipt.target)) {
563
+ throw new Error('Promotion source/eval/target link hash 不一致');
564
+ }
565
+ const evalPlan = safeProjectPath(root, join(receipt.eval.evidencePath, 'plan.json'), true);
566
+ const evalSuite = safeProjectPath(root, join(receipt.eval.evidencePath, 'suite.json'), true);
567
+ const evalReport = safeProjectPath(root, join(receipt.eval.evidencePath, 'report.json'), true);
568
+ if (sha256(readFileSync(evalPlan)) !== receipt.eval.planHash ||
569
+ digest(stableStringify(readJson(evalSuite, 'Promotion Eval suite'))) !== receipt.eval.suiteHash ||
570
+ sha256(readFileSync(evalReport)) !== receipt.eval.reportHash) {
571
+ throw new Error('Promotion Eval plan/suite/report 发生漂移');
572
+ }
573
+ const deploymentStatus = statusMulticaDeployment(root, receipt.target.workspace, {
574
+ operationId: receipt.target.deploymentOperationId,
575
+ });
576
+ if (!deploymentStatus.receipt || deploymentStatus.receipt.receiptId !== receipt.target.deploymentReceiptId ||
577
+ deploymentStatus.receipt.evidencePath !== receipt.target.deploymentReceiptPath ||
578
+ sha256(readFileSync(safeProjectPath(root, receipt.target.deploymentReceiptPath, true))) !==
579
+ receipt.target.deploymentReceiptHash ||
580
+ deploymentStatus.receipt.deploymentHash !== receipt.target.deploymentHash ||
581
+ deploymentStatus.receipt.observedHash !== receipt.target.observedHash) {
582
+ throw new Error('Promotion 绑定的 Multica deployment Receipt 不一致');
583
+ }
584
+ if (!HASH.test(receiptHash))
585
+ throw new Error('Promotion Receipt hash 非法');
586
+ }
587
+ function promotionLinkHash(source, evalEvidence, target) {
588
+ return digest(stableStringify({
589
+ source: {
590
+ workspace: source.workspace, stage: source.stage, sourceHash: source.sourceHash,
591
+ desiredHash: source.desiredHash, observedHash: source.observedHash,
592
+ definitionHash: source.definitionHash, skills: source.skills,
593
+ },
594
+ eval: {
595
+ runId: evalEvidence.runId, evidenceHash: evalEvidence.evidenceHash,
596
+ suiteId: evalEvidence.suiteId, suiteHash: evalEvidence.suiteHash,
597
+ reportHash: evalEvidence.reportHash,
598
+ },
599
+ target: {
600
+ workspace: target.workspace, stage: target.stage,
601
+ deploymentHash: target.deploymentHash,
602
+ deploymentReceiptId: 'deploymentReceiptId' in target ? target.deploymentReceiptId : '',
603
+ deploymentReceiptHash: 'deploymentReceiptHash' in target ? target.deploymentReceiptHash : '',
604
+ observedHash: 'observedHash' in target ? target.observedHash : '',
605
+ },
606
+ }));
607
+ }
608
+ function emptyDeploymentPlan(target) {
609
+ return {
610
+ $schema: 'dingtalk-agent/multica-deployment-plan@1',
611
+ planId: 'multica_deploy_plan_unavailable', action: 'apply', workspace: target.name,
612
+ provider: 'multica', profile: '', inspectionId: '', desiredHash: target.desiredHash,
613
+ deploymentHash: emptyHash(), expected: {
614
+ workspaceId: '', runtimeId: '', agentId: '', agentName: '', skills: [],
615
+ definitionHash: emptyHash(), instructionsHash: emptyHash(),
616
+ }, operations: [], writeBudget: { forward: 0, rollback: 0 }, blocking: ['unavailable'],
617
+ requiresPlanId: true, requiresExplicitYes: true, remoteRead: false, remoteWrite: false,
618
+ triggerWrite: false, sideEffect: false, dingtalkSideEffect: false,
619
+ };
620
+ }
621
+ function emptyEvalEvidence(route) {
622
+ return {
623
+ runId: '', evidencePath: '', evidenceHash: emptyHash(), planHash: emptyHash(),
624
+ suiteHash: emptyHash(), reportHash: emptyHash(), suiteId: '', model: '', runs: 0,
625
+ configurations: [], caseIds: [...route.requiredCases],
626
+ requiredSurfaces: [...route.requiredSurfaces], loadGate: false, hardGate: false,
627
+ falseCompletionGate: false, behaviorGate: false,
628
+ };
629
+ }
630
+ function hashTree(root) {
631
+ if (!lstatSync(root).isDirectory() || lstatSync(root).isSymbolicLink()) {
632
+ throw new Error('Managed Workspace 必须是普通目录');
633
+ }
634
+ const base = realpathSync(root);
635
+ const entries = [];
636
+ const visit = (directory, prefix = '') => {
637
+ for (const entry of readdirSync(directory, { withFileTypes: true })
638
+ .sort((a, b) => a.name.localeCompare(b.name))) {
639
+ const path = join(directory, entry.name);
640
+ const ref = prefix ? `${prefix}/${entry.name}` : entry.name;
641
+ if (entry.isSymbolicLink())
642
+ throw new Error(`Managed Workspace 不允许符号链接: ${ref}`);
643
+ if (entry.isDirectory())
644
+ visit(path, ref);
645
+ else if (entry.isFile())
646
+ entries.push({ path: ref, hash: sha256(readFileSync(path)) });
647
+ else
648
+ throw new Error(`Managed Workspace 包含特殊文件: ${ref}`);
649
+ }
650
+ };
651
+ visit(base);
652
+ return digest(stableStringify(entries));
653
+ }
654
+ function assertRedactedObservation(values) {
655
+ const text = values.join('\n');
656
+ const forbidden = [
657
+ /(?:client[_-]?secret|access[_-]?token|refresh[_-]?token|password|credentials?|authorization|bearer)/iu,
658
+ /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/iu,
659
+ /(?:https?:\/\/|dingtalk:\/\/)/iu,
660
+ /(?:^|\D)(?:\+?86[- ]?)?1[3-9]\d{9}(?:\D|$)/u,
661
+ /\b[A-Za-z0-9_-]{40,}\b/u,
662
+ ];
663
+ if (forbidden.some((pattern) => pattern.test(text))) {
664
+ throw new Error('Observation 包含凭据、PII、URL 或未脱敏长标识');
665
+ }
666
+ }
667
+ function promotionReceiptPath(receiptId) {
668
+ return join(PROMOTION_ROOT, 'receipts', `${safeResourceId(receiptId, 'receiptId')}.json`);
669
+ }
670
+ function promotionBindingPath(promotionId) {
671
+ return join(PROMOTION_ROOT, 'bindings', `${safeResourceId(promotionId, 'promotionId')}.json`);
672
+ }
673
+ function candidatePath(candidateId) {
674
+ return join(CANDIDATE_ROOT, `${safeResourceId(candidateId, 'candidateId')}.json`);
675
+ }
676
+ function safeProjectPath(root, ref, mustExist) {
677
+ const rootReal = realpathSync(resolve(root));
678
+ const candidate = resolve(rootReal, ref);
679
+ const rel = relative(rootReal, candidate);
680
+ if (rel === '..' || rel.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)) {
681
+ throw new Error(`Promotion 路径必须位于 Project 内: ${ref}`);
682
+ }
683
+ if (mustExist && !existsSync(candidate))
684
+ throw new Error(`Promotion 路径不存在: ${ref}`);
685
+ if (existsSync(candidate)) {
686
+ if (lstatSync(candidate).isSymbolicLink())
687
+ throw new Error(`Promotion 路径不能是符号链接: ${ref}`);
688
+ if (realpathSync(candidate) !== candidate && !isInside(rootReal, realpathSync(candidate))) {
689
+ throw new Error(`Promotion 真实路径越界: ${ref}`);
690
+ }
691
+ }
692
+ let ancestor = dirname(candidate);
693
+ while (!existsSync(ancestor)) {
694
+ const parent = dirname(ancestor);
695
+ if (parent === ancestor)
696
+ break;
697
+ ancestor = parent;
698
+ }
699
+ if (!existsSync(ancestor) || !isInside(rootReal, realpathSync(ancestor))) {
700
+ throw new Error(`Promotion 父目录越界: ${ref}`);
701
+ }
702
+ return candidate;
703
+ }
704
+ function atomicJson(path, value) {
705
+ mkdirSync(dirname(path), { recursive: true });
706
+ const temp = `${path}.tmp-${process.pid}-${randomUUID().slice(0, 8)}`;
707
+ writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx' });
708
+ renameSync(temp, path);
709
+ }
710
+ function readJson(path, label) {
711
+ try {
712
+ const value = JSON.parse(readFileSync(path, 'utf8'));
713
+ return objectValue(value, label);
714
+ }
715
+ catch (error) {
716
+ throw new Error(`${label} 不是合法 JSON object: ${error.message}`);
717
+ }
718
+ }
719
+ function exactKeys(value, keys, label) {
720
+ const expected = [...keys].sort();
721
+ const actual = Object.keys(value).sort();
722
+ if (stableStringify(actual) !== stableStringify(expected)) {
723
+ throw new Error(`${label} 字段不精确: ${actual.join(',')}`);
724
+ }
725
+ }
726
+ function objectValue(value, label) {
727
+ if (!value || typeof value !== 'object' || Array.isArray(value))
728
+ throw new Error(`${label} 必须是 object`);
729
+ return value;
730
+ }
731
+ function stringField(value, label) {
732
+ if (typeof value !== 'string' || !value.trim())
733
+ throw new Error(`${label} 必须是非空字符串`);
734
+ return value.trim();
735
+ }
736
+ function boundedString(value, label, max) {
737
+ const item = stringField(value, label);
738
+ if (item.length > max)
739
+ throw new Error(`${label} 超过 ${max} 字符预算`);
740
+ return item;
741
+ }
742
+ function stringList(value, label) {
743
+ if (!Array.isArray(value) || value.some((item) => typeof item !== 'string' || !item.trim())) {
744
+ throw new Error(`${label} 必须是非空字符串数组`);
745
+ }
746
+ return value.map((item) => item.trim());
747
+ }
748
+ function uniqueSafeIds(value, label, max) {
749
+ const items = stringList(value, label).map((item) => safeId(item, label));
750
+ if (!items.length || items.length > max || new Set(items).size !== items.length) {
751
+ throw new Error(`${label} 数量或唯一性非法`);
752
+ }
753
+ return items;
754
+ }
755
+ function safeId(value, label) {
756
+ const item = stringField(value, label);
757
+ if (!SAFE_ID.test(item))
758
+ throw new Error(`${label} 不是安全 ID: ${item}`);
759
+ return item;
760
+ }
761
+ function safeResourceId(value, label) {
762
+ const item = stringField(value, label);
763
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$/.test(item))
764
+ throw new Error(`${label} 不是安全资源 ID`);
765
+ return item;
766
+ }
767
+ function hashField(value, label) {
768
+ const item = stringField(value, label);
769
+ if (!HASH.test(item))
770
+ throw new Error(`${label} 不是 SHA-256`);
771
+ return item;
772
+ }
773
+ function sha256(value) {
774
+ return createHash('sha256').update(value).digest('hex');
775
+ }
776
+ function emptyHash() {
777
+ return '0'.repeat(64);
778
+ }
779
+ function errorCategory(error) {
780
+ return String(error?.message || error).replace(/[^a-zA-Z0-9._-]+/g, '-').slice(0, 120);
781
+ }
782
+ function isInside(root, candidate) {
783
+ const rel = relative(root, candidate);
784
+ return rel === '' || (rel !== '..' && !rel.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`));
785
+ }
786
+ //# sourceMappingURL=promotion.js.map