@open-agent-toolkit/cli 0.2.52 → 0.2.54
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/assets/agents/recon-worker.md +105 -0
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/tool-packs.md +62 -8
- package/assets/docs/contributing/skills.md +26 -0
- package/assets/docs/workflows/skills/index.md +3 -0
- package/assets/docs/workflows/skills/recon.md +194 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-dispatch-subagents/SKILL.md +105 -4
- package/assets/skills/oat-dispatch-subagents/references/record-schema.md +226 -0
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +2 -2
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/recon/SKILL.md +260 -0
- package/assets/skills/recon/references/packet-contract.md +307 -0
- package/assets/skills/recon/references/profiles.md +58 -0
- package/assets/skills/recon/references/worker-contract.md +71 -0
- package/assets/skills/recon/scripts/create-review-brief.mjs +452 -0
- package/assets/skills/recon/scripts/lib/canonical-json.mjs +55 -0
- package/assets/skills/recon/scripts/lib/contracts.mjs +2462 -0
- package/assets/skills/recon/scripts/lib/safe-path.mjs +120 -0
- package/assets/skills/recon/scripts/lib/validated-run.mjs +86 -0
- package/assets/skills/recon/scripts/reconcile-ledger.mjs +360 -0
- package/assets/skills/recon/scripts/render-packet.mjs +392 -0
- package/assets/skills/recon/scripts/validate-artifact.mjs +96 -0
- package/assets/skills/recon/scripts/validate-packet.mjs +2359 -0
- package/dist/commands/doctor/index.js +5 -5
- package/dist/commands/init/index.d.ts.map +1 -1
- package/dist/commands/init/index.js +7 -8
- package/dist/commands/status/index.d.ts +2 -2
- package/dist/commands/status/index.d.ts.map +1 -1
- package/dist/commands/status/index.js +8 -8
- package/dist/commands/sync/index.d.ts.map +1 -1
- package/dist/commands/sync/index.js +25 -9
- package/dist/commands/sync/sync.types.d.ts +2 -2
- package/dist/commands/sync/sync.types.d.ts.map +1 -1
- package/dist/commands/tools/migrate/index.d.ts.map +1 -1
- package/dist/commands/tools/migrate/index.js +25 -1
- package/dist/commands/tools/migrate/migrate-pack.d.ts +5 -0
- package/dist/commands/tools/migrate/migrate-pack.d.ts.map +1 -1
- package/dist/commands/tools/migrate/migrate-pack.js +66 -7
- package/dist/commands/tools/remove/index.js +1 -1
- package/dist/commands/tools/remove/remove-tools.d.ts +5 -1
- package/dist/commands/tools/remove/remove-tools.d.ts.map +1 -1
- package/dist/commands/tools/remove/remove-tools.js +81 -1
- package/dist/commands/tools/shared/apply-pack-reconcile.d.ts +3 -0
- package/dist/commands/tools/shared/apply-pack-reconcile.d.ts.map +1 -1
- package/dist/commands/tools/shared/apply-pack-reconcile.js +40 -9
- package/dist/commands/tools/shared/format-pack-inventory.d.ts.map +1 -1
- package/dist/commands/tools/shared/format-pack-inventory.js +3 -0
- package/dist/commands/tools/shared/install-sync-context.d.ts +2 -1
- package/dist/commands/tools/shared/install-sync-context.d.ts.map +1 -1
- package/dist/commands/tools/shared/install-sync-context.js +14 -3
- package/dist/commands/tools/shared/pack-dependencies.d.ts +14 -0
- package/dist/commands/tools/shared/pack-dependencies.d.ts.map +1 -0
- package/dist/commands/tools/shared/pack-dependencies.js +54 -0
- package/dist/commands/tools/shared/pack-inventory.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-inventory.js +9 -9
- package/dist/commands/tools/shared/pack-lifecycle.d.ts +9 -1
- package/dist/commands/tools/shared/pack-lifecycle.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-lifecycle.js +102 -9
- package/dist/commands/tools/shared/pack-manifest.d.ts +2 -2
- package/dist/commands/tools/shared/pack-manifest.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-manifest.js +91 -2
- package/dist/commands/tools/shared/pack-reconcile.d.ts +15 -1
- package/dist/commands/tools/shared/pack-reconcile.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-reconcile.js +75 -13
- package/dist/commands/tools/shared/scoped-pack-intent.d.ts +8 -0
- package/dist/commands/tools/shared/scoped-pack-intent.d.ts.map +1 -1
- package/dist/commands/tools/shared/scoped-pack-intent.js +61 -15
- package/dist/commands/tools/shared/types.d.ts +7 -0
- package/dist/commands/tools/shared/types.d.ts.map +1 -1
- package/dist/commands/tools/update/update-tools.d.ts.map +1 -1
- package/dist/commands/tools/update/update-tools.js +7 -1
- package/dist/config/oat-config.d.ts +5 -1
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +19 -0
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +1 -1
- package/dist/engine/scanner.d.ts +16 -1
- package/dist/engine/scanner.d.ts.map +1 -1
- package/dist/engine/scanner.js +98 -4
- package/dist/providers/codex/codec/sync-extension.d.ts +1 -1
- package/dist/providers/codex/codec/sync-extension.d.ts.map +1 -1
- package/dist/providers/codex/codec/sync-extension.js +7 -7
- package/dist/providers/cursor/codec/sync-extension.d.ts +1 -1
- package/dist/providers/cursor/codec/sync-extension.d.ts.map +1 -1
- package/dist/providers/cursor/codec/sync-extension.js +11 -13
- package/dist/shared/types.d.ts +1 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +3 -0
- package/package.json +2 -2
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { lstat, open, rename, rm } from 'node:fs/promises';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
|
|
8
|
+
import { hashFile } from './lib/canonical-json.mjs';
|
|
9
|
+
import {
|
|
10
|
+
assertSafeExistingPath,
|
|
11
|
+
assertSafeOutputPath,
|
|
12
|
+
assertUnchangedRoot,
|
|
13
|
+
} from './lib/safe-path.mjs';
|
|
14
|
+
import { assertValidatedRun } from './lib/validated-run.mjs';
|
|
15
|
+
import { compileValidatedRun } from './validate-packet.mjs';
|
|
16
|
+
|
|
17
|
+
function escapeInline(value) {
|
|
18
|
+
return String(value)
|
|
19
|
+
.replace(/([\\`*_[\]<>])/g, '\\$1')
|
|
20
|
+
.replace(/\r?\n/g, ' ');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatLocator(locator) {
|
|
24
|
+
if (locator.kind === 'repository') {
|
|
25
|
+
return `${locator.path}:${locator.lineStart}-${locator.lineEnd}@${locator.revision}`;
|
|
26
|
+
}
|
|
27
|
+
if (locator.kind === 'file') {
|
|
28
|
+
const range = locator.lineStart
|
|
29
|
+
? `:${locator.lineStart}${locator.lineEnd && locator.lineEnd !== locator.lineStart ? `-${locator.lineEnd}` : ''}`
|
|
30
|
+
: '';
|
|
31
|
+
return `${locator.path}${range}`;
|
|
32
|
+
}
|
|
33
|
+
if (locator.kind === 'url') {
|
|
34
|
+
return `${locator.url}${locator.fragment ? `#${locator.fragment}` : ''}`;
|
|
35
|
+
}
|
|
36
|
+
if (locator.kind === 'command-output') {
|
|
37
|
+
return `${locator.artifactPath}:${locator.lineStart}-${locator.lineEnd}`;
|
|
38
|
+
}
|
|
39
|
+
if (locator.kind === 'connected-resource') {
|
|
40
|
+
return `${locator.system}:${locator.resourceId}${locator.fieldOrSection ? `#${locator.fieldOrSection}` : ''}${locator.resourceVersion ? `@${locator.resourceVersion}` : ''}`;
|
|
41
|
+
}
|
|
42
|
+
return 'unknown locator';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function bulletLines(values, empty = 'None.') {
|
|
46
|
+
return values.length > 0
|
|
47
|
+
? values.map((value) => `- ${value}`)
|
|
48
|
+
: [`- ${empty}`];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function renderPacketDocument(validatedRun) {
|
|
52
|
+
const { manifest, ledger, topology } = assertValidatedRun(validatedRun);
|
|
53
|
+
const evidenceById = new Map(
|
|
54
|
+
ledger.evidence.map((evidence) => [evidence.id, evidence]),
|
|
55
|
+
);
|
|
56
|
+
const claimsById = new Map(ledger.claims.map((claim) => [claim.id, claim]));
|
|
57
|
+
const keyClaims = ledger.synthesis.keyClaimIds
|
|
58
|
+
.map((id) => claimsById.get(id))
|
|
59
|
+
.filter(Boolean);
|
|
60
|
+
const contradictions = ledger.claims.filter(
|
|
61
|
+
(claim) =>
|
|
62
|
+
claim.status === 'contested' ||
|
|
63
|
+
claim.status === 'unsupported' ||
|
|
64
|
+
(claim.challenges ?? []).length > 0,
|
|
65
|
+
);
|
|
66
|
+
const failedStages = topology.stages
|
|
67
|
+
.map(({ stage }) => stage)
|
|
68
|
+
.filter((stage) => stage.status !== 'complete');
|
|
69
|
+
const omittedGaps = manifest.gaps.filter((gap) =>
|
|
70
|
+
/PASS_(?:FAILED|OMITTED)/.test(gap.code),
|
|
71
|
+
);
|
|
72
|
+
const lines = [
|
|
73
|
+
'# Recon Evidence Packet',
|
|
74
|
+
'',
|
|
75
|
+
`- **Status:** ${escapeInline(manifest.run.status)}`,
|
|
76
|
+
`- **Requested profile:** ${escapeInline(manifest.run.requestedProfile)}`,
|
|
77
|
+
`- **Achieved profile:** ${escapeInline(manifest.run.achievedProfile ?? 'none')}`,
|
|
78
|
+
`- **Run:** ${escapeInline(manifest.run.id)}`,
|
|
79
|
+
`- **Topic:** ${escapeInline(manifest.run.topic)}`,
|
|
80
|
+
'',
|
|
81
|
+
'## Synthesis',
|
|
82
|
+
'',
|
|
83
|
+
ledger.synthesis.answer,
|
|
84
|
+
'',
|
|
85
|
+
...bulletLines(
|
|
86
|
+
ledger.synthesis.caveats.map((item) => `Caveat: ${escapeInline(item)}`),
|
|
87
|
+
),
|
|
88
|
+
'',
|
|
89
|
+
'## Key Claims',
|
|
90
|
+
'',
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
for (const claim of keyClaims) {
|
|
94
|
+
lines.push(`### ${escapeInline(claim.id)}`, '');
|
|
95
|
+
lines.push(`- **State:** **${escapeInline(claim.status)}**`);
|
|
96
|
+
lines.push(`- **Claim:** ${escapeInline(claim.statement)}`);
|
|
97
|
+
const links = claim.evidence
|
|
98
|
+
.map((link) => ({ link, evidence: evidenceById.get(link.evidenceId) }))
|
|
99
|
+
.filter(({ evidence }) => evidence);
|
|
100
|
+
if (links.length === 0) {
|
|
101
|
+
lines.push('- **Evidence:** None.');
|
|
102
|
+
} else {
|
|
103
|
+
lines.push('- **Evidence:**');
|
|
104
|
+
for (const { link, evidence } of links) {
|
|
105
|
+
lines.push(
|
|
106
|
+
` - ${escapeInline(link.relation)} — ${escapeInline(evidence.displayExcerpt)} (${escapeInline(formatLocator(evidence.locator))}; ${escapeInline(evidence.locatorValidation.status)})`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const qualification of claim.qualifications) {
|
|
111
|
+
lines.push(`- **Qualification:** ${escapeInline(qualification)}`);
|
|
112
|
+
}
|
|
113
|
+
lines.push('');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
lines.push('## Contradictions and Qualifications', '');
|
|
117
|
+
lines.push(
|
|
118
|
+
...bulletLines(
|
|
119
|
+
contradictions.map(
|
|
120
|
+
(claim) =>
|
|
121
|
+
`**${escapeInline(claim.id)}** (${escapeInline(claim.status)}): ${escapeInline(claim.statement)}${claim.qualifications.length ? ` — ${escapeInline(claim.qualifications.join('; '))}` : ''}`,
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
'',
|
|
125
|
+
'## Unresolved Questions',
|
|
126
|
+
'',
|
|
127
|
+
...bulletLines(
|
|
128
|
+
ledger.unresolvedQuestions.map(
|
|
129
|
+
(question) =>
|
|
130
|
+
`**${escapeInline(question.id)}:** ${escapeInline(question.question ?? question.text ?? '')}`,
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
'',
|
|
134
|
+
'## Coverage Gaps',
|
|
135
|
+
'',
|
|
136
|
+
...bulletLines(
|
|
137
|
+
manifest.gaps.map(
|
|
138
|
+
(gap) => `**${escapeInline(gap.code)}:** ${escapeInline(gap.message)}`,
|
|
139
|
+
),
|
|
140
|
+
),
|
|
141
|
+
'',
|
|
142
|
+
'## Failed or Omitted Passes',
|
|
143
|
+
'',
|
|
144
|
+
...bulletLines([
|
|
145
|
+
...failedStages.map(
|
|
146
|
+
(stage) =>
|
|
147
|
+
`**${escapeInline(stage.mode)}:** ${escapeInline(stage.status)}${stage.message ? ` — ${escapeInline(stage.message)}` : ''}`,
|
|
148
|
+
),
|
|
149
|
+
...omittedGaps.map(
|
|
150
|
+
(gap) => `**${escapeInline(gap.code)}:** ${escapeInline(gap.message)}`,
|
|
151
|
+
),
|
|
152
|
+
]),
|
|
153
|
+
'',
|
|
154
|
+
'## Provenance',
|
|
155
|
+
'',
|
|
156
|
+
'- Machine-readable manifest: [`manifest.json`](manifest.json)',
|
|
157
|
+
'- Canonical claim ledger: [`claims.json`](claims.json)',
|
|
158
|
+
'- Compact review artifacts: [`reviews/`](reviews/)',
|
|
159
|
+
'- Exact locators are scoped to the source identities and observations in `manifest.json`.',
|
|
160
|
+
'',
|
|
161
|
+
);
|
|
162
|
+
return lines.join('\n');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function claimCounts(ledger) {
|
|
166
|
+
return Object.fromEntries(
|
|
167
|
+
[...ledger.claims]
|
|
168
|
+
.sort((a, b) => a.status.localeCompare(b.status))
|
|
169
|
+
.reduce((counts, claim) => {
|
|
170
|
+
counts.set(claim.status, (counts.get(claim.status) ?? 0) + 1);
|
|
171
|
+
return counts;
|
|
172
|
+
}, new Map()),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function hashOpenFile(file) {
|
|
177
|
+
const hash = createHash('sha256');
|
|
178
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
179
|
+
let position = 0;
|
|
180
|
+
while (true) {
|
|
181
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, position);
|
|
182
|
+
if (bytesRead === 0) break;
|
|
183
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
184
|
+
position += bytesRead;
|
|
185
|
+
}
|
|
186
|
+
return `sha256:${hash.digest('hex')}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function assertFileIdentity(path, identity) {
|
|
190
|
+
const current = await lstat(path);
|
|
191
|
+
if (
|
|
192
|
+
current.isSymbolicLink() ||
|
|
193
|
+
!current.isFile() ||
|
|
194
|
+
current.dev !== identity.device ||
|
|
195
|
+
current.ino !== identity.inode
|
|
196
|
+
) {
|
|
197
|
+
throw Object.assign(
|
|
198
|
+
new Error('Rendered packet file identity changed before publication'),
|
|
199
|
+
{ code: 'PACKET_FILE_IDENTITY_CHANGED' },
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function packetRootIdentity(run) {
|
|
205
|
+
return run.filesystemIdentities.find(
|
|
206
|
+
(identity) => identity.path === run.packetRoot,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function assertCanonicalPacketBytes(run) {
|
|
211
|
+
const rootIdentity = packetRootIdentity(run);
|
|
212
|
+
for (const retained of run.canonicalByteDigests) {
|
|
213
|
+
const path = join(run.packetRoot, retained.path);
|
|
214
|
+
try {
|
|
215
|
+
await assertUnchangedRoot(rootIdentity);
|
|
216
|
+
await assertSafeExistingPath(run.packetRoot, path);
|
|
217
|
+
if ((await hashFile(path)) !== retained.digest) {
|
|
218
|
+
throw new Error(`Canonical packet bytes changed: ${retained.path}`);
|
|
219
|
+
}
|
|
220
|
+
} catch (cause) {
|
|
221
|
+
if (
|
|
222
|
+
cause?.code === 'ROOT_IDENTITY_CHANGED' ||
|
|
223
|
+
cause?.code === 'SYMLINK_ESCAPE'
|
|
224
|
+
) {
|
|
225
|
+
throw cause;
|
|
226
|
+
}
|
|
227
|
+
throw Object.assign(
|
|
228
|
+
new Error(`Canonical packet bytes changed: ${retained.path}`, {
|
|
229
|
+
cause,
|
|
230
|
+
}),
|
|
231
|
+
{ code: 'PACKET_CANONICAL_BYTES_CHANGED' },
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
await assertUnchangedRoot(rootIdentity);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function withdrawPacket(run, target, originalError) {
|
|
239
|
+
try {
|
|
240
|
+
await assertUnchangedRoot(packetRootIdentity(run));
|
|
241
|
+
} catch (identityError) {
|
|
242
|
+
if (
|
|
243
|
+
originalError?.code === 'ROOT_IDENTITY_CHANGED' ||
|
|
244
|
+
originalError?.code === 'SYMLINK_ESCAPE'
|
|
245
|
+
) {
|
|
246
|
+
throw originalError;
|
|
247
|
+
}
|
|
248
|
+
throw identityError;
|
|
249
|
+
}
|
|
250
|
+
await rm(target, { force: true });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function removeTemporaryIfRootUnchanged(run, temporary) {
|
|
254
|
+
try {
|
|
255
|
+
await assertUnchangedRoot(packetRootIdentity(run));
|
|
256
|
+
} catch {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
await rm(temporary, { force: true });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function renderPacket(packetDirectory) {
|
|
263
|
+
const packetRoot = resolve(packetDirectory);
|
|
264
|
+
const validation = await compileValidatedRun(packetRoot);
|
|
265
|
+
if (!validation.valid || !validation.publishable) {
|
|
266
|
+
const diagnosticCodes = validation.errors.map((error) => error.code);
|
|
267
|
+
throw Object.assign(
|
|
268
|
+
new Error(`Packet validation failed: ${diagnosticCodes.join(', ')}`),
|
|
269
|
+
{
|
|
270
|
+
code: diagnosticCodes.includes('PACKET_NOT_PUBLISHABLE')
|
|
271
|
+
? 'PACKET_NOT_PUBLISHABLE'
|
|
272
|
+
: 'PACKET_VALIDATION_FAILED',
|
|
273
|
+
},
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return renderValidatedPacket(validation.validatedRun);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function renderValidatedPacket(
|
|
280
|
+
validatedRun,
|
|
281
|
+
{ promote = rename, afterPromotionChecks = () => {} } = {},
|
|
282
|
+
) {
|
|
283
|
+
const run = assertValidatedRun(validatedRun);
|
|
284
|
+
const { manifest, ledger, packetRoot } = run;
|
|
285
|
+
if (manifest.run.status !== 'complete' && manifest.run.status !== 'partial') {
|
|
286
|
+
throw Object.assign(
|
|
287
|
+
new Error(
|
|
288
|
+
`Cannot render non-publishable run status: ${manifest.run.status}`,
|
|
289
|
+
),
|
|
290
|
+
{ code: 'PACKET_NOT_PUBLISHABLE' },
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
const target = join(packetRoot, 'packet.md');
|
|
294
|
+
const temporary = join(packetRoot, `.packet.md.${randomUUID()}.tmp`);
|
|
295
|
+
let temporaryFile;
|
|
296
|
+
try {
|
|
297
|
+
const document = renderPacketDocument(run);
|
|
298
|
+
await Promise.all(
|
|
299
|
+
run.filesystemIdentities.map((identity) => assertUnchangedRoot(identity)),
|
|
300
|
+
);
|
|
301
|
+
await assertSafeOutputPath(packetRoot, temporary);
|
|
302
|
+
await assertSafeOutputPath(packetRoot, target);
|
|
303
|
+
temporaryFile = await open(temporary, 'wx+', 0o600);
|
|
304
|
+
const temporaryStat = await temporaryFile.stat();
|
|
305
|
+
const temporaryIdentity = {
|
|
306
|
+
device: temporaryStat.dev,
|
|
307
|
+
inode: temporaryStat.ino,
|
|
308
|
+
};
|
|
309
|
+
await temporaryFile.writeFile(document, 'utf8');
|
|
310
|
+
await temporaryFile.sync();
|
|
311
|
+
const digest = await hashOpenFile(temporaryFile);
|
|
312
|
+
await assertFileIdentity(temporary, temporaryIdentity);
|
|
313
|
+
await Promise.all(
|
|
314
|
+
run.filesystemIdentities.map((identity) => assertUnchangedRoot(identity)),
|
|
315
|
+
);
|
|
316
|
+
const result = {
|
|
317
|
+
directory: packetRoot,
|
|
318
|
+
status: manifest.run.status,
|
|
319
|
+
requestedProfile: manifest.run.requestedProfile,
|
|
320
|
+
achievedProfile: run.achievedProfile,
|
|
321
|
+
claimCounts: claimCounts(ledger),
|
|
322
|
+
gapCount: manifest.gaps.length,
|
|
323
|
+
failedOrOmittedPasses: [
|
|
324
|
+
...run.topology.stages
|
|
325
|
+
.map(({ stage }) => stage)
|
|
326
|
+
.filter((stage) => stage.status !== 'complete')
|
|
327
|
+
.map((stage) => stage.mode),
|
|
328
|
+
...manifest.gaps
|
|
329
|
+
.filter((gap) => /PASS_(?:FAILED|OMITTED)/.test(gap.code))
|
|
330
|
+
.map((gap) => gap.code),
|
|
331
|
+
],
|
|
332
|
+
digest,
|
|
333
|
+
};
|
|
334
|
+
await assertCanonicalPacketBytes(run);
|
|
335
|
+
await promote(temporary, target);
|
|
336
|
+
await assertFileIdentity(target, temporaryIdentity);
|
|
337
|
+
const [promotedDigest, retainedDigest] = await Promise.all([
|
|
338
|
+
hashFile(target),
|
|
339
|
+
hashOpenFile(temporaryFile),
|
|
340
|
+
]);
|
|
341
|
+
if (promotedDigest !== digest || retainedDigest !== digest) {
|
|
342
|
+
throw Object.assign(
|
|
343
|
+
new Error('Promoted packet digest differs from rendered bytes'),
|
|
344
|
+
{ code: 'PACKET_DIGEST_MISMATCH' },
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
await Promise.all(
|
|
348
|
+
run.filesystemIdentities.map((identity) => assertUnchangedRoot(identity)),
|
|
349
|
+
);
|
|
350
|
+
await afterPromotionChecks();
|
|
351
|
+
await assertCanonicalPacketBytes(run);
|
|
352
|
+
return result;
|
|
353
|
+
} catch (error) {
|
|
354
|
+
try {
|
|
355
|
+
await withdrawPacket(run, target, error);
|
|
356
|
+
} catch (withdrawalError) {
|
|
357
|
+
if (
|
|
358
|
+
withdrawalError === error ||
|
|
359
|
+
withdrawalError?.code === 'ROOT_IDENTITY_CHANGED' ||
|
|
360
|
+
withdrawalError?.code === 'SYMLINK_ESCAPE'
|
|
361
|
+
) {
|
|
362
|
+
throw withdrawalError;
|
|
363
|
+
}
|
|
364
|
+
throw new Error(
|
|
365
|
+
`Packet rendering failed (${error instanceof Error ? error.message : error}) and the consumer entry point could not be withdrawn`,
|
|
366
|
+
{ cause: withdrawalError },
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
throw error;
|
|
370
|
+
} finally {
|
|
371
|
+
await temporaryFile?.close();
|
|
372
|
+
await removeTemporaryIfRootUnchanged(run, temporary);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function main(argv) {
|
|
377
|
+
const [packetDirectory] = argv;
|
|
378
|
+
if (!packetDirectory)
|
|
379
|
+
throw new Error('Usage: render-packet.mjs <packet-directory>');
|
|
380
|
+
const result = await renderPacket(packetDirectory);
|
|
381
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (
|
|
385
|
+
process.argv[1] &&
|
|
386
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
387
|
+
) {
|
|
388
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
389
|
+
process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
|
|
390
|
+
process.exitCode = 1;
|
|
391
|
+
});
|
|
392
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { basename, join, resolve } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
7
|
+
import { validateArtifactShape } from './lib/contracts.mjs';
|
|
8
|
+
import {
|
|
9
|
+
assertSafeExistingPath,
|
|
10
|
+
assertSafeOutputPath,
|
|
11
|
+
} from './lib/safe-path.mjs';
|
|
12
|
+
|
|
13
|
+
export function validateArtifactValue(value) {
|
|
14
|
+
return validateArtifactShape(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function validateArtifactFile(path) {
|
|
18
|
+
try {
|
|
19
|
+
const value = JSON.parse(await readFile(path, 'utf8'));
|
|
20
|
+
return { ...validateArtifactValue(value), path: resolve(path) };
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return {
|
|
23
|
+
valid: false,
|
|
24
|
+
path: resolve(path),
|
|
25
|
+
errors: [
|
|
26
|
+
{
|
|
27
|
+
code: 'INVALID_JSON',
|
|
28
|
+
message: error instanceof Error ? error.message : 'Invalid JSON',
|
|
29
|
+
path: '$',
|
|
30
|
+
severity: 'error',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function quarantineInvalidArtifact(path, packetRoot, result) {
|
|
38
|
+
if (result.valid) return null;
|
|
39
|
+
const root = resolve(packetRoot);
|
|
40
|
+
await assertSafeExistingPath(root, resolve(path));
|
|
41
|
+
const quarantineRoot = join(root, 'raw', 'quarantine');
|
|
42
|
+
await assertSafeOutputPath(root, quarantineRoot);
|
|
43
|
+
await mkdir(quarantineRoot, { recursive: true });
|
|
44
|
+
const target = join(quarantineRoot, `${basename(path)}.invalid`);
|
|
45
|
+
await assertSafeOutputPath(root, target);
|
|
46
|
+
await assertSafeOutputPath(root, `${target}.failure.json`);
|
|
47
|
+
await rename(path, target);
|
|
48
|
+
await writeFile(
|
|
49
|
+
`${target}.failure.json`,
|
|
50
|
+
`${JSON.stringify(
|
|
51
|
+
{
|
|
52
|
+
kind: 'recon.validation-failure',
|
|
53
|
+
schemaVersion: 1,
|
|
54
|
+
artifact: basename(path),
|
|
55
|
+
errors: result.errors,
|
|
56
|
+
},
|
|
57
|
+
null,
|
|
58
|
+
2,
|
|
59
|
+
)}\n`,
|
|
60
|
+
'utf8',
|
|
61
|
+
);
|
|
62
|
+
return target;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function main(argv) {
|
|
66
|
+
const args = [...argv];
|
|
67
|
+
const quarantineIndex = args.indexOf('--quarantine-root');
|
|
68
|
+
const quarantineRoot =
|
|
69
|
+
quarantineIndex === -1 ? null : args.splice(quarantineIndex, 2)[1];
|
|
70
|
+
const [path] = args;
|
|
71
|
+
if (!path) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
'Usage: validate-artifact.mjs <artifact.json> [--quarantine-root <packet-dir>]',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const result = await validateArtifactFile(path);
|
|
77
|
+
if (!result.valid && quarantineRoot) {
|
|
78
|
+
result.quarantinedPath = await quarantineInvalidArtifact(
|
|
79
|
+
path,
|
|
80
|
+
quarantineRoot,
|
|
81
|
+
result,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
85
|
+
process.exitCode = result.valid ? 0 : 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
process.argv[1] &&
|
|
90
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
91
|
+
) {
|
|
92
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
93
|
+
process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
|
|
94
|
+
process.exitCode = 2;
|
|
95
|
+
});
|
|
96
|
+
}
|