@quolu/lattice 0.52.2 → 0.52.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/artifact-contracts-v2.mjs +325 -325
- package/src/artifact-contracts.mjs +895 -895
- package/src/boundary-compiler.mjs +712 -712
- package/src/boundary-observation-compiler-v2.mjs +344 -344
- package/src/bounded-seam.mjs +230 -230
- package/src/bridge-address.mjs +107 -107
- package/src/bridge-cli.mjs +297 -297
- package/src/bridge-config.mjs +346 -346
- package/src/bridge-daemon.mjs +378 -378
- package/src/bridge-hub-protocol.mjs +198 -0
- package/src/bridge-launch-agent.mjs +323 -323
- package/src/bridge-registrar.mjs +102 -102
- package/src/bridge-server.mjs +376 -376
- package/src/cli-help.mjs +308 -308
- package/src/cli-stdio.mjs +40 -40
- package/src/control-compiler.mjs +532 -532
- package/src/dag-chain.mjs +261 -261
- package/src/factory-diagnostics.mjs +188 -188
- package/src/git-process.mjs +72 -0
- package/src/hash-chain.mjs +76 -76
- package/src/hooks-cli.mjs +1057 -1057
- package/src/isolation-runner.mjs +409 -409
- package/src/node-version-guard.mjs +44 -44
- package/src/project-cli.mjs +699 -697
- package/src/project-identity.mjs +130 -130
- package/src/rc1-black-box-oracle.mjs +906 -906
- package/src/rc1-comparison.mjs +154 -154
- package/src/rc1-evidence-bundle.mjs +456 -456
- package/src/rc1-v4-campaign.mjs +708 -708
- package/src/rc1-v4-transform.mjs +467 -467
- package/src/rc1-v5-artifact-set.mjs +855 -855
- package/src/rc1-v5-behavior-evidence.mjs +594 -594
- package/src/rc1-v5-campaign.mjs +797 -797
- package/src/rc1-v5-transform.mjs +421 -421
- package/src/rc1-v6-artifact-set.mjs +807 -807
- package/src/rc1-v6-behavior-evidence.mjs +273 -273
- package/src/rc1-v6-campaign.mjs +623 -623
- package/src/rc1-v6-causal-binding.mjs +473 -473
- package/src/rc1-v6-measurement.mjs +313 -313
- package/src/rc2-artifact-set.mjs +1584 -1584
- package/src/rc2-campaign.mjs +1506 -1506
- package/src/rc2-delivery-policy-front-end.mjs +1079 -1079
- package/src/rc2-delivery-policy-oracle.mjs +134 -134
- package/src/rc2-delivery-policy-transform.mjs +1127 -1127
- package/src/rc2-rc1-transfer-front-end.mjs +511 -511
- package/src/rc3-actual-dogfood.mjs +652 -652
- package/src/rc3-dogfood-scaffold.mjs +315 -315
- package/src/rc3-scripted-campaign.mjs +1383 -1383
- package/src/rc4-stage1-dogfood.mjs +673 -673
- package/src/runtime-adapter-registry.mjs +524 -524
- package/src/runtime-cli.mjs +4588 -4588
- package/src/runtime-contracts.mjs +824 -824
- package/src/runtime-control-store.mjs +604 -604
- package/src/runtime-controller-protocol.mjs +587 -587
- package/src/runtime-decision-verifier.mjs +701 -701
- package/src/runtime-diff-observer.mjs +361 -361
- package/src/runtime-direct-os-observer.mjs +301 -301
- package/src/runtime-driver-state.mjs +166 -166
- package/src/runtime-engine.mjs +779 -779
- package/src/runtime-errors.mjs +356 -356
- package/src/runtime-event-store.mjs +189 -189
- package/src/runtime-front-end.mjs +925 -925
- package/src/runtime-gate-store.mjs +481 -481
- package/src/runtime-hold-recompile.mjs +916 -916
- package/src/runtime-io-sentinel.mjs +391 -391
- package/src/runtime-lifecycle-lock.mjs +294 -294
- package/src/runtime-managed-supervisor.mjs +1490 -1490
- package/src/runtime-multi-epoch-store.mjs +838 -838
- package/src/runtime-projection.mjs +269 -269
- package/src/runtime-pull-intake.mjs +1192 -1192
- package/src/runtime-scripted-adapter-controller.mjs +1160 -1160
- package/src/runtime-scripted-executor.mjs +163 -163
- package/src/runtime-scripted-worktree.mjs +104 -104
- package/src/runtime-seam-resolve.mjs +428 -428
- package/src/runtime-seam-treatment.mjs +173 -173
- package/src/runtime-socket-owner.mjs +125 -125
- package/src/runtime-work-order-contracts.mjs +91 -91
- package/src/runtime-work-order-controller.mjs +1171 -1171
- package/src/runtime-worktree-executor.mjs +199 -199
- package/src/schedulability-compiler-v2.mjs +303 -303
- package/src/schedulability-verifier-v2.mjs +317 -317
- package/src/seam-apply.mjs +549 -549
- package/src/seam-commit-shared.mjs +22 -22
- package/src/seam-commit-transform.mjs +81 -81
- package/src/seam-commit.mjs +18 -18
- package/src/seam-cost.mjs +322 -322
- package/src/seam-derivation.mjs +188 -188
- package/src/seam-gate.mjs +146 -146
- package/src/seam-proposal-contracts.mjs +446 -446
- package/src/seam-proposal-queries.mjs +521 -521
- package/src/seam-proposal.mjs +2011 -2011
- package/src/seam-ref.mjs +33 -33
- package/src/seam-rewrite.mjs +286 -286
- package/src/seam-transform.mjs +554 -554
- package/src/seam-verification.mjs +260 -260
- package/src/sensor-adapter.mjs +432 -432
- package/src/sensor-cli.mjs +139 -139
- package/src/sensor-diff.mjs +661 -661
- package/src/sensor-node-runtime.mjs +53 -53
- package/src/sensor-runtime.mjs +52 -52
- package/src/timestamp-contract.mjs +8 -8
- package/src/todo-audit-pending.mjs +91 -91
- package/src/todo-chain.mjs +178 -178
- package/src/todo-cli.mjs +3143 -3141
- package/src/todo-contracts.mjs +728 -728
- package/src/todo-dashboard-registry.mjs +573 -573
- package/src/todo-dispatch-shape.mjs +190 -190
- package/src/todo-gantt-html-independence.mjs +239 -239
- package/src/todo-gantt-html-shared.mjs +226 -226
- package/src/todo-gantt-html-style.mjs +131 -131
- package/src/todo-gantt-html.mjs +248 -248
- package/src/todo-gantt-layout.mjs +974 -974
- package/src/todo-gantt-live.mjs +361 -361
- package/src/todo-gantt-nested.mjs +263 -263
- package/src/todo-gantt-presentation.mjs +217 -217
- package/src/todo-gantt-scope.mjs +123 -123
- package/src/todo-gantt-svg.mjs +353 -353
- package/src/todo-independence-contracts.mjs +595 -595
- package/src/todo-independence-guidance.mjs +322 -322
- package/src/todo-independence.mjs +640 -640
- package/src/todo-markdown-renderer.mjs +260 -260
- package/src/todo-migration.mjs +448 -448
- package/src/todo-narrative-anchor.mjs +130 -130
- package/src/todo-note-store.mjs +629 -629
- package/src/todo-parallel-candidates.mjs +114 -114
- package/src/todo-revision.mjs +995 -995
- package/src/todo-split.mjs +472 -472
- package/src/todo-status.mjs +690 -690
- package/src/todo-store-git-transaction.mjs +418 -418
- package/src/todo-store.mjs +4360 -4322
- package/src/treatment-compiler.mjs +728 -728
- package/src/treatment-runner.mjs +656 -656
- package/src/witness-scaffold.mjs +180 -180
|
@@ -1,313 +1,313 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import {
|
|
4
|
-
lstat,
|
|
5
|
-
readFile,
|
|
6
|
-
realpath,
|
|
7
|
-
} from 'node:fs/promises';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import { promisify } from 'node:util';
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
digestArtifact,
|
|
13
|
-
validateTransformArtifact,
|
|
14
|
-
} from './artifact-contracts.mjs';
|
|
15
|
-
import { compileBoundaryCondition } from './boundary-compiler.mjs';
|
|
16
|
-
import { validateRc1EvidenceBundle } from './rc1-evidence-bundle.mjs';
|
|
17
|
-
import {
|
|
18
|
-
createRc1V6EvidenceBundleDescriptor,
|
|
19
|
-
verifyRc1V6RunEvidence,
|
|
20
|
-
} from './rc1-v6-causal-binding.mjs';
|
|
21
|
-
import { LATTICE_SENSOR_CLI } from './sensor-runtime.mjs';
|
|
22
|
-
|
|
23
|
-
const SHA256 = /^[0-9a-f]{64}$/;
|
|
24
|
-
const GIT_SHA1 = /^[0-9a-f]{40}$/;
|
|
25
|
-
const VERSION = /^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/;
|
|
26
|
-
const execFileAsync = promisify(execFile);
|
|
27
|
-
|
|
28
|
-
function exactRecord(value, keys) {
|
|
29
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value)
|
|
30
|
-
|| Object.getPrototypeOf(value) !== Object.prototype) {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
const actual = Object.keys(value).sort();
|
|
34
|
-
const expected = [...keys].sort();
|
|
35
|
-
return actual.length === expected.length
|
|
36
|
-
&& actual.every((key, index) => key === expected[index]);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function repoPath(value) {
|
|
40
|
-
return typeof value === 'string'
|
|
41
|
-
&& value.length > 0
|
|
42
|
-
&& value.length <= 512
|
|
43
|
-
&& !value.startsWith('/')
|
|
44
|
-
&& !value.includes('\\')
|
|
45
|
-
&& !value.split('/').includes('..');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function sha256(bytes) {
|
|
49
|
-
return createHash('sha256').update(bytes).digest('hex');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function sourceSnapshot(files) {
|
|
53
|
-
const snapshot = {
|
|
54
|
-
schema: 'lattice.rc1.source_snapshot.v1',
|
|
55
|
-
files,
|
|
56
|
-
};
|
|
57
|
-
digestArtifact(snapshot);
|
|
58
|
-
return snapshot;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function validSnapshotFile(value, index, files) {
|
|
62
|
-
return exactRecord(value, ['path', 'state', 'content_digest'])
|
|
63
|
-
&& repoPath(value.path)
|
|
64
|
-
&& (index === 0 || files[index - 1].path < value.path)
|
|
65
|
-
&& (value.state === 'file' || value.state === 'absent')
|
|
66
|
-
&& (value.state === 'file'
|
|
67
|
-
? typeof value.content_digest === 'string' && SHA256.test(value.content_digest)
|
|
68
|
-
: value.content_digest === null);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function validateSourceSnapshot(value) {
|
|
72
|
-
return exactRecord(value, ['schema', 'files'])
|
|
73
|
-
&& value.schema === 'lattice.rc1.source_snapshot.v1'
|
|
74
|
-
&& Array.isArray(value.files)
|
|
75
|
-
&& value.files.length > 0
|
|
76
|
-
&& value.files.length <= 256
|
|
77
|
-
&& value.files.every(validSnapshotFile);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** behavior fixed surfaceをLatticeSensor measurement用source snapshotへ投影する。 */
|
|
81
|
-
export function sourceSnapshotFromRc1BehaviorSurface(surface) {
|
|
82
|
-
if (!exactRecord(surface, ['schema', 'files'])
|
|
83
|
-
|| surface.schema !== 'lattice.rc1.behavior_surface_snapshot.v1'
|
|
84
|
-
|| !Array.isArray(surface.files)
|
|
85
|
-
|| surface.files.length === 0) {
|
|
86
|
-
throw new TypeError('RC1 v6 behavior surface snapshotが不正');
|
|
87
|
-
}
|
|
88
|
-
const files = surface.files.map((file, index) => {
|
|
89
|
-
if (!exactRecord(file, ['path', 'state', 'content_digest'])
|
|
90
|
-
|| !repoPath(file.path)
|
|
91
|
-
|| (index > 0 && surface.files[index - 1].path >= file.path)
|
|
92
|
-
|| (file.state !== 'present' && file.state !== 'absent')
|
|
93
|
-
|| (file.state === 'present' && !SHA256.test(file.content_digest))
|
|
94
|
-
|| (file.state === 'absent' && file.content_digest !== null)) {
|
|
95
|
-
throw new TypeError('RC1 v6 behavior surface fileが不正');
|
|
96
|
-
}
|
|
97
|
-
return {
|
|
98
|
-
path: file.path,
|
|
99
|
-
state: file.state === 'present' ? 'file' : 'absent',
|
|
100
|
-
content_digest: file.content_digest,
|
|
101
|
-
};
|
|
102
|
-
});
|
|
103
|
-
return sourceSnapshot(files);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** accepted transform outputをtreatmentの期待source snapshotへ投影する。 */
|
|
107
|
-
export function sourceSnapshotFromRc1TransformOutput(transformArtifact) {
|
|
108
|
-
if (!validateTransformArtifact(transformArtifact)
|
|
109
|
-
|| transformArtifact.status !== 'accepted'
|
|
110
|
-
|| !Array.isArray(transformArtifact.output?.files)
|
|
111
|
-
|| transformArtifact.output.files.length === 0) {
|
|
112
|
-
throw new TypeError('RC1 v6 accepted transform outputが不正');
|
|
113
|
-
}
|
|
114
|
-
const files = transformArtifact.output.files.map((file, index) => {
|
|
115
|
-
if (!exactRecord(file, ['path', 'content_digest'])
|
|
116
|
-
|| !repoPath(file.path)
|
|
117
|
-
|| !SHA256.test(file.content_digest)
|
|
118
|
-
|| (index > 0 && transformArtifact.output.files[index - 1].path >= file.path)) {
|
|
119
|
-
throw new TypeError('RC1 v6 transform output fileが不正');
|
|
120
|
-
}
|
|
121
|
-
return { path: file.path, state: 'file', content_digest: file.content_digest };
|
|
122
|
-
});
|
|
123
|
-
return sourceSnapshot(files);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** 実repoのfixed path bytesからsorted source snapshotを取得する。 */
|
|
127
|
-
export async function captureRc1V6SourceSnapshot({ repoRoot, paths } = {}) {
|
|
128
|
-
if (typeof repoRoot !== 'string'
|
|
129
|
-
|| repoRoot.length === 0
|
|
130
|
-
|| !Array.isArray(paths)
|
|
131
|
-
|| paths.length === 0
|
|
132
|
-
|| paths.length > 256
|
|
133
|
-
|| !paths.every((entry, index) => (
|
|
134
|
-
repoPath(entry) && (index === 0 || paths[index - 1] < entry)
|
|
135
|
-
))) {
|
|
136
|
-
throw new TypeError('RC1 v6 source snapshot inputが不正');
|
|
137
|
-
}
|
|
138
|
-
const root = await realpath(repoRoot);
|
|
139
|
-
const files = [];
|
|
140
|
-
for (const relativePath of paths) {
|
|
141
|
-
const target = path.resolve(root, relativePath);
|
|
142
|
-
const relative = path.relative(root, target);
|
|
143
|
-
if (relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
144
|
-
throw new TypeError('RC1 v6 source snapshot pathがrepo外を指す');
|
|
145
|
-
}
|
|
146
|
-
try {
|
|
147
|
-
const stat = await lstat(target);
|
|
148
|
-
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
149
|
-
throw new TypeError(`RC1 v6 source snapshot pathが通常fileでない: ${relativePath}`);
|
|
150
|
-
}
|
|
151
|
-
files.push({
|
|
152
|
-
path: relativePath,
|
|
153
|
-
state: 'file',
|
|
154
|
-
content_digest: sha256(await readFile(target)),
|
|
155
|
-
});
|
|
156
|
-
} catch (error) {
|
|
157
|
-
if (error?.code !== 'ENOENT') throw error;
|
|
158
|
-
files.push({ path: relativePath, state: 'absent', content_digest: null });
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return sourceSnapshot(files);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Lattice内蔵sensorのidentityと、そのdigestを再計算できる実bytesを同時captureする。 */
|
|
165
|
-
export async function captureRc1V6LatticeSensorExecutable() {
|
|
166
|
-
const executableBytes = await readFile(LATTICE_SENSOR_CLI);
|
|
167
|
-
const { stdout } = await execFileAsync(process.execPath, [LATTICE_SENSOR_CLI, '--version'], {
|
|
168
|
-
encoding: 'utf8',
|
|
169
|
-
maxBuffer: 64 * 1024,
|
|
170
|
-
env: { ...process.env, NO_COLOR: '1' },
|
|
171
|
-
});
|
|
172
|
-
const version = stdout.trim();
|
|
173
|
-
if (!VERSION.test(version)) {
|
|
174
|
-
throw new TypeError('RC1 v6 LatticeSensor versionがsemverでない');
|
|
175
|
-
}
|
|
176
|
-
return {
|
|
177
|
-
identity: {
|
|
178
|
-
schema: 'lattice.rc1.sensor_identity.v1',
|
|
179
|
-
version,
|
|
180
|
-
executable_ref: 'lattice-sensor',
|
|
181
|
-
executable_digest: sha256(executableBytes),
|
|
182
|
-
},
|
|
183
|
-
executable_bytes: executableBytes,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/** portable identityだけを必要とする既存caller向け入口。 */
|
|
188
|
-
export async function resolveRc1V6LatticeSensorIdentity() {
|
|
189
|
-
return (await captureRc1V6LatticeSensorExecutable()).identity;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/** v1 evidence bundleへsource/tool measurementを追加する。 */
|
|
193
|
-
export function bindRc1V6EvidenceBundle(options = {}) {
|
|
194
|
-
if (!exactRecord(options, [
|
|
195
|
-
'bundle',
|
|
196
|
-
'base_sha',
|
|
197
|
-
'patch_digest',
|
|
198
|
-
'snapshot',
|
|
199
|
-
'sensor_identity',
|
|
200
|
-
'query_set_digest',
|
|
201
|
-
])
|
|
202
|
-
|| !validateRc1EvidenceBundle(options.bundle)
|
|
203
|
-
|| !GIT_SHA1.test(options.base_sha)
|
|
204
|
-
|| (options.patch_digest !== null && !SHA256.test(options.patch_digest))
|
|
205
|
-
|| !validateSourceSnapshot(options.snapshot)
|
|
206
|
-
|| options.query_set_digest !== options.bundle.query_set_digest) {
|
|
207
|
-
throw new TypeError('RC1 v6 evidence measurement inputが不正');
|
|
208
|
-
}
|
|
209
|
-
const measurement = {
|
|
210
|
-
schema: 'lattice.rc1.sensor_measurement.v1',
|
|
211
|
-
base_sha: options.base_sha,
|
|
212
|
-
patch_digest: options.patch_digest,
|
|
213
|
-
snapshot: structuredClone(options.snapshot),
|
|
214
|
-
snapshot_digest: digestArtifact(options.snapshot),
|
|
215
|
-
sensor_identity: structuredClone(options.sensor_identity),
|
|
216
|
-
sensor_identity_digest: digestArtifact(options.sensor_identity),
|
|
217
|
-
query_set_digest: options.query_set_digest,
|
|
218
|
-
raw_evidence_digest: options.bundle.raw.payload_digest,
|
|
219
|
-
};
|
|
220
|
-
const bundle = {
|
|
221
|
-
...structuredClone(options.bundle),
|
|
222
|
-
schema: 'lattice.rc1.evidence_bundle.v2',
|
|
223
|
-
measurement,
|
|
224
|
-
measurement_digest: digestArtifact(measurement),
|
|
225
|
-
};
|
|
226
|
-
const run = createRc1V6ConditionRun(bundle);
|
|
227
|
-
const verification = verifyRc1V6RunEvidence({
|
|
228
|
-
run,
|
|
229
|
-
bundle,
|
|
230
|
-
expected: {
|
|
231
|
-
base_sha: options.base_sha,
|
|
232
|
-
patch_digest: options.patch_digest,
|
|
233
|
-
snapshot: options.snapshot,
|
|
234
|
-
sensor_identity: options.sensor_identity,
|
|
235
|
-
query_set_digest: options.query_set_digest,
|
|
236
|
-
},
|
|
237
|
-
});
|
|
238
|
-
if (!verification.valid) {
|
|
239
|
-
throw new TypeError(
|
|
240
|
-
`RC1 v6 evidence measurement binding failed: ${verification.failed_conditions.join(', ')}`,
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
return bundle;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** v2 bundleのbounded descriptorからcondition run identityを生成する。 */
|
|
247
|
-
export function createRc1V6ConditionRun(bundle) {
|
|
248
|
-
if (bundle === null || typeof bundle !== 'object' || bundle.schema !== 'lattice.rc1.evidence_bundle.v2') {
|
|
249
|
-
throw new TypeError('RC1 v6 condition run bundleが不正');
|
|
250
|
-
}
|
|
251
|
-
return {
|
|
252
|
-
schema: 'lattice.rc1.condition_run.v2',
|
|
253
|
-
condition: bundle.condition,
|
|
254
|
-
run_id: bundle.run_id,
|
|
255
|
-
evidence_bundle_descriptor_digest: digestArtifact(
|
|
256
|
-
createRc1V6EvidenceBundleDescriptor(bundle),
|
|
257
|
-
),
|
|
258
|
-
measurement_digest: bundle.measurement_digest,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function decodeRawEvidence(bundle) {
|
|
263
|
-
try {
|
|
264
|
-
return JSON.parse(Buffer.from(bundle.raw.payload_base64, 'base64').toString('utf8'));
|
|
265
|
-
} catch {
|
|
266
|
-
throw new TypeError('RC1 v6 raw LatticeSensor evidenceをdecodeできない');
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/** verified bundle preimageだけからboundary compilerを実行する。 */
|
|
271
|
-
export function compileRc1V6BoundaryCondition(options = {}) {
|
|
272
|
-
if (!exactRecord(options, [
|
|
273
|
-
'planInput',
|
|
274
|
-
'candidateSpec',
|
|
275
|
-
'manualEvidence',
|
|
276
|
-
'querySet',
|
|
277
|
-
'run',
|
|
278
|
-
'bundle',
|
|
279
|
-
'expected',
|
|
280
|
-
'planVersion',
|
|
281
|
-
])) {
|
|
282
|
-
throw new TypeError('RC1 v6 boundary compile input shapeが不正');
|
|
283
|
-
}
|
|
284
|
-
const verification = verifyRc1V6RunEvidence({
|
|
285
|
-
run: options.run,
|
|
286
|
-
bundle: options.bundle,
|
|
287
|
-
expected: options.expected,
|
|
288
|
-
});
|
|
289
|
-
if (!verification.valid) {
|
|
290
|
-
throw new TypeError(
|
|
291
|
-
`RC1 v6 measurement binding failed: ${verification.failed_conditions.join(', ')}`,
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
if (digestArtifact(options.querySet) !== options.bundle.query_set_digest) {
|
|
295
|
-
throw new TypeError('RC1 v6 compiler query setがbundleと一致しない');
|
|
296
|
-
}
|
|
297
|
-
const compiled = compileBoundaryCondition({
|
|
298
|
-
planInput: options.planInput,
|
|
299
|
-
candidateSpec: options.candidateSpec,
|
|
300
|
-
manualEvidence: options.manualEvidence,
|
|
301
|
-
querySet: options.querySet,
|
|
302
|
-
sensorEvidence: decodeRawEvidence(options.bundle),
|
|
303
|
-
codeSnapshotDigest: options.bundle.measurement.snapshot_digest,
|
|
304
|
-
planVersion: options.planVersion,
|
|
305
|
-
});
|
|
306
|
-
if (compiled.boundary_manifest.source.code_snapshot_digest
|
|
307
|
-
!== options.bundle.measurement.snapshot_digest
|
|
308
|
-
|| compiled.boundary_manifest.source.sensor_version
|
|
309
|
-
!== options.bundle.measurement.sensor_identity.version) {
|
|
310
|
-
throw new TypeError('RC1 v6 compiler outputがmeasurement identityと一致しない');
|
|
311
|
-
}
|
|
312
|
-
return compiled;
|
|
313
|
-
}
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import {
|
|
4
|
+
lstat,
|
|
5
|
+
readFile,
|
|
6
|
+
realpath,
|
|
7
|
+
} from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { promisify } from 'node:util';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
digestArtifact,
|
|
13
|
+
validateTransformArtifact,
|
|
14
|
+
} from './artifact-contracts.mjs';
|
|
15
|
+
import { compileBoundaryCondition } from './boundary-compiler.mjs';
|
|
16
|
+
import { validateRc1EvidenceBundle } from './rc1-evidence-bundle.mjs';
|
|
17
|
+
import {
|
|
18
|
+
createRc1V6EvidenceBundleDescriptor,
|
|
19
|
+
verifyRc1V6RunEvidence,
|
|
20
|
+
} from './rc1-v6-causal-binding.mjs';
|
|
21
|
+
import { LATTICE_SENSOR_CLI } from './sensor-runtime.mjs';
|
|
22
|
+
|
|
23
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
24
|
+
const GIT_SHA1 = /^[0-9a-f]{40}$/;
|
|
25
|
+
const VERSION = /^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/;
|
|
26
|
+
const execFileAsync = promisify(execFile);
|
|
27
|
+
|
|
28
|
+
function exactRecord(value, keys) {
|
|
29
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)
|
|
30
|
+
|| Object.getPrototypeOf(value) !== Object.prototype) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const actual = Object.keys(value).sort();
|
|
34
|
+
const expected = [...keys].sort();
|
|
35
|
+
return actual.length === expected.length
|
|
36
|
+
&& actual.every((key, index) => key === expected[index]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function repoPath(value) {
|
|
40
|
+
return typeof value === 'string'
|
|
41
|
+
&& value.length > 0
|
|
42
|
+
&& value.length <= 512
|
|
43
|
+
&& !value.startsWith('/')
|
|
44
|
+
&& !value.includes('\\')
|
|
45
|
+
&& !value.split('/').includes('..');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sha256(bytes) {
|
|
49
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sourceSnapshot(files) {
|
|
53
|
+
const snapshot = {
|
|
54
|
+
schema: 'lattice.rc1.source_snapshot.v1',
|
|
55
|
+
files,
|
|
56
|
+
};
|
|
57
|
+
digestArtifact(snapshot);
|
|
58
|
+
return snapshot;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validSnapshotFile(value, index, files) {
|
|
62
|
+
return exactRecord(value, ['path', 'state', 'content_digest'])
|
|
63
|
+
&& repoPath(value.path)
|
|
64
|
+
&& (index === 0 || files[index - 1].path < value.path)
|
|
65
|
+
&& (value.state === 'file' || value.state === 'absent')
|
|
66
|
+
&& (value.state === 'file'
|
|
67
|
+
? typeof value.content_digest === 'string' && SHA256.test(value.content_digest)
|
|
68
|
+
: value.content_digest === null);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function validateSourceSnapshot(value) {
|
|
72
|
+
return exactRecord(value, ['schema', 'files'])
|
|
73
|
+
&& value.schema === 'lattice.rc1.source_snapshot.v1'
|
|
74
|
+
&& Array.isArray(value.files)
|
|
75
|
+
&& value.files.length > 0
|
|
76
|
+
&& value.files.length <= 256
|
|
77
|
+
&& value.files.every(validSnapshotFile);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** behavior fixed surfaceをLatticeSensor measurement用source snapshotへ投影する。 */
|
|
81
|
+
export function sourceSnapshotFromRc1BehaviorSurface(surface) {
|
|
82
|
+
if (!exactRecord(surface, ['schema', 'files'])
|
|
83
|
+
|| surface.schema !== 'lattice.rc1.behavior_surface_snapshot.v1'
|
|
84
|
+
|| !Array.isArray(surface.files)
|
|
85
|
+
|| surface.files.length === 0) {
|
|
86
|
+
throw new TypeError('RC1 v6 behavior surface snapshotが不正');
|
|
87
|
+
}
|
|
88
|
+
const files = surface.files.map((file, index) => {
|
|
89
|
+
if (!exactRecord(file, ['path', 'state', 'content_digest'])
|
|
90
|
+
|| !repoPath(file.path)
|
|
91
|
+
|| (index > 0 && surface.files[index - 1].path >= file.path)
|
|
92
|
+
|| (file.state !== 'present' && file.state !== 'absent')
|
|
93
|
+
|| (file.state === 'present' && !SHA256.test(file.content_digest))
|
|
94
|
+
|| (file.state === 'absent' && file.content_digest !== null)) {
|
|
95
|
+
throw new TypeError('RC1 v6 behavior surface fileが不正');
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
path: file.path,
|
|
99
|
+
state: file.state === 'present' ? 'file' : 'absent',
|
|
100
|
+
content_digest: file.content_digest,
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
return sourceSnapshot(files);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** accepted transform outputをtreatmentの期待source snapshotへ投影する。 */
|
|
107
|
+
export function sourceSnapshotFromRc1TransformOutput(transformArtifact) {
|
|
108
|
+
if (!validateTransformArtifact(transformArtifact)
|
|
109
|
+
|| transformArtifact.status !== 'accepted'
|
|
110
|
+
|| !Array.isArray(transformArtifact.output?.files)
|
|
111
|
+
|| transformArtifact.output.files.length === 0) {
|
|
112
|
+
throw new TypeError('RC1 v6 accepted transform outputが不正');
|
|
113
|
+
}
|
|
114
|
+
const files = transformArtifact.output.files.map((file, index) => {
|
|
115
|
+
if (!exactRecord(file, ['path', 'content_digest'])
|
|
116
|
+
|| !repoPath(file.path)
|
|
117
|
+
|| !SHA256.test(file.content_digest)
|
|
118
|
+
|| (index > 0 && transformArtifact.output.files[index - 1].path >= file.path)) {
|
|
119
|
+
throw new TypeError('RC1 v6 transform output fileが不正');
|
|
120
|
+
}
|
|
121
|
+
return { path: file.path, state: 'file', content_digest: file.content_digest };
|
|
122
|
+
});
|
|
123
|
+
return sourceSnapshot(files);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 実repoのfixed path bytesからsorted source snapshotを取得する。 */
|
|
127
|
+
export async function captureRc1V6SourceSnapshot({ repoRoot, paths } = {}) {
|
|
128
|
+
if (typeof repoRoot !== 'string'
|
|
129
|
+
|| repoRoot.length === 0
|
|
130
|
+
|| !Array.isArray(paths)
|
|
131
|
+
|| paths.length === 0
|
|
132
|
+
|| paths.length > 256
|
|
133
|
+
|| !paths.every((entry, index) => (
|
|
134
|
+
repoPath(entry) && (index === 0 || paths[index - 1] < entry)
|
|
135
|
+
))) {
|
|
136
|
+
throw new TypeError('RC1 v6 source snapshot inputが不正');
|
|
137
|
+
}
|
|
138
|
+
const root = await realpath(repoRoot);
|
|
139
|
+
const files = [];
|
|
140
|
+
for (const relativePath of paths) {
|
|
141
|
+
const target = path.resolve(root, relativePath);
|
|
142
|
+
const relative = path.relative(root, target);
|
|
143
|
+
if (relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
144
|
+
throw new TypeError('RC1 v6 source snapshot pathがrepo外を指す');
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const stat = await lstat(target);
|
|
148
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
149
|
+
throw new TypeError(`RC1 v6 source snapshot pathが通常fileでない: ${relativePath}`);
|
|
150
|
+
}
|
|
151
|
+
files.push({
|
|
152
|
+
path: relativePath,
|
|
153
|
+
state: 'file',
|
|
154
|
+
content_digest: sha256(await readFile(target)),
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
158
|
+
files.push({ path: relativePath, state: 'absent', content_digest: null });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return sourceSnapshot(files);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Lattice内蔵sensorのidentityと、そのdigestを再計算できる実bytesを同時captureする。 */
|
|
165
|
+
export async function captureRc1V6LatticeSensorExecutable() {
|
|
166
|
+
const executableBytes = await readFile(LATTICE_SENSOR_CLI);
|
|
167
|
+
const { stdout } = await execFileAsync(process.execPath, [LATTICE_SENSOR_CLI, '--version'], {
|
|
168
|
+
encoding: 'utf8',
|
|
169
|
+
maxBuffer: 64 * 1024,
|
|
170
|
+
env: { ...process.env, NO_COLOR: '1' },
|
|
171
|
+
});
|
|
172
|
+
const version = stdout.trim();
|
|
173
|
+
if (!VERSION.test(version)) {
|
|
174
|
+
throw new TypeError('RC1 v6 LatticeSensor versionがsemverでない');
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
identity: {
|
|
178
|
+
schema: 'lattice.rc1.sensor_identity.v1',
|
|
179
|
+
version,
|
|
180
|
+
executable_ref: 'lattice-sensor',
|
|
181
|
+
executable_digest: sha256(executableBytes),
|
|
182
|
+
},
|
|
183
|
+
executable_bytes: executableBytes,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** portable identityだけを必要とする既存caller向け入口。 */
|
|
188
|
+
export async function resolveRc1V6LatticeSensorIdentity() {
|
|
189
|
+
return (await captureRc1V6LatticeSensorExecutable()).identity;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** v1 evidence bundleへsource/tool measurementを追加する。 */
|
|
193
|
+
export function bindRc1V6EvidenceBundle(options = {}) {
|
|
194
|
+
if (!exactRecord(options, [
|
|
195
|
+
'bundle',
|
|
196
|
+
'base_sha',
|
|
197
|
+
'patch_digest',
|
|
198
|
+
'snapshot',
|
|
199
|
+
'sensor_identity',
|
|
200
|
+
'query_set_digest',
|
|
201
|
+
])
|
|
202
|
+
|| !validateRc1EvidenceBundle(options.bundle)
|
|
203
|
+
|| !GIT_SHA1.test(options.base_sha)
|
|
204
|
+
|| (options.patch_digest !== null && !SHA256.test(options.patch_digest))
|
|
205
|
+
|| !validateSourceSnapshot(options.snapshot)
|
|
206
|
+
|| options.query_set_digest !== options.bundle.query_set_digest) {
|
|
207
|
+
throw new TypeError('RC1 v6 evidence measurement inputが不正');
|
|
208
|
+
}
|
|
209
|
+
const measurement = {
|
|
210
|
+
schema: 'lattice.rc1.sensor_measurement.v1',
|
|
211
|
+
base_sha: options.base_sha,
|
|
212
|
+
patch_digest: options.patch_digest,
|
|
213
|
+
snapshot: structuredClone(options.snapshot),
|
|
214
|
+
snapshot_digest: digestArtifact(options.snapshot),
|
|
215
|
+
sensor_identity: structuredClone(options.sensor_identity),
|
|
216
|
+
sensor_identity_digest: digestArtifact(options.sensor_identity),
|
|
217
|
+
query_set_digest: options.query_set_digest,
|
|
218
|
+
raw_evidence_digest: options.bundle.raw.payload_digest,
|
|
219
|
+
};
|
|
220
|
+
const bundle = {
|
|
221
|
+
...structuredClone(options.bundle),
|
|
222
|
+
schema: 'lattice.rc1.evidence_bundle.v2',
|
|
223
|
+
measurement,
|
|
224
|
+
measurement_digest: digestArtifact(measurement),
|
|
225
|
+
};
|
|
226
|
+
const run = createRc1V6ConditionRun(bundle);
|
|
227
|
+
const verification = verifyRc1V6RunEvidence({
|
|
228
|
+
run,
|
|
229
|
+
bundle,
|
|
230
|
+
expected: {
|
|
231
|
+
base_sha: options.base_sha,
|
|
232
|
+
patch_digest: options.patch_digest,
|
|
233
|
+
snapshot: options.snapshot,
|
|
234
|
+
sensor_identity: options.sensor_identity,
|
|
235
|
+
query_set_digest: options.query_set_digest,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
if (!verification.valid) {
|
|
239
|
+
throw new TypeError(
|
|
240
|
+
`RC1 v6 evidence measurement binding failed: ${verification.failed_conditions.join(', ')}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
return bundle;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** v2 bundleのbounded descriptorからcondition run identityを生成する。 */
|
|
247
|
+
export function createRc1V6ConditionRun(bundle) {
|
|
248
|
+
if (bundle === null || typeof bundle !== 'object' || bundle.schema !== 'lattice.rc1.evidence_bundle.v2') {
|
|
249
|
+
throw new TypeError('RC1 v6 condition run bundleが不正');
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
schema: 'lattice.rc1.condition_run.v2',
|
|
253
|
+
condition: bundle.condition,
|
|
254
|
+
run_id: bundle.run_id,
|
|
255
|
+
evidence_bundle_descriptor_digest: digestArtifact(
|
|
256
|
+
createRc1V6EvidenceBundleDescriptor(bundle),
|
|
257
|
+
),
|
|
258
|
+
measurement_digest: bundle.measurement_digest,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function decodeRawEvidence(bundle) {
|
|
263
|
+
try {
|
|
264
|
+
return JSON.parse(Buffer.from(bundle.raw.payload_base64, 'base64').toString('utf8'));
|
|
265
|
+
} catch {
|
|
266
|
+
throw new TypeError('RC1 v6 raw LatticeSensor evidenceをdecodeできない');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** verified bundle preimageだけからboundary compilerを実行する。 */
|
|
271
|
+
export function compileRc1V6BoundaryCondition(options = {}) {
|
|
272
|
+
if (!exactRecord(options, [
|
|
273
|
+
'planInput',
|
|
274
|
+
'candidateSpec',
|
|
275
|
+
'manualEvidence',
|
|
276
|
+
'querySet',
|
|
277
|
+
'run',
|
|
278
|
+
'bundle',
|
|
279
|
+
'expected',
|
|
280
|
+
'planVersion',
|
|
281
|
+
])) {
|
|
282
|
+
throw new TypeError('RC1 v6 boundary compile input shapeが不正');
|
|
283
|
+
}
|
|
284
|
+
const verification = verifyRc1V6RunEvidence({
|
|
285
|
+
run: options.run,
|
|
286
|
+
bundle: options.bundle,
|
|
287
|
+
expected: options.expected,
|
|
288
|
+
});
|
|
289
|
+
if (!verification.valid) {
|
|
290
|
+
throw new TypeError(
|
|
291
|
+
`RC1 v6 measurement binding failed: ${verification.failed_conditions.join(', ')}`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
if (digestArtifact(options.querySet) !== options.bundle.query_set_digest) {
|
|
295
|
+
throw new TypeError('RC1 v6 compiler query setがbundleと一致しない');
|
|
296
|
+
}
|
|
297
|
+
const compiled = compileBoundaryCondition({
|
|
298
|
+
planInput: options.planInput,
|
|
299
|
+
candidateSpec: options.candidateSpec,
|
|
300
|
+
manualEvidence: options.manualEvidence,
|
|
301
|
+
querySet: options.querySet,
|
|
302
|
+
sensorEvidence: decodeRawEvidence(options.bundle),
|
|
303
|
+
codeSnapshotDigest: options.bundle.measurement.snapshot_digest,
|
|
304
|
+
planVersion: options.planVersion,
|
|
305
|
+
});
|
|
306
|
+
if (compiled.boundary_manifest.source.code_snapshot_digest
|
|
307
|
+
!== options.bundle.measurement.snapshot_digest
|
|
308
|
+
|| compiled.boundary_manifest.source.sensor_version
|
|
309
|
+
!== options.bundle.measurement.sensor_identity.version) {
|
|
310
|
+
throw new TypeError('RC1 v6 compiler outputがmeasurement identityと一致しない');
|
|
311
|
+
}
|
|
312
|
+
return compiled;
|
|
313
|
+
}
|