@dharma-ai-labs/agent-fabric-task-runner 0.1.35 → 0.1.37
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/README.md +58 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +158 -12
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +474 -3
- package/dist/index.test.js.map +1 -1
- package/package.json +3 -3
package/dist/index.test.js
CHANGED
|
@@ -1,12 +1,483 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { test } from 'node:test';
|
|
3
3
|
import { createHash, generateKeyPairSync, randomUUID } from 'node:crypto';
|
|
4
|
-
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { chmod, link, mkdtemp, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { resolve } from 'node:path';
|
|
7
|
-
import { spawnSync } from 'node:child_process';
|
|
8
|
-
import {
|
|
7
|
+
import childProcess, { spawnSync } from 'node:child_process';
|
|
8
|
+
import { syncBuiltinESMExports } from 'node:module';
|
|
9
|
+
import { actionDecisionDigest, signCanonicalObject, validateTaskEnvelopeContract, validateTaskActionContract, } from '@dharma-ai-labs/agent-fabric-contracts';
|
|
9
10
|
import { assertTaskWithinLocalPolicy, canonicalTaskActionForTask, executeTask, FileActionDecisionReplayGuard, FileActionExecutionJournal, FileTaskReceiptStore, providerInstructionsForTask, verifyTaskEnvelope, } from './index.js';
|
|
11
|
+
async function requiredArtifactFixture(t, requiredArtifacts) {
|
|
12
|
+
const root = await mkdtemp(resolve(tmpdir(), 'dharma-required-artifact-'));
|
|
13
|
+
t.after(() => rm(root, { recursive: true, force: true }));
|
|
14
|
+
const workspace = resolve(root, 'workspace');
|
|
15
|
+
await mkdir(workspace);
|
|
16
|
+
for (const argv of [['init', '-q'], ['config', 'user.email', 'test@dharma.ai'],
|
|
17
|
+
['config', 'user.name', 'Artifact Fixture'], ['commit', '--allow-empty', '-qm', 'initial']]) {
|
|
18
|
+
assert.equal(spawnSync('git', argv, { cwd: workspace }).status, 0);
|
|
19
|
+
}
|
|
20
|
+
const policy = {
|
|
21
|
+
schema: 'dharma.organization-policy/v1', organizationId: 'org_test', revision: '1',
|
|
22
|
+
evidence: { defaultMode: 'deep', registeredWorkspaceOnly: true, excludePaths: [], maximumCapsuleBytes: 1,
|
|
23
|
+
maximumDailyUploadBytes: 1, maximumExpansionBytes: 1 },
|
|
24
|
+
tasks: { defaultNetwork: 'deny', defaultGit: 'task_branch', allowedCommands: {}, writePaths: ['reports/**'],
|
|
25
|
+
requireLocalConfirmationFor: [] },
|
|
26
|
+
skills: { automaticInstall: true, automaticPromotionMaxRisk: 'R2', canaryPercent: 10 }, retention: {}, budgets: {},
|
|
27
|
+
};
|
|
28
|
+
const unsigned = {
|
|
29
|
+
schema: 'dharma.task/v1', taskId: randomUUID(), organizationId: 'org_test', workspaceId: 'workspace',
|
|
30
|
+
taskType: 'external_request', target: { deviceId: 'device', provider: 'codex' },
|
|
31
|
+
skillBundle: null, instructions: 'Produce the signed required artifacts.', requiredSkills: [],
|
|
32
|
+
authority: { readPaths: ['reports/**'], writePaths: ['reports/**'], commands: [],
|
|
33
|
+
network: 'deny', git: 'task_branch' },
|
|
34
|
+
execution: { isolation: 'git_worktree', timeoutSeconds: 10, leaseSeconds: 20, maximumConcurrentAgents: 1 },
|
|
35
|
+
acceptance: { commands: [], requiredArtifacts },
|
|
36
|
+
budget: { mode: 'byok_local', maximumDharmaCostCents: 0 },
|
|
37
|
+
createdAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 60_000).toISOString(), nonce: randomUUID(),
|
|
38
|
+
};
|
|
39
|
+
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
|
40
|
+
const signed = (overrides = {}) => {
|
|
41
|
+
const task = { ...unsigned, ...overrides };
|
|
42
|
+
return { ...task, signature: signCanonicalObject(task, privateKey) };
|
|
43
|
+
};
|
|
44
|
+
const providerResult = {
|
|
45
|
+
provider: 'codex', exitCode: 0, signal: null, timedOut: false, stdout: 'synthetic completion', stderr: '',
|
|
46
|
+
stdoutSha256: `sha256:${'1'.repeat(64)}`, stderrSha256: `sha256:${'0'.repeat(64)}`,
|
|
47
|
+
};
|
|
48
|
+
return { root, workspace, policy, unsigned, signed, providerResult, publicKey,
|
|
49
|
+
input: { policy, workspace, relayStateDirectory: resolve(root, 'state'), serverPublicKey: publicKey,
|
|
50
|
+
receiptStore: new FileTaskReceiptStore(resolve(root, 'receipts')) } };
|
|
51
|
+
}
|
|
52
|
+
test('required artifact review v10: incomplete Git streams fail closed', async (t) => {
|
|
53
|
+
for (const stream of ['tracked-overflow', 'untracked-overflow', 'unterminated']) {
|
|
54
|
+
await t.test(stream, async (child) => {
|
|
55
|
+
const fixture = await requiredArtifactFixture(child, []);
|
|
56
|
+
const originalSpawn = childProcess.spawn;
|
|
57
|
+
const body = stream === 'unterminated' ? 'reports/partial'
|
|
58
|
+
: 'reports/a\0'.repeat(99_999) + 'reports/zzextra\0';
|
|
59
|
+
const outputPath = resolve(fixture.root, 'synthetic-git-output');
|
|
60
|
+
await writeFile(outputPath, body);
|
|
61
|
+
let intercepted = 0;
|
|
62
|
+
const patch = child.mock.method(childProcess, 'spawn', (command, argv, options) => {
|
|
63
|
+
if (command === 'git' && (stream === 'untracked-overflow' ? argv[0] === 'ls-files' : argv[0] === 'diff')) {
|
|
64
|
+
intercepted++;
|
|
65
|
+
return originalSpawn(process.execPath, ['-e', 'process.stdout.write(require("node:fs").readFileSync(process.argv[1]));', outputPath], options);
|
|
66
|
+
}
|
|
67
|
+
return originalSpawn(command, argv, options);
|
|
68
|
+
});
|
|
69
|
+
syncBuiltinESMExports();
|
|
70
|
+
try {
|
|
71
|
+
const receipt = await executeTask({ ...fixture.input, task: fixture.signed(),
|
|
72
|
+
providerExecutor: async ({ workspace }) => {
|
|
73
|
+
if (stream !== 'untracked-overflow')
|
|
74
|
+
await writeFile(resolve(workspace, 'outside.txt'), 'fixture');
|
|
75
|
+
return fixture.providerResult;
|
|
76
|
+
} });
|
|
77
|
+
assert.equal(intercepted, 1);
|
|
78
|
+
assert.equal(receipt.status, 'failed');
|
|
79
|
+
assert.match(receipt.commandResults.at(-1)?.stderr || '', /output.*limit|Git path output was incomplete/);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
patch.mock.restore();
|
|
83
|
+
syncBuiltinESMExports();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
test('required artifact review v10: ignored outputs require signed write authority', async (t) => {
|
|
89
|
+
const fixture = await requiredArtifactFixture(t, ['reports/result.json']);
|
|
90
|
+
await writeFile(resolve(fixture.workspace, '.gitignore'), 'reports/result.json\n');
|
|
91
|
+
assert.equal(spawnSync('git', ['add', '.gitignore'], { cwd: fixture.workspace }).status, 0);
|
|
92
|
+
assert.equal(spawnSync('git', ['commit', '-qm', 'ignore output'], { cwd: fixture.workspace }).status, 0);
|
|
93
|
+
fixture.unsigned.authority.writePaths = ['reports/allowed/**'];
|
|
94
|
+
let calls = 0;
|
|
95
|
+
const receipt = await executeTask({ ...fixture.input, task: fixture.signed(),
|
|
96
|
+
providerExecutor: async ({ workspace }) => {
|
|
97
|
+
calls++;
|
|
98
|
+
await mkdir(resolve(workspace, 'reports'));
|
|
99
|
+
await writeFile(resolve(workspace, 'reports/result.json'), 'forbidden ignored output');
|
|
100
|
+
return fixture.providerResult;
|
|
101
|
+
} });
|
|
102
|
+
assert.equal(receipt.status, 'failed');
|
|
103
|
+
assert.equal(calls, 0);
|
|
104
|
+
assert.match(receipt.commandResults.at(-1)?.stderr || '', /required_artifact_missing/);
|
|
105
|
+
});
|
|
106
|
+
test('required artifact review v10: existing read-only artifacts remain unchanged through acceptance', async (t) => {
|
|
107
|
+
for (const mutate of [false, true]) {
|
|
108
|
+
await t.test(mutate ? 'changed by acceptance' : 'unchanged', async (child) => {
|
|
109
|
+
const fixture = await requiredArtifactFixture(child, ['reports/result.json']);
|
|
110
|
+
await mkdir(resolve(fixture.workspace, 'reports'));
|
|
111
|
+
await writeFile(resolve(fixture.workspace, 'reports/result.json'), 'private-read-only-sentinel');
|
|
112
|
+
assert.equal(spawnSync('git', ['add', 'reports/result.json'], { cwd: fixture.workspace }).status, 0);
|
|
113
|
+
assert.equal(spawnSync('git', ['commit', '-qm', 'existing output'], { cwd: fixture.workspace }).status, 0);
|
|
114
|
+
fixture.unsigned.authority.writePaths = ['reports/allowed/**'];
|
|
115
|
+
if (mutate) {
|
|
116
|
+
fixture.policy.tasks.allowedCommands.verify = { argv: [process.execPath, '-e',
|
|
117
|
+
'require("node:fs").writeFileSync("reports/result.json","unauthorized-replacement");'], timeoutSeconds: 5 };
|
|
118
|
+
fixture.unsigned.authority.commands.push({ commandId: 'verify' });
|
|
119
|
+
fixture.unsigned.acceptance.commands.push({ commandId: 'verify' });
|
|
120
|
+
}
|
|
121
|
+
const receipt = await executeTask({ ...fixture.input, task: fixture.signed(), providerExecutor: async () => fixture.providerResult });
|
|
122
|
+
assert.equal(receipt.status, mutate ? 'failed' : 'completed');
|
|
123
|
+
if (mutate)
|
|
124
|
+
assert.match(receipt.commandResults.at(-1)?.stderr || '', /required_artifact_write_unauthorized/);
|
|
125
|
+
assert.equal(JSON.stringify(receipt).includes('private-read-only-sentinel'), false);
|
|
126
|
+
assert.equal(JSON.stringify(receipt).includes('unauthorized-replacement'), false);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
test('required artifact review v10: ignored write-authorized outputs remain supported', async (t) => {
|
|
131
|
+
const fixture = await requiredArtifactFixture(t, ['reports/result.json']);
|
|
132
|
+
await writeFile(resolve(fixture.workspace, '.gitignore'), 'reports/result.json\n');
|
|
133
|
+
assert.equal(spawnSync('git', ['add', '.gitignore'], { cwd: fixture.workspace }).status, 0);
|
|
134
|
+
assert.equal(spawnSync('git', ['commit', '-qm', 'ignore output'], { cwd: fixture.workspace }).status, 0);
|
|
135
|
+
const receipt = await executeTask({ ...fixture.input, task: fixture.signed(), providerExecutor: async ({ workspace }) => {
|
|
136
|
+
await mkdir(resolve(workspace, 'reports'));
|
|
137
|
+
await writeFile(resolve(workspace, 'reports/result.json'), '{}');
|
|
138
|
+
return fixture.providerResult;
|
|
139
|
+
} });
|
|
140
|
+
assert.equal(receipt.status, 'completed');
|
|
141
|
+
});
|
|
142
|
+
test('required artifacts prevent successful completion when the signed output is missing', async (t) => {
|
|
143
|
+
const fixture = await requiredArtifactFixture(t, ['reports/result.json']);
|
|
144
|
+
const receipt = await executeTask({ ...fixture.input, task: fixture.signed(),
|
|
145
|
+
providerExecutor: async () => fixture.providerResult });
|
|
146
|
+
assert.equal(receipt.commandResults[0]?.commandId, 'provider.codex');
|
|
147
|
+
assert.equal(receipt.status, 'failed');
|
|
148
|
+
assert.match(receipt.commandResults.at(-1)?.stderr || '', /required_artifact_missing/);
|
|
149
|
+
});
|
|
150
|
+
test('required artifacts accept regular files, including empty files, without exposing contents', async (t) => {
|
|
151
|
+
const fixture = await requiredArtifactFixture(t, ['reports/result.json', 'reports/empty.txt']);
|
|
152
|
+
const receipt = await executeTask({ ...fixture.input, task: fixture.signed(),
|
|
153
|
+
providerExecutor: async ({ workspace }) => {
|
|
154
|
+
await mkdir(resolve(workspace, 'reports'));
|
|
155
|
+
await writeFile(resolve(workspace, 'reports/result.json'), 'private fixture output');
|
|
156
|
+
await writeFile(resolve(workspace, 'reports/empty.txt'), '');
|
|
157
|
+
return fixture.providerResult;
|
|
158
|
+
} });
|
|
159
|
+
assert.equal(receipt.status, 'completed');
|
|
160
|
+
assert.equal(JSON.stringify(receipt).includes('private fixture output'), false);
|
|
161
|
+
const durable = await readFile(resolve(fixture.root, 'receipts', `${receipt.taskId}.json`), 'utf8');
|
|
162
|
+
assert.equal(durable.includes('private fixture output'), false);
|
|
163
|
+
});
|
|
164
|
+
test('required artifacts fail if any required file is missing', async (t) => {
|
|
165
|
+
const fixture = await requiredArtifactFixture(t, ['reports/present.json', 'reports/missing.json']);
|
|
166
|
+
const receipt = await executeTask({ ...fixture.input, task: fixture.signed(),
|
|
167
|
+
providerExecutor: async ({ workspace }) => {
|
|
168
|
+
await mkdir(resolve(workspace, 'reports'));
|
|
169
|
+
await writeFile(resolve(workspace, 'reports/present.json'), '{}');
|
|
170
|
+
return fixture.providerResult;
|
|
171
|
+
} });
|
|
172
|
+
assert.equal(receipt.status, 'failed');
|
|
173
|
+
assert.match(receipt.commandResults.at(-1)?.stderr || '', /required_artifact_missing/);
|
|
174
|
+
});
|
|
175
|
+
test('required artifacts reject ambiguous or unauthorized references before provider execution', async (t) => {
|
|
176
|
+
for (const artifact of ['', '../outside.txt', '/tmp/outside.txt', 'C:/outside.txt', 'reports\\result.json',
|
|
177
|
+
'reports/../outside.txt', 'reports/./result.json', 'reports//result.json', 'reports/result:stream',
|
|
178
|
+
'reports/NUL.txt', 'reports/result.', 'reports/result ', 'reports/*.json', 'outside/result.json']) {
|
|
179
|
+
await t.test(JSON.stringify(artifact), async (child) => {
|
|
180
|
+
const fixture = await requiredArtifactFixture(child, [artifact]);
|
|
181
|
+
let calls = 0;
|
|
182
|
+
await assert.rejects(executeTask({ ...fixture.input, task: fixture.signed(),
|
|
183
|
+
providerExecutor: async () => { calls++; return fixture.providerResult; } }), /required_artifact_(invalid|unauthorized)|Task failed schema validation/);
|
|
184
|
+
assert.equal(calls, 0);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
test('required artifacts check the final state after registered acceptance commands', async (t) => {
|
|
189
|
+
for (const operation of ['create', 'remove']) {
|
|
190
|
+
await t.test(operation, async (child) => {
|
|
191
|
+
const fixture = await requiredArtifactFixture(child, ['reports/result.json']);
|
|
192
|
+
const script = operation === 'create'
|
|
193
|
+
? 'const fs=require("node:fs");fs.mkdirSync("reports",{recursive:true});fs.writeFileSync("reports/result.json","{}");'
|
|
194
|
+
: 'require("node:fs").unlinkSync("reports/result.json");';
|
|
195
|
+
fixture.policy.tasks.allowedCommands.verify = { argv: [process.execPath, '-e', script], timeoutSeconds: 5 };
|
|
196
|
+
fixture.unsigned.authority.commands.push({ commandId: 'verify' });
|
|
197
|
+
fixture.unsigned.acceptance.commands.push({ commandId: 'verify' });
|
|
198
|
+
const receipt = await executeTask({ ...fixture.input, task: fixture.signed(),
|
|
199
|
+
providerExecutor: async ({ workspace }) => {
|
|
200
|
+
if (operation === 'remove') {
|
|
201
|
+
await mkdir(resolve(workspace, 'reports'));
|
|
202
|
+
await writeFile(resolve(workspace, 'reports/result.json'), '{}');
|
|
203
|
+
}
|
|
204
|
+
return fixture.providerResult;
|
|
205
|
+
} });
|
|
206
|
+
assert.equal(receipt.commandResults[1]?.commandId, 'verify');
|
|
207
|
+
assert.equal(receipt.commandResults[1]?.exitCode, 0);
|
|
208
|
+
assert.equal(receipt.status, operation === 'create' ? 'completed' : 'failed');
|
|
209
|
+
if (operation === 'remove')
|
|
210
|
+
assert.match(receipt.commandResults.at(-1)?.stderr || '', /required_artifact_missing/);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
test('required artifacts reject directories and hard-linked files', async (t) => {
|
|
215
|
+
for (const kind of ['directory', 'hardlink']) {
|
|
216
|
+
await t.test(kind, async (child) => {
|
|
217
|
+
const fixture = await requiredArtifactFixture(child, ['reports/result.json']);
|
|
218
|
+
const outside = resolve(fixture.root, 'outside.txt');
|
|
219
|
+
await writeFile(outside, 'outside-content-sentinel');
|
|
220
|
+
const receipt = await executeTask({ ...fixture.input, task: fixture.signed(),
|
|
221
|
+
providerExecutor: async ({ workspace }) => {
|
|
222
|
+
await mkdir(resolve(workspace, 'reports'));
|
|
223
|
+
if (kind === 'directory')
|
|
224
|
+
await mkdir(resolve(workspace, 'reports/result.json'));
|
|
225
|
+
else
|
|
226
|
+
await link(outside, resolve(workspace, 'reports/result.json'));
|
|
227
|
+
return fixture.providerResult;
|
|
228
|
+
} });
|
|
229
|
+
assert.equal(receipt.status, 'failed');
|
|
230
|
+
assert.match(receipt.commandResults.at(-1)?.stderr || '', /required_artifact_(not_file|unsafe)/);
|
|
231
|
+
assert.equal(JSON.stringify(receipt).includes('outside-content-sentinel'), false);
|
|
232
|
+
assert.equal(await readFile(outside, 'utf8'), 'outside-content-sentinel');
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
test('required artifacts reject a linked parent directory without disclosing the target', async (t) => {
|
|
237
|
+
const fixture = await requiredArtifactFixture(t, ['reports/linked/result.json']);
|
|
238
|
+
const outside = resolve(fixture.root, 'outside');
|
|
239
|
+
await mkdir(outside);
|
|
240
|
+
await writeFile(resolve(outside, 'result.json'), 'outside-content-sentinel');
|
|
241
|
+
const receipt = await executeTask({ ...fixture.input, task: fixture.signed(),
|
|
242
|
+
providerExecutor: async ({ workspace }) => {
|
|
243
|
+
await mkdir(resolve(workspace, 'reports'));
|
|
244
|
+
await symlink(outside, resolve(workspace, 'reports/linked'), 'junction');
|
|
245
|
+
return fixture.providerResult;
|
|
246
|
+
} });
|
|
247
|
+
assert.equal(receipt.status, 'failed');
|
|
248
|
+
assert.match(receipt.commandResults.at(-1)?.stderr || '', /required_artifact_unsafe/);
|
|
249
|
+
assert.equal(JSON.stringify(receipt).includes('outside-content-sentinel'), false);
|
|
250
|
+
assert.equal(JSON.stringify(receipt).includes(outside), false);
|
|
251
|
+
});
|
|
252
|
+
test('required artifacts reject unreadable files', { skip: process.platform === 'win32' }, async (t) => {
|
|
253
|
+
const fixture = await requiredArtifactFixture(t, ['reports/result.json']);
|
|
254
|
+
const receipt = await executeTask({ ...fixture.input, task: fixture.signed(),
|
|
255
|
+
providerExecutor: async ({ workspace }) => {
|
|
256
|
+
await mkdir(resolve(workspace, 'reports'));
|
|
257
|
+
await writeFile(resolve(workspace, 'reports/result.json'), '{}');
|
|
258
|
+
await chmod(resolve(workspace, 'reports/result.json'), 0o000);
|
|
259
|
+
return fixture.providerResult;
|
|
260
|
+
} });
|
|
261
|
+
assert.equal(receipt.status, 'failed');
|
|
262
|
+
assert.match(receipt.commandResults.at(-1)?.stderr || '', /required_artifact_unreadable/);
|
|
263
|
+
});
|
|
264
|
+
test('required artifacts preserve provider failure and cancelled status', async (t) => {
|
|
265
|
+
for (const kind of ['provider-failure', 'cancelled']) {
|
|
266
|
+
await t.test(kind, async (child) => {
|
|
267
|
+
const fixture = await requiredArtifactFixture(child, ['reports/result.json']);
|
|
268
|
+
const controller = new AbortController();
|
|
269
|
+
const receipt = await executeTask({ ...fixture.input, task: fixture.signed(), signal: controller.signal,
|
|
270
|
+
providerExecutor: async () => {
|
|
271
|
+
if (kind === 'cancelled')
|
|
272
|
+
controller.abort();
|
|
273
|
+
return { ...fixture.providerResult, exitCode: kind === 'provider-failure' ? 1 : 0 };
|
|
274
|
+
} });
|
|
275
|
+
assert.equal(receipt.status, kind === 'cancelled' ? 'cancelled' : 'failed');
|
|
276
|
+
assert.equal(receipt.commandResults[0]?.exitCode, kind === 'provider-failure' ? 1 : 0);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
test('required artifacts keep failed receipts durable and suppress duplicate execution', async (t) => {
|
|
281
|
+
const fixture = await requiredArtifactFixture(t, ['reports/result.json']);
|
|
282
|
+
const task = fixture.signed();
|
|
283
|
+
let calls = 0;
|
|
284
|
+
const input = { ...fixture.input, task, providerExecutor: async () => { calls++; return fixture.providerResult; } };
|
|
285
|
+
const first = await executeTask(input);
|
|
286
|
+
const duplicate = await executeTask(input);
|
|
287
|
+
assert.equal(first.status, 'failed');
|
|
288
|
+
assert.equal(duplicate.status, 'failed');
|
|
289
|
+
assert.equal(calls, 1);
|
|
290
|
+
});
|
|
291
|
+
test('required artifacts cannot emit an executed acknowledgement after missing output', async (t) => {
|
|
292
|
+
const fixture = await requiredArtifactFixture(t, ['reports/result.json']);
|
|
293
|
+
const decisionKeys = generateKeyPairSync('ed25519');
|
|
294
|
+
const base = { ...fixture.unsigned, target: { ...fixture.unsigned.target, endpointId: randomUUID() },
|
|
295
|
+
requiredCapabilities: ['action_decision_receipts_v1'] };
|
|
296
|
+
const actionDecision = embedDecision(base, 'release', decisionKeys.privateKey, new Date());
|
|
297
|
+
const receipt = await executeTask({ ...fixture.input, task: fixture.signed({ ...base, actionDecision }),
|
|
298
|
+
actionDecisions: { resolvePublicKey: () => decisionKeys.publicKey },
|
|
299
|
+
providerExecutor: async () => fixture.providerResult });
|
|
300
|
+
assert.equal(receipt.status, 'failed');
|
|
301
|
+
assert.equal(receipt.actionAcknowledgement?.disposition, 'unknown');
|
|
302
|
+
});
|
|
303
|
+
test('required artifact review: signed output paths reach the provider without user instruction duplication', async (t) => {
|
|
304
|
+
const fixture = await requiredArtifactFixture(t, ['reports/result.json']);
|
|
305
|
+
const receipt = await executeTask({ ...fixture.input, task: fixture.signed(),
|
|
306
|
+
providerExecutor: async ({ workspace, instructions }) => {
|
|
307
|
+
const matched = instructions.match(/<dharma_required_artifacts>\n([\s\S]*?)\n<\/dharma_required_artifacts>/);
|
|
308
|
+
assert.ok(matched, 'provider must receive the separately signed artifact list');
|
|
309
|
+
const outputs = JSON.parse(matched[1]);
|
|
310
|
+
assert.deepEqual(outputs, ['reports/result.json']);
|
|
311
|
+
await mkdir(resolve(workspace, 'reports'));
|
|
312
|
+
await writeFile(resolve(workspace, outputs[0]), '{}');
|
|
313
|
+
return fixture.providerResult;
|
|
314
|
+
} });
|
|
315
|
+
assert.equal(receipt.status, 'completed');
|
|
316
|
+
});
|
|
317
|
+
test('required artifact review: superscript Windows device aliases are rejected before provider execution', async (t) => {
|
|
318
|
+
for (const artifact of ['reports/COM\u00b9', 'reports/COM\u00b2.json', 'reports/COM\u00b3',
|
|
319
|
+
'reports/LPT\u00b9', 'reports/LPT\u00b2.txt', 'reports/lpt\u00b3']) {
|
|
320
|
+
await t.test(artifact, async (child) => {
|
|
321
|
+
const fixture = await requiredArtifactFixture(child, [artifact]);
|
|
322
|
+
let calls = 0;
|
|
323
|
+
await assert.rejects(executeTask({ ...fixture.input, task: fixture.signed(),
|
|
324
|
+
providerExecutor: async () => { calls++; return fixture.providerResult; } }), /required_artifact_invalid|Task failed schema validation/);
|
|
325
|
+
assert.equal(calls, 0);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
test('required artifact review: portable case and normalization collisions are rejected before execution', async (t) => {
|
|
330
|
+
for (const artifacts of [['reports/A.json', 'reports/a.json'], ['reports/Result.json', 'Reports/result.json'],
|
|
331
|
+
['reports/\u00e9.json', 'reports/e\u0301.json']]) {
|
|
332
|
+
await t.test(JSON.stringify(artifacts), async (child) => {
|
|
333
|
+
const fixture = await requiredArtifactFixture(child, artifacts);
|
|
334
|
+
fixture.unsigned.authority.readPaths = ['.'];
|
|
335
|
+
let calls = 0;
|
|
336
|
+
await assert.rejects(executeTask({ ...fixture.input, task: fixture.signed(),
|
|
337
|
+
providerExecutor: async () => { calls++; return fixture.providerResult; } }), /required_artifact_invalid/);
|
|
338
|
+
assert.equal(calls, 0);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
test('required artifact review: unusable component and total path lengths are rejected before execution', async (t) => {
|
|
343
|
+
for (const artifact of ['reports/' + 'x'.repeat(256), 'reports/' + '\u00e9'.repeat(128),
|
|
344
|
+
'reports/' + Array(25).fill('x'.repeat(200)).join('/')]) {
|
|
345
|
+
await t.test('UTF8 bytes ' + Buffer.byteLength(artifact), async (child) => {
|
|
346
|
+
const fixture = await requiredArtifactFixture(child, [artifact]);
|
|
347
|
+
let calls = 0;
|
|
348
|
+
await assert.rejects(executeTask({ ...fixture.input, task: fixture.signed(),
|
|
349
|
+
providerExecutor: async () => { calls++; return fixture.providerResult; } }), /required_artifact_invalid|Task failed schema validation/);
|
|
350
|
+
assert.equal(calls, 0);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
test('required artifact review v3: console aliases and lone surrogates fail before provider execution', async (t) => {
|
|
355
|
+
for (const artifact of ['reports/CONIN$', 'reports/conout$', 'reports/ConIn$.txt', 'reports/CONOUT$.json',
|
|
356
|
+
'reports/\ud800', 'reports/\ud801', 'reports/\udc00', 'reports/x\udfff.json']) {
|
|
357
|
+
await t.test(JSON.stringify(artifact), async (child) => {
|
|
358
|
+
const fixture = await requiredArtifactFixture(child, [artifact]);
|
|
359
|
+
let calls = 0;
|
|
360
|
+
await assert.rejects(executeTask({ ...fixture.input, task: fixture.signed(),
|
|
361
|
+
providerExecutor: async () => { calls++; return fixture.providerResult; } }), /required_artifact_invalid/);
|
|
362
|
+
assert.equal(calls, 0);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
test('required artifact review v3: metadata does not consume the schema instruction allowance', async (t) => {
|
|
367
|
+
const fixture = await requiredArtifactFixture(t, ['reports/result.json']);
|
|
368
|
+
const instructions = 'x'.repeat(20_000);
|
|
369
|
+
const receipt = await executeTask({ ...fixture.input, task: fixture.signed({ instructions }),
|
|
370
|
+
providerExecutor: async ({ workspace, instructions: formatted }) => {
|
|
371
|
+
assert.ok(formatted.startsWith(instructions + '\n'));
|
|
372
|
+
assert.match(formatted, /reports\/result.json/);
|
|
373
|
+
await mkdir(resolve(workspace, 'reports'));
|
|
374
|
+
await writeFile(resolve(workspace, 'reports/result.json'), '{}');
|
|
375
|
+
return fixture.providerResult;
|
|
376
|
+
} });
|
|
377
|
+
assert.equal(receipt.status, 'completed');
|
|
378
|
+
});
|
|
379
|
+
test('required artifact review v3: full artifact list and paired Unicode retain bounded instructions', async (t) => {
|
|
380
|
+
const artifacts = Array.from({ length: 100 }, (_, index) => `reports/${index}-${'x'.repeat(90)}-\ud83d\ude00.json`);
|
|
381
|
+
const fixture = await requiredArtifactFixture(t, artifacts);
|
|
382
|
+
const formatted = providerInstructionsForTask(fixture.signed({ instructions: 'x'.repeat(20_000) }));
|
|
383
|
+
const matched = formatted.match(/<dharma_required_artifacts>\n([\s\S]*?)\n<\/dharma_required_artifacts>/);
|
|
384
|
+
assert.ok(matched);
|
|
385
|
+
assert.deepEqual(JSON.parse(matched[1]), artifacts);
|
|
386
|
+
assert.ok(formatted.length < 276_001);
|
|
387
|
+
assert.throws(() => providerInstructionsForTask(fixture.signed({ instructions: 'x'.repeat(20_001) })), /Provider instructions exceed the execution limit/);
|
|
388
|
+
});
|
|
389
|
+
test('required artifact review v3: raw artifact count is bounded before metadata formatting', async (t) => {
|
|
390
|
+
const artifacts = Array.from({ length: 10_000 }, (_, index) => `reports/${index}-${'x'.repeat(30)}.json`);
|
|
391
|
+
const fixture = await requiredArtifactFixture(t, artifacts);
|
|
392
|
+
assert.throws(() => providerInstructionsForTask(fixture.signed()), /required_artifact_invalid/);
|
|
393
|
+
});
|
|
394
|
+
test('required artifact review v4: Git preserves Unicode paths in tracked and untracked results', async (t) => {
|
|
395
|
+
for (const tracked of [false, true]) {
|
|
396
|
+
await t.test(tracked ? 'tracked' : 'untracked', async (child) => {
|
|
397
|
+
const artifact = 'reports/\ud83d\ude00 output.json';
|
|
398
|
+
const fixture = await requiredArtifactFixture(child, [artifact]);
|
|
399
|
+
const receipt = await executeTask({ ...fixture.input, task: fixture.signed(),
|
|
400
|
+
providerExecutor: async ({ workspace }) => {
|
|
401
|
+
await mkdir(resolve(workspace, 'reports'));
|
|
402
|
+
await writeFile(resolve(workspace, artifact), '{}');
|
|
403
|
+
if (tracked)
|
|
404
|
+
assert.equal(spawnSync('git', ['add', '--', artifact], { cwd: workspace }).status, 0);
|
|
405
|
+
return fixture.providerResult;
|
|
406
|
+
} });
|
|
407
|
+
assert.equal(receipt.status, 'completed');
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
test('required artifact review v4: delimiter-safe paths do not widen signed write scope', async (t) => {
|
|
412
|
+
for (const outside of ['outside/\ud83d\ude00.json', ' reports/leak.txt']) {
|
|
413
|
+
await t.test(JSON.stringify(outside), async (child) => {
|
|
414
|
+
const fixture = await requiredArtifactFixture(child, []);
|
|
415
|
+
const receipt = await executeTask({ ...fixture.input, task: fixture.signed(),
|
|
416
|
+
providerExecutor: async ({ workspace }) => {
|
|
417
|
+
await mkdir(resolve(workspace, outside.split('/')[0]));
|
|
418
|
+
await writeFile(resolve(workspace, outside), '{}');
|
|
419
|
+
return fixture.providerResult;
|
|
420
|
+
} });
|
|
421
|
+
assert.equal(receipt.status, 'failed');
|
|
422
|
+
const expected = 'Error: Provider changed a path outside the signed task authority.';
|
|
423
|
+
assert.equal(receipt.commandResults.at(-1)?.stderrSha256, `sha256:${createHash('sha256').update(expected).digest('hex')}`);
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
test('required artifact review v5: Unicode caseless aliases fail before provider execution', async (t) => {
|
|
428
|
+
for (const pair of [['reports/\u03c3.json', 'reports/\u03c2.json'], ['reports/\u00df.json', 'reports/\u1e9e.json']]) {
|
|
429
|
+
const fixture = await requiredArtifactFixture(t, pair);
|
|
430
|
+
let calls = 0;
|
|
431
|
+
await assert.rejects(executeTask({ ...fixture.input, task: fixture.signed(),
|
|
432
|
+
providerExecutor: async () => { calls++; return fixture.providerResult; } }), /required_artifact_invalid/);
|
|
433
|
+
assert.equal(calls, 0);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
test('required artifact review v5: signed schemas bound raw artifact entries before deduplication', async (t) => {
|
|
437
|
+
const fixture = await requiredArtifactFixture(t, []);
|
|
438
|
+
const task = fixture.signed({ target: { ...fixture.unsigned.target, endpointId: randomUUID() } });
|
|
439
|
+
for (const [artifacts, valid] of [[Array(100).fill('reports/a'), true], [Array(101).fill('reports/a'), false],
|
|
440
|
+
[['x'.repeat(1000)], true], [['x'.repeat(1001)], false]]) {
|
|
441
|
+
const value = { ...task, acceptance: { ...task.acceptance, requiredArtifacts: artifacts } };
|
|
442
|
+
assert.equal(validateTaskEnvelopeContract(value).ok, valid);
|
|
443
|
+
assert.equal(validateTaskActionContract(canonicalTaskActionForTask(value, randomUUID())).ok, valid);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
test('required artifact review v6: acceptance count reserves capacity for a durable failure receipt', async (t) => {
|
|
447
|
+
const fixture = await requiredArtifactFixture(t, []);
|
|
448
|
+
const task = fixture.signed({ target: { ...fixture.unsigned.target, endpointId: randomUUID() } });
|
|
449
|
+
for (const [count, valid] of [[98, true], [99, false]]) {
|
|
450
|
+
const value = { ...task, acceptance: { ...task.acceptance,
|
|
451
|
+
commands: Array.from({ length: count }, () => ({ commandId: 'verify' })) } };
|
|
452
|
+
assert.equal(validateTaskEnvelopeContract(value).ok, valid);
|
|
453
|
+
assert.equal(validateTaskActionContract(canonicalTaskActionForTask(value, randomUUID())).ok, valid);
|
|
454
|
+
}
|
|
455
|
+
const oversized = fixture.signed({ acceptance: { ...fixture.unsigned.acceptance,
|
|
456
|
+
commands: Array.from({ length: 99 }, () => ({ commandId: 'verify' })) } });
|
|
457
|
+
let calls = 0;
|
|
458
|
+
await assert.rejects(executeTask({ ...fixture.input, task: oversized,
|
|
459
|
+
providerExecutor: async () => { calls++; return fixture.providerResult; } }), /schema validation|receipt capacity/);
|
|
460
|
+
assert.equal(calls, 0);
|
|
461
|
+
});
|
|
462
|
+
test('required artifact review v8: signed schemas reject empty artifact references', async (t) => {
|
|
463
|
+
const fixture = await requiredArtifactFixture(t, ['']);
|
|
464
|
+
const task = fixture.signed({ target: { ...fixture.unsigned.target, endpointId: randomUUID() } });
|
|
465
|
+
assert.equal(validateTaskEnvelopeContract(task).ok, false);
|
|
466
|
+
assert.equal(validateTaskActionContract(canonicalTaskActionForTask(task, randomUUID())).ok, false);
|
|
467
|
+
});
|
|
468
|
+
test('required artifact review v8: impossible ancestor file requirements fail before effects', async (t) => {
|
|
469
|
+
for (const artifacts of [['reports/result', 'reports/result/details.json'],
|
|
470
|
+
['reports/result/details.json', 'reports/result'], ['reports/RESULT', 'reports/result/details.json'],
|
|
471
|
+
['reports/\u00e9', 'reports/e\u0301/details.json']]) {
|
|
472
|
+
await t.test(JSON.stringify(artifacts), async (child) => {
|
|
473
|
+
const fixture = await requiredArtifactFixture(child, artifacts);
|
|
474
|
+
let calls = 0;
|
|
475
|
+
await assert.rejects(executeTask({ ...fixture.input, task: fixture.signed(),
|
|
476
|
+
providerExecutor: async () => { calls++; return fixture.providerResult; } }), /required_artifact_invalid/);
|
|
477
|
+
assert.equal(calls, 0);
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
});
|
|
10
481
|
test('signed task runs only a registered command in a relay-owned worktree and deduplicates delivery', async () => {
|
|
11
482
|
const root = await mkdtemp(resolve(tmpdir(), 'dharma-task-runner-'));
|
|
12
483
|
const workspace = resolve(root, 'workspace');
|