@xdxer/dingtalk-agent 0.1.5-beta.2 → 0.1.5-beta.4
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 +36 -0
- package/README.en.md +92 -63
- package/README.md +92 -63
- package/dist/bin/dingtalk-agent.js +142 -15
- package/dist/bin/dingtalk-agent.js.map +1 -1
- package/dist/src/agent-audit.js +69 -55
- package/dist/src/agent-audit.js.map +1 -1
- package/dist/src/agent-enhance.js +49 -30
- package/dist/src/agent-enhance.js.map +1 -1
- package/dist/src/agent-platform.js +1 -0
- 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 +108 -18
- package/dist/src/development-workspace.js.map +1 -1
- package/dist/src/multica-deploy.js +128 -18
- package/dist/src/multica-deploy.js.map +1 -1
- package/dist/src/multica-provider.js +157 -2
- package/dist/src/multica-provider.js.map +1 -1
- package/dist/src/opencode-provider.js +17 -5
- package/dist/src/opencode-provider.js.map +1 -1
- package/dist/src/schedule-plan.js +105 -0
- package/dist/src/schedule-plan.js.map +1 -0
- package/dist/src/skills.js +2 -0
- package/dist/src/skills.js.map +1 -1
- package/dist/src/workspace.js +11 -6
- package/dist/src/workspace.js.map +1 -1
- package/docs/ARCHITECTURE.md +142 -24
- package/docs/INSTALLATION.md +1 -1
- package/docs/PRIOR-ART.md +4 -0
- package/docs/SELF-TEST.md +1 -1
- 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-workspace-run.schema.json +150 -33
- package/docs/schemas/project.schema.json +27 -0
- package/docs/schemas/workspace-scaffold.schema.json +37 -0
- package/examples/agents/README.md +9 -7
- 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/project-workspace/fake-multica-provider.mjs +43 -8
- package/lab/project-workspace/opencode-provider-suite.json +1 -1
- package/lab/robot-eval/suite.json +1 -1
- package/package.json +2 -2
- package/skills/core/dingtalk-agent-compose/SKILL.md +25 -18
- package/skills/core/dingtalk-agent-compose/assets/AGENTS.template.md +1 -1
- package/skills/core/dingtalk-agent-compose/assets/REPOSITORY.template.md +10 -0
- package/skills/core/dingtalk-agent-compose/assets/agent.bindings.dingtalk-doc.template.json +2 -2
- package/skills/core/dingtalk-agent-compose/assets/agent.bindings.local.template.json +2 -2
- package/skills/core/dingtalk-agent-compose/assets/hosts/opencode/opencode.template.json +2 -1
- package/skills/core/dingtalk-agent-compose/evals/evals.json +1 -1
- package/skills/core/dingtalk-agent-compose/references/agent-definition-contract.md +6 -6
- package/skills/core/dingtalk-agent-compose/references/drive-and-schedules.md +124 -0
- package/skills/core/dingtalk-agent-compose/references/host-loading-contract.md +10 -12
- package/skills/core/dingtalk-agent-compose/references/hosts/claude-code.md +13 -12
- package/skills/core/dingtalk-agent-compose/references/hosts/opencode.md +13 -12
- package/skills/core/dingtalk-basic-behavior/SKILL.md +11 -7
- package/skills/core/dingtalk-basic-behavior/references/event-to-behavior.md +15 -1
- package/skills/core/dingtalk-basic-behavior/references/perception-and-gates.md +5 -0
- package/skills/core/dingtalk-basic-behavior/references/risk-authority-and-privacy.md +12 -0
- package/skills/platforms/deap/PLATFORM.md +30 -1
- package/skills/platforms/multica-dingtalk/PLATFORM.md +25 -0
- package/skills/platforms/multica-dingtalk/dingtalk-agent-deploy-multica/SKILL.md +3 -2
- package/skills/platforms/multica-dingtalk/multica-external/SKILL.md +15 -0
- package/docs/assets/agent-delivery-lifecycle.svg +0 -103
- /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
|
@@ -3,7 +3,7 @@ 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
8
|
import { MULTICA_EVIDENCE_ROOT, inspectMulticaWorkspace, planMulticaWorkspace, statusMulticaWorkspace, } from './multica-provider.js';
|
|
9
9
|
export const MULTICA_DEPLOYMENT_SCHEMA = 'dingtalk-agent/multica-deployment-receipt@1';
|
|
@@ -63,7 +63,11 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
|
|
|
63
63
|
throw new Error(`Multica workspace run 前置条件不完整: ${plan.blocking.join(', ')}`);
|
|
64
64
|
}
|
|
65
65
|
const command = options.command || env.DTA_MULTICA_BIN || 'multica';
|
|
66
|
-
|
|
66
|
+
// A real Issue-channel run does agent work plus issue get/comment/status side-tasks and
|
|
67
|
+
// routinely needs minutes; the old 30s default timed out on almost every honest run. Wait
|
|
68
|
+
// 4 minutes by default (still capped at 5). A run that overruns is resumable via
|
|
69
|
+
// `workspace run --status --run-id` against the same Issue — no duplicate Issue.
|
|
70
|
+
const timeoutMs = positiveTimeout(options.timeoutMs, 240_000, 300_000);
|
|
67
71
|
const calls = [];
|
|
68
72
|
const inspection = inspectMulticaWorkspace(info.root, name, {
|
|
69
73
|
execute: true, yes: true, persist: false, command, env,
|
|
@@ -86,6 +90,18 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
|
|
|
86
90
|
'--description', normalizedPrompt, '--assignee-id', plan.agentId, '--output', 'json',
|
|
87
91
|
]), 'workspace-run.issue.create', 'write', info.root, env, Math.min(timeoutMs, 30_000), calls).stdout, 'workspace run issue');
|
|
88
92
|
const issueId = resourceId(issue.id, 'workspace run issue.id');
|
|
93
|
+
return readbackWorkspaceRun({
|
|
94
|
+
root: info.root, name, plan, issueId, runId, command, env, timeoutMs, calls, out: options.out,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Poll an existing Issue's assignee-agent task to a terminal state, then reconstruct the
|
|
99
|
+
* run report from its trace. Shared by the initial `workspace run` (right after it creates the
|
|
100
|
+
* Issue) and `workspace run --status --run-id` (resume against the SAME Issue, no new write) so
|
|
101
|
+
* a timed-out run reconciles instead of spawning a duplicate Issue.
|
|
102
|
+
*/
|
|
103
|
+
function readbackWorkspaceRun(ctx) {
|
|
104
|
+
const { root, name, plan, issueId, runId, command, env, timeoutMs, calls } = ctx;
|
|
89
105
|
const started = Date.now();
|
|
90
106
|
const pollIntervalMs = 2_000;
|
|
91
107
|
const pollCalls = [];
|
|
@@ -95,7 +111,7 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
|
|
|
95
111
|
pollAttempts += 1;
|
|
96
112
|
const runs = jsonArray(runProvider(command, scopedArgs({ profile: plan.profile, expected: { workspaceId: plan.workspaceId } }, [
|
|
97
113
|
'issue', 'runs', issueId, '--full-id', '--output', 'json',
|
|
98
|
-
]), 'workspace-run.issue.runs', 'read',
|
|
114
|
+
]), 'workspace-run.issue.runs', 'read', root, env, Math.min(timeoutMs, 30_000), pollCalls).stdout, 'workspace run issue runs');
|
|
99
115
|
task = runs.filter((item) => !optionalString(item.agent_id) ||
|
|
100
116
|
optionalString(item.agent_id) === plan.agentId)
|
|
101
117
|
.sort((a, b) => optionalString(b.created_at).localeCompare(optionalString(a.created_at)))[0];
|
|
@@ -111,7 +127,7 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
|
|
|
111
127
|
if (taskId && ['completed', 'failed', 'cancelled'].includes(taskStatus)) {
|
|
112
128
|
messages = jsonArray(runProvider(command, scopedArgs({ profile: plan.profile, expected: { workspaceId: plan.workspaceId } }, [
|
|
113
129
|
'issue', 'run-messages', taskId, '--issue', issueId, '--output', 'json',
|
|
114
|
-
]), 'workspace-run.issue.messages', 'read',
|
|
130
|
+
]), 'workspace-run.issue.messages', 'read', root, env, Math.min(timeoutMs, 30_000), calls).stdout, 'workspace run messages');
|
|
115
131
|
}
|
|
116
132
|
const toolUses = messages.filter((item) => optionalString(item.type) === 'tool_use');
|
|
117
133
|
const toolNames = toolUses.map((item) => optionalString(item.tool)).filter(Boolean);
|
|
@@ -125,7 +141,7 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
|
|
|
125
141
|
const answer = safeRemoteOutput(replyWrites.at(-1) || texts.at(-1) || '', 64 * 1024);
|
|
126
142
|
const issueReadback = jsonObject(runProvider(command, scopedArgs({ profile: plan.profile, expected: { workspaceId: plan.workspaceId } }, [
|
|
127
143
|
'issue', 'get', issueId, '--output', 'json',
|
|
128
|
-
]), 'workspace-run.issue.readback', 'read',
|
|
144
|
+
]), 'workspace-run.issue.readback', 'read', root, env, Math.min(timeoutMs, 30_000), calls).stdout, 'workspace run issue readback');
|
|
129
145
|
const issueStatus = optionalString(issueReadback.status);
|
|
130
146
|
const failures = [];
|
|
131
147
|
if (!task)
|
|
@@ -165,17 +181,88 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
|
|
|
165
181
|
calls,
|
|
166
182
|
evidencePath: '',
|
|
167
183
|
remoteRead: true,
|
|
168
|
-
remoteWrite:
|
|
184
|
+
remoteWrite: calls.some((call) => call.kind === 'write'),
|
|
169
185
|
triggerWrite: false,
|
|
170
186
|
sideEffect: 'multica-issue-read-write',
|
|
171
187
|
dingtalkSideEffect: false,
|
|
172
188
|
};
|
|
173
189
|
const defaultEvidence = join(MULTICA_EVIDENCE_ROOT, safeName(name), 'multica-runs', `${runId}.json`);
|
|
174
|
-
report.evidencePath = relative(
|
|
190
|
+
report.evidencePath = relative(root, safeProjectPath(root, ctx.out || defaultEvidence, false));
|
|
175
191
|
assertReportRedacted(report);
|
|
176
|
-
atomicJson(safeProjectPath(
|
|
192
|
+
atomicJson(safeProjectPath(root, report.evidencePath, false), report);
|
|
177
193
|
return report;
|
|
178
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Resume a `workspace run` by re-reading the SAME Issue's task instead of creating a new one.
|
|
197
|
+
* Loads the persisted run evidence by runId, confirms the workspace scope still resolves to the
|
|
198
|
+
* same target (fail closed on drift), then polls that Issue's assignee-agent task to a terminal
|
|
199
|
+
* state and rewrites the run report. Read-only against Multica — never enqueues a second Issue.
|
|
200
|
+
*/
|
|
201
|
+
export function statusMulticaWorkspaceRun(start, name, options = {}) {
|
|
202
|
+
const env = options.env || process.env;
|
|
203
|
+
const cliVersion = options.cliVersion || '';
|
|
204
|
+
const runId = String(options.runId || '').trim();
|
|
205
|
+
if (!/^multica_workspace_run_[0-9]+_[a-f0-9]{8}$/.test(runId)) {
|
|
206
|
+
throw new Error('workspace run --status 需要一个合法的 --run-id(来自上次 run 的 runId)');
|
|
207
|
+
}
|
|
208
|
+
if (!options.execute) {
|
|
209
|
+
throw new Error('workspace run --status 会向 Multica 独立回读同一 Issue;必须传 --execute');
|
|
210
|
+
}
|
|
211
|
+
if (!options.yes)
|
|
212
|
+
throw new Error('workspace run --status 远端回读必须同时传 --execute --yes');
|
|
213
|
+
const info = inspectAgentProject(start, cliVersion);
|
|
214
|
+
requireMulticaWorkspace(info.root, name, cliVersion);
|
|
215
|
+
// The original run persists evidence to `--out` when given, else the default runId path. Resume
|
|
216
|
+
// must look where the run actually wrote, so honor the same `--out` (and rewrite back to it).
|
|
217
|
+
const evidenceRel = options.out || join(MULTICA_EVIDENCE_ROOT, safeName(name), 'multica-runs', `${runId}.json`);
|
|
218
|
+
const evidenceAbs = safeProjectPath(info.root, evidenceRel, false);
|
|
219
|
+
if (!existsSync(evidenceAbs)) {
|
|
220
|
+
throw new Error(`找不到 run 证据 ${evidenceRel};--run-id 必须是本 Workspace 已有的 runId` +
|
|
221
|
+
`${options.out ? '' : '(若原 run 用了 --out,请对 --status 传相同 --out)'}`);
|
|
222
|
+
}
|
|
223
|
+
const prior = JSON.parse(readFileSync(evidenceAbs, 'utf8'));
|
|
224
|
+
if (prior.$schema !== 'dingtalk-agent/multica-workspace-run@1' ||
|
|
225
|
+
prior.provider !== 'multica' || prior.workspace !== name || !prior.issueId) {
|
|
226
|
+
throw new Error('run 证据不是本 Workspace 的 multica run,或缺少 issueId;已 fail closed');
|
|
227
|
+
}
|
|
228
|
+
// The evidence file must belong to THIS runId. Two runs sharing one --out overwrite each
|
|
229
|
+
// other, so resuming the first runId against that path would otherwise read the second run's
|
|
230
|
+
// Issue while reporting the first runId — cross-wiring answer and run. Fail closed on mismatch.
|
|
231
|
+
if (prior.runId !== runId) {
|
|
232
|
+
throw new Error(`run 证据 ${evidenceRel} 属于 runId ${prior.runId},与 --run-id ${runId} 不一致;已 fail closed` +
|
|
233
|
+
`(同一 --out 被后一次 run 覆盖时会这样)`);
|
|
234
|
+
}
|
|
235
|
+
// Re-resolve scope offline and refuse unless the WHOLE frozen target still matches: an env
|
|
236
|
+
// pointing at a different runtime/agent/profile (or an invalid ref surfacing in blocking) must
|
|
237
|
+
// not let the resume mis-record the old task or overwrite the frozen values. workspaceId alone
|
|
238
|
+
// is not enough.
|
|
239
|
+
const providerPlan = planMulticaWorkspace(info.root, name, { env, cliVersion });
|
|
240
|
+
const scopeDrift = providerPlan.blocking.length ? ['blocking:' + providerPlan.blocking.join(',')] : [];
|
|
241
|
+
if (providerPlan.profile !== prior.profile)
|
|
242
|
+
scopeDrift.push('profile');
|
|
243
|
+
if (providerPlan.expected.workspaceId !== prior.workspaceId)
|
|
244
|
+
scopeDrift.push('workspace');
|
|
245
|
+
if (providerPlan.expected.runtimeId !== prior.runtimeId)
|
|
246
|
+
scopeDrift.push('runtime');
|
|
247
|
+
if (providerPlan.expected.agentId !== prior.agentId)
|
|
248
|
+
scopeDrift.push('agent');
|
|
249
|
+
if (scopeDrift.length) {
|
|
250
|
+
throw new Error(`workspace run --status: 当前 workspace scope 与 run 证据不一致 [${scopeDrift.join(', ')}];已 fail closed`);
|
|
251
|
+
}
|
|
252
|
+
const command = options.command || env.DTA_MULTICA_BIN || 'multica';
|
|
253
|
+
const timeoutMs = positiveTimeout(options.timeoutMs, 240_000, 300_000);
|
|
254
|
+
const plan = {
|
|
255
|
+
profile: providerPlan.profile,
|
|
256
|
+
workspaceId: providerPlan.expected.workspaceId,
|
|
257
|
+
runtimeId: providerPlan.expected.runtimeId,
|
|
258
|
+
agentId: providerPlan.expected.agentId,
|
|
259
|
+
promptHash: prior.promptHash,
|
|
260
|
+
};
|
|
261
|
+
return readbackWorkspaceRun({
|
|
262
|
+
root: info.root, name, plan, issueId: prior.issueId, runId,
|
|
263
|
+
command, env, timeoutMs, calls: [], out: options.out,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
179
266
|
export function planMulticaDeployment(start, name, options = {}) {
|
|
180
267
|
const action = options.action || 'apply';
|
|
181
268
|
const env = options.env || process.env;
|
|
@@ -299,7 +386,7 @@ export function applyMulticaDeployment(start, name, options = {}, action = 'appl
|
|
|
299
386
|
receipt = retireDeployment(info.root, workspace, source, plan, preflight, operation, command, env, timeoutMs, calls);
|
|
300
387
|
}
|
|
301
388
|
else {
|
|
302
|
-
receipt = executeApply(info.root, workspace, source, plan, preflight, operation, command, env, timeoutMs, positiveTimeout(options.smokeTimeoutMs, 30_000,
|
|
389
|
+
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);
|
|
303
390
|
}
|
|
304
391
|
}
|
|
305
392
|
catch (error) {
|
|
@@ -499,8 +586,7 @@ function loadDeploymentSource(root) {
|
|
|
499
586
|
const definitionHash = sha256(definition);
|
|
500
587
|
const skillRoots = [];
|
|
501
588
|
for (const name of info.project.skills) {
|
|
502
|
-
const ref = name
|
|
503
|
-
? `.agents/skills/${name}` : `skills/${name}`;
|
|
589
|
+
const ref = projectSkillRef(info.project, name);
|
|
504
590
|
skillRoots.push({ name, root: safeProjectPath(root, ref, true) });
|
|
505
591
|
}
|
|
506
592
|
if (skillRoots.length > MAX_DEPLOY_SKILLS) {
|
|
@@ -711,7 +797,7 @@ function createAndReadSmoke(root, plan, source, agentId, command, env, timeoutMs
|
|
|
711
797
|
`required_skills=${JSON.stringify(requiredSkills)}`,
|
|
712
798
|
'This is a deployment control-plane verification task.',
|
|
713
799
|
'Use the Host native Skill tool exactly once for every required skill above, in the listed order.',
|
|
714
|
-
'Do not call DWS,
|
|
800
|
+
'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.',
|
|
715
801
|
`Return exactly this JSON and nothing else: ${expectedResponse}`,
|
|
716
802
|
].join('\n'),
|
|
717
803
|
'--assignee-id', agentId, '--output', 'json',
|
|
@@ -778,7 +864,12 @@ function readSmoke(root, plan, marker, issueId, agentId, requiredSkills, command
|
|
|
778
864
|
const responseWrites = toolUses.filter((item) => optionalString(item.tool) === 'write')
|
|
779
865
|
.map((item) => smokeToolInput(item)).filter((input) => safeSmokeResponsePath(optionalString(input.filePath)))
|
|
780
866
|
.map((input) => optionalString(input.content)).filter(Boolean);
|
|
781
|
-
const
|
|
867
|
+
const responseShells = toolUses.filter((item) => optionalString(item.tool) === 'bash')
|
|
868
|
+
.map((item) => optionalString(smokeToolInput(item).command))
|
|
869
|
+
.filter((command) => safeCombinedSmokeResponse(command, issueId, marker, requiredSkills))
|
|
870
|
+
.map(() => JSON.stringify({ schema: 'dta-multica-load-smoke@1', marker, loaded: requiredSkills }));
|
|
871
|
+
const responseEvidence = [...texts, ...responseWrites, ...responseShells]
|
|
872
|
+
.find(matchesResponse) || '';
|
|
782
873
|
const responsePassed = Boolean(responseEvidence);
|
|
783
874
|
const toolTracePassed = toolUses.every((item) => allowedSmokeToolUse(item, issueId, marker, requiredSkills)) && stableStringify(skillNames) === stableStringify(requiredSkills);
|
|
784
875
|
const failures = [];
|
|
@@ -836,26 +927,45 @@ function allowedSmokeToolUse(item, issueId, marker, requiredSkills) {
|
|
|
836
927
|
const cleanupFirstPrefix = 'rm ';
|
|
837
928
|
const cleanupFirstSuffix = ` && multica issue status ${issueId} in_review`;
|
|
838
929
|
if (command.startsWith(cleanupFirstPrefix) && command.endsWith(cleanupFirstSuffix)) {
|
|
839
|
-
return
|
|
930
|
+
return safeSmokeCleanup(command.slice(cleanupFirstPrefix.length, command.length - cleanupFirstSuffix.length));
|
|
840
931
|
}
|
|
841
932
|
const statusFirstPrefix = `multica issue status ${issueId} in_review && rm `;
|
|
842
933
|
if (command.startsWith(statusFirstPrefix)) {
|
|
843
|
-
return
|
|
934
|
+
return safeSmokeCleanup(command.slice(statusFirstPrefix.length));
|
|
844
935
|
}
|
|
845
936
|
if (command.startsWith(cleanupFirstPrefix)) {
|
|
846
|
-
return
|
|
937
|
+
return safeSmokeCleanup(command.slice(cleanupFirstPrefix.length));
|
|
847
938
|
}
|
|
939
|
+
if (safeCombinedSmokeResponse(command, issueId, marker, requiredSkills))
|
|
940
|
+
return true;
|
|
848
941
|
return false;
|
|
849
942
|
}
|
|
943
|
+
function safeCombinedSmokeResponse(command, issueId, marker, requiredSkills) {
|
|
944
|
+
const response = JSON.stringify({ schema: 'dta-multica-load-smoke@1', marker, loaded: requiredSkills });
|
|
945
|
+
return command === [
|
|
946
|
+
"cat > ./reply.md << 'REPLY_EOF'",
|
|
947
|
+
response,
|
|
948
|
+
'REPLY_EOF',
|
|
949
|
+
`multica issue comment add ${issueId} --content-file ./reply.md && rm ./reply.md`,
|
|
950
|
+
].join('\n');
|
|
951
|
+
}
|
|
850
952
|
function safeSmokeResponsePath(value) {
|
|
851
|
-
const names = new Set(['reply.md', 'result.json']);
|
|
953
|
+
const names = new Set(['reply.md', 'result.md', 'result.json']);
|
|
852
954
|
if (value.startsWith('./'))
|
|
853
955
|
return names.has(value.slice(2));
|
|
854
|
-
|
|
956
|
+
// 裸文件名与 `./` 前缀等价:Agent 在 workdir 里写绝对路径、随后用裸名引用同一个文件是
|
|
957
|
+
// 平台的常见写法。只放行精确 basename —— `foo/result.md` 与 `../result.md` 仍然落到
|
|
958
|
+
// 下面的绝对路径分支并被拒绝,白名单没有变宽。
|
|
959
|
+
if (!value.startsWith('/'))
|
|
960
|
+
return names.has(value);
|
|
961
|
+
if (!names.has(value.split('/').at(-1) || ''))
|
|
855
962
|
return false;
|
|
856
963
|
return value.split('/').every((part) => !part ||
|
|
857
964
|
(part !== '.' && part !== '..' && /^[A-Za-z0-9._-]+$/.test(part)));
|
|
858
965
|
}
|
|
966
|
+
function safeSmokeCleanup(value) {
|
|
967
|
+
return safeSmokeResponsePath(value.startsWith('-f ') ? value.slice(3) : value);
|
|
968
|
+
}
|
|
859
969
|
function rollbackConfirmed(root, plan, command, env, timeoutMs, calls, rollback, temp) {
|
|
860
970
|
const report = emptyRollback(true);
|
|
861
971
|
const attempt = (id, argv) => {
|