@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.
@@ -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`. `build.ps1` compiles it with the inbox .NET
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 launcher applies only the process controls it reports: maximum-privilege
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
- Get-FileHash -LiteralPath $output -Algorithm SHA256 | Select-Object Path, Hash
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holoscript/holosystem",
3
- "version": "0.2.4",
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
+ }