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

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.
@@ -2,7 +2,7 @@ import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import crypto from 'node:crypto';
4
4
  import fs from 'node:fs/promises';
5
- import { createWriteStream } from 'node:fs';
5
+ import { constants, createWriteStream } from 'node:fs';
6
6
  import os from 'node:os';
7
7
  import path from 'node:path';
8
8
  import { createRequire } from 'node:module';
@@ -41,6 +41,61 @@ function journal(identity) {
41
41
  return { launch: read('launch'), policy: read('policy') };
42
42
  } finally { db.close(); }
43
43
  }
44
+
45
+ /** Failure evidence observes existing owners without copying credentials or
46
+ * creating another update decision. Each input and output has a byte bound. */
47
+ export async function runtimeFailureEvidence(packageRoot, identity) {
48
+ if (!identity) return { unavailable: 'The installed service did not return an identity before failing' };
49
+ const evidence = {};
50
+ const capture = async (name, read) => {
51
+ try { evidence[name] = await read(); }
52
+ catch (error) { evidence[name] = { error: String(error.message).slice(0, 1024), code: error.code ?? null }; }
53
+ };
54
+ const pick = (value, names) => value ? Object.fromEntries(names.filter(name => value[name] !== undefined).map(name => [name, value[name]])) : null;
55
+ const release = value => value ? { digest: value.digest, id: value.target?.id, sequence: value.target?.sequence } : null;
56
+ const command = value => pick(value, ['node', 'bin']);
57
+ await capture('controller', async () => {
58
+ // This existing API authenticates the owner and bounds its response to
59
+ // 16 KiB/two seconds; never copy controller.json or its control token.
60
+ const { keepaliveStatus } = await import(pathToFileURL(path.join(packageRoot, 'dist/service/control.js')).href);
61
+ const status = await keepaliveStatus(identity);
62
+ return pick(status, ['alive', 'pid', 'bootstrapPid', 'childPid', 'restarts', 'phase', 'recovery', 'update']);
63
+ });
64
+ await capture('journal', () => {
65
+ const db = new DatabaseSync(path.toNamespacedPath(path.join(identity.serviceDir, 'updates.sqlite')), { readOnly: true });
66
+ try {
67
+ db.exec('PRAGMA busy_timeout = 1000');
68
+ const statement = db.prepare('SELECT length(CAST(value AS BLOB)) AS bytes, CASE WHEN length(CAST(value AS BLOB)) <= ? THEN value END AS value FROM update_state WHERE key = ?');
69
+ const read = (key, project) => {
70
+ const row = statement.get(128 * 1024, key);
71
+ return !row ? null : row.value === null ? { omittedBytes: row.bytes } : project(JSON.parse(row.value));
72
+ };
73
+ return {
74
+ launch: read('launch', value => ({ ...pick(value, ['schema', 'attempts', 'maxAttempts']), current: command(value?.current),
75
+ pending: value?.pending ? { ...pick(value.pending, ['attempted', 'recoveryAttempted']),
76
+ candidate: command(value.pending.candidate), recovery: command(value.pending.recovery) } : null })),
77
+ policy: read('policy', value => value ? ({ ...pick(value, ['schema', 'epoch', 'activeEpoch', 'activeHealthy', 'quarantine']),
78
+ active: release(value.active), desired: release(value.desired),
79
+ transaction: value.transaction ? { ...pick(value.transaction,
80
+ ['phase', 'epoch', 'previousEpoch', 'recoveryEpoch', 'step', 'attempt', 'notBefore', 'deadline', 'lastHealthyAt', 'reason']),
81
+ previous: release(value.transaction.previous), candidate: release(value.transaction.candidate) } : null }) : null),
82
+ };
83
+ } finally { db.close(); }
84
+ });
85
+ await capture('controllerLog', async () => {
86
+ const file = path.toNamespacedPath(path.join(identity.serviceDir, 'service.log'));
87
+ assert.ok((await fs.lstat(file)).isFile(), 'Controller log must be a regular owned file');
88
+ const handle = await fs.open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
89
+ try {
90
+ const stat = await handle.stat();
91
+ assert.ok(stat.isFile(), 'Controller log must be a regular owned file');
92
+ const bytes = Buffer.alloc(Math.min(stat.size, 64 * 1024));
93
+ const { bytesRead } = await handle.read(bytes, 0, bytes.length, Math.max(0, stat.size - bytes.length));
94
+ return { name: 'service.log', totalBytes: stat.size, tail: bytes.subarray(0, bytesRead).toString('utf8') };
95
+ } finally { await handle.close(); }
96
+ });
97
+ return evidence;
98
+ }
44
99
  function validateReceipt(receipt, roots, lane) {
45
100
  assert.ok(receipt?.target && receipt.authorization, 'An exact signed installed receipt is required');
46
101
  const target = receipt.target;
@@ -128,6 +183,14 @@ test('the real native Shell service replaces its authorized package and survives
128
183
  let network; let service;
129
184
  t.after(async () => {
130
185
  const errors = [];
186
+ if (!proof.passed) {
187
+ try {
188
+ const evidence = await runtimeFailureEvidence(service ? packageRoot : null, service?.identity);
189
+ await fs.writeFile(path.join(directory, `runtime-update-${process.platform}.diagnostics.json`),
190
+ JSON.stringify({ schema: 1, platform: process.platform, lane: examination.lane,
191
+ lastCheckpoint: proof.checkpoints.at(-1)?.phase ?? null, ...evidence }, null, 2) + '\n', { mode: 0o600 });
192
+ } catch (error) { proof.diagnosticError = String(error.message).slice(0, 1024); }
193
+ }
131
194
  try { await service?.close(); } catch (error) { errors.push(error); }
132
195
  try { await network?.close(); } catch (error) { errors.push(error); }
133
196
  if (errors.length) { proof.passed = false; proof.cleanupErrors = errors.map(error => error.message); }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@amalgm/shell",
3
- "version": "0.1.164-preview.34192807991",
3
+ "version": "0.1.164-preview.34197903040",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@amalgm/shell",
9
- "version": "0.1.164-preview.34192807991",
9
+ "version": "0.1.164-preview.34197903040",
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.34192807991",
3
+ "version": "0.1.164-preview.34197903040",
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": "f61742639720d43804c6c19aa14c2bdda763a78d",
154
+ "gitHead": "60b8e4033de92fa4168b099e3d9a7e0591939c8b",
155
155
  "amalgmSource": {
156
156
  "repository": "amalgm-inc/amalgm-shell",
157
157
  "branch": "preview",
158
- "commit": "f61742639720d43804c6c19aa14c2bdda763a78d",
159
- "occurrence": "34192807991"
158
+ "commit": "60b8e4033de92fa4168b099e3d9a7e0591939c8b",
159
+ "occurrence": "34197903040"
160
160
  }
161
161
  }