@dsh-enhanced/assistant-delivery 0.1.62 → 0.1.63

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.
Files changed (46) hide show
  1. package/README.md +27 -10
  2. package/bin/dsh-restart-supervisor.js +76 -0
  3. package/lib/agent-runtime.d.ts +19 -1
  4. package/lib/agent-runtime.d.ts.map +1 -1
  5. package/lib/agent-runtime.js +122 -15
  6. package/lib/agent-runtime.js.map +1 -1
  7. package/lib/operator-snapshot.d.ts +1 -1
  8. package/lib/operator-snapshot.d.ts.map +1 -1
  9. package/lib/operator-snapshot.js +4 -4
  10. package/lib/operator-snapshot.js.map +1 -1
  11. package/lib/restart-native.d.ts +47 -0
  12. package/lib/restart-native.d.ts.map +1 -0
  13. package/lib/restart-native.js +170 -0
  14. package/lib/restart-native.js.map +1 -0
  15. package/lib/restart-runtime.d.ts +38 -0
  16. package/lib/restart-runtime.d.ts.map +1 -0
  17. package/lib/restart-runtime.js +335 -0
  18. package/lib/restart-runtime.js.map +1 -0
  19. package/lib/restart-store.d.ts +132 -0
  20. package/lib/restart-store.d.ts.map +1 -0
  21. package/lib/restart-store.js +757 -0
  22. package/lib/restart-store.js.map +1 -0
  23. package/lib/restart-supervisor.d.ts +129 -0
  24. package/lib/restart-supervisor.d.ts.map +1 -0
  25. package/lib/restart-supervisor.js +615 -0
  26. package/lib/restart-supervisor.js.map +1 -0
  27. package/lib/service.d.ts +42 -1
  28. package/lib/service.d.ts.map +1 -1
  29. package/lib/service.js +455 -6
  30. package/lib/service.js.map +1 -1
  31. package/lib/sqlite.d.ts +1 -1
  32. package/lib/sqlite.d.ts.map +1 -1
  33. package/lib/sqlite.js +44 -2
  34. package/lib/sqlite.js.map +1 -1
  35. package/lib/store.d.ts +23 -0
  36. package/lib/store.d.ts.map +1 -1
  37. package/lib/store.js +222 -85
  38. package/lib/store.js.map +1 -1
  39. package/lib/tools.d.ts.map +1 -1
  40. package/lib/tools.js +21 -2
  41. package/lib/tools.js.map +1 -1
  42. package/lib/types.d.ts +2 -0
  43. package/lib/types.d.ts.map +1 -1
  44. package/lib/version.d.ts +1 -1
  45. package/lib/version.js +1 -1
  46. package/package.json +11 -10
@@ -0,0 +1,615 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { spawn, spawnSync } from 'node:child_process';
10
+ import { createHash, randomUUID } from 'node:crypto';
11
+ import { constants } from 'node:fs';
12
+ import { lstat, open, readlink, realpath } from 'node:fs/promises';
13
+ import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
14
+ import { pathToFileURL } from 'node:url';
15
+ import { DatabaseSync } from 'node:sqlite';
16
+ import { isExactDeliveryCommand, parseDeliveryCommand } from './session-commands.js';
17
+ const idPattern = /^[a-f0-9]{32}$/u;
18
+ const shaPattern = /^[a-f0-9]{64}$/u;
19
+ const profilePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
20
+ const unitPattern = /^dsh-profile-[A-Za-z0-9][A-Za-z0-9._-]{0,63}\.service$/u;
21
+ const commitPattern = /^[a-f0-9]{40}$/u;
22
+ const ownerRoutePattern = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/u;
23
+ function positiveInteger(value, label, limit) {
24
+ if (!Number.isSafeInteger(value) || value < 1 || value > limit)
25
+ throw new Error(`restart-supervisor: invalid ${label}`);
26
+ return value;
27
+ }
28
+ export function validateRestartSupervisorConfig(value) {
29
+ if (value === null || typeof value !== 'object'
30
+ || Object.keys(value).sort().join(',') !== [
31
+ 'automationDatabasePath', 'cohortReceiptDigest', 'databasePath', 'deliveryDatabasePath', 'dshHome', 'expectedUnitDigest',
32
+ 'hostBindingDigest', 'hostReceiptPath', 'hostReceiptSourceDigest',
33
+ 'maxRestarts', 'policyDatabasePath', 'policyEvaluatorPath', 'policyRules', 'profile', 'profilePatchDigest',
34
+ 'readyTimeoutMs', 'restartOwnerRouteId', 'sourceCommit', 'stableWindowMs', 'trustedFiles', 'unit', 'windowMs',
35
+ ].sort().join(','))
36
+ throw new Error('restart-supervisor: invalid config shape');
37
+ for (const name of ['dshHome', 'databasePath', 'deliveryDatabasePath', 'policyDatabasePath',
38
+ 'policyEvaluatorPath', 'automationDatabasePath', 'hostReceiptPath']) {
39
+ const path = value[name];
40
+ if (typeof path !== 'string' || !isAbsolute(path) || resolve(path) !== path) {
41
+ throw new Error(`restart-supervisor: ${name} must be a normalized absolute path`);
42
+ }
43
+ }
44
+ if (!profilePattern.test(value.profile) || value.unit !== `dsh-profile-${value.profile}.service`
45
+ || !unitPattern.test(value.unit) || !shaPattern.test(value.expectedUnitDigest)
46
+ || !shaPattern.test(value.profilePatchDigest) || !shaPattern.test(value.cohortReceiptDigest)
47
+ || !shaPattern.test(value.hostBindingDigest) || !shaPattern.test(value.hostReceiptSourceDigest)
48
+ || !commitPattern.test(value.sourceCommit) || !ownerRoutePattern.test(value.restartOwnerRouteId)
49
+ || value.hostReceiptPath.startsWith(value.dshHome + sep)) {
50
+ throw new Error('restart-supervisor: unit/profile or pinned digest is invalid');
51
+ }
52
+ if (!Array.isArray(value.trustedFiles) || value.trustedFiles.length < 3 || value.trustedFiles.length > 2048
53
+ || value.trustedFiles.some(file => file === null || typeof file !== 'object'
54
+ || Object.keys(file).sort().join(',') !== 'digest,path'
55
+ || typeof file.path !== 'string' || !isAbsolute(file.path) || resolve(file.path) !== file.path
56
+ || !file.path.startsWith(value.dshHome + sep) || !shaPattern.test(file.digest))
57
+ || !Array.isArray(value.policyRules) || value.policyRules.length > 512
58
+ || value.policyRules.some(rule => rule === null || typeof rule !== 'object' || Array.isArray(rule))) {
59
+ throw new Error('restart-supervisor: invalid pinned files or policy rules');
60
+ }
61
+ if (!value.trustedFiles.some(file => file.path === value.policyEvaluatorPath)) {
62
+ throw new Error('restart-supervisor: unpinned Policy evaluator');
63
+ }
64
+ positiveInteger(value.readyTimeoutMs, 'readyTimeoutMs', 600_000);
65
+ positiveInteger(value.stableWindowMs, 'stableWindowMs', 120_000);
66
+ if (value.stableWindowMs >= value.readyTimeoutMs)
67
+ throw new Error('restart-supervisor: stability window exceeds readiness deadline');
68
+ positiveInteger(value.maxRestarts, 'maxRestarts', 10);
69
+ positiveInteger(value.windowMs, 'windowMs', 86_400_000);
70
+ return Object.freeze({ ...value });
71
+ }
72
+ function requestIdentityValid(request, task) {
73
+ return typeof request.id === 'string' && request.id.length > 0
74
+ && Number.isSafeInteger(request.updatedAt) && request.updatedAt >= 0
75
+ && request.inboxId === task.inboxId
76
+ && idPattern.test(request.sourceInvocationId)
77
+ && request.sourceInvocationId === task.sourceInvocationId
78
+ && Number.isSafeInteger(task.deadline) && task.deadline > 0
79
+ && Number.isSafeInteger(task.binding.version) && task.binding.version > 0
80
+ && Number.isSafeInteger(task.binding.generation) && task.binding.generation > 0
81
+ && Number.isSafeInteger(task.owner.principalVersion) && task.owner.principalVersion > 0
82
+ && shaPattern.test(task.binding.authorityHash);
83
+ }
84
+ function canonical(value) {
85
+ if (Array.isArray(value))
86
+ return `[${value.map(canonical).join(',')}]`;
87
+ if (value !== null && typeof value === 'object')
88
+ return `{${Object.keys(value).sort()
89
+ .map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
90
+ return JSON.stringify(value);
91
+ }
92
+ /** Read-only Delivery witness. A possibly sent maintenance notice defers restart until reconciled. */
93
+ export function inspectRestartDeliveryAuthority(databasePath, task, requestId) {
94
+ const db = new DatabaseSync(databasePath, { readOnly: true });
95
+ try {
96
+ db.exec('PRAGMA query_only = ON');
97
+ const row = db.prepare(`
98
+ SELECT inbox.id AS inbox_id, inbox.event_id, inbox.status AS inbox_status,
99
+ inbox.failure_code AS inbox_failure, binding.id AS binding_id,
100
+ binding.version AS binding_version, binding.generation, binding.session_id,
101
+ binding.workspace, binding.agent_preset, binding.policy_ref,
102
+ binding.conversation_json, binding.principal_json, binding.status AS binding_status,
103
+ active.id AS active_binding_id, owner.id AS owner_id, owner.version AS owner_version,
104
+ owner.role, owner.status AS owner_status, lease.state AS lease_state,
105
+ execution.principal_record_id AS execution_owner_id,
106
+ execution.principal_version AS execution_owner_version,
107
+ execution.binding_id AS execution_binding_id,
108
+ execution.binding_version AS execution_binding_version,
109
+ execution.binding_generation AS execution_binding_generation,
110
+ execution.workspace AS execution_workspace, execution.preset AS execution_preset,
111
+ execution.status AS execution_status
112
+ FROM inbox_messages AS inbox
113
+ JOIN conversation_bindings AS binding ON binding.id = inbox.binding_id
114
+ JOIN delivery_principals AS owner ON owner.id = binding.principal_id
115
+ LEFT JOIN conversation_bindings AS active
116
+ ON active.conversation_hash = binding.conversation_hash AND active.status = 'active'
117
+ LEFT JOIN delivery_session_leases AS lease ON lease.session_id = binding.session_id
118
+ JOIN delivery_foreground_executions AS execution ON execution.inbox_id = inbox.id
119
+ WHERE inbox.id = ?
120
+ `).get(task.inboxId);
121
+ if (row === undefined || row.inbox_id !== task.inboxId || row.event_id !== task.eventId
122
+ || row.inbox_status !== 'processed' || row.inbox_failure !== null
123
+ || row.binding_id !== task.binding.id || row.binding_version !== task.binding.version
124
+ || row.generation !== task.binding.generation || row.session_id !== task.binding.sessionId
125
+ || row.workspace !== task.binding.workspace || row.agent_preset !== task.binding.preset
126
+ || row.policy_ref !== task.binding.policyRef
127
+ || canonical(JSON.parse(String(row.conversation_json))) !== canonical(task.binding.conversation)
128
+ || canonical(JSON.parse(String(row.principal_json))) !== canonical(task.binding.principal)
129
+ || row.binding_status !== 'active' || row.active_binding_id !== task.binding.id
130
+ || row.owner_id !== task.owner.principalRecordId || row.owner_version !== task.owner.principalVersion
131
+ || row.role !== 'owner' || row.owner_status !== 'active'
132
+ || row.lease_state !== 'released'
133
+ || row.execution_owner_id !== task.owner.principalRecordId
134
+ || row.execution_owner_version !== task.owner.principalVersion
135
+ || row.execution_binding_id !== task.binding.id
136
+ || row.execution_binding_version !== task.binding.version
137
+ || row.execution_binding_generation !== task.binding.generation
138
+ || row.execution_workspace !== task.binding.workspace || row.execution_preset !== task.binding.preset
139
+ || row.execution_status !== 'pending')
140
+ return 'invalid';
141
+ const controls = db.prepare(`SELECT message.envelope_json FROM inbox_messages AS message
142
+ JOIN delivery_inbox_admissions AS admission ON admission.inbox_id = message.id
143
+ WHERE admission.admission_sequence > (SELECT admission_sequence FROM delivery_inbox_admissions WHERE inbox_id = ?)
144
+ AND message.status != 'dead_letter' AND json_extract(message.envelope_json, '$.kind') = 'command'`)
145
+ .all(task.inboxId);
146
+ if (controls.some(control => {
147
+ const envelope = JSON.parse(control.envelope_json);
148
+ return canonical(envelope.conversation) === canonical(task.binding.conversation)
149
+ && canonical(envelope.principal) === canonical(task.binding.principal)
150
+ && isExactDeliveryCommand(parseDeliveryCommand(envelope), 'stop', 'new', 'clear');
151
+ }))
152
+ return 'invalid';
153
+ const outstanding = db.prepare("SELECT count(*) AS count FROM inbox_messages WHERE status = 'claimed'")
154
+ .get();
155
+ if (outstanding.count !== 0)
156
+ return 'invalid';
157
+ const inFlightLeases = db.prepare("SELECT count(*) AS count FROM delivery_session_leases WHERE state IN ('prepared','dispatched','unknown')")
158
+ .get();
159
+ if (inFlightLeases.count !== 0)
160
+ return 'invalid';
161
+ const fence = db.prepare('SELECT request_id,source_invocation_id FROM delivery_restart_fence WHERE singleton=1')
162
+ .get();
163
+ if (fence?.request_id !== requestId || fence.source_invocation_id !== task.sourceInvocationId)
164
+ return 'invalid';
165
+ const reply = db.prepare(`SELECT id FROM outbox_messages WHERE idempotency_key IN (?, ?, ?)
166
+ LIMIT 1`).get(`inbound:${task.inboxId}:reply`, `inbound:${task.eventId}:reply`, `inbound:${task.inboxId}:interrupted`);
167
+ if (reply !== undefined)
168
+ return 'invalid';
169
+ const sending = db.prepare("SELECT count(*) AS count FROM outbox_messages WHERE status = 'attempting'").get();
170
+ if (sending.count !== 0)
171
+ return 'notice-pending';
172
+ const notice = db.prepare(`SELECT status,provider_message_id FROM outbox_messages
173
+ WHERE idempotency_key = ? AND binding_id = ?`).get(`inbound:${task.inboxId}:maintenance`, task.binding.id);
174
+ return notice !== undefined && ['accepted', 'delivered', 'read'].includes(notice.status)
175
+ && typeof notice.provider_message_id === 'string' && notice.provider_message_id.length > 0
176
+ ? 'ready' : 'notice-pending';
177
+ }
178
+ finally {
179
+ db.close();
180
+ }
181
+ }
182
+ /** Scheduled/unknown work is durable; only live dispatch/attempts prevent process replacement. */
183
+ export function inspectRestartAutomationQuiescence(databasePath) {
184
+ const db = new DatabaseSync(databasePath, { readOnly: true });
185
+ try {
186
+ db.exec('PRAGMA query_only = ON');
187
+ const tasks = db.prepare("SELECT count(*) AS count FROM automation_tasks WHERE status IN ('claimed','running')")
188
+ .get();
189
+ const attempts = db.prepare("SELECT count(*) AS count FROM automation_attempts WHERE status IN ('claimed','running')")
190
+ .get();
191
+ return tasks.count === 0 && attempts.count === 0;
192
+ }
193
+ finally {
194
+ db.close();
195
+ }
196
+ }
197
+ /** A tick issues at most one command. A persisted restarting request is observation-only. */
198
+ export async function runRestartSupervisorTick(configInput, ledger, options = {}) {
199
+ const config = validateRestartSupervisorConfig(configInput);
200
+ const system = options.system ?? createSystemRestartSupervisor();
201
+ const now = options.now ?? Date.now;
202
+ const signal = options.signal ?? new AbortController().signal;
203
+ signal.throwIfAborted();
204
+ const request = ledger.listRequests({ statuses: ['ready', 'restarting'], limit: 1 })[0];
205
+ if (request === undefined)
206
+ return 'idle';
207
+ if (request.status !== 'ready' && request.status !== 'restarting') {
208
+ throw new Error('restart-supervisor: Store returned an ineligible request');
209
+ }
210
+ const task = ledger.getTask(request.inboxId);
211
+ if (task === undefined || !requestIdentityValid(request, task) || task.deadline <= now()) {
212
+ ledger.fail(request.id, 'restart-task-invalid-or-expired');
213
+ return 'failed';
214
+ }
215
+ if (await system.transactionExists(config.dshHome))
216
+ return 'deferred';
217
+ return await system.withLock(config.dshHome, async () => {
218
+ signal.throwIfAborted();
219
+ if (await system.transactionExists(config.dshHome))
220
+ return 'deferred';
221
+ const authority = inspectRestartDeliveryAuthority(config.deliveryDatabasePath, task, request.id);
222
+ if (authority === 'notice-pending')
223
+ return 'deferred';
224
+ if (authority !== 'ready') {
225
+ ledger.fail(request.id, 'restart-owner-or-drain-changed');
226
+ return 'failed';
227
+ }
228
+ if (!inspectRestartAutomationQuiescence(config.automationDatabasePath)
229
+ || !await system.authority(config, task)) {
230
+ ledger.fail(request.id, 'restart-policy-or-code-changed');
231
+ return 'failed';
232
+ }
233
+ const unit = await system.unit(config.unit);
234
+ if (await system.unitDigest(unit.fragmentPath) !== config.expectedUnitDigest) {
235
+ ledger.fail(request.id, 'restart-unit-drift');
236
+ return 'failed';
237
+ }
238
+ const observationStartedAt = request.status === 'restarting' ? request.updatedAt : now();
239
+ if (request.status === 'ready' && unit.invocationId === request.sourceInvocationId) {
240
+ if (!unit.active || unit.mainPid < 1)
241
+ return 'deferred';
242
+ if (ledger.countDispatchedSince(Math.max(0, now() - config.windowMs)) >= config.maxRestarts) {
243
+ ledger.fail(request.id, 'restart-budget-exhausted');
244
+ return 'failed';
245
+ }
246
+ // SQLite CAS commits before crossing the OS boundary. An uncertain
247
+ // subprocess result never grants another restart for this operation.
248
+ const claimed = ledger.claimRestart(request.id, {
249
+ holderId: options.holderId ?? randomUUID(), invocationId: unit.invocationId,
250
+ });
251
+ if (!claimed)
252
+ return 'deferred';
253
+ try {
254
+ await system.restart(config.unit, signal);
255
+ }
256
+ catch { /* Outcome may be unknown; observe only. */ }
257
+ }
258
+ const deadline = Math.min(task.deadline, observationStartedAt + config.readyTimeoutMs);
259
+ let stableSince;
260
+ let stablePid;
261
+ let stableInvocation;
262
+ while (now() < deadline) {
263
+ signal.throwIfAborted();
264
+ const current = await system.unit(config.unit);
265
+ if (current.active && current.mainPid > 0 && idPattern.test(current.invocationId)
266
+ && current.invocationId !== request.sourceInvocationId
267
+ && await system.ready(config.unit, current.invocationId)) {
268
+ if (stablePid !== current.mainPid || stableInvocation !== current.invocationId) {
269
+ stableSince = undefined;
270
+ }
271
+ stablePid = current.mainPid;
272
+ stableInvocation = current.invocationId;
273
+ stableSince ??= now();
274
+ if (now() - stableSince >= config.stableWindowMs) {
275
+ if (now() >= task.deadline) {
276
+ ledger.fail(request.id, 'restart-task-deadline-expired');
277
+ return 'failed';
278
+ }
279
+ if (inspectRestartDeliveryAuthority(config.deliveryDatabasePath, task, request.id) !== 'ready'
280
+ || !inspectRestartAutomationQuiescence(config.automationDatabasePath)
281
+ || !await system.authority(config, task)) {
282
+ ledger.fail(request.id, 'restart-owner-or-notice-changed');
283
+ return 'failed';
284
+ }
285
+ if (await system.unitDigest(current.fragmentPath) !== config.expectedUnitDigest) {
286
+ ledger.fail(request.id, 'restart-unit-drift');
287
+ return 'failed';
288
+ }
289
+ ledger.markRestarted(request.id, { invocationId: current.invocationId });
290
+ return 'restarted';
291
+ }
292
+ }
293
+ else {
294
+ stableSince = undefined;
295
+ stablePid = undefined;
296
+ stableInvocation = undefined;
297
+ }
298
+ await system.sleep(Math.min(1_000, deadline - now()), signal);
299
+ }
300
+ ledger.fail(request.id, 'restart-outcome-unknown');
301
+ return 'failed';
302
+ }) ?? 'deferred';
303
+ }
304
+ /** Lock before opening either journal. The installer may be replacing the entire Home. */
305
+ export async function runLockedRestartSupervisorTick(configInput, openLedger, options = {}) {
306
+ const config = validateRestartSupervisorConfig(configInput);
307
+ const system = options.system ?? createSystemRestartSupervisor();
308
+ return await system.withLock(config.dshHome, async () => {
309
+ if (await system.transactionExists(config.dshHome))
310
+ return 'deferred';
311
+ const ledger = openLedger();
312
+ try {
313
+ const held = {
314
+ ...system,
315
+ withLock: async (_home, task) => task(),
316
+ };
317
+ return await runRestartSupervisorTick(config, ledger, { ...options, system: held });
318
+ }
319
+ finally {
320
+ ledger.close();
321
+ }
322
+ }) ?? 'deferred';
323
+ }
324
+ async function command(executable, args, signal, timeoutMs = 15_000) {
325
+ signal.throwIfAborted();
326
+ const runtimeDirectory = process.env.XDG_RUNTIME_DIR ?? `/run/user/${process.getuid?.()}`;
327
+ const child = spawn(executable, args, { stdio: ['ignore', 'pipe', 'ignore'],
328
+ env: { PATH: '/usr/bin:/bin', LANG: 'C',
329
+ DBUS_SESSION_BUS_ADDRESS: process.env.DBUS_SESSION_BUS_ADDRESS ?? `unix:path=${runtimeDirectory}/bus`,
330
+ XDG_RUNTIME_DIR: runtimeDirectory } });
331
+ const output = [];
332
+ let size = 0;
333
+ const abort = () => child.kill('SIGKILL');
334
+ signal.addEventListener('abort', abort, { once: true });
335
+ const timer = setTimeout(abort, timeoutMs);
336
+ const closed = new Promise((resolve, reject) => {
337
+ child.once('error', reject);
338
+ child.once('close', value => resolve(value ?? 1));
339
+ });
340
+ try {
341
+ for await (const chunk of child.stdout) {
342
+ size += chunk.length;
343
+ if (size > 262_144) {
344
+ abort();
345
+ throw new Error('restart-supervisor: command output too large');
346
+ }
347
+ output.push(chunk);
348
+ }
349
+ const code = await closed;
350
+ if (code !== 0)
351
+ throw new Error('restart-supervisor: command failed');
352
+ return Buffer.concat(output).toString('utf8');
353
+ }
354
+ finally {
355
+ clearTimeout(timer);
356
+ signal.removeEventListener('abort', abort);
357
+ }
358
+ }
359
+ function parseSystemctlState(source, expectedUnit) {
360
+ const fields = new Map();
361
+ for (const line of source.trim().split('\n')) {
362
+ const separator = line.indexOf('=');
363
+ if (separator > 0)
364
+ fields.set(line.slice(0, separator), line.slice(separator + 1));
365
+ }
366
+ const invocationId = fields.get('InvocationID') ?? '';
367
+ const mainPid = Number(fields.get('MainPID'));
368
+ const fragmentPath = fields.get('FragmentPath') ?? '';
369
+ if (fields.get('Id') !== expectedUnit || fields.get('LoadState') !== 'loaded'
370
+ || invocationId !== '' && !idPattern.test(invocationId)
371
+ || !Number.isSafeInteger(mainPid) || mainPid < 0
372
+ || !isAbsolute(fragmentPath))
373
+ throw new Error('restart-supervisor: invalid service identity');
374
+ return { invocationId, mainPid, fragmentPath,
375
+ active: fields.get('ActiveState') === 'active' && fields.get('SubState') === 'running' };
376
+ }
377
+ function delay(ms, signal) {
378
+ return new Promise((resolve, reject) => {
379
+ const timer = setTimeout(() => { signal.removeEventListener('abort', abort); resolve(); }, ms);
380
+ const abort = () => { clearTimeout(timer); signal.removeEventListener('abort', abort); reject(signal.reason); };
381
+ signal.addEventListener('abort', abort, { once: true });
382
+ if (signal.aborted)
383
+ abort();
384
+ });
385
+ }
386
+ async function pinnedDigest(path, maxSize, allowEmpty = false) {
387
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
388
+ try {
389
+ const before = await handle.stat(), named = await lstat(path);
390
+ if (!before.isFile() || !named.isFile() || named.isSymbolicLink()
391
+ || before.uid !== process.getuid?.() || before.dev !== named.dev || before.ino !== named.ino
392
+ || before.mode & 0o022 || !allowEmpty && before.size < 1 || before.size > maxSize) {
393
+ throw new Error('restart-supervisor: unsafe pinned file');
394
+ }
395
+ const source = await handle.readFile();
396
+ const after = await handle.stat();
397
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) {
398
+ throw new Error('restart-supervisor: pinned file changed while reading');
399
+ }
400
+ return { digest: createHash('sha256').update(source).digest('hex'), source };
401
+ }
402
+ finally {
403
+ await handle.close();
404
+ }
405
+ }
406
+ async function hostCodeMatches(config) {
407
+ const binding = await pinnedDigest(join(config.dshHome, '.dsh-rsi-host.json'), 16_384);
408
+ if (binding.digest !== config.hostBindingDigest)
409
+ return false;
410
+ const bound = JSON.parse(binding.source.toString('utf8'));
411
+ if (bound.schemaVersion !== 1 || typeof bound.cacheRoot !== 'string'
412
+ || !isAbsolute(bound.cacheRoot) || resolve(bound.cacheRoot) !== bound.cacheRoot
413
+ || typeof bound.version !== 'string' || !/^0\.2\.[0-9]+-rc\.[0-9]+$/u.test(bound.version)
414
+ || typeof bound.receiptDigest !== 'string' || !shaPattern.test(bound.receiptDigest))
415
+ return false;
416
+ const hostRoot = join(bound.cacheRoot, bound.version);
417
+ if (hostRoot !== dirname(config.hostReceiptPath) || await realpath(hostRoot) !== hostRoot)
418
+ return false;
419
+ const source = await pinnedDigest(config.hostReceiptPath, 16_777_216);
420
+ if (source.digest !== config.hostReceiptSourceDigest)
421
+ return false;
422
+ const receipt = JSON.parse(source.source.toString('utf8'));
423
+ if (receipt.schemaVersion !== 1 || receipt.version !== bound.version
424
+ || !Array.isArray(receipt.entries) || receipt.entries.length < 2 || receipt.entries.length > 150_000
425
+ || createHash('sha256').update(JSON.stringify(receipt)).digest('hex') !== bound.receiptDigest)
426
+ return false;
427
+ const executable = join(hostRoot, 'node_modules', '.bin', 'dsh');
428
+ const link = receipt.entries.find(entry => entry.path === 'node_modules/.bin/dsh');
429
+ if (link?.type !== 'link' || typeof link.target !== 'string'
430
+ || await readlink(executable) !== link.target
431
+ || !(await realpath(executable)).startsWith(hostRoot + sep))
432
+ return false;
433
+ for (const entry of receipt.entries) {
434
+ if (entry.type !== 'link')
435
+ continue;
436
+ if (typeof entry.path !== 'string' || !/^[A-Za-z0-9_./+@=-]+$/u.test(entry.path)
437
+ || entry.path.includes('..') || typeof entry.target !== 'string'
438
+ || await readlink(join(hostRoot, entry.path)) !== entry.target)
439
+ return false;
440
+ }
441
+ const code = receipt.entries.filter(entry => entry.type === 'file'
442
+ && typeof entry.path === 'string'
443
+ && (/\.(?:js|mjs|cjs|node)$/u.test(entry.path) || entry.path.endsWith('/package.json')));
444
+ if (code.length < 2 || code.length > 30_000)
445
+ return false;
446
+ for (let index = 0; index < code.length; index += 64) {
447
+ const valid = await Promise.all(code.slice(index, index + 64).map(async (entry) => {
448
+ if (typeof entry.path !== 'string' || !/^[A-Za-z0-9_./+@=-]+$/u.test(entry.path)
449
+ || entry.path.includes('..') || typeof entry.sha256 !== 'string' || !shaPattern.test(entry.sha256))
450
+ return false;
451
+ const path = join(hostRoot, entry.path);
452
+ return (await realpath(path)).startsWith(hostRoot + sep)
453
+ && (await pinnedDigest(path, 16_777_216, true)).digest === entry.sha256;
454
+ }));
455
+ if (valid.some(value => !value))
456
+ return false;
457
+ }
458
+ return true;
459
+ }
460
+ /** Use the installed Policy's pure evaluator after verifying its frozen cohort bytes. */
461
+ export async function inspectRestartPolicyAndCode(config, task) {
462
+ try {
463
+ const receiptPath = join(config.dshHome, 'rsi-local-cohorts', config.profile, 'receipt.json');
464
+ const patchPath = join(config.dshHome, 'profiles', config.profile, 'cordis.patch.yml');
465
+ const [receipt, patch] = await Promise.all([
466
+ pinnedDigest(receiptPath, 2_000_000), pinnedDigest(patchPath, 2_000_000),
467
+ ]);
468
+ if (receipt.digest !== config.cohortReceiptDigest || patch.digest !== config.profilePatchDigest)
469
+ return false;
470
+ const cohort = JSON.parse(receipt.source.toString('utf8'));
471
+ if (cohort.sourceCommit !== config.sourceCommit
472
+ || cohort.root !== join(config.dshHome, 'rsi-local-cohorts', config.profile)
473
+ || !Array.isArray(cohort.packages))
474
+ return false;
475
+ for (const file of config.trustedFiles) {
476
+ const result = await pinnedDigest(file.path, 2_000_000);
477
+ if (result.digest !== file.digest)
478
+ return false;
479
+ }
480
+ if (!await hostCodeMatches(config))
481
+ return false;
482
+ const db = new DatabaseSync(config.policyDatabasePath, { readOnly: true });
483
+ let emergency;
484
+ try {
485
+ db.exec('PRAGMA query_only = ON');
486
+ emergency = db.prepare('SELECT enabled FROM emergency_state WHERE singleton=1').get();
487
+ }
488
+ finally {
489
+ db.close();
490
+ }
491
+ if (emergency !== undefined && emergency.enabled !== 0)
492
+ return false;
493
+ const evaluator = await import(__rewriteRelativeImportExtension(pathToFileURL(config.policyEvaluatorPath).href));
494
+ if (typeof evaluator.compilePolicy !== 'function' || typeof evaluator.evaluatePolicy !== 'function')
495
+ return false;
496
+ const principal = [task.binding.principal.channel, task.binding.principal.account,
497
+ task.binding.principal.tenant, task.binding.principal.user].map(encodeURIComponent).join('/');
498
+ const decision = evaluator.evaluatePolicy(evaluator.compilePolicy(config.policyRules), {
499
+ subject: { kind: 'agent', id: task.binding.preset, workspace: task.binding.workspace, principal },
500
+ action: 'maintenance.restart', resource: { kind: 'message', id: `maintenance:${config.restartOwnerRouteId}` },
501
+ context: { initiator: 'external' },
502
+ });
503
+ return decision.effect === 'allow';
504
+ }
505
+ catch {
506
+ return false;
507
+ }
508
+ }
509
+ export function createSystemRestartSupervisor() {
510
+ const systemctl = '/usr/bin/systemctl';
511
+ return {
512
+ async unit(unit) {
513
+ const raw = await command(systemctl, ['--user', 'show', unit, '-p', 'Id', '-p', 'LoadState',
514
+ '-p', 'ActiveState', '-p', 'SubState', '-p', 'MainPID', '-p', 'InvocationID', '-p', 'FragmentPath'], new AbortController().signal);
515
+ return parseSystemctlState(raw, unit);
516
+ },
517
+ restart(unit, signal) { return command(systemctl, ['--user', 'restart', '--job-mode=fail', unit], signal).then(() => { }); },
518
+ async ready(unit, invocationId) {
519
+ let raw;
520
+ try {
521
+ raw = await command('/usr/bin/journalctl', ['--user', '-u', unit,
522
+ `_SYSTEMD_INVOCATION_ID=${invocationId}`, '--no-pager', '--output=json',
523
+ '--grep=dsh-enhanced host ready: v1|lark-channel: connected', '-n', '256'], new AbortController().signal);
524
+ }
525
+ catch {
526
+ return false; /* journalctl exits 1 before this instance has either marker. */
527
+ }
528
+ let hostReady = false, channelReady = false;
529
+ for (const line of raw.trim().split('\n')) {
530
+ if (!line)
531
+ continue;
532
+ const entry = JSON.parse(line);
533
+ if (entry._SYSTEMD_INVOCATION_ID !== invocationId)
534
+ continue;
535
+ const message = entry.MESSAGE;
536
+ if (typeof message !== 'string')
537
+ continue;
538
+ if (message.includes('dsh-enhanced host ready: v1'))
539
+ hostReady = true;
540
+ if (message.includes('lark-channel: connected'))
541
+ channelReady = true;
542
+ }
543
+ return hostReady && channelReady;
544
+ },
545
+ async transactionExists(home) {
546
+ try {
547
+ await lstat(`${home}.dsh-enhanced-transaction`);
548
+ return true;
549
+ }
550
+ catch (error) {
551
+ if (error.code === 'ENOENT')
552
+ return false;
553
+ throw error;
554
+ }
555
+ },
556
+ async unitDigest(fragmentPath) {
557
+ const handle = await open(fragmentPath, constants.O_RDONLY | constants.O_NOFOLLOW);
558
+ try {
559
+ const before = await handle.stat();
560
+ const linked = await lstat(fragmentPath);
561
+ if (!before.isFile() || !linked.isFile() || linked.isSymbolicLink()
562
+ || before.uid !== process.getuid?.() || before.dev !== linked.dev || before.ino !== linked.ino
563
+ || before.mode & 0o022)
564
+ throw new Error('restart-supervisor: unsafe unit fragment');
565
+ const source = await handle.readFile();
566
+ const after = await handle.stat();
567
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || source.byteLength > 128_000) {
568
+ throw new Error('restart-supervisor: unit fragment changed');
569
+ }
570
+ return createHash('sha256').update(source).digest('hex');
571
+ }
572
+ finally {
573
+ await handle.close();
574
+ }
575
+ },
576
+ authority: inspectRestartPolicyAndCode,
577
+ sleep: delay,
578
+ async withLock(home, task) {
579
+ const physical = await realpath(home);
580
+ if (physical !== home)
581
+ throw new Error('restart-supervisor: noncanonical Home');
582
+ const temporary = await lstat('/tmp');
583
+ if (!temporary.isDirectory() || temporary.isSymbolicLink() || temporary.uid !== 0
584
+ || (temporary.mode & 0o7777) !== 0o1777)
585
+ throw new Error('restart-supervisor: unsafe /tmp');
586
+ const digest = createHash('sha256').update(physical).digest('hex');
587
+ const uid = process.getuid?.();
588
+ if (!Number.isSafeInteger(uid))
589
+ throw new Error('restart-supervisor: missing UID');
590
+ const lockPath = `/tmp/.dsh-enhanced-lifecycle-${uid}-${digest}.lock`;
591
+ const fd = await open(lockPath, constants.O_RDWR | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
592
+ try {
593
+ const [opened, linked] = await Promise.all([fd.stat(), lstat(lockPath)]);
594
+ if (!opened.isFile() || !linked.isFile() || linked.isSymbolicLink()
595
+ || opened.uid !== uid || opened.nlink !== 1 || (opened.mode & 0o777) !== 0o600
596
+ || linked.dev !== opened.dev || linked.ino !== opened.ino) {
597
+ throw new Error('restart-supervisor: unsafe lifecycle lock');
598
+ }
599
+ const result = spawnSync('/usr/bin/flock', ['-n', '-E', '75', '3'], {
600
+ stdio: ['ignore', 'ignore', 'pipe', fd.fd], encoding: 'utf8', timeout: 10_000,
601
+ env: { PATH: '/usr/bin:/bin', LANG: 'C' },
602
+ });
603
+ if (result.status === 75)
604
+ return undefined;
605
+ if (result.status !== 0)
606
+ throw new Error('restart-supervisor: lifecycle lock failed');
607
+ return await task();
608
+ }
609
+ finally {
610
+ await fd.close();
611
+ }
612
+ },
613
+ };
614
+ }
615
+ //# sourceMappingURL=restart-supervisor.js.map