@xdxer/dingtalk-agent 0.1.5-beta.11 → 0.1.5-beta.13

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.
@@ -4,6 +4,8 @@ import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSyn
4
4
  import { tmpdir } from 'node:os';
5
5
  import { basename, dirname, join, relative, resolve } from 'node:path';
6
6
  import { WORKSPACE_STATE_ROOT, inspectAgentProject, projectSkillRef, showDevelopmentWorkspace, } from './development-workspace.js';
7
+ import { PREDICATE_REGISTRY_PATH, buildPredicateRegistry, predicateRegistryOwner, serializeRegistry, } from './predicate-registry.js';
8
+ import { completionPredicateId } from './schedule-plan.js';
7
9
  import { digest, stableStringify } from './events.js';
8
10
  import { MULTICA_EVIDENCE_ROOT, emitProviderDiagnostic, inspectMulticaWorkspace, planMulticaWorkspace, sanitizeProviderEnv, statusMulticaWorkspace, } from './multica-provider.js';
9
11
  import { allRuntimeVocabularies, commandInvokesDws, forbiddenSmokeTool, skillsLoadedInContent, vocabulariesForRuntimeProvider, } from './multica-runtime-vocabulary.js';
@@ -368,6 +370,7 @@ export function planMulticaDeployment(start, name, options = {}) {
368
370
  inspectionId,
369
371
  desiredHash: workspace.desiredHash,
370
372
  deploymentHash: source.deploymentHash,
373
+ predicateRegistry: source.predicateRegistry,
371
374
  expected: {
372
375
  workspaceId: readPlan.expected.workspaceId,
373
376
  runtimeId: readPlan.expected.runtimeId,
@@ -565,6 +568,7 @@ function executeApply(root, workspace, source, plan, preflight, operation, comma
565
568
  inspectionEvidencePath: readback.evidencePath,
566
569
  desiredHash: workspace.desiredHash,
567
570
  deploymentHash: source.deploymentHash,
571
+ predicateRegistry: source.predicateRegistry,
568
572
  observedHash,
569
573
  agent: {
570
574
  id: finalSnapshot.agent?.id || agentId,
@@ -626,6 +630,7 @@ function retireDeployment(root, workspace, source, plan, preflight, operation, c
626
630
  inspectionEvidencePath: preflight.evidencePath,
627
631
  desiredHash: workspace.desiredHash,
628
632
  deploymentHash: source.deploymentHash,
633
+ predicateRegistry: source.predicateRegistry,
629
634
  observedHash,
630
635
  agent: {
631
636
  id: agent.id,
@@ -674,11 +679,61 @@ function loadDeploymentSource(root) {
674
679
  // second runtime protocol beside the Host's native Skill assignment.
675
680
  const instructions = definition;
676
681
  const instructionsHash = sha256(instructions);
682
+ // predicate registry 投影作为受管 supporting file 进入归属 Skill(issue #46):
683
+ // 走既有 skill-push 链路交付,投影 hash 因此进 Skill tree hash → deployment hash →
684
+ // Receipt,回读用现有 skill hash 对账就能发现漂移。不引入第二条传输通道。
685
+ const predicateRegistry = attachPredicateRegistry(info, skills);
677
686
  const deploymentHash = digest(stableStringify({
678
687
  definitionHash, instructionsHash,
679
688
  skills: skills.map((item) => ({ name: item.name, hash: item.hash })),
689
+ predicateRegistryHash: predicateRegistry?.hash || '',
680
690
  }));
681
- return { definitionHash, instructions, instructionsHash, skills, deploymentHash };
691
+ return { definitionHash, instructions, instructionsHash, skills, predicateRegistry, deploymentHash };
692
+ }
693
+ /**
694
+ * 把 manifest 的 predicates 注册表投影成归属 Skill 的受管 supporting file。
695
+ *
696
+ * 归属 Skill 必须真在受管集合里:注册表声明了、却没有任何一个部署的 Skill 承载它,
697
+ * 那么运行拍读不到判据——这时**拒绝部署**,而不是部署一个「声明说有判据、运行侧其实
698
+ * 拿不到」的 Agent。同路径已有作者手写文件时同样拒绝:受管 artifact 不覆盖人写的东西。
699
+ */
700
+ function attachPredicateRegistry(info, skills) {
701
+ const projection = buildPredicateRegistry({
702
+ name: info.project.name,
703
+ predicates: info.project.predicates,
704
+ // completion 文法只有一个 parser(schedule-plan),投影这边不再自己解析一遍
705
+ scheduleRefs: info.project.schedules.map((schedule) => ({
706
+ name: schedule.name, predicate: completionPredicateId(schedule.completion),
707
+ })),
708
+ });
709
+ if (!projection)
710
+ return null;
711
+ const owner = predicateRegistryOwner(info.project.skills);
712
+ const bundle = skills.find((item) => item.name === owner);
713
+ if (!bundle) {
714
+ throw new Error(`Project 声明了 predicates 注册表,但受管 Skill 集合里没有承载它的 Skill(期望 ${owner || '-'})——` +
715
+ '运行拍会读不到判据,先把该 Skill 加入 agent.skills 再部署');
716
+ }
717
+ if (bundle.files.some((item) => item.path === PREDICATE_REGISTRY_PATH)) {
718
+ throw new Error(`Skill ${owner} 已有 ${PREDICATE_REGISTRY_PATH}——受管 registry 投影不覆盖作者手写文件`);
719
+ }
720
+ const text = serializeRegistry(projection);
721
+ const content = Buffer.from(text, 'utf8');
722
+ bundle.files = [...bundle.files, {
723
+ path: PREDICATE_REGISTRY_PATH, absolutePath: '', managedContent: text,
724
+ hash: sha256(content), bytes: content.length,
725
+ }].sort((a, b) => a.path.localeCompare(b.path));
726
+ if (bundle.files.length > MAX_SKILL_SUPPORTING_FILES) {
727
+ throw new Error(`Skill ${owner} 加上受管 registry 投影后 supporting files 超过 ${MAX_SKILL_SUPPORTING_FILES} 个`);
728
+ }
729
+ bundle.hash = skillTreeHash(bundle.content, bundle.files.map((item) => ({ path: item.path, hash: item.hash })));
730
+ return {
731
+ skill: owner,
732
+ path: PREDICATE_REGISTRY_PATH,
733
+ hash: projection.hash,
734
+ ids: Object.keys(projection.predicates),
735
+ schedules: projection.schedules.map((item) => item.name),
736
+ };
682
737
  }
683
738
  function loadSkillBundle(name, root) {
684
739
  const actual = realpathSync(root);
@@ -780,14 +835,14 @@ function reconcileSkill(root, plan, local, remote, command, env, timeoutMs, call
780
835
  rollback.createdSkillIds.push(id);
781
836
  action = 'create';
782
837
  for (const file of local.files)
783
- upsertSkillFile(root, plan, id, file.path, file.absolutePath, command, env, timeoutMs, calls);
838
+ upsertSkillFile(root, plan, id, file.path, skillFileContentPath(file, temp), command, env, timeoutMs, calls);
784
839
  }
785
840
  else if (remote.hash !== local.hash) {
786
841
  rollback.updatedSkills.push(remote);
787
842
  runProvider(command, scopedArgs(plan, [
788
843
  'skill', 'update', id, '--content-file', local.mainPath, '--output', 'json',
789
844
  ]), `skill.update:${local.name}`, 'write', root, env, timeoutMs, calls);
790
- syncSkillFiles(root, plan, id, local, remote, command, env, timeoutMs, calls);
845
+ syncSkillFiles(root, plan, id, local, remote, command, env, timeoutMs, calls, temp);
791
846
  action = 'update';
792
847
  }
793
848
  const readback = parseRemoteSkill(jsonObject(runProvider(command, scopedArgs(plan, ['skill', 'get', id, '--output', 'json']), `skill.get.readback:${local.name}`, 'read', root, env, timeoutMs, calls).stdout, `skill readback ${local.name}`));
@@ -835,7 +890,7 @@ function reconcileAgent(root, plan, source, remote, command, env, timeoutMs, cal
835
890
  }
836
891
  return { agent, action };
837
892
  }
838
- function syncSkillFiles(root, plan, skillId, local, remote, command, env, timeoutMs, calls) {
893
+ function syncSkillFiles(root, plan, skillId, local, remote, command, env, timeoutMs, calls, temp) {
839
894
  const remoteByPath = new Map(remote.files.map((item) => [item.path, item]));
840
895
  const localPaths = new Set(local.files.map((item) => item.path));
841
896
  for (const file of remote.files) {
@@ -846,9 +901,25 @@ function syncSkillFiles(root, plan, skillId, local, remote, command, env, timeou
846
901
  for (const file of local.files) {
847
902
  if (remoteByPath.get(file.path)?.hash === file.hash)
848
903
  continue;
849
- upsertSkillFile(root, plan, skillId, file.path, file.absolutePath, command, env, timeoutMs, calls);
904
+ upsertSkillFile(root, plan, skillId, file.path, skillFileContentPath(file, temp), command, env, timeoutMs, calls);
850
905
  }
851
906
  }
907
+ /**
908
+ * 上传用的正文来源。磁盘文件直接给路径;受管 artifact(registry 投影)在这里才物化——
909
+ * plan 阶段只参与 hash,不落任何文件,dry-run 的「零副作用」保持为真。
910
+ */
911
+ function skillFileContentPath(file, temp) {
912
+ if (file.absolutePath)
913
+ return file.absolutePath;
914
+ if (file.managedContent === undefined) {
915
+ throw new Error(`受管 Skill 文件缺正文: ${file.path}`);
916
+ }
917
+ const path = writeTemporaryContent(temp, `managed-${sha256(file.path).slice(0, 8)}`, file.managedContent);
918
+ if (sha256(readFileSync(path)) !== file.hash) {
919
+ throw new Error(`受管 Skill 文件物化后 hash 不一致: ${file.path}`);
920
+ }
921
+ return path;
922
+ }
852
923
  function upsertSkillFile(root, plan, skillId, path, contentFile, command, env, timeoutMs, calls) {
853
924
  runProvider(command, scopedArgs(plan, [
854
925
  'skill', 'files', 'upsert', skillId, '--path', path,
@@ -1301,6 +1372,7 @@ function failureReceipt(root, workspace, source, plan, preflight, operation, cal
1301
1372
  inspectionEvidencePath: preflight.evidencePath,
1302
1373
  desiredHash: workspace.desiredHash,
1303
1374
  deploymentHash: source.deploymentHash,
1375
+ predicateRegistry: source.predicateRegistry,
1304
1376
  observedHash: '',
1305
1377
  agent: {
1306
1378
  id: operation.agentId,