@amalgm/shell 0.1.164-preview.34187698689 → 0.1.164-preview.34192807991

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/PURPOSE.md CHANGED
@@ -6,6 +6,9 @@ while its host can execute, recover without opening a UI, contain a failing
6
6
  workload, and leave the machine's resources available for useful work. Shell
7
7
  owns composition, process boot, and native host effects; Core and the product
8
8
  SDKs own the decisions those effects execute.
9
+ At the completed brick/mortar boundary, compatible bricks advance through independently selected artifacts while Shell
10
+ remains the mortar; publishing a brick must not require publishing unchanged
11
+ Shell code.
9
12
 
10
13
  ## Operating axioms
11
14
 
@@ -15,6 +18,9 @@ defines the terms and examinations; the [September 2026 audit](docs/operating-au
15
18
  records implementation gaps and historical evidence.
16
19
  The [release environments plan](docs/release-environments-plan.md) records the
17
20
  agreed CI/CD and update evolution; its target behavior is not yet delivered.
21
+ The owner permits temporary bundled Shell releases for brick changes during
22
+ boundary cleanup. Independent brick adoption remains the target, and Engine
23
+ migration is out of scope because no existing Engine users need it.
18
24
 
19
25
  - **O1 — Lifetime follows installation intent.** One installation owner keeps
20
26
  the enabled runtime available independently of its launching UI or terminal
@@ -66,12 +72,12 @@ native examinations before either becomes a managed public candidate. Public
66
72
  metadata records the exact internal archive it was derived from; no wrapper,
67
73
  copied Engine implementation or second dependency graph supplies runtime behavior.
68
74
 
69
- Consumer assembly reads dependencies from each declared package manifest and
70
- selects only registry candidates from the same managed branch. It changes exact
71
- pins and their locks in one reproducible composition change; the consumer's
72
- complete checks gate merging that exact tree against its unchanged base. Reconciliation keeps one
73
- pending change per branch, preserves human edits, and never republishes an
74
- unchanged consumer merely because a notification or check was repeated.
75
+ At the target boundary, release assembly selects exact registry artifacts from the same managed branch
76
+ into a signed installation recipe. Compatible brick changes reuse the existing
77
+ Shell artifact without a Shell dependency-update PR or lockfile edit. A product
78
+ owns changes to libraries embedded in its own artifact. Reconciliation preserves
79
+ human edits, coalesces candidates, and never republishes unchanged components
80
+ because a notification or check was repeated.
75
81
 
76
82
  1. **The shell implements host effects; SDKs own domain decisions.** Shell
77
83
  assembles published capabilities, implements their native effect ports,
@@ -157,17 +163,25 @@ changes it. Examinations inject their credential store explicitly.
157
163
  decisions receive effects explicitly and never discover ambient process
158
164
  state or disk on their own.
159
165
 
160
- 5. **Bricks never update themselves; the shell updates.** Every direct `@amalgm/*`
161
- dependency is pinned to an exact version — no ranges. Publishing a brick
162
- changes nothing on any user machine until the shell bumps its pin and ships
163
- a new release through its lane's signed runtime authority. Registry tags
164
- locate published artifacts; fresh TUF metadata selects installed adoption.
165
- One version number, one atomic update, one combination that was
166
- actually tested together — never a per-brick drift of versions in the wild.
167
- The shrinkwrap fixes the transitive graph as well; direct pins do not
168
- imply that every consumer resolves the same Core version. Release proof
169
- checks those actual resolutions and their public contracts, and upgrades
170
- affected consumers deliberately instead of forcing an untested override.
166
+ 5. **Bricks release independently; one installation owner adopts the graph.**
167
+ Shell carries allowed component identities and supported host contracts,
168
+ without bundling brick implementations or pinning their runtime versions.
169
+ A separately signed recipe selects exact compatible artifacts and dependency
170
+ graphs; compatible brick changes reuse unchanged Shell and UI artifacts.
171
+ Registry tags locate candidates; fresh TUF metadata authorizes adoption.
172
+ Bricks never update themselves, and one coherent tested graph governs each
173
+ activation and its recovery. Artifact identity is separate from process
174
+ lifetime: a changed in-process implementation requires draining its actual
175
+ process cohort even when Shell's bytes stay unchanged. Release proof checks
176
+ actual dependency resolutions and public contracts; it never forces all
177
+ consumers onto one untested Core version.
178
+
179
+ Accepted transition: current Shell packages still bundle a pinned SDK graph,
180
+ and the current updater stages one Shell archive. A brick change may therefore
181
+ temporarily require a checked Shell release while the boundary is completed;
182
+ this does not block initial delivery. The
183
+ [release contract](docs/release-environments-plan.md#current-release-contract--2026-09-08)
184
+ defines the required transition and distinguishes it from existing evidence.
171
185
 
172
186
  The installed runtime follows the [update axioms](src/updates/README.md).
173
187
  Each public managed package declares its controller, process and state-format
@@ -997,7 +1011,7 @@ Local profiles retain their selected sources and do not follow managed targets.
997
1011
 
998
1012
  65. **A Shell release is tested as the artifact users install.** The release
999
1013
  gate packs Shell, installs that tarball with the exact public Amalgm
1000
- dependency versions declared by its manifest, and exercises authorization
1014
+ artifacts selected by the installation recipe, and exercises authorization
1001
1015
  through Chat using package exports only. Sibling repositories and source
1002
1016
  paths are never an integration-test dependency.
1003
1017
 
@@ -18,6 +18,32 @@ const run = (file, args, options = {}) => execFileSync(file, args, { encoding: '
18
18
  const sudo = (file, args) => run('sudo', ['-n', file, ...args]);
19
19
  const powershell = (source, env = {}) => run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', source], { env: { ...process.env, ...env } });
20
20
 
21
+ async function waitForNativeResolution(hostsFile, expectedHosts) {
22
+ const deadline = Date.now() + 15_000;
23
+ let observed;
24
+ do {
25
+ try {
26
+ // A fresh, bounded process uses the OS resolver used by installed clients.
27
+ observed = JSON.parse(run(process.execPath, ['--input-type=module', '-e',
28
+ "import { lookup } from 'node:dns/promises'; process.stdout.write(JSON.stringify(await lookup(process.argv[1], { all: true })));",
29
+ canonicalHost], { timeout: Math.max(1, deadline - Date.now()) }));
30
+ if (observed.length > 0 && observed.every(value => value.address === '127.0.0.1')) return;
31
+ } catch (error) { observed = error.message; }
32
+ await new Promise(resolve => setTimeout(resolve, 100));
33
+ } while (Date.now() < deadline);
34
+ const bytes = await fs.readFile(hostsFile);
35
+ const stat = await fs.stat(hostsFile);
36
+ const hosts = { path: await fs.realpath(hostsFile), mode: (stat.mode & 0o777).toString(8),
37
+ uid: stat.uid, gid: stat.gid, exact: bytes.equals(expectedHosts),
38
+ entries: bytes.toString('utf8').split(/\r?\n/).filter(line => line.includes(canonicalHost)) };
39
+ let native;
40
+ if (process.platform === 'darwin') {
41
+ try { native = run('dscacheutil', ['-q', 'host', '-a', 'name', canonicalHost]); }
42
+ catch (error) { native = error.message; }
43
+ }
44
+ throw new Error(`The native resolver did not adopt the examination host: ${JSON.stringify({ observed, hosts, native })}`);
45
+ }
46
+
21
47
  export async function startNetworkView({ temporary, immutable, previous = {}, candidate, artifacts = [], events, hint = {}, serviceEnvironment = false }) {
22
48
  assert.equal(process.env.CI, 'true');
23
49
  assert.equal(process.env.GITHUB_ACTIONS, 'true', 'Never replace DNS or TLS trust on a developer account');
@@ -29,7 +55,8 @@ export async function startNetworkView({ temporary, immutable, previous = {}, ca
29
55
  const ca = path.join(directory, 'ca.crt');
30
56
  const key = path.join(directory, 'server.key');
31
57
  const certificate = path.join(directory, 'server.crt');
32
- const name = `Amalgm disposable native examination ${crypto.randomUUID()}`;
58
+ const name = `Amalgm native examination ${crypto.randomUUID()}`;
59
+ assert.ok(Buffer.byteLength(name, 'utf8') <= 64, 'A disposable authority must fit the X.509 common-name limit');
33
60
  const extension = path.join(directory, 'server.ext');
34
61
  await fs.writeFile(extension, `subjectAltName=DNS:${canonicalHost}\nextendedKeyUsage=serverAuth\n`);
35
62
  run('openssl', ['req', '-x509', '-newkey', 'rsa:2048', '-nodes', '-days', '1', '-subj', `/CN=${name}`, '-keyout', path.join(directory, 'ca.key'), '-out', ca], { stdio: 'pipe' });
@@ -39,9 +66,7 @@ export async function startNetworkView({ temporary, immutable, previous = {}, ca
39
66
  await fs.rm(path.join(directory, 'ca.key'));
40
67
  const hostsFile = process.platform === 'win32' ? path.join(process.env.SystemRoot, 'System32/drivers/etc/hosts') : '/etc/hosts';
41
68
  const originalHosts = await fs.readFile(hostsFile);
42
- const hosts = path.join(directory, 'hosts');
43
- await fs.writeFile(hosts, Buffer.concat([originalHosts, Buffer.from(`\n127.0.0.1 ${canonicalHost}\n`)]));
44
- const original = path.join(directory, 'hosts.original'); await fs.writeFile(original, originalHosts);
69
+ const examinationHosts = Buffer.concat([originalHosts, Buffer.from(`\n127.0.0.1 ${canonicalHost}\n`)]);
45
70
  const linuxCa = `/usr/local/share/ca-certificates/amalgm-examination-${crypto.randomUUID()}.crt`;
46
71
  const nss = path.join(process.env.HOME || process.env.USERPROFILE, '.pki/nssdb');
47
72
  const fingerprint = run('openssl', ['x509', '-in', ca, '-noout', '-fingerprint', '-sha1']).split('=')[1].trim().replace(/:/g, '');
@@ -52,13 +77,27 @@ export async function startNetworkView({ temporary, immutable, previous = {}, ca
52
77
  else if (process.platform === 'linux') run('systemctl', ['--user', value === null ? 'unset-environment' : 'set-environment', value === null ? 'NODE_EXTRA_CA_CERTS' : `NODE_EXTRA_CA_CERTS=${value}`]);
53
78
  else powershell("[Environment]::SetEnvironmentVariable('NODE_EXTRA_CA_CERTS', $env:AMALGM_EXAMINATION_CA, 'User')", { AMALGM_EXAMINATION_CA: value ?? '' });
54
79
  };
55
- const copyHosts = async source => {
56
- if (process.platform === 'win32') await fs.copyFile(source, hostsFile);
57
- else sudo('cp', [source, hostsFile]);
80
+ const writeHosts = bytes => {
81
+ // Preserve the existing readable inode and metadata for native observers.
82
+ // File-copy tools can temporarily make the destination root-only, causing
83
+ // the resolver to miss the data change while it cannot reopen the file.
84
+ run(process.platform === 'win32' ? process.execPath : 'sudo', [
85
+ ...(process.platform === 'win32' ? [] : ['-n', process.execPath]),
86
+ '--input-type=module', '-e', `
87
+ import fs from 'node:fs';
88
+ const bytes = fs.readFileSync(0);
89
+ const fd = fs.openSync(process.argv[1], 'r+');
90
+ try { fs.writeFileSync(fd, bytes); fs.ftruncateSync(fd, bytes.length); fs.fsyncSync(fd); }
91
+ finally { fs.closeSync(fd); }
92
+ `, hostsFile,
93
+ ], { input: bytes });
58
94
  };
59
95
  const close = async () => {
60
96
  const errors = [];
61
- try {
97
+ const attempt = async effect => {
98
+ try { await effect(); } catch (error) { errors.push(error); }
99
+ };
100
+ await attempt(async () => {
62
101
  if (child && child.exitCode === null) {
63
102
  child.stdin.end();
64
103
  await new Promise((resolve, reject) => {
@@ -66,19 +105,28 @@ export async function startNetworkView({ temporary, immutable, previous = {}, ca
66
105
  child.once('exit', () => { clearTimeout(timeout); resolve(); });
67
106
  });
68
107
  }
69
- } catch (error) { errors.push(error); }
70
- if (changedHosts) await copyHosts(original);
71
- if (changedServiceEnvironment) serviceCa(priorServiceCa);
108
+ });
109
+ if (changedHosts) await attempt(() => writeHosts(originalHosts));
110
+ if (changedServiceEnvironment) await attempt(() => serviceCa(priorServiceCa));
72
111
  if (process.platform === 'darwin') {
73
- if (trusted) sudo('security', ['remove-trusted-cert', '-d', ca]);
74
- sudo('dscacheutil', ['-flushcache']);
75
- sudo('killall', ['-HUP', 'mDNSResponder']);
112
+ if (trusted) {
113
+ // Removing the last admin record can request GUI authorization on macOS.
114
+ // Explicitly revoke this CA, then delete its owned certificate; the
115
+ // negative trust record lasts only until this disposable CI VM ends.
116
+ await attempt(() => sudo('security', ['add-trusted-cert', '-d', '-r', 'deny', '-k', '/Library/Keychains/System.keychain', ca]));
117
+ await attempt(() => sudo('security', ['delete-certificate', '-Z', fingerprint, '/Library/Keychains/System.keychain']));
118
+ }
119
+ await attempt(() => sudo('dscacheutil', ['-flushcache']));
120
+ await attempt(() => sudo('killall', ['-HUP', 'mDNSResponder']));
76
121
  } else if (process.platform === 'linux') {
77
- if (nssTrusted) run('certutil', ['-D', '-d', `sql:${nss}`, '-n', name]);
78
- if (trusted) { sudo('rm', ['-f', linuxCa]); sudo('update-ca-certificates', []); }
122
+ if (nssTrusted) await attempt(() => run('certutil', ['-D', '-d', `sql:${nss}`, '-n', name]));
123
+ if (trusted) {
124
+ await attempt(() => sudo('rm', ['-f', linuxCa]));
125
+ await attempt(() => sudo('update-ca-certificates', []));
126
+ }
79
127
  } else {
80
- if (trusted) run('certutil.exe', ['-user', '-delstore', 'Root', fingerprint]);
81
- run('ipconfig.exe', ['/flushdns']);
128
+ if (trusted) await attempt(() => run('certutil.exe', ['-delstore', 'Root', fingerprint]));
129
+ await attempt(() => run('ipconfig.exe', ['/flushdns']));
82
130
  }
83
131
  if (errors.length) throw new AggregateError(errors, 'Examination transport cleanup failed');
84
132
  };
@@ -93,15 +141,16 @@ export async function startNetworkView({ temporary, immutable, previous = {}, ca
93
141
  run('certutil', ['-A', '-d', `sql:${nss}`, '-n', name, '-t', 'C,,', '-i', ca]); nssTrusted = true;
94
142
  } else {
95
143
  powershell("if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Disposable Windows CI account must be an administrator' }");
96
- run('certutil.exe', ['-user', '-addstore', 'Root', ca]); trusted = true;
144
+ run('certutil.exe', ['-addstore', '-f', 'Root', ca]); trusted = true;
97
145
  }
98
146
  if (serviceEnvironment) {
99
147
  if (process.platform === 'darwin') { try { priorServiceCa = run('launchctl', ['getenv', 'NODE_EXTRA_CA_CERTS']).trim() || null; } catch { /* unset */ } }
100
148
  else if (process.platform === 'linux') priorServiceCa = run('systemctl', ['--user', 'show-environment']).split('\n').find(line => line.startsWith('NODE_EXTRA_CA_CERTS='))?.slice('NODE_EXTRA_CA_CERTS='.length) ?? null;
101
- else priorServiceCa = JSON.parse(powershell("ConvertTo-Json -Compress -InputObject ([Environment]::GetEnvironmentVariable('NODE_EXTRA_CA_CERTS', 'User'))"));
149
+ else priorServiceCa = JSON.parse(powershell("ConvertTo-Json -Compress -InputObject @{ value = [Environment]::GetEnvironmentVariable('NODE_EXTRA_CA_CERTS', 'User') }")).value;
102
150
  serviceCa(ca); changedServiceEnvironment = true;
103
151
  }
104
- await copyHosts(hosts); changedHosts = true;
152
+ changedHosts = true; writeHosts(examinationHosts);
153
+ assert.deepEqual(await fs.readFile(hostsFile), examinationHosts, 'The OS hosts file must contain the exact examination view');
105
154
  if (process.platform === 'darwin') { sudo('dscacheutil', ['-flushcache']); sudo('killall', ['-HUP', 'mDNSResponder']); }
106
155
  else if (process.platform === 'win32') run('ipconfig.exe', ['/flushdns']);
107
156
  const config = path.join(directory, 'config.json');
@@ -128,8 +177,13 @@ export async function startNetworkView({ temporary, immutable, previous = {}, ca
128
177
  });
129
178
  };
130
179
  await command({ operation: 'status' });
180
+ await waitForNativeResolution(hostsFile, examinationHosts);
131
181
  return { ca, close, command };
132
- } catch (error) { await close(); throw error; }
182
+ } catch (error) {
183
+ try { await close(); }
184
+ catch (cleanup) { throw new AggregateError([error, cleanup], 'Examination transport initialization and cleanup failed'); }
185
+ throw error;
186
+ }
133
187
  }
134
188
 
135
189
  async function serve(configFile) {
@@ -0,0 +1,159 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs/promises';
4
+ import https from 'node:https';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { execFile, execFileSync } from 'node:child_process';
8
+ import { promisify } from 'node:util';
9
+ import { canonicalHost, startNetworkView } from './network-view.mjs';
10
+
11
+ const execute = promisify(execFile);
12
+ const resource = value => ({ body: Buffer.from(value).toString('base64') });
13
+ const run = (file, args) => execFileSync(file, args, { encoding: 'utf8', timeout: 15_000 });
14
+ function serviceCa() {
15
+ if (process.platform === 'darwin') {
16
+ try { return run('launchctl', ['getenv', 'NODE_EXTRA_CA_CERTS']).trim() || null; }
17
+ catch { return null; }
18
+ }
19
+ if (process.platform === 'linux') return run('systemctl', ['--user', 'show-environment'])
20
+ .split('\n').find(line => line.startsWith('NODE_EXTRA_CA_CERTS='))?.slice('NODE_EXTRA_CA_CERTS='.length) ?? null;
21
+ return JSON.parse(run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command',
22
+ "ConvertTo-Json -Compress -InputObject @{ value = [Environment]::GetEnvironmentVariable('NODE_EXTRA_CA_CERTS', 'User') }"])).value;
23
+ }
24
+
25
+ // Each request discovers the OS trust store anew. No supplied CA, DNS lookup,
26
+ // disabled TLS validation or inherited Node options can substitute for it.
27
+ async function nativeRequest(options) {
28
+ const env = { ...process.env };
29
+ for (const name of Object.keys(env)) {
30
+ if (/^(NODE_EXTRA_CA_CERTS|NODE_OPTIONS|NODE_TLS_REJECT_UNAUTHORIZED|SSL_CERT_FILE|SSL_CERT_DIR)$/i.test(name)) delete env[name];
31
+ }
32
+ const { stdout } = await execute(process.execPath, ['--use-system-ca', '--input-type=module', '-e', `
33
+ import https from 'node:https';
34
+ const result = await new Promise(resolve => {
35
+ const request = https.get({ ...JSON.parse(process.argv[1]), agent: false, signal: AbortSignal.timeout(10_000) }, response => {
36
+ const peer = { authorized: response.socket.authorized, address: response.socket.remoteAddress };
37
+ let body = '';
38
+ response.setEncoding('utf8');
39
+ response.on('data', data => { body += data; });
40
+ response.on('end', () => resolve({ status: response.statusCode, body, ...peer }));
41
+ response.on('error', error => resolve({ error: error.code, message: error.message }));
42
+ });
43
+ request.on('error', error => resolve({ error: error.code, message: error.message }));
44
+ });
45
+ process.stdout.write(JSON.stringify(result));
46
+ `, JSON.stringify(options)], { env, timeout: 15_000, maxBuffer: 64 * 1024 });
47
+ return JSON.parse(stdout);
48
+ }
49
+
50
+ test('disposable native transport owns DNS, TLS trust, selected views and cleanup', {
51
+ skip: process.env.AMALGM_TEST_NATIVE_NETWORK !== '1', timeout: 180_000,
52
+ }, async () => {
53
+ assert.equal(process.env.CI, 'true');
54
+ assert.equal(process.env.GITHUB_ACTIONS, 'true', 'Native transport examination requires a disposable GitHub runner');
55
+ const hostsFile = process.platform === 'win32' ? path.join(process.env.SystemRoot, 'System32/drivers/etc/hosts') : '/etc/hosts';
56
+ const originalHosts = await fs.readFile(hostsFile);
57
+ const originalServiceCa = serviceCa();
58
+ const originalProcessCa = process.env.NODE_EXTRA_CA_CERTS;
59
+ const temporary = await fs.mkdtemp(path.join(os.tmpdir(), 'amalgm-network-preflight-'));
60
+ const scope = '/trust/native-preflight';
61
+ const immutablePath = `${scope}/immutable`;
62
+ const selectedPath = `${scope}/selected`;
63
+ const events = `${scope}/events`;
64
+ let network; let leafServer; let streamRequest;
65
+ const failures = [];
66
+ const request = pathname => nativeRequest({ hostname: canonicalHost, path: pathname });
67
+ const expect = async (pathname, status, body) => {
68
+ assert.deepEqual(await request(pathname), { status, body, authorized: true, address: '127.0.0.1' });
69
+ };
70
+ try {
71
+ network = await startNetworkView({ temporary, events, serviceEnvironment: true,
72
+ immutable: { [immutablePath]: resource('immutable\n') },
73
+ previous: { [selectedPath]: resource('previous\n') },
74
+ candidate: { [selectedPath]: resource('candidate\n') },
75
+ });
76
+ assert.equal(serviceCa(), network.ca);
77
+ await expect(immutablePath, 200, 'immutable\n');
78
+ await expect(selectedPath, 200, 'previous\n');
79
+ await network.command({ operation: 'publish' });
80
+ await expect(selectedPath, 200, 'candidate\n');
81
+ await expect(immutablePath, 200, 'immutable\n');
82
+ await network.command({ operation: 'unavailable' });
83
+ await expect(selectedPath, 404, '');
84
+ await expect(events, 503, '');
85
+ await expect(immutablePath, 200, 'immutable\n');
86
+ await network.command({ operation: 'reset' });
87
+ await expect(selectedPath, 200, 'previous\n');
88
+
89
+ // Supply the original CA with the actual leaf so cleanup must revoke trust
90
+ // even when the peer supplies its full chain after OS certificate deletion.
91
+ const leaf = path.join(temporary, 'network/server.crt');
92
+ const caFile = network.ca;
93
+ const ca = await fs.readFile(caFile);
94
+ const nativeEvaluation = ['verify-cert', '-c', leaf, '-c', caFile, '-p', 'ssl', '-s', canonicalHost, '-L'];
95
+ if (process.platform === 'darwin') run('security', nativeEvaluation);
96
+ leafServer = https.createServer({
97
+ key: await fs.readFile(path.join(temporary, 'network/server.key')),
98
+ cert: Buffer.concat([await fs.readFile(leaf), ca]),
99
+ }, (_request, response) => response.end('same leaf\n'));
100
+ await new Promise((resolve, reject) => {
101
+ leafServer.once('error', reject); leafServer.listen(0, '127.0.0.1', resolve);
102
+ });
103
+ const leafAddress = { hostname: '127.0.0.1', port: leafServer.address().port, servername: canonicalHost, path: '/' };
104
+ assert.deepEqual(await nativeRequest(leafAddress), { status: 200, body: 'same leaf\n', authorized: true, address: '127.0.0.1' });
105
+
106
+ // Hold an event stream through cleanup; its lifetime belongs to the view.
107
+ let streamClosed;
108
+ await new Promise((resolve, reject) => {
109
+ streamRequest = https.get(`https://${canonicalHost}${events}`, { ca, agent: false, signal: AbortSignal.timeout(15_000) }, response => {
110
+ streamClosed = new Promise(done => response.once('close', done));
111
+ response.on('error', () => {}); // Shutdown may terminate the open stream.
112
+ if (response.statusCode !== 200) { response.resume(); reject(new Error(`Event stream returned ${response.statusCode}`)); return; }
113
+ let frame = ''; let ready = false;
114
+ response.setEncoding('utf8');
115
+ response.on('data', data => {
116
+ if (ready) return;
117
+ frame += data;
118
+ if (!frame.includes('\n\n')) return;
119
+ try { assert.match(frame, /^event: ready\n/); ready = true; resolve(); } catch (error) { reject(error); }
120
+ });
121
+ response.once('close', () => { if (!ready) reject(new Error('Event stream closed before readiness')); });
122
+ });
123
+ streamRequest.on('error', reject);
124
+ });
125
+ assert.equal((await network.command({ operation: 'status' })).subscribers, 1);
126
+ const closing = network; network = undefined;
127
+ await closing.close();
128
+ await streamClosed;
129
+ assert.deepEqual(await fs.readFile(hostsFile), originalHosts, 'DNS cleanup restores the exact prior hosts bytes');
130
+ assert.equal(serviceCa(), originalServiceCa, 'Native service-manager trust environment is restored');
131
+ assert.equal(process.env.NODE_EXTRA_CA_CERTS, originalProcessCa, 'Caller trust environment remains owned by the caller');
132
+ assert.equal((await nativeRequest({ hostname: '127.0.0.1', servername: canonicalHost, path: selectedPath })).error,
133
+ 'ECONNREFUSED', 'The privileged HTTPS listener exits with its view');
134
+ const untrusted = await nativeRequest(leafAddress);
135
+ assert.match(untrusted.error ?? '', /^(UNABLE_TO_VERIFY_LEAF_SIGNATURE|UNABLE_TO_GET_ISSUER_CERT_LOCALLY|CERT_UNTRUSTED|CERT_REJECTED|SELF_SIGNED_CERT_IN_CHAIN|DEPTH_ZERO_SELF_SIGNED_CERT)$/,
136
+ `The retired disposable CA must no longer authorize its supplied chain: ${JSON.stringify(untrusted)}`);
137
+ if (process.platform === 'darwin') {
138
+ assert.throws(() => run('security', nativeEvaluation),
139
+ error => Number.isInteger(error.status) && error.status > 0 && error.signal === null,
140
+ 'The same native SSL policy invocation must reject the chain it previously accepted');
141
+ }
142
+ } catch (error) { failures.push(error); }
143
+ finally {
144
+ // Cleanup has its own verdict; it must not erase the initiating failure or
145
+ // prevent the other owned resources from being released.
146
+ for (const close of [
147
+ () => streamRequest?.destroy(),
148
+ () => network?.close(),
149
+ async () => {
150
+ if (leafServer) { leafServer.closeAllConnections(); await new Promise(resolve => leafServer.close(resolve)); }
151
+ },
152
+ () => fs.rm(temporary, { recursive: true, force: true }),
153
+ ]) {
154
+ try { await close(); } catch (error) { failures.push(error); }
155
+ }
156
+ }
157
+ if (failures.length === 1) throw failures[0];
158
+ if (failures.length) throw new AggregateError(failures, 'Native transport examination and cleanup failed');
159
+ });
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@amalgm/shell",
3
- "version": "0.1.164-preview.34187698689",
3
+ "version": "0.1.164-preview.34192807991",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@amalgm/shell",
9
- "version": "0.1.164-preview.34187698689",
9
+ "version": "0.1.164-preview.34192807991",
10
10
  "license": "UNLICENSED",
11
11
  "dependencies": {
12
12
  "@amalgm/agents": "0.2.3-preview.34176069706",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amalgm/shell",
3
- "version": "0.1.164-preview.34187698689",
3
+ "version": "0.1.164-preview.34192807991",
4
4
  "description": "The Amalgm runtime host assembled from the published @amalgm SDK capabilities.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -151,11 +151,11 @@
151
151
  }
152
152
  ]
153
153
  },
154
- "gitHead": "dda47143429c299e10b59709f6dc328d96cedbd7",
154
+ "gitHead": "f61742639720d43804c6c19aa14c2bdda763a78d",
155
155
  "amalgmSource": {
156
156
  "repository": "amalgm-inc/amalgm-shell",
157
157
  "branch": "preview",
158
- "commit": "dda47143429c299e10b59709f6dc328d96cedbd7",
159
- "occurrence": "34187698689"
158
+ "commit": "f61742639720d43804c6c19aa14c2bdda763a78d",
159
+ "occurrence": "34192807991"
160
160
  }
161
161
  }