instar 1.3.896 → 1.3.898

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 (90) hide show
  1. package/dashboard/index.html +30 -0
  2. package/dist/commands/server.d.ts.map +1 -1
  3. package/dist/commands/server.js +111 -1
  4. package/dist/commands/server.js.map +1 -1
  5. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  6. package/dist/config/ConfigDefaults.js +12 -0
  7. package/dist/config/ConfigDefaults.js.map +1 -1
  8. package/dist/core/CoherenceJournal.d.ts +17 -1
  9. package/dist/core/CoherenceJournal.d.ts.map +1 -1
  10. package/dist/core/CoherenceJournal.js +29 -2
  11. package/dist/core/CoherenceJournal.js.map +1 -1
  12. package/dist/core/JournalSyncApplier.d.ts.map +1 -1
  13. package/dist/core/JournalSyncApplier.js +11 -0
  14. package/dist/core/JournalSyncApplier.js.map +1 -1
  15. package/dist/core/MachineSshEndpoint.d.ts +47 -0
  16. package/dist/core/MachineSshEndpoint.d.ts.map +1 -0
  17. package/dist/core/MachineSshEndpoint.js +221 -0
  18. package/dist/core/MachineSshEndpoint.js.map +1 -0
  19. package/dist/core/MachineSshIdentity.d.ts +26 -0
  20. package/dist/core/MachineSshIdentity.d.ts.map +1 -0
  21. package/dist/core/MachineSshIdentity.js +97 -0
  22. package/dist/core/MachineSshIdentity.js.map +1 -0
  23. package/dist/core/MeshRpc.d.ts +6 -0
  24. package/dist/core/MeshRpc.d.ts.map +1 -1
  25. package/dist/core/MeshRpc.js +2 -0
  26. package/dist/core/MeshRpc.js.map +1 -1
  27. package/dist/core/MutualSshHealthController.d.ts +33 -0
  28. package/dist/core/MutualSshHealthController.d.ts.map +1 -0
  29. package/dist/core/MutualSshHealthController.js +113 -0
  30. package/dist/core/MutualSshHealthController.js.map +1 -0
  31. package/dist/core/MutualSshProbeScheduler.d.ts +21 -0
  32. package/dist/core/MutualSshProbeScheduler.d.ts.map +1 -0
  33. package/dist/core/MutualSshProbeScheduler.js +57 -0
  34. package/dist/core/MutualSshProbeScheduler.js.map +1 -0
  35. package/dist/core/MutualSshRuntime.d.ts +106 -0
  36. package/dist/core/MutualSshRuntime.d.ts.map +1 -0
  37. package/dist/core/MutualSshRuntime.js +423 -0
  38. package/dist/core/MutualSshRuntime.js.map +1 -0
  39. package/dist/core/MutualSshVerifier.d.ts +44 -0
  40. package/dist/core/MutualSshVerifier.d.ts.map +1 -0
  41. package/dist/core/MutualSshVerifier.js +97 -0
  42. package/dist/core/MutualSshVerifier.js.map +1 -0
  43. package/dist/core/PostUpdateMigrator.d.ts +2 -0
  44. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  45. package/dist/core/PostUpdateMigrator.js +112 -0
  46. package/dist/core/PostUpdateMigrator.js.map +1 -1
  47. package/dist/core/SshBootstrapAdvert.d.ts +23 -0
  48. package/dist/core/SshBootstrapAdvert.d.ts.map +1 -0
  49. package/dist/core/SshBootstrapAdvert.js +43 -0
  50. package/dist/core/SshBootstrapAdvert.js.map +1 -0
  51. package/dist/core/SshHostKeyLifecycle.d.ts +43 -0
  52. package/dist/core/SshHostKeyLifecycle.d.ts.map +1 -0
  53. package/dist/core/SshHostKeyLifecycle.js +68 -0
  54. package/dist/core/SshHostKeyLifecycle.js.map +1 -0
  55. package/dist/core/SshPeerAdmissionStore.d.ts +29 -0
  56. package/dist/core/SshPeerAdmissionStore.d.ts.map +1 -0
  57. package/dist/core/SshPeerAdmissionStore.js +86 -0
  58. package/dist/core/SshPeerAdmissionStore.js.map +1 -0
  59. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  60. package/dist/core/devGatedFeatures.js +6 -0
  61. package/dist/core/devGatedFeatures.js.map +1 -1
  62. package/dist/core/machineCoherenceManifest.d.ts.map +1 -1
  63. package/dist/core/machineCoherenceManifest.js +8 -0
  64. package/dist/core/machineCoherenceManifest.js.map +1 -1
  65. package/dist/core/types.d.ts +10 -0
  66. package/dist/core/types.d.ts.map +1 -1
  67. package/dist/core/types.js.map +1 -1
  68. package/dist/server/AgentServer.d.ts +1 -0
  69. package/dist/server/AgentServer.d.ts.map +1 -1
  70. package/dist/server/AgentServer.js +22 -0
  71. package/dist/server/AgentServer.js.map +1 -1
  72. package/dist/server/CapabilityIndex.js +2 -2
  73. package/dist/server/CapabilityIndex.js.map +1 -1
  74. package/dist/server/routes.d.ts +2 -0
  75. package/dist/server/routes.d.ts.map +1 -1
  76. package/dist/server/routes.js +9 -0
  77. package/dist/server/routes.js.map +1 -1
  78. package/dist/testing/selfActionRegistry.d.ts.map +1 -1
  79. package/dist/testing/selfActionRegistry.js +39 -0
  80. package/dist/testing/selfActionRegistry.js.map +1 -1
  81. package/package.json +4 -1
  82. package/scripts/feature-maturation-plan-gate.mjs +1 -0
  83. package/skills/spec-converge/scripts/write-convergence-tag.mjs +13 -0
  84. package/src/core/FeatureMaturationPlanGate.mjs +58 -0
  85. package/src/data/builtin-manifest.json +64 -64
  86. package/src/data/state-coherence-registry.json +36 -0
  87. package/upgrades/1.3.897.md +39 -0
  88. package/upgrades/1.3.898.md +24 -0
  89. package/upgrades/side-effects/feature-maturation-plan-visibility.md +84 -0
  90. package/upgrades/side-effects/mutual-ssh-autobootstrap.md +137 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MutualSshHealthController.js","sourceRoot":"","sources":["../../src/core/MutualSshHealthController.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ export interface ScheduledSshDirection {
2
+ sourceMachineId: string;
3
+ targetMachineId: string;
4
+ healthy: boolean;
5
+ }
6
+ export interface ProbeSweepResult {
7
+ attempted: string[];
8
+ peakConcurrency: number;
9
+ elapsedMs: number;
10
+ }
11
+ /** Fair global work bucket with a 25% reserve for healthy refreshes. */
12
+ export declare class MutualSshProbeScheduler {
13
+ readonly concurrency: number;
14
+ readonly freshnessMs: number;
15
+ readonly probeDeadlineMs: number;
16
+ constructor(concurrency?: number, freshnessMs?: number, probeDeadlineMs?: number);
17
+ validate(machineCount: number): void;
18
+ worstCaseSweepMs(machineCount: number): number;
19
+ sweep(directions: ScheduledSshDirection[], probe: (direction: ScheduledSshDirection) => Promise<void>): Promise<ProbeSweepResult>;
20
+ }
21
+ //# sourceMappingURL=MutualSshProbeScheduler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MutualSshProbeScheduler.d.ts","sourceRoot":"","sources":["../../src/core/MutualSshProbeScheduler.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACpC,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wEAAwE;AACxE,qBAAa,uBAAuB;IACtB,QAAQ,CAAC,WAAW;IAAM,QAAQ,CAAC,WAAW;IAAY,QAAQ,CAAC,eAAe;gBAAzE,WAAW,SAAI,EAAW,WAAW,SAAU,EAAW,eAAe,SAAQ;IAItG,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI;IAKpC,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAIxC,KAAK,CAAC,UAAU,EAAE,qBAAqB,EAAE,EAAE,KAAK,EAAE,CAAC,SAAS,EAAE,qBAAqB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,gBAAgB,CAAC;CA2BxI"}
@@ -0,0 +1,57 @@
1
+ /** Fair global work bucket with a 25% reserve for healthy refreshes. */
2
+ export class MutualSshProbeScheduler {
3
+ concurrency;
4
+ freshnessMs;
5
+ probeDeadlineMs;
6
+ constructor(concurrency = 4, freshnessMs = 300_000, probeDeadlineMs = 8_000) {
7
+ this.concurrency = concurrency;
8
+ this.freshnessMs = freshnessMs;
9
+ this.probeDeadlineMs = probeDeadlineMs;
10
+ if (concurrency < 1 || concurrency > 32)
11
+ throw new Error('mutual-ssh-concurrency-out-of-range');
12
+ }
13
+ validate(machineCount) {
14
+ const sweepMs = this.worstCaseSweepMs(machineCount);
15
+ if (sweepMs >= this.freshnessMs)
16
+ throw new Error(`mutual-ssh-capacity-invalid machines=${machineCount} sweepMs=${sweepMs} freshnessMs=${this.freshnessMs}`);
17
+ }
18
+ worstCaseSweepMs(machineCount) {
19
+ return Math.ceil(machineCount * (machineCount - 1) / this.concurrency) * this.probeDeadlineMs;
20
+ }
21
+ async sweep(directions, probe) {
22
+ const started = Date.now();
23
+ const healthy = directions.filter(x => x.healthy);
24
+ const unhealthy = directions.filter(x => !x.healthy);
25
+ const ordered = [];
26
+ const reserveEvery = Math.max(1, Math.floor(this.concurrency / Math.max(1, Math.ceil(this.concurrency * 0.25))));
27
+ while (healthy.length || unhealthy.length) {
28
+ for (let i = 0; i < reserveEvery - 1 && unhealthy.length; i += 1)
29
+ ordered.push(unhealthy.shift());
30
+ if (healthy.length)
31
+ ordered.push(healthy.shift());
32
+ else if (unhealthy.length)
33
+ ordered.push(unhealthy.shift());
34
+ }
35
+ let cursor = 0;
36
+ let active = 0;
37
+ let peak = 0;
38
+ const attempted = [];
39
+ const workers = Array.from({ length: Math.min(this.concurrency, ordered.length) }, async () => {
40
+ while (cursor < ordered.length) {
41
+ const item = ordered[cursor++];
42
+ active += 1;
43
+ peak = Math.max(peak, active);
44
+ attempted.push(`${item.sourceMachineId}->${item.targetMachineId}`);
45
+ try {
46
+ await probe(item);
47
+ }
48
+ finally {
49
+ active -= 1;
50
+ }
51
+ }
52
+ });
53
+ await Promise.all(workers);
54
+ return { attempted, peakConcurrency: peak, elapsedMs: Date.now() - started };
55
+ }
56
+ }
57
+ //# sourceMappingURL=MutualSshProbeScheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MutualSshProbeScheduler.js","sourceRoot":"","sources":["../../src/core/MutualSshProbeScheduler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,106 @@
1
+ import { type DirectionalSshProof } from './MutualSshVerifier.js';
2
+ import { type MutualSshPairHealth } from './MutualSshHealthController.js';
3
+ import { type SshBootstrapAdvert } from './SshBootstrapAdvert.js';
4
+ export interface MutualSshPeer {
5
+ machineId: string;
6
+ pairingEpoch: number;
7
+ machineFingerprint: string;
8
+ endpoints: string[];
9
+ }
10
+ export interface MutualSshRuntimeDeps {
11
+ stateDir: string;
12
+ agentId: string;
13
+ selfMachineId: string;
14
+ selfMachineFingerprint: string;
15
+ observerBootId: string;
16
+ bindHost: string;
17
+ bindPort: number;
18
+ dryRun: boolean;
19
+ requiredForReadiness?: boolean;
20
+ freshnessMs?: number;
21
+ cadenceMs?: number;
22
+ concurrency?: number;
23
+ listPeers(): MutualSshPeer[];
24
+ send(machineId: string, command: MutualSshWireCommand): Promise<unknown>;
25
+ sign(payload: string): string;
26
+ verify(machineId: string, payload: string, signature: string): boolean;
27
+ emitJournalProof?(proof: DirectionalSshProof): void;
28
+ audit?(event: Record<string, unknown>): void;
29
+ notifySecurity?(event: Record<string, unknown>): void;
30
+ }
31
+ export type MutualSshWireCommand = {
32
+ type: 'ssh-bootstrap-advert';
33
+ advert: SshBootstrapAdvert;
34
+ } | {
35
+ type: 'ssh-proof-publish';
36
+ proof: DirectionalSshProof;
37
+ };
38
+ /** Production coordinator for endpoint lifecycle, signed adverts, directional probes and rollback. */
39
+ export declare class MutualSshRuntime {
40
+ private readonly d;
41
+ private readonly identityManager;
42
+ private readonly admissionStore;
43
+ private readonly verifier;
44
+ private readonly controller;
45
+ private readonly scheduler;
46
+ private identity;
47
+ private endpoint;
48
+ private listen;
49
+ private readonly adverts;
50
+ private readonly candidateAdverts;
51
+ private readonly hostKeys;
52
+ private readonly proofs;
53
+ private readonly bootstrapFailures;
54
+ private readonly knownPeers;
55
+ private timer;
56
+ private hostOverlapTimer;
57
+ private servePreviousHost;
58
+ private running;
59
+ private stopped;
60
+ private readonly startedAtWall;
61
+ private readonly proofFile;
62
+ constructor(d: MutualSshRuntimeDeps);
63
+ start(): Promise<void>;
64
+ tick(): Promise<void>;
65
+ handleAdvert(raw: unknown, authenticatedSender: string): {
66
+ advert: SshBootstrapAdvert;
67
+ };
68
+ handleProof(raw: unknown, authenticatedSender: string): {
69
+ accepted: true;
70
+ };
71
+ /** Delayed journal delivery is observational only and fails closed when stale. */
72
+ handleJournalProof(raw: unknown, authenticatedSender: string): boolean;
73
+ status(): {
74
+ enabled: true;
75
+ dryRun: boolean;
76
+ listener: boolean;
77
+ readinessRequired: boolean;
78
+ ready: boolean;
79
+ enrollmentState: 'paired' | 'ssh-bootstrap' | 'ssh-bootstrap-blocked' | 'ssh-proving' | 'ready';
80
+ blockedReasons: string[];
81
+ directions: MutualSshPairHealth[];
82
+ pairs: Array<{
83
+ machines: string[];
84
+ mutual: boolean;
85
+ }>;
86
+ };
87
+ revoke(machineId: string): void;
88
+ rollback(): Promise<void>;
89
+ private startEndpoint;
90
+ private ensureEndpoint;
91
+ private localAdvert;
92
+ private exchangeAdvert;
93
+ private installAdvert;
94
+ private targetFor;
95
+ private acceptProof;
96
+ private promoteProvenHostKey;
97
+ private clearPeerEvidence;
98
+ private maybeRetirePreviousHost;
99
+ private retirePreviousHost;
100
+ private proofFresh;
101
+ private key;
102
+ private audit;
103
+ private invalidatePersistedProofs;
104
+ private persistProofs;
105
+ }
106
+ //# sourceMappingURL=MutualSshRuntime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MutualSshRuntime.d.ts","sourceRoot":"","sources":["../../src/core/MutualSshRuntime.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,423 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { MachineSshIdentity } from './MachineSshIdentity.js';
5
+ import { MachineSshEndpoint } from './MachineSshEndpoint.js';
6
+ import { MutualSshVerifier, canonicalDirectionalSshProof } from './MutualSshVerifier.js';
7
+ import { MutualSshHealthController, classifyMutualSshFailure } from './MutualSshHealthController.js';
8
+ import { MutualSshProbeScheduler } from './MutualSshProbeScheduler.js';
9
+ import { SshPeerAdmissionStore } from './SshPeerAdmissionStore.js';
10
+ import { SshHostKeyLifecycle, canonicalHostKeyProposal } from './SshHostKeyLifecycle.js';
11
+ import { canonicalSshBootstrapAdvert, validateSshBootstrapAdvert } from './SshBootstrapAdvert.js';
12
+ /** Production coordinator for endpoint lifecycle, signed adverts, directional probes and rollback. */
13
+ export class MutualSshRuntime {
14
+ d;
15
+ identityManager;
16
+ admissionStore;
17
+ verifier = new MutualSshVerifier();
18
+ controller;
19
+ scheduler;
20
+ identity;
21
+ endpoint = null;
22
+ listen = null;
23
+ adverts = new Map();
24
+ candidateAdverts = new Map();
25
+ hostKeys = new Map();
26
+ proofs = new Map();
27
+ bootstrapFailures = new Map();
28
+ knownPeers = new Set();
29
+ timer = null;
30
+ hostOverlapTimer = null;
31
+ servePreviousHost = true;
32
+ running = false;
33
+ stopped = false;
34
+ startedAtWall = Date.now();
35
+ proofFile;
36
+ constructor(d) {
37
+ this.d = d;
38
+ const freshness = d.freshnessMs ?? 300_000;
39
+ const concurrency = d.concurrency ?? 4;
40
+ MutualSshHealthController.validateCapacity(d.listPeers().length + 1, concurrency, 8_000, freshness);
41
+ this.identityManager = new MachineSshIdentity(d.stateDir, d.agentId, d.selfMachineId);
42
+ this.admissionStore = new SshPeerAdmissionStore(d.stateDir);
43
+ this.scheduler = new MutualSshProbeScheduler(concurrency, freshness, 8_000);
44
+ this.controller = new MutualSshHealthController(this.verifier, {
45
+ refreshAdvert: async (target) => { await this.exchangeAdvert(target.targetMachineId); },
46
+ reconcileAdmission: async (target) => { const advert = this.adverts.get(target.targetMachineId); if (advert)
47
+ this.installAdvert(advert); },
48
+ rotateSourceKey: async (target) => {
49
+ this.identity = this.identityManager.ensure({ forceClientRotation: true });
50
+ target.sourceClientKeyGeneration = this.identity.clientGeneration;
51
+ target.clientPrivateKeyPath = this.identity.clientPrivateKeyPath;
52
+ target.expectedSourceClientKeyFingerprint = MachineSshIdentity.fingerprint(this.identity.clientPublicKey);
53
+ await this.exchangeAdvert(target.targetMachineId);
54
+ },
55
+ notifySecurity: async (target) => this.d.notifySecurity?.({ type: 'host-key-change', target: target.targetMachineId }),
56
+ notifyExhausted: async (target, failure) => this.audit({ type: 'repair-exhausted', target: target.targetMachineId, failure }),
57
+ }, concurrency);
58
+ this.proofFile = path.join(d.stateDir, 'machine-ssh', 'directional-proofs.json');
59
+ this.invalidatePersistedProofs();
60
+ }
61
+ async start() {
62
+ this.identity = this.identityManager.ensure();
63
+ if (!this.d.dryRun)
64
+ await this.ensureEndpoint();
65
+ await this.tick();
66
+ this.timer = setInterval(() => { void this.tick(); }, this.d.cadenceMs ?? 60_000);
67
+ this.timer.unref?.();
68
+ }
69
+ async tick() {
70
+ if (this.running || this.stopped)
71
+ return;
72
+ this.running = true;
73
+ try {
74
+ if (!this.d.dryRun && !this.endpoint)
75
+ await this.ensureEndpoint();
76
+ const checkedIdentity = this.identityManager.ensure();
77
+ if (checkedIdentity.clientGeneration !== this.identity.clientGeneration || checkedIdentity.hostGeneration !== this.identity.hostGeneration) {
78
+ const hostChanged = checkedIdentity.hostGeneration !== this.identity.hostGeneration;
79
+ this.audit({ type: 'local-key-rotation', clientGeneration: checkedIdentity.clientGeneration, hostGeneration: checkedIdentity.hostGeneration });
80
+ this.identity = checkedIdentity;
81
+ if (hostChanged && !this.d.dryRun) {
82
+ this.servePreviousHost = true;
83
+ await this.endpoint?.close();
84
+ this.endpoint = null;
85
+ this.listen = null;
86
+ await this.ensureEndpoint();
87
+ }
88
+ }
89
+ const peers = this.d.listPeers().filter(peer => peer.machineId !== this.d.selfMachineId);
90
+ const livePeerIds = new Set(peers.map(peer => peer.machineId));
91
+ for (const machineId of this.knownPeers)
92
+ if (!livePeerIds.has(machineId))
93
+ this.revoke(machineId);
94
+ for (const machineId of livePeerIds)
95
+ this.knownPeers.add(machineId);
96
+ this.scheduler.validate(peers.length + 1);
97
+ for (const peer of peers) {
98
+ try {
99
+ await this.exchangeAdvert(peer.machineId);
100
+ this.bootstrapFailures.delete(peer.machineId);
101
+ }
102
+ catch (error) {
103
+ // @silent-fallback-ok — an advert exchange failure is retained as a
104
+ // named readiness blocker and retried on the next bounded sweep.
105
+ this.bootstrapFailures.set(peer.machineId, classifyMutualSshFailure(error));
106
+ }
107
+ }
108
+ if (this.d.dryRun) {
109
+ this.audit({ type: 'dry-run', wouldProbe: peers.length });
110
+ return;
111
+ }
112
+ const targets = peers.flatMap(peer => { const target = this.targetFor(peer); return target ? [target] : []; });
113
+ await this.scheduler.sweep(targets.map(target => ({ sourceMachineId: target.sourceMachineId, targetMachineId: target.targetMachineId, healthy: this.proofFresh(this.proofs.get(this.key(target.sourceMachineId, target.targetMachineId))) })), async (direction) => {
114
+ const target = targets.find(row => row.sourceMachineId === direction.sourceMachineId && row.targetMachineId === direction.targetMachineId);
115
+ if (!target)
116
+ return;
117
+ const proof = await this.controller.check(target);
118
+ if (!proof)
119
+ return;
120
+ this.promoteProvenHostKey(target.targetMachineId, proof.targetHostKeyGeneration);
121
+ this.acceptProof(proof, this.d.selfMachineId);
122
+ this.d.emitJournalProof?.(proof);
123
+ await this.d.send(target.targetMachineId, { type: 'ssh-proof-publish', proof });
124
+ });
125
+ }
126
+ finally {
127
+ this.running = false;
128
+ }
129
+ }
130
+ handleAdvert(raw, authenticatedSender) {
131
+ const advert = validateSshBootstrapAdvert(raw, authenticatedSender, this.d.agentId);
132
+ const { machineSignature, ...unsigned } = advert;
133
+ if (!this.d.verify(authenticatedSender, canonicalSshBootstrapAdvert(unsigned), machineSignature))
134
+ throw new Error('ssh-advert-machine-signature-invalid');
135
+ const peer = this.d.listPeers().find(row => row.machineId === authenticatedSender);
136
+ if (!peer || peer.pairingEpoch !== advert.pairingEpoch)
137
+ throw new Error('ssh-advert-epoch-mismatch');
138
+ this.installAdvert(advert);
139
+ return { advert: this.localAdvert(peer.pairingEpoch) };
140
+ }
141
+ handleProof(raw, authenticatedSender) {
142
+ if (!raw || typeof raw !== 'object')
143
+ throw new Error('ssh-proof-invalid');
144
+ const proof = raw;
145
+ if (proof.sourceMachineId !== authenticatedSender || proof.targetMachineId !== this.d.selfMachineId || typeof proof.machineSignature !== 'string')
146
+ throw new Error('ssh-proof-principal-mismatch');
147
+ for (const value of [proof.observerBootId, proof.endpointId, proof.targetHostKeyFingerprint, proof.challengeDigest, proof.machineSignature])
148
+ if (typeof value !== 'string' || value.length < 1 || value.length > 512)
149
+ throw new Error('ssh-proof-invalid');
150
+ if (!Number.isSafeInteger(proof.pairingEpoch) || !Number.isSafeInteger(proof.sourceClientKeyGeneration) || !Number.isSafeInteger(proof.targetHostKeyGeneration))
151
+ throw new Error('ssh-proof-invalid');
152
+ if (!Number.isFinite(Date.parse(proof.verifiedAt)) || !Number.isFinite(Date.parse(proof.expiresAt)))
153
+ throw new Error('ssh-proof-invalid');
154
+ const peer = this.d.listPeers().find(row => row.machineId === authenticatedSender);
155
+ if (!peer || peer.pairingEpoch !== proof.pairingEpoch)
156
+ throw new Error('ssh-proof-epoch-mismatch');
157
+ const advert = this.adverts.get(authenticatedSender);
158
+ if (!advert || advert.observerBootId !== proof.observerBootId || advert.clientKeyGeneration !== proof.sourceClientKeyGeneration)
159
+ throw new Error('ssh-proof-source-generation-mismatch');
160
+ if (proof.targetHostKeyGeneration !== this.identity.hostGeneration)
161
+ throw new Error('ssh-proof-target-generation-mismatch');
162
+ if (Date.parse(proof.verifiedAt) < this.startedAtWall)
163
+ throw new Error('ssh-proof-precedes-runtime-boot');
164
+ if (!this.d.verify(authenticatedSender, canonicalDirectionalSshProof(proof), proof.machineSignature))
165
+ throw new Error('ssh-proof-signature-invalid');
166
+ if (!this.proofFresh(proof))
167
+ throw new Error('ssh-proof-expired');
168
+ const wallRemaining = Date.parse(proof.expiresAt) - Date.now();
169
+ this.acceptProof({ ...proof, monotonicDeadlineMs: performance.now() + Math.max(0, Math.min(wallRemaining, this.d.freshnessMs ?? 300_000)) }, authenticatedSender);
170
+ this.maybeRetirePreviousHost();
171
+ return { accepted: true };
172
+ }
173
+ /** Delayed journal delivery is observational only and fails closed when stale. */
174
+ handleJournalProof(raw, authenticatedSender) {
175
+ try {
176
+ this.handleProof(raw, authenticatedSender);
177
+ return true;
178
+ }
179
+ catch (error) {
180
+ // @silent-fallback-ok — journal replication is an observational backup;
181
+ // invalid/stale evidence is audited and must never enter readiness state.
182
+ this.audit({ type: 'journal-proof-rejected', sourceMachineId: authenticatedSender, reason: error instanceof Error ? error.message : 'invalid' });
183
+ return false;
184
+ }
185
+ }
186
+ status() {
187
+ const targets = this.d.listPeers().flatMap(peer => { const target = this.targetFor(peer); return target ? [target] : []; });
188
+ const directions = this.controller.health(targets).map(row => ({ ...row, proof: this.proofs.get(this.key(row.sourceMachineId, row.targetMachineId)) ?? row.proof }));
189
+ const pairs = this.d.listPeers().map(peer => {
190
+ const local = this.proofs.get(this.key(this.d.selfMachineId, peer.machineId));
191
+ const remote = this.proofs.get(this.key(peer.machineId, this.d.selfMachineId));
192
+ const advert = this.adverts.get(peer.machineId);
193
+ return {
194
+ machines: [this.d.selfMachineId, peer.machineId],
195
+ mutual: Boolean(advert && MutualSshVerifier.mutual(local, remote, Date.now(), {
196
+ monotonicNow: performance.now(),
197
+ liveBootIds: new Set([this.d.observerBootId, advert.observerBootId]),
198
+ sourceClientGenerations: new Map([[this.d.selfMachineId, this.identity.clientGeneration], [peer.machineId, advert.clientKeyGeneration]]),
199
+ targetHostGenerations: new Map([[this.d.selfMachineId, this.identity.hostGeneration], [peer.machineId, advert.hostKeyGeneration]]),
200
+ })),
201
+ };
202
+ });
203
+ const peerCount = this.d.listPeers().filter(peer => peer.machineId !== this.d.selfMachineId).length;
204
+ const proofReady = peerCount === 0 || (pairs.length === peerCount && pairs.every(pair => pair.mutual));
205
+ const readinessRequired = this.d.requiredForReadiness === true && !this.stopped;
206
+ const ready = !readinessRequired || proofReady;
207
+ const enrollmentState = this.stopped || peerCount === 0 ? 'ready' : this.bootstrapFailures.size > 0 ? 'ssh-bootstrap-blocked' : this.d.dryRun || this.adverts.size < peerCount || !this.endpoint ? 'ssh-bootstrap' : proofReady ? 'ready' : 'ssh-proving';
208
+ return { enabled: true, dryRun: this.d.dryRun, listener: this.endpoint !== null, readinessRequired, ready, enrollmentState, blockedReasons: [...new Set(this.bootstrapFailures.values())].sort(), directions, pairs };
209
+ }
210
+ revoke(machineId) {
211
+ this.clearPeerEvidence(machineId);
212
+ this.knownPeers.delete(machineId);
213
+ }
214
+ async rollback() {
215
+ this.stopped = true;
216
+ if (this.timer)
217
+ clearInterval(this.timer);
218
+ if (this.hostOverlapTimer)
219
+ clearTimeout(this.hostOverlapTimer);
220
+ this.timer = null;
221
+ this.hostOverlapTimer = null;
222
+ await Promise.race([this.endpoint?.close() ?? Promise.resolve(), new Promise(resolve => setTimeout(resolve, 10_000))]);
223
+ this.endpoint = null;
224
+ this.listen = null;
225
+ for (const peer of this.d.listPeers())
226
+ this.admissionStore.revoke(peer.machineId);
227
+ }
228
+ async startEndpoint() {
229
+ this.endpoint = new MachineSshEndpoint({
230
+ hostPrivateKeyPath: this.identity.hostPrivateKeyPath, admissionStore: this.admissionStore,
231
+ hostPrivateKeyPaths: [this.identity.hostPrivateKeyPath, ...(this.servePreviousHost && this.identity.previousHostPrivateKeyPath ? [this.identity.previousHostPrivateKeyPath] : [])],
232
+ machineId: this.d.selfMachineId, machineFingerprint: this.d.selfMachineFingerprint,
233
+ hostGeneration: this.identity.hostGeneration,
234
+ respond: (challenge, admission) => {
235
+ const unsigned = { ...challenge, machineId: this.d.selfMachineId, machineFingerprint: this.d.selfMachineFingerprint, sourceClientKeyFingerprint: MachineSshIdentity.fingerprint(admission.publicKey) };
236
+ return { ...unsigned, signature: this.d.sign(JSON.stringify({ nonce: unsigned.nonce, pairingEpoch: unsigned.pairingEpoch, observerBootId: unsigned.observerBootId, clientGeneration: unsigned.clientGeneration, hostGeneration: unsigned.hostGeneration, machineId: unsigned.machineId, sourceClientKeyFingerprint: unsigned.sourceClientKeyFingerprint, machineFingerprint: unsigned.machineFingerprint })) };
237
+ },
238
+ });
239
+ this.listen = await this.endpoint.listen(this.d.bindHost, this.d.bindPort);
240
+ if (this.servePreviousHost && this.identity.previousHostPrivateKeyPath && !this.hostOverlapTimer) {
241
+ this.hostOverlapTimer = setTimeout(() => { void this.retirePreviousHost(); }, 600_000);
242
+ this.hostOverlapTimer.unref?.();
243
+ }
244
+ }
245
+ async ensureEndpoint() {
246
+ try {
247
+ await this.startEndpoint();
248
+ this.bootstrapFailures.delete(this.d.selfMachineId);
249
+ }
250
+ catch (error) {
251
+ // @silent-fallback-ok — endpoint bootstrap failure is retained as a
252
+ // stable blocked reason and retried by the bounded repair controller.
253
+ await this.endpoint?.close().catch(() => {
254
+ // @silent-fallback-ok — best-effort cleanup of an endpoint that already
255
+ // failed to bind; the original bootstrap failure remains authoritative.
256
+ });
257
+ this.endpoint = null;
258
+ this.listen = null;
259
+ this.bootstrapFailures.set(this.d.selfMachineId, classifyMutualSshFailure(error));
260
+ }
261
+ }
262
+ localAdvert(pairingEpoch) {
263
+ const now = Date.now();
264
+ const issuedAt = new Date(now).toISOString();
265
+ const transition = this.identity.hostGeneration > 1 ? {
266
+ agentId: this.d.agentId, machineId: this.d.selfMachineId, pairingEpoch,
267
+ generation: this.identity.hostGeneration, previousGeneration: this.identity.hostGeneration - 1,
268
+ publicKey: this.identity.hostPublicKey, issuedAt,
269
+ } : null;
270
+ const unsigned = {
271
+ machineId: this.d.selfMachineId, agentId: this.d.agentId, pairingEpoch,
272
+ observerBootId: this.d.observerBootId, clientKeyGeneration: this.identity.clientGeneration,
273
+ hostKeyGeneration: this.identity.hostGeneration, clientPublicKey: this.identity.clientPublicKey,
274
+ sshHostPublicKeys: [this.identity.hostPublicKey], endpoints: this.listen && !isLoopback(this.listen.host) ? [{ ...this.listen, source: 'configured' }] : [],
275
+ issuedAt, expiresAt: new Date(now + Math.min(300_000, this.d.freshnessMs ?? 300_000)).toISOString(),
276
+ ...(transition ? { hostKeyTransitionSignature: this.d.sign(canonicalHostKeyProposal(transition)) } : {}),
277
+ };
278
+ return { ...unsigned, machineSignature: this.d.sign(canonicalSshBootstrapAdvert(unsigned)) };
279
+ }
280
+ async exchangeAdvert(machineId) {
281
+ const peer = this.d.listPeers().find(row => row.machineId === machineId);
282
+ if (!peer)
283
+ return;
284
+ const response = await this.d.send(machineId, { type: 'ssh-bootstrap-advert', advert: this.localAdvert(peer.pairingEpoch) });
285
+ if (response?.advert)
286
+ this.handleAdvert(response.advert, machineId);
287
+ }
288
+ installAdvert(advert) {
289
+ let prior = this.adverts.get(advert.machineId);
290
+ if (prior && (advert.pairingEpoch < prior.pairingEpoch || (advert.pairingEpoch === prior.pairingEpoch && (advert.clientKeyGeneration < prior.clientKeyGeneration || advert.hostKeyGeneration < prior.hostKeyGeneration))))
291
+ throw new Error('ssh-advert-rollback-rejected');
292
+ if (prior && advert.pairingEpoch > prior.pairingEpoch) {
293
+ this.clearPeerEvidence(advert.machineId);
294
+ prior = undefined;
295
+ }
296
+ if (prior && advert.hostKeyGeneration === prior.hostKeyGeneration && advert.sshHostPublicKeys[0] !== prior.sshHostPublicKeys[0]) {
297
+ this.d.notifySecurity?.({ type: 'host-key-substitution', machineId: advert.machineId });
298
+ throw new Error('ssh-host-key-substitution');
299
+ }
300
+ if (prior && advert.hostKeyGeneration > prior.hostKeyGeneration) {
301
+ if (!advert.hostKeyTransitionSignature)
302
+ throw new Error('ssh-host-key-transition-signature-missing');
303
+ const lifecycle = this.hostKeys.get(advert.machineId) ?? new SshHostKeyLifecycle({ generation: prior.hostKeyGeneration, publicKey: prior.sshHostPublicKeys[0] });
304
+ this.hostKeys.set(advert.machineId, lifecycle);
305
+ const proposal = {
306
+ agentId: advert.agentId, machineId: advert.machineId, pairingEpoch: advert.pairingEpoch,
307
+ generation: advert.hostKeyGeneration, previousGeneration: prior.hostKeyGeneration,
308
+ publicKey: advert.sshHostPublicKeys[0], issuedAt: advert.issuedAt, machineSignature: advert.hostKeyTransitionSignature,
309
+ };
310
+ lifecycle.propose(proposal, {
311
+ verifyMachine: row => this.d.verify(advert.machineId, canonicalHostKeyProposal(row), row.machineSignature),
312
+ verifyPreviousHost: () => false,
313
+ });
314
+ this.candidateAdverts.set(advert.machineId, advert);
315
+ this.d.notifySecurity?.({ type: 'host-key-quarantined', machineId: advert.machineId, generation: advert.hostKeyGeneration });
316
+ return;
317
+ }
318
+ this.adverts.set(advert.machineId, advert);
319
+ if (!this.hostKeys.has(advert.machineId))
320
+ this.hostKeys.set(advert.machineId, new SshHostKeyLifecycle({ generation: advert.hostKeyGeneration, publicKey: advert.sshHostPublicKeys[0] }));
321
+ if (!this.d.dryRun)
322
+ this.admissionStore.reconcile([...this.adverts.values()].map(row => ({ agentId: row.agentId, machineId: row.machineId, pairingEpoch: row.pairingEpoch, clientGeneration: row.clientKeyGeneration, observerBootId: row.observerBootId, publicKey: row.clientPublicKey, expiresAt: row.expiresAt })));
323
+ }
324
+ targetFor(peer) {
325
+ const advert = this.candidateAdverts.get(peer.machineId) ?? this.adverts.get(peer.machineId);
326
+ const endpoint = advert?.endpoints[0];
327
+ if (!advert || !endpoint)
328
+ return null;
329
+ return {
330
+ sourceMachineId: this.d.selfMachineId, targetMachineId: peer.machineId,
331
+ host: endpoint.host, port: endpoint.port, endpointId: `${endpoint.source}:${createHash('sha256').update(`${endpoint.host}:${endpoint.port}`).digest('hex').slice(0, 12)}`,
332
+ pairingEpoch: peer.pairingEpoch, observerBootId: this.d.observerBootId,
333
+ sourceClientKeyGeneration: this.identity.clientGeneration, targetHostKeyGeneration: advert.hostKeyGeneration,
334
+ targetHostPublicKey: advert.sshHostPublicKeys[0], clientPrivateKeyPath: this.identity.clientPrivateKeyPath,
335
+ expectedMachineFingerprint: peer.machineFingerprint,
336
+ expectedSourceClientKeyFingerprint: MachineSshIdentity.fingerprint(this.identity.clientPublicKey),
337
+ verifyMachineResponse: (payload, signature) => this.d.verify(peer.machineId, payload, signature),
338
+ signProof: payload => this.d.sign(payload),
339
+ };
340
+ }
341
+ acceptProof(proof, origin) {
342
+ if (proof.sourceMachineId !== origin)
343
+ throw new Error('ssh-proof-origin-mismatch');
344
+ this.proofs.set(this.key(proof.sourceMachineId, proof.targetMachineId), proof);
345
+ this.persistProofs();
346
+ }
347
+ promoteProvenHostKey(machineId, generation) {
348
+ const candidate = this.candidateAdverts.get(machineId);
349
+ const lifecycle = this.hostKeys.get(machineId);
350
+ if (!candidate || !lifecycle || candidate.hostKeyGeneration !== generation)
351
+ return;
352
+ lifecycle.proveCandidate(generation);
353
+ lifecycle.promote(generation, true);
354
+ this.candidateAdverts.delete(machineId);
355
+ this.adverts.set(machineId, candidate);
356
+ this.admissionStore.reconcile([...this.adverts.values()].map(row => ({ agentId: row.agentId, machineId: row.machineId, pairingEpoch: row.pairingEpoch, clientGeneration: row.clientKeyGeneration, observerBootId: row.observerBootId, publicKey: row.clientPublicKey, expiresAt: row.expiresAt })));
357
+ this.audit({ type: 'host-key-promoted', machineId, generation });
358
+ }
359
+ clearPeerEvidence(machineId) {
360
+ this.admissionStore.revoke(machineId);
361
+ this.endpoint?.revoke(machineId);
362
+ this.adverts.delete(machineId);
363
+ this.candidateAdverts.delete(machineId);
364
+ this.hostKeys.delete(machineId);
365
+ for (const key of [...this.proofs.keys()])
366
+ if (key.startsWith(`${machineId}->`) || key.endsWith(`->${machineId}`))
367
+ this.proofs.delete(key);
368
+ this.persistProofs();
369
+ }
370
+ maybeRetirePreviousHost() {
371
+ if (!this.servePreviousHost || !this.identity.previousHostPrivateKeyPath)
372
+ return;
373
+ const peers = this.d.listPeers().filter(peer => peer.machineId !== this.d.selfMachineId);
374
+ const allProven = peers.length > 0 && peers.every(peer => {
375
+ const proof = this.proofs.get(this.key(peer.machineId, this.d.selfMachineId));
376
+ return this.proofFresh(proof) && proof?.targetHostKeyGeneration === this.identity.hostGeneration && proof.pairingEpoch === peer.pairingEpoch;
377
+ });
378
+ if (allProven)
379
+ void this.retirePreviousHost();
380
+ }
381
+ async retirePreviousHost() {
382
+ if (!this.servePreviousHost || this.stopped)
383
+ return;
384
+ this.servePreviousHost = false;
385
+ if (this.hostOverlapTimer)
386
+ clearTimeout(this.hostOverlapTimer);
387
+ this.hostOverlapTimer = null;
388
+ await this.endpoint?.close();
389
+ this.endpoint = null;
390
+ this.listen = null;
391
+ await this.ensureEndpoint();
392
+ this.audit({ type: 'local-host-key-overlap-retired', generation: this.identity.hostGeneration });
393
+ }
394
+ proofFresh(proof) {
395
+ return Boolean(proof && Date.parse(proof.expiresAt) > Date.now() && (proof.monotonicDeadlineMs === undefined || proof.monotonicDeadlineMs > performance.now()));
396
+ }
397
+ key(source, target) { return `${source}->${target}`; }
398
+ audit(event) { this.d.audit?.({ ...event, at: new Date().toISOString() }); }
399
+ invalidatePersistedProofs() {
400
+ try {
401
+ if (fs.lstatSync(this.proofFile).isSymbolicLink())
402
+ throw new Error('ssh-proof-store-symlink-refused');
403
+ JSON.parse(fs.readFileSync(this.proofFile, 'utf8'));
404
+ // Proof authority is process-local. A restart gets a new observer boot id and
405
+ // must actively re-observe both directions instead of reviving wall-clock data.
406
+ this.persistProofs();
407
+ }
408
+ catch (error) {
409
+ if (error.code !== 'ENOENT')
410
+ throw error;
411
+ }
412
+ }
413
+ persistProofs() {
414
+ fs.mkdirSync(path.dirname(this.proofFile), { recursive: true, mode: 0o700 });
415
+ if (fs.existsSync(this.proofFile) && fs.lstatSync(this.proofFile).isSymbolicLink())
416
+ throw new Error('ssh-proof-store-symlink-refused');
417
+ const tmp = `${this.proofFile}.${process.pid}.${randomUUID()}.tmp`;
418
+ fs.writeFileSync(tmp, `${JSON.stringify({ version: 1, proofs: [...this.proofs.values()].map(({ monotonicDeadlineMs: _, ...proof }) => proof) }, null, 2)}\n`, { mode: 0o600 });
419
+ fs.renameSync(tmp, this.proofFile);
420
+ }
421
+ }
422
+ function isLoopback(host) { return host === 'localhost' || host === '::1' || host.startsWith('127.'); }
423
+ //# sourceMappingURL=MutualSshRuntime.js.map