archgraph-argo 0.10.43 → 0.10.44
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/LICENSE +201 -201
- package/argo/package.json +8 -8
- package/argo/schema/ImplementationToCodingHandoff.schema.json +251 -251
- package/argo/schema/ImplementationToIntentTraceProposal.schema.json +180 -180
- package/argo/schema/IntentToImplementationHandoff.schema.json +74 -74
- package/argo/schema/SystemArchitecture.schema.json +378 -378
- package/argo/schema/archimate3.2.md +7153 -7153
- package/argo/scripts/archimate32-rules.js +12301 -12301
- package/argo/scripts/generateArchitectureDiffPlantuml.js +466 -466
- package/argo/scripts/graph-rag/ARCHITECTURE.md +192 -192
- package/argo/scripts/graph-rag/canonicalProjectionAuthority.js +45 -45
- package/argo/scripts/graph-rag/embeddingQualificationGate.js +59 -59
- package/argo/scripts/graph-rag/externalProductionConfig.js +74 -74
- package/argo/scripts/graph-rag/liveEmbeddingProviderClient.js +49 -49
- package/argo/scripts/graph-rag/neo4jNativeRetrieval.js +37 -37
- package/argo/scripts/graph-rag/semantic-persistence/ARCHITECTURE.md +51 -51
- package/argo/scripts/graph-rag/semantic-persistence/productionSemanticCheckpointStore.js +99 -99
- package/argo/scripts/graph-rag/semantic-persistence/productionSemanticProjectionStore.js +171 -171
- package/argo/scripts/graph-rag/semanticOperatorError.js +38 -38
- package/argo/scripts/graph-rag/semanticOperatorJourney.js +459 -459
- package/argo/scripts/graph-rag/semanticReadinessAttestationStore.js +398 -398
- package/argo/scripts/graph-rag/systemMetadataCommandAdapter.js +269 -269
- package/argo/scripts/graph-semantics.js +220 -220
- package/argo/scripts/repositoryArgoEnvironment.js +101 -101
- package/argo/scripts/runArchitectureTests.js +583 -583
- package/argo/scripts/semanticOperatorJourneyCli.js +91 -91
- package/argo/scripts/syncSystemArchitectureToNeo4j.js +66 -66
- package/argo/scripts/test-executors/_template.js +58 -58
- package/argo/scripts/test-executors/default.js +199 -199
- package/argo/scripts/validateStageHandoff.js +458 -458
- package/argo/scripts/validateSystemArchitecture.js +253 -253
- package/argo/scripts/validateTraceProposal.js +181 -181
- package/bin/argo-deploy.js +12 -12
- package/install-argo.ps1 +27 -8
- package/package.json +1 -1
|
@@ -1,398 +1,398 @@
|
|
|
1
|
-
const crypto = require('node:crypto');
|
|
2
|
-
const fs = require('node:fs');
|
|
3
|
-
const path = require('node:path');
|
|
4
|
-
|
|
5
|
-
const ATTESTATION_PATH = '.argo/temp/semantic-readiness-attestation.json';
|
|
6
|
-
const SCHEMA_VERSION = '1.0';
|
|
7
|
-
const ACL_REMEDIATION = 'Restrict .argo/temp and semantic-readiness-attestation.json ownership and ACLs to the current OS identity and SYSTEM, then run semantic readiness again';
|
|
8
|
-
const FIELDS = Object.freeze([
|
|
9
|
-
'schemaVersion',
|
|
10
|
-
'authorizationOperation',
|
|
11
|
-
'graphPath',
|
|
12
|
-
'verified',
|
|
13
|
-
'canonicalVersion',
|
|
14
|
-
'contentVersion',
|
|
15
|
-
'indexVersion',
|
|
16
|
-
'completedChannels',
|
|
17
|
-
'missingChannels',
|
|
18
|
-
'mismatchedChannels',
|
|
19
|
-
'fullSnapshotFallback',
|
|
20
|
-
'canonicalDigest',
|
|
21
|
-
'integrityDigest',
|
|
22
|
-
]);
|
|
23
|
-
|
|
24
|
-
function createSemanticReadinessAttestationStore(options = {}) {
|
|
25
|
-
const repositoryRoot = path.resolve(options.repositoryRoot);
|
|
26
|
-
const graphPath = normalizeGraphPath(options.graphPath);
|
|
27
|
-
const metadataAdapter = options.metadataAdapter;
|
|
28
|
-
const attestationPath = path.join(repositoryRoot, ...ATTESTATION_PATH.split('/'));
|
|
29
|
-
const directoryPath = path.dirname(attestationPath);
|
|
30
|
-
const canonicalPath = path.join(repositoryRoot, ...graphPath.split('/'));
|
|
31
|
-
|
|
32
|
-
return Object.freeze({
|
|
33
|
-
record(readiness) {
|
|
34
|
-
const record = buildRecord(readiness, {
|
|
35
|
-
repositoryRoot,
|
|
36
|
-
graphPath,
|
|
37
|
-
canonicalPath,
|
|
38
|
-
});
|
|
39
|
-
writeAttestationAtomically(record, {
|
|
40
|
-
attestationPath,
|
|
41
|
-
directoryPath,
|
|
42
|
-
metadataAdapter,
|
|
43
|
-
});
|
|
44
|
-
return Object.freeze(record);
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
read() {
|
|
48
|
-
if (!fs.existsSync(attestationPath)) return null;
|
|
49
|
-
assertPathTrust(attestationPath, directoryPath);
|
|
50
|
-
let record;
|
|
51
|
-
try {
|
|
52
|
-
record = JSON.parse(fs.readFileSync(attestationPath, 'utf8'));
|
|
53
|
-
} catch {
|
|
54
|
-
throw attestationError('SEMANTIC_READINESS_ATTESTATION_INVALID');
|
|
55
|
-
}
|
|
56
|
-
assertRecord(record, repositoryRoot);
|
|
57
|
-
assertOperatingSystemTrust({
|
|
58
|
-
attestationPath,
|
|
59
|
-
directoryPath,
|
|
60
|
-
metadataAdapter,
|
|
61
|
-
});
|
|
62
|
-
if (digestFile(canonicalPath) !== record.canonicalDigest) {
|
|
63
|
-
throw attestationError('SEMANTIC_READINESS_ATTESTATION_STALE');
|
|
64
|
-
}
|
|
65
|
-
return Object.freeze(record);
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
clear() {
|
|
69
|
-
try {
|
|
70
|
-
fs.rmSync(attestationPath, { force: true });
|
|
71
|
-
} catch {
|
|
72
|
-
throw attestationError('SEMANTIC_READINESS_ATTESTATION_INVALID');
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
|
|
76
|
-
validate(attestation, readiness) {
|
|
77
|
-
assertOperatingSystemTrust({
|
|
78
|
-
attestationPath,
|
|
79
|
-
directoryPath,
|
|
80
|
-
metadataAdapter,
|
|
81
|
-
});
|
|
82
|
-
if (digestFile(canonicalPath) !== attestation.canonicalDigest) return false;
|
|
83
|
-
return exactReadinessMatch(attestation, readiness);
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function buildRecord(readiness, context) {
|
|
89
|
-
const record = {
|
|
90
|
-
schemaVersion: SCHEMA_VERSION,
|
|
91
|
-
authorizationOperation: 'verifyReadiness',
|
|
92
|
-
graphPath: context.graphPath,
|
|
93
|
-
verified: readiness.verified === true,
|
|
94
|
-
canonicalVersion: readiness.canonicalVersion,
|
|
95
|
-
contentVersion: readiness.contentVersion,
|
|
96
|
-
indexVersion: readiness.indexVersion,
|
|
97
|
-
completedChannels: copyChannels(readiness.completedChannels),
|
|
98
|
-
missingChannels: copyChannels(readiness.missingChannels),
|
|
99
|
-
mismatchedChannels: copyChannels(readiness.mismatchedChannels),
|
|
100
|
-
fullSnapshotFallback: false,
|
|
101
|
-
canonicalDigest: digestFile(context.canonicalPath),
|
|
102
|
-
};
|
|
103
|
-
record.integrityDigest = integrityDigest(record, context.repositoryRoot);
|
|
104
|
-
assertRecord(record, context.repositoryRoot);
|
|
105
|
-
return record;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function writeAttestationAtomically(readiness, context) {
|
|
109
|
-
fs.mkdirSync(context.directoryPath, { recursive: true, mode: 0o700 });
|
|
110
|
-
assertPathTrust(undefined, context.directoryPath);
|
|
111
|
-
const temporaryPath = `${context.attestationPath}.${process.pid}.${crypto.randomBytes(12).toString('hex')}.tmp`;
|
|
112
|
-
let descriptor;
|
|
113
|
-
try {
|
|
114
|
-
descriptor = fs.openSync(temporaryPath, 'wx', 0o600);
|
|
115
|
-
fs.writeSync(descriptor, `${JSON.stringify(readiness)}\n`, null, 'utf8');
|
|
116
|
-
fs.fsyncSync(descriptor);
|
|
117
|
-
fs.closeSync(descriptor);
|
|
118
|
-
descriptor = undefined;
|
|
119
|
-
fs.renameSync(temporaryPath, context.attestationPath);
|
|
120
|
-
assertMetadataResult(context.metadataAdapter.readReadinessAttestationAcl());
|
|
121
|
-
assertMetadataResult(context.metadataAdapter.readReadinessAttestationOwner());
|
|
122
|
-
assertOperatingSystemTrust(context);
|
|
123
|
-
if (process.platform === 'win32') {
|
|
124
|
-
if (path.dirname(temporaryPath) !== path.dirname(context.attestationPath)) {
|
|
125
|
-
throw attestationError('ATTESTATION_RENAME_VOLUME_CHANGED');
|
|
126
|
-
}
|
|
127
|
-
recordDirectoryFlushFallback('WINDOWS_DIRECTORY_FSYNC_UNSUPPORTED_SAME_DIRECTORY_RENAME');
|
|
128
|
-
} else {
|
|
129
|
-
const directoryDescriptor = fs.openSync(path.dirname(context.attestationPath), 'r');
|
|
130
|
-
fs.fsyncSync(directoryDescriptor);
|
|
131
|
-
fs.closeSync(directoryDescriptor);
|
|
132
|
-
}
|
|
133
|
-
return readiness;
|
|
134
|
-
} catch (error) {
|
|
135
|
-
if (descriptor !== undefined) {
|
|
136
|
-
try {
|
|
137
|
-
fs.closeSync(descriptor);
|
|
138
|
-
} catch {}
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
fs.rmSync(temporaryPath, { force: true });
|
|
142
|
-
} catch {}
|
|
143
|
-
if (error && error.category) throw error;
|
|
144
|
-
throw attestationError('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function assertRecord(record, repositoryRoot) {
|
|
149
|
-
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
|
150
|
-
throw attestationError('SEMANTIC_READINESS_ATTESTATION_INVALID');
|
|
151
|
-
}
|
|
152
|
-
if (
|
|
153
|
-
Object.keys(record).sort().join('\n') !== [...FIELDS].sort().join('\n')
|
|
154
|
-
|| record.schemaVersion !== SCHEMA_VERSION
|
|
155
|
-
|| record.authorizationOperation !== 'verifyReadiness'
|
|
156
|
-
|| record.verified !== true
|
|
157
|
-
|| record.fullSnapshotFallback !== false
|
|
158
|
-
|| !isString(record.graphPath)
|
|
159
|
-
|| !isString(record.canonicalVersion)
|
|
160
|
-
|| !isString(record.contentVersion)
|
|
161
|
-
|| !isString(record.indexVersion)
|
|
162
|
-
|| !isString(record.canonicalDigest)
|
|
163
|
-
|| !isString(record.integrityDigest)
|
|
164
|
-
|| !areChannels(record.completedChannels)
|
|
165
|
-
|| !areChannels(record.missingChannels)
|
|
166
|
-
|| !areChannels(record.mismatchedChannels)
|
|
167
|
-
|| record.integrityDigest !== integrityDigest(record, repositoryRoot)
|
|
168
|
-
) {
|
|
169
|
-
throw attestationError('SEMANTIC_READINESS_ATTESTATION_INVALID');
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function assertPathTrust(attestationPath, directoryPath) {
|
|
174
|
-
let directory;
|
|
175
|
-
try {
|
|
176
|
-
directory = fs.lstatSync(directoryPath);
|
|
177
|
-
} catch {
|
|
178
|
-
throw attestationError('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
179
|
-
}
|
|
180
|
-
if (!directory.isDirectory() || directory.isSymbolicLink()) {
|
|
181
|
-
throw attestationError('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
182
|
-
}
|
|
183
|
-
if (!attestationPath) return;
|
|
184
|
-
let file;
|
|
185
|
-
try {
|
|
186
|
-
file = fs.lstatSync(attestationPath);
|
|
187
|
-
} catch {
|
|
188
|
-
throw attestationError('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
189
|
-
}
|
|
190
|
-
if (!file.isFile() || file.isSymbolicLink()) {
|
|
191
|
-
throw attestationError('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function assertOperatingSystemTrust(context) {
|
|
196
|
-
assertPathTrust(context.attestationPath, context.directoryPath);
|
|
197
|
-
if (process.platform === 'win32') {
|
|
198
|
-
const identity = assertCommandSucceeded(context.metadataAdapter, 'readCurrentIdentity');
|
|
199
|
-
const directoryAcl = normalizeWindowsAclEvidence(assertCommandSucceeded(
|
|
200
|
-
context.metadataAdapter,
|
|
201
|
-
'readReadinessAttestationDirectoryAcl',
|
|
202
|
-
), context.directoryPath);
|
|
203
|
-
const fileAcl = normalizeWindowsAclEvidence(
|
|
204
|
-
assertCommandSucceeded(context.metadataAdapter, 'readReadinessAttestationAcl'),
|
|
205
|
-
context.attestationPath,
|
|
206
|
-
);
|
|
207
|
-
const owner = assertCommandSucceeded(
|
|
208
|
-
context.metadataAdapter,
|
|
209
|
-
'readReadinessAttestationOwner',
|
|
210
|
-
);
|
|
211
|
-
assertWindowsAclTrust({ identity, owner, directoryAcl, fileAcl });
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
const file = fs.lstatSync(context.attestationPath);
|
|
215
|
-
const directory = fs.lstatSync(context.directoryPath);
|
|
216
|
-
if (
|
|
217
|
-
(file.mode & 0o077) !== 0
|
|
218
|
-
|| (directory.mode & 0o077) !== 0
|
|
219
|
-
|| (typeof process.getuid === 'function'
|
|
220
|
-
&& (file.uid !== process.getuid() || directory.uid !== process.getuid()))
|
|
221
|
-
) {
|
|
222
|
-
throw attestationError('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function assertCommandSucceeded(adapter, capability) {
|
|
227
|
-
if (!adapter || typeof adapter[capability] !== 'function') {
|
|
228
|
-
throw attestationError('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
229
|
-
}
|
|
230
|
-
const result = adapter[capability]();
|
|
231
|
-
if (!result || result.status !== 0 || typeof result.stdout !== 'string') {
|
|
232
|
-
throw attestationError('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
233
|
-
}
|
|
234
|
-
return result.stdout;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function assertMetadataResult(result) {
|
|
238
|
-
if (!result || result.status !== 0 || typeof result.stdout !== 'string') {
|
|
239
|
-
throw attestationError('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
240
|
-
}
|
|
241
|
-
return result.stdout;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function exactReadinessMatch(attestation, readiness) {
|
|
245
|
-
return Boolean(
|
|
246
|
-
readiness
|
|
247
|
-
&& readiness.verified === true
|
|
248
|
-
&& attestation.canonicalVersion === readiness.canonicalVersion
|
|
249
|
-
&& attestation.contentVersion === readiness.contentVersion
|
|
250
|
-
&& attestation.indexVersion === readiness.indexVersion
|
|
251
|
-
&& sameArray(attestation.completedChannels, readiness.completedChannels)
|
|
252
|
-
&& sameArray(attestation.missingChannels, readiness.missingChannels)
|
|
253
|
-
&& sameArray(attestation.mismatchedChannels, readiness.mismatchedChannels)
|
|
254
|
-
&& readiness.fullSnapshotFallback === false
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function integrityDigest(record, repositoryRoot) {
|
|
259
|
-
const evidence = {};
|
|
260
|
-
for (const field of FIELDS) {
|
|
261
|
-
if (field !== 'integrityDigest') evidence[field] = record[field];
|
|
262
|
-
}
|
|
263
|
-
return digest(`${path.resolve(repositoryRoot)}\n${JSON.stringify(evidence)}`);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function digestFile(filePath) {
|
|
267
|
-
try {
|
|
268
|
-
return digest(fs.readFileSync(filePath));
|
|
269
|
-
} catch {
|
|
270
|
-
throw attestationError('SEMANTIC_READINESS_ATTESTATION_INVALID');
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function digest(value) {
|
|
275
|
-
return crypto.createHash('sha256').update(value).digest('hex');
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function copyChannels(value) {
|
|
279
|
-
return Array.isArray(value) ? [...value] : [];
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function sameArray(left, right) {
|
|
283
|
-
return Array.isArray(left)
|
|
284
|
-
&& Array.isArray(right)
|
|
285
|
-
&& left.length === right.length
|
|
286
|
-
&& left.every((value, index) => value === right[index]);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function areChannels(value) {
|
|
290
|
-
return Array.isArray(value) && value.every(isString);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function isString(value) {
|
|
294
|
-
return typeof value === 'string' && value.length > 0;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function normalizeGraphPath(value) {
|
|
298
|
-
if (typeof value !== 'string' || value.trim() === '') {
|
|
299
|
-
throw new TypeError('graphPath is required');
|
|
300
|
-
}
|
|
301
|
-
return value.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function normalizeIdentity(value) {
|
|
305
|
-
return String(value).trim().toLowerCase();
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function normalizeWindowsAclEvidence(value, subjectPath) {
|
|
309
|
-
const lines = String(value).split(/\r?\n/)
|
|
310
|
-
.map(line => line.trim())
|
|
311
|
-
.filter(line => (
|
|
312
|
-
line
|
|
313
|
-
&& !/^Successfully processed \d+ files?; Failed processing \d+ files?$/i.test(line)
|
|
314
|
-
&& !/\b\d+\b.*;\s*.*\b\d+\b/.test(line)
|
|
315
|
-
));
|
|
316
|
-
if (lines.length > 0) {
|
|
317
|
-
const prefix = String(subjectPath);
|
|
318
|
-
if (lines[0].toLowerCase().startsWith(prefix.toLowerCase())) {
|
|
319
|
-
lines[0] = lines[0].slice(prefix.length).trim();
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
return lines.filter(Boolean).join('\n');
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function parseWindowsAcl(value) {
|
|
326
|
-
const lines = String(value).split(/\r?\n/).filter(Boolean);
|
|
327
|
-
if (lines.length === 0) throw windowsTrustError();
|
|
328
|
-
return lines.map(line => {
|
|
329
|
-
const match = line.match(/^(.+?):((?:\([^)]*\))+?)$/);
|
|
330
|
-
if (!match) throw windowsTrustError();
|
|
331
|
-
const flags = Array.from(
|
|
332
|
-
match[2].matchAll(/\(([^)]*)\)/g),
|
|
333
|
-
entry => entry[1].toUpperCase(),
|
|
334
|
-
);
|
|
335
|
-
return {
|
|
336
|
-
principal: normalizeIdentity(match[1]),
|
|
337
|
-
denied: flags.includes('DENY'),
|
|
338
|
-
permissions: flags
|
|
339
|
-
.filter(flag => !['DENY', 'I', 'OI', 'CI', 'IO'].includes(flag))
|
|
340
|
-
.flatMap(flag => flag.split(',')),
|
|
341
|
-
};
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function grantsProtectedAccess(entry) {
|
|
346
|
-
return entry.permissions.some(permission => /^(?:F|M|RX|R|W|D|DE|RC|WDAC|WO|S|AS|MA|GR|GW|GE|GA|RD|WD|AD|REA|WEA|X|DC|RA|WA)$/.test(permission));
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function grantsRequiredIdentityAccess(entry) {
|
|
350
|
-
return entry.permissions.some(permission => permission === 'F' || permission === 'M');
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function windowsTrustError() {
|
|
354
|
-
const error = new Error('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
355
|
-
error.category = 'SEMANTIC_READINESS_ATTESTATION_UNTRUSTED';
|
|
356
|
-
error.fullSnapshotFallback = false;
|
|
357
|
-
error.action = ACL_REMEDIATION;
|
|
358
|
-
return error;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function assertWindowsAclTrust({ identity, owner, directoryAcl, fileAcl }) {
|
|
362
|
-
const current = normalizeIdentity(identity);
|
|
363
|
-
if (!current || normalizeIdentity(owner) !== current) throw windowsTrustError();
|
|
364
|
-
for (const acl of [directoryAcl, fileAcl]) {
|
|
365
|
-
const entries = parseWindowsAcl(acl);
|
|
366
|
-
const currentEntries = entries.filter(entry => entry.principal === current);
|
|
367
|
-
if (
|
|
368
|
-
!currentEntries.some(entry => !entry.denied && grantsRequiredIdentityAccess(entry))
|
|
369
|
-
|| currentEntries.some(entry => entry.denied && grantsProtectedAccess(entry))
|
|
370
|
-
|| entries.some(entry => (
|
|
371
|
-
!entry.denied
|
|
372
|
-
&& grantsProtectedAccess(entry)
|
|
373
|
-
&& entry.principal !== current
|
|
374
|
-
&& entry.principal !== 'nt authority\\system'
|
|
375
|
-
))
|
|
376
|
-
) {
|
|
377
|
-
throw windowsTrustError();
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function recordDirectoryFlushFallback(reason) {
|
|
383
|
-
return Object.freeze({ reason });
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function attestationError(category) {
|
|
387
|
-
const error = new Error(category);
|
|
388
|
-
error.category = category;
|
|
389
|
-
error.fullSnapshotFallback = false;
|
|
390
|
-
error.action = category === 'SEMANTIC_READINESS_ATTESTATION_UNTRUSTED'
|
|
391
|
-
? ACL_REMEDIATION
|
|
392
|
-
: 'Run semantic readiness verification again before semantic query';
|
|
393
|
-
return error;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
module.exports = {
|
|
397
|
-
createSemanticReadinessAttestationStore,
|
|
398
|
-
};
|
|
1
|
+
const crypto = require('node:crypto');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const ATTESTATION_PATH = '.argo/temp/semantic-readiness-attestation.json';
|
|
6
|
+
const SCHEMA_VERSION = '1.0';
|
|
7
|
+
const ACL_REMEDIATION = 'Restrict .argo/temp and semantic-readiness-attestation.json ownership and ACLs to the current OS identity and SYSTEM, then run semantic readiness again';
|
|
8
|
+
const FIELDS = Object.freeze([
|
|
9
|
+
'schemaVersion',
|
|
10
|
+
'authorizationOperation',
|
|
11
|
+
'graphPath',
|
|
12
|
+
'verified',
|
|
13
|
+
'canonicalVersion',
|
|
14
|
+
'contentVersion',
|
|
15
|
+
'indexVersion',
|
|
16
|
+
'completedChannels',
|
|
17
|
+
'missingChannels',
|
|
18
|
+
'mismatchedChannels',
|
|
19
|
+
'fullSnapshotFallback',
|
|
20
|
+
'canonicalDigest',
|
|
21
|
+
'integrityDigest',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function createSemanticReadinessAttestationStore(options = {}) {
|
|
25
|
+
const repositoryRoot = path.resolve(options.repositoryRoot);
|
|
26
|
+
const graphPath = normalizeGraphPath(options.graphPath);
|
|
27
|
+
const metadataAdapter = options.metadataAdapter;
|
|
28
|
+
const attestationPath = path.join(repositoryRoot, ...ATTESTATION_PATH.split('/'));
|
|
29
|
+
const directoryPath = path.dirname(attestationPath);
|
|
30
|
+
const canonicalPath = path.join(repositoryRoot, ...graphPath.split('/'));
|
|
31
|
+
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
record(readiness) {
|
|
34
|
+
const record = buildRecord(readiness, {
|
|
35
|
+
repositoryRoot,
|
|
36
|
+
graphPath,
|
|
37
|
+
canonicalPath,
|
|
38
|
+
});
|
|
39
|
+
writeAttestationAtomically(record, {
|
|
40
|
+
attestationPath,
|
|
41
|
+
directoryPath,
|
|
42
|
+
metadataAdapter,
|
|
43
|
+
});
|
|
44
|
+
return Object.freeze(record);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
read() {
|
|
48
|
+
if (!fs.existsSync(attestationPath)) return null;
|
|
49
|
+
assertPathTrust(attestationPath, directoryPath);
|
|
50
|
+
let record;
|
|
51
|
+
try {
|
|
52
|
+
record = JSON.parse(fs.readFileSync(attestationPath, 'utf8'));
|
|
53
|
+
} catch {
|
|
54
|
+
throw attestationError('SEMANTIC_READINESS_ATTESTATION_INVALID');
|
|
55
|
+
}
|
|
56
|
+
assertRecord(record, repositoryRoot);
|
|
57
|
+
assertOperatingSystemTrust({
|
|
58
|
+
attestationPath,
|
|
59
|
+
directoryPath,
|
|
60
|
+
metadataAdapter,
|
|
61
|
+
});
|
|
62
|
+
if (digestFile(canonicalPath) !== record.canonicalDigest) {
|
|
63
|
+
throw attestationError('SEMANTIC_READINESS_ATTESTATION_STALE');
|
|
64
|
+
}
|
|
65
|
+
return Object.freeze(record);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
clear() {
|
|
69
|
+
try {
|
|
70
|
+
fs.rmSync(attestationPath, { force: true });
|
|
71
|
+
} catch {
|
|
72
|
+
throw attestationError('SEMANTIC_READINESS_ATTESTATION_INVALID');
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
validate(attestation, readiness) {
|
|
77
|
+
assertOperatingSystemTrust({
|
|
78
|
+
attestationPath,
|
|
79
|
+
directoryPath,
|
|
80
|
+
metadataAdapter,
|
|
81
|
+
});
|
|
82
|
+
if (digestFile(canonicalPath) !== attestation.canonicalDigest) return false;
|
|
83
|
+
return exactReadinessMatch(attestation, readiness);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildRecord(readiness, context) {
|
|
89
|
+
const record = {
|
|
90
|
+
schemaVersion: SCHEMA_VERSION,
|
|
91
|
+
authorizationOperation: 'verifyReadiness',
|
|
92
|
+
graphPath: context.graphPath,
|
|
93
|
+
verified: readiness.verified === true,
|
|
94
|
+
canonicalVersion: readiness.canonicalVersion,
|
|
95
|
+
contentVersion: readiness.contentVersion,
|
|
96
|
+
indexVersion: readiness.indexVersion,
|
|
97
|
+
completedChannels: copyChannels(readiness.completedChannels),
|
|
98
|
+
missingChannels: copyChannels(readiness.missingChannels),
|
|
99
|
+
mismatchedChannels: copyChannels(readiness.mismatchedChannels),
|
|
100
|
+
fullSnapshotFallback: false,
|
|
101
|
+
canonicalDigest: digestFile(context.canonicalPath),
|
|
102
|
+
};
|
|
103
|
+
record.integrityDigest = integrityDigest(record, context.repositoryRoot);
|
|
104
|
+
assertRecord(record, context.repositoryRoot);
|
|
105
|
+
return record;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function writeAttestationAtomically(readiness, context) {
|
|
109
|
+
fs.mkdirSync(context.directoryPath, { recursive: true, mode: 0o700 });
|
|
110
|
+
assertPathTrust(undefined, context.directoryPath);
|
|
111
|
+
const temporaryPath = `${context.attestationPath}.${process.pid}.${crypto.randomBytes(12).toString('hex')}.tmp`;
|
|
112
|
+
let descriptor;
|
|
113
|
+
try {
|
|
114
|
+
descriptor = fs.openSync(temporaryPath, 'wx', 0o600);
|
|
115
|
+
fs.writeSync(descriptor, `${JSON.stringify(readiness)}\n`, null, 'utf8');
|
|
116
|
+
fs.fsyncSync(descriptor);
|
|
117
|
+
fs.closeSync(descriptor);
|
|
118
|
+
descriptor = undefined;
|
|
119
|
+
fs.renameSync(temporaryPath, context.attestationPath);
|
|
120
|
+
assertMetadataResult(context.metadataAdapter.readReadinessAttestationAcl());
|
|
121
|
+
assertMetadataResult(context.metadataAdapter.readReadinessAttestationOwner());
|
|
122
|
+
assertOperatingSystemTrust(context);
|
|
123
|
+
if (process.platform === 'win32') {
|
|
124
|
+
if (path.dirname(temporaryPath) !== path.dirname(context.attestationPath)) {
|
|
125
|
+
throw attestationError('ATTESTATION_RENAME_VOLUME_CHANGED');
|
|
126
|
+
}
|
|
127
|
+
recordDirectoryFlushFallback('WINDOWS_DIRECTORY_FSYNC_UNSUPPORTED_SAME_DIRECTORY_RENAME');
|
|
128
|
+
} else {
|
|
129
|
+
const directoryDescriptor = fs.openSync(path.dirname(context.attestationPath), 'r');
|
|
130
|
+
fs.fsyncSync(directoryDescriptor);
|
|
131
|
+
fs.closeSync(directoryDescriptor);
|
|
132
|
+
}
|
|
133
|
+
return readiness;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (descriptor !== undefined) {
|
|
136
|
+
try {
|
|
137
|
+
fs.closeSync(descriptor);
|
|
138
|
+
} catch {}
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
142
|
+
} catch {}
|
|
143
|
+
if (error && error.category) throw error;
|
|
144
|
+
throw attestationError('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function assertRecord(record, repositoryRoot) {
|
|
149
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
|
150
|
+
throw attestationError('SEMANTIC_READINESS_ATTESTATION_INVALID');
|
|
151
|
+
}
|
|
152
|
+
if (
|
|
153
|
+
Object.keys(record).sort().join('\n') !== [...FIELDS].sort().join('\n')
|
|
154
|
+
|| record.schemaVersion !== SCHEMA_VERSION
|
|
155
|
+
|| record.authorizationOperation !== 'verifyReadiness'
|
|
156
|
+
|| record.verified !== true
|
|
157
|
+
|| record.fullSnapshotFallback !== false
|
|
158
|
+
|| !isString(record.graphPath)
|
|
159
|
+
|| !isString(record.canonicalVersion)
|
|
160
|
+
|| !isString(record.contentVersion)
|
|
161
|
+
|| !isString(record.indexVersion)
|
|
162
|
+
|| !isString(record.canonicalDigest)
|
|
163
|
+
|| !isString(record.integrityDigest)
|
|
164
|
+
|| !areChannels(record.completedChannels)
|
|
165
|
+
|| !areChannels(record.missingChannels)
|
|
166
|
+
|| !areChannels(record.mismatchedChannels)
|
|
167
|
+
|| record.integrityDigest !== integrityDigest(record, repositoryRoot)
|
|
168
|
+
) {
|
|
169
|
+
throw attestationError('SEMANTIC_READINESS_ATTESTATION_INVALID');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function assertPathTrust(attestationPath, directoryPath) {
|
|
174
|
+
let directory;
|
|
175
|
+
try {
|
|
176
|
+
directory = fs.lstatSync(directoryPath);
|
|
177
|
+
} catch {
|
|
178
|
+
throw attestationError('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
179
|
+
}
|
|
180
|
+
if (!directory.isDirectory() || directory.isSymbolicLink()) {
|
|
181
|
+
throw attestationError('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
182
|
+
}
|
|
183
|
+
if (!attestationPath) return;
|
|
184
|
+
let file;
|
|
185
|
+
try {
|
|
186
|
+
file = fs.lstatSync(attestationPath);
|
|
187
|
+
} catch {
|
|
188
|
+
throw attestationError('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
189
|
+
}
|
|
190
|
+
if (!file.isFile() || file.isSymbolicLink()) {
|
|
191
|
+
throw attestationError('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function assertOperatingSystemTrust(context) {
|
|
196
|
+
assertPathTrust(context.attestationPath, context.directoryPath);
|
|
197
|
+
if (process.platform === 'win32') {
|
|
198
|
+
const identity = assertCommandSucceeded(context.metadataAdapter, 'readCurrentIdentity');
|
|
199
|
+
const directoryAcl = normalizeWindowsAclEvidence(assertCommandSucceeded(
|
|
200
|
+
context.metadataAdapter,
|
|
201
|
+
'readReadinessAttestationDirectoryAcl',
|
|
202
|
+
), context.directoryPath);
|
|
203
|
+
const fileAcl = normalizeWindowsAclEvidence(
|
|
204
|
+
assertCommandSucceeded(context.metadataAdapter, 'readReadinessAttestationAcl'),
|
|
205
|
+
context.attestationPath,
|
|
206
|
+
);
|
|
207
|
+
const owner = assertCommandSucceeded(
|
|
208
|
+
context.metadataAdapter,
|
|
209
|
+
'readReadinessAttestationOwner',
|
|
210
|
+
);
|
|
211
|
+
assertWindowsAclTrust({ identity, owner, directoryAcl, fileAcl });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const file = fs.lstatSync(context.attestationPath);
|
|
215
|
+
const directory = fs.lstatSync(context.directoryPath);
|
|
216
|
+
if (
|
|
217
|
+
(file.mode & 0o077) !== 0
|
|
218
|
+
|| (directory.mode & 0o077) !== 0
|
|
219
|
+
|| (typeof process.getuid === 'function'
|
|
220
|
+
&& (file.uid !== process.getuid() || directory.uid !== process.getuid()))
|
|
221
|
+
) {
|
|
222
|
+
throw attestationError('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function assertCommandSucceeded(adapter, capability) {
|
|
227
|
+
if (!adapter || typeof adapter[capability] !== 'function') {
|
|
228
|
+
throw attestationError('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
229
|
+
}
|
|
230
|
+
const result = adapter[capability]();
|
|
231
|
+
if (!result || result.status !== 0 || typeof result.stdout !== 'string') {
|
|
232
|
+
throw attestationError('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
233
|
+
}
|
|
234
|
+
return result.stdout;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function assertMetadataResult(result) {
|
|
238
|
+
if (!result || result.status !== 0 || typeof result.stdout !== 'string') {
|
|
239
|
+
throw attestationError('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
240
|
+
}
|
|
241
|
+
return result.stdout;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function exactReadinessMatch(attestation, readiness) {
|
|
245
|
+
return Boolean(
|
|
246
|
+
readiness
|
|
247
|
+
&& readiness.verified === true
|
|
248
|
+
&& attestation.canonicalVersion === readiness.canonicalVersion
|
|
249
|
+
&& attestation.contentVersion === readiness.contentVersion
|
|
250
|
+
&& attestation.indexVersion === readiness.indexVersion
|
|
251
|
+
&& sameArray(attestation.completedChannels, readiness.completedChannels)
|
|
252
|
+
&& sameArray(attestation.missingChannels, readiness.missingChannels)
|
|
253
|
+
&& sameArray(attestation.mismatchedChannels, readiness.mismatchedChannels)
|
|
254
|
+
&& readiness.fullSnapshotFallback === false
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function integrityDigest(record, repositoryRoot) {
|
|
259
|
+
const evidence = {};
|
|
260
|
+
for (const field of FIELDS) {
|
|
261
|
+
if (field !== 'integrityDigest') evidence[field] = record[field];
|
|
262
|
+
}
|
|
263
|
+
return digest(`${path.resolve(repositoryRoot)}\n${JSON.stringify(evidence)}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function digestFile(filePath) {
|
|
267
|
+
try {
|
|
268
|
+
return digest(fs.readFileSync(filePath));
|
|
269
|
+
} catch {
|
|
270
|
+
throw attestationError('SEMANTIC_READINESS_ATTESTATION_INVALID');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function digest(value) {
|
|
275
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function copyChannels(value) {
|
|
279
|
+
return Array.isArray(value) ? [...value] : [];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function sameArray(left, right) {
|
|
283
|
+
return Array.isArray(left)
|
|
284
|
+
&& Array.isArray(right)
|
|
285
|
+
&& left.length === right.length
|
|
286
|
+
&& left.every((value, index) => value === right[index]);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function areChannels(value) {
|
|
290
|
+
return Array.isArray(value) && value.every(isString);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function isString(value) {
|
|
294
|
+
return typeof value === 'string' && value.length > 0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function normalizeGraphPath(value) {
|
|
298
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
299
|
+
throw new TypeError('graphPath is required');
|
|
300
|
+
}
|
|
301
|
+
return value.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function normalizeIdentity(value) {
|
|
305
|
+
return String(value).trim().toLowerCase();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function normalizeWindowsAclEvidence(value, subjectPath) {
|
|
309
|
+
const lines = String(value).split(/\r?\n/)
|
|
310
|
+
.map(line => line.trim())
|
|
311
|
+
.filter(line => (
|
|
312
|
+
line
|
|
313
|
+
&& !/^Successfully processed \d+ files?; Failed processing \d+ files?$/i.test(line)
|
|
314
|
+
&& !/\b\d+\b.*;\s*.*\b\d+\b/.test(line)
|
|
315
|
+
));
|
|
316
|
+
if (lines.length > 0) {
|
|
317
|
+
const prefix = String(subjectPath);
|
|
318
|
+
if (lines[0].toLowerCase().startsWith(prefix.toLowerCase())) {
|
|
319
|
+
lines[0] = lines[0].slice(prefix.length).trim();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return lines.filter(Boolean).join('\n');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function parseWindowsAcl(value) {
|
|
326
|
+
const lines = String(value).split(/\r?\n/).filter(Boolean);
|
|
327
|
+
if (lines.length === 0) throw windowsTrustError();
|
|
328
|
+
return lines.map(line => {
|
|
329
|
+
const match = line.match(/^(.+?):((?:\([^)]*\))+?)$/);
|
|
330
|
+
if (!match) throw windowsTrustError();
|
|
331
|
+
const flags = Array.from(
|
|
332
|
+
match[2].matchAll(/\(([^)]*)\)/g),
|
|
333
|
+
entry => entry[1].toUpperCase(),
|
|
334
|
+
);
|
|
335
|
+
return {
|
|
336
|
+
principal: normalizeIdentity(match[1]),
|
|
337
|
+
denied: flags.includes('DENY'),
|
|
338
|
+
permissions: flags
|
|
339
|
+
.filter(flag => !['DENY', 'I', 'OI', 'CI', 'IO'].includes(flag))
|
|
340
|
+
.flatMap(flag => flag.split(',')),
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function grantsProtectedAccess(entry) {
|
|
346
|
+
return entry.permissions.some(permission => /^(?:F|M|RX|R|W|D|DE|RC|WDAC|WO|S|AS|MA|GR|GW|GE|GA|RD|WD|AD|REA|WEA|X|DC|RA|WA)$/.test(permission));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function grantsRequiredIdentityAccess(entry) {
|
|
350
|
+
return entry.permissions.some(permission => permission === 'F' || permission === 'M');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function windowsTrustError() {
|
|
354
|
+
const error = new Error('SEMANTIC_READINESS_ATTESTATION_UNTRUSTED');
|
|
355
|
+
error.category = 'SEMANTIC_READINESS_ATTESTATION_UNTRUSTED';
|
|
356
|
+
error.fullSnapshotFallback = false;
|
|
357
|
+
error.action = ACL_REMEDIATION;
|
|
358
|
+
return error;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function assertWindowsAclTrust({ identity, owner, directoryAcl, fileAcl }) {
|
|
362
|
+
const current = normalizeIdentity(identity);
|
|
363
|
+
if (!current || normalizeIdentity(owner) !== current) throw windowsTrustError();
|
|
364
|
+
for (const acl of [directoryAcl, fileAcl]) {
|
|
365
|
+
const entries = parseWindowsAcl(acl);
|
|
366
|
+
const currentEntries = entries.filter(entry => entry.principal === current);
|
|
367
|
+
if (
|
|
368
|
+
!currentEntries.some(entry => !entry.denied && grantsRequiredIdentityAccess(entry))
|
|
369
|
+
|| currentEntries.some(entry => entry.denied && grantsProtectedAccess(entry))
|
|
370
|
+
|| entries.some(entry => (
|
|
371
|
+
!entry.denied
|
|
372
|
+
&& grantsProtectedAccess(entry)
|
|
373
|
+
&& entry.principal !== current
|
|
374
|
+
&& entry.principal !== 'nt authority\\system'
|
|
375
|
+
))
|
|
376
|
+
) {
|
|
377
|
+
throw windowsTrustError();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function recordDirectoryFlushFallback(reason) {
|
|
383
|
+
return Object.freeze({ reason });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function attestationError(category) {
|
|
387
|
+
const error = new Error(category);
|
|
388
|
+
error.category = category;
|
|
389
|
+
error.fullSnapshotFallback = false;
|
|
390
|
+
error.action = category === 'SEMANTIC_READINESS_ATTESTATION_UNTRUSTED'
|
|
391
|
+
? ACL_REMEDIATION
|
|
392
|
+
: 'Run semantic readiness verification again before semantic query';
|
|
393
|
+
return error;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
module.exports = {
|
|
397
|
+
createSemanticReadinessAttestationStore,
|
|
398
|
+
};
|