@xdxer/dingtalk-agent 0.1.5-beta.8 → 0.1.5-beta.9

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 (47) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.en.md +1 -0
  3. package/README.md +1 -0
  4. package/dist/bin/dingtalk-agent.js +115 -14
  5. package/dist/bin/dingtalk-agent.js.map +1 -1
  6. package/dist/src/agent-audit.js +108 -9
  7. package/dist/src/agent-audit.js.map +1 -1
  8. package/dist/src/development-workspace.js +37 -1
  9. package/dist/src/development-workspace.js.map +1 -1
  10. package/dist/src/dws.js +9 -0
  11. package/dist/src/dws.js.map +1 -1
  12. package/dist/src/multica-deploy.js +347 -105
  13. package/dist/src/multica-deploy.js.map +1 -1
  14. package/dist/src/multica-provider.js +151 -27
  15. package/dist/src/multica-provider.js.map +1 -1
  16. package/dist/src/multica-runtime-vocabulary.js +110 -0
  17. package/dist/src/multica-runtime-vocabulary.js.map +1 -0
  18. package/dist/src/promotion.js +2 -1
  19. package/dist/src/promotion.js.map +1 -1
  20. package/dist/src/schedule-plan.js +192 -24
  21. package/dist/src/schedule-plan.js.map +1 -1
  22. package/docs/AGENT-IN-PRODUCTION.md +255 -0
  23. package/docs/ARCHITECTURE.md +5 -1
  24. package/docs/PLATFORM-GUARDRAILS.md +188 -0
  25. package/docs/schemas/multica-deployment-plan.schema.json +3 -1
  26. package/docs/schemas/multica-deployment-receipt.schema.json +15 -1
  27. package/docs/schemas/multica-deployment-status.schema.json +6 -2
  28. package/docs/schemas/multica-workspace-inspection.schema.json +16 -0
  29. package/docs/schemas/multica-workspace-status.schema.json +2 -0
  30. package/docs/schemas/workspace-scaffold.schema.json +1 -0
  31. package/lab/project-workspace/fake-multica-provider.mjs +97 -7
  32. package/lab/project-workspace/multica-deploy.fixture.json +2 -2
  33. package/lab/project-workspace/multica-readonly.fixture.json +2 -2
  34. package/lab/project-workspace/project.fixture.json +1 -1
  35. package/lab/robot-eval/suite.json +1 -1
  36. package/package.json +5 -2
  37. package/skills/core/dta-agent-compose/SKILL.md +1 -1
  38. package/skills/core/dta-agent-compose/references/drive-and-schedules.md +22 -2
  39. package/skills/core/dta-basic-behavior/SKILL.md +1 -1
  40. package/skills/core/dta-basic-behavior/references/memory-and-evolution.md +1 -1
  41. package/skills/core/dta-people-group-memory/SKILL.md +4 -4
  42. package/skills/core/dta-people-group-memory/references/model.md +1 -1
  43. package/skills/platforms/multica-dingtalk/PLATFORM.md +2 -2
  44. package/skills/platforms/multica-dingtalk/dta-deploy-multica/SKILL.md +6 -5
  45. package/skills/platforms/multica-dingtalk/dta-deploy-multica/references/multica-deployment-contract.md +14 -4
  46. package/skills/platforms/multica-dingtalk/dta-ops-multica/SKILL.md +33 -7
  47. package/skills/platforms/multica-dingtalk/dta-ops-multica/scripts/multica_ext.py +118 -12
@@ -2,14 +2,33 @@ import { createHash } from 'node:crypto';
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync, } from 'node:fs';
4
4
  import { dirname, join, relative, resolve } from 'node:path';
5
- import { WORKSPACE_STATE_ROOT, inspectAgentProject, showDevelopmentWorkspace, } from './development-workspace.js';
5
+ import { RESERVED_MULTICA_ENV, WORKSPACE_STATE_ROOT, inspectAgentProject, multicaAgentIdSource, showDevelopmentWorkspace, } from './development-workspace.js';
6
6
  import { digest, stableStringify } from './events.js';
7
+ import { supportedRuntimeProviders, vocabulariesForRuntimeProvider, } from './multica-runtime-vocabulary.js';
7
8
  export const MULTICA_EVIDENCE_ROOT = '.dingtalk-agent/development-workspace-evidence';
9
+ /**
10
+ * Strip env variables the provider CLI reinterprets as mode switches (see
11
+ * RESERVED_MULTICA_ENV) from a child-process environment. dta reads manifest env refs from
12
+ * the raw env *before* this runs, so a ref named after a reserved variable still resolves —
13
+ * only the multica subprocess stops seeing it.
14
+ */
15
+ export function sanitizeProviderEnv(env) {
16
+ const removed = RESERVED_MULTICA_ENV
17
+ .filter((key) => env[key] !== undefined).sort();
18
+ if (!removed.length)
19
+ return { env, removed };
20
+ const child = { ...env };
21
+ for (const key of removed)
22
+ delete child[key];
23
+ return { env: child, removed };
24
+ }
8
25
  class MulticaReadFailure extends Error {
9
26
  category;
10
- constructor(category) {
27
+ detail;
28
+ constructor(category, detail) {
11
29
  super(category);
12
30
  this.category = category;
31
+ this.detail = detail;
13
32
  }
14
33
  }
15
34
  export function planMulticaWorkspace(start, name, options = {}) {
@@ -151,7 +170,7 @@ export function scaffoldMulticaWorkspace(start, name, options = {}) {
151
170
  },
152
171
  };
153
172
  }
154
- const result = (applied) => ({
173
+ const result = (applied, createdPaths = []) => ({
155
174
  $schema: 'dingtalk-agent/workspace-scaffold@1',
156
175
  workspace: name,
157
176
  provider: 'multica',
@@ -162,6 +181,7 @@ export function scaffoldMulticaWorkspace(start, name, options = {}) {
162
181
  manifest: 'dingtalk-agent.json',
163
182
  block,
164
183
  envRefs,
184
+ createdPaths,
165
185
  nextSteps: alreadyDeclared
166
186
  ? [
167
187
  `Workspace ${name} 已声明(未改写);以下是它当前真实的配置`,
@@ -178,7 +198,10 @@ export function scaffoldMulticaWorkspace(start, name, options = {}) {
178
198
  `export ${envRefs.expectedUserId}=<dws user id>`,
179
199
  ]
180
200
  : [
181
- applied ? '已写入 dingtalk-agent.json' : '预览(加 --yes 落盘)',
201
+ applied
202
+ ? '已写入 dingtalk-agent.json;只改了本地 manifest(含 artifacts 目录),' +
203
+ '未创建任何 Multica 远端资源——远端 workspace/runtime 用 ops 技能供给,Agent 由 dta deploy 创建'
204
+ : '预览(加 --yes 落盘);本命令只写本地 manifest,不创建任何 Multica 远端资源',
182
205
  `export ${envRefs.workspaceId}=<multica workspace uuid>`,
183
206
  `export ${envRefs.runtimeId}=<multica runtime uuid>`,
184
207
  `export ${envRefs.dwsProfile}=<dws profile 或 corpId>`,
@@ -219,7 +242,19 @@ export function scaffoldMulticaWorkspace(start, name, options = {}) {
219
242
  renameSync(restore, manifestPath);
220
243
  throw error;
221
244
  }
222
- return result(true);
245
+ // 声明的路径必须原子性地存在:manifest 里写了 artifacts 目录却不创建它,新项目
246
+ // 第一次 doctor 就挂在 storage.artifacts(E-01)——doctor 不该要求用户补脚手架遗漏。
247
+ // .gitkeep 让空目录进 git,fresh clone 后 doctor 依然通过。回滚路径不清理该目录
248
+ // (幂等无害,保持回滚逻辑简单)。
249
+ const artifactsDir = join(info.root, '.dingtalk-agent', 'artifacts', name);
250
+ mkdirSync(artifactsDir, { recursive: true });
251
+ const gitkeep = join(artifactsDir, '.gitkeep');
252
+ if (!existsSync(gitkeep))
253
+ writeFileSync(gitkeep, '');
254
+ return result(true, [
255
+ relative(info.root, artifactsDir).split('\\').join('/'),
256
+ relative(info.root, gitkeep).split('\\').join('/'),
257
+ ]);
223
258
  }
224
259
  export function inspectMulticaWorkspace(start, name, options = {}) {
225
260
  const plan = planMulticaWorkspace(start, name, options);
@@ -297,6 +332,8 @@ export function statusMulticaWorkspace(start, name, cliVersion = '') {
297
332
  observedHash: report.observedHash,
298
333
  diff: [...report.diff],
299
334
  failures: [...report.failures],
335
+ failureDetails: Array.isArray(report.failureDetails) ? [...report.failureDetails] : [],
336
+ observedAgentRuntimeId: report.resources?.agent?.runtimeId || '',
300
337
  remoteRead: false,
301
338
  remoteWrite: false,
302
339
  sideEffect: false,
@@ -304,9 +341,11 @@ export function statusMulticaWorkspace(start, name, cliVersion = '') {
304
341
  };
305
342
  }
306
343
  function executeInspection(start, name, plan, options, persist) {
307
- const env = options.env || process.env;
308
- const target = resolveMulticaTarget(start, name, env, options.cliVersion || '', options.agentIdOverride);
309
- const command = options.command || env.DTA_MULTICA_BIN || 'multica';
344
+ const rawEnv = options.env || process.env;
345
+ const target = resolveMulticaTarget(start, name, rawEnv, options.cliVersion || '', options.agentIdOverride);
346
+ const sanitized = sanitizeProviderEnv(rawEnv);
347
+ const env = sanitized.env;
348
+ const command = options.command || rawEnv.DTA_MULTICA_BIN || 'multica';
310
349
  const timeoutMs = positiveTimeout(options.timeoutMs);
311
350
  if (persist) {
312
351
  const plannedEvidence = options.out || join(MULTICA_EVIDENCE_ROOT, safeName(name), 'multica-inspections', 'planned.json');
@@ -316,6 +355,7 @@ function executeInspection(start, name, plan, options, persist) {
316
355
  }
317
356
  const calls = [];
318
357
  const failures = [];
358
+ const failureDetails = [];
319
359
  const diff = [];
320
360
  let cliVersion = '';
321
361
  let principalHash = '';
@@ -328,12 +368,23 @@ function executeInspection(start, name, plan, options, persist) {
328
368
  const parsedConfig = parseConfig(config.stdout, target.profile);
329
369
  serverHash = parsedConfig.server ? digest(parsedConfig.server) : '';
330
370
  if (parsedConfig.workspaceId && parsedConfig.workspaceId !== target.workspaceId) {
331
- throw new MulticaReadFailure('scope.profile-workspace-mismatch');
371
+ throw new MulticaReadFailure('scope.profile-workspace-mismatch', {
372
+ code: 'scope.profile-workspace-mismatch',
373
+ expected: target.workspaceId,
374
+ actual: safeDisplay(parsedConfig.workspaceId),
375
+ hint: `profile ${target.profile} 的 workspace_id 指向另一个 Workspace;` +
376
+ '为目标 Workspace 建专属具名 profile(复制 config 只改 workspace_id),或修正现有 profile',
377
+ });
332
378
  }
333
379
  const auth = runRead(command, profileArgs(target, ['auth', 'status']), 'auth.status', target.root, env, timeoutMs, calls);
334
380
  const principal = parseAuth(auth.stdout + '\n' + auth.stderr);
335
381
  if (!principal)
336
- throw new MulticaReadFailure('auth.not-authenticated');
382
+ throw new MulticaReadFailure('auth.not-authenticated', {
383
+ code: 'auth.not-authenticated',
384
+ hint: `确认 multica --profile ${target.profile} 已登录;` +
385
+ 'dta 调用 CLI 时已剔除保留变量(MULTICA_AGENT_ID/MULTICA_SERVER_URL/MULTICA_TOKEN),' +
386
+ '身份与 endpoint 只认具名 profile',
387
+ });
337
388
  principalHash = digest(principal);
338
389
  const workspaceList = jsonArray(runRead(command, profileArgs(target, ['workspace', 'list', '--output', 'json']), 'workspace.list', target.root, env, timeoutMs, calls).stdout, 'workspace list');
339
390
  const workspaceMatches = workspaceList.filter((item) => idOf(item) === target.workspaceId);
@@ -364,6 +415,27 @@ function executeInspection(start, name, plan, options, persist) {
364
415
  provider: safeDisplay(runtime.provider),
365
416
  status: safeDisplay(runtime.status),
366
417
  };
418
+ // Skill 装载合同只在注册过 trace 词汇的 runtime provider 上成立。hermes 上部署会
419
+ // "成功"、Skill 显示 enabled,agent 一加载却 100% `Skill not found` 且零告警——
420
+ // 这里 fail-closed,把确定性死局挡在任何远端写入之前。
421
+ const runtimeProvider = resources.runtime.provider;
422
+ if (!runtimeProvider) {
423
+ throw new MulticaReadFailure('scope.runtime-provider-unknown', {
424
+ code: 'scope.runtime-provider-unknown',
425
+ expected: supportedRuntimeProviders(),
426
+ actual: '(empty)',
427
+ hint: '远端 runtime 未报告 provider 字段;用 multica runtime get 核实后再部署',
428
+ });
429
+ }
430
+ if (!vocabulariesForRuntimeProvider(runtimeProvider).length) {
431
+ throw new MulticaReadFailure('scope.runtime-provider-unsupported', {
432
+ code: 'scope.runtime-provider-unsupported',
433
+ expected: supportedRuntimeProviders(),
434
+ actual: runtimeProvider,
435
+ hint: 'dta 的 Skill 装载合同只在这些 runtime provider 上成立' +
436
+ '(如 hermes 上 workspace skill 一律加载不到);换用受支持 provider 的 runtime',
437
+ });
438
+ }
367
439
  const agents = jsonArray(runRead(command, scopedArgs(target, ['agent', 'list', '--include-archived', '--output', 'json']), 'agent.list', target.root, env, timeoutMs, calls).stdout, 'agent list');
368
440
  resources.agentCandidates = agents
369
441
  .filter((item) => optionalField(item.name) === target.projectName)
@@ -389,9 +461,26 @@ function executeInspection(start, name, plan, options, persist) {
389
461
  const agent = jsonObject(runRead(command, scopedArgs(target, ['agent', 'get', target.agentId, '--output', 'json']), 'agent.get', target.root, env, timeoutMs, calls).stdout, 'agent get');
390
462
  const agentWorkspace = optionalField(agent.workspace_id);
391
463
  const agentRuntime = optionalField(agent.runtime_id);
392
- if (idOf(agent) !== target.agentId || agentWorkspace !== target.workspaceId ||
393
- agentRuntime !== target.runtimeId) {
394
- throw new MulticaReadFailure('scope.agent-binding-mismatch');
464
+ if (idOf(agent) !== target.agentId || agentWorkspace !== target.workspaceId) {
465
+ throw new MulticaReadFailure('scope.agent-binding-mismatch', {
466
+ code: 'scope.agent-binding-mismatch',
467
+ expected: `workspace=${target.workspaceId}`,
468
+ actual: `workspace=${safeDisplay(agentWorkspace) || '(empty)'}`,
469
+ hint: 'Agent 不属于目标 Workspace;Workspace 边界不允许跨越',
470
+ });
471
+ }
472
+ // Runtime 归属漂移不再当 inspection 硬失败:那会把 deploy 与 --retire 一起锁死
473
+ // (retire 被它要解开的锁挡住)。漂移记录成 diff;apply 层负责在没有显式
474
+ // --rebind-runtime 时拒绝,retire 不受它阻挡。
475
+ if (agentRuntime !== target.runtimeId) {
476
+ diff.push('agent.runtime-drift');
477
+ failureDetails.push({
478
+ code: 'agent.runtime-drift',
479
+ expected: target.runtimeId,
480
+ actual: safeDisplay(agentRuntime) || '(empty)',
481
+ hint: '迁移 runtime 用 dta deploy --rebind-runtime(先 --dry-run);' +
482
+ '退役用 deploy --retire,不受漂移阻挡',
483
+ });
395
484
  }
396
485
  resources.agent = {
397
486
  id: target.agentId,
@@ -409,7 +498,12 @@ function executeInspection(start, name, plan, options, persist) {
409
498
  })).sort(compareNamedResource);
410
499
  }
411
500
  else if (resources.agentCandidates.length > 0) {
412
- throw new MulticaReadFailure('agent.unbound-name-candidates');
501
+ throw new MulticaReadFailure('agent.unbound-name-candidates', {
502
+ code: 'agent.unbound-name-candidates',
503
+ actual: resources.agentCandidates.map((item) => `${item.id}${item.archived ? ' (archived)' : ''}`),
504
+ hint: '远端已有同名 Agent,dta 不按名字猜绑定;核对候选后在 manifest 声明 ' +
505
+ 'provider.agentIdFrom 指向正确 ID(或依赖首次 deploy 写入的 state.binding.agentId)',
506
+ });
413
507
  }
414
508
  else {
415
509
  diff.push('agent.create');
@@ -428,7 +522,13 @@ function executeInspection(start, name, plan, options, persist) {
428
522
  }
429
523
  }
430
524
  catch (error) {
431
- failures.push(error instanceof MulticaReadFailure ? error.category : 'provider.read-failed');
525
+ if (error instanceof MulticaReadFailure) {
526
+ failures.push(error.category);
527
+ if (error.detail)
528
+ failureDetails.push(error.detail);
529
+ }
530
+ else
531
+ failures.push('provider.read-failed');
432
532
  }
433
533
  const canonical = {
434
534
  cliVersion,
@@ -468,6 +568,8 @@ function executeInspection(start, name, plan, options, persist) {
468
568
  resources,
469
569
  diff: [...diff].sort(),
470
570
  failures: [...failures].sort(),
571
+ failureDetails,
572
+ envSanitized: sanitized.removed,
471
573
  calls,
472
574
  evidencePath,
473
575
  remoteRead: true,
@@ -557,19 +659,22 @@ function resolveMulticaTarget(start, name, env, cliVersion, agentIdOverride = ''
557
659
  blocking.push('agent-id-override.invalid');
558
660
  }
559
661
  }
560
- else if (provider.agentIdFrom) {
662
+ else {
663
+ // 来源选择收口到 multicaAgentIdSource(schedule 共用同一份,防两处漂移)。
561
664
  // A declared-but-empty agentIdFrom means "not yet bound" on a workspace whose Agent
562
665
  // has not been created. Treat it like an omitted agentIdFrom (createable, no blocker):
563
666
  // the deploy W4 create path saves the real ID to state, and the run/retire paths add
564
667
  // their own `agent-id.missing` blocker. A malformed (non-empty, invalid) value still blocks.
565
- agentId = resolveRef(provider.agentIdFrom, 'agent-id', true);
566
- }
567
- else if (workspace.state?.binding.agentId) {
568
- try {
569
- agentId = resourceId(workspace.state.binding.agentId, 'state.agent-id');
570
- }
571
- catch {
572
- blocking.push('state.agent-id.invalid');
668
+ const source = multicaAgentIdSource(workspace);
669
+ if (source.kind === 'declared')
670
+ agentId = resolveRef(source.ref, 'agent-id', true);
671
+ else if (source.kind === 'state') {
672
+ try {
673
+ agentId = resourceId(source.agentId, 'state.agent-id');
674
+ }
675
+ catch {
676
+ blocking.push('state.agent-id.invalid');
677
+ }
573
678
  }
574
679
  }
575
680
  return {
@@ -615,7 +720,7 @@ function scopedArgs(target, args) {
615
720
  function runRead(command, argv, id, cwd, env, timeoutMs, calls) {
616
721
  const run = spawnSync(command, argv, {
617
722
  cwd,
618
- env,
723
+ env: sanitizeProviderEnv(env).env,
619
724
  encoding: 'utf8',
620
725
  timeout: timeoutMs,
621
726
  maxBuffer: 16 * 1024 * 1024,
@@ -633,10 +738,27 @@ function runRead(command, argv, id, cwd, env, timeoutMs, calls) {
633
738
  stderrHash: sha256(stderr),
634
739
  passed,
635
740
  });
636
- if (!passed)
637
- throw new MulticaReadFailure(`command.${id}`);
741
+ if (!passed) {
742
+ emitProviderDiagnostic(id, exitCode, stderr || stdout);
743
+ throw new MulticaReadFailure(`command.${id}`, {
744
+ code: `command.${id}`,
745
+ actual: `exit=${exitCode}`,
746
+ hint: 'provider CLI 原始 stderr 已透传到终端(证据文件只存 hash)',
747
+ });
748
+ }
638
749
  return { stdout, stderr };
639
750
  }
751
+ /**
752
+ * Provider CLI failures used to vanish into a hash: the receipt recorded `stderrHash` and the
753
+ * user saw only `command.<id>`. The raw text still must never enter evidence files, so it goes
754
+ * straight to dta's own stderr — visible in the moment, never persisted.
755
+ */
756
+ export function emitProviderDiagnostic(id, exitCode, output) {
757
+ const text = output.trim();
758
+ const body = !text ? '(无 stderr 输出)'
759
+ : text.length > 2048 ? `${text.slice(0, 2048)}…` : text;
760
+ process.stderr.write(`[multica ${id}] exit=${exitCode} ${body}\n`);
761
+ }
640
762
  function parseMulticaVersion(value) {
641
763
  const match = value.match(/\bmultica\s+([0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?)/i);
642
764
  if (!match)
@@ -757,6 +879,8 @@ function emptyStatus(workspace) {
757
879
  observedHash: workspace.observedHash,
758
880
  diff: [],
759
881
  failures: ['inspection.missing'],
882
+ failureDetails: [],
883
+ observedAgentRuntimeId: '',
760
884
  remoteRead: false,
761
885
  remoteWrite: false,
762
886
  sideEffect: false,