@xdxer/dingtalk-agent 0.1.4-beta.8 → 0.1.4-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.
- package/CHANGELOG.md +35 -0
- package/README.en.md +395 -0
- package/README.md +466 -35
- package/dist/bin/dingtalk-agent.js +1058 -10
- 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 +460 -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 +678 -0
- package/dist/src/agent-enhance.js.map +1 -0
- package/dist/src/bootstrap.js +125 -17
- package/dist/src/bootstrap.js.map +1 -1
- package/dist/src/development-workspace.js +729 -0
- package/dist/src/development-workspace.js.map +1 -0
- 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/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/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 +1480 -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 +1062 -0
- package/dist/src/opencode-evals.js.map +1 -0
- package/dist/src/opencode-provider.js +531 -0
- package/dist/src/opencode-provider.js.map +1 -0
- package/dist/src/opencode-workspace.js +197 -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 +501 -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 +770 -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/skill-manager.js +25 -16
- package/dist/src/skill-manager.js.map +1 -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/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 +47 -0
- 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/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/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 +112 -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 +65 -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/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 +266 -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 +21 -3
- package/skills/dingtalk-agent-boot-multica/SKILL.md +40 -0
- package/skills/dingtalk-agent-compose/SKILL.md +110 -0
- package/skills/dingtalk-agent-compose/assets/AGENTS.template.md +26 -0
- package/skills/dingtalk-agent-compose/assets/agent.bindings.dingtalk-doc.template.json +13 -0
- package/skills/dingtalk-agent-compose/assets/agent.bindings.local.template.json +13 -0
- package/skills/dingtalk-agent-compose/assets/opencode.template.json +12 -0
- package/skills/dingtalk-agent-compose/assets/role-skill.template.md +24 -0
- package/skills/dingtalk-agent-compose/evals/evals.json +94 -0
- package/skills/dingtalk-agent-compose/references/agent-definition-contract.md +55 -0
- package/skills/dingtalk-agent-compose/references/opencode-host-contract.md +65 -0
- package/skills/dingtalk-agent-compose/references/storage-routing.md +20 -0
- package/skills/dingtalk-agent-deploy/SKILL.md +60 -0
- package/skills/dingtalk-agent-deploy/references/multica-deployment-contract.md +49 -0
- package/skills/dingtalk-agent-deploy/references/promotion-observation-contract.md +49 -0
- package/skills/dingtalk-agent-eval/SKILL.md +116 -0
- package/skills/dingtalk-agent-eval/assets/eval-catalog.template.json +18 -0
- package/skills/dingtalk-agent-eval/evals/evals.json +61 -0
- package/skills/dingtalk-agent-eval/references/eval-topology.md +34 -0
- package/skills/dingtalk-agent-eval/references/evidence-contract.md +31 -0
- package/skills/dingtalk-agent-eval/references/scenario-taxonomy.md +25 -0
- package/skills/dingtalk-agent-eval/references/storage-modes.md +75 -0
- package/skills/dingtalk-basic-behavior/SKILL.md +63 -3
- package/skills/dingtalk-basic-behavior/assets/memory-candidate-proposal.json +10 -0
- package/skills/dingtalk-basic-behavior/assets/{task-checkpoint.md → task-checkpoint.json} +2 -21
- package/skills/dingtalk-basic-behavior/references/action-contract.md +2 -0
- package/skills/dingtalk-basic-behavior/references/memory-and-evolution.md +15 -1
- package/skills/dingtalk-basic-behavior/references/perception-and-gates.md +28 -0
- package/skills/dingtalk-basic-behavior/references/task-lifecycle.md +32 -7
|
@@ -0,0 +1,1480 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
6
|
+
import { WORKSPACE_STATE_ROOT, inspectAgentProject, showDevelopmentWorkspace, } from './development-workspace.js';
|
|
7
|
+
import { digest, stableStringify } from './events.js';
|
|
8
|
+
import { MULTICA_BOOT_SKILL, MULTICA_EVIDENCE_ROOT, inspectMulticaWorkspace, planMulticaWorkspace, statusMulticaWorkspace, } from './multica-provider.js';
|
|
9
|
+
import { resolvePackageRoot } from './package-root.js';
|
|
10
|
+
export const MULTICA_DEPLOYMENT_SCHEMA = 'dingtalk-agent/multica-deployment-receipt@1';
|
|
11
|
+
const MAX_DEPLOY_SKILLS = 32;
|
|
12
|
+
const MAX_SKILL_SUPPORTING_FILES = 128;
|
|
13
|
+
const deploymentWriteBudgets = new WeakMap();
|
|
14
|
+
class DeploymentFailure extends Error {
|
|
15
|
+
category;
|
|
16
|
+
ambiguous;
|
|
17
|
+
constructor(category, ambiguous = false) {
|
|
18
|
+
super(category);
|
|
19
|
+
this.category = category;
|
|
20
|
+
this.ambiguous = ambiguous;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function planMulticaDeployment(start, name, options = {}) {
|
|
24
|
+
const action = options.action || 'apply';
|
|
25
|
+
const env = options.env || process.env;
|
|
26
|
+
const cliVersion = options.cliVersion || '';
|
|
27
|
+
const info = inspectAgentProject(start, cliVersion);
|
|
28
|
+
const workspace = requireMulticaWorkspace(info.root, name, cliVersion);
|
|
29
|
+
const source = loadDeploymentSource(info.root);
|
|
30
|
+
const readPlan = planMulticaWorkspace(info.root, name, { env, cliVersion });
|
|
31
|
+
const blocking = [...readPlan.blocking];
|
|
32
|
+
let inspectionId = '';
|
|
33
|
+
try {
|
|
34
|
+
const status = statusMulticaWorkspace(info.root, name, cliVersion);
|
|
35
|
+
inspectionId = status.inspectionId;
|
|
36
|
+
if (!status.readyForApply || !status.evidenceIntegrity || !status.stateMatched) {
|
|
37
|
+
blocking.push(...status.failures.map((item) => `inspection.${item}`));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
blocking.push('inspection.invalid');
|
|
42
|
+
}
|
|
43
|
+
if (!inspectionId)
|
|
44
|
+
blocking.push('inspection.missing');
|
|
45
|
+
if (action === 'retire' && !readPlan.expected.agentId)
|
|
46
|
+
blocking.push('agent-id.missing');
|
|
47
|
+
const expectedSkills = source.skills.map((skill) => ({
|
|
48
|
+
name: skill.name, hash: skill.hash, files: skill.files.length + 1,
|
|
49
|
+
}));
|
|
50
|
+
const operations = action === 'retire'
|
|
51
|
+
? ['scope.reinspect', 'agent.archive', 'agent.readback', 'receipt.persist']
|
|
52
|
+
: [
|
|
53
|
+
'scope.reinspect', 'skills.readback', 'skills.reconcile', 'agent.reconcile',
|
|
54
|
+
'skills.assign-exact', 'provider.readback', 'load-smoke.create',
|
|
55
|
+
'load-smoke.readback', 'receipt.persist',
|
|
56
|
+
];
|
|
57
|
+
const writeBudget = action === 'retire'
|
|
58
|
+
? { forward: 1, rollback: 0 }
|
|
59
|
+
: {
|
|
60
|
+
forward: source.skills.length * (1 + MAX_SKILL_SUPPORTING_FILES * 2) + 4,
|
|
61
|
+
rollback: source.skills.length * (1 + MAX_SKILL_SUPPORTING_FILES * 2) + 3,
|
|
62
|
+
};
|
|
63
|
+
const planId = `multica_deploy_plan_${digest(stableStringify({
|
|
64
|
+
action, workspace: name, profile: readPlan.profile, inspectionId,
|
|
65
|
+
desiredHash: workspace.desiredHash, deploymentHash: source.deploymentHash,
|
|
66
|
+
expected: readPlan.expected, operations, writeBudget,
|
|
67
|
+
blocking: [...new Set(blocking)].sort(),
|
|
68
|
+
})).slice(0, 24)}`;
|
|
69
|
+
return {
|
|
70
|
+
$schema: 'dingtalk-agent/multica-deployment-plan@1',
|
|
71
|
+
planId,
|
|
72
|
+
action,
|
|
73
|
+
workspace: name,
|
|
74
|
+
provider: 'multica',
|
|
75
|
+
profile: readPlan.profile,
|
|
76
|
+
inspectionId,
|
|
77
|
+
desiredHash: workspace.desiredHash,
|
|
78
|
+
deploymentHash: source.deploymentHash,
|
|
79
|
+
expected: {
|
|
80
|
+
workspaceId: readPlan.expected.workspaceId,
|
|
81
|
+
runtimeId: readPlan.expected.runtimeId,
|
|
82
|
+
agentId: readPlan.expected.agentId,
|
|
83
|
+
agentName: info.project.name,
|
|
84
|
+
skills: expectedSkills,
|
|
85
|
+
definitionHash: source.definitionHash,
|
|
86
|
+
instructionsHash: source.instructionsHash,
|
|
87
|
+
},
|
|
88
|
+
operations,
|
|
89
|
+
writeBudget,
|
|
90
|
+
blocking: [...new Set(blocking)].sort(),
|
|
91
|
+
requiresPlanId: true,
|
|
92
|
+
requiresExplicitYes: true,
|
|
93
|
+
remoteRead: false,
|
|
94
|
+
remoteWrite: false,
|
|
95
|
+
triggerWrite: false,
|
|
96
|
+
sideEffect: false,
|
|
97
|
+
dingtalkSideEffect: false,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function applyMulticaDeployment(start, name, options = {}, action = 'apply') {
|
|
101
|
+
if (!options.yes) {
|
|
102
|
+
throw new Error('dta deploy 会修改 Multica 远端资源;必须显式传 --yes 和当前 --plan-id');
|
|
103
|
+
}
|
|
104
|
+
if (!options.planId)
|
|
105
|
+
throw new Error('dta deploy 必须传 dry-run 返回的 --plan-id');
|
|
106
|
+
const env = options.env || process.env;
|
|
107
|
+
const cliVersion = options.cliVersion || '';
|
|
108
|
+
const plan = planMulticaDeployment(start, name, { action, env, cliVersion });
|
|
109
|
+
if (options.planId !== plan.planId) {
|
|
110
|
+
throw new Error(`deploy plan 已失效;期望当前 planId ${plan.planId}`);
|
|
111
|
+
}
|
|
112
|
+
if (plan.blocking.length)
|
|
113
|
+
throw new Error(`deploy 前置条件不完整: ${plan.blocking.join(', ')}`);
|
|
114
|
+
const info = inspectAgentProject(start, cliVersion);
|
|
115
|
+
const workspace = requireMulticaWorkspace(info.root, name, cliVersion);
|
|
116
|
+
const source = loadDeploymentSource(info.root);
|
|
117
|
+
const command = options.command || env.DTA_MULTICA_BIN || 'multica';
|
|
118
|
+
const timeoutMs = positiveTimeout(options.timeoutMs, 30_000, 300_000);
|
|
119
|
+
const calls = [];
|
|
120
|
+
deploymentWriteBudgets.set(calls, plan.writeBudget);
|
|
121
|
+
const preflight = inspectMulticaWorkspace(info.root, name, {
|
|
122
|
+
execute: true, yes: true, persist: false, command, env, timeoutMs, cliVersion,
|
|
123
|
+
});
|
|
124
|
+
if (!preflight.passed || preflight.inspectionId !== plan.inspectionId ||
|
|
125
|
+
preflight.profile !== plan.profile ||
|
|
126
|
+
preflight.expected.workspaceId !== plan.expected.workspaceId ||
|
|
127
|
+
preflight.expected.runtimeId !== plan.expected.runtimeId) {
|
|
128
|
+
throw new Error('deploy scope/readback 与冻结 plan 不一致;未执行任何远端写入');
|
|
129
|
+
}
|
|
130
|
+
appendInspectionCalls(preflight, 'preflight', calls);
|
|
131
|
+
const operationId = `multica_operation_${Date.now()}_${randomUUID().slice(0, 12)}`;
|
|
132
|
+
const operation = createOperation(operationId, plan);
|
|
133
|
+
operation.calls = [...calls];
|
|
134
|
+
persistOperation(info.root, operation);
|
|
135
|
+
const rollback = {
|
|
136
|
+
createdSkillIds: [], updatedSkills: [], createdAgentId: '', updatedAgent: null,
|
|
137
|
+
assignmentChanged: false, previousAssignedIds: [],
|
|
138
|
+
};
|
|
139
|
+
let receipt;
|
|
140
|
+
const temp = mkdtempSync(join(tmpdir(), `dta-multica-deploy-${safeName(name)}-`));
|
|
141
|
+
try {
|
|
142
|
+
if (action === 'retire') {
|
|
143
|
+
receipt = retireDeployment(info.root, workspace, source, plan, preflight, operation, command, env, timeoutMs, calls);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
receipt = executeApply(info.root, workspace, source, plan, preflight, operation, command, env, timeoutMs, positiveTimeout(options.smokeTimeoutMs, 30_000, 60_000), Boolean(options.noWait), calls, rollback, temp);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
const failure = error instanceof DeploymentFailure
|
|
151
|
+
? error : new DeploymentFailure('deploy.internal-failure');
|
|
152
|
+
const rollbackReport = failure.ambiguous
|
|
153
|
+
? emptyRollback(false)
|
|
154
|
+
: rollbackConfirmed(info.root, plan, command, env, timeoutMs, calls, rollback, temp);
|
|
155
|
+
const status = failure.ambiguous ? 'reconciling' : 'failed';
|
|
156
|
+
operation.status = status;
|
|
157
|
+
operation.ambiguousAction = failure.ambiguous ? failure.category : '';
|
|
158
|
+
operation.calls = calls;
|
|
159
|
+
operation.failures = [failure.category, ...rollbackReport.failures];
|
|
160
|
+
operation.updatedAt = new Date().toISOString();
|
|
161
|
+
receipt = failureReceipt(info.root, workspace, source, plan, preflight, operation, calls, rollbackReport, status, failure.category);
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
rmSync(temp, { recursive: true, force: true });
|
|
165
|
+
deploymentWriteBudgets.delete(calls);
|
|
166
|
+
}
|
|
167
|
+
persistReceiptAndState(info.root, workspace, operation, receipt);
|
|
168
|
+
return receipt;
|
|
169
|
+
}
|
|
170
|
+
function executeApply(root, workspace, source, plan, preflight, operation, command, env, timeoutMs, smokeTimeoutMs, noWait, calls, rollback, temp) {
|
|
171
|
+
const snapshot = readRemoteSnapshot(root, source, preflight, command, env, timeoutMs, calls);
|
|
172
|
+
const exactBefore = remoteMatchesDesired(snapshot, source, plan.expected.runtimeId);
|
|
173
|
+
const previous = loadLatestReceipt(root, workspace.name);
|
|
174
|
+
if (exactBefore && workspace.state?.status === 'ready' &&
|
|
175
|
+
workspace.state.binding.deploymentHash === source.deploymentHash &&
|
|
176
|
+
previous?.passed && previous.providerReady && previous.deploymentHash === source.deploymentHash) {
|
|
177
|
+
const receiptId = `multica_receipt_${digest(stableStringify({
|
|
178
|
+
operationId: operation.operationId, planId: plan.planId, reuse: previous.receiptId,
|
|
179
|
+
})).slice(0, 24)}`;
|
|
180
|
+
return {
|
|
181
|
+
...previous,
|
|
182
|
+
receiptId,
|
|
183
|
+
operationId: operation.operationId,
|
|
184
|
+
planId: plan.planId,
|
|
185
|
+
idempotent: true,
|
|
186
|
+
calls: [...calls],
|
|
187
|
+
agent: { ...previous.agent, action: 'noop' },
|
|
188
|
+
skills: previous.skills.map((skill) => ({ ...skill, action: 'noop' })),
|
|
189
|
+
evidencePath: receiptRelativePath(workspace.name, receiptId),
|
|
190
|
+
remoteWrite: false,
|
|
191
|
+
sideEffect: 'multica-read',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
operation.status = 'applying';
|
|
195
|
+
operation.calls = calls;
|
|
196
|
+
operation.updatedAt = new Date().toISOString();
|
|
197
|
+
persistOperation(root, operation);
|
|
198
|
+
const skillResults = [];
|
|
199
|
+
const skillIds = [];
|
|
200
|
+
for (const local of source.skills) {
|
|
201
|
+
const remote = snapshot.skills.get(local.name) || null;
|
|
202
|
+
const result = reconcileSkill(root, plan, local, remote, command, env, timeoutMs, calls, rollback, temp);
|
|
203
|
+
skillIds.push(result.skill.id);
|
|
204
|
+
skillResults.push({
|
|
205
|
+
id: result.skill.id,
|
|
206
|
+
name: local.name,
|
|
207
|
+
sourceHash: local.hash,
|
|
208
|
+
remoteHash: result.skill.hash,
|
|
209
|
+
action: result.action,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
const agentResult = reconcileAgent(root, plan, source, snapshot.agent, command, env, timeoutMs, calls, rollback);
|
|
213
|
+
const agentId = agentResult.agent.id;
|
|
214
|
+
operation.agentId = agentId;
|
|
215
|
+
const expectedAssignments = [...skillIds].sort();
|
|
216
|
+
rollback.previousAssignedIds = [...snapshot.assignedIds];
|
|
217
|
+
if (stableStringify([...snapshot.assignedIds].sort()) !== stableStringify(expectedAssignments)) {
|
|
218
|
+
runProvider(command, scopedArgs(plan, [
|
|
219
|
+
'agent', 'skills', 'set', agentId, '--skill-ids', expectedAssignments.join(','), '--output', 'json',
|
|
220
|
+
]), 'agent.skills.set', 'write', root, env, timeoutMs, calls);
|
|
221
|
+
rollback.assignmentChanged = true;
|
|
222
|
+
}
|
|
223
|
+
const readback = inspectMulticaWorkspace(root, workspace.name, {
|
|
224
|
+
execute: true, yes: true, persist: true, command, env, timeoutMs,
|
|
225
|
+
agentIdOverride: agentId,
|
|
226
|
+
});
|
|
227
|
+
if (!readback.passed || !readback.resources.agent ||
|
|
228
|
+
readback.resources.agent.id !== agentId ||
|
|
229
|
+
readback.resources.agent.runtimeId !== plan.expected.runtimeId) {
|
|
230
|
+
throw new DeploymentFailure('readback.agent-mismatch');
|
|
231
|
+
}
|
|
232
|
+
const finalSnapshot = readRemoteSnapshot(root, source, readback, command, env, timeoutMs, calls);
|
|
233
|
+
if (!remoteMatchesDesired(finalSnapshot, source, plan.expected.runtimeId)) {
|
|
234
|
+
throw new DeploymentFailure('readback.deployment-mismatch');
|
|
235
|
+
}
|
|
236
|
+
const observedHash = deploymentObservedHash(finalSnapshot, readback.inspectionId);
|
|
237
|
+
const smoke = createAndReadSmoke(root, plan, source, agentId, command, env, timeoutMs, smokeTimeoutMs, noWait, calls);
|
|
238
|
+
const status = smoke.status === 'passed'
|
|
239
|
+
? 'ready' : smoke.status === 'pending' ? 'verifying' : 'failed';
|
|
240
|
+
const receiptId = `multica_receipt_${digest(stableStringify({
|
|
241
|
+
operationId: operation.operationId, planId: plan.planId, observedHash,
|
|
242
|
+
smoke: { markerHash: smoke.markerHash, issueId: smoke.issueId, taskId: smoke.taskId },
|
|
243
|
+
})).slice(0, 24)}`;
|
|
244
|
+
return {
|
|
245
|
+
$schema: MULTICA_DEPLOYMENT_SCHEMA,
|
|
246
|
+
receiptId,
|
|
247
|
+
operationId: operation.operationId,
|
|
248
|
+
planId: plan.planId,
|
|
249
|
+
action: 'apply',
|
|
250
|
+
workspace: workspace.name,
|
|
251
|
+
provider: 'multica',
|
|
252
|
+
profile: plan.profile,
|
|
253
|
+
status,
|
|
254
|
+
passed: status === 'ready',
|
|
255
|
+
providerReady: status === 'ready',
|
|
256
|
+
idempotent: false,
|
|
257
|
+
inspectionId: readback.inspectionId,
|
|
258
|
+
inspectionObservedHash: readback.observedHash,
|
|
259
|
+
inspectionEvidencePath: readback.evidencePath,
|
|
260
|
+
desiredHash: workspace.desiredHash,
|
|
261
|
+
deploymentHash: source.deploymentHash,
|
|
262
|
+
observedHash,
|
|
263
|
+
agent: {
|
|
264
|
+
id: finalSnapshot.agent?.id || agentId,
|
|
265
|
+
runtimeId: finalSnapshot.agent?.runtimeId || '',
|
|
266
|
+
instructionsHash: sha256(finalSnapshot.agent?.instructions || ''),
|
|
267
|
+
archived: Boolean(finalSnapshot.agent?.archived),
|
|
268
|
+
action: agentResult.action,
|
|
269
|
+
},
|
|
270
|
+
skills: skillResults.sort((a, b) => a.name.localeCompare(b.name)),
|
|
271
|
+
assignmentHash: digest(stableStringify(expectedAssignments)),
|
|
272
|
+
calls: [...calls],
|
|
273
|
+
smoke,
|
|
274
|
+
rollback: emptyRollback(false),
|
|
275
|
+
failures: [...smoke.failures],
|
|
276
|
+
evidencePath: receiptRelativePath(workspace.name, receiptId),
|
|
277
|
+
remoteRead: true,
|
|
278
|
+
remoteWrite: calls.some((call) => call.kind === 'write'),
|
|
279
|
+
triggerWrite: false,
|
|
280
|
+
sideEffect: 'multica-read-write',
|
|
281
|
+
dingtalkSideEffect: false,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function retireDeployment(root, workspace, source, plan, preflight, operation, command, env, timeoutMs, calls) {
|
|
285
|
+
const agentId = preflight.resources.agent?.id || plan.expected.agentId;
|
|
286
|
+
if (!agentId)
|
|
287
|
+
throw new DeploymentFailure('retire.agent-id-missing');
|
|
288
|
+
if (!preflight.resources.agent?.archived) {
|
|
289
|
+
runProvider(command, scopedArgs(plan, ['agent', 'archive', agentId, '--output', 'json']), 'agent.archive', 'write', root, env, timeoutMs, calls);
|
|
290
|
+
}
|
|
291
|
+
const agent = parseRemoteAgent(jsonObject(runProvider(command, scopedArgs(plan, ['agent', 'get', agentId, '--output', 'json']), 'agent.get.retire-readback', 'read', root, env, timeoutMs, calls).stdout, 'retire agent'));
|
|
292
|
+
if (!agent.archived || agent.id !== agentId)
|
|
293
|
+
throw new DeploymentFailure('retire.readback-mismatch');
|
|
294
|
+
const observedHash = digest(stableStringify({ agentId, archived: true, deploymentHash: source.deploymentHash }));
|
|
295
|
+
const receiptId = `multica_receipt_${digest(stableStringify({
|
|
296
|
+
operationId: operation.operationId, action: 'retire', observedHash,
|
|
297
|
+
})).slice(0, 24)}`;
|
|
298
|
+
return {
|
|
299
|
+
$schema: MULTICA_DEPLOYMENT_SCHEMA,
|
|
300
|
+
receiptId,
|
|
301
|
+
operationId: operation.operationId,
|
|
302
|
+
planId: plan.planId,
|
|
303
|
+
action: 'retire',
|
|
304
|
+
workspace: workspace.name,
|
|
305
|
+
provider: 'multica',
|
|
306
|
+
profile: plan.profile,
|
|
307
|
+
status: 'retired',
|
|
308
|
+
passed: true,
|
|
309
|
+
providerReady: false,
|
|
310
|
+
idempotent: Boolean(preflight.resources.agent?.archived),
|
|
311
|
+
inspectionId: preflight.inspectionId,
|
|
312
|
+
inspectionObservedHash: preflight.observedHash,
|
|
313
|
+
inspectionEvidencePath: preflight.evidencePath,
|
|
314
|
+
desiredHash: workspace.desiredHash,
|
|
315
|
+
deploymentHash: source.deploymentHash,
|
|
316
|
+
observedHash,
|
|
317
|
+
agent: {
|
|
318
|
+
id: agent.id,
|
|
319
|
+
runtimeId: agent.runtimeId,
|
|
320
|
+
instructionsHash: sha256(agent.instructions),
|
|
321
|
+
archived: true,
|
|
322
|
+
action: preflight.resources.agent?.archived ? 'noop' : 'archive',
|
|
323
|
+
},
|
|
324
|
+
skills: [],
|
|
325
|
+
assignmentHash: '',
|
|
326
|
+
calls: [...calls],
|
|
327
|
+
smoke: emptySmoke('not-run', source.skills.map((item) => item.name)),
|
|
328
|
+
rollback: emptyRollback(false),
|
|
329
|
+
failures: [],
|
|
330
|
+
evidencePath: receiptRelativePath(workspace.name, receiptId),
|
|
331
|
+
remoteRead: true,
|
|
332
|
+
remoteWrite: calls.some((call) => call.kind === 'write'),
|
|
333
|
+
triggerWrite: false,
|
|
334
|
+
sideEffect: 'multica-read-write',
|
|
335
|
+
dingtalkSideEffect: false,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function loadDeploymentSource(root) {
|
|
339
|
+
const info = inspectAgentProject(root);
|
|
340
|
+
const definitionPath = safeProjectPath(root, info.project.definition, true);
|
|
341
|
+
const definition = readUtf8(definitionPath, 'Agent Definition', 512 * 1024);
|
|
342
|
+
rejectCredentialMaterial(definition, 'Agent Definition');
|
|
343
|
+
const definitionHash = sha256(definition);
|
|
344
|
+
const skillRoots = [];
|
|
345
|
+
for (const name of info.project.skills) {
|
|
346
|
+
const ref = name === 'dingtalk-basic-behavior'
|
|
347
|
+
? `.agents/skills/${name}` : `skills/${name}`;
|
|
348
|
+
skillRoots.push({ name, root: safeProjectPath(root, ref, true) });
|
|
349
|
+
}
|
|
350
|
+
const packageRoot = resolvePackageRoot(import.meta.url);
|
|
351
|
+
skillRoots.push({
|
|
352
|
+
name: MULTICA_BOOT_SKILL,
|
|
353
|
+
root: safeExternalSkillRoot(join(packageRoot, 'skills', MULTICA_BOOT_SKILL)),
|
|
354
|
+
});
|
|
355
|
+
if (skillRoots.length > MAX_DEPLOY_SKILLS) {
|
|
356
|
+
throw new Error(`Multica deploy 最多支持 ${MAX_DEPLOY_SKILLS} 个受管 Skills`);
|
|
357
|
+
}
|
|
358
|
+
const skills = skillRoots.map(({ name, root: skillRoot }) => loadSkillBundle(name, skillRoot))
|
|
359
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
360
|
+
if (new Set(skills.map((item) => item.name)).size !== skills.length) {
|
|
361
|
+
throw new Error('Multica deploy required Skills 不能重名');
|
|
362
|
+
}
|
|
363
|
+
const required = skills.map((item) => ({ name: item.name, sha256: item.hash }));
|
|
364
|
+
const deploymentSourceHash = digest(stableStringify({ definitionHash, required }));
|
|
365
|
+
const instructions = [
|
|
366
|
+
'<!-- DTA Multica Boot: managed; do not edit remotely -->',
|
|
367
|
+
`definition_sha256=${definitionHash}`,
|
|
368
|
+
`deployment_sha256=${deploymentSourceHash}`,
|
|
369
|
+
`required_skills=${JSON.stringify(required)}`,
|
|
370
|
+
'Before every task, use dingtalk-agent-boot-multica and follow its startup order.',
|
|
371
|
+
'Never create a robot, webhook, autopilot, schedule, or other Trigger as part of deployment.',
|
|
372
|
+
'<!-- /DTA Multica Boot -->',
|
|
373
|
+
'',
|
|
374
|
+
definition.trimEnd(),
|
|
375
|
+
'',
|
|
376
|
+
].join('\n');
|
|
377
|
+
const instructionsHash = sha256(instructions);
|
|
378
|
+
const deploymentHash = digest(stableStringify({
|
|
379
|
+
definitionHash, instructionsHash,
|
|
380
|
+
skills: skills.map((item) => ({ name: item.name, hash: item.hash })),
|
|
381
|
+
}));
|
|
382
|
+
return { definition, definitionHash, instructions, instructionsHash, skills, deploymentHash };
|
|
383
|
+
}
|
|
384
|
+
function loadSkillBundle(name, root) {
|
|
385
|
+
const actual = realpathSync(root);
|
|
386
|
+
if (!lstatSync(actual).isDirectory())
|
|
387
|
+
throw new Error(`Skill ${name} source 不是目录`);
|
|
388
|
+
const files = [];
|
|
389
|
+
const visit = (directory, prefix = '') => {
|
|
390
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })
|
|
391
|
+
.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
392
|
+
const path = join(directory, entry.name);
|
|
393
|
+
const ref = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
394
|
+
if (entry.isSymbolicLink())
|
|
395
|
+
throw new Error(`Skill ${name} 不允许符号链接: ${ref}`);
|
|
396
|
+
if (entry.isDirectory())
|
|
397
|
+
visit(path, ref);
|
|
398
|
+
else if (entry.isFile()) {
|
|
399
|
+
const content = readFileSync(path);
|
|
400
|
+
if (content.length > 1024 * 1024)
|
|
401
|
+
throw new Error(`Skill ${name} 文件超过 1 MiB: ${ref}`);
|
|
402
|
+
const text = content.toString('utf8');
|
|
403
|
+
if (Buffer.from(text, 'utf8').compare(content) !== 0 || content.includes(0)) {
|
|
404
|
+
throw new Error(`Skill ${name} 只支持 UTF-8 supporting files: ${ref}`);
|
|
405
|
+
}
|
|
406
|
+
rejectCredentialMaterial(text, `Skill ${name}/${ref}`);
|
|
407
|
+
files.push({ path: ref, absolutePath: path, hash: sha256(content), bytes: content.length });
|
|
408
|
+
if (files.length > MAX_SKILL_SUPPORTING_FILES + 1) {
|
|
409
|
+
throw new Error(`Skill ${name} files 超过 ${MAX_SKILL_SUPPORTING_FILES + 1} 个`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
else
|
|
413
|
+
throw new Error(`Skill ${name} 包含特殊文件: ${ref}`);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
visit(actual);
|
|
417
|
+
if (files.reduce((sum, item) => sum + item.bytes, 0) > 8 * 1024 * 1024) {
|
|
418
|
+
throw new Error(`Skill ${name} 总大小超过 8 MiB`);
|
|
419
|
+
}
|
|
420
|
+
const main = files.find((item) => item.path === 'SKILL.md');
|
|
421
|
+
if (!main)
|
|
422
|
+
throw new Error(`Skill ${name} 缺少 SKILL.md`);
|
|
423
|
+
const content = readUtf8(main.absolutePath, `Skill ${name}`, 1024 * 1024);
|
|
424
|
+
const declared = frontmatterName(content);
|
|
425
|
+
if (declared !== name)
|
|
426
|
+
throw new Error(`Skill ${name} frontmatter name 不一致: ${declared || '<missing>'}`);
|
|
427
|
+
rejectCredentialMaterial(content, `Skill ${name}`);
|
|
428
|
+
const supporting = files.filter((item) => item.path !== 'SKILL.md');
|
|
429
|
+
if (supporting.length > MAX_SKILL_SUPPORTING_FILES) {
|
|
430
|
+
throw new Error(`Skill ${name} supporting files 超过 ${MAX_SKILL_SUPPORTING_FILES} 个`);
|
|
431
|
+
}
|
|
432
|
+
const hash = skillTreeHash(content, supporting.map((item) => ({ path: item.path, hash: item.hash })));
|
|
433
|
+
return { name, root: actual, mainPath: main.absolutePath, content, files: supporting, hash };
|
|
434
|
+
}
|
|
435
|
+
function readRemoteSnapshot(root, source, inspection, command, env, timeoutMs, calls) {
|
|
436
|
+
const planLike = {
|
|
437
|
+
profile: inspection.profile,
|
|
438
|
+
expected: inspection.expected,
|
|
439
|
+
};
|
|
440
|
+
const skills = new Map();
|
|
441
|
+
for (const local of source.skills) {
|
|
442
|
+
const candidates = inspection.resources.catalogSkills.filter((item) => item.name === local.name);
|
|
443
|
+
if (candidates.length > 1)
|
|
444
|
+
throw new DeploymentFailure(`skills.duplicate:${local.name}`);
|
|
445
|
+
if (!candidates.length) {
|
|
446
|
+
skills.set(local.name, null);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const raw = jsonObject(runProvider(command, scopedArgs(planLike, ['skill', 'get', candidates[0].id, '--output', 'json']), `skill.get:${local.name}`, 'read', root, env, timeoutMs, calls).stdout, `skill ${local.name}`);
|
|
450
|
+
const skill = parseRemoteSkill(raw);
|
|
451
|
+
if (skill.id !== candidates[0].id || skill.name !== local.name) {
|
|
452
|
+
throw new DeploymentFailure(`skills.binding-mismatch:${local.name}`);
|
|
453
|
+
}
|
|
454
|
+
skills.set(local.name, skill);
|
|
455
|
+
}
|
|
456
|
+
let agent = null;
|
|
457
|
+
if (inspection.resources.agent) {
|
|
458
|
+
agent = parseRemoteAgent(jsonObject(runProvider(command, scopedArgs(planLike, [
|
|
459
|
+
'agent', 'get', inspection.resources.agent.id, '--output', 'json',
|
|
460
|
+
]), 'agent.get.deploy', 'read', root, env, timeoutMs, calls).stdout, 'agent deploy'));
|
|
461
|
+
if (agent.id !== inspection.resources.agent.id)
|
|
462
|
+
throw new DeploymentFailure('agent.binding-mismatch');
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
agent,
|
|
466
|
+
skills,
|
|
467
|
+
assignedIds: inspection.resources.assignedSkills.filter((item) => item.enabled)
|
|
468
|
+
.map((item) => item.id).sort(),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function reconcileSkill(root, plan, local, remote, command, env, timeoutMs, calls, rollback, temp) {
|
|
472
|
+
let id = remote?.id || '';
|
|
473
|
+
let action = 'noop';
|
|
474
|
+
if (!remote) {
|
|
475
|
+
const created = jsonObject(runProvider(command, scopedArgs(plan, [
|
|
476
|
+
'skill', 'create', '--name', local.name,
|
|
477
|
+
'--description', `Managed by dingtalk-agent (${local.hash.slice(0, 12)})`,
|
|
478
|
+
'--content-file', local.mainPath, '--output', 'json',
|
|
479
|
+
]), `skill.create:${local.name}`, 'write', root, env, timeoutMs, calls).stdout, `created skill ${local.name}`);
|
|
480
|
+
id = resourceId(created.id, `created skill ${local.name}.id`);
|
|
481
|
+
rollback.createdSkillIds.push(id);
|
|
482
|
+
action = 'create';
|
|
483
|
+
for (const file of local.files)
|
|
484
|
+
upsertSkillFile(root, plan, id, file.path, file.absolutePath, command, env, timeoutMs, calls);
|
|
485
|
+
}
|
|
486
|
+
else if (remote.hash !== local.hash) {
|
|
487
|
+
rollback.updatedSkills.push(remote);
|
|
488
|
+
runProvider(command, scopedArgs(plan, [
|
|
489
|
+
'skill', 'update', id, '--content-file', local.mainPath, '--output', 'json',
|
|
490
|
+
]), `skill.update:${local.name}`, 'write', root, env, timeoutMs, calls);
|
|
491
|
+
syncSkillFiles(root, plan, id, local, remote, command, env, timeoutMs, calls);
|
|
492
|
+
action = 'update';
|
|
493
|
+
}
|
|
494
|
+
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}`));
|
|
495
|
+
if (readback.name !== local.name || readback.hash !== local.hash) {
|
|
496
|
+
throw new DeploymentFailure(`skill.readback-mismatch:${local.name}`);
|
|
497
|
+
}
|
|
498
|
+
return { skill: readback, action };
|
|
499
|
+
}
|
|
500
|
+
function reconcileAgent(root, plan, source, remote, command, env, timeoutMs, calls, rollback) {
|
|
501
|
+
let id = remote?.id || '';
|
|
502
|
+
let action = 'noop';
|
|
503
|
+
if (!remote) {
|
|
504
|
+
const created = jsonObject(runProvider(command, scopedArgs(plan, [
|
|
505
|
+
'agent', 'create', '--name', plan.expected.agentName,
|
|
506
|
+
'--description', `Managed dingtalk-agent ${plan.deploymentHash.slice(0, 12)}`,
|
|
507
|
+
'--instructions', source.instructions,
|
|
508
|
+
'--runtime-id', plan.expected.runtimeId,
|
|
509
|
+
'--permission-mode', 'private', '--output', 'json',
|
|
510
|
+
]), 'agent.create', 'write', root, env, timeoutMs, calls).stdout, 'created agent');
|
|
511
|
+
id = resourceId(created.id, 'created agent.id');
|
|
512
|
+
rollback.createdAgentId = id;
|
|
513
|
+
action = 'create';
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
if (remote.archived) {
|
|
517
|
+
rollback.updatedAgent = remote;
|
|
518
|
+
runProvider(command, scopedArgs(plan, ['agent', 'restore', id, '--output', 'json']), 'agent.restore', 'write', root, env, timeoutMs, calls);
|
|
519
|
+
action = 'restore';
|
|
520
|
+
}
|
|
521
|
+
if (remote.name !== plan.expected.agentName || remote.runtimeId !== plan.expected.runtimeId ||
|
|
522
|
+
remote.instructions !== source.instructions) {
|
|
523
|
+
rollback.updatedAgent ||= remote;
|
|
524
|
+
runProvider(command, scopedArgs(plan, [
|
|
525
|
+
'agent', 'update', id, '--name', plan.expected.agentName,
|
|
526
|
+
'--instructions', source.instructions, '--runtime-id', plan.expected.runtimeId,
|
|
527
|
+
'--output', 'json',
|
|
528
|
+
]), 'agent.update', 'write', root, env, timeoutMs, calls);
|
|
529
|
+
action = action === 'restore' ? 'restore' : 'update';
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const agent = parseRemoteAgent(jsonObject(runProvider(command, scopedArgs(plan, ['agent', 'get', id, '--output', 'json']), 'agent.get.readback', 'read', root, env, timeoutMs, calls).stdout, 'agent readback'));
|
|
533
|
+
if (agent.id !== id || agent.name !== plan.expected.agentName ||
|
|
534
|
+
agent.runtimeId !== plan.expected.runtimeId || agent.instructions !== source.instructions || agent.archived) {
|
|
535
|
+
throw new DeploymentFailure('agent.readback-mismatch');
|
|
536
|
+
}
|
|
537
|
+
return { agent, action };
|
|
538
|
+
}
|
|
539
|
+
function syncSkillFiles(root, plan, skillId, local, remote, command, env, timeoutMs, calls) {
|
|
540
|
+
const remoteByPath = new Map(remote.files.map((item) => [item.path, item]));
|
|
541
|
+
const localPaths = new Set(local.files.map((item) => item.path));
|
|
542
|
+
for (const file of remote.files) {
|
|
543
|
+
if (localPaths.has(file.path))
|
|
544
|
+
continue;
|
|
545
|
+
runProvider(command, scopedArgs(plan, ['skill', 'files', 'delete', skillId, file.id]), `skill.file.delete:${local.name}:${file.path}`, 'write', root, env, timeoutMs, calls);
|
|
546
|
+
}
|
|
547
|
+
for (const file of local.files) {
|
|
548
|
+
if (remoteByPath.get(file.path)?.hash === file.hash)
|
|
549
|
+
continue;
|
|
550
|
+
upsertSkillFile(root, plan, skillId, file.path, file.absolutePath, command, env, timeoutMs, calls);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function upsertSkillFile(root, plan, skillId, path, contentFile, command, env, timeoutMs, calls) {
|
|
554
|
+
runProvider(command, scopedArgs(plan, [
|
|
555
|
+
'skill', 'files', 'upsert', skillId, '--path', path,
|
|
556
|
+
'--content-file', contentFile, '--output', 'json',
|
|
557
|
+
]), `skill.file.upsert:${path}`, 'write', root, env, timeoutMs, calls);
|
|
558
|
+
}
|
|
559
|
+
function createAndReadSmoke(root, plan, source, agentId, command, env, timeoutMs, smokeTimeoutMs, noWait, calls) {
|
|
560
|
+
const marker = `DTA-MULTICA-LOAD-${randomUUID()}`;
|
|
561
|
+
const requiredSkills = source.skills.map((item) => item.name).sort();
|
|
562
|
+
const issue = jsonObject(runProvider(command, scopedArgs(plan, [
|
|
563
|
+
'issue', 'create', '--title', `[DTA load smoke] ${marker}`,
|
|
564
|
+
'--description', [
|
|
565
|
+
'DTA_MULTICA_LOAD_SMOKE@1', `marker=${marker}`,
|
|
566
|
+
'Use only the trusted DTA Multica Boot required_skills list.',
|
|
567
|
+
'Do not call DWS, Shell, network, or write tools.',
|
|
568
|
+
].join('\n'),
|
|
569
|
+
'--assignee-id', agentId, '--output', 'json',
|
|
570
|
+
]), 'smoke.issue.create', 'write', root, env, timeoutMs, calls).stdout, 'smoke issue');
|
|
571
|
+
const issueId = resourceId(issue.id, 'smoke issue.id');
|
|
572
|
+
if (noWait)
|
|
573
|
+
return readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command, env, timeoutMs, calls);
|
|
574
|
+
const started = Date.now();
|
|
575
|
+
let smoke = readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command, env, timeoutMs, calls);
|
|
576
|
+
while (smoke.status === 'pending' && Date.now() - started < smokeTimeoutMs) {
|
|
577
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
|
|
578
|
+
smoke = readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command, env, timeoutMs, calls);
|
|
579
|
+
}
|
|
580
|
+
return smoke;
|
|
581
|
+
}
|
|
582
|
+
function readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command, env, timeoutMs, calls) {
|
|
583
|
+
const runs = jsonArray(runProvider(command, scopedArgs(plan, ['issue', 'runs', issueId, '--full-id', '--output', 'json']), 'smoke.issue.runs', 'read', root, env, timeoutMs, calls).stdout, 'smoke runs');
|
|
584
|
+
const candidates = runs.filter((item) => !optionalString(item.agent_id) ||
|
|
585
|
+
optionalString(item.agent_id) === agentId);
|
|
586
|
+
const run = candidates.sort((a, b) => optionalString(b.created_at).localeCompare(optionalString(a.created_at)))[0];
|
|
587
|
+
if (!run)
|
|
588
|
+
return {
|
|
589
|
+
...emptySmoke('pending', requiredSkills), marker, markerHash: sha256(marker), issueId,
|
|
590
|
+
};
|
|
591
|
+
const taskId = resourceId(run.id, 'smoke task.id');
|
|
592
|
+
const taskStatus = optionalString(run.status);
|
|
593
|
+
if (!['completed', 'failed', 'cancelled'].includes(taskStatus))
|
|
594
|
+
return {
|
|
595
|
+
...emptySmoke('pending', requiredSkills), marker, markerHash: sha256(marker), issueId, taskId, taskStatus,
|
|
596
|
+
};
|
|
597
|
+
const messages = jsonArray(runProvider(command, scopedArgs(plan, [
|
|
598
|
+
'issue', 'run-messages', taskId, '--issue', issueId, '--output', 'json',
|
|
599
|
+
]), 'smoke.issue.messages', 'read', root, env, timeoutMs, calls).stdout, 'smoke messages');
|
|
600
|
+
const toolUses = messages.filter((item) => optionalString(item.type) === 'tool_use');
|
|
601
|
+
const nonSkillTools = toolUses.filter((item) => !/skill/i.test(optionalString(item.tool)));
|
|
602
|
+
const loadedSkills = requiredSkills.filter((name) => toolUses.some((item) => JSON.stringify(item.input || {}).includes(name))).sort();
|
|
603
|
+
const texts = messages.filter((item) => optionalString(item.type) === 'text')
|
|
604
|
+
.map((item) => optionalString(item.content)).filter(Boolean);
|
|
605
|
+
const finalText = texts.at(-1) || '';
|
|
606
|
+
let responsePassed = false;
|
|
607
|
+
try {
|
|
608
|
+
const parsed = JSON.parse(finalText);
|
|
609
|
+
responsePassed = parsed?.schema === 'dta-multica-load-smoke@1' &&
|
|
610
|
+
parsed?.marker === marker &&
|
|
611
|
+
stableStringify(Array.isArray(parsed?.loaded) ? [...parsed.loaded].sort() : []) ===
|
|
612
|
+
stableStringify(requiredSkills);
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
responsePassed = false;
|
|
616
|
+
}
|
|
617
|
+
const toolTracePassed = nonSkillTools.length === 0 &&
|
|
618
|
+
stableStringify(loadedSkills) === stableStringify(requiredSkills);
|
|
619
|
+
const failures = [];
|
|
620
|
+
if (taskStatus !== 'completed')
|
|
621
|
+
failures.push(`smoke.task.${taskStatus || 'unknown'}`);
|
|
622
|
+
if (!toolTracePassed)
|
|
623
|
+
failures.push('smoke.skill-tool-trace');
|
|
624
|
+
if (!responsePassed)
|
|
625
|
+
failures.push('smoke.response');
|
|
626
|
+
return {
|
|
627
|
+
status: failures.length ? 'failed' : 'passed',
|
|
628
|
+
marker, markerHash: sha256(marker), issueId, taskId, taskStatus, requiredSkills, loadedSkills,
|
|
629
|
+
toolTracePassed, responsePassed, responseHash: sha256(finalText), failures,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function rollbackConfirmed(root, plan, command, env, timeoutMs, calls, rollback, temp) {
|
|
633
|
+
const report = emptyRollback(true);
|
|
634
|
+
const attempt = (id, argv) => {
|
|
635
|
+
try {
|
|
636
|
+
runProvider(command, scopedArgs(plan, argv), id, 'rollback', root, env, timeoutMs, calls);
|
|
637
|
+
report.actions.push(id);
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
report.failures.push(id);
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
const assignmentAgentId = rollback.createdAgentId || rollback.updatedAgent?.id || '';
|
|
644
|
+
if (rollback.assignmentChanged && assignmentAgentId) {
|
|
645
|
+
attempt('rollback.agent.skills', [
|
|
646
|
+
'agent', 'skills', 'set', assignmentAgentId,
|
|
647
|
+
'--skill-ids', rollback.previousAssignedIds.join(','), '--output', 'json',
|
|
648
|
+
]);
|
|
649
|
+
}
|
|
650
|
+
if (rollback.createdAgentId) {
|
|
651
|
+
attempt('rollback.agent.archive', ['agent', 'archive', rollback.createdAgentId, '--output', 'json']);
|
|
652
|
+
}
|
|
653
|
+
else if (rollback.updatedAgent) {
|
|
654
|
+
const previous = rollback.updatedAgent;
|
|
655
|
+
const argv = [
|
|
656
|
+
'agent', 'update', previous.id, '--name', previous.name,
|
|
657
|
+
'--instructions', previous.instructions, '--runtime-id', previous.runtimeId,
|
|
658
|
+
];
|
|
659
|
+
if (previous.status)
|
|
660
|
+
argv.push('--status', previous.status);
|
|
661
|
+
argv.push('--output', 'json');
|
|
662
|
+
attempt('rollback.agent.update', argv);
|
|
663
|
+
if (previous.archived) {
|
|
664
|
+
attempt('rollback.agent.archive', ['agent', 'archive', previous.id, '--output', 'json']);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
for (const previous of [...rollback.updatedSkills].reverse()) {
|
|
668
|
+
const main = writeTemporaryContent(temp, `${previous.id}-SKILL.md`, previous.content);
|
|
669
|
+
attempt(`rollback.skill.update:${previous.name}`, [
|
|
670
|
+
'skill', 'update', previous.id, '--content-file', main, '--output', 'json',
|
|
671
|
+
]);
|
|
672
|
+
try {
|
|
673
|
+
const current = parseRemoteSkill(jsonObject(runProvider(command, scopedArgs(plan, ['skill', 'get', previous.id, '--output', 'json']), `rollback.skill.get:${previous.name}`, 'read', root, env, timeoutMs, calls).stdout, `rollback skill ${previous.name}`));
|
|
674
|
+
const previousPaths = new Set(previous.files.map((file) => file.path));
|
|
675
|
+
for (const extra of current.files.filter((file) => !previousPaths.has(file.path))) {
|
|
676
|
+
attempt(`rollback.skill.file.delete:${previous.name}:${extra.path}`, [
|
|
677
|
+
'skill', 'files', 'delete', previous.id, extra.id,
|
|
678
|
+
]);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
catch {
|
|
682
|
+
report.failures.push(`rollback.skill.verify:${previous.name}`);
|
|
683
|
+
}
|
|
684
|
+
for (const file of previous.files) {
|
|
685
|
+
const path = writeTemporaryContent(temp, `${previous.id}-${sha256(file.path).slice(0, 8)}`, file.content);
|
|
686
|
+
attempt(`rollback.skill.file:${previous.name}:${file.path}`, [
|
|
687
|
+
'skill', 'files', 'upsert', previous.id, '--path', file.path,
|
|
688
|
+
'--content-file', path, '--output', 'json',
|
|
689
|
+
]);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
for (const id of [...rollback.createdSkillIds].reverse()) {
|
|
693
|
+
attempt(`rollback.skill.delete:${id}`, ['skill', 'delete', id, '--yes']);
|
|
694
|
+
}
|
|
695
|
+
verifyRollback(root, plan, command, env, timeoutMs, calls, rollback, report);
|
|
696
|
+
report.passed = report.failures.length === 0;
|
|
697
|
+
return report;
|
|
698
|
+
}
|
|
699
|
+
function verifyRollback(root, plan, command, env, timeoutMs, calls, rollback, report) {
|
|
700
|
+
const verify = (id, action) => {
|
|
701
|
+
try {
|
|
702
|
+
if (!action())
|
|
703
|
+
report.failures.push(id);
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
report.failures.push(id);
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
if (rollback.createdSkillIds.length || rollback.updatedSkills.length) {
|
|
710
|
+
verify('rollback.verify.skills', () => {
|
|
711
|
+
const catalog = jsonArray(runProvider(command, scopedArgs(plan, ['skill', 'list', '--output', 'json']), 'rollback.verify.skill-list', 'read', root, env, timeoutMs, calls).stdout, 'rollback skill list');
|
|
712
|
+
const ids = new Set(catalog.map((item) => resourceId(item.id, 'rollback skill.id')));
|
|
713
|
+
return rollback.createdSkillIds.every((id) => !ids.has(id));
|
|
714
|
+
});
|
|
715
|
+
for (const previous of rollback.updatedSkills) {
|
|
716
|
+
verify(`rollback.verify.skill:${previous.name}`, () => {
|
|
717
|
+
const restored = parseRemoteSkill(jsonObject(runProvider(command, scopedArgs(plan, ['skill', 'get', previous.id, '--output', 'json']), `rollback.verify.skill-get:${previous.name}`, 'read', root, env, timeoutMs, calls).stdout, `rollback verify skill ${previous.name}`));
|
|
718
|
+
return restored.id === previous.id && restored.name === previous.name &&
|
|
719
|
+
restored.hash === previous.hash;
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const agentId = rollback.createdAgentId || rollback.updatedAgent?.id || '';
|
|
724
|
+
if (!agentId)
|
|
725
|
+
return;
|
|
726
|
+
verify('rollback.verify.agent', () => {
|
|
727
|
+
const restored = parseRemoteAgent(jsonObject(runProvider(command, scopedArgs(plan, ['agent', 'get', agentId, '--output', 'json']), 'rollback.verify.agent-get', 'read', root, env, timeoutMs, calls).stdout, 'rollback verify agent'));
|
|
728
|
+
if (rollback.createdAgentId)
|
|
729
|
+
return restored.archived;
|
|
730
|
+
const previous = rollback.updatedAgent;
|
|
731
|
+
return Boolean(previous && restored.id === previous.id && restored.name === previous.name &&
|
|
732
|
+
restored.runtimeId === previous.runtimeId && restored.instructions === previous.instructions &&
|
|
733
|
+
restored.archived === previous.archived);
|
|
734
|
+
});
|
|
735
|
+
if (rollback.assignmentChanged) {
|
|
736
|
+
verify('rollback.verify.assignment', () => {
|
|
737
|
+
const assigned = jsonArray(runProvider(command, scopedArgs(plan, [
|
|
738
|
+
'agent', 'skills', 'list', agentId, '--output', 'json',
|
|
739
|
+
]), 'rollback.verify.agent-skills', 'read', root, env, timeoutMs, calls).stdout, 'rollback verify assignments');
|
|
740
|
+
const ids = assigned.map((item) => resourceId(item.id, 'rollback assignment.id')).sort();
|
|
741
|
+
return stableStringify(ids) === stableStringify([...rollback.previousAssignedIds].sort());
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function failureReceipt(root, workspace, source, plan, preflight, operation, calls, rollback, status, failure) {
|
|
746
|
+
const receiptId = `multica_receipt_${digest(stableStringify({
|
|
747
|
+
operationId: operation.operationId, status, failure,
|
|
748
|
+
})).slice(0, 24)}`;
|
|
749
|
+
return {
|
|
750
|
+
$schema: MULTICA_DEPLOYMENT_SCHEMA,
|
|
751
|
+
receiptId,
|
|
752
|
+
operationId: operation.operationId,
|
|
753
|
+
planId: plan.planId,
|
|
754
|
+
action: plan.action,
|
|
755
|
+
workspace: workspace.name,
|
|
756
|
+
provider: 'multica',
|
|
757
|
+
profile: plan.profile,
|
|
758
|
+
status,
|
|
759
|
+
passed: false,
|
|
760
|
+
providerReady: false,
|
|
761
|
+
idempotent: false,
|
|
762
|
+
inspectionId: preflight.inspectionId,
|
|
763
|
+
inspectionObservedHash: preflight.observedHash,
|
|
764
|
+
inspectionEvidencePath: preflight.evidencePath,
|
|
765
|
+
desiredHash: workspace.desiredHash,
|
|
766
|
+
deploymentHash: source.deploymentHash,
|
|
767
|
+
observedHash: '',
|
|
768
|
+
agent: {
|
|
769
|
+
id: operation.agentId,
|
|
770
|
+
runtimeId: plan.expected.runtimeId,
|
|
771
|
+
instructionsHash: source.instructionsHash,
|
|
772
|
+
archived: false,
|
|
773
|
+
action: 'unknown',
|
|
774
|
+
},
|
|
775
|
+
skills: source.skills.map((item) => ({
|
|
776
|
+
id: '', name: item.name, sourceHash: item.hash, remoteHash: '', action: 'unknown',
|
|
777
|
+
})),
|
|
778
|
+
assignmentHash: '',
|
|
779
|
+
calls: [...calls],
|
|
780
|
+
smoke: emptySmoke('not-run', source.skills.map((item) => item.name)),
|
|
781
|
+
rollback,
|
|
782
|
+
failures: [failure, ...rollback.failures],
|
|
783
|
+
evidencePath: receiptRelativePath(workspace.name, receiptId),
|
|
784
|
+
remoteRead: true,
|
|
785
|
+
remoteWrite: calls.some((call) => call.kind === 'write' || call.kind === 'rollback'),
|
|
786
|
+
triggerWrite: false,
|
|
787
|
+
sideEffect: calls.some((call) => call.kind === 'write' || call.kind === 'rollback')
|
|
788
|
+
? 'multica-read-write' : 'multica-read',
|
|
789
|
+
dingtalkSideEffect: false,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
export function statusMulticaDeployment(start, name, options = {}) {
|
|
793
|
+
const info = inspectAgentProject(start, options.cliVersion || '');
|
|
794
|
+
requireMulticaWorkspace(info.root, name, options.cliVersion || '');
|
|
795
|
+
const loaded = loadOperationAndReceipt(info.root, name, options.operationId);
|
|
796
|
+
if (!loaded)
|
|
797
|
+
return {
|
|
798
|
+
$schema: 'dingtalk-agent/multica-deployment-status@1',
|
|
799
|
+
workspace: name, provider: 'multica', available: false, operationId: '', receiptId: '',
|
|
800
|
+
status: 'missing', passed: false, providerReady: false,
|
|
801
|
+
operationIntegrity: false, receiptIntegrity: false, reconciled: false,
|
|
802
|
+
failures: ['deployment.missing'], receipt: null,
|
|
803
|
+
remoteRead: false, remoteWrite: false, triggerWrite: false,
|
|
804
|
+
sideEffect: false, dingtalkSideEffect: false,
|
|
805
|
+
};
|
|
806
|
+
let { operation, receipt } = loaded;
|
|
807
|
+
let reconciled = false;
|
|
808
|
+
let remoteRead = false;
|
|
809
|
+
if (options.execute) {
|
|
810
|
+
if (!options.yes)
|
|
811
|
+
throw new Error('deploy --status 远端 reconcile 必须同时传 --execute --yes');
|
|
812
|
+
const command = options.command || options.env?.DTA_MULTICA_BIN || process.env.DTA_MULTICA_BIN || 'multica';
|
|
813
|
+
const env = options.env || process.env;
|
|
814
|
+
const timeoutMs = positiveTimeout(options.timeoutMs, 30_000, 300_000);
|
|
815
|
+
if (receipt.status === 'verifying' && receipt.smoke.issueId) {
|
|
816
|
+
remoteRead = true;
|
|
817
|
+
const smokeReadback = readSmokeFromReceipt(info.root, receipt, command, env, timeoutMs);
|
|
818
|
+
const smoke = smokeReadback.smoke;
|
|
819
|
+
receipt = { ...receipt, calls: smokeReadback.calls, smoke, status: smoke.status === 'passed' ? 'ready'
|
|
820
|
+
: smoke.status === 'pending' ? 'verifying' : 'failed',
|
|
821
|
+
passed: smoke.status === 'passed', providerReady: smoke.status === 'passed',
|
|
822
|
+
failures: [...smoke.failures] };
|
|
823
|
+
reconciled = true;
|
|
824
|
+
persistReceiptAndState(info.root, showDevelopmentWorkspace(info.root, name), operation, receipt);
|
|
825
|
+
}
|
|
826
|
+
else if (receipt.status === 'reconciling') {
|
|
827
|
+
remoteRead = true;
|
|
828
|
+
const source = loadDeploymentSource(info.root);
|
|
829
|
+
const calls = [...receipt.calls];
|
|
830
|
+
const reconciledTarget = reconcileRemoteTarget(info.root, name, operation, source, command, env, timeoutMs, calls);
|
|
831
|
+
const inspection = reconciledTarget.inspection;
|
|
832
|
+
const snapshot = reconciledTarget.snapshot;
|
|
833
|
+
const matched = Boolean(snapshot && remoteMatchesDesired(snapshot, source, operation.runtimeId));
|
|
834
|
+
const safelyBound = Boolean(snapshot?.agent && inspection.passed &&
|
|
835
|
+
snapshot.agent.id === operation.agentId &&
|
|
836
|
+
snapshot.agent.name === info.project.name &&
|
|
837
|
+
snapshot.agent.runtimeId === operation.runtimeId &&
|
|
838
|
+
snapshot.agent.instructions === source.instructions && !snapshot.agent.archived);
|
|
839
|
+
receipt = { ...receipt,
|
|
840
|
+
calls,
|
|
841
|
+
inspectionId: inspection.inspectionId,
|
|
842
|
+
inspectionObservedHash: inspection.observedHash,
|
|
843
|
+
inspectionEvidencePath: inspection.evidencePath,
|
|
844
|
+
agent: { ...receipt.agent, id: operation.agentId },
|
|
845
|
+
status: matched || safelyBound ? 'verifying' : 'failed',
|
|
846
|
+
failures: matched ? ['smoke.required-after-recovery']
|
|
847
|
+
: safelyBound ? ['apply.required-after-recovery']
|
|
848
|
+
: reconciledTarget.failures.length ? reconciledTarget.failures
|
|
849
|
+
: inspection.passed ? ['reconcile.remote-mismatch'] : [...inspection.failures],
|
|
850
|
+
};
|
|
851
|
+
reconciled = true;
|
|
852
|
+
persistReceiptAndState(info.root, showDevelopmentWorkspace(info.root, name), operation, receipt);
|
|
853
|
+
}
|
|
854
|
+
operation = loadOperationAndReceipt(info.root, name, operation.operationId)?.operation || operation;
|
|
855
|
+
}
|
|
856
|
+
return {
|
|
857
|
+
$schema: 'dingtalk-agent/multica-deployment-status@1',
|
|
858
|
+
workspace: name,
|
|
859
|
+
provider: 'multica',
|
|
860
|
+
available: true,
|
|
861
|
+
operationId: operation.operationId,
|
|
862
|
+
receiptId: receipt.receiptId,
|
|
863
|
+
status: receipt.status,
|
|
864
|
+
passed: receipt.passed,
|
|
865
|
+
providerReady: receipt.providerReady,
|
|
866
|
+
operationIntegrity: true,
|
|
867
|
+
receiptIntegrity: true,
|
|
868
|
+
reconciled,
|
|
869
|
+
failures: [...receipt.failures],
|
|
870
|
+
receipt,
|
|
871
|
+
remoteRead,
|
|
872
|
+
remoteWrite: false,
|
|
873
|
+
triggerWrite: false,
|
|
874
|
+
sideEffect: remoteRead ? 'multica-read' : false,
|
|
875
|
+
dingtalkSideEffect: false,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
export function listMulticaDeployments(start, name, cliVersion = '') {
|
|
879
|
+
const info = inspectAgentProject(start, cliVersion);
|
|
880
|
+
requireMulticaWorkspace(info.root, name, cliVersion);
|
|
881
|
+
const directory = safeProjectPath(info.root, receiptsRoot(name), false);
|
|
882
|
+
const deployments = [];
|
|
883
|
+
if (existsSync(directory)) {
|
|
884
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })
|
|
885
|
+
.filter((item) => item.isFile() && item.name.endsWith('.json'))
|
|
886
|
+
.sort((a, b) => b.name.localeCompare(a.name))) {
|
|
887
|
+
const path = join(directory, entry.name);
|
|
888
|
+
if (lstatSync(path).isSymbolicLink())
|
|
889
|
+
throw new Error('Multica Receipt 不能是符号链接');
|
|
890
|
+
const candidate = parseReceipt(readJson(path, 'Multica Receipt'));
|
|
891
|
+
const loaded = loadOperationAndReceipt(info.root, name, candidate.operationId);
|
|
892
|
+
if (!loaded || loaded.receipt.receiptId !== candidate.receiptId ||
|
|
893
|
+
safeProjectPath(info.root, loaded.receipt.evidencePath, true) !== path) {
|
|
894
|
+
throw new Error('Multica deployment 历史 Receipt 绑定不一致');
|
|
895
|
+
}
|
|
896
|
+
const receipt = loaded.receipt;
|
|
897
|
+
deployments.push({
|
|
898
|
+
receiptId: receipt.receiptId,
|
|
899
|
+
operationId: receipt.operationId,
|
|
900
|
+
action: receipt.action,
|
|
901
|
+
status: receipt.status,
|
|
902
|
+
passed: receipt.passed,
|
|
903
|
+
providerReady: receipt.providerReady,
|
|
904
|
+
planId: receipt.planId,
|
|
905
|
+
deploymentHash: receipt.deploymentHash,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return {
|
|
910
|
+
$schema: 'dingtalk-agent/multica-deployment-list@1',
|
|
911
|
+
workspace: name, provider: 'multica', deployments,
|
|
912
|
+
remoteRead: false, remoteWrite: false, triggerWrite: false,
|
|
913
|
+
sideEffect: false, dingtalkSideEffect: false,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
function reconcileRemoteTarget(root, name, operation, source, command, env, timeoutMs, calls) {
|
|
917
|
+
if (operation.agentId) {
|
|
918
|
+
const inspection = inspectMulticaWorkspace(root, name, {
|
|
919
|
+
execute: true, yes: true, persist: true, command, env, timeoutMs,
|
|
920
|
+
agentIdOverride: operation.agentId,
|
|
921
|
+
});
|
|
922
|
+
appendInspectionCalls(inspection, 'reconcile.scope', calls);
|
|
923
|
+
const snapshot = inspection.passed ? readRemoteSnapshot(root, source, inspection, command, env, timeoutMs, calls) : null;
|
|
924
|
+
return { inspection, snapshot, failures: [] };
|
|
925
|
+
}
|
|
926
|
+
const discovery = inspectMulticaWorkspace(root, name, {
|
|
927
|
+
execute: true, yes: true, persist: false, command, env, timeoutMs,
|
|
928
|
+
});
|
|
929
|
+
appendInspectionCalls(discovery, 'reconcile.discovery', calls);
|
|
930
|
+
const candidates = discovery.resources.agentCandidates;
|
|
931
|
+
if (!candidates.length) {
|
|
932
|
+
return { inspection: discovery, snapshot: null, failures: ['reconcile.agent-not-found'] };
|
|
933
|
+
}
|
|
934
|
+
if (candidates.length > 10) {
|
|
935
|
+
return { inspection: discovery, snapshot: null, failures: ['reconcile.agent-candidate-budget'] };
|
|
936
|
+
}
|
|
937
|
+
const projectName = inspectAgentProject(root).project.name;
|
|
938
|
+
const matches = [];
|
|
939
|
+
for (const [index, candidate] of candidates.entries()) {
|
|
940
|
+
const inspection = inspectMulticaWorkspace(root, name, {
|
|
941
|
+
execute: true, yes: true, persist: false, command, env, timeoutMs,
|
|
942
|
+
agentIdOverride: candidate.id,
|
|
943
|
+
});
|
|
944
|
+
appendInspectionCalls(inspection, `reconcile.candidate.${index + 1}`, calls);
|
|
945
|
+
if (!inspection.passed)
|
|
946
|
+
continue;
|
|
947
|
+
const snapshot = readRemoteSnapshot(root, source, inspection, command, env, timeoutMs, calls);
|
|
948
|
+
if (snapshot.agent && snapshot.agent.id === candidate.id &&
|
|
949
|
+
snapshot.agent.name === projectName && snapshot.agent.runtimeId === operation.runtimeId &&
|
|
950
|
+
snapshot.agent.instructions === source.instructions && !snapshot.agent.archived) {
|
|
951
|
+
matches.push({ id: candidate.id });
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
if (matches.length !== 1) {
|
|
955
|
+
return {
|
|
956
|
+
inspection: discovery,
|
|
957
|
+
snapshot: null,
|
|
958
|
+
failures: [matches.length ? 'reconcile.agent-ambiguous' : 'reconcile.agent-fingerprint-mismatch'],
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
operation.agentId = matches[0].id;
|
|
962
|
+
const inspection = inspectMulticaWorkspace(root, name, {
|
|
963
|
+
execute: true, yes: true, persist: true, command, env, timeoutMs,
|
|
964
|
+
agentIdOverride: operation.agentId,
|
|
965
|
+
});
|
|
966
|
+
appendInspectionCalls(inspection, 'reconcile.bound', calls);
|
|
967
|
+
const snapshot = inspection.passed ? readRemoteSnapshot(root, source, inspection, command, env, timeoutMs, calls) : null;
|
|
968
|
+
return { inspection, snapshot, failures: inspection.passed ? [] : [...inspection.failures] };
|
|
969
|
+
}
|
|
970
|
+
function appendInspectionCalls(inspection, prefix, calls) {
|
|
971
|
+
calls.push(...inspection.calls.map((call) => ({
|
|
972
|
+
id: `${prefix}.${call.id}`,
|
|
973
|
+
kind: 'read',
|
|
974
|
+
argv: [...call.argv],
|
|
975
|
+
exitCode: call.exitCode,
|
|
976
|
+
stdoutHash: call.stdoutHash,
|
|
977
|
+
stderrHash: call.stderrHash,
|
|
978
|
+
passed: call.passed,
|
|
979
|
+
ambiguous: false,
|
|
980
|
+
})));
|
|
981
|
+
}
|
|
982
|
+
function readSmokeFromReceipt(root, receipt, command, env, timeoutMs) {
|
|
983
|
+
const plan = {
|
|
984
|
+
profile: receipt.profile,
|
|
985
|
+
expected: { workspaceId: '', runtimeId: receipt.agent.runtimeId },
|
|
986
|
+
};
|
|
987
|
+
const operation = loadOperationAndReceipt(root, receipt.workspace, receipt.operationId)?.operation;
|
|
988
|
+
if (!operation)
|
|
989
|
+
throw new Error('Multica deployment operation 缺失');
|
|
990
|
+
plan.expected.workspaceId = operation.workspaceId;
|
|
991
|
+
const calls = [...receipt.calls];
|
|
992
|
+
const markerValue = receipt.smoke.marker;
|
|
993
|
+
if (!markerValue || sha256(markerValue) !== receipt.smoke.markerHash) {
|
|
994
|
+
return { calls, smoke: {
|
|
995
|
+
...receipt.smoke,
|
|
996
|
+
status: 'failed',
|
|
997
|
+
failures: ['smoke.marker-not-recoverable'],
|
|
998
|
+
} };
|
|
999
|
+
}
|
|
1000
|
+
const smoke = readSmoke(root, plan, markerValue, receipt.smoke.issueId, receipt.agent.id, receipt.smoke.requiredSkills, command, env, timeoutMs, calls);
|
|
1001
|
+
return { smoke, calls };
|
|
1002
|
+
}
|
|
1003
|
+
function persistReceiptAndState(root, workspace, operation, receipt) {
|
|
1004
|
+
assertReportRedacted(receipt);
|
|
1005
|
+
const receiptPath = safeProjectPath(root, receipt.evidencePath, false);
|
|
1006
|
+
atomicJson(receiptPath, receipt);
|
|
1007
|
+
const receiptHash = sha256(readFileSync(receiptPath));
|
|
1008
|
+
operation.status = receipt.status;
|
|
1009
|
+
operation.agentId = receipt.agent.id || operation.agentId;
|
|
1010
|
+
operation.receiptId = receipt.receiptId;
|
|
1011
|
+
operation.receiptPath = receipt.evidencePath;
|
|
1012
|
+
operation.calls = receipt.calls;
|
|
1013
|
+
operation.failures = receipt.failures;
|
|
1014
|
+
operation.updatedAt = new Date().toISOString();
|
|
1015
|
+
persistOperation(root, operation);
|
|
1016
|
+
const operationPath = operationRelativePath(workspace.name, operation.operationId);
|
|
1017
|
+
const operationHash = sha256(readFileSync(safeProjectPath(root, operationPath, true)));
|
|
1018
|
+
const binding = {
|
|
1019
|
+
$schema: 'dingtalk-agent/multica-deployment-pointer@1',
|
|
1020
|
+
operationId: operation.operationId,
|
|
1021
|
+
operationPath,
|
|
1022
|
+
operationHash,
|
|
1023
|
+
receiptId: receipt.receiptId,
|
|
1024
|
+
receiptPath: receipt.evidencePath,
|
|
1025
|
+
receiptHash,
|
|
1026
|
+
};
|
|
1027
|
+
atomicJson(safeProjectPath(root, operationBindingPath(workspace.name, operation.operationId), false), binding);
|
|
1028
|
+
atomicJson(safeProjectPath(root, latestDeploymentPointer(workspace.name), false), binding);
|
|
1029
|
+
const stateStatus = receipt.status === 'ready' ? 'ready'
|
|
1030
|
+
: receipt.status === 'retired' ? 'retired'
|
|
1031
|
+
: receipt.status === 'failed' ? 'failed' : 'verifying';
|
|
1032
|
+
const state = {
|
|
1033
|
+
$schema: 'dingtalk-agent/workspace-state@1',
|
|
1034
|
+
workspace: workspace.name,
|
|
1035
|
+
provider: 'multica',
|
|
1036
|
+
status: stateStatus,
|
|
1037
|
+
binding: {
|
|
1038
|
+
...(workspace.state?.binding || {}),
|
|
1039
|
+
profile: receipt.profile,
|
|
1040
|
+
remoteWorkspaceId: operation.workspaceId,
|
|
1041
|
+
runtimeId: operation.runtimeId,
|
|
1042
|
+
agentId: receipt.agent.id || operation.agentId,
|
|
1043
|
+
deploymentHash: receipt.deploymentHash,
|
|
1044
|
+
inspectionId: receipt.inspectionId,
|
|
1045
|
+
inspectionObservedHash: receipt.inspectionObservedHash,
|
|
1046
|
+
inspectionEvidencePath: receipt.inspectionEvidencePath,
|
|
1047
|
+
receiptPath: receipt.evidencePath,
|
|
1048
|
+
operationId: operation.operationId,
|
|
1049
|
+
sourceHash: workspace.sourceHash,
|
|
1050
|
+
},
|
|
1051
|
+
desiredHash: workspace.desiredHash,
|
|
1052
|
+
observedHash: receipt.inspectionObservedHash || receipt.observedHash,
|
|
1053
|
+
lastPlanId: receipt.planId,
|
|
1054
|
+
lastReceiptId: receipt.receiptId,
|
|
1055
|
+
lastEvalId: receipt.smoke.taskId,
|
|
1056
|
+
};
|
|
1057
|
+
atomicJson(safeProjectPath(root, join(WORKSPACE_STATE_ROOT, `${workspace.name}.json`), false), state);
|
|
1058
|
+
}
|
|
1059
|
+
function createOperation(operationId, plan) {
|
|
1060
|
+
return {
|
|
1061
|
+
$schema: 'dingtalk-agent/multica-deployment-operation@1',
|
|
1062
|
+
operationId,
|
|
1063
|
+
planId: plan.planId,
|
|
1064
|
+
workspace: plan.workspace,
|
|
1065
|
+
action: plan.action,
|
|
1066
|
+
status: 'applying',
|
|
1067
|
+
profile: plan.profile,
|
|
1068
|
+
workspaceId: plan.expected.workspaceId,
|
|
1069
|
+
runtimeId: plan.expected.runtimeId,
|
|
1070
|
+
agentId: plan.expected.agentId,
|
|
1071
|
+
deploymentHash: plan.deploymentHash,
|
|
1072
|
+
ambiguousAction: '',
|
|
1073
|
+
receiptId: '',
|
|
1074
|
+
receiptPath: '',
|
|
1075
|
+
calls: [],
|
|
1076
|
+
failures: [],
|
|
1077
|
+
updatedAt: new Date().toISOString(),
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
function persistOperation(root, operation) {
|
|
1081
|
+
assertReportRedacted(operation);
|
|
1082
|
+
atomicJson(safeProjectPath(root, operationRelativePath(operation.workspace, operation.operationId), false), operation);
|
|
1083
|
+
}
|
|
1084
|
+
function loadOperationAndReceipt(root, name, operationId = '') {
|
|
1085
|
+
let operationPath = '';
|
|
1086
|
+
let expectedOperationHash = '';
|
|
1087
|
+
let receiptPath = '';
|
|
1088
|
+
let expectedReceiptHash = '';
|
|
1089
|
+
let expectedOperationId = '';
|
|
1090
|
+
let expectedReceiptId = '';
|
|
1091
|
+
const requestedOperationId = operationId ? resourceId(operationId, 'operation-id') : '';
|
|
1092
|
+
const pointerPath = safeProjectPath(root, requestedOperationId
|
|
1093
|
+
? operationBindingPath(name, requestedOperationId)
|
|
1094
|
+
: latestDeploymentPointer(name), false);
|
|
1095
|
+
if (!existsSync(pointerPath)) {
|
|
1096
|
+
if (requestedOperationId)
|
|
1097
|
+
throw new Error('Multica deployment operation binding 不存在');
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1100
|
+
const pointer = readJson(pointerPath, 'Multica deployment pointer');
|
|
1101
|
+
if (pointer.$schema !== 'dingtalk-agent/multica-deployment-pointer@1') {
|
|
1102
|
+
throw new Error('Multica deployment pointer schema 非法');
|
|
1103
|
+
}
|
|
1104
|
+
expectedOperationId = resourceId(pointer.operationId, 'pointer.operationId');
|
|
1105
|
+
expectedReceiptId = resourceId(pointer.receiptId, 'pointer.receiptId');
|
|
1106
|
+
if (requestedOperationId && expectedOperationId !== requestedOperationId) {
|
|
1107
|
+
throw new Error('Multica deployment operation binding ID 不一致');
|
|
1108
|
+
}
|
|
1109
|
+
operationPath = stringField(pointer.operationPath, 'pointer.operationPath');
|
|
1110
|
+
expectedOperationHash = stringField(pointer.operationHash, 'pointer.operationHash');
|
|
1111
|
+
receiptPath = stringField(pointer.receiptPath, 'pointer.receiptPath');
|
|
1112
|
+
expectedReceiptHash = stringField(pointer.receiptHash, 'pointer.receiptHash');
|
|
1113
|
+
const operationFile = safeProjectPath(root, operationPath, true);
|
|
1114
|
+
const operationBytes = readFileSync(operationFile);
|
|
1115
|
+
if (expectedOperationHash && sha256(operationBytes) !== expectedOperationHash) {
|
|
1116
|
+
throw new Error('Multica deployment operation hash 不一致');
|
|
1117
|
+
}
|
|
1118
|
+
const operation = parseOperation(JSON.parse(operationBytes.toString('utf8')));
|
|
1119
|
+
if (operation.workspace !== name || operation.operationId !== expectedOperationId) {
|
|
1120
|
+
throw new Error('Multica deployment operation 绑定不一致');
|
|
1121
|
+
}
|
|
1122
|
+
if (!receiptPath)
|
|
1123
|
+
receiptPath = operation.receiptPath;
|
|
1124
|
+
if (!receiptPath)
|
|
1125
|
+
throw new Error('Multica deployment operation 尚无 Receipt');
|
|
1126
|
+
const receiptFile = safeProjectPath(root, receiptPath, true);
|
|
1127
|
+
const receiptBytes = readFileSync(receiptFile);
|
|
1128
|
+
if (expectedReceiptHash && sha256(receiptBytes) !== expectedReceiptHash) {
|
|
1129
|
+
throw new Error('Multica deployment Receipt hash 不一致');
|
|
1130
|
+
}
|
|
1131
|
+
const receipt = parseReceipt(JSON.parse(receiptBytes.toString('utf8')));
|
|
1132
|
+
if (receipt.operationId !== operation.operationId || receipt.workspace !== name ||
|
|
1133
|
+
receipt.receiptId !== expectedReceiptId || receipt.evidencePath !== receiptPath ||
|
|
1134
|
+
operation.receiptId !== receipt.receiptId || operation.receiptPath !== receiptPath ||
|
|
1135
|
+
operation.planId !== receipt.planId || operation.action !== receipt.action ||
|
|
1136
|
+
operation.profile !== receipt.profile || operation.deploymentHash !== receipt.deploymentHash) {
|
|
1137
|
+
throw new Error('Multica deployment Receipt 绑定不一致');
|
|
1138
|
+
}
|
|
1139
|
+
assertReportRedacted({ operation, receipt });
|
|
1140
|
+
return { operation, receipt };
|
|
1141
|
+
}
|
|
1142
|
+
function loadLatestReceipt(root, name) {
|
|
1143
|
+
// A missing pointer means this is the first deployment. Once a pointer exists,
|
|
1144
|
+
// any integrity or binding failure is evidence corruption and must fail closed.
|
|
1145
|
+
return loadOperationAndReceipt(root, name)?.receipt || null;
|
|
1146
|
+
}
|
|
1147
|
+
function parseOperation(value) {
|
|
1148
|
+
const item = objectValue(value, 'Multica deployment operation');
|
|
1149
|
+
if (item.$schema !== 'dingtalk-agent/multica-deployment-operation@1') {
|
|
1150
|
+
throw new Error('Multica deployment operation schema 非法');
|
|
1151
|
+
}
|
|
1152
|
+
return item;
|
|
1153
|
+
}
|
|
1154
|
+
function parseReceipt(value) {
|
|
1155
|
+
const item = objectValue(value, 'Multica deployment Receipt');
|
|
1156
|
+
if (item.$schema !== MULTICA_DEPLOYMENT_SCHEMA) {
|
|
1157
|
+
throw new Error('Multica deployment Receipt schema 非法');
|
|
1158
|
+
}
|
|
1159
|
+
return item;
|
|
1160
|
+
}
|
|
1161
|
+
function runProvider(command, argv, id, kind, cwd, env, timeoutMs, calls) {
|
|
1162
|
+
const budget = deploymentWriteBudgets.get(calls);
|
|
1163
|
+
if (kind === 'write' && budget && calls.filter((call) => call.kind === 'write').length >= budget.forward) {
|
|
1164
|
+
throw new DeploymentFailure('budget.forward-exhausted');
|
|
1165
|
+
}
|
|
1166
|
+
if (kind === 'rollback' && budget && calls.filter((call) => call.kind === 'rollback').length >= budget.rollback) {
|
|
1167
|
+
throw new DeploymentFailure('budget.rollback-exhausted');
|
|
1168
|
+
}
|
|
1169
|
+
const run = spawnSync(command, argv, {
|
|
1170
|
+
cwd, env, encoding: 'utf8', timeout: timeoutMs,
|
|
1171
|
+
maxBuffer: 32 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'],
|
|
1172
|
+
});
|
|
1173
|
+
const stdout = String(run.stdout || '');
|
|
1174
|
+
const stderr = String(run.stderr || '');
|
|
1175
|
+
const exitCode = typeof run.status === 'number' ? run.status : 1;
|
|
1176
|
+
const ambiguous = Boolean(run.error && run.error.code === 'ETIMEDOUT');
|
|
1177
|
+
const passed = !run.error && exitCode === 0;
|
|
1178
|
+
calls.push({
|
|
1179
|
+
id, kind, argv: redactArgv(argv), exitCode,
|
|
1180
|
+
stdoutHash: sha256(stdout), stderrHash: sha256(stderr), passed, ambiguous,
|
|
1181
|
+
});
|
|
1182
|
+
if (!passed)
|
|
1183
|
+
throw new DeploymentFailure(`command.${id}`, ambiguous);
|
|
1184
|
+
return { stdout, stderr };
|
|
1185
|
+
}
|
|
1186
|
+
function scopedArgs(plan, args) {
|
|
1187
|
+
return ['--profile', plan.profile, '--workspace-id', plan.expected.workspaceId, ...args];
|
|
1188
|
+
}
|
|
1189
|
+
function redactArgv(argv) {
|
|
1190
|
+
const redactNext = new Set([
|
|
1191
|
+
'--instructions', '--description', '--title', '--content', '--content-file',
|
|
1192
|
+
'--custom-env', '--mcp-config',
|
|
1193
|
+
]);
|
|
1194
|
+
const result = [];
|
|
1195
|
+
let redact = false;
|
|
1196
|
+
for (const arg of argv) {
|
|
1197
|
+
if (redact) {
|
|
1198
|
+
result.push(`[redacted:${sha256(arg).slice(0, 12)}]`);
|
|
1199
|
+
redact = false;
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
result.push(arg);
|
|
1203
|
+
if (redactNext.has(arg))
|
|
1204
|
+
redact = true;
|
|
1205
|
+
}
|
|
1206
|
+
return result;
|
|
1207
|
+
}
|
|
1208
|
+
function parseRemoteSkill(value) {
|
|
1209
|
+
const id = resourceId(value.id, 'remote skill.id');
|
|
1210
|
+
const name = stringField(value.name, 'remote skill.name');
|
|
1211
|
+
const content = optionalString(value.content);
|
|
1212
|
+
const rawFiles = Array.isArray(value.files) ? value.files : [];
|
|
1213
|
+
if (rawFiles.length > MAX_SKILL_SUPPORTING_FILES) {
|
|
1214
|
+
throw new Error(`远端 Skill supporting files 超过 ${MAX_SKILL_SUPPORTING_FILES} 个`);
|
|
1215
|
+
}
|
|
1216
|
+
const files = rawFiles.map((raw, index) => {
|
|
1217
|
+
const item = objectValue(raw, `remote skill.files[${index}]`);
|
|
1218
|
+
const fileContent = optionalString(item.content);
|
|
1219
|
+
const path = safeSkillPath(stringField(item.path, `remote skill.files[${index}].path`));
|
|
1220
|
+
return {
|
|
1221
|
+
id: resourceId(item.id, `remote skill.files[${index}].id`),
|
|
1222
|
+
path, content: fileContent, hash: sha256(fileContent),
|
|
1223
|
+
};
|
|
1224
|
+
}).sort((a, b) => a.path.localeCompare(b.path));
|
|
1225
|
+
const hash = skillTreeHash(content, files.map((item) => ({ path: item.path, hash: item.hash })));
|
|
1226
|
+
return { id, name, content, files, hash };
|
|
1227
|
+
}
|
|
1228
|
+
function parseRemoteAgent(value) {
|
|
1229
|
+
return {
|
|
1230
|
+
id: resourceId(value.id, 'remote agent.id'),
|
|
1231
|
+
name: stringField(value.name, 'remote agent.name'),
|
|
1232
|
+
runtimeId: resourceId(value.runtime_id, 'remote agent.runtime_id'),
|
|
1233
|
+
instructions: optionalString(value.instructions),
|
|
1234
|
+
status: optionalString(value.status),
|
|
1235
|
+
archived: Boolean(optionalString(value.archived_at)) || value.archived === true,
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
function remoteMatchesDesired(snapshot, source, runtimeId) {
|
|
1239
|
+
if (!snapshot.agent || snapshot.agent.archived || snapshot.agent.runtimeId !== runtimeId ||
|
|
1240
|
+
snapshot.agent.instructions !== source.instructions)
|
|
1241
|
+
return false;
|
|
1242
|
+
const ids = [];
|
|
1243
|
+
for (const local of source.skills) {
|
|
1244
|
+
const remote = snapshot.skills.get(local.name);
|
|
1245
|
+
if (!remote || remote.hash !== local.hash)
|
|
1246
|
+
return false;
|
|
1247
|
+
ids.push(remote.id);
|
|
1248
|
+
}
|
|
1249
|
+
return stableStringify(ids.sort()) === stableStringify([...snapshot.assignedIds].sort());
|
|
1250
|
+
}
|
|
1251
|
+
function deploymentObservedHash(snapshot, inspectionId) {
|
|
1252
|
+
return digest(stableStringify({
|
|
1253
|
+
inspectionId,
|
|
1254
|
+
agent: snapshot.agent ? {
|
|
1255
|
+
id: snapshot.agent.id,
|
|
1256
|
+
runtimeId: snapshot.agent.runtimeId,
|
|
1257
|
+
instructionsHash: sha256(snapshot.agent.instructions),
|
|
1258
|
+
archived: snapshot.agent.archived,
|
|
1259
|
+
} : null,
|
|
1260
|
+
skills: [...snapshot.skills.entries()].map(([name, skill]) => ({
|
|
1261
|
+
name, id: skill?.id || '', hash: skill?.hash || '',
|
|
1262
|
+
})).sort((a, b) => a.name.localeCompare(b.name)),
|
|
1263
|
+
assignments: [...snapshot.assignedIds].sort(),
|
|
1264
|
+
}));
|
|
1265
|
+
}
|
|
1266
|
+
function skillTreeHash(content, files) {
|
|
1267
|
+
return digest(stableStringify({
|
|
1268
|
+
main: sha256(content),
|
|
1269
|
+
files: [...files].sort((a, b) => a.path.localeCompare(b.path)),
|
|
1270
|
+
}));
|
|
1271
|
+
}
|
|
1272
|
+
function frontmatterName(content) {
|
|
1273
|
+
const block = content.match(/^---\s*\n([\s\S]*?)\n---(?:\s*\n|$)/);
|
|
1274
|
+
if (!block)
|
|
1275
|
+
return '';
|
|
1276
|
+
const match = block[1].match(/^name:\s*["']?([^"'\n]+?)["']?\s*$/m);
|
|
1277
|
+
return match ? match[1].trim() : '';
|
|
1278
|
+
}
|
|
1279
|
+
function rejectCredentialMaterial(content, label) {
|
|
1280
|
+
const patterns = [
|
|
1281
|
+
/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
1282
|
+
/\bBearer\s+[A-Za-z0-9._~+\/-]{20,}/i,
|
|
1283
|
+
/\b(?:sk|pat|token)[-_][A-Za-z0-9_-]{24,}\b/i,
|
|
1284
|
+
/(?:access[_-]?token|client[_-]?secret|password)\s*[:=]\s*["']?(?!<|\*|replace|example|redacted)[A-Za-z0-9._~+\/-]{16,}/i,
|
|
1285
|
+
];
|
|
1286
|
+
if (patterns.some((pattern) => pattern.test(content))) {
|
|
1287
|
+
throw new Error(`${label} 检测到疑似真实凭据,拒绝部署`);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function assertReportRedacted(value) {
|
|
1291
|
+
const source = JSON.stringify(value);
|
|
1292
|
+
if (/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----|\bBearer\s+[A-Za-z0-9._~+\/-]{20,}|\b(?:sk|pat|token)[-_][A-Za-z0-9_-]{24,}\b/i.test(source)) {
|
|
1293
|
+
throw new Error('Multica deployment evidence 脱敏失败');
|
|
1294
|
+
}
|
|
1295
|
+
if (/"(?:instructions|content|stdout|stderr|email|serverUrl|server_url)"\s*:/i.test(source)) {
|
|
1296
|
+
throw new Error('Multica deployment evidence 包含禁止的原始内容字段');
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
function emptySmoke(status, requiredSkills) {
|
|
1300
|
+
return {
|
|
1301
|
+
status, marker: '', markerHash: '', issueId: '', taskId: '', taskStatus: '',
|
|
1302
|
+
requiredSkills: [...requiredSkills].sort(), loadedSkills: [],
|
|
1303
|
+
toolTracePassed: false, responsePassed: false, responseHash: '', failures: [],
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
function emptyRollback(attempted) {
|
|
1307
|
+
return { attempted, passed: !attempted, actions: [], failures: [] };
|
|
1308
|
+
}
|
|
1309
|
+
function writeTemporaryContent(root, name, content) {
|
|
1310
|
+
const path = join(root, `${safeFileName(name)}.txt`);
|
|
1311
|
+
writeFileSync(path, content, { mode: 0o600 });
|
|
1312
|
+
return path;
|
|
1313
|
+
}
|
|
1314
|
+
function safeFileName(value) {
|
|
1315
|
+
return value.replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 120);
|
|
1316
|
+
}
|
|
1317
|
+
function readUtf8(path, label, limit) {
|
|
1318
|
+
const bytes = readFileSync(path);
|
|
1319
|
+
if (bytes.length > limit)
|
|
1320
|
+
throw new Error(`${label} 超过 ${limit} bytes`);
|
|
1321
|
+
if (bytes.includes(0))
|
|
1322
|
+
throw new Error(`${label} 不是 UTF-8 文本`);
|
|
1323
|
+
const content = bytes.toString('utf8');
|
|
1324
|
+
if (Buffer.from(content, 'utf8').compare(bytes) !== 0)
|
|
1325
|
+
throw new Error(`${label} 不是合法 UTF-8`);
|
|
1326
|
+
return content;
|
|
1327
|
+
}
|
|
1328
|
+
function readJson(path, label) {
|
|
1329
|
+
if (lstatSync(path).isSymbolicLink())
|
|
1330
|
+
throw new Error(`${label} 不能是符号链接`);
|
|
1331
|
+
try {
|
|
1332
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
1333
|
+
}
|
|
1334
|
+
catch {
|
|
1335
|
+
throw new Error(`${label} 不是合法 JSON`);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
function jsonObject(value, label) {
|
|
1339
|
+
let parsed = value;
|
|
1340
|
+
if (typeof value === 'string') {
|
|
1341
|
+
try {
|
|
1342
|
+
parsed = JSON.parse(value);
|
|
1343
|
+
}
|
|
1344
|
+
catch {
|
|
1345
|
+
throw new DeploymentFailure(`json.${label}`);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
return objectValue(parsed, label);
|
|
1349
|
+
}
|
|
1350
|
+
function jsonArray(value, label) {
|
|
1351
|
+
let parsed;
|
|
1352
|
+
try {
|
|
1353
|
+
parsed = JSON.parse(value);
|
|
1354
|
+
}
|
|
1355
|
+
catch {
|
|
1356
|
+
throw new DeploymentFailure(`json.${label}`);
|
|
1357
|
+
}
|
|
1358
|
+
if (!Array.isArray(parsed) || parsed.some((item) => !item || typeof item !== 'object' || Array.isArray(item))) {
|
|
1359
|
+
throw new DeploymentFailure(`shape.${label}`);
|
|
1360
|
+
}
|
|
1361
|
+
return parsed;
|
|
1362
|
+
}
|
|
1363
|
+
function objectValue(value, label) {
|
|
1364
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1365
|
+
throw new Error(`${label} 必须是 object`);
|
|
1366
|
+
}
|
|
1367
|
+
return value;
|
|
1368
|
+
}
|
|
1369
|
+
function stringField(value, label) {
|
|
1370
|
+
if (typeof value !== 'string' || !value)
|
|
1371
|
+
throw new Error(`${label} 必须是非空字符串`);
|
|
1372
|
+
return value;
|
|
1373
|
+
}
|
|
1374
|
+
function optionalString(value) {
|
|
1375
|
+
return typeof value === 'string' ? value : '';
|
|
1376
|
+
}
|
|
1377
|
+
function resourceId(value, label) {
|
|
1378
|
+
const item = stringField(value, label);
|
|
1379
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$/.test(item) ||
|
|
1380
|
+
/^(?:sk-|bearer|token[-_])/i.test(item)) {
|
|
1381
|
+
throw new Error(`${label} 不是安全稳定 ID`);
|
|
1382
|
+
}
|
|
1383
|
+
return item;
|
|
1384
|
+
}
|
|
1385
|
+
function safeSkillPath(value) {
|
|
1386
|
+
if (!value || value === 'SKILL.md' || value.startsWith('/') || value.includes('\\') ||
|
|
1387
|
+
value.split('/').some((part) => !part || part === '.' || part === '..')) {
|
|
1388
|
+
throw new Error(`远端 Skill supporting file 路径非法: ${value}`);
|
|
1389
|
+
}
|
|
1390
|
+
return value;
|
|
1391
|
+
}
|
|
1392
|
+
function requireMulticaWorkspace(start, name, cliVersion = '') {
|
|
1393
|
+
const workspace = showDevelopmentWorkspace(start, name, cliVersion);
|
|
1394
|
+
if (workspace.provider.kind !== 'multica' || workspace.legacy) {
|
|
1395
|
+
throw new Error(`Workspace ${name} 不是 multica Provider;deploy 不执行 ${workspace.provider.kind}`);
|
|
1396
|
+
}
|
|
1397
|
+
return workspace;
|
|
1398
|
+
}
|
|
1399
|
+
function safeExternalSkillRoot(path) {
|
|
1400
|
+
if (!existsSync(path) || lstatSync(path).isSymbolicLink()) {
|
|
1401
|
+
throw new Error(`Canonical Multica Boot Skill 不存在或是符号链接: ${path}`);
|
|
1402
|
+
}
|
|
1403
|
+
return realpathSync(path);
|
|
1404
|
+
}
|
|
1405
|
+
function safeProjectPath(root, ref, mustExist) {
|
|
1406
|
+
const rootReal = realpathSync(resolve(root));
|
|
1407
|
+
const candidate = resolve(rootReal, ref);
|
|
1408
|
+
const rel = relative(rootReal, candidate);
|
|
1409
|
+
if (!isInside(rootReal, candidate))
|
|
1410
|
+
throw new Error(`Multica deployment 路径越出 Project: ${ref}`);
|
|
1411
|
+
if (mustExist && !existsSync(candidate))
|
|
1412
|
+
throw new Error(`Multica deployment 路径不存在: ${ref}`);
|
|
1413
|
+
if (existsSync(candidate)) {
|
|
1414
|
+
if (lstatSync(candidate).isSymbolicLink())
|
|
1415
|
+
throw new Error(`Multica deployment 路径不能是符号链接: ${ref}`);
|
|
1416
|
+
if (!isInside(rootReal, realpathSync(candidate)))
|
|
1417
|
+
throw new Error(`Multica deployment 真实路径越界: ${ref}`);
|
|
1418
|
+
}
|
|
1419
|
+
let ancestor = dirname(candidate);
|
|
1420
|
+
while (!existsSync(ancestor)) {
|
|
1421
|
+
const parent = dirname(ancestor);
|
|
1422
|
+
if (parent === ancestor)
|
|
1423
|
+
break;
|
|
1424
|
+
ancestor = parent;
|
|
1425
|
+
}
|
|
1426
|
+
if (!existsSync(ancestor) || !isInside(rootReal, realpathSync(ancestor))) {
|
|
1427
|
+
throw new Error(`Multica deployment 父目录真实路径越界: ${ref}`);
|
|
1428
|
+
}
|
|
1429
|
+
void rel;
|
|
1430
|
+
return candidate;
|
|
1431
|
+
}
|
|
1432
|
+
function isInside(root, candidate) {
|
|
1433
|
+
const rel = relative(root, candidate);
|
|
1434
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`));
|
|
1435
|
+
}
|
|
1436
|
+
function deploymentsRoot(name) {
|
|
1437
|
+
return join(MULTICA_EVIDENCE_ROOT, safeName(name), 'multica-deployments');
|
|
1438
|
+
}
|
|
1439
|
+
function receiptsRoot(name) {
|
|
1440
|
+
return join(deploymentsRoot(name), 'receipts');
|
|
1441
|
+
}
|
|
1442
|
+
function receiptRelativePath(name, receiptId) {
|
|
1443
|
+
return join(receiptsRoot(name), `${resourceId(receiptId, 'receipt-id')}.json`);
|
|
1444
|
+
}
|
|
1445
|
+
function operationRelativePath(name, operationId) {
|
|
1446
|
+
return join(deploymentsRoot(name), 'operations', `${resourceId(operationId, 'operation-id')}.json`);
|
|
1447
|
+
}
|
|
1448
|
+
function latestDeploymentPointer(name) {
|
|
1449
|
+
return join(deploymentsRoot(name), 'latest.json');
|
|
1450
|
+
}
|
|
1451
|
+
function operationBindingPath(name, operationId) {
|
|
1452
|
+
return join(deploymentsRoot(name), 'bindings', `${resourceId(operationId, 'operation-id')}.json`);
|
|
1453
|
+
}
|
|
1454
|
+
function safeName(value) {
|
|
1455
|
+
if (!/^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/.test(value)) {
|
|
1456
|
+
throw new Error(`Workspace name 非法: ${value}`);
|
|
1457
|
+
}
|
|
1458
|
+
return value;
|
|
1459
|
+
}
|
|
1460
|
+
function positiveTimeout(value, fallback, max) {
|
|
1461
|
+
if (value === undefined)
|
|
1462
|
+
return fallback;
|
|
1463
|
+
if (!Number.isFinite(value) || value < 100 || value > max) {
|
|
1464
|
+
throw new Error(`Multica timeout 必须在 100..${max} ms`);
|
|
1465
|
+
}
|
|
1466
|
+
return Math.floor(value);
|
|
1467
|
+
}
|
|
1468
|
+
function atomicJson(path, value) {
|
|
1469
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1470
|
+
const temp = `${path}.tmp-${process.pid}-${randomUUID().slice(0, 8)}`;
|
|
1471
|
+
const source = `${JSON.stringify(value, null, 2)}\n`;
|
|
1472
|
+
writeFileSync(temp, source, { mode: 0o600, flag: 'wx' });
|
|
1473
|
+
renameSync(temp, path);
|
|
1474
|
+
if (readFileSync(path, 'utf8') !== source)
|
|
1475
|
+
throw new Error(`原子写后回读不一致: ${path}`);
|
|
1476
|
+
}
|
|
1477
|
+
function sha256(value) {
|
|
1478
|
+
return createHash('sha256').update(value).digest('hex');
|
|
1479
|
+
}
|
|
1480
|
+
//# sourceMappingURL=multica-deploy.js.map
|