@holoscript/holosystem 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -0
- package/bin/holosystem.mjs +76 -0
- package/docs/vm-launch-threat-model.md +43 -5
- package/native/windows-sandbox/Canary.c +107 -0
- package/native/windows-sandbox/Program.cs +462 -12
- package/native/windows-sandbox/README.md +14 -3
- package/native/windows-sandbox/build.ps1 +31 -1
- package/native/windows-x64/holosystem-appcontainer-canary.exe +0 -0
- package/native/windows-x64/holosystem-sandbox-launcher.exe +0 -0
- package/package.json +10 -2
- package/src/index.mjs +20 -0
- package/src/source-canon.mjs +307 -0
- package/src/vm-launch.mjs +336 -45
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# Windows low-integrity Job launcher
|
|
2
2
|
|
|
3
3
|
`Program.cs` is the auditable source for the Windows AMD64 launcher consumed by
|
|
4
|
-
`vm-launch-whpx-sandboxed`. `
|
|
4
|
+
`vm-launch-whpx-sandboxed`. `Canary.c` is the native zero-capability file and
|
|
5
|
+
loopback probe consumed only by `vm-launch-whpx-appcontainer`. `build.ps1`
|
|
6
|
+
compiles the launcher with the inbox .NET
|
|
5
7
|
Framework AMD64 compiler and writes the measured artifact to
|
|
6
|
-
`../windows-x64/holosystem-sandbox-launcher.exe
|
|
8
|
+
`../windows-x64/holosystem-sandbox-launcher.exe`; it compiles the canary with
|
|
9
|
+
the installed Visual C++ AMD64 tools into
|
|
10
|
+
`../windows-x64/holosystem-appcontainer-canary.exe`.
|
|
7
11
|
|
|
8
12
|
The release contract pins the executable digest, not an unverified source-build
|
|
9
13
|
claim. The inbox compiler does not offer deterministic output, so a rebuild is
|
|
@@ -12,8 +16,15 @@ the source and binary delta, update the plan fixture, run the HoloSystem package
|
|
|
12
16
|
checks, and complete a real two-launch WHPX receipt before publishing a rebuilt
|
|
13
17
|
launcher.
|
|
14
18
|
|
|
15
|
-
The
|
|
19
|
+
The low-integrity adapter applies only the process controls it reports: maximum-privilege
|
|
16
20
|
filtering, low integrity, an explicit three-handle inheritance list, and a
|
|
17
21
|
pre-resume Job Object with kill, process-count, memory, and UI limits. It is not
|
|
18
22
|
an AppContainer and does not claim filesystem confidentiality or network
|
|
19
23
|
capability isolation.
|
|
24
|
+
|
|
25
|
+
The AppContainer adapter creates an ephemeral zero-capability identity, applies
|
|
26
|
+
fixed snapshot/temp grants, and runs the measured native canary under the same
|
|
27
|
+
restricted AppContainer token before starting QEMU. Both binaries are pinned by
|
|
28
|
+
the plan and remeasured around each launch. A successful canary is not enough:
|
|
29
|
+
the launcher requires exact file and Winsock access-denied evidence and verifies
|
|
30
|
+
the suspended QEMU token independently.
|
|
@@ -3,6 +3,8 @@ $ErrorActionPreference = 'Stop'
|
|
|
3
3
|
$source = Join-Path $PSScriptRoot 'Program.cs'
|
|
4
4
|
$outputDirectory = Join-Path (Split-Path $PSScriptRoot -Parent) 'windows-x64'
|
|
5
5
|
$output = Join-Path $outputDirectory 'holosystem-sandbox-launcher.exe'
|
|
6
|
+
$canarySource = Join-Path $PSScriptRoot 'Canary.c'
|
|
7
|
+
$canaryOutput = Join-Path $outputDirectory 'holosystem-appcontainer-canary.exe'
|
|
6
8
|
$compiler = Join-Path $env:SystemRoot 'Microsoft.NET\Framework64\v4.0.30319\csc.exe'
|
|
7
9
|
|
|
8
10
|
if (-not (Test-Path -LiteralPath $compiler -PathType Leaf)) {
|
|
@@ -15,4 +17,32 @@ if ($LASTEXITCODE -ne 0) {
|
|
|
15
17
|
throw "Native sandbox launcher compilation failed with exit code $LASTEXITCODE."
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
|
|
21
|
+
$visualStudio = & $vswhere -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
|
|
22
|
+
if (-not $visualStudio) {
|
|
23
|
+
throw 'The Visual C++ AMD64 build tools are unavailable.'
|
|
24
|
+
}
|
|
25
|
+
$msvc = Get-ChildItem (Join-Path $visualStudio 'VC\Tools\MSVC') -Directory | Sort-Object Name -Descending | Select-Object -First 1
|
|
26
|
+
$kitsRoot = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10'
|
|
27
|
+
$kit = Get-ChildItem (Join-Path $kitsRoot 'Include') -Directory | Sort-Object Name -Descending | Select-Object -First 1
|
|
28
|
+
if (-not $msvc -or -not $kit) {
|
|
29
|
+
throw 'The Visual C++ or Windows SDK directories are unavailable.'
|
|
30
|
+
}
|
|
31
|
+
$env:Path = "$(Join-Path $msvc.FullName 'bin\Hostx64\x64');$env:Path"
|
|
32
|
+
$env:Include = @(
|
|
33
|
+
(Join-Path $msvc.FullName 'include'),
|
|
34
|
+
(Join-Path $kit.FullName 'ucrt'),
|
|
35
|
+
(Join-Path $kit.FullName 'shared'),
|
|
36
|
+
(Join-Path $kit.FullName 'um')
|
|
37
|
+
) -join ';'
|
|
38
|
+
$env:Lib = @(
|
|
39
|
+
(Join-Path $msvc.FullName 'lib\x64'),
|
|
40
|
+
(Join-Path $kitsRoot "Lib\$($kit.Name)\ucrt\x64"),
|
|
41
|
+
(Join-Path $kitsRoot "Lib\$($kit.Name)\um\x64")
|
|
42
|
+
) -join ';'
|
|
43
|
+
& cl.exe /nologo /O2 /MT /W4 /WX /DUNICODE /D_UNICODE "/Fe:$canaryOutput" $canarySource /link ws2_32.lib advapi32.lib
|
|
44
|
+
if ($LASTEXITCODE -ne 0) {
|
|
45
|
+
throw "Native AppContainer canary compilation failed with exit code $LASTEXITCODE."
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Get-FileHash -LiteralPath $output, $canaryOutput -Algorithm SHA256 | Select-Object Path, Hash
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holoscript/holosystem",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Portable bootstrap, consumption catalog, lineage, and bounded-work contract for HoloSystem consumers.",
|
|
5
5
|
"releaseLane": "v0-preview",
|
|
6
6
|
"keywords": [
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"node": ">=18"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
|
-
"build": "node --check src/index.mjs && node --check src/catalog.mjs && node --check src/substrate.mjs && node --check src/substrate-import.mjs && node --check src/substrate-debian-release.mjs && node --check src/substrate-import-debian.mjs && node --check src/native-build.mjs && node --check src/vm-launch.mjs && node --check bin/holosystem.mjs",
|
|
46
|
+
"build": "node --check src/index.mjs && node --check src/catalog.mjs && node --check src/source-canon.mjs && node --check src/substrate.mjs && node --check src/substrate-import.mjs && node --check src/substrate-debian-release.mjs && node --check src/substrate-import-debian.mjs && node --check src/native-build.mjs && node --check src/vm-launch.mjs && node --check bin/holosystem.mjs",
|
|
47
47
|
"test": "node --test test/*.test.mjs",
|
|
48
48
|
"smoke": "node bin/holosystem.mjs --help",
|
|
49
49
|
"check": "pnpm run build && pnpm run test && pnpm run smoke",
|
|
@@ -57,5 +57,13 @@
|
|
|
57
57
|
"localBoundary": "Machine paths, credentials, private repositories, and operator state are caller inputs, never package defaults.",
|
|
58
58
|
"supportBoundary": "Configuration, public discovery, lineage, and bounded selection are supported; installation, credential acquisition, board mutation, service mutation, spending, and publishing remain caller-authorized operations."
|
|
59
59
|
}
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"@holoscript/core": "8.0.14"
|
|
63
|
+
},
|
|
64
|
+
"peerDependenciesMeta": {
|
|
65
|
+
"@holoscript/core": {
|
|
66
|
+
"optional": true
|
|
67
|
+
}
|
|
60
68
|
}
|
|
61
69
|
}
|
package/src/index.mjs
CHANGED
|
@@ -32,20 +32,40 @@ export {
|
|
|
32
32
|
HOLOSYSTEM_WHPX_VM_LAUNCH_RECEIPT_SCHEMA,
|
|
33
33
|
HOLOSYSTEM_WHPX_SANDBOXED_VM_LAUNCH_PLAN_SCHEMA,
|
|
34
34
|
HOLOSYSTEM_WHPX_SANDBOXED_VM_LAUNCH_RECEIPT_SCHEMA,
|
|
35
|
+
HOLOSYSTEM_WHPX_APPCONTAINER_VM_LAUNCH_PLAN_SCHEMA,
|
|
36
|
+
HOLOSYSTEM_WHPX_APPCONTAINER_VM_LAUNCH_RECEIPT_SCHEMA,
|
|
37
|
+
HOLOSYSTEM_APPCONTAINER_VM_LAUNCH_PLAN_SCHEMA,
|
|
38
|
+
HOLOSYSTEM_APPCONTAINER_VM_LAUNCH_RECEIPT_SCHEMA,
|
|
39
|
+
HOLOSYSTEM_WINDOWS_APPCONTAINER_PROTOCOL,
|
|
40
|
+
HOLOSYSTEM_WINDOWS_APPCONTAINER_CANARY_DIGEST,
|
|
41
|
+
HOLOSYSTEM_WINDOWS_APPCONTAINER_CANARY_SCHEMA,
|
|
35
42
|
HOLOSYSTEM_WINDOWS_SANDBOX_LAUNCHER_DIGEST,
|
|
36
43
|
HOLOSYSTEM_WINDOWS_SANDBOX_LAUNCHER_SCHEMA,
|
|
37
44
|
HOLOSYSTEM_WINDOWS_SANDBOX_PROTOCOL,
|
|
38
45
|
inspectVmExecutor,
|
|
46
|
+
inspectAppContainerVmLaunchPlan,
|
|
39
47
|
inspectVmLaunchAsset,
|
|
40
48
|
inspectVmLaunchPlan,
|
|
41
49
|
inspectWindowsVmSandboxLauncher,
|
|
50
|
+
inspectWindowsVmAppContainerCanary,
|
|
42
51
|
inspectWhpxSandboxedVmLaunchPlan,
|
|
52
|
+
inspectWhpxAppContainerVmLaunchPlan,
|
|
43
53
|
inspectWhpxVmLaunchPlan,
|
|
44
54
|
runVmLaunch,
|
|
55
|
+
runAppContainerVmLaunch,
|
|
45
56
|
runWhpxSandboxedVmLaunch,
|
|
57
|
+
runWhpxAppContainerVmLaunch,
|
|
46
58
|
runWhpxVmLaunch,
|
|
47
59
|
} from './vm-launch.mjs';
|
|
48
60
|
|
|
61
|
+
export {
|
|
62
|
+
HOLOSCRIPT_CANONICAL_SOURCE_EXTENSIONS,
|
|
63
|
+
HOLOSYSTEM_SOURCE_CANON_SCHEMA,
|
|
64
|
+
inspectGitTrackedSourceCanon,
|
|
65
|
+
inspectSourceCanon,
|
|
66
|
+
renderSourceCanonProjection,
|
|
67
|
+
} from './source-canon.mjs';
|
|
68
|
+
|
|
49
69
|
export {
|
|
50
70
|
HOLOSYSTEM_CATALOG_SCHEMA,
|
|
51
71
|
HOLOSYSTEM_CONSUMER_INPUT_SCHEMA,
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { basename, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export const HOLOSYSTEM_SOURCE_CANON_SCHEMA = 'holoscript.holosystem.source-canon.v1';
|
|
7
|
+
export const HOLOSCRIPT_CANONICAL_SOURCE_EXTENSIONS = Object.freeze([
|
|
8
|
+
'.holo',
|
|
9
|
+
'.hs',
|
|
10
|
+
'.hsplus',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const KNOWN_OPTIONS = new Set(['repository', 'trackedFiles', 'now']);
|
|
14
|
+
const PORTABLE_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/iu;
|
|
15
|
+
|
|
16
|
+
function sha256(value) {
|
|
17
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function requireKnownOptions(value) {
|
|
25
|
+
for (const key of Object.keys(value)) {
|
|
26
|
+
if (!KNOWN_OPTIONS.has(key)) throw new TypeError(`Unknown source-canon option ${key}.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeRepository(value = {}) {
|
|
31
|
+
if (!isRecord(value)) throw new TypeError('repository must be an object.');
|
|
32
|
+
const id = value.id ?? 'workspace';
|
|
33
|
+
const head = value.head ?? null;
|
|
34
|
+
if (!PORTABLE_ID.test(id)) throw new TypeError('repository.id must be a portable identifier.');
|
|
35
|
+
if (head !== null && (typeof head !== 'string' || !/^[a-f0-9]{7,64}$/iu.test(head))) {
|
|
36
|
+
throw new TypeError('repository.head must be a Git object id or null.');
|
|
37
|
+
}
|
|
38
|
+
return { id, head };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeTrackedPath(value) {
|
|
42
|
+
if (
|
|
43
|
+
typeof value !== 'string' ||
|
|
44
|
+
value.length === 0 ||
|
|
45
|
+
value.length > 512 ||
|
|
46
|
+
/[\0-\x1f\x7f]/u.test(value) ||
|
|
47
|
+
/^[A-Za-z]:[\\/]/u.test(value) ||
|
|
48
|
+
/^(?:[\\/]{1,2}|~[\\/])/u.test(value)
|
|
49
|
+
) {
|
|
50
|
+
throw new TypeError(`${String(value)} is not a portable repository-relative path.`);
|
|
51
|
+
}
|
|
52
|
+
const normalized = value.replaceAll('\\', '/');
|
|
53
|
+
const segments = normalized.split('/');
|
|
54
|
+
if (
|
|
55
|
+
segments.some((segment) => segment === '' || segment === '.' || segment === '..') ||
|
|
56
|
+
normalized.startsWith('.//')
|
|
57
|
+
) {
|
|
58
|
+
throw new TypeError(`${value} is not a portable repository-relative path.`);
|
|
59
|
+
}
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sourceExtension(path) {
|
|
64
|
+
return HOLOSCRIPT_CANONICAL_SOURCE_EXTENSIONS.find((extension) => path.endsWith(extension)) ?? null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function foreignExtension(path) {
|
|
68
|
+
const name = path.slice(path.lastIndexOf('/') + 1);
|
|
69
|
+
const index = name.lastIndexOf('.');
|
|
70
|
+
return index <= 0 ? '<none>' : name.slice(index).toLowerCase();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildSourceCanonReport(options, { sources = {}, parser = null } = {}) {
|
|
74
|
+
const repository = normalizeRepository(options.repository);
|
|
75
|
+
if (options.trackedFiles !== undefined && !Array.isArray(options.trackedFiles)) {
|
|
76
|
+
throw new TypeError('trackedFiles must be an array.');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const files = (options.trackedFiles ?? []).map(normalizeTrackedPath).sort();
|
|
80
|
+
if (new Set(files).size !== files.length) throw new TypeError('trackedFiles must be unique.');
|
|
81
|
+
|
|
82
|
+
const generatedAt = new Date(options.now ?? Date.now());
|
|
83
|
+
if (Number.isNaN(generatedAt.getTime())) throw new TypeError('now must be a valid date.');
|
|
84
|
+
|
|
85
|
+
const holoScriptFiles = [];
|
|
86
|
+
const foreignFiles = [];
|
|
87
|
+
const byFormat = Object.fromEntries(
|
|
88
|
+
HOLOSCRIPT_CANONICAL_SOURCE_EXTENSIONS.map((extension) => [extension, 0])
|
|
89
|
+
);
|
|
90
|
+
const foreignByFormat = {};
|
|
91
|
+
|
|
92
|
+
for (const path of files) {
|
|
93
|
+
const extension = sourceExtension(path);
|
|
94
|
+
if (extension) {
|
|
95
|
+
holoScriptFiles.push(path);
|
|
96
|
+
byFormat[extension] += 1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
foreignFiles.push(path);
|
|
100
|
+
const foreign = foreignExtension(path);
|
|
101
|
+
foreignByFormat[foreign] = (foreignByFormat[foreign] ?? 0) + 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const issues = foreignFiles.map((path) => ({
|
|
105
|
+
code: 'foreign-source-format',
|
|
106
|
+
path,
|
|
107
|
+
message: 'Git-tracked canon must be authored in a parser-owned HoloScript source format.',
|
|
108
|
+
}));
|
|
109
|
+
if (holoScriptFiles.length === 0) {
|
|
110
|
+
issues.unshift({
|
|
111
|
+
code: 'holoscript-source-missing',
|
|
112
|
+
path: '$',
|
|
113
|
+
message: 'At least one Git-tracked .holo, .hs, or .hsplus source file is required.',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const path of Object.keys(sources)) {
|
|
118
|
+
const normalized = normalizeTrackedPath(path);
|
|
119
|
+
if (!holoScriptFiles.includes(normalized)) {
|
|
120
|
+
throw new TypeError(`Source ${path} is not a tracked canonical HoloScript path.`);
|
|
121
|
+
}
|
|
122
|
+
if (typeof sources[path] !== 'string') {
|
|
123
|
+
throw new TypeError(`Source ${path} must be a string.`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const parseChecks = holoScriptFiles.map((path) => {
|
|
128
|
+
const source = sources[path];
|
|
129
|
+
if (typeof parser !== 'function' || typeof source !== 'string') {
|
|
130
|
+
issues.push({
|
|
131
|
+
code: 'holoscript-parse-evidence-missing',
|
|
132
|
+
path,
|
|
133
|
+
message:
|
|
134
|
+
'Canonical source requires repository-owned bytes and a real @holoscript/core parse result.',
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
path,
|
|
138
|
+
status: 'not-run',
|
|
139
|
+
sourceDigest: typeof source === 'string' ? sha256(source) : null,
|
|
140
|
+
errors: [],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
let parsed;
|
|
144
|
+
try {
|
|
145
|
+
parsed = parser(source);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
parsed = {
|
|
148
|
+
success: false,
|
|
149
|
+
errors: [{ code: 'parser-threw', message: error?.message ?? String(error) }],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const errors = (Array.isArray(parsed?.errors) ? parsed.errors : []).slice(0, 8).map((error) => ({
|
|
153
|
+
code: error?.code ?? null,
|
|
154
|
+
message: error?.message ?? String(error),
|
|
155
|
+
line: Number.isInteger(error?.line) ? error.line : null,
|
|
156
|
+
column: Number.isInteger(error?.column) ? error.column : null,
|
|
157
|
+
}));
|
|
158
|
+
const passed = parsed?.success === true && errors.length === 0;
|
|
159
|
+
if (!passed) {
|
|
160
|
+
issues.push({
|
|
161
|
+
code: 'holoscript-source-invalid',
|
|
162
|
+
path,
|
|
163
|
+
message: 'Canonical source did not pass @holoscript/core.parse.',
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
path,
|
|
168
|
+
status: passed ? 'passed' : 'failed',
|
|
169
|
+
sourceDigest: sha256(source),
|
|
170
|
+
errors,
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const formatVerified = holoScriptFiles.length > 0 && foreignFiles.length === 0;
|
|
175
|
+
const parserVerified =
|
|
176
|
+
holoScriptFiles.length > 0 && parseChecks.every((item) => item.status === 'passed');
|
|
177
|
+
const verified = formatVerified && parserVerified && issues.length === 0;
|
|
178
|
+
const stableReceipt = {
|
|
179
|
+
schema: HOLOSYSTEM_SOURCE_CANON_SCHEMA,
|
|
180
|
+
scope: 'git-tracked-canon',
|
|
181
|
+
repository,
|
|
182
|
+
formatRegistry: [...HOLOSCRIPT_CANONICAL_SOURCE_EXTENSIONS],
|
|
183
|
+
trackedFiles: files,
|
|
184
|
+
holoScriptFiles,
|
|
185
|
+
foreignFiles,
|
|
186
|
+
parseChecks,
|
|
187
|
+
formatVerified,
|
|
188
|
+
parserVerified,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
schema: HOLOSYSTEM_SOURCE_CANON_SCHEMA,
|
|
193
|
+
generatedAt: generatedAt.toISOString(),
|
|
194
|
+
status: verified ? 'verified' : 'blocked',
|
|
195
|
+
verified,
|
|
196
|
+
formatVerified,
|
|
197
|
+
parserVerified,
|
|
198
|
+
scope: 'git-tracked-canon',
|
|
199
|
+
repository,
|
|
200
|
+
formatRegistry: {
|
|
201
|
+
owner: '@holoscript/holosystem',
|
|
202
|
+
extensions: [...HOLOSCRIPT_CANONICAL_SOURCE_EXTENSIONS],
|
|
203
|
+
callerExtensionsAccepted: false,
|
|
204
|
+
rule: 'New canonical extensions require a parser-owned HoloScript release; callers cannot widen the registry.',
|
|
205
|
+
},
|
|
206
|
+
summary: {
|
|
207
|
+
trackedFiles: files.length,
|
|
208
|
+
holoScriptFiles: holoScriptFiles.length,
|
|
209
|
+
foreignFiles: foreignFiles.length,
|
|
210
|
+
parsePassed: parseChecks.filter((item) => item.status === 'passed').length,
|
|
211
|
+
parseFailed: parseChecks.filter((item) => item.status === 'failed').length,
|
|
212
|
+
parseMissing: parseChecks.filter((item) => item.status === 'not-run').length,
|
|
213
|
+
byFormat,
|
|
214
|
+
foreignByFormat: Object.fromEntries(Object.entries(foreignByFormat).sort()),
|
|
215
|
+
},
|
|
216
|
+
holoScriptFiles,
|
|
217
|
+
foreignFiles,
|
|
218
|
+
parseChecks,
|
|
219
|
+
issues,
|
|
220
|
+
boundaries: {
|
|
221
|
+
untrackedRuntimeIsCanon: false,
|
|
222
|
+
dependencyCachesAreCanon: false,
|
|
223
|
+
generatedArtifactsAreCanonOnlyWhenTracked: true,
|
|
224
|
+
extensionOnlyClaimsAccepted: false,
|
|
225
|
+
},
|
|
226
|
+
receiptHash: sha256(JSON.stringify(stableReceipt)),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function inspectSourceCanon(options = {}) {
|
|
231
|
+
if (!isRecord(options)) throw new TypeError('source-canon options must be an object.');
|
|
232
|
+
requireKnownOptions(options);
|
|
233
|
+
return buildSourceCanonReport(options);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function runGit(rootDirectory, args) {
|
|
237
|
+
const result = spawnSync('git', ['-C', rootDirectory, ...args], {
|
|
238
|
+
encoding: 'utf8',
|
|
239
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
240
|
+
});
|
|
241
|
+
if (result.status !== 0) {
|
|
242
|
+
throw new Error(`Git ${args.join(' ')} failed: ${String(result.stderr || '').trim()}`);
|
|
243
|
+
}
|
|
244
|
+
return result.stdout;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function inspectGitTrackedSourceCanon({ rootDirectory = process.cwd(), now } = {}) {
|
|
248
|
+
const root = resolve(rootDirectory);
|
|
249
|
+
const repositoryRoot = resolve(runGit(root, ['rev-parse', '--show-toplevel']).trim());
|
|
250
|
+
if (repositoryRoot.toLowerCase() !== root.toLowerCase()) {
|
|
251
|
+
throw new Error('source-canon must run from the repository root.');
|
|
252
|
+
}
|
|
253
|
+
const idCandidate = basename(root).toLowerCase();
|
|
254
|
+
const repository = {
|
|
255
|
+
id: PORTABLE_ID.test(idCandidate) ? idCandidate : 'workspace',
|
|
256
|
+
head: runGit(root, ['rev-parse', 'HEAD']).trim(),
|
|
257
|
+
};
|
|
258
|
+
const trackedFiles = runGit(root, ['ls-files', '-z'])
|
|
259
|
+
.split('\0')
|
|
260
|
+
.filter(Boolean);
|
|
261
|
+
const sources = Object.fromEntries(
|
|
262
|
+
trackedFiles
|
|
263
|
+
.map(normalizeTrackedPath)
|
|
264
|
+
.filter((path) => sourceExtension(path))
|
|
265
|
+
.map((path) => [path, readFileSync(resolve(root, path), 'utf8')])
|
|
266
|
+
);
|
|
267
|
+
let parser = null;
|
|
268
|
+
try {
|
|
269
|
+
const core = await import('@holoscript/core');
|
|
270
|
+
if (typeof core.parse === 'function') parser = core.parse;
|
|
271
|
+
} catch {
|
|
272
|
+
// The report fails closed with parse-evidence-missing issues below.
|
|
273
|
+
}
|
|
274
|
+
return buildSourceCanonReport({ repository, trackedFiles, now }, { sources, parser });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function renderSourceCanonProjection(report) {
|
|
278
|
+
if (!isRecord(report) || report.schema !== HOLOSYSTEM_SOURCE_CANON_SCHEMA) {
|
|
279
|
+
throw new TypeError(`Expected ${HOLOSYSTEM_SOURCE_CANON_SCHEMA}.`);
|
|
280
|
+
}
|
|
281
|
+
return `composition "HoloSystemSourceCanon" {
|
|
282
|
+
state {
|
|
283
|
+
status: "${report.status}"
|
|
284
|
+
verified: ${report.verified}
|
|
285
|
+
formatVerified: ${report.formatVerified}
|
|
286
|
+
parserVerified: ${report.parserVerified}
|
|
287
|
+
scope: "${report.scope}"
|
|
288
|
+
trackedFiles: ${report.summary.trackedFiles}
|
|
289
|
+
holoScriptFiles: ${report.summary.holoScriptFiles}
|
|
290
|
+
foreignFiles: ${report.summary.foreignFiles}
|
|
291
|
+
holoFiles: ${report.summary.byFormat['.holo']}
|
|
292
|
+
hsFiles: ${report.summary.byFormat['.hs']}
|
|
293
|
+
hsplusFiles: ${report.summary.byFormat['.hsplus']}
|
|
294
|
+
parsePassed: ${report.summary.parsePassed}
|
|
295
|
+
parseFailed: ${report.summary.parseFailed}
|
|
296
|
+
parseMissing: ${report.summary.parseMissing}
|
|
297
|
+
receiptHash: "${report.receiptHash}"
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
object "LanguageSovereignty" {
|
|
301
|
+
canonicalFormats: ".holo,.hs,.hsplus"
|
|
302
|
+
callerExtensionsAccepted: false
|
|
303
|
+
migrationRequired: ${!report.verified}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
`;
|
|
307
|
+
}
|