instar 1.3.42 → 1.3.44
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/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +292 -3
- package/dist/commands/server.js.map +1 -1
- package/dist/core/HandoffReceiver.d.ts +56 -0
- package/dist/core/HandoffReceiver.d.ts.map +1 -0
- package/dist/core/HandoffReceiver.js +103 -0
- package/dist/core/HandoffReceiver.js.map +1 -0
- package/dist/core/HandoffWireTransport.d.ts +61 -0
- package/dist/core/HandoffWireTransport.d.ts.map +1 -0
- package/dist/core/HandoffWireTransport.js +114 -0
- package/dist/core/HandoffWireTransport.js.map +1 -0
- package/dist/core/HttpLeaseTransport.d.ts +68 -0
- package/dist/core/HttpLeaseTransport.d.ts.map +1 -0
- package/dist/core/HttpLeaseTransport.js +97 -0
- package/dist/core/HttpLeaseTransport.js.map +1 -0
- package/dist/core/HttpLiveTailTransport.d.ts +88 -0
- package/dist/core/HttpLiveTailTransport.d.ts.map +1 -0
- package/dist/core/HttpLiveTailTransport.js +95 -0
- package/dist/core/HttpLiveTailTransport.js.map +1 -0
- package/dist/core/LeaseCoordinator.d.ts +14 -0
- package/dist/core/LeaseCoordinator.d.ts.map +1 -1
- package/dist/core/LeaseCoordinator.js +38 -0
- package/dist/core/LeaseCoordinator.js.map +1 -1
- package/dist/core/LiveTailSource.d.ts +65 -0
- package/dist/core/LiveTailSource.d.ts.map +1 -0
- package/dist/core/LiveTailSource.js +87 -0
- package/dist/core/LiveTailSource.js.map +1 -0
- package/dist/core/MultiMachineCoordinator.d.ts +14 -0
- package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
- package/dist/core/MultiMachineCoordinator.js +22 -0
- package/dist/core/MultiMachineCoordinator.js.map +1 -1
- package/dist/core/ReplyMarkerTransport.d.ts +58 -0
- package/dist/core/ReplyMarkerTransport.d.ts.map +1 -0
- package/dist/core/ReplyMarkerTransport.js +66 -0
- package/dist/core/ReplyMarkerTransport.js.map +1 -0
- package/dist/core/SafeGitExecutor.d.ts.map +1 -1
- package/dist/core/SafeGitExecutor.js +21 -2
- package/dist/core/SafeGitExecutor.js.map +1 -1
- package/dist/core/featureRolloutScan.d.ts.map +1 -1
- package/dist/core/featureRolloutScan.js +5 -4
- package/dist/core/featureRolloutScan.js.map +1 -1
- package/dist/core/handoffReceiverWiring.d.ts +57 -0
- package/dist/core/handoffReceiverWiring.d.ts.map +1 -0
- package/dist/core/handoffReceiverWiring.js +65 -0
- package/dist/core/handoffReceiverWiring.js.map +1 -0
- package/dist/core/handoffSentinelBootWiring.d.ts +53 -0
- package/dist/core/handoffSentinelBootWiring.d.ts.map +1 -0
- package/dist/core/handoffSentinelBootWiring.js +52 -0
- package/dist/core/handoffSentinelBootWiring.js.map +1 -0
- package/dist/core/handoffSentinelWiring.d.ts +60 -0
- package/dist/core/handoffSentinelWiring.d.ts.map +1 -0
- package/dist/core/handoffSentinelWiring.js +59 -0
- package/dist/core/handoffSentinelWiring.js.map +1 -0
- package/dist/core/seamlessnessConfig.d.ts +8 -0
- package/dist/core/seamlessnessConfig.d.ts.map +1 -1
- package/dist/core/seamlessnessConfig.js +1 -0
- package/dist/core/seamlessnessConfig.js.map +1 -1
- package/dist/core/types.d.ts +6 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/messaging/TelegramAdapter.d.ts +6 -0
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +8 -0
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/messaging/ingressDedup.d.ts +55 -0
- package/dist/messaging/ingressDedup.d.ts.map +1 -0
- package/dist/messaging/ingressDedup.js +69 -0
- package/dist/messaging/ingressDedup.js.map +1 -0
- package/dist/messaging/stuckMessageRecovery.d.ts +46 -0
- package/dist/messaging/stuckMessageRecovery.d.ts.map +1 -0
- package/dist/messaging/stuckMessageRecovery.js +52 -0
- package/dist/messaging/stuckMessageRecovery.js.map +1 -0
- package/dist/monitoring/releaseReadinessWiring.d.ts.map +1 -1
- package/dist/monitoring/releaseReadinessWiring.js +4 -2
- package/dist/monitoring/releaseReadinessWiring.js.map +1 -1
- package/dist/server/AgentServer.d.ts +64 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +24 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/handoffInitiateRoutes.d.ts +40 -0
- package/dist/server/handoffInitiateRoutes.d.ts.map +1 -0
- package/dist/server/handoffInitiateRoutes.js +57 -0
- package/dist/server/handoffInitiateRoutes.js.map +1 -0
- package/dist/server/machineRoutes.d.ts +47 -0
- package/dist/server/machineRoutes.d.ts.map +1 -1
- package/dist/server/machineRoutes.js +138 -0
- package/dist/server/machineRoutes.js.map +1 -1
- package/dist/server/routes.d.ts +20 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +65 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +49 -49
- package/upgrades/1.3.41.md +9 -5
- package/upgrades/1.3.43.md +21 -0
- package/upgrades/1.3.44.md +72 -0
- package/upgrades/side-effects/cross-machine-seamlessness-consent-acquire.md +52 -0
- package/upgrades/side-effects/cross-machine-seamlessness-exactly-once-ingress.md +73 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-ack-yield.md +82 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-begin.md +48 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-boot-wiring.md +68 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-receiver-wiring.md +52 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-receiver.md +46 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-sentinel-wiring.md +47 -0
- package/upgrades/side-effects/cross-machine-seamlessness-lease-wire.md +46 -0
- package/upgrades/side-effects/cross-machine-seamlessness-livetail-source.md +50 -0
- package/upgrades/side-effects/cross-machine-seamlessness-livetail-streaming.md +50 -0
- package/upgrades/side-effects/cross-machine-seamlessness-livetail-wire.md +65 -0
- package/upgrades/side-effects/cross-machine-seamlessness-noloss-recovery.md +61 -0
- package/upgrades/side-effects/cross-machine-seamlessness-reply-marker.md +65 -0
- package/upgrades/side-effects/source-tree-guard-readsync-bypass.md +34 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HandoffReceiver — the INCOMING-machine side of the planned handoff (spec §8
|
|
3
|
+
* G3d/G3e), the counterpart to the outgoing-only HandoffSentinel.
|
|
4
|
+
*
|
|
5
|
+
* The incoming machine is already receiving the live tail (via /api/live-tail).
|
|
6
|
+
* When the outgoing machine signals a handoff is beginning, this receiver:
|
|
7
|
+
* 1. builds its "caught up" ack — echoing the live-tail sequence it holds, the
|
|
8
|
+
* ingress position it will resume from, and a hash of the thread history it
|
|
9
|
+
* loaded (the outgoing verifies this echo before yielding);
|
|
10
|
+
* 2. sends the ack to the outgoing machine;
|
|
11
|
+
* 3. on the explicit `yield` signal (the ONLY trigger), acquires the lease via
|
|
12
|
+
* the consented path.
|
|
13
|
+
*
|
|
14
|
+
* State machine: idle → catching_up → ack_sent → acquired | failed.
|
|
15
|
+
*
|
|
16
|
+
* The yield is acted on ONLY while a handoff is in progress (ack_sent) — a stray
|
|
17
|
+
* yield with no handoff underway is ignored (and the lease's own consent guard
|
|
18
|
+
* refuses a yield from a non-holder regardless). All I/O is injected, so the
|
|
19
|
+
* sequencing is unit-testable without a network.
|
|
20
|
+
*/
|
|
21
|
+
import type { HandoffAck } from './HandoffSentinel.js';
|
|
22
|
+
export type HandoffReceiverState = 'idle' | 'catching_up' | 'ack_sent' | 'acquired' | 'failed';
|
|
23
|
+
export interface HandoffReceiverOps {
|
|
24
|
+
/** Build the "caught up" ack from the live-tail buffer + adapter ingress + thread hash. */
|
|
25
|
+
buildAck: () => Promise<HandoffAck>;
|
|
26
|
+
/** Send the ack to the outgoing machine (HandoffWireTransport.sendAck). */
|
|
27
|
+
sendAck: (ack: HandoffAck) => Promise<boolean>;
|
|
28
|
+
/** Acquire the lease on the verified yield (coordinator.acquireLeaseOnConsent). */
|
|
29
|
+
acquireOnYield: () => Promise<boolean>;
|
|
30
|
+
}
|
|
31
|
+
export interface HandoffReceiverConfig {
|
|
32
|
+
logger?: (msg: string) => void;
|
|
33
|
+
onTerminal?: (state: 'acquired' | 'failed', detail: string) => void;
|
|
34
|
+
}
|
|
35
|
+
export declare class HandoffReceiver {
|
|
36
|
+
private readonly ops;
|
|
37
|
+
private readonly cfg;
|
|
38
|
+
private _state;
|
|
39
|
+
constructor(ops: HandoffReceiverOps, cfg?: HandoffReceiverConfig);
|
|
40
|
+
get state(): HandoffReceiverState;
|
|
41
|
+
private log;
|
|
42
|
+
/**
|
|
43
|
+
* The outgoing machine has begun a handoff to us. Build + send our caught-up
|
|
44
|
+
* ack. Returns true if the ack was sent (the outgoing will verify the echo and,
|
|
45
|
+
* if it matches + validation passes, send a yield).
|
|
46
|
+
*/
|
|
47
|
+
onBeginHandoff(): Promise<boolean>;
|
|
48
|
+
/**
|
|
49
|
+
* The outgoing machine sent the explicit yield. Acquire the lease — but ONLY if
|
|
50
|
+
* a handoff is genuinely in progress (we sent an ack). A stray yield is ignored.
|
|
51
|
+
*/
|
|
52
|
+
onYield(): Promise<boolean>;
|
|
53
|
+
/** Reset to idle (e.g. a handoff was aborted or completed). */
|
|
54
|
+
reset(): void;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=HandoffReceiver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HandoffReceiver.d.ts","sourceRoot":"","sources":["../../src/core/HandoffReceiver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAOH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,aAAa,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;AAE/F,MAAM,WAAW,kBAAkB;IACjC,2FAA2F;IAC3F,QAAQ,EAAE,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC;IACpC,2EAA2E;IAC3E,OAAO,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,mFAAmF;IACnF,cAAc,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,GAAG,QAAQ,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACrE;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAqB;IACzC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAwB;IAC5C,OAAO,CAAC,MAAM,CAAgC;gBAElC,GAAG,EAAE,kBAAkB,EAAE,GAAG,GAAE,qBAA0B;IAKpE,IAAI,KAAK,IAAI,oBAAoB,CAEhC;IAED,OAAO,CAAC,GAAG;IAIX;;;;OAIG;IACG,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC;IA2BxC;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;IAuBjC,+DAA+D;IAC/D,KAAK,IAAI,IAAI;CAGd"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HandoffReceiver — the INCOMING-machine side of the planned handoff (spec §8
|
|
3
|
+
* G3d/G3e), the counterpart to the outgoing-only HandoffSentinel.
|
|
4
|
+
*
|
|
5
|
+
* The incoming machine is already receiving the live tail (via /api/live-tail).
|
|
6
|
+
* When the outgoing machine signals a handoff is beginning, this receiver:
|
|
7
|
+
* 1. builds its "caught up" ack — echoing the live-tail sequence it holds, the
|
|
8
|
+
* ingress position it will resume from, and a hash of the thread history it
|
|
9
|
+
* loaded (the outgoing verifies this echo before yielding);
|
|
10
|
+
* 2. sends the ack to the outgoing machine;
|
|
11
|
+
* 3. on the explicit `yield` signal (the ONLY trigger), acquires the lease via
|
|
12
|
+
* the consented path.
|
|
13
|
+
*
|
|
14
|
+
* State machine: idle → catching_up → ack_sent → acquired | failed.
|
|
15
|
+
*
|
|
16
|
+
* The yield is acted on ONLY while a handoff is in progress (ack_sent) — a stray
|
|
17
|
+
* yield with no handoff underway is ignored (and the lease's own consent guard
|
|
18
|
+
* refuses a yield from a non-holder regardless). All I/O is injected, so the
|
|
19
|
+
* sequencing is unit-testable without a network.
|
|
20
|
+
*/
|
|
21
|
+
export class HandoffReceiver {
|
|
22
|
+
ops;
|
|
23
|
+
cfg;
|
|
24
|
+
_state = 'idle';
|
|
25
|
+
constructor(ops, cfg = {}) {
|
|
26
|
+
this.ops = ops;
|
|
27
|
+
this.cfg = cfg;
|
|
28
|
+
}
|
|
29
|
+
get state() {
|
|
30
|
+
return this._state;
|
|
31
|
+
}
|
|
32
|
+
log(m) {
|
|
33
|
+
this.cfg.logger?.(`[handoff-recv] ${m}`);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The outgoing machine has begun a handoff to us. Build + send our caught-up
|
|
37
|
+
* ack. Returns true if the ack was sent (the outgoing will verify the echo and,
|
|
38
|
+
* if it matches + validation passes, send a yield).
|
|
39
|
+
*/
|
|
40
|
+
async onBeginHandoff() {
|
|
41
|
+
this._state = 'catching_up';
|
|
42
|
+
let ack;
|
|
43
|
+
try {
|
|
44
|
+
ack = await this.ops.buildAck();
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
this._state = 'failed';
|
|
48
|
+
this.log(`buildAck failed: ${msg(err)}`);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
let sent;
|
|
52
|
+
try {
|
|
53
|
+
sent = await this.ops.sendAck(ack);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
this._state = 'failed';
|
|
57
|
+
this.log(`sendAck failed: ${msg(err)}`);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (!sent) {
|
|
61
|
+
this._state = 'failed';
|
|
62
|
+
this.log('ack not accepted by the outgoing machine');
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
this._state = 'ack_sent';
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The outgoing machine sent the explicit yield. Acquire the lease — but ONLY if
|
|
70
|
+
* a handoff is genuinely in progress (we sent an ack). A stray yield is ignored.
|
|
71
|
+
*/
|
|
72
|
+
async onYield() {
|
|
73
|
+
if (this._state !== 'ack_sent') {
|
|
74
|
+
this.log(`ignoring yield in state '${this._state}' — no handoff in progress`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
let acquired;
|
|
78
|
+
try {
|
|
79
|
+
acquired = await this.ops.acquireOnYield();
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
this._state = 'failed';
|
|
83
|
+
this.cfg.onTerminal?.('failed', `acquire threw: ${msg(err)}`);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
if (acquired) {
|
|
87
|
+
this._state = 'acquired';
|
|
88
|
+
this.cfg.onTerminal?.('acquired', 'lease acquired on yield — now awake');
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
this._state = 'failed';
|
|
92
|
+
this.cfg.onTerminal?.('failed', 'consent acquire did not take the lease — staying standby');
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
/** Reset to idle (e.g. a handoff was aborted or completed). */
|
|
96
|
+
reset() {
|
|
97
|
+
this._state = 'idle';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function msg(err) {
|
|
101
|
+
return err instanceof Error ? err.message : String(err);
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=HandoffReceiver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HandoffReceiver.js","sourceRoot":"","sources":["../../src/core/HandoffReceiver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HandoffWireTransport — the point-to-point ack/yield channel for the planned
|
|
3
|
+
* handoff protocol (spec §8 G3d/G3e). Symmetric: both machines run it, using the
|
|
4
|
+
* half that matches their role in a given handoff.
|
|
5
|
+
*
|
|
6
|
+
* OUTGOING machine (HandoffSentinel.initiate):
|
|
7
|
+
* - awaitAck(timeoutMs): resolves when the incoming POSTs its verified-ack to
|
|
8
|
+
* /api/handoff/ack (recordAck fires the pending promise); null on timeout.
|
|
9
|
+
* - sendYield(): POSTs the explicit yield signal to the incoming's
|
|
10
|
+
* /api/handoff/yield — the ONLY trigger for the incoming's lease CAS. Sent
|
|
11
|
+
* ONLY after a verified ack + passing validation (HandoffSentinel enforces).
|
|
12
|
+
*
|
|
13
|
+
* INCOMING machine:
|
|
14
|
+
* - sendAck(ack): POSTs its "caught up" echo (tailSeq + ingressPosition +
|
|
15
|
+
* threadHistoryHash) to the outgoing's /api/handoff/ack.
|
|
16
|
+
* - onYield(cb): the /api/handoff/yield route invokes the registered handler,
|
|
17
|
+
* which triggers the incoming's lease-CAS acquisition.
|
|
18
|
+
*
|
|
19
|
+
* All POSTs ride the authenticated machine channel (signRequest +
|
|
20
|
+
* machineAuthMiddleware on the receiver). Injected fetch/clock for testability.
|
|
21
|
+
* A handoff is strictly 1:1 with the single peer, resolved by the caller.
|
|
22
|
+
*/
|
|
23
|
+
import type { HandoffAck } from './HandoffSentinel.js';
|
|
24
|
+
export interface HandoffWirePeer {
|
|
25
|
+
machineId: string;
|
|
26
|
+
url: string;
|
|
27
|
+
}
|
|
28
|
+
export interface HandoffWireTransportDeps {
|
|
29
|
+
selfMachineId: string;
|
|
30
|
+
signingKeyPem: string;
|
|
31
|
+
/** Resolve the single counterpart machine for a handoff (excludes self). */
|
|
32
|
+
peer: () => HandoffWirePeer | null;
|
|
33
|
+
nextSequence: () => number;
|
|
34
|
+
fetchImpl?: typeof fetch;
|
|
35
|
+
now?: () => number;
|
|
36
|
+
logger?: (msg: string) => void;
|
|
37
|
+
}
|
|
38
|
+
export declare class HandoffWireTransport {
|
|
39
|
+
private readonly d;
|
|
40
|
+
private pendingAck;
|
|
41
|
+
private yieldHandler;
|
|
42
|
+
constructor(deps: HandoffWireTransportDeps);
|
|
43
|
+
private log;
|
|
44
|
+
private fetchImpl;
|
|
45
|
+
private post;
|
|
46
|
+
/** Wait for the incoming machine's ack (resolved by recordAck), or null on timeout. */
|
|
47
|
+
awaitAck(timeoutMs: number): Promise<HandoffAck | null>;
|
|
48
|
+
/** Called by POST /api/handoff/ack — delivers the incoming machine's ack. */
|
|
49
|
+
recordAck(ack: HandoffAck): void;
|
|
50
|
+
/** Send the yield signal to the incoming machine. */
|
|
51
|
+
sendYield(): Promise<boolean>;
|
|
52
|
+
/** Open a planned handoff: POST the flush manifest to the incoming's begin route. */
|
|
53
|
+
sendBegin(manifest: unknown): Promise<boolean>;
|
|
54
|
+
/** Send this machine's "caught up" ack to the outgoing machine. */
|
|
55
|
+
sendAck(ack: HandoffAck): Promise<boolean>;
|
|
56
|
+
/** Register the handler the /api/handoff/yield route invokes (incoming side). */
|
|
57
|
+
onYield(cb: () => void): void;
|
|
58
|
+
/** Called by POST /api/handoff/yield — triggers the incoming's lease CAS. */
|
|
59
|
+
recordYield(): void;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=HandoffWireTransport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HandoffWireTransport.d.ts","sourceRoot":"","sources":["../../src/core/HandoffWireTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,wBAAwB;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,IAAI,EAAE,MAAM,eAAe,GAAG,IAAI,CAAC;IACnC,YAAY,EAAE,MAAM,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAED,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,CAAC,CAA2B;IAC7C,OAAO,CAAC,UAAU,CAAoG;IACtH,OAAO,CAAC,YAAY,CAA6B;gBAErC,IAAI,EAAE,wBAAwB;IAI1C,OAAO,CAAC,GAAG;IAGX,OAAO,CAAC,SAAS;YAGH,IAAI;IAsBlB,uFAAuF;IACvF,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAmBvD,6EAA6E;IAC7E,SAAS,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI;IAWhC,qDAAqD;IAC/C,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;IAInC,qFAAqF;IAC/E,SAAS,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAMpD,mEAAmE;IAC7D,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC;IAIhD,iFAAiF;IACjF,OAAO,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI;IAI7B,6EAA6E;IAC7E,WAAW,IAAI,IAAI;CAOpB"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HandoffWireTransport — the point-to-point ack/yield channel for the planned
|
|
3
|
+
* handoff protocol (spec §8 G3d/G3e). Symmetric: both machines run it, using the
|
|
4
|
+
* half that matches their role in a given handoff.
|
|
5
|
+
*
|
|
6
|
+
* OUTGOING machine (HandoffSentinel.initiate):
|
|
7
|
+
* - awaitAck(timeoutMs): resolves when the incoming POSTs its verified-ack to
|
|
8
|
+
* /api/handoff/ack (recordAck fires the pending promise); null on timeout.
|
|
9
|
+
* - sendYield(): POSTs the explicit yield signal to the incoming's
|
|
10
|
+
* /api/handoff/yield — the ONLY trigger for the incoming's lease CAS. Sent
|
|
11
|
+
* ONLY after a verified ack + passing validation (HandoffSentinel enforces).
|
|
12
|
+
*
|
|
13
|
+
* INCOMING machine:
|
|
14
|
+
* - sendAck(ack): POSTs its "caught up" echo (tailSeq + ingressPosition +
|
|
15
|
+
* threadHistoryHash) to the outgoing's /api/handoff/ack.
|
|
16
|
+
* - onYield(cb): the /api/handoff/yield route invokes the registered handler,
|
|
17
|
+
* which triggers the incoming's lease-CAS acquisition.
|
|
18
|
+
*
|
|
19
|
+
* All POSTs ride the authenticated machine channel (signRequest +
|
|
20
|
+
* machineAuthMiddleware on the receiver). Injected fetch/clock for testability.
|
|
21
|
+
* A handoff is strictly 1:1 with the single peer, resolved by the caller.
|
|
22
|
+
*/
|
|
23
|
+
import { signRequest } from '../server/machineAuth.js';
|
|
24
|
+
export class HandoffWireTransport {
|
|
25
|
+
d;
|
|
26
|
+
pendingAck = null;
|
|
27
|
+
yieldHandler = null;
|
|
28
|
+
constructor(deps) {
|
|
29
|
+
this.d = deps;
|
|
30
|
+
}
|
|
31
|
+
log(m) {
|
|
32
|
+
this.d.logger?.(`[handoff-wire] ${m}`);
|
|
33
|
+
}
|
|
34
|
+
fetchImpl() {
|
|
35
|
+
return this.d.fetchImpl ?? fetch;
|
|
36
|
+
}
|
|
37
|
+
async post(path, body) {
|
|
38
|
+
const peer = this.d.peer();
|
|
39
|
+
if (!peer) {
|
|
40
|
+
this.log(`no peer for ${path}`);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const headers = signRequest(this.d.selfMachineId, this.d.signingKeyPem, body, this.d.nextSequence());
|
|
45
|
+
const res = await this.fetchImpl()(`${peer.url.replace(/\/$/, '')}${path}`, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
48
|
+
body: JSON.stringify(body),
|
|
49
|
+
});
|
|
50
|
+
return !!(res && res.ok);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
this.log(`POST ${path} to ${peer.machineId} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// ── OUTGOING side ──────────────────────────────────────────────
|
|
58
|
+
/** Wait for the incoming machine's ack (resolved by recordAck), or null on timeout. */
|
|
59
|
+
awaitAck(timeoutMs) {
|
|
60
|
+
// Only one handoff at a time; a new wait supersedes any stale pending one.
|
|
61
|
+
if (this.pendingAck) {
|
|
62
|
+
clearTimeout(this.pendingAck.timer);
|
|
63
|
+
this.pendingAck.resolve(null);
|
|
64
|
+
this.pendingAck = null;
|
|
65
|
+
}
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const timer = setTimeout(() => {
|
|
68
|
+
this.pendingAck = null;
|
|
69
|
+
this.log(`ack timed out after ${timeoutMs}ms`);
|
|
70
|
+
resolve(null);
|
|
71
|
+
}, timeoutMs);
|
|
72
|
+
// Don't let a pending handoff ack keep the process alive.
|
|
73
|
+
timer.unref?.();
|
|
74
|
+
this.pendingAck = { resolve, timer };
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Called by POST /api/handoff/ack — delivers the incoming machine's ack. */
|
|
78
|
+
recordAck(ack) {
|
|
79
|
+
if (!this.pendingAck) {
|
|
80
|
+
this.log('received an ack with no pending awaitAck — dropping');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
clearTimeout(this.pendingAck.timer);
|
|
84
|
+
const { resolve } = this.pendingAck;
|
|
85
|
+
this.pendingAck = null;
|
|
86
|
+
resolve(ack);
|
|
87
|
+
}
|
|
88
|
+
/** Send the yield signal to the incoming machine. */
|
|
89
|
+
async sendYield() {
|
|
90
|
+
return this.post('/api/handoff/yield', { yield: true, from: this.d.selfMachineId });
|
|
91
|
+
}
|
|
92
|
+
/** Open a planned handoff: POST the flush manifest to the incoming's begin route. */
|
|
93
|
+
async sendBegin(manifest) {
|
|
94
|
+
return this.post('/api/handoff/begin', { manifest });
|
|
95
|
+
}
|
|
96
|
+
// ── INCOMING side ──────────────────────────────────────────────
|
|
97
|
+
/** Send this machine's "caught up" ack to the outgoing machine. */
|
|
98
|
+
async sendAck(ack) {
|
|
99
|
+
return this.post('/api/handoff/ack', { ack });
|
|
100
|
+
}
|
|
101
|
+
/** Register the handler the /api/handoff/yield route invokes (incoming side). */
|
|
102
|
+
onYield(cb) {
|
|
103
|
+
this.yieldHandler = cb;
|
|
104
|
+
}
|
|
105
|
+
/** Called by POST /api/handoff/yield — triggers the incoming's lease CAS. */
|
|
106
|
+
recordYield() {
|
|
107
|
+
if (!this.yieldHandler) {
|
|
108
|
+
this.log('received a yield with no handler registered — dropping');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.yieldHandler();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=HandoffWireTransport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HandoffWireTransport.js","sourceRoot":"","sources":["../../src/core/HandoffWireTransport.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HttpLeaseTransport — the low-latency wire path for the fenced lease (spec §6:
|
|
3
|
+
* "the low-latency authoritative copy of the lease travels over the tunnel").
|
|
4
|
+
*
|
|
5
|
+
* Implements LeaseTransport over the existing authenticated machine-to-machine
|
|
6
|
+
* HTTP channel (signRequest + machineAuthMiddleware, the same path /api/heartbeat
|
|
7
|
+
* uses). The git copy remains the durable audit/CAS substrate; this transport
|
|
8
|
+
* ACCELERATES acquisition + carries renewals, and a holder that cannot reach
|
|
9
|
+
* peers for > leaseTtlMs self-suspends (the renewal-requires-medium rule).
|
|
10
|
+
*
|
|
11
|
+
* observed() returns the freshest lease this machine has RECEIVED from a peer
|
|
12
|
+
* (fed by the /api/lease endpoint via recordObserved) plus the per-holder nonce
|
|
13
|
+
* map for replay detection. isReachable() reflects whether a recent broadcast
|
|
14
|
+
* reached at least one peer.
|
|
15
|
+
*
|
|
16
|
+
* The HTTP layer is injected (fetchImpl) so the broadcast/observe/reachability
|
|
17
|
+
* logic is unit-testable without a network.
|
|
18
|
+
*/
|
|
19
|
+
import type { LeaseTransport } from './LeaseCoordinator.js';
|
|
20
|
+
import type { LeaseRecord } from './types.js';
|
|
21
|
+
export interface LeasePeer {
|
|
22
|
+
machineId: string;
|
|
23
|
+
/** Base URL of the peer (its lastKnownUrl / tunnel URL). */
|
|
24
|
+
url: string;
|
|
25
|
+
}
|
|
26
|
+
export interface HttpLeaseTransportDeps {
|
|
27
|
+
selfMachineId: string;
|
|
28
|
+
signingKeyPem: string;
|
|
29
|
+
/** Resolve the current set of reachable peers (excludes self). */
|
|
30
|
+
peers: () => LeasePeer[];
|
|
31
|
+
/** Monotonic per-request sequence (reuse the machine's nonce/sequence source). */
|
|
32
|
+
nextSequence: () => number;
|
|
33
|
+
/** Injected fetch (defaults to global fetch). */
|
|
34
|
+
fetchImpl?: typeof fetch;
|
|
35
|
+
/** How recent a successful broadcast counts as "reachable". Default = leaseTtlMs. */
|
|
36
|
+
reachabilityWindowMs?: number;
|
|
37
|
+
now?: () => number;
|
|
38
|
+
logger?: (msg: string) => void;
|
|
39
|
+
}
|
|
40
|
+
export declare class HttpLeaseTransport implements LeaseTransport {
|
|
41
|
+
private readonly d;
|
|
42
|
+
private lastObserved;
|
|
43
|
+
private lastNonceByHolder;
|
|
44
|
+
private lastBroadcastOkAt;
|
|
45
|
+
private readonly windowMs;
|
|
46
|
+
constructor(deps: HttpLeaseTransportDeps);
|
|
47
|
+
private now;
|
|
48
|
+
private log;
|
|
49
|
+
/**
|
|
50
|
+
* Broadcast our lease to every peer over the authenticated channel. Resolves
|
|
51
|
+
* true if at least one peer accepted (we have a live medium); false if none
|
|
52
|
+
* were reachable.
|
|
53
|
+
*/
|
|
54
|
+
broadcast(lease: LeaseRecord): Promise<boolean>;
|
|
55
|
+
observed(): {
|
|
56
|
+
lease: LeaseRecord | null;
|
|
57
|
+
lastNonceByHolder: Record<string, number>;
|
|
58
|
+
};
|
|
59
|
+
isReachable(): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Record a lease received from a peer (called by the /api/lease endpoint after
|
|
62
|
+
* machine-auth verification). Keeps only the highest-epoch observed lease and
|
|
63
|
+
* advances the per-holder nonce watermark (replay detection happens in
|
|
64
|
+
* FencedLease.acceptTunnelLease which reads this map).
|
|
65
|
+
*/
|
|
66
|
+
recordObserved(lease: LeaseRecord): void;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=HttpLeaseTransport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HttpLeaseTransport.d.ts","sourceRoot":"","sources":["../../src/core/HttpLeaseTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,sBAAsB;IACrC,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,KAAK,EAAE,MAAM,SAAS,EAAE,CAAC;IACzB,kFAAkF;IAClF,YAAY,EAAE,MAAM,MAAM,CAAC;IAC3B,iDAAiD;IACjD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,qFAAqF;IACrF,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAED,qBAAa,kBAAmB,YAAW,cAAc;IACvD,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAyB;IAC3C,OAAO,CAAC,YAAY,CAA4B;IAChD,OAAO,CAAC,iBAAiB,CAA8B;IACvD,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAEtB,IAAI,EAAE,sBAAsB;IAKxC,OAAO,CAAC,GAAG;IAGX,OAAO,CAAC,GAAG;IAIX;;;;OAIG;IACG,SAAS,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;IA6BrD,QAAQ,IAAI;QAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE;IAIpF,WAAW,IAAI,OAAO;IAItB;;;;;OAKG;IACH,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;CAYzC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HttpLeaseTransport — the low-latency wire path for the fenced lease (spec §6:
|
|
3
|
+
* "the low-latency authoritative copy of the lease travels over the tunnel").
|
|
4
|
+
*
|
|
5
|
+
* Implements LeaseTransport over the existing authenticated machine-to-machine
|
|
6
|
+
* HTTP channel (signRequest + machineAuthMiddleware, the same path /api/heartbeat
|
|
7
|
+
* uses). The git copy remains the durable audit/CAS substrate; this transport
|
|
8
|
+
* ACCELERATES acquisition + carries renewals, and a holder that cannot reach
|
|
9
|
+
* peers for > leaseTtlMs self-suspends (the renewal-requires-medium rule).
|
|
10
|
+
*
|
|
11
|
+
* observed() returns the freshest lease this machine has RECEIVED from a peer
|
|
12
|
+
* (fed by the /api/lease endpoint via recordObserved) plus the per-holder nonce
|
|
13
|
+
* map for replay detection. isReachable() reflects whether a recent broadcast
|
|
14
|
+
* reached at least one peer.
|
|
15
|
+
*
|
|
16
|
+
* The HTTP layer is injected (fetchImpl) so the broadcast/observe/reachability
|
|
17
|
+
* logic is unit-testable without a network.
|
|
18
|
+
*/
|
|
19
|
+
import { signRequest } from '../server/machineAuth.js';
|
|
20
|
+
export class HttpLeaseTransport {
|
|
21
|
+
d;
|
|
22
|
+
lastObserved = null;
|
|
23
|
+
lastNonceByHolder = {};
|
|
24
|
+
lastBroadcastOkAt = 0;
|
|
25
|
+
windowMs;
|
|
26
|
+
constructor(deps) {
|
|
27
|
+
this.d = deps;
|
|
28
|
+
this.windowMs = deps.reachabilityWindowMs ?? 60_000;
|
|
29
|
+
}
|
|
30
|
+
now() {
|
|
31
|
+
return (this.d.now ?? Date.now)();
|
|
32
|
+
}
|
|
33
|
+
log(m) {
|
|
34
|
+
this.d.logger?.(`[lease-wire] ${m}`);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Broadcast our lease to every peer over the authenticated channel. Resolves
|
|
38
|
+
* true if at least one peer accepted (we have a live medium); false if none
|
|
39
|
+
* were reachable.
|
|
40
|
+
*/
|
|
41
|
+
async broadcast(lease) {
|
|
42
|
+
const peers = this.d.peers();
|
|
43
|
+
if (peers.length === 0) {
|
|
44
|
+
// No peers → a single-machine mesh; treat as "reachable" (nothing to fail).
|
|
45
|
+
this.lastBroadcastOkAt = this.now();
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
const fetchImpl = this.d.fetchImpl ?? fetch;
|
|
49
|
+
let anyOk = false;
|
|
50
|
+
await Promise.all(peers.map(async (peer) => {
|
|
51
|
+
try {
|
|
52
|
+
const body = { lease };
|
|
53
|
+
const headers = signRequest(this.d.selfMachineId, this.d.signingKeyPem, body, this.d.nextSequence());
|
|
54
|
+
const res = await fetchImpl(`${peer.url.replace(/\/$/, '')}/api/lease`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
57
|
+
body: JSON.stringify(body),
|
|
58
|
+
});
|
|
59
|
+
if (res && res.ok)
|
|
60
|
+
anyOk = true;
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
this.log(`broadcast to ${peer.machineId} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
66
|
+
if (anyOk)
|
|
67
|
+
this.lastBroadcastOkAt = this.now();
|
|
68
|
+
return anyOk;
|
|
69
|
+
}
|
|
70
|
+
observed() {
|
|
71
|
+
return { lease: this.lastObserved, lastNonceByHolder: { ...this.lastNonceByHolder } };
|
|
72
|
+
}
|
|
73
|
+
isReachable() {
|
|
74
|
+
return this.now() - this.lastBroadcastOkAt <= this.windowMs;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Record a lease received from a peer (called by the /api/lease endpoint after
|
|
78
|
+
* machine-auth verification). Keeps only the highest-epoch observed lease and
|
|
79
|
+
* advances the per-holder nonce watermark (replay detection happens in
|
|
80
|
+
* FencedLease.acceptTunnelLease which reads this map).
|
|
81
|
+
*/
|
|
82
|
+
recordObserved(lease) {
|
|
83
|
+
if (!lease || typeof lease.epoch !== 'number')
|
|
84
|
+
return;
|
|
85
|
+
const prevNonce = this.lastNonceByHolder[lease.holder] ?? -1;
|
|
86
|
+
// Only accept a strictly-newer nonce for this holder (drop replays here too).
|
|
87
|
+
if (lease.nonce <= prevNonce && this.lastObserved && this.lastObserved.epoch >= lease.epoch) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (lease.nonce > prevNonce)
|
|
91
|
+
this.lastNonceByHolder[lease.holder] = lease.nonce;
|
|
92
|
+
if (!this.lastObserved || lease.epoch >= this.lastObserved.epoch) {
|
|
93
|
+
this.lastObserved = lease;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=HttpLeaseTransport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HttpLeaseTransport.js","sourceRoot":"","sources":["../../src/core/HttpLeaseTransport.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HttpLiveTailTransport — the encrypted server-to-server wire that streams the
|
|
3
|
+
* live conversation tail from the lease HOLDER to the standby (spec §8 G3b/c).
|
|
4
|
+
*
|
|
5
|
+
* This is the SENDER side. The holder calls broadcast() with a per-topic,
|
|
6
|
+
* monotonic-sequence flush of recent conversation content; the transport:
|
|
7
|
+
* 1. REDACTS the content (liveTailRedaction) BEFORE anything leaves the
|
|
8
|
+
* machine — credential categories are scrubbed at the boundary, not trusted
|
|
9
|
+
* to TLS or the receiver (spec §8 G3c: "redact/exclude secrets ... never
|
|
10
|
+
* sent in the clear over the tail").
|
|
11
|
+
* 2. ENCRYPTS the redacted content for the peer using the v3 machine-to-machine
|
|
12
|
+
* scheme (encryptForSync: ephemeral X25519 key agreement + AES-256-GCM,
|
|
13
|
+
* forward-secret per flush — a fresh ephemeral key per call).
|
|
14
|
+
* 3. POSTs it to the peer's /api/live-tail over the existing AUTHENTICATED
|
|
15
|
+
* machine channel (signRequest + machineAuthMiddleware on the receiver),
|
|
16
|
+
* giving mutual authentication: the receiver verifies the sender's machine
|
|
17
|
+
* identity, and only the intended receiver (holding the X25519 private key)
|
|
18
|
+
* can decrypt.
|
|
19
|
+
*
|
|
20
|
+
* Single-machine safe: no peers → broadcast is a reachable no-op (identical to
|
|
21
|
+
* a git-only / disabled mesh, so a solo agent is unaffected).
|
|
22
|
+
*
|
|
23
|
+
* Only the designated next-in-line standby peer(s) receive a tail subscription
|
|
24
|
+
* (the cost ceiling that keeps "more machines = more reliable, not more chatter"
|
|
25
|
+
* — spec §8 G3b); peer selection is the caller's via the injected peers().
|
|
26
|
+
*
|
|
27
|
+
* The HTTP layer, the encryptor, and the clock are all injected so the
|
|
28
|
+
* redact→encrypt→post logic is unit-testable without a network or real keys.
|
|
29
|
+
*/
|
|
30
|
+
import type { EncryptedSecretPayload } from './SecretStore.js';
|
|
31
|
+
export interface LiveTailPeer {
|
|
32
|
+
machineId: string;
|
|
33
|
+
/** Base URL of the peer (its lastKnownUrl / tunnel URL). */
|
|
34
|
+
url: string;
|
|
35
|
+
/** Peer's X25519 encryption public key (base64) — from the mesh registry. */
|
|
36
|
+
encryptionPublicKey: string;
|
|
37
|
+
}
|
|
38
|
+
/** What actually travels on the wire — the content is encrypted, never plaintext. */
|
|
39
|
+
export interface LiveTailFlushWire {
|
|
40
|
+
topic: string;
|
|
41
|
+
seq: number;
|
|
42
|
+
enc: EncryptedSecretPayload;
|
|
43
|
+
/** The redaction category-set version the sender applied (spec §8 G3c). */
|
|
44
|
+
redactionVersion: number;
|
|
45
|
+
}
|
|
46
|
+
export interface HttpLiveTailTransportDeps {
|
|
47
|
+
selfMachineId: string;
|
|
48
|
+
signingKeyPem: string;
|
|
49
|
+
/** Resolve the next-in-line standby peer(s) to push the live tail to (excludes self). */
|
|
50
|
+
peers: () => LiveTailPeer[];
|
|
51
|
+
/** Monotonic per-request sequence for machine-auth replay protection. */
|
|
52
|
+
nextSequence: () => number;
|
|
53
|
+
/**
|
|
54
|
+
* Encrypt already-redacted content for a recipient's X25519 public key (base64).
|
|
55
|
+
* Wired to SecretStore.encryptForSync in production; injected for tests.
|
|
56
|
+
*/
|
|
57
|
+
encryptFor: (content: string, recipientEncPubB64: string) => EncryptedSecretPayload;
|
|
58
|
+
/** Injected fetch (defaults to global fetch). */
|
|
59
|
+
fetchImpl?: typeof fetch;
|
|
60
|
+
/** How recent a successful broadcast counts as "reachable". Default = 60s. */
|
|
61
|
+
reachabilityWindowMs?: number;
|
|
62
|
+
now?: () => number;
|
|
63
|
+
logger?: (msg: string) => void;
|
|
64
|
+
}
|
|
65
|
+
export interface LiveTailFlushInput {
|
|
66
|
+
topic: string;
|
|
67
|
+
/** Per-topic monotonic sequence (the standby applies only lastAppliedSeq+1). */
|
|
68
|
+
seq: number;
|
|
69
|
+
/** Raw (un-redacted) tail content for this flush. */
|
|
70
|
+
content: string;
|
|
71
|
+
}
|
|
72
|
+
export declare class HttpLiveTailTransport {
|
|
73
|
+
private readonly d;
|
|
74
|
+
private lastBroadcastOkAt;
|
|
75
|
+
private readonly windowMs;
|
|
76
|
+
constructor(deps: HttpLiveTailTransportDeps);
|
|
77
|
+
private now;
|
|
78
|
+
private log;
|
|
79
|
+
/**
|
|
80
|
+
* Redact → encrypt → POST a flush to every standby peer over the authenticated
|
|
81
|
+
* channel. Resolves true if at least one peer accepted (a live medium exists);
|
|
82
|
+
* false if no peer was reachable. No peers → a reachable no-op (true).
|
|
83
|
+
*/
|
|
84
|
+
broadcast(flush: LiveTailFlushInput): Promise<boolean>;
|
|
85
|
+
/** Whether a recent broadcast reached at least one peer (the renewal-medium check). */
|
|
86
|
+
isReachable(): boolean;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=HttpLiveTailTransport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HttpLiveTailTransport.d.ts","sourceRoot":"","sources":["../../src/core/HttpLiveTailTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE/D,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,GAAG,EAAE,MAAM,CAAC;IACZ,6EAA6E;IAC7E,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED,qFAAqF;AACrF,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,sBAAsB,CAAC;IAC5B,2EAA2E;IAC3E,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,yFAAyF;IACzF,KAAK,EAAE,MAAM,YAAY,EAAE,CAAC;IAC5B,yEAAyE;IACzE,YAAY,EAAE,MAAM,MAAM,CAAC;IAC3B;;;OAGG;IACH,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,KAAK,sBAAsB,CAAC;IACpF,iDAAiD;IACjD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,8EAA8E;IAC9E,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,GAAG,EAAE,MAAM,CAAC;IACZ,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAA4B;IAC9C,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAEtB,IAAI,EAAE,yBAAyB;IAK3C,OAAO,CAAC,GAAG;IAGX,OAAO,CAAC,GAAG;IAIX;;;;OAIG;IACG,SAAS,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAsC5D,uFAAuF;IACvF,WAAW,IAAI,OAAO;CAGvB"}
|