@xdxer/dingtalk-agent 0.1.5-beta.1 → 0.1.5-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +201 -0
- package/README.en.md +99 -66
- package/README.md +99 -66
- package/dist/bin/dingtalk-agent.js +735 -149
- package/dist/bin/dingtalk-agent.js.map +1 -1
- package/dist/src/actions.js +3 -2
- package/dist/src/actions.js.map +1 -1
- package/dist/src/agent-audit.js +202 -85
- package/dist/src/agent-audit.js.map +1 -1
- package/dist/src/agent-definition.js +7 -3
- package/dist/src/agent-definition.js.map +1 -1
- package/dist/src/agent-enhance.js +51 -32
- package/dist/src/agent-enhance.js.map +1 -1
- package/dist/src/agent-platform.js +3 -3
- package/dist/src/agent-platform.js.map +1 -1
- package/dist/src/bootstrap.js +6 -2
- package/dist/src/bootstrap.js.map +1 -1
- package/dist/src/development-workspace.js +210 -34
- package/dist/src/development-workspace.js.map +1 -1
- package/dist/src/doctor.js +65 -9
- package/dist/src/doctor.js.map +1 -1
- package/dist/src/dws.js +67 -3
- package/dist/src/dws.js.map +1 -1
- package/dist/src/init.js +2 -1
- package/dist/src/init.js.map +1 -1
- package/dist/src/memory/noop-receipt.js +306 -0
- package/dist/src/memory/noop-receipt.js.map +1 -0
- package/dist/src/memory/operational.js +27 -3
- package/dist/src/memory/operational.js.map +1 -1
- package/dist/src/memory/remote-state.js +2 -1
- package/dist/src/memory/remote-state.js.map +1 -1
- package/dist/src/multica-deploy.js +692 -125
- package/dist/src/multica-deploy.js.map +1 -1
- package/dist/src/multica-provider.js +303 -25
- package/dist/src/multica-provider.js.map +1 -1
- package/dist/src/multica-runtime-vocabulary.js +110 -0
- package/dist/src/multica-runtime-vocabulary.js.map +1 -0
- package/dist/src/opencode-evals.js +6 -6
- package/dist/src/opencode-evals.js.map +1 -1
- package/dist/src/opencode-provider.js +21 -7
- package/dist/src/opencode-provider.js.map +1 -1
- package/dist/src/opencode-workspace.js +3 -3
- package/dist/src/opencode-workspace.js.map +1 -1
- package/dist/src/personal-event-evals.js +4 -2
- package/dist/src/personal-event-evals.js.map +1 -1
- package/dist/src/promotion.js +2 -1
- package/dist/src/promotion.js.map +1 -1
- package/dist/src/remote-semantic-state-live-evals.js +14 -8
- package/dist/src/remote-semantic-state-live-evals.js.map +1 -1
- package/dist/src/remote-state-evals.js +2 -2
- package/dist/src/remote-state-evals.js.map +1 -1
- package/dist/src/robot-evals.js +3 -3
- package/dist/src/robot-evals.js.map +1 -1
- package/dist/src/schedule-plan.js +380 -0
- package/dist/src/schedule-plan.js.map +1 -0
- package/dist/src/sessions.js +1 -1
- package/dist/src/sessions.js.map +1 -1
- package/dist/src/skill-manager.js +145 -13
- package/dist/src/skill-manager.js.map +1 -1
- package/dist/src/skills.js +2 -0
- package/dist/src/skills.js.map +1 -1
- package/dist/src/tui.js +369 -0
- package/dist/src/tui.js.map +1 -0
- package/dist/src/upgrade.js +113 -33
- package/dist/src/upgrade.js.map +1 -1
- package/dist/src/waits.js +2 -1
- package/dist/src/waits.js.map +1 -1
- package/dist/src/workspace.js +12 -7
- package/dist/src/workspace.js.map +1 -1
- package/docs/AGENT-IN-PRODUCTION.md +255 -0
- package/docs/ARCHITECTURE.md +165 -31
- package/docs/INSTALLATION.md +7 -7
- package/docs/PLATFORM-GUARDRAILS.md +188 -0
- package/docs/PRIOR-ART.md +4 -0
- package/docs/SELF-TEST.md +4 -4
- package/docs/architecture/agent-platform-connection-layer.svg +120 -0
- package/docs/architecture/digital-employee-composition.svg +92 -0
- package/docs/architecture/dingtalk-agent-architecture.svg +125 -0
- package/docs/assets/digital-employee-at-work.svg +77 -0
- package/docs/schemas/multica-deployment-plan.schema.json +3 -1
- package/docs/schemas/multica-deployment-receipt.schema.json +17 -3
- package/docs/schemas/multica-deployment-status.schema.json +6 -2
- package/docs/schemas/multica-workspace-inspection.schema.json +16 -0
- package/docs/schemas/multica-workspace-run-plan.schema.json +31 -0
- package/docs/schemas/multica-workspace-run.schema.json +161 -0
- package/docs/schemas/multica-workspace-status.schema.json +2 -0
- package/docs/schemas/project.schema.json +54 -3
- package/docs/schemas/workspace-scaffold.schema.json +38 -0
- package/examples/agents/README.md +10 -8
- package/examples/agents/fde-coach/AGENTS.md +2 -34
- package/examples/agents/fde-coach/agent/AGENTS.md +35 -0
- package/examples/agents/fde-coach/agent.bindings.json +10 -0
- package/examples/agents/release-manager/AGENTS.md +2 -34
- package/examples/agents/release-manager/agent/AGENTS.md +35 -0
- package/examples/agents/release-manager/agent.bindings.json +10 -0
- package/lab/agent-eval/catalog.json +5 -5
- package/lab/agent-eval/classic-failures.json +4 -4
- package/lab/agent-eval/completion-gate-regression.json +9 -9
- package/lab/agent-eval/personal-event-live.example.json +3 -3
- package/lab/agent-eval/remote-semantic-state-live.example.json +1 -1
- package/lab/agent-eval/workspace/opencode.json +2 -2
- package/lab/project-workspace/fake-multica-provider.mjs +171 -17
- package/lab/project-workspace/multica-deploy.fixture.json +2 -2
- package/lab/project-workspace/multica-readonly.fixture.json +4 -16
- package/lab/project-workspace/opencode-provider-suite.json +3 -3
- package/lab/project-workspace/project.fixture.json +2 -6
- package/lab/robot-eval/suite.json +1 -1
- package/lab/robot-eval/workspace/AGENTS.md +1 -1
- package/lab/robot-eval/workspace/opencode.json +2 -2
- package/lab/schemas/personal-event-eval.schema.json +1 -1
- package/package.json +12 -9
- package/skills/README.md +10 -8
- package/skills/core/{dingtalk-agent-compose → dta-agent-compose}/SKILL.md +48 -23
- package/skills/core/{dingtalk-agent-compose → dta-agent-compose}/assets/AGENTS.template.md +1 -1
- package/skills/core/dta-agent-compose/assets/REPOSITORY.template.md +10 -0
- package/skills/core/{dingtalk-agent-compose → dta-agent-compose}/assets/agent.bindings.dingtalk-doc.template.json +2 -2
- package/skills/core/{dingtalk-agent-compose → dta-agent-compose}/assets/agent.bindings.local.template.json +2 -2
- package/skills/core/{dingtalk-agent-compose → dta-agent-compose}/assets/hosts/opencode/opencode.template.json +3 -2
- package/skills/core/{dingtalk-agent-compose → dta-agent-compose}/evals/evals.json +4 -4
- package/skills/core/{dingtalk-agent-compose → dta-agent-compose}/references/agent-definition-contract.md +7 -7
- package/skills/core/dta-agent-compose/references/drive-and-schedules.md +166 -0
- package/skills/core/{dingtalk-agent-compose → dta-agent-compose}/references/host-loading-contract.md +11 -13
- package/skills/core/{dingtalk-agent-compose → dta-agent-compose}/references/hosts/claude-code.md +13 -12
- package/skills/core/{dingtalk-agent-compose → dta-agent-compose}/references/hosts/opencode.md +14 -13
- package/skills/core/{dingtalk-agent-eval → dta-agent-eval}/SKILL.md +2 -2
- package/skills/core/{dingtalk-agent-eval → dta-agent-eval}/assets/eval-catalog.template.json +1 -1
- package/skills/core/{dingtalk-agent-eval → dta-agent-eval}/evals/evals.json +1 -1
- package/skills/core/{dingtalk-agent-eval → dta-agent-eval}/references/eval-topology.md +3 -3
- package/skills/core/{dingtalk-agent-eval → dta-agent-eval}/references/interactive-debug-channels.md +10 -4
- package/skills/core/{dingtalk-basic-behavior → dta-basic-behavior}/SKILL.md +21 -5
- package/skills/core/dta-basic-behavior/references/event-to-behavior.md +38 -0
- package/skills/core/{dingtalk-basic-behavior → dta-basic-behavior}/references/memory-and-evolution.md +3 -1
- package/skills/core/dta-basic-behavior/references/perception-and-gates.md +87 -0
- package/skills/core/{dingtalk-basic-behavior → dta-basic-behavior}/references/risk-authority-and-privacy.md +12 -0
- package/skills/core/{dingtalk-basic-behavior → dta-basic-behavior}/references/truth-and-recovery.md +4 -2
- package/skills/core/dta-people-group-memory/COMPLETENESS.md +36 -0
- package/skills/core/dta-people-group-memory/SKILL.md +69 -0
- package/skills/core/dta-people-group-memory/references/adapters.md +273 -0
- package/skills/core/dta-people-group-memory/references/assembly-guidance.md +40 -0
- package/skills/core/dta-people-group-memory/references/binding.md +110 -0
- package/skills/core/dta-people-group-memory/references/cold-start.md +70 -0
- package/skills/core/dta-people-group-memory/references/config-binding.md +89 -0
- package/skills/core/dta-people-group-memory/references/consent-and-visibility.md +83 -0
- package/skills/core/dta-people-group-memory/references/consolidation.md +162 -0
- package/skills/core/dta-people-group-memory/references/event-ingest.md +103 -0
- package/skills/core/dta-people-group-memory/references/guided-setup.md +70 -0
- package/skills/core/dta-people-group-memory/references/model.md +148 -0
- package/skills/core/dta-people-group-memory/references/storage-port.md +107 -0
- package/skills/platforms/deap/PLATFORM.md +30 -1
- package/skills/platforms/multica-dingtalk/PLATFORM.md +34 -8
- package/skills/platforms/multica-dingtalk/{dingtalk-agent-deploy-multica → dta-deploy-multica}/SKILL.md +10 -8
- package/skills/platforms/multica-dingtalk/dta-deploy-multica/references/multica-deployment-contract.md +67 -0
- package/skills/platforms/multica-dingtalk/{multica-external → dta-ops-multica}/SKILL.md +81 -11
- package/skills/platforms/multica-dingtalk/{multica-external → dta-ops-multica}/scripts/bootstrap.sh +2 -2
- package/skills/platforms/multica-dingtalk/{multica-external → dta-ops-multica}/scripts/multica_ext.py +264 -16
- package/docs/assets/agent-delivery-lifecycle.svg +0 -103
- package/skills/core/dingtalk-basic-behavior/references/event-to-behavior.md +0 -24
- package/skills/core/dingtalk-basic-behavior/references/perception-and-gates.md +0 -28
- package/skills/platforms/multica-dingtalk/dingtalk-agent-boot-multica/SKILL.md +0 -40
- package/skills/platforms/multica-dingtalk/dingtalk-agent-deploy-multica/references/multica-deployment-contract.md +0 -49
- /package/examples/agents/fde-coach/{skills → agent/skills}/fde-coach/SKILL.md +0 -0
- /package/examples/agents/release-manager/{skills → agent/skills}/release-manager/SKILL.md +0 -0
- /package/skills/core/{dingtalk-agent-compose → dta-agent-compose}/assets/role-skill.template.md +0 -0
- /package/skills/core/{dingtalk-agent-compose → dta-agent-compose}/references/storage-routing.md +0 -0
- /package/skills/core/{dingtalk-agent-eval → dta-agent-eval}/references/evidence-contract.md +0 -0
- /package/skills/core/{dingtalk-agent-eval → dta-agent-eval}/references/failure-to-case.md +0 -0
- /package/skills/core/{dingtalk-agent-eval → dta-agent-eval}/references/local-connector-smoke.md +0 -0
- /package/skills/core/{dingtalk-agent-eval → dta-agent-eval}/references/scenario-taxonomy.md +0 -0
- /package/skills/core/{dingtalk-agent-eval → dta-agent-eval}/references/storage-modes.md +0 -0
- /package/skills/core/{dingtalk-basic-behavior → dta-basic-behavior}/assets/memory-candidate-proposal.json +0 -0
- /package/skills/core/{dingtalk-basic-behavior → dta-basic-behavior}/assets/task-checkpoint.json +0 -0
- /package/skills/core/{dingtalk-basic-behavior → dta-basic-behavior}/references/action-contract.md +0 -0
- /package/skills/core/{dingtalk-basic-behavior → dta-basic-behavior}/references/runtime-modes.md +0 -0
- /package/skills/core/{dingtalk-basic-behavior → dta-basic-behavior}/references/task-lifecycle.md +0 -0
- /package/skills/platforms/multica-dingtalk/{dingtalk-agent-deploy-multica → dta-deploy-multica}/references/promotion-observation-contract.md +0 -0
|
@@ -3,11 +3,10 @@ import { spawnSync } from 'node:child_process';
|
|
|
3
3
|
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
6
|
-
import { WORKSPACE_STATE_ROOT, inspectAgentProject, showDevelopmentWorkspace, } from './development-workspace.js';
|
|
6
|
+
import { WORKSPACE_STATE_ROOT, inspectAgentProject, projectSkillRef, showDevelopmentWorkspace, } from './development-workspace.js';
|
|
7
7
|
import { digest, stableStringify } from './events.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { bundledSkillPath } from './skill-manager.js';
|
|
8
|
+
import { MULTICA_EVIDENCE_ROOT, emitProviderDiagnostic, inspectMulticaWorkspace, planMulticaWorkspace, sanitizeProviderEnv, statusMulticaWorkspace, } from './multica-provider.js';
|
|
9
|
+
import { allRuntimeVocabularies, commandInvokesDws, forbiddenSmokeTool, skillsLoadedInContent, vocabulariesForRuntimeProvider, } from './multica-runtime-vocabulary.js';
|
|
11
10
|
export const MULTICA_DEPLOYMENT_SCHEMA = 'dingtalk-agent/multica-deployment-receipt@1';
|
|
12
11
|
const MAX_DEPLOY_SKILLS = 32;
|
|
13
12
|
const MAX_SKILL_SUPPORTING_FILES = 128;
|
|
@@ -15,14 +14,293 @@ const deploymentWriteBudgets = new WeakMap();
|
|
|
15
14
|
class DeploymentFailure extends Error {
|
|
16
15
|
category;
|
|
17
16
|
ambiguous;
|
|
18
|
-
|
|
17
|
+
detail;
|
|
18
|
+
constructor(category, ambiguous = false, detail) {
|
|
19
19
|
super(category);
|
|
20
20
|
this.category = category;
|
|
21
21
|
this.ambiguous = ambiguous;
|
|
22
|
+
this.detail = detail;
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
|
|
26
|
+
const normalizedPrompt = prompt.trim();
|
|
27
|
+
if (!normalizedPrompt)
|
|
28
|
+
throw new Error('workspace run prompt 不能为空');
|
|
29
|
+
if (Buffer.byteLength(normalizedPrompt, 'utf8') > 64 * 1024) {
|
|
30
|
+
throw new Error('workspace run prompt 不能超过 65536 bytes');
|
|
31
|
+
}
|
|
32
|
+
rejectCredentialMaterial(normalizedPrompt, 'workspace run prompt');
|
|
33
|
+
const env = options.env || process.env;
|
|
34
|
+
const cliVersion = options.cliVersion || '';
|
|
35
|
+
const info = inspectAgentProject(start, cliVersion);
|
|
36
|
+
requireMulticaWorkspace(info.root, name, cliVersion);
|
|
37
|
+
const providerPlan = planMulticaWorkspace(info.root, name, { env, cliVersion });
|
|
38
|
+
const blocking = [...providerPlan.blocking];
|
|
39
|
+
if (!providerPlan.expected.agentId)
|
|
40
|
+
blocking.push('agent-id.missing');
|
|
41
|
+
const plan = {
|
|
42
|
+
$schema: 'dingtalk-agent/multica-workspace-run-plan@1',
|
|
43
|
+
workspace: name,
|
|
44
|
+
provider: 'multica',
|
|
45
|
+
profile: providerPlan.profile,
|
|
46
|
+
workspaceId: providerPlan.expected.workspaceId,
|
|
47
|
+
runtimeId: providerPlan.expected.runtimeId,
|
|
48
|
+
agentId: providerPlan.expected.agentId,
|
|
49
|
+
promptHash: sha256(normalizedPrompt),
|
|
50
|
+
operations: ['scope.inspect', 'issue.create', 'issue.runs', 'issue.run-messages', 'evidence.persist'],
|
|
51
|
+
blocking: [...new Set(blocking)].sort(),
|
|
52
|
+
requiresExecute: true,
|
|
53
|
+
requiresExplicitYes: true,
|
|
54
|
+
remoteRead: false,
|
|
55
|
+
remoteWrite: false,
|
|
56
|
+
triggerWrite: false,
|
|
57
|
+
sideEffect: false,
|
|
58
|
+
dingtalkSideEffect: false,
|
|
59
|
+
};
|
|
60
|
+
if (!options.execute)
|
|
61
|
+
return plan;
|
|
62
|
+
if (!options.yes) {
|
|
63
|
+
throw new Error('Multica workspace run 会创建远端 Issue;必须同时传 --execute --yes');
|
|
64
|
+
}
|
|
65
|
+
if (plan.blocking.length) {
|
|
66
|
+
throw new Error(`Multica workspace run 前置条件不完整: ${plan.blocking.join(', ')}`);
|
|
67
|
+
}
|
|
68
|
+
const command = options.command || env.DTA_MULTICA_BIN || 'multica';
|
|
69
|
+
// A real Issue-channel run does agent work plus issue get/comment/status side-tasks and
|
|
70
|
+
// routinely needs minutes; the old 30s default timed out on almost every honest run. Wait
|
|
71
|
+
// 4 minutes by default (still capped at 5). A run that overruns is resumable via
|
|
72
|
+
// `workspace run --status --run-id` against the same Issue — no duplicate Issue.
|
|
73
|
+
const timeoutMs = positiveTimeout(options.timeoutMs, 240_000, 300_000);
|
|
74
|
+
const calls = [];
|
|
75
|
+
const inspection = inspectMulticaWorkspace(info.root, name, {
|
|
76
|
+
execute: true, yes: true, persist: false, command, env,
|
|
77
|
+
timeoutMs: Math.min(timeoutMs, 30_000), cliVersion,
|
|
78
|
+
});
|
|
79
|
+
appendInspectionCalls(inspection, 'run.preflight', calls);
|
|
80
|
+
const assignedSkills = inspection.resources.assignedSkills
|
|
81
|
+
.filter((item) => item.enabled).map((item) => item.name).sort();
|
|
82
|
+
const expectedSkills = [...providerPlan.expected.skills].sort();
|
|
83
|
+
if (!inspection.passed || inspection.resources.workspace?.id !== plan.workspaceId ||
|
|
84
|
+
inspection.resources.runtime?.id !== plan.runtimeId ||
|
|
85
|
+
inspection.resources.agent?.id !== plan.agentId ||
|
|
86
|
+
inspection.diff.includes('agent.runtime-drift') ||
|
|
87
|
+
stableStringify(assignedSkills) !== stableStringify(expectedSkills)) {
|
|
88
|
+
throw new Error('Multica workspace run scope 或 Skill assignment 与 Project 不一致;未创建 Issue');
|
|
89
|
+
}
|
|
90
|
+
const runId = `multica_workspace_run_${Date.now()}_${randomUUID().slice(0, 8)}`;
|
|
91
|
+
const marker = `DTA-WORKSPACE-RUN-${randomUUID()}`;
|
|
92
|
+
const issue = jsonObject(runProvider(command, scopedArgs({ profile: plan.profile, expected: { workspaceId: plan.workspaceId } }, [
|
|
93
|
+
'issue', 'create', '--title', `[DTA workspace run] ${marker}`,
|
|
94
|
+
'--description', normalizedPrompt, '--assignee-id', plan.agentId, '--output', 'json',
|
|
95
|
+
]), 'workspace-run.issue.create', 'write', info.root, env, Math.min(timeoutMs, 30_000), calls).stdout, 'workspace run issue');
|
|
96
|
+
const issueId = resourceId(issue.id, 'workspace run issue.id');
|
|
97
|
+
return readbackWorkspaceRun({
|
|
98
|
+
root: info.root, name, plan, issueId, runId, command, env, timeoutMs, calls, out: options.out,
|
|
99
|
+
runtimeProvider: inspection.resources.runtime?.provider || '',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Poll an existing Issue's assignee-agent task to a terminal state, then reconstruct the
|
|
104
|
+
* run report from its trace. Shared by the initial `workspace run` (right after it creates the
|
|
105
|
+
* Issue) and `workspace run --status --run-id` (resume against the SAME Issue, no new write) so
|
|
106
|
+
* a timed-out run reconciles instead of spawning a duplicate Issue.
|
|
107
|
+
*/
|
|
108
|
+
function readbackWorkspaceRun(ctx) {
|
|
109
|
+
const { root, name, plan, issueId, runId, command, env, timeoutMs, calls } = ctx;
|
|
110
|
+
const started = Date.now();
|
|
111
|
+
const pollIntervalMs = 2_000;
|
|
112
|
+
const pollCalls = [];
|
|
113
|
+
let pollAttempts = 0;
|
|
114
|
+
let task;
|
|
115
|
+
while (Date.now() - started < timeoutMs) {
|
|
116
|
+
pollAttempts += 1;
|
|
117
|
+
const runs = jsonArray(runProvider(command, scopedArgs({ profile: plan.profile, expected: { workspaceId: plan.workspaceId } }, [
|
|
118
|
+
'issue', 'runs', issueId, '--full-id', '--output', 'json',
|
|
119
|
+
]), 'workspace-run.issue.runs', 'read', root, env, Math.min(timeoutMs, 30_000), pollCalls).stdout, 'workspace run issue runs');
|
|
120
|
+
task = runs.filter((item) => !optionalString(item.agent_id) ||
|
|
121
|
+
optionalString(item.agent_id) === plan.agentId)
|
|
122
|
+
.sort((a, b) => optionalString(b.created_at).localeCompare(optionalString(a.created_at)))[0];
|
|
123
|
+
const status = optionalString(task?.status);
|
|
124
|
+
if (['completed', 'failed', 'cancelled'].includes(status))
|
|
125
|
+
break;
|
|
126
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, pollIntervalMs);
|
|
127
|
+
}
|
|
128
|
+
calls.push(...uniqueCalls(pollCalls));
|
|
129
|
+
const taskId = task?.id ? resourceId(task.id, 'workspace run task.id') : '';
|
|
130
|
+
const taskStatus = optionalString(task?.status) || 'not-created';
|
|
131
|
+
let messages = [];
|
|
132
|
+
if (taskId && ['completed', 'failed', 'cancelled'].includes(taskStatus)) {
|
|
133
|
+
messages = jsonArray(runProvider(command, scopedArgs({ profile: plan.profile, expected: { workspaceId: plan.workspaceId } }, [
|
|
134
|
+
'issue', 'run-messages', taskId, '--issue', issueId, '--output', 'json',
|
|
135
|
+
]), 'workspace-run.issue.messages', 'read', root, env, Math.min(timeoutMs, 30_000), calls).stdout, 'workspace run messages');
|
|
136
|
+
}
|
|
137
|
+
const toolUses = messages.filter((item) => optionalString(item.type) === 'tool_use');
|
|
138
|
+
const toolNames = toolUses.map((item) => optionalString(item.tool)).filter(Boolean);
|
|
139
|
+
// Skill 装载与文件写入按 runtime 词汇解读(并集),并叠加 tool_result 的
|
|
140
|
+
// `**Skill loaded**` 正向证据 —— 与 smoke 判定同一套词汇,不再假定工具名叫 skill。
|
|
141
|
+
const vocabularies = ctx.runtimeProvider
|
|
142
|
+
? vocabulariesForRuntimeProvider(ctx.runtimeProvider) : [];
|
|
143
|
+
const active = vocabularies.length
|
|
144
|
+
? vocabularies : allRuntimeVocabularies();
|
|
145
|
+
const loadedSet = new Set();
|
|
146
|
+
const runBannerTools = new Set(active.flatMap((item) => [...item.skillResultTools]));
|
|
147
|
+
for (const item of messages) {
|
|
148
|
+
const type = optionalString(item.type);
|
|
149
|
+
const tool = optionalString(item.tool);
|
|
150
|
+
if (type === 'tool_use') {
|
|
151
|
+
const message = { tool, input: smokeToolInput(item) };
|
|
152
|
+
for (const vocabulary of active) {
|
|
153
|
+
const name = vocabulary.skillLoadFromToolUse(message);
|
|
154
|
+
if (name)
|
|
155
|
+
loadedSet.add(name);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (type === 'tool_result' && runBannerTools.has(tool)) {
|
|
159
|
+
for (const name of skillsLoadedInContent(optionalString(item.content)))
|
|
160
|
+
loadedSet.add(name);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const loadedSkills = [...loadedSet].sort();
|
|
164
|
+
const texts = messages.filter((item) => optionalString(item.type) === 'text')
|
|
165
|
+
.map((item) => optionalString(item.content)).filter(Boolean);
|
|
166
|
+
const replyWrites = toolUses.flatMap((item) => {
|
|
167
|
+
const message = {
|
|
168
|
+
tool: optionalString(item.tool), input: smokeToolInput(item),
|
|
169
|
+
};
|
|
170
|
+
return active.map((vocabulary) => vocabulary.writeEvidence(message))
|
|
171
|
+
.filter((write) => Boolean(write))
|
|
172
|
+
// 裸文件名与 ./、绝对路径等价(平台常见写法,与 smoke 白名单同一课)。
|
|
173
|
+
.filter((write) => write.path === 'reply.md' || write.path.endsWith('/reply.md'))
|
|
174
|
+
.map((write) => write.content);
|
|
175
|
+
}).filter(Boolean);
|
|
176
|
+
const answer = safeRemoteOutput(replyWrites.at(-1) || texts.at(-1) || '', 64 * 1024);
|
|
177
|
+
const issueReadback = jsonObject(runProvider(command, scopedArgs({ profile: plan.profile, expected: { workspaceId: plan.workspaceId } }, [
|
|
178
|
+
'issue', 'get', issueId, '--output', 'json',
|
|
179
|
+
]), 'workspace-run.issue.readback', 'read', root, env, Math.min(timeoutMs, 30_000), calls).stdout, 'workspace run issue readback');
|
|
180
|
+
const issueStatus = optionalString(issueReadback.status);
|
|
181
|
+
const failures = [];
|
|
182
|
+
if (!task)
|
|
183
|
+
failures.push('task.not-created');
|
|
184
|
+
else if (!['completed', 'failed', 'cancelled'].includes(taskStatus))
|
|
185
|
+
failures.push('task.timeout');
|
|
186
|
+
else if (taskStatus !== 'completed')
|
|
187
|
+
failures.push(`task.${taskStatus}`);
|
|
188
|
+
if (!answer)
|
|
189
|
+
failures.push('answer.missing');
|
|
190
|
+
const taskDiagnostic = task
|
|
191
|
+
? remoteTaskDiagnostic(task)
|
|
192
|
+
: `未发现关联 task;Issue 状态=${issueStatus || 'unknown'},已轮询 ${pollAttempts} 次`;
|
|
193
|
+
const report = {
|
|
194
|
+
$schema: 'dingtalk-agent/multica-workspace-run@1',
|
|
195
|
+
passed: failures.length === 0,
|
|
196
|
+
runId,
|
|
197
|
+
workspace: name,
|
|
198
|
+
provider: 'multica',
|
|
199
|
+
profile: plan.profile,
|
|
200
|
+
workspaceId: plan.workspaceId,
|
|
201
|
+
runtimeId: plan.runtimeId,
|
|
202
|
+
agentId: plan.agentId,
|
|
203
|
+
promptHash: plan.promptHash,
|
|
204
|
+
issueId,
|
|
205
|
+
issueStatus,
|
|
206
|
+
taskId,
|
|
207
|
+
taskStatus,
|
|
208
|
+
taskDiagnostic,
|
|
209
|
+
pollAttempts,
|
|
210
|
+
pollIntervalMs,
|
|
211
|
+
answer,
|
|
212
|
+
answerHash: sha256(answer),
|
|
213
|
+
toolNames,
|
|
214
|
+
loadedSkills,
|
|
215
|
+
failures,
|
|
216
|
+
calls,
|
|
217
|
+
evidencePath: '',
|
|
218
|
+
remoteRead: true,
|
|
219
|
+
remoteWrite: calls.some((call) => call.kind === 'write'),
|
|
220
|
+
triggerWrite: false,
|
|
221
|
+
sideEffect: 'multica-issue-read-write',
|
|
222
|
+
dingtalkSideEffect: false,
|
|
223
|
+
};
|
|
224
|
+
const defaultEvidence = join(MULTICA_EVIDENCE_ROOT, safeName(name), 'multica-runs', `${runId}.json`);
|
|
225
|
+
report.evidencePath = relative(root, safeProjectPath(root, ctx.out || defaultEvidence, false));
|
|
226
|
+
assertReportRedacted(report);
|
|
227
|
+
atomicJson(safeProjectPath(root, report.evidencePath, false), report);
|
|
228
|
+
return report;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Resume a `workspace run` by re-reading the SAME Issue's task instead of creating a new one.
|
|
232
|
+
* Loads the persisted run evidence by runId, confirms the workspace scope still resolves to the
|
|
233
|
+
* same target (fail closed on drift), then polls that Issue's assignee-agent task to a terminal
|
|
234
|
+
* state and rewrites the run report. Read-only against Multica — never enqueues a second Issue.
|
|
235
|
+
*/
|
|
236
|
+
export function statusMulticaWorkspaceRun(start, name, options = {}) {
|
|
237
|
+
const env = options.env || process.env;
|
|
238
|
+
const cliVersion = options.cliVersion || '';
|
|
239
|
+
const runId = String(options.runId || '').trim();
|
|
240
|
+
if (!/^multica_workspace_run_[0-9]+_[a-f0-9]{8}$/.test(runId)) {
|
|
241
|
+
throw new Error('workspace run --status 需要一个合法的 --run-id(来自上次 run 的 runId)');
|
|
242
|
+
}
|
|
243
|
+
if (!options.execute) {
|
|
244
|
+
throw new Error('workspace run --status 会向 Multica 独立回读同一 Issue;必须传 --execute');
|
|
245
|
+
}
|
|
246
|
+
if (!options.yes)
|
|
247
|
+
throw new Error('workspace run --status 远端回读必须同时传 --execute --yes');
|
|
248
|
+
const info = inspectAgentProject(start, cliVersion);
|
|
249
|
+
requireMulticaWorkspace(info.root, name, cliVersion);
|
|
250
|
+
// The original run persists evidence to `--out` when given, else the default runId path. Resume
|
|
251
|
+
// must look where the run actually wrote, so honor the same `--out` (and rewrite back to it).
|
|
252
|
+
const evidenceRel = options.out || join(MULTICA_EVIDENCE_ROOT, safeName(name), 'multica-runs', `${runId}.json`);
|
|
253
|
+
const evidenceAbs = safeProjectPath(info.root, evidenceRel, false);
|
|
254
|
+
if (!existsSync(evidenceAbs)) {
|
|
255
|
+
throw new Error(`找不到 run 证据 ${evidenceRel};--run-id 必须是本 Workspace 已有的 runId` +
|
|
256
|
+
`${options.out ? '' : '(若原 run 用了 --out,请对 --status 传相同 --out)'}`);
|
|
257
|
+
}
|
|
258
|
+
const prior = JSON.parse(readFileSync(evidenceAbs, 'utf8'));
|
|
259
|
+
if (prior.$schema !== 'dingtalk-agent/multica-workspace-run@1' ||
|
|
260
|
+
prior.provider !== 'multica' || prior.workspace !== name || !prior.issueId) {
|
|
261
|
+
throw new Error('run 证据不是本 Workspace 的 multica run,或缺少 issueId;已 fail closed');
|
|
262
|
+
}
|
|
263
|
+
// The evidence file must belong to THIS runId. Two runs sharing one --out overwrite each
|
|
264
|
+
// other, so resuming the first runId against that path would otherwise read the second run's
|
|
265
|
+
// Issue while reporting the first runId — cross-wiring answer and run. Fail closed on mismatch.
|
|
266
|
+
if (prior.runId !== runId) {
|
|
267
|
+
throw new Error(`run 证据 ${evidenceRel} 属于 runId ${prior.runId},与 --run-id ${runId} 不一致;已 fail closed` +
|
|
268
|
+
`(同一 --out 被后一次 run 覆盖时会这样)`);
|
|
269
|
+
}
|
|
270
|
+
// Re-resolve scope offline and refuse unless the WHOLE frozen target still matches: an env
|
|
271
|
+
// pointing at a different runtime/agent/profile (or an invalid ref surfacing in blocking) must
|
|
272
|
+
// not let the resume mis-record the old task or overwrite the frozen values. workspaceId alone
|
|
273
|
+
// is not enough.
|
|
274
|
+
const providerPlan = planMulticaWorkspace(info.root, name, { env, cliVersion });
|
|
275
|
+
const scopeDrift = providerPlan.blocking.length ? ['blocking:' + providerPlan.blocking.join(',')] : [];
|
|
276
|
+
if (providerPlan.profile !== prior.profile)
|
|
277
|
+
scopeDrift.push('profile');
|
|
278
|
+
if (providerPlan.expected.workspaceId !== prior.workspaceId)
|
|
279
|
+
scopeDrift.push('workspace');
|
|
280
|
+
if (providerPlan.expected.runtimeId !== prior.runtimeId)
|
|
281
|
+
scopeDrift.push('runtime');
|
|
282
|
+
if (providerPlan.expected.agentId !== prior.agentId)
|
|
283
|
+
scopeDrift.push('agent');
|
|
284
|
+
if (scopeDrift.length) {
|
|
285
|
+
throw new Error(`workspace run --status: 当前 workspace scope 与 run 证据不一致 [${scopeDrift.join(', ')}];已 fail closed`);
|
|
286
|
+
}
|
|
287
|
+
const command = options.command || env.DTA_MULTICA_BIN || 'multica';
|
|
288
|
+
const timeoutMs = positiveTimeout(options.timeoutMs, 240_000, 300_000);
|
|
289
|
+
const plan = {
|
|
290
|
+
profile: providerPlan.profile,
|
|
291
|
+
workspaceId: providerPlan.expected.workspaceId,
|
|
292
|
+
runtimeId: providerPlan.expected.runtimeId,
|
|
293
|
+
agentId: providerPlan.expected.agentId,
|
|
294
|
+
promptHash: prior.promptHash,
|
|
295
|
+
};
|
|
296
|
+
return readbackWorkspaceRun({
|
|
297
|
+
root: info.root, name, plan, issueId: prior.issueId, runId,
|
|
298
|
+
command, env, timeoutMs, calls: [], out: options.out,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
24
301
|
export function planMulticaDeployment(start, name, options = {}) {
|
|
25
302
|
const action = options.action || 'apply';
|
|
303
|
+
const rebindRuntime = Boolean(options.rebindRuntime) && action === 'apply';
|
|
26
304
|
const env = options.env || process.env;
|
|
27
305
|
const cliVersion = options.cliVersion || '';
|
|
28
306
|
const info = inspectAgentProject(start, cliVersion);
|
|
@@ -31,12 +309,21 @@ export function planMulticaDeployment(start, name, options = {}) {
|
|
|
31
309
|
const readPlan = planMulticaWorkspace(info.root, name, { env, cliVersion });
|
|
32
310
|
const blocking = [...readPlan.blocking];
|
|
33
311
|
let inspectionId = '';
|
|
312
|
+
let previousRuntimeId = '';
|
|
34
313
|
try {
|
|
35
314
|
const status = statusMulticaWorkspace(info.root, name, cliVersion);
|
|
36
315
|
inspectionId = status.inspectionId;
|
|
37
316
|
if (!status.readyForApply || !status.evidenceIntegrity || !status.stateMatched) {
|
|
38
317
|
blocking.push(...status.failures.map((item) => `inspection.${item}`));
|
|
39
318
|
}
|
|
319
|
+
// Runtime 归属漂移只挡「普通 apply」:没有显式 --rebind-runtime 时不允许悄悄把
|
|
320
|
+
// Agent 搬到新 runtime。retire 是收敛动作,不受漂移阻挡(否则 deploy 与 retire
|
|
321
|
+
// 会被同一把锁一起锁死)。
|
|
322
|
+
if (status.diff.includes('agent.runtime-drift')) {
|
|
323
|
+
previousRuntimeId = status.observedAgentRuntimeId || '';
|
|
324
|
+
if (action === 'apply' && !rebindRuntime)
|
|
325
|
+
blocking.push('agent.runtime-drift');
|
|
326
|
+
}
|
|
40
327
|
}
|
|
41
328
|
catch {
|
|
42
329
|
blocking.push('inspection.invalid');
|
|
@@ -45,26 +332,30 @@ export function planMulticaDeployment(start, name, options = {}) {
|
|
|
45
332
|
blocking.push('inspection.missing');
|
|
46
333
|
if (action === 'retire' && !readPlan.expected.agentId)
|
|
47
334
|
blocking.push('agent-id.missing');
|
|
335
|
+
if (rebindRuntime && !readPlan.expected.agentId)
|
|
336
|
+
blocking.push('rebind.agent-id-missing');
|
|
48
337
|
const expectedSkills = source.skills.map((skill) => ({
|
|
49
338
|
name: skill.name, hash: skill.hash, files: skill.files.length + 1,
|
|
50
339
|
}));
|
|
51
340
|
const operations = action === 'retire'
|
|
52
341
|
? ['scope.reinspect', 'agent.archive', 'agent.readback', 'receipt.persist']
|
|
53
342
|
: [
|
|
54
|
-
'scope.reinspect', 'skills.readback', 'skills.reconcile',
|
|
343
|
+
'scope.reinspect', 'skills.readback', 'skills.reconcile',
|
|
344
|
+
...(rebindRuntime ? ['agent.rebind-runtime'] : []), 'agent.reconcile',
|
|
55
345
|
'skills.assign-exact', 'provider.readback', 'load-smoke.create',
|
|
56
|
-
'load-smoke.readback', 'receipt.persist',
|
|
346
|
+
'load-smoke.recover-if-unstarted', 'load-smoke.readback', 'receipt.persist',
|
|
57
347
|
];
|
|
58
348
|
const writeBudget = action === 'retire'
|
|
59
349
|
? { forward: 1, rollback: 0 }
|
|
60
350
|
: {
|
|
61
|
-
|
|
351
|
+
// +3 on top of the base 5: bounded skills-not-visible smoke retries (cold sandbox).
|
|
352
|
+
forward: source.skills.length * (1 + MAX_SKILL_SUPPORTING_FILES * 2) + 8,
|
|
62
353
|
rollback: source.skills.length * (1 + MAX_SKILL_SUPPORTING_FILES * 2) + 3,
|
|
63
354
|
};
|
|
64
355
|
const planId = `multica_deploy_plan_${digest(stableStringify({
|
|
65
356
|
action, workspace: name, profile: readPlan.profile, inspectionId,
|
|
66
357
|
desiredHash: workspace.desiredHash, deploymentHash: source.deploymentHash,
|
|
67
|
-
expected: readPlan.expected, operations, writeBudget,
|
|
358
|
+
expected: readPlan.expected, operations, writeBudget, rebindRuntime, previousRuntimeId,
|
|
68
359
|
blocking: [...new Set(blocking)].sort(),
|
|
69
360
|
})).slice(0, 24)}`;
|
|
70
361
|
return {
|
|
@@ -85,7 +376,9 @@ export function planMulticaDeployment(start, name, options = {}) {
|
|
|
85
376
|
skills: expectedSkills,
|
|
86
377
|
definitionHash: source.definitionHash,
|
|
87
378
|
instructionsHash: source.instructionsHash,
|
|
379
|
+
...(rebindRuntime ? { previousRuntimeId } : {}),
|
|
88
380
|
},
|
|
381
|
+
rebindRuntime,
|
|
89
382
|
operations,
|
|
90
383
|
writeBudget,
|
|
91
384
|
blocking: [...new Set(blocking)].sort(),
|
|
@@ -106,9 +399,12 @@ export function applyMulticaDeployment(start, name, options = {}, action = 'appl
|
|
|
106
399
|
throw new Error('dta deploy 必须传 dry-run 返回的 --plan-id');
|
|
107
400
|
const env = options.env || process.env;
|
|
108
401
|
const cliVersion = options.cliVersion || '';
|
|
109
|
-
const
|
|
402
|
+
const rebindRuntime = Boolean(options.rebindRuntime) && action === 'apply';
|
|
403
|
+
const plan = planMulticaDeployment(start, name, { action, env, cliVersion, rebindRuntime });
|
|
110
404
|
if (options.planId !== plan.planId) {
|
|
111
|
-
throw new Error(`deploy plan 已失效;期望当前 planId ${plan.planId}
|
|
405
|
+
throw new Error(`deploy plan 已失效;期望当前 planId ${plan.planId}。` +
|
|
406
|
+
'plan 对中间状态敏感:dry-run 之后、apply 之前不要插入 workspace inspect 等操作;' +
|
|
407
|
+
'重跑 --dry-run 取新 planId 后立即 apply');
|
|
112
408
|
}
|
|
113
409
|
if (plan.blocking.length)
|
|
114
410
|
throw new Error(`deploy 前置条件不完整: ${plan.blocking.join(', ')}`);
|
|
@@ -126,7 +422,12 @@ export function applyMulticaDeployment(start, name, options = {}, action = 'appl
|
|
|
126
422
|
preflight.profile !== plan.profile ||
|
|
127
423
|
preflight.expected.workspaceId !== plan.expected.workspaceId ||
|
|
128
424
|
preflight.expected.runtimeId !== plan.expected.runtimeId) {
|
|
129
|
-
throw new Error('deploy scope/readback 与冻结 plan 不一致;未执行任何远端写入'
|
|
425
|
+
throw new Error('deploy scope/readback 与冻结 plan 不一致;未执行任何远端写入' +
|
|
426
|
+
'(dry-run 与 apply 之间若插入过 inspect 或远端有变化,重跑 --dry-run 后立即 apply)');
|
|
427
|
+
}
|
|
428
|
+
if (action === 'apply' && !rebindRuntime && preflight.diff.includes('agent.runtime-drift')) {
|
|
429
|
+
throw new Error('deploy 前置条件不完整: agent.runtime-drift' +
|
|
430
|
+
'(Agent 当前绑定在其他 runtime;显式迁移用 --rebind-runtime,退役用 --retire)');
|
|
130
431
|
}
|
|
131
432
|
appendInspectionCalls(preflight, 'preflight', calls);
|
|
132
433
|
const operationId = `multica_operation_${Date.now()}_${randomUUID().slice(0, 12)}`;
|
|
@@ -144,7 +445,7 @@ export function applyMulticaDeployment(start, name, options = {}, action = 'appl
|
|
|
144
445
|
receipt = retireDeployment(info.root, workspace, source, plan, preflight, operation, command, env, timeoutMs, calls);
|
|
145
446
|
}
|
|
146
447
|
else {
|
|
147
|
-
receipt = executeApply(info.root, workspace, source, plan, preflight, operation, command, env, timeoutMs, positiveTimeout(options.smokeTimeoutMs, 30_000,
|
|
448
|
+
receipt = executeApply(info.root, workspace, source, plan, preflight, operation, command, env, timeoutMs, positiveTimeout(options.smokeTimeoutMs, 30_000, 300_000), Boolean(options.noWait), calls, rollback, temp);
|
|
148
449
|
}
|
|
149
450
|
}
|
|
150
451
|
catch (error) {
|
|
@@ -159,7 +460,7 @@ export function applyMulticaDeployment(start, name, options = {}, action = 'appl
|
|
|
159
460
|
operation.calls = calls;
|
|
160
461
|
operation.failures = [failure.category, ...rollbackReport.failures];
|
|
161
462
|
operation.updatedAt = new Date().toISOString();
|
|
162
|
-
receipt = failureReceipt(info.root, workspace, source, plan, preflight, operation, calls, rollbackReport, status, failure.category);
|
|
463
|
+
receipt = failureReceipt(info.root, workspace, source, plan, preflight, operation, calls, rollbackReport, status, failure.category, failure.detail);
|
|
163
464
|
}
|
|
164
465
|
finally {
|
|
165
466
|
rmSync(temp, { recursive: true, force: true });
|
|
@@ -187,6 +488,7 @@ function executeApply(root, workspace, source, plan, preflight, operation, comma
|
|
|
187
488
|
calls: [...calls],
|
|
188
489
|
agent: { ...previous.agent, action: 'noop' },
|
|
189
490
|
skills: previous.skills.map((skill) => ({ ...skill, action: 'noop' })),
|
|
491
|
+
failureDetails: Array.isArray(previous.failureDetails) ? previous.failureDetails : [],
|
|
190
492
|
evidencePath: receiptRelativePath(workspace.name, receiptId),
|
|
191
493
|
remoteWrite: false,
|
|
192
494
|
sideEffect: 'multica-read',
|
|
@@ -235,9 +537,12 @@ function executeApply(root, workspace, source, plan, preflight, operation, comma
|
|
|
235
537
|
throw new DeploymentFailure('readback.deployment-mismatch');
|
|
236
538
|
}
|
|
237
539
|
const observedHash = deploymentObservedHash(finalSnapshot, readback.inspectionId);
|
|
238
|
-
const smoke = createAndReadSmoke(root, plan, source, agentId, command, env, timeoutMs, smokeTimeoutMs, noWait, calls);
|
|
540
|
+
const smoke = createAndReadSmoke(root, plan, source, agentId, readback.resources.runtime?.provider || '', command, env, timeoutMs, smokeTimeoutMs, noWait, calls);
|
|
541
|
+
// skills-not-visible 是冷沙箱竞态(推送的 Skill 尚未物化),不是真实部署失败:落
|
|
542
|
+
// verifying 而非 failed,避免把一次正常部署误判成失败、把人推去改本体换模板。
|
|
239
543
|
const status = smoke.status === 'passed'
|
|
240
|
-
? 'ready' : smoke.status === 'pending' ? 'verifying'
|
|
544
|
+
? 'ready' : smoke.status === 'pending' ? 'verifying'
|
|
545
|
+
: smoke.classification === 'skills-not-visible' ? 'verifying' : 'failed';
|
|
241
546
|
const receiptId = `multica_receipt_${digest(stableStringify({
|
|
242
547
|
operationId: operation.operationId, planId: plan.planId, observedHash,
|
|
243
548
|
smoke: { markerHash: smoke.markerHash, issueId: smoke.issueId, taskId: smoke.taskId },
|
|
@@ -274,6 +579,13 @@ function executeApply(root, workspace, source, plan, preflight, operation, comma
|
|
|
274
579
|
smoke,
|
|
275
580
|
rollback: emptyRollback(false),
|
|
276
581
|
failures: [...smoke.failures],
|
|
582
|
+
failureDetails: [...smoke.failureDetails],
|
|
583
|
+
...(plan.rebindRuntime && plan.expected.previousRuntimeId ? {
|
|
584
|
+
rebind: {
|
|
585
|
+
fromRuntimeId: plan.expected.previousRuntimeId,
|
|
586
|
+
toRuntimeId: plan.expected.runtimeId,
|
|
587
|
+
},
|
|
588
|
+
} : {}),
|
|
277
589
|
evidencePath: receiptRelativePath(workspace.name, receiptId),
|
|
278
590
|
remoteRead: true,
|
|
279
591
|
remoteWrite: calls.some((call) => call.kind === 'write'),
|
|
@@ -328,6 +640,7 @@ function retireDeployment(root, workspace, source, plan, preflight, operation, c
|
|
|
328
640
|
smoke: emptySmoke('not-run', source.skills.map((item) => item.name)),
|
|
329
641
|
rollback: emptyRollback(false),
|
|
330
642
|
failures: [],
|
|
643
|
+
failureDetails: [],
|
|
331
644
|
evidencePath: receiptRelativePath(workspace.name, receiptId),
|
|
332
645
|
remoteRead: true,
|
|
333
646
|
remoteWrite: calls.some((call) => call.kind === 'write'),
|
|
@@ -344,15 +657,9 @@ function loadDeploymentSource(root) {
|
|
|
344
657
|
const definitionHash = sha256(definition);
|
|
345
658
|
const skillRoots = [];
|
|
346
659
|
for (const name of info.project.skills) {
|
|
347
|
-
const ref = name
|
|
348
|
-
? `.agents/skills/${name}` : `skills/${name}`;
|
|
660
|
+
const ref = projectSkillRef(info.project, name);
|
|
349
661
|
skillRoots.push({ name, root: safeProjectPath(root, ref, true) });
|
|
350
662
|
}
|
|
351
|
-
const packageRoot = resolvePackageRoot(import.meta.url);
|
|
352
|
-
skillRoots.push({
|
|
353
|
-
name: MULTICA_BOOT_SKILL,
|
|
354
|
-
root: safeExternalSkillRoot(bundledSkillPath(packageRoot, MULTICA_BOOT_SKILL)),
|
|
355
|
-
});
|
|
356
663
|
if (skillRoots.length > MAX_DEPLOY_SKILLS) {
|
|
357
664
|
throw new Error(`Multica deploy 最多支持 ${MAX_DEPLOY_SKILLS} 个受管 Skills`);
|
|
358
665
|
}
|
|
@@ -361,26 +668,17 @@ function loadDeploymentSource(root) {
|
|
|
361
668
|
if (new Set(skills.map((item) => item.name)).size !== skills.length) {
|
|
362
669
|
throw new Error('Multica deploy required Skills 不能重名');
|
|
363
670
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
`deployment_sha256=${deploymentSourceHash}`,
|
|
370
|
-
`required_skills=${JSON.stringify(required)}`,
|
|
371
|
-
'Before every task, use dingtalk-agent-boot-multica and follow its startup order.',
|
|
372
|
-
'Never create a robot, webhook, autopilot, schedule, or other Trigger as part of deployment.',
|
|
373
|
-
'<!-- /DTA Multica Boot -->',
|
|
374
|
-
'',
|
|
375
|
-
definition.trimEnd(),
|
|
376
|
-
'',
|
|
377
|
-
].join('\n');
|
|
671
|
+
// Agent instructions are the human-authored Definition, byte for byte. Deployment
|
|
672
|
+
// hashes and the managed Skill set belong to the control plane and its Receipt;
|
|
673
|
+
// injecting them into the System Prompt makes the role unreadable and creates a
|
|
674
|
+
// second runtime protocol beside the Host's native Skill assignment.
|
|
675
|
+
const instructions = definition;
|
|
378
676
|
const instructionsHash = sha256(instructions);
|
|
379
677
|
const deploymentHash = digest(stableStringify({
|
|
380
678
|
definitionHash, instructionsHash,
|
|
381
679
|
skills: skills.map((item) => ({ name: item.name, hash: item.hash })),
|
|
382
680
|
}));
|
|
383
|
-
return {
|
|
681
|
+
return { definitionHash, instructions, instructionsHash, skills, deploymentHash };
|
|
384
682
|
}
|
|
385
683
|
function loadSkillBundle(name, root) {
|
|
386
684
|
const actual = realpathSync(root);
|
|
@@ -557,73 +855,277 @@ function upsertSkillFile(root, plan, skillId, path, contentFile, command, env, t
|
|
|
557
855
|
'--content-file', contentFile, '--output', 'json',
|
|
558
856
|
]), `skill.file.upsert:${path}`, 'write', root, env, timeoutMs, calls);
|
|
559
857
|
}
|
|
560
|
-
function createAndReadSmoke(root, plan, source, agentId, command, env, timeoutMs, smokeTimeoutMs, noWait, calls) {
|
|
858
|
+
function createAndReadSmoke(root, plan, source, agentId, runtimeProvider, command, env, timeoutMs, smokeTimeoutMs, noWait, calls) {
|
|
561
859
|
const marker = `DTA-MULTICA-LOAD-${randomUUID()}`;
|
|
562
860
|
const requiredSkills = source.skills.map((item) => item.name).sort();
|
|
861
|
+
const expectedResponse = JSON.stringify({
|
|
862
|
+
schema: 'dta-multica-load-smoke@1', marker, loaded: requiredSkills,
|
|
863
|
+
});
|
|
563
864
|
const issue = jsonObject(runProvider(command, scopedArgs(plan, [
|
|
564
865
|
'issue', 'create', '--title', `[DTA load smoke] ${marker}`,
|
|
565
866
|
'--description', [
|
|
566
867
|
'DTA_MULTICA_LOAD_SMOKE@1', `marker=${marker}`,
|
|
567
|
-
|
|
568
|
-
'
|
|
868
|
+
`required_skills=${JSON.stringify(requiredSkills)}`,
|
|
869
|
+
'This is a deployment control-plane verification task.',
|
|
870
|
+
'Load every required skill above through the Host skill mechanism, in the listed order.',
|
|
871
|
+
'Do not call DWS, network, or business tools. Only the Host Issue control-plane calls needed to read this Issue and persist the exact result are exempt.',
|
|
872
|
+
`Post exactly this JSON as an Issue comment (and nothing else): ${expectedResponse}`,
|
|
569
873
|
].join('\n'),
|
|
570
874
|
'--assignee-id', agentId, '--output', 'json',
|
|
571
875
|
]), 'smoke.issue.create', 'write', root, env, timeoutMs, calls).stdout, 'smoke issue');
|
|
572
876
|
const issueId = resourceId(issue.id, 'smoke issue.id');
|
|
877
|
+
const read = () => readSmoke(root, plan, marker, issueId, agentId, requiredSkills, runtimeProvider, command, env, timeoutMs, calls);
|
|
878
|
+
let smoke = read();
|
|
879
|
+
// Multica persists the Issue before attempting the initial agent enqueue. If
|
|
880
|
+
// that enqueue loses a transient readiness race, the API still returns a
|
|
881
|
+
// valid assigned todo Issue but issue runs remains empty forever. Recover
|
|
882
|
+
// exactly that observable state once through an explicit ID-bound mention on
|
|
883
|
+
// the same Issue. A visible task — queued, running or terminal — is never
|
|
884
|
+
// duplicated. The mention route is used instead of issue rerun because it is
|
|
885
|
+
// the platform's normal first-run trigger and does not require a prior task.
|
|
886
|
+
if (smoke.status === 'pending' && !smoke.taskId) {
|
|
887
|
+
try {
|
|
888
|
+
runProvider(command, scopedArgs(plan, [
|
|
889
|
+
'issue', 'comment', 'add', issueId,
|
|
890
|
+
'--content', `[@DTA load smoke](mention://agent/${agentId}) Execute the deployment verification exactly as specified in this Issue description.`,
|
|
891
|
+
'--output', 'json',
|
|
892
|
+
]), 'smoke.issue.recover', 'write', root, env, timeoutMs, calls);
|
|
893
|
+
smoke = read();
|
|
894
|
+
}
|
|
895
|
+
catch {
|
|
896
|
+
// 与重试 mention 同一不变量:部署本体已回读验证,一次评论写入的抖动只让 smoke
|
|
897
|
+
// 停在 pending(收敛为 verifying,由 --status 收口),不允许升级为回滚。
|
|
898
|
+
}
|
|
899
|
+
}
|
|
573
900
|
if (noWait)
|
|
574
|
-
return
|
|
901
|
+
return smoke;
|
|
575
902
|
const started = Date.now();
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
903
|
+
const pollToTerminal = () => {
|
|
904
|
+
while (smoke.status === 'pending' && Date.now() - started < smokeTimeoutMs) {
|
|
905
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2_000);
|
|
906
|
+
smoke = read();
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
pollToTerminal();
|
|
910
|
+
// 冷沙箱竞态:推送的 Skill 要数分钟才对运行时可见,任务报 `Skill not found` 不是
|
|
911
|
+
// 真失败。在预算内用 mention 触发新 task(新沙箱有机会看到新 Skill),有限次重试。
|
|
912
|
+
// 默认 30s 预算下基本轮不到重试;--wait / --smoke-timeout 放宽后才生效。
|
|
913
|
+
const retryDelayMs = Math.min(20_000, Math.max(500, Math.floor(smokeTimeoutMs / 12)));
|
|
914
|
+
let retries = 0;
|
|
915
|
+
while (smoke.status === 'failed' && smoke.classification === 'skills-not-visible' &&
|
|
916
|
+
retries < 3 && Date.now() - started < smokeTimeoutMs) {
|
|
917
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, retryDelayMs);
|
|
918
|
+
try {
|
|
919
|
+
runProvider(command, scopedArgs(plan, [
|
|
920
|
+
'issue', 'comment', 'add', issueId,
|
|
921
|
+
'--content', `[@DTA load smoke](mention://agent/${agentId}) Skills were not yet visible in the sandbox; re-run the deployment verification exactly as specified in this Issue description.`,
|
|
922
|
+
'--output', 'json',
|
|
923
|
+
]), 'smoke.issue.retry', 'write', root, env, timeoutMs, calls);
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
// 一次评论写入的抖动不允许升级为对已验证部署的回滚:失败已记入 calls,
|
|
927
|
+
// 停止重试并保持当前 smoke 判定(skills-not-visible → verifying)。
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
retries += 1;
|
|
931
|
+
// mention 触发的新 run 在平台上是异步创建的:只有当最新 run 的 taskId 变化后
|
|
932
|
+
// 才重新判定;等不到新 run 就停手——绝不因为还读到旧终态 run 而重复 mention。
|
|
933
|
+
const staleTaskId = smoke.taskId;
|
|
934
|
+
let sawNewRun = false;
|
|
935
|
+
while (Date.now() - started < smokeTimeoutMs) {
|
|
936
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2_000);
|
|
937
|
+
smoke = read();
|
|
938
|
+
if (smoke.taskId && smoke.taskId !== staleTaskId) {
|
|
939
|
+
sawNewRun = true;
|
|
940
|
+
break;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (!sawNewRun)
|
|
944
|
+
break;
|
|
945
|
+
pollToTerminal();
|
|
946
|
+
}
|
|
947
|
+
if (smoke.status === 'pending' && !smoke.taskDiagnostic) {
|
|
948
|
+
const detail = smoke.taskId
|
|
949
|
+
? `task ${smoke.taskId} 在 ${smokeTimeoutMs}ms 内未进入终态(当前 ${smoke.taskStatus || 'unknown'})`
|
|
950
|
+
: `Issue ${issueId} 在 ${smokeTimeoutMs}ms 内未创建关联 task`;
|
|
951
|
+
return { ...smoke, taskDiagnostic: detail };
|
|
580
952
|
}
|
|
581
953
|
return smoke;
|
|
582
954
|
}
|
|
583
|
-
|
|
955
|
+
/**
|
|
956
|
+
* Judge a smoke run from positive evidence instead of a tool-name whitelist. The old judge
|
|
957
|
+
* required every tool_use to match hardcoded `skill`/`write`/`bash` shapes, so a healthy
|
|
958
|
+
* cloud runtime emitting `read_file`/`terminal` could never reach ready — and one extra
|
|
959
|
+
* harmless tool failed the whole trace. Now: each required skill needs load evidence
|
|
960
|
+
* (vocabulary tool_use or host-generated `**Skill loaded**` tool_result), the response is
|
|
961
|
+
* taken from the Issue comments themselves first (platform readback, vocabulary-independent),
|
|
962
|
+
* and only the explicit dws denylist is fatal. Unknown tool names are recorded, not judged.
|
|
963
|
+
*/
|
|
964
|
+
function readSmoke(root, plan, marker, issueId, agentId, requiredSkills, runtimeProvider, command, env, timeoutMs, calls) {
|
|
965
|
+
const provider = runtimeProvider || '';
|
|
966
|
+
const registered = vocabulariesForRuntimeProvider(provider);
|
|
967
|
+
const active = registered.length
|
|
968
|
+
? registered : allRuntimeVocabularies();
|
|
969
|
+
const vocabularyIds = active.map((item) => item.id);
|
|
970
|
+
const base = {
|
|
971
|
+
...emptySmoke('pending', requiredSkills), marker, markerHash: sha256(marker), issueId,
|
|
972
|
+
runtimeProvider: provider, vocabularyIds,
|
|
973
|
+
};
|
|
584
974
|
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');
|
|
585
975
|
const candidates = runs.filter((item) => !optionalString(item.agent_id) ||
|
|
586
976
|
optionalString(item.agent_id) === agentId);
|
|
587
977
|
const run = candidates.sort((a, b) => optionalString(b.created_at).localeCompare(optionalString(a.created_at)))[0];
|
|
588
978
|
if (!run)
|
|
589
|
-
return
|
|
590
|
-
...emptySmoke('pending', requiredSkills), marker, markerHash: sha256(marker), issueId,
|
|
591
|
-
};
|
|
979
|
+
return base;
|
|
592
980
|
const taskId = resourceId(run.id, 'smoke task.id');
|
|
593
981
|
const taskStatus = optionalString(run.status);
|
|
594
982
|
if (!['completed', 'failed', 'cancelled'].includes(taskStatus))
|
|
595
983
|
return {
|
|
596
|
-
...
|
|
984
|
+
...base, taskId, taskStatus,
|
|
597
985
|
};
|
|
598
986
|
const messages = jsonArray(runProvider(command, scopedArgs(plan, [
|
|
599
987
|
'issue', 'run-messages', taskId, '--issue', issueId, '--output', 'json',
|
|
600
988
|
]), 'smoke.issue.messages', 'read', root, env, timeoutMs, calls).stdout, 'smoke messages');
|
|
601
989
|
const toolUses = messages.filter((item) => optionalString(item.type) === 'tool_use');
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
990
|
+
const toolNames = [...new Set(toolUses.map((item) => optionalString(item.tool))
|
|
991
|
+
.filter(Boolean))].sort();
|
|
992
|
+
// Skill-load evidence: a native skill tool_use (the host mechanism itself), or the
|
|
993
|
+
// host-generated `**Skill loaded**` banner in a skill-capable tool's tool_result. Two
|
|
994
|
+
// things are deliberately NOT evidence: model text (a reply can claim anything), and
|
|
995
|
+
// load ATTEMPTS — a failed cold-sandbox load emits the same "Loading skill" tool_use
|
|
996
|
+
// before its error result, and a terminal/bash result echoes whatever the model printed.
|
|
997
|
+
const evidence = new Set();
|
|
998
|
+
const attempted = new Set();
|
|
999
|
+
const evidenceTools = new Set();
|
|
1000
|
+
const bannerTools = new Set(active.flatMap((item) => [...item.skillResultTools]));
|
|
1001
|
+
for (const item of messages) {
|
|
1002
|
+
const type = optionalString(item.type);
|
|
1003
|
+
const tool = optionalString(item.tool);
|
|
1004
|
+
if (type === 'tool_use') {
|
|
1005
|
+
const message = { tool, input: smokeToolInput(item) };
|
|
1006
|
+
for (const vocabulary of active) {
|
|
1007
|
+
const name = vocabulary.skillLoadFromToolUse(message);
|
|
1008
|
+
if (name) {
|
|
1009
|
+
evidence.add(name);
|
|
1010
|
+
evidenceTools.add(tool);
|
|
1011
|
+
}
|
|
1012
|
+
const attempt = vocabulary.skillAttemptFromToolUse(message);
|
|
1013
|
+
if (attempt) {
|
|
1014
|
+
attempted.add(attempt);
|
|
1015
|
+
evidenceTools.add(tool);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
else if (type === 'tool_result' && bannerTools.has(tool)) {
|
|
1020
|
+
for (const name of skillsLoadedInContent(optionalString(item.content))) {
|
|
1021
|
+
evidence.add(name);
|
|
1022
|
+
evidenceTools.add(tool);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const loadedSkills = requiredSkills.filter((name) => evidence.has(name)).sort();
|
|
1027
|
+
const skillEvidencePassed = loadedSkills.length === requiredSkills.length;
|
|
1028
|
+
// dws is the DingTalk side-effect boundary: as a named tool or shelled out in a command.
|
|
1029
|
+
const commandTexts = toolUses.flatMap((item) => {
|
|
1030
|
+
const message = {
|
|
1031
|
+
tool: optionalString(item.tool), input: smokeToolInput(item),
|
|
1032
|
+
};
|
|
1033
|
+
return active.map((vocabulary) => vocabulary.commandText(message)).filter(Boolean);
|
|
1034
|
+
});
|
|
1035
|
+
const forbiddenTools = toolNames.filter((name) => forbiddenSmokeTool(name));
|
|
1036
|
+
const forbiddenCommands = commandTexts.some((text) => commandInvokesDws(text));
|
|
1037
|
+
const knownTools = new Set(active.flatMap((item) => [...item.tools]));
|
|
1038
|
+
const unrecognizedTools = toolNames.filter((name) => !knownTools.has(name) && !evidenceTools.has(name) && !forbiddenSmokeTool(name));
|
|
1039
|
+
// Response evidence, most stable source first: the Issue's own comments (platform
|
|
1040
|
+
// readback — no tool vocabulary involved), then trace texts, writes and shell heredocs.
|
|
1041
|
+
const matchesResponse = (value) => smokeResponseMatches(value, marker, requiredSkills);
|
|
1042
|
+
let commentContents = [];
|
|
1043
|
+
try {
|
|
1044
|
+
commentContents = jsonArray(runProvider(command, scopedArgs(plan, [
|
|
1045
|
+
'issue', 'comment', 'list', issueId, '--recent', '20', '--output', 'json',
|
|
1046
|
+
]), 'smoke.issue.comments', 'read', root, env, timeoutMs, calls).stdout, 'smoke comments').map((item) => optionalString(item.content)).filter(Boolean);
|
|
1047
|
+
}
|
|
1048
|
+
catch {
|
|
1049
|
+
commentContents = []; // 旧版 provider CLI 可能没有 comment list;回退 trace 内取证
|
|
1050
|
+
}
|
|
606
1051
|
const texts = messages.filter((item) => optionalString(item.type) === 'text')
|
|
607
1052
|
.map((item) => optionalString(item.content)).filter(Boolean);
|
|
608
1053
|
const finalText = texts.at(-1) || '';
|
|
609
|
-
const
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
1054
|
+
const responseWrites = [];
|
|
1055
|
+
const responseShells = [];
|
|
1056
|
+
for (const item of toolUses) {
|
|
1057
|
+
const message = {
|
|
1058
|
+
tool: optionalString(item.tool), input: smokeToolInput(item),
|
|
1059
|
+
};
|
|
1060
|
+
for (const vocabulary of active) {
|
|
1061
|
+
const write = vocabulary.writeEvidence(message);
|
|
1062
|
+
if (write && safeSmokeResponsePath(write.path))
|
|
1063
|
+
responseWrites.push(write.content);
|
|
1064
|
+
const text = vocabulary.commandText(message);
|
|
1065
|
+
if (text && safeCombinedSmokeResponse(text, issueId, marker, requiredSkills)) {
|
|
1066
|
+
responseShells.push(JSON.stringify({
|
|
1067
|
+
schema: 'dta-multica-load-smoke@1', marker, loaded: requiredSkills,
|
|
1068
|
+
}));
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
const responseEvidence = [...commentContents, ...texts, ...responseWrites, ...responseShells]
|
|
1073
|
+
.find(matchesResponse) || '';
|
|
614
1074
|
const responsePassed = Boolean(responseEvidence);
|
|
615
|
-
const toolTracePassed =
|
|
1075
|
+
const toolTracePassed = skillEvidencePassed && !forbiddenTools.length && !forbiddenCommands;
|
|
616
1076
|
const failures = [];
|
|
1077
|
+
const failureDetails = [];
|
|
617
1078
|
if (taskStatus !== 'completed')
|
|
618
1079
|
failures.push(`smoke.task.${taskStatus || 'unknown'}`);
|
|
619
|
-
if (!
|
|
620
|
-
failures.push('smoke.skill-
|
|
621
|
-
|
|
1080
|
+
if (!skillEvidencePassed) {
|
|
1081
|
+
failures.push('smoke.skill-load-evidence');
|
|
1082
|
+
const attemptedMissing = requiredSkills.filter((name) => attempted.has(name) && !evidence.has(name));
|
|
1083
|
+
failureDetails.push({
|
|
1084
|
+
code: 'smoke.skill-load-evidence',
|
|
1085
|
+
expected: requiredSkills,
|
|
1086
|
+
actual: loadedSkills,
|
|
1087
|
+
hint: attemptedMissing.length
|
|
1088
|
+
? `有装载尝试但无成功证据(tool_result 缺 **Skill loaded**): ${attemptedMissing.join(', ')};` +
|
|
1089
|
+
'尝试不算装载成功'
|
|
1090
|
+
: unrecognizedTools.length
|
|
1091
|
+
? `轨迹里有词汇表不认识的工具名: ${unrecognizedTools.join(', ')};` +
|
|
1092
|
+
'若属新 runtime 词汇,需要在 multica-runtime-vocabulary 注册'
|
|
1093
|
+
: '轨迹中没有这些 Skill 的装载证据(tool_result 的 **Skill loaded** 或 Skill 工具调用)',
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
for (const tool of forbiddenTools)
|
|
1097
|
+
failures.push(`smoke.forbidden-tool:${tool}`);
|
|
1098
|
+
if (forbiddenCommands)
|
|
1099
|
+
failures.push('smoke.forbidden-command:dws');
|
|
1100
|
+
if (!responsePassed) {
|
|
622
1101
|
failures.push('smoke.response');
|
|
1102
|
+
failureDetails.push({
|
|
1103
|
+
code: 'smoke.response',
|
|
1104
|
+
actual: `comments=${commentContents.length} texts=${texts.length} writes=${responseWrites.length}`,
|
|
1105
|
+
hint: '未在 Issue comment、回复文本或落盘文件中找到精确的 dta-multica-load-smoke@1 JSON',
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
// 分类冷沙箱竞态:任务终态且缺装载证据,轨迹里出现 skill not found 类措辞 → 可重试,
|
|
1109
|
+
// 与「配置/模板真错了」区分开(B7 的两个误导方向都来自不区分这两态)。
|
|
1110
|
+
let classification = '';
|
|
1111
|
+
if (!skillEvidencePassed) {
|
|
1112
|
+
const searchable = messages.map((item) => optionalString(item.content)).join('\n');
|
|
1113
|
+
if (/skill[^\n]{0,80}?not\s+found|not\s+found[^\n]{0,60}?skill|skill\s+view\s+failed/i.test(searchable)) {
|
|
1114
|
+
classification = 'skills-not-visible';
|
|
1115
|
+
failures.unshift('smoke.skills-not-visible');
|
|
1116
|
+
failureDetails.unshift({
|
|
1117
|
+
code: 'smoke.skills-not-visible',
|
|
1118
|
+
hint: '推送的 Skill 尚未物化进沙箱(冷启动竞态,通常数分钟内自愈);' +
|
|
1119
|
+
'用 deploy --status --execute --yes 重试,或部署时用 --wait 放宽预算',
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
623
1123
|
return {
|
|
624
|
-
status: failures.length ? 'failed' : 'passed',
|
|
625
|
-
|
|
1124
|
+
status: failures.length ? 'failed' : 'passed', marker, markerHash: sha256(marker),
|
|
1125
|
+
issueId, taskId, taskStatus, taskDiagnostic: remoteTaskDiagnostic(run), requiredSkills, loadedSkills,
|
|
626
1126
|
toolTracePassed, responsePassed, responseHash: sha256(responseEvidence || finalText), failures,
|
|
1127
|
+
runtimeProvider: provider, vocabularyIds, toolNames, unrecognizedTools, classification,
|
|
1128
|
+
failureDetails,
|
|
627
1129
|
};
|
|
628
1130
|
}
|
|
629
1131
|
function smokeToolInput(item) {
|
|
@@ -641,48 +1143,25 @@ function smokeResponseMatches(value, marker, requiredSkills) {
|
|
|
641
1143
|
return false;
|
|
642
1144
|
}
|
|
643
1145
|
}
|
|
644
|
-
function
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
653
|
-
if (tool !== 'bash')
|
|
654
|
-
return false;
|
|
655
|
-
const command = optionalString(input.command);
|
|
656
|
-
if (new Set([
|
|
657
|
-
`multica issue get ${issueId} --output json`,
|
|
658
|
-
`multica issue metadata list ${issueId} --output json`,
|
|
659
|
-
`multica issue comment list ${issueId} --recent 10 --output json`,
|
|
660
|
-
`multica issue status ${issueId} in_progress`,
|
|
661
|
-
`multica issue status ${issueId} in_review`,
|
|
662
|
-
]).has(command))
|
|
663
|
-
return true;
|
|
664
|
-
const commentPrefix = `multica issue comment add ${issueId} --content-file `;
|
|
665
|
-
if (command.startsWith(commentPrefix)) {
|
|
666
|
-
return safeSmokeReplyPath(command.slice(commentPrefix.length));
|
|
667
|
-
}
|
|
668
|
-
const cleanupFirstPrefix = 'rm ';
|
|
669
|
-
const cleanupFirstSuffix = ` && multica issue status ${issueId} in_review`;
|
|
670
|
-
if (command.startsWith(cleanupFirstPrefix) && command.endsWith(cleanupFirstSuffix)) {
|
|
671
|
-
return safeSmokeReplyPath(command.slice(cleanupFirstPrefix.length, command.length - cleanupFirstSuffix.length));
|
|
672
|
-
}
|
|
673
|
-
const statusFirstPrefix = `multica issue status ${issueId} in_review && rm `;
|
|
674
|
-
if (command.startsWith(statusFirstPrefix)) {
|
|
675
|
-
return safeSmokeReplyPath(command.slice(statusFirstPrefix.length));
|
|
676
|
-
}
|
|
677
|
-
if (command.startsWith(cleanupFirstPrefix)) {
|
|
678
|
-
return safeSmokeReplyPath(command.slice(cleanupFirstPrefix.length));
|
|
679
|
-
}
|
|
680
|
-
return false;
|
|
1146
|
+
function safeCombinedSmokeResponse(command, issueId, marker, requiredSkills) {
|
|
1147
|
+
const response = JSON.stringify({ schema: 'dta-multica-load-smoke@1', marker, loaded: requiredSkills });
|
|
1148
|
+
return command === [
|
|
1149
|
+
"cat > ./reply.md << 'REPLY_EOF'",
|
|
1150
|
+
response,
|
|
1151
|
+
'REPLY_EOF',
|
|
1152
|
+
`multica issue comment add ${issueId} --content-file ./reply.md && rm ./reply.md`,
|
|
1153
|
+
].join('\n');
|
|
681
1154
|
}
|
|
682
|
-
function
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
1155
|
+
function safeSmokeResponsePath(value) {
|
|
1156
|
+
const names = new Set(['reply.md', 'result.md', 'result.json']);
|
|
1157
|
+
if (value.startsWith('./'))
|
|
1158
|
+
return names.has(value.slice(2));
|
|
1159
|
+
// 裸文件名与 `./` 前缀等价:Agent 在 workdir 里写绝对路径、随后用裸名引用同一个文件是
|
|
1160
|
+
// 平台的常见写法。只放行精确 basename —— `foo/result.md` 与 `../result.md` 仍然落到
|
|
1161
|
+
// 下面的绝对路径分支并被拒绝,白名单没有变宽。
|
|
1162
|
+
if (!value.startsWith('/'))
|
|
1163
|
+
return names.has(value);
|
|
1164
|
+
if (!names.has(value.split('/').at(-1) || ''))
|
|
686
1165
|
return false;
|
|
687
1166
|
return value.split('/').every((part) => !part ||
|
|
688
1167
|
(part !== '.' && part !== '..' && /^[A-Za-z0-9._-]+$/.test(part)));
|
|
@@ -800,7 +1279,7 @@ function verifyRollback(root, plan, command, env, timeoutMs, calls, rollback, re
|
|
|
800
1279
|
});
|
|
801
1280
|
}
|
|
802
1281
|
}
|
|
803
|
-
function failureReceipt(root, workspace, source, plan, preflight, operation, calls, rollback, status, failure) {
|
|
1282
|
+
function failureReceipt(root, workspace, source, plan, preflight, operation, calls, rollback, status, failure, failureDetail) {
|
|
804
1283
|
const receiptId = `multica_receipt_${digest(stableStringify({
|
|
805
1284
|
operationId: operation.operationId, status, failure,
|
|
806
1285
|
})).slice(0, 24)}`;
|
|
@@ -838,6 +1317,7 @@ function failureReceipt(root, workspace, source, plan, preflight, operation, cal
|
|
|
838
1317
|
smoke: emptySmoke('not-run', source.skills.map((item) => item.name)),
|
|
839
1318
|
rollback,
|
|
840
1319
|
failures: [failure, ...rollback.failures],
|
|
1320
|
+
failureDetails: failureDetail ? [failureDetail] : [],
|
|
841
1321
|
evidencePath: receiptRelativePath(workspace.name, receiptId),
|
|
842
1322
|
remoteRead: true,
|
|
843
1323
|
remoteWrite: calls.some((call) => call.kind === 'write' || call.kind === 'rollback'),
|
|
@@ -857,6 +1337,7 @@ export function statusMulticaDeployment(start, name, options = {}) {
|
|
|
857
1337
|
workspace: name, provider: 'multica', available: false, operationId: '', receiptId: '',
|
|
858
1338
|
status: 'missing', passed: false, providerReady: false,
|
|
859
1339
|
operationIntegrity: false, receiptIntegrity: false, reconciled: false,
|
|
1340
|
+
receiptFrozen: false, smokeStale: false, nextAction: '',
|
|
860
1341
|
failures: ['deployment.missing'], receipt: null,
|
|
861
1342
|
remoteRead: false, remoteWrite: false, triggerWrite: false,
|
|
862
1343
|
sideEffect: false, dingtalkSideEffect: false,
|
|
@@ -864,6 +1345,7 @@ export function statusMulticaDeployment(start, name, options = {}) {
|
|
|
864
1345
|
let { operation, receipt } = loaded;
|
|
865
1346
|
let reconciled = false;
|
|
866
1347
|
let remoteRead = false;
|
|
1348
|
+
let remoteWrite = false;
|
|
867
1349
|
if (options.execute) {
|
|
868
1350
|
if (!options.yes)
|
|
869
1351
|
throw new Error('deploy --status 远端 reconcile 必须同时传 --execute --yes');
|
|
@@ -872,12 +1354,41 @@ export function statusMulticaDeployment(start, name, options = {}) {
|
|
|
872
1354
|
const timeoutMs = positiveTimeout(options.timeoutMs, 30_000, 300_000);
|
|
873
1355
|
if (receipt.status === 'verifying' && receipt.smoke.issueId) {
|
|
874
1356
|
remoteRead = true;
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1357
|
+
let smokeReadback = readSmokeFromReceipt(info.root, receipt, command, env, timeoutMs);
|
|
1358
|
+
let smoke = smokeReadback.smoke;
|
|
1359
|
+
let calls = smokeReadback.calls;
|
|
1360
|
+
// 冷沙箱竞态的收口入口:终态失败但分类为 skills-not-visible 时,补一次 mention
|
|
1361
|
+
// 触发新 task(唯一的写;如实上报 remoteWrite/sideEffect)。新 run 是异步创建的:
|
|
1362
|
+
// 以 taskId 变化为「新 run 出现」的判据再判定,等不到就按旧判定收场(仍 verifying),
|
|
1363
|
+
// 不再重复 mention。
|
|
1364
|
+
if (smoke.status === 'failed' && smoke.classification === 'skills-not-visible' &&
|
|
1365
|
+
receipt.agent.id) {
|
|
1366
|
+
const planLike = {
|
|
1367
|
+
profile: receipt.profile, expected: { workspaceId: operation.workspaceId },
|
|
1368
|
+
};
|
|
1369
|
+
const staleTaskId = smoke.taskId;
|
|
1370
|
+
runProvider(command, scopedArgs(planLike, [
|
|
1371
|
+
'issue', 'comment', 'add', receipt.smoke.issueId,
|
|
1372
|
+
'--content', `[@DTA load smoke](mention://agent/${receipt.agent.id}) Skills were not yet visible in the sandbox; re-run the deployment verification exactly as specified in this Issue description.`,
|
|
1373
|
+
'--output', 'json',
|
|
1374
|
+
]), 'smoke.issue.retry', 'write', info.root, env, timeoutMs, calls);
|
|
1375
|
+
remoteWrite = true;
|
|
1376
|
+
const deadline = Date.now() + timeoutMs;
|
|
1377
|
+
do {
|
|
1378
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2_000);
|
|
1379
|
+
smokeReadback = readSmokeFromReceipt(info.root, { ...receipt, calls }, command, env, timeoutMs);
|
|
1380
|
+
smoke = smokeReadback.smoke;
|
|
1381
|
+
calls = smokeReadback.calls;
|
|
1382
|
+
} while ((smoke.status === 'pending' ||
|
|
1383
|
+
(Boolean(staleTaskId) && smoke.taskId === staleTaskId)) && Date.now() < deadline);
|
|
1384
|
+
}
|
|
1385
|
+
const nextStatus = smoke.status === 'passed' ? 'ready'
|
|
1386
|
+
: smoke.status === 'pending' ? 'verifying'
|
|
1387
|
+
: smoke.classification === 'skills-not-visible' ? 'verifying' : 'failed';
|
|
1388
|
+
receipt = { ...receipt, calls, smoke, status: nextStatus,
|
|
879
1389
|
passed: smoke.status === 'passed', providerReady: smoke.status === 'passed',
|
|
880
|
-
failures: [...smoke.failures]
|
|
1390
|
+
failures: [...smoke.failures],
|
|
1391
|
+
failureDetails: [...smoke.failureDetails] };
|
|
881
1392
|
reconciled = true;
|
|
882
1393
|
persistReceiptAndState(info.root, showDevelopmentWorkspace(info.root, name), operation, receipt);
|
|
883
1394
|
}
|
|
@@ -911,6 +1422,7 @@ export function statusMulticaDeployment(start, name, options = {}) {
|
|
|
911
1422
|
}
|
|
912
1423
|
operation = loadOperationAndReceipt(info.root, name, operation.operationId)?.operation || operation;
|
|
913
1424
|
}
|
|
1425
|
+
const receiptFrozen = !options.execute;
|
|
914
1426
|
return {
|
|
915
1427
|
$schema: 'dingtalk-agent/multica-deployment-status@1',
|
|
916
1428
|
workspace: name,
|
|
@@ -924,12 +1436,17 @@ export function statusMulticaDeployment(start, name, options = {}) {
|
|
|
924
1436
|
operationIntegrity: true,
|
|
925
1437
|
receiptIntegrity: true,
|
|
926
1438
|
reconciled,
|
|
1439
|
+
receiptFrozen,
|
|
1440
|
+
smokeStale: receiptFrozen && receipt.status === 'verifying',
|
|
1441
|
+
nextAction: ['verifying', 'reconciling'].includes(receipt.status)
|
|
1442
|
+
? `dta deploy --workspace ${name} --status --operation-id ${operation.operationId} --execute --yes`
|
|
1443
|
+
: '',
|
|
927
1444
|
failures: [...receipt.failures],
|
|
928
1445
|
receipt,
|
|
929
1446
|
remoteRead,
|
|
930
|
-
remoteWrite
|
|
1447
|
+
remoteWrite,
|
|
931
1448
|
triggerWrite: false,
|
|
932
|
-
sideEffect: remoteRead ? 'multica-read' : false,
|
|
1449
|
+
sideEffect: remoteWrite ? 'multica-read-write' : remoteRead ? 'multica-read' : false,
|
|
933
1450
|
dingtalkSideEffect: false,
|
|
934
1451
|
};
|
|
935
1452
|
}
|
|
@@ -1055,7 +1572,7 @@ function readSmokeFromReceipt(root, receipt, command, env, timeoutMs) {
|
|
|
1055
1572
|
failures: ['smoke.marker-not-recoverable'],
|
|
1056
1573
|
} };
|
|
1057
1574
|
}
|
|
1058
|
-
const smoke = readSmoke(root, plan, markerValue, receipt.smoke.issueId, receipt.agent.id, receipt.smoke.requiredSkills, command, env, timeoutMs, calls);
|
|
1575
|
+
const smoke = readSmoke(root, plan, markerValue, receipt.smoke.issueId, receipt.agent.id, receipt.smoke.requiredSkills, receipt.smoke.runtimeProvider || '', command, env, timeoutMs, calls);
|
|
1059
1576
|
return { smoke, calls };
|
|
1060
1577
|
}
|
|
1061
1578
|
function persistReceiptAndState(root, workspace, operation, receipt) {
|
|
@@ -1225,7 +1742,7 @@ function runProvider(command, argv, id, kind, cwd, env, timeoutMs, calls) {
|
|
|
1225
1742
|
throw new DeploymentFailure('budget.rollback-exhausted');
|
|
1226
1743
|
}
|
|
1227
1744
|
const run = spawnSync(command, argv, {
|
|
1228
|
-
cwd, env, encoding: 'utf8', timeout: timeoutMs,
|
|
1745
|
+
cwd, env: sanitizeProviderEnv(env).env, encoding: 'utf8', timeout: timeoutMs,
|
|
1229
1746
|
maxBuffer: 32 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'],
|
|
1230
1747
|
});
|
|
1231
1748
|
const stdout = String(run.stdout || '');
|
|
@@ -1237,8 +1754,10 @@ function runProvider(command, argv, id, kind, cwd, env, timeoutMs, calls) {
|
|
|
1237
1754
|
id, kind, argv: redactArgv(argv), exitCode,
|
|
1238
1755
|
stdoutHash: sha256(stdout), stderrHash: sha256(stderr), passed, ambiguous,
|
|
1239
1756
|
});
|
|
1240
|
-
if (!passed)
|
|
1757
|
+
if (!passed) {
|
|
1758
|
+
emitProviderDiagnostic(id, exitCode, stderr || stdout);
|
|
1241
1759
|
throw new DeploymentFailure(`command.${id}`, ambiguous);
|
|
1760
|
+
}
|
|
1242
1761
|
return { stdout, stderr };
|
|
1243
1762
|
}
|
|
1244
1763
|
function scopedArgs(plan, args) {
|
|
@@ -1356,9 +1875,11 @@ function assertReportRedacted(value) {
|
|
|
1356
1875
|
}
|
|
1357
1876
|
function emptySmoke(status, requiredSkills) {
|
|
1358
1877
|
return {
|
|
1359
|
-
status, marker: '', markerHash: '', issueId: '', taskId: '', taskStatus: '',
|
|
1878
|
+
status, marker: '', markerHash: '', issueId: '', taskId: '', taskStatus: '', taskDiagnostic: '',
|
|
1360
1879
|
requiredSkills: [...requiredSkills].sort(), loadedSkills: [],
|
|
1361
1880
|
toolTracePassed: false, responsePassed: false, responseHash: '', failures: [],
|
|
1881
|
+
runtimeProvider: '', vocabularyIds: [], toolNames: [], unrecognizedTools: [],
|
|
1882
|
+
classification: '', failureDetails: [],
|
|
1362
1883
|
};
|
|
1363
1884
|
}
|
|
1364
1885
|
function emptyRollback(attempted) {
|
|
@@ -1432,6 +1953,58 @@ function stringField(value, label) {
|
|
|
1432
1953
|
function optionalString(value) {
|
|
1433
1954
|
return typeof value === 'string' ? value : '';
|
|
1434
1955
|
}
|
|
1956
|
+
function safeRemoteOutput(value, maxBytes) {
|
|
1957
|
+
const redacted = value
|
|
1958
|
+
.replace(/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g, '[redacted-private-key]')
|
|
1959
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+\/-]{20,}/gi, 'Bearer [redacted]')
|
|
1960
|
+
.replace(/\b(?:mul_|mcn_|sk-)[A-Za-z0-9._~+\/-]{12,}/gi, '[redacted-token]');
|
|
1961
|
+
const bytes = Buffer.from(redacted, 'utf8');
|
|
1962
|
+
return bytes.length <= maxBytes ? redacted : `${bytes.subarray(0, maxBytes).toString('utf8')}…`;
|
|
1963
|
+
}
|
|
1964
|
+
function uniqueCalls(calls) {
|
|
1965
|
+
const seen = new Set();
|
|
1966
|
+
return calls.filter((call) => {
|
|
1967
|
+
const key = stableStringify({
|
|
1968
|
+
id: call.id,
|
|
1969
|
+
kind: call.kind,
|
|
1970
|
+
argv: call.argv,
|
|
1971
|
+
exitCode: call.exitCode,
|
|
1972
|
+
stdoutHash: call.stdoutHash,
|
|
1973
|
+
stderrHash: call.stderrHash,
|
|
1974
|
+
passed: call.passed,
|
|
1975
|
+
ambiguous: call.ambiguous,
|
|
1976
|
+
});
|
|
1977
|
+
if (seen.has(key))
|
|
1978
|
+
return false;
|
|
1979
|
+
seen.add(key);
|
|
1980
|
+
return true;
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
function remoteTaskDiagnostic(task) {
|
|
1984
|
+
if (!task)
|
|
1985
|
+
return '';
|
|
1986
|
+
const values = [];
|
|
1987
|
+
const add = (label, value) => {
|
|
1988
|
+
if (typeof value === 'string' && value.trim())
|
|
1989
|
+
values.push(`${label}=${value.trim()}`);
|
|
1990
|
+
else if (typeof value === 'number' || typeof value === 'boolean')
|
|
1991
|
+
values.push(`${label}=${value}`);
|
|
1992
|
+
else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
1993
|
+
const nested = value;
|
|
1994
|
+
for (const key of ['code', 'category', 'message', 'reason', 'detail']) {
|
|
1995
|
+
if (nested[key] !== undefined)
|
|
1996
|
+
add(`${label}.${key}`, nested[key]);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
for (const key of [
|
|
2001
|
+
'error', 'error_message', 'failure_reason', 'status_message', 'reason', 'message', 'detail',
|
|
2002
|
+
]) {
|
|
2003
|
+
if (task[key] !== undefined)
|
|
2004
|
+
add(key, task[key]);
|
|
2005
|
+
}
|
|
2006
|
+
return safeRemoteOutput(values.join('; '), 2_000);
|
|
2007
|
+
}
|
|
1435
2008
|
function resourceId(value, label) {
|
|
1436
2009
|
const item = stringField(value, label);
|
|
1437
2010
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$/.test(item) ||
|
|
@@ -1454,12 +2027,6 @@ function requireMulticaWorkspace(start, name, cliVersion = '') {
|
|
|
1454
2027
|
}
|
|
1455
2028
|
return workspace;
|
|
1456
2029
|
}
|
|
1457
|
-
function safeExternalSkillRoot(path) {
|
|
1458
|
-
if (!existsSync(path) || lstatSync(path).isSymbolicLink()) {
|
|
1459
|
-
throw new Error(`Canonical Multica Boot Skill 不存在或是符号链接: ${path}`);
|
|
1460
|
-
}
|
|
1461
|
-
return realpathSync(path);
|
|
1462
|
-
}
|
|
1463
2030
|
function safeProjectPath(root, ref, mustExist) {
|
|
1464
2031
|
const rootReal = realpathSync(resolve(root));
|
|
1465
2032
|
const candidate = resolve(rootReal, ref);
|