instar 1.3.17 → 1.3.19
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 +104 -6
- package/dist/commands/server.js.map +1 -1
- package/dist/core/FencedLease.d.ts +126 -0
- package/dist/core/FencedLease.d.ts.map +1 -0
- package/dist/core/FencedLease.js +189 -0
- package/dist/core/FencedLease.js.map +1 -0
- package/dist/core/GitLeaseStore.d.ts +46 -0
- package/dist/core/GitLeaseStore.d.ts.map +1 -0
- package/dist/core/GitLeaseStore.js +79 -0
- package/dist/core/GitLeaseStore.js.map +1 -0
- package/dist/core/GitSync.d.ts +19 -0
- package/dist/core/GitSync.d.ts.map +1 -1
- package/dist/core/GitSync.js +77 -0
- package/dist/core/GitSync.js.map +1 -1
- package/dist/core/HandoffSentinel.d.ts +80 -0
- package/dist/core/HandoffSentinel.d.ts.map +1 -0
- package/dist/core/HandoffSentinel.js +141 -0
- package/dist/core/HandoffSentinel.js.map +1 -0
- package/dist/core/LeaseCoordinator.d.ts +141 -0
- package/dist/core/LeaseCoordinator.d.ts.map +1 -0
- package/dist/core/LeaseCoordinator.js +226 -0
- package/dist/core/LeaseCoordinator.js.map +1 -0
- package/dist/core/LiveTailBuffer.d.ts +64 -0
- package/dist/core/LiveTailBuffer.d.ts.map +1 -0
- package/dist/core/LiveTailBuffer.js +122 -0
- package/dist/core/LiveTailBuffer.js.map +1 -0
- package/dist/core/MultiMachineCoordinator.d.ts +49 -0
- package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
- package/dist/core/MultiMachineCoordinator.js +136 -0
- package/dist/core/MultiMachineCoordinator.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +24 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/RegistrySyncDebouncer.d.ts +77 -0
- package/dist/core/RegistrySyncDebouncer.d.ts.map +1 -0
- package/dist/core/RegistrySyncDebouncer.js +131 -0
- package/dist/core/RegistrySyncDebouncer.js.map +1 -0
- package/dist/core/liveTailRedaction.d.ts +36 -0
- package/dist/core/liveTailRedaction.d.ts.map +1 -0
- package/dist/core/liveTailRedaction.js +57 -0
- package/dist/core/liveTailRedaction.js.map +1 -0
- package/dist/core/registryReplayGuard.d.ts +59 -0
- package/dist/core/registryReplayGuard.d.ts.map +1 -0
- package/dist/core/registryReplayGuard.js +86 -0
- package/dist/core/registryReplayGuard.js.map +1 -0
- package/dist/core/seamlessnessConfig.d.ts +79 -0
- package/dist/core/seamlessnessConfig.d.ts.map +1 -0
- package/dist/core/seamlessnessConfig.js +131 -0
- package/dist/core/seamlessnessConfig.js.map +1 -0
- package/dist/core/types.d.ts +117 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/wireRegistrySync.d.ts +32 -0
- package/dist/core/wireRegistrySync.d.ts.map +1 -0
- package/dist/core/wireRegistrySync.js +31 -0
- package/dist/core/wireRegistrySync.js.map +1 -0
- package/dist/feedback-factory/dispatch/dispatch.d.ts +47 -0
- package/dist/feedback-factory/dispatch/dispatch.d.ts.map +1 -0
- package/dist/feedback-factory/dispatch/dispatch.js +72 -0
- package/dist/feedback-factory/dispatch/dispatch.js.map +1 -0
- package/dist/feedback-factory/receiver/defense.d.ts +82 -0
- package/dist/feedback-factory/receiver/defense.d.ts.map +1 -0
- package/dist/feedback-factory/receiver/defense.js +147 -0
- package/dist/feedback-factory/receiver/defense.js.map +1 -0
- package/dist/messaging/FencedOutbox.d.ts +42 -0
- package/dist/messaging/FencedOutbox.d.ts.map +1 -0
- package/dist/messaging/FencedOutbox.js +60 -0
- package/dist/messaging/FencedOutbox.js.map +1 -0
- package/dist/messaging/MessageProcessingLedger.d.ts +105 -0
- package/dist/messaging/MessageProcessingLedger.d.ts.map +1 -0
- package/dist/messaging/MessageProcessingLedger.js +206 -0
- package/dist/messaging/MessageProcessingLedger.js.map +1 -0
- package/dist/messaging/TelegramAdapter.d.ts +18 -1
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +50 -0
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +7 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +6 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +64 -64
- package/src/scaffold/templates.ts +7 -0
- package/upgrades/1.3.18.md +27 -0
- package/upgrades/1.3.19.md +23 -0
- package/upgrades/side-effects/PR-BODY-cross-machine-seamlessness.md +63 -0
- package/upgrades/side-effects/cross-machine-seamlessness-g1-lease.md +71 -0
- package/upgrades/side-effects/cross-machine-seamlessness-g2-foundation.md +75 -0
- package/upgrades/side-effects/cross-machine-seamlessness-g3-telegram-contract.md +36 -0
- package/upgrades/side-effects/cross-machine-seamlessness-g3a-ledger-handoff.md +56 -0
- package/upgrades/side-effects/cross-machine-seamlessness-g3bc-livetail-outbox.md +51 -0
- package/upgrades/side-effects/cross-machine-seamlessness-p4-awareness-observability.md +54 -0
- package/upgrades/side-effects/feedback-factory-dispatch.md +31 -0
- package/upgrades/side-effects/feedback-factory-receiver-defense.md +33 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FencedLease — the single coordination primitive for cross-machine
|
|
3
|
+
* seamlessness: "exactly one holder, safe under clock skew and partition."
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §6.
|
|
6
|
+
*
|
|
7
|
+
* This module is the PURE LOGIC of the lease — epoch arithmetic, signing,
|
|
8
|
+
* expiry, the fencing check, tunnel-message acceptance, and the acquisition
|
|
9
|
+
* decision. The transport (git push-or-reject-reread, tunnel broadcast) is
|
|
10
|
+
* driven by a higher-level coordinator that calls these methods; keeping the
|
|
11
|
+
* logic pure makes the dangerous parts (CAS, fencing, clock-skew) unit-testable
|
|
12
|
+
* with in-memory fakes.
|
|
13
|
+
*
|
|
14
|
+
* Authority is the EPOCH (a monotonically increasing integer advanced once per
|
|
15
|
+
* acquisition), never wall-clock time. A machine with a fast clock cannot win
|
|
16
|
+
* anything — it has no way to advance the epoch without a valid CAS. `lastSeen`
|
|
17
|
+
* is retained only for the liveness heuristic (presumed-dead), and that
|
|
18
|
+
* threshold must exceed worst-case NTP drift by ≥2× (enforced by the caller).
|
|
19
|
+
*/
|
|
20
|
+
import type { LeaseRecord } from './types.js';
|
|
21
|
+
/** Crypto seam — injected so the logic is testable without real key files. */
|
|
22
|
+
export interface LeaseCrypto {
|
|
23
|
+
/** This machine's id (the only id we may name as `holder` when signing). */
|
|
24
|
+
selfMachineId: string;
|
|
25
|
+
/** Sign canonical bytes with this machine's Ed25519 private key → base64. */
|
|
26
|
+
sign(canonical: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Verify a signature attributed to `holderMachineId` against that machine's
|
|
29
|
+
* REGISTERED public key (looked up from the SAS-verified pairing registry).
|
|
30
|
+
* Returns false if the holder is unknown or the signature is invalid — so a
|
|
31
|
+
* forged lease naming an unknown/foreign holder never verifies.
|
|
32
|
+
*/
|
|
33
|
+
verify(canonical: string, signature: string, holderMachineId: string): boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface FencedLeaseConfig {
|
|
36
|
+
leaseTtlMs: number;
|
|
37
|
+
failoverThresholdMs: number;
|
|
38
|
+
/** Bounded CAS retry before the livelock backoff kicks in. Default 5. */
|
|
39
|
+
casMaxRetries?: number;
|
|
40
|
+
}
|
|
41
|
+
export interface AcquireDecision {
|
|
42
|
+
can: boolean;
|
|
43
|
+
reason: string;
|
|
44
|
+
}
|
|
45
|
+
export interface TunnelAcceptDecision {
|
|
46
|
+
accept: boolean;
|
|
47
|
+
reason: string;
|
|
48
|
+
}
|
|
49
|
+
export declare class FencedLease {
|
|
50
|
+
private readonly crypto;
|
|
51
|
+
private readonly leaseTtlMs;
|
|
52
|
+
private readonly failoverThresholdMs;
|
|
53
|
+
private readonly casMaxRetries;
|
|
54
|
+
constructor(crypto: LeaseCrypto, config: FencedLeaseConfig);
|
|
55
|
+
get selfMachineId(): string;
|
|
56
|
+
/**
|
|
57
|
+
* Stable, field-ordered serialization of the signable lease fields. The
|
|
58
|
+
* signature covers exactly these so a holder cannot later be impersonated
|
|
59
|
+
* by re-ordering fields or smuggling extra keys.
|
|
60
|
+
*/
|
|
61
|
+
static canonicalize(lease: Pick<LeaseRecord, 'holder' | 'epoch' | 'acquiredAt' | 'expiresAt' | 'nonce'>): string;
|
|
62
|
+
/** Build + sign a lease record naming THIS machine as holder. */
|
|
63
|
+
signLease(epoch: number, acquiredAtIso: string, expiresAtIso: string, nonce: number): LeaseRecord;
|
|
64
|
+
/** Verify a lease's signature against its claimed holder's registered key. */
|
|
65
|
+
verifyLease(lease: LeaseRecord): boolean;
|
|
66
|
+
/** True if the lease's holder-local expiry has passed. */
|
|
67
|
+
isExpired(lease: LeaseRecord | undefined | null, nowMs: number): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* The epoch the fencing check uses: max(tunnel-observed, git-committed).
|
|
70
|
+
* A fast tunnel copy can ACCELERATE acquisition but can never lower the
|
|
71
|
+
* observed epoch below what git has already committed (spec §6).
|
|
72
|
+
*/
|
|
73
|
+
static effectiveEpoch(tunnelEpoch: number, gitEpoch: number): number;
|
|
74
|
+
/**
|
|
75
|
+
* Does THIS machine hold the lease at the current effective epoch? Used to
|
|
76
|
+
* gate ingress polls, scheduler ticks, outbound sends, and authority-bearing
|
|
77
|
+
* registry writes. A wedged old-awake whose lease moved on fails this and is
|
|
78
|
+
* fenced out (spec §6 "Fencing check").
|
|
79
|
+
*/
|
|
80
|
+
holdsValidLease(lease: LeaseRecord | undefined | null, effectiveEpoch: number, nowMs: number): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Should an action stamped with `stampedEpoch` be honored, given the current
|
|
83
|
+
* effective epoch? Any consumer (registry, outbox, channel-send) rejects
|
|
84
|
+
* actions stamped with a stale epoch (spec §6). Late writes/sends from a
|
|
85
|
+
* fenced old-awake carry an old epoch and are dropped.
|
|
86
|
+
*/
|
|
87
|
+
static isStampCurrent(stampedEpoch: number, effectiveEpoch: number): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Decide whether to accept a lease record observed over the tunnel. Accept
|
|
90
|
+
* ONLY if: the signature verifies, the epoch is ≥ the git-committed floor
|
|
91
|
+
* (a below-floor message can neither trick a standby into believing a stale
|
|
92
|
+
* lease is current nor suppress a legitimate acquisition), AND the per-holder
|
|
93
|
+
* nonce is strictly greater than the last seen for that holder (replay guard).
|
|
94
|
+
*/
|
|
95
|
+
acceptTunnelLease(msg: LeaseRecord, gitCommittedEpoch: number, lastNonceByHolder: Record<string, number>): TunnelAcceptDecision;
|
|
96
|
+
/**
|
|
97
|
+
* May this machine attempt to acquire the lease right now? True when:
|
|
98
|
+
* - there is no current lease, OR
|
|
99
|
+
* - the current lease is expired, OR
|
|
100
|
+
* - the current holder is presumed dead (caller passes the set, derived
|
|
101
|
+
* from lastSeen > failoverThresholdMs — never from a raw clock compare).
|
|
102
|
+
* A genuinely-live contended lease is resolved by the CAS itself (only one
|
|
103
|
+
* epoch advance wins); this method only decides whether to try.
|
|
104
|
+
*/
|
|
105
|
+
canAcquire(currentLease: LeaseRecord | undefined | null, presumedDeadHolders: ReadonlySet<string>, nowMs: number): AcquireDecision;
|
|
106
|
+
/**
|
|
107
|
+
* Build the CAS candidate that advances the epoch by exactly one. Acquisition
|
|
108
|
+
* may only ever write `epoch = currentEpoch + 1`; epoch GAPS are explicitly
|
|
109
|
+
* safe (a higher epoch observed on re-read always wins). The expiry is
|
|
110
|
+
* holder-local (now + TTL), used for liveness/display, never for authority.
|
|
111
|
+
*/
|
|
112
|
+
buildAcquisition(currentLease: LeaseRecord | undefined | null, nowMs: number, nonce: number): LeaseRecord;
|
|
113
|
+
/**
|
|
114
|
+
* After CAS exhaustion (bounded retry), should THIS machine back off to let
|
|
115
|
+
* the other side land (livelock prevention, spec §6)? The deterministic rule:
|
|
116
|
+
* the higher unforgeable machineId backs off for one lease-TTL, guaranteeing
|
|
117
|
+
* one side wins. Below the retry cap, no backoff (keep contending).
|
|
118
|
+
*/
|
|
119
|
+
shouldBackoffAfterContention(retryCount: number, contenderMachineId: string): boolean;
|
|
120
|
+
/** Backoff duration after losing CAS contention. */
|
|
121
|
+
get backoffMs(): number;
|
|
122
|
+
/** Expose config for callers that derive timers from it. */
|
|
123
|
+
get ttlMs(): number;
|
|
124
|
+
get presumedDeadThresholdMs(): number;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=FencedLease.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FencedLease.d.ts","sourceRoot":"","sources":["../../src/core/FencedLease.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FencedLease — the single coordination primitive for cross-machine
|
|
3
|
+
* seamlessness: "exactly one holder, safe under clock skew and partition."
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §6.
|
|
6
|
+
*
|
|
7
|
+
* This module is the PURE LOGIC of the lease — epoch arithmetic, signing,
|
|
8
|
+
* expiry, the fencing check, tunnel-message acceptance, and the acquisition
|
|
9
|
+
* decision. The transport (git push-or-reject-reread, tunnel broadcast) is
|
|
10
|
+
* driven by a higher-level coordinator that calls these methods; keeping the
|
|
11
|
+
* logic pure makes the dangerous parts (CAS, fencing, clock-skew) unit-testable
|
|
12
|
+
* with in-memory fakes.
|
|
13
|
+
*
|
|
14
|
+
* Authority is the EPOCH (a monotonically increasing integer advanced once per
|
|
15
|
+
* acquisition), never wall-clock time. A machine with a fast clock cannot win
|
|
16
|
+
* anything — it has no way to advance the epoch without a valid CAS. `lastSeen`
|
|
17
|
+
* is retained only for the liveness heuristic (presumed-dead), and that
|
|
18
|
+
* threshold must exceed worst-case NTP drift by ≥2× (enforced by the caller).
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_CAS_MAX_RETRIES = 5;
|
|
21
|
+
export class FencedLease {
|
|
22
|
+
crypto;
|
|
23
|
+
leaseTtlMs;
|
|
24
|
+
failoverThresholdMs;
|
|
25
|
+
casMaxRetries;
|
|
26
|
+
constructor(crypto, config) {
|
|
27
|
+
this.crypto = crypto;
|
|
28
|
+
this.leaseTtlMs = config.leaseTtlMs;
|
|
29
|
+
this.failoverThresholdMs = config.failoverThresholdMs;
|
|
30
|
+
this.casMaxRetries = config.casMaxRetries ?? DEFAULT_CAS_MAX_RETRIES;
|
|
31
|
+
}
|
|
32
|
+
get selfMachineId() {
|
|
33
|
+
return this.crypto.selfMachineId;
|
|
34
|
+
}
|
|
35
|
+
// ── Canonical serialization + signing ─────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* Stable, field-ordered serialization of the signable lease fields. The
|
|
38
|
+
* signature covers exactly these so a holder cannot later be impersonated
|
|
39
|
+
* by re-ordering fields or smuggling extra keys.
|
|
40
|
+
*/
|
|
41
|
+
static canonicalize(lease) {
|
|
42
|
+
return JSON.stringify([lease.holder, lease.epoch, lease.acquiredAt, lease.expiresAt, lease.nonce]);
|
|
43
|
+
}
|
|
44
|
+
/** Build + sign a lease record naming THIS machine as holder. */
|
|
45
|
+
signLease(epoch, acquiredAtIso, expiresAtIso, nonce) {
|
|
46
|
+
const base = {
|
|
47
|
+
holder: this.crypto.selfMachineId,
|
|
48
|
+
epoch,
|
|
49
|
+
acquiredAt: acquiredAtIso,
|
|
50
|
+
expiresAt: expiresAtIso,
|
|
51
|
+
nonce,
|
|
52
|
+
};
|
|
53
|
+
const signature = this.crypto.sign(FencedLease.canonicalize(base));
|
|
54
|
+
return { ...base, signature };
|
|
55
|
+
}
|
|
56
|
+
/** Verify a lease's signature against its claimed holder's registered key. */
|
|
57
|
+
verifyLease(lease) {
|
|
58
|
+
if (!lease || typeof lease.holder !== 'string' || typeof lease.epoch !== 'number')
|
|
59
|
+
return false;
|
|
60
|
+
return this.crypto.verify(FencedLease.canonicalize(lease), lease.signature, lease.holder);
|
|
61
|
+
}
|
|
62
|
+
// ── Expiry + epoch ────────────────────────────────────────────────
|
|
63
|
+
/** True if the lease's holder-local expiry has passed. */
|
|
64
|
+
isExpired(lease, nowMs) {
|
|
65
|
+
if (!lease)
|
|
66
|
+
return true;
|
|
67
|
+
const expMs = Date.parse(lease.expiresAt);
|
|
68
|
+
if (Number.isNaN(expMs))
|
|
69
|
+
return true;
|
|
70
|
+
return nowMs >= expMs;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The epoch the fencing check uses: max(tunnel-observed, git-committed).
|
|
74
|
+
* A fast tunnel copy can ACCELERATE acquisition but can never lower the
|
|
75
|
+
* observed epoch below what git has already committed (spec §6).
|
|
76
|
+
*/
|
|
77
|
+
static effectiveEpoch(tunnelEpoch, gitEpoch) {
|
|
78
|
+
return Math.max(tunnelEpoch | 0, gitEpoch | 0);
|
|
79
|
+
}
|
|
80
|
+
// ── Fencing check (before EVERY awake-only action) ────────────────
|
|
81
|
+
/**
|
|
82
|
+
* Does THIS machine hold the lease at the current effective epoch? Used to
|
|
83
|
+
* gate ingress polls, scheduler ticks, outbound sends, and authority-bearing
|
|
84
|
+
* registry writes. A wedged old-awake whose lease moved on fails this and is
|
|
85
|
+
* fenced out (spec §6 "Fencing check").
|
|
86
|
+
*/
|
|
87
|
+
holdsValidLease(lease, effectiveEpoch, nowMs) {
|
|
88
|
+
if (!lease)
|
|
89
|
+
return false;
|
|
90
|
+
if (lease.holder !== this.crypto.selfMachineId)
|
|
91
|
+
return false;
|
|
92
|
+
// The lease we hold must be AT the current effective epoch — a stale-epoch
|
|
93
|
+
// lease (someone advanced past us) does not grant authority.
|
|
94
|
+
if (lease.epoch !== effectiveEpoch)
|
|
95
|
+
return false;
|
|
96
|
+
if (this.isExpired(lease, nowMs))
|
|
97
|
+
return false;
|
|
98
|
+
return this.verifyLease(lease);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Should an action stamped with `stampedEpoch` be honored, given the current
|
|
102
|
+
* effective epoch? Any consumer (registry, outbox, channel-send) rejects
|
|
103
|
+
* actions stamped with a stale epoch (spec §6). Late writes/sends from a
|
|
104
|
+
* fenced old-awake carry an old epoch and are dropped.
|
|
105
|
+
*/
|
|
106
|
+
static isStampCurrent(stampedEpoch, effectiveEpoch) {
|
|
107
|
+
return stampedEpoch === effectiveEpoch;
|
|
108
|
+
}
|
|
109
|
+
// ── Tunnel lease acceptance (replay + floor guard) ────────────────
|
|
110
|
+
/**
|
|
111
|
+
* Decide whether to accept a lease record observed over the tunnel. Accept
|
|
112
|
+
* ONLY if: the signature verifies, the epoch is ≥ the git-committed floor
|
|
113
|
+
* (a below-floor message can neither trick a standby into believing a stale
|
|
114
|
+
* lease is current nor suppress a legitimate acquisition), AND the per-holder
|
|
115
|
+
* nonce is strictly greater than the last seen for that holder (replay guard).
|
|
116
|
+
*/
|
|
117
|
+
acceptTunnelLease(msg, gitCommittedEpoch, lastNonceByHolder) {
|
|
118
|
+
if (!this.verifyLease(msg)) {
|
|
119
|
+
return { accept: false, reason: 'signature-invalid-or-unknown-holder' };
|
|
120
|
+
}
|
|
121
|
+
if (msg.epoch < gitCommittedEpoch) {
|
|
122
|
+
return { accept: false, reason: `below-git-floor (msg epoch ${msg.epoch} < committed ${gitCommittedEpoch})` };
|
|
123
|
+
}
|
|
124
|
+
const lastNonce = lastNonceByHolder[msg.holder] ?? -1;
|
|
125
|
+
if (msg.nonce <= lastNonce) {
|
|
126
|
+
return { accept: false, reason: `replayed-or-stale-nonce (${msg.nonce} <= ${lastNonce})` };
|
|
127
|
+
}
|
|
128
|
+
return { accept: true, reason: 'accepted' };
|
|
129
|
+
}
|
|
130
|
+
// ── Acquisition decision (CAS candidate) ──────────────────────────
|
|
131
|
+
/**
|
|
132
|
+
* May this machine attempt to acquire the lease right now? True when:
|
|
133
|
+
* - there is no current lease, OR
|
|
134
|
+
* - the current lease is expired, OR
|
|
135
|
+
* - the current holder is presumed dead (caller passes the set, derived
|
|
136
|
+
* from lastSeen > failoverThresholdMs — never from a raw clock compare).
|
|
137
|
+
* A genuinely-live contended lease is resolved by the CAS itself (only one
|
|
138
|
+
* epoch advance wins); this method only decides whether to try.
|
|
139
|
+
*/
|
|
140
|
+
canAcquire(currentLease, presumedDeadHolders, nowMs) {
|
|
141
|
+
if (!currentLease)
|
|
142
|
+
return { can: true, reason: 'no-current-lease' };
|
|
143
|
+
if (this.isExpired(currentLease, nowMs))
|
|
144
|
+
return { can: true, reason: 'current-lease-expired' };
|
|
145
|
+
if (currentLease.holder === this.crypto.selfMachineId) {
|
|
146
|
+
return { can: true, reason: 'self-renew' };
|
|
147
|
+
}
|
|
148
|
+
if (presumedDeadHolders.has(currentLease.holder)) {
|
|
149
|
+
return { can: true, reason: `holder-presumed-dead (${currentLease.holder})` };
|
|
150
|
+
}
|
|
151
|
+
return { can: false, reason: `held-by-live-peer (${currentLease.holder})` };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Build the CAS candidate that advances the epoch by exactly one. Acquisition
|
|
155
|
+
* may only ever write `epoch = currentEpoch + 1`; epoch GAPS are explicitly
|
|
156
|
+
* safe (a higher epoch observed on re-read always wins). The expiry is
|
|
157
|
+
* holder-local (now + TTL), used for liveness/display, never for authority.
|
|
158
|
+
*/
|
|
159
|
+
buildAcquisition(currentLease, nowMs, nonce) {
|
|
160
|
+
const currentEpoch = currentLease?.epoch ?? 0;
|
|
161
|
+
const acquiredAt = new Date(nowMs).toISOString();
|
|
162
|
+
const expiresAt = new Date(nowMs + this.leaseTtlMs).toISOString();
|
|
163
|
+
return this.signLease(currentEpoch + 1, acquiredAt, expiresAt, nonce);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* After CAS exhaustion (bounded retry), should THIS machine back off to let
|
|
167
|
+
* the other side land (livelock prevention, spec §6)? The deterministic rule:
|
|
168
|
+
* the higher unforgeable machineId backs off for one lease-TTL, guaranteeing
|
|
169
|
+
* one side wins. Below the retry cap, no backoff (keep contending).
|
|
170
|
+
*/
|
|
171
|
+
shouldBackoffAfterContention(retryCount, contenderMachineId) {
|
|
172
|
+
if (retryCount < this.casMaxRetries)
|
|
173
|
+
return false;
|
|
174
|
+
// Higher machineId yields; lower machineId keeps trying → exactly one lands.
|
|
175
|
+
return this.crypto.selfMachineId > contenderMachineId;
|
|
176
|
+
}
|
|
177
|
+
/** Backoff duration after losing CAS contention. */
|
|
178
|
+
get backoffMs() {
|
|
179
|
+
return this.leaseTtlMs;
|
|
180
|
+
}
|
|
181
|
+
/** Expose config for callers that derive timers from it. */
|
|
182
|
+
get ttlMs() {
|
|
183
|
+
return this.leaseTtlMs;
|
|
184
|
+
}
|
|
185
|
+
get presumedDeadThresholdMs() {
|
|
186
|
+
return this.failoverThresholdMs;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=FencedLease.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FencedLease.js","sourceRoot":"","sources":["../../src/core/FencedLease.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAoCH,MAAM,uBAAuB,GAAG,CAAC,CAAC;AAElC,MAAM,OAAO,WAAW;IACL,MAAM,CAAc;IACpB,UAAU,CAAS;IACnB,mBAAmB,CAAS;IAC5B,aAAa,CAAS;IAEvC,YAAY,MAAmB,EAAE,MAAyB;QACxD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;QACpC,IAAI,CAAC,mBAAmB,GAAG,MAAM,CAAC,mBAAmB,CAAC;QACtD,IAAI,CAAC,aAAa,GAAG,MAAM,CAAC,aAAa,IAAI,uBAAuB,CAAC;IACvE,CAAC;IAED,IAAI,aAAa;QACf,OAAO,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC;IACnC,CAAC;IAED,qEAAqE;IAErE;;;;OAIG;IACH,MAAM,CAAC,YAAY,CAAC,KAAmF;QACrG,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IACrG,CAAC;IAED,iEAAiE;IACjE,SAAS,CAAC,KAAa,EAAE,aAAqB,EAAE,YAAoB,EAAE,KAAa;QACjF,MAAM,IAAI,GAAG;YACX,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,aAAa;YACjC,KAAK;YACL,UAAU,EAAE,aAAa;YACzB,SAAS,EAAE,YAAY;YACvB,KAAK;SACN,CAAC;QACF,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;QACnE,OAAO,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,CAAC;IAChC,CAAC;IAED,8EAA8E;IAC9E,WAAW,CAAC,KAAkB;QAC5B,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAChG,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC5F,CAAC;IAED,qEAAqE;IAErE,0DAA0D;IAC1D,SAAS,CAAC,KAAqC,EAAE,KAAa;QAC5D,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACxB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC1C,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACrC,OAAO,KAAK,IAAI,KAAK,CAAC;IACxB,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,cAAc,CAAC,WAAmB,EAAE,QAAgB;QACzD,OAAO,IAAI,CAAC,GAAG,CAAC,WAAW,GAAG,CAAC,EAAE,QAAQ,GAAG,CAAC,CAAC,CAAC;IACjD,CAAC;IAED,qEAAqE;IAErE;;;;;OAKG;IACH,eAAe,CAAC,KAAqC,EAAE,cAAsB,EAAE,KAAa;QAC1F,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAC;QACzB,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,CAAC,aAAa;YAAE,OAAO,KAAK,CAAC;QAC7D,2EAA2E;QAC3E,6DAA6D;QAC7D,IAAI,KAAK,CAAC,KAAK,KAAK,cAAc;YAAE,OAAO,KAAK,CAAC;QACjD,IAAI,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QAC/C,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,cAAc,CAAC,YAAoB,EAAE,cAAsB;QAChE,OAAO,YAAY,KAAK,cAAc,CAAC;IACzC,CAAC;IAED,qEAAqE;IAErE;;;;;;OAMG;IACH,iBAAiB,CACf,GAAgB,EAChB,iBAAyB,EACzB,iBAAyC;QAEzC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3B,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,qCAAqC,EAAE,CAAC;QAC1E,CAAC;QACD,IAAI,GAAG,CAAC,KAAK,GAAG,iBAAiB,EAAE,CAAC;YAClC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,8BAA8B,GAAG,CAAC,KAAK,gBAAgB,iBAAiB,GAAG,EAAE,CAAC;QAChH,CAAC;QACD,MAAM,SAAS,GAAG,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QACtD,IAAI,GAAG,CAAC,KAAK,IAAI,SAAS,EAAE,CAAC;YAC3B,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,4BAA4B,GAAG,CAAC,KAAK,OAAO,SAAS,GAAG,EAAE,CAAC;QAC7F,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAC9C,CAAC;IAED,qEAAqE;IAErE;;;;;;;;OAQG;IACH,UAAU,CACR,YAA4C,EAC5C,mBAAwC,EACxC,KAAa;QAEb,IAAI,CAAC,YAAY;YAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;QACpE,IAAI,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,KAAK,CAAC;YAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,uBAAuB,EAAE,CAAC;QAC/F,IAAI,YAAY,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;YACtD,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;QAC7C,CAAC;QACD,IAAI,mBAAmB,CAAC,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC;YACjD,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,yBAAyB,YAAY,CAAC,MAAM,GAAG,EAAE,CAAC;QAChF,CAAC;QACD,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,sBAAsB,YAAY,CAAC,MAAM,GAAG,EAAE,CAAC;IAC9E,CAAC;IAED;;;;;OAKG;IACH,gBAAgB,CAAC,YAA4C,EAAE,KAAa,EAAE,KAAa;QACzF,MAAM,YAAY,GAAG,YAAY,EAAE,KAAK,IAAI,CAAC,CAAC;QAC9C,MAAM,UAAU,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QACjD,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,WAAW,EAAE,CAAC;QAClE,OAAO,IAAI,CAAC,SAAS,CAAC,YAAY,GAAG,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;IACxE,CAAC;IAED;;;;;OAKG;IACH,4BAA4B,CAAC,UAAkB,EAAE,kBAA0B;QACzE,IAAI,UAAU,GAAG,IAAI,CAAC,aAAa;YAAE,OAAO,KAAK,CAAC;QAClD,6EAA6E;QAC7E,OAAO,IAAI,CAAC,MAAM,CAAC,aAAa,GAAG,kBAAkB,CAAC;IACxD,CAAC;IAED,oDAAoD;IACpD,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAED,4DAA4D;IAC5D,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IACD,IAAI,uBAAuB;QACzB,OAAO,IAAI,CAAC,mBAAmB,CAAC;IAClC,CAAC;CACF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitLeaseStore — the durable (git-backed) LeaseStore for LeaseCoordinator.
|
|
3
|
+
*
|
|
4
|
+
* Implements the lease CAS over git: git has no native compare-and-swap, so we
|
|
5
|
+
* do optimistic concurrency — pull-rebase to minimize the reject window, re-read
|
|
6
|
+
* the committed epoch, refuse to write unless our candidate strictly advances
|
|
7
|
+
* it, then commit+push. A non-fast-forward push (a peer advanced first) returns
|
|
8
|
+
* ok:false with the freshly-observed lease so LeaseCoordinator re-evaluates and
|
|
9
|
+
* either yields or retries with a higher epoch. We NEVER force-push.
|
|
10
|
+
*
|
|
11
|
+
* The write also bumps the holder's own registry entry (syncSequence +
|
|
12
|
+
* authoredUnderEpoch) so the lease commit passes peers' registryReplayGuard.
|
|
13
|
+
*/
|
|
14
|
+
import type { LeaseStore } from './LeaseCoordinator.js';
|
|
15
|
+
import type { LeaseRecord, MachineRegistry } from './types.js';
|
|
16
|
+
export interface GitLeaseStoreDeps {
|
|
17
|
+
/** This machine's id (the only entry we bump syncSequence on). */
|
|
18
|
+
machineId: string;
|
|
19
|
+
loadRegistry: () => MachineRegistry;
|
|
20
|
+
saveRegistry: (r: MachineRegistry) => void;
|
|
21
|
+
/** Absolute path of the registry file to stage. */
|
|
22
|
+
registryAbsPath: string;
|
|
23
|
+
/** Pull-rebase (best-effort) before reading/writing. */
|
|
24
|
+
pullRebase: () => boolean;
|
|
25
|
+
/** Commit + push the registry; false on non-ff/no-op. */
|
|
26
|
+
commitAndPush: (message: string, paths: string[]) => boolean;
|
|
27
|
+
logger?: (msg: string) => void;
|
|
28
|
+
}
|
|
29
|
+
export declare class GitLeaseStore implements LeaseStore {
|
|
30
|
+
private readonly d;
|
|
31
|
+
constructor(deps: GitLeaseStoreDeps);
|
|
32
|
+
private log;
|
|
33
|
+
read(): {
|
|
34
|
+
lease: LeaseRecord | null;
|
|
35
|
+
epoch: number;
|
|
36
|
+
};
|
|
37
|
+
casWrite(candidate: LeaseRecord): {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
observed: {
|
|
40
|
+
lease: LeaseRecord | null;
|
|
41
|
+
epoch: number;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
refresh(lease: LeaseRecord): boolean;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=GitLeaseStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GitLeaseStore.d.ts","sourceRoot":"","sources":["../../src/core/GitLeaseStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE/D,MAAM,WAAW,iBAAiB;IAChC,kEAAkE;IAClE,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,eAAe,CAAC;IACpC,YAAY,EAAE,CAAC,CAAC,EAAE,eAAe,KAAK,IAAI,CAAC;IAC3C,mDAAmD;IACnD,eAAe,EAAE,MAAM,CAAC;IACxB,wDAAwD;IACxD,UAAU,EAAE,MAAM,OAAO,CAAC;IAC1B,yDAAyD;IACzD,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC;IAC7D,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAED,qBAAa,aAAc,YAAW,UAAU;IAC9C,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAoB;gBAE1B,IAAI,EAAE,iBAAiB;IAInC,OAAO,CAAC,GAAG;IAIX,IAAI,IAAI;QAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE;IAKpD,QAAQ,CAAC,SAAS,EAAE,WAAW,GAAG;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE;YAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE;IAyCzG,OAAO,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO;CAmBrC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitLeaseStore — the durable (git-backed) LeaseStore for LeaseCoordinator.
|
|
3
|
+
*
|
|
4
|
+
* Implements the lease CAS over git: git has no native compare-and-swap, so we
|
|
5
|
+
* do optimistic concurrency — pull-rebase to minimize the reject window, re-read
|
|
6
|
+
* the committed epoch, refuse to write unless our candidate strictly advances
|
|
7
|
+
* it, then commit+push. A non-fast-forward push (a peer advanced first) returns
|
|
8
|
+
* ok:false with the freshly-observed lease so LeaseCoordinator re-evaluates and
|
|
9
|
+
* either yields or retries with a higher epoch. We NEVER force-push.
|
|
10
|
+
*
|
|
11
|
+
* The write also bumps the holder's own registry entry (syncSequence +
|
|
12
|
+
* authoredUnderEpoch) so the lease commit passes peers' registryReplayGuard.
|
|
13
|
+
*/
|
|
14
|
+
export class GitLeaseStore {
|
|
15
|
+
d;
|
|
16
|
+
constructor(deps) {
|
|
17
|
+
this.d = deps;
|
|
18
|
+
}
|
|
19
|
+
log(m) {
|
|
20
|
+
this.d.logger?.(`[git-lease] ${m}`);
|
|
21
|
+
}
|
|
22
|
+
read() {
|
|
23
|
+
const reg = this.d.loadRegistry();
|
|
24
|
+
return { lease: reg.lease ?? null, epoch: reg.lease?.epoch ?? 0 };
|
|
25
|
+
}
|
|
26
|
+
casWrite(candidate) {
|
|
27
|
+
// 1. Pull-rebase to shrink the reject window, then re-read the committed epoch.
|
|
28
|
+
this.d.pullRebase();
|
|
29
|
+
let reg = this.d.loadRegistry();
|
|
30
|
+
const committedEpoch = reg.lease?.epoch ?? 0;
|
|
31
|
+
// 2. CAS pre-check: only a strict +1 advance over the committed epoch is valid.
|
|
32
|
+
if (candidate.epoch <= committedEpoch) {
|
|
33
|
+
this.log(`CAS pre-check failed: candidate epoch ${candidate.epoch} <= committed ${committedEpoch}`);
|
|
34
|
+
return { ok: false, observed: { lease: reg.lease ?? null, epoch: committedEpoch } };
|
|
35
|
+
}
|
|
36
|
+
// 3. Write the lease + bump the holder's own freshness fields so peers'
|
|
37
|
+
// replay guard accepts it.
|
|
38
|
+
reg.lease = candidate;
|
|
39
|
+
const entry = reg.machines[this.d.machineId];
|
|
40
|
+
if (entry) {
|
|
41
|
+
entry.syncSequence = (entry.syncSequence ?? 0) + 1;
|
|
42
|
+
entry.authoredUnderEpoch = candidate.epoch;
|
|
43
|
+
}
|
|
44
|
+
this.d.saveRegistry(reg);
|
|
45
|
+
// 4. Commit + push. On success the CAS landed.
|
|
46
|
+
const pushed = this.d.commitAndPush(`chore(mesh): lease epoch ${candidate.epoch} → ${candidate.holder}`, [this.d.registryAbsPath]);
|
|
47
|
+
if (pushed) {
|
|
48
|
+
this.log(`lease epoch ${candidate.epoch} committed + pushed`);
|
|
49
|
+
return { ok: true, observed: { lease: candidate, epoch: candidate.epoch } };
|
|
50
|
+
}
|
|
51
|
+
// 5. Push rejected (a peer advanced) or no-op. Re-read after a pull and
|
|
52
|
+
// report the observed state so the caller re-evaluates / retries.
|
|
53
|
+
this.d.pullRebase();
|
|
54
|
+
reg = this.d.loadRegistry();
|
|
55
|
+
const observedEpoch = reg.lease?.epoch ?? 0;
|
|
56
|
+
this.log(`push rejected/no-op; observed epoch now ${observedEpoch}`);
|
|
57
|
+
return { ok: false, observed: { lease: reg.lease ?? null, epoch: observedEpoch } };
|
|
58
|
+
}
|
|
59
|
+
refresh(lease) {
|
|
60
|
+
// Pull first; if a peer has advanced past our epoch we've been superseded —
|
|
61
|
+
// do NOT overwrite a higher epoch with a same-epoch refresh.
|
|
62
|
+
this.d.pullRebase();
|
|
63
|
+
const reg = this.d.loadRegistry();
|
|
64
|
+
const committedEpoch = reg.lease?.epoch ?? 0;
|
|
65
|
+
if (committedEpoch > lease.epoch) {
|
|
66
|
+
this.log(`refresh declined: superseded (committed ${committedEpoch} > ${lease.epoch})`);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
reg.lease = lease; // same epoch, fresh expiry + nonce
|
|
70
|
+
const entry = reg.machines[this.d.machineId];
|
|
71
|
+
if (entry) {
|
|
72
|
+
entry.syncSequence = (entry.syncSequence ?? 0) + 1;
|
|
73
|
+
entry.authoredUnderEpoch = lease.epoch;
|
|
74
|
+
}
|
|
75
|
+
this.d.saveRegistry(reg);
|
|
76
|
+
return this.d.commitAndPush(`chore(mesh): lease renew epoch ${lease.epoch}`, [this.d.registryAbsPath]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=GitLeaseStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GitLeaseStore.js","sourceRoot":"","sources":["../../src/core/GitLeaseStore.ts"],"names":[],"mappings":"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"}
|
package/dist/core/GitSync.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import type { MachineIdentityManager } from './MachineIdentity.js';
|
|
14
14
|
import type { SecurityLog } from './SecurityLog.js';
|
|
15
15
|
import type { IntelligenceProvider } from './types.js';
|
|
16
|
+
import type { MachineRegistry } from './types.js';
|
|
16
17
|
export interface GitSyncConfig {
|
|
17
18
|
/** Project directory (repo root). */
|
|
18
19
|
projectDir: string;
|
|
@@ -103,6 +104,24 @@ export declare class GitSyncManager {
|
|
|
103
104
|
* Full sync: pull → verify → resolve → push.
|
|
104
105
|
*/
|
|
105
106
|
sync(): Promise<SyncResult>;
|
|
107
|
+
/**
|
|
108
|
+
* §8 G2 — reconcile a freshly-pulled registry against the pre-pull snapshot,
|
|
109
|
+
* scrubbing any entry that fails the replay/freshness or unknown-key guard.
|
|
110
|
+
* Signed-commit verification (verifyPulledCommits) already rejected unsigned
|
|
111
|
+
* or revoked COMMITS; this rejects stale/unauthorized registry ENTRIES that a
|
|
112
|
+
* valid signature alone would let through (e.g. a re-keyed machine whose
|
|
113
|
+
* per-author sequence reset, or an unknown key asserting an awake role). The
|
|
114
|
+
* lease object itself is reconciled by the FencedLease layer (epoch CAS), not
|
|
115
|
+
* here. Returns the rejected machineIds (for the caller's diagnostics/tests).
|
|
116
|
+
*/
|
|
117
|
+
reconcilePulledRegistry(registryBefore: MachineRegistry): string[];
|
|
118
|
+
/**
|
|
119
|
+
* Targeted pull-rebase (no auto-commit). Used by the lease CAS to minimize
|
|
120
|
+
* the push-reject window before writing a lease candidate. Returns true if
|
|
121
|
+
* the pull succeeded (HEAD may or may not have moved). Best-effort: returns
|
|
122
|
+
* false on any error rather than throwing.
|
|
123
|
+
*/
|
|
124
|
+
pullRebase(): boolean;
|
|
106
125
|
/**
|
|
107
126
|
* Stage files and commit with machine signing.
|
|
108
127
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GitSync.d.ts","sourceRoot":"","sources":["../../src/core/GitSync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"GitSync.d.ts","sourceRoot":"","sources":["../../src/core/GitSync.ts"],"names":[],"mappings":"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"}
|
package/dist/core/GitSync.js
CHANGED
|
@@ -17,6 +17,7 @@ import { LLMConflictResolver } from './LLMConflictResolver.js';
|
|
|
17
17
|
import { FileClassifier } from './FileClassifier.js';
|
|
18
18
|
import { DegradationReporter } from '../monitoring/DegradationReporter.js';
|
|
19
19
|
import { assertNotInstarSourceTree } from './SourceTreeGuard.js';
|
|
20
|
+
import { reconcileRegistryEntries } from './registryReplayGuard.js';
|
|
20
21
|
import { SafeFsExecutor } from './SafeFsExecutor.js';
|
|
21
22
|
import { SafeGitExecutor } from './SafeGitExecutor.js';
|
|
22
23
|
// ── Git Sync Manager ─────────────────────────────────────────────────
|
|
@@ -127,6 +128,15 @@ export class GitSyncManager {
|
|
|
127
128
|
// No git repo — return clean no-op (standalone agent without git backup)
|
|
128
129
|
if (!this.isGitRepo())
|
|
129
130
|
return result;
|
|
131
|
+
// Cross-Machine Seamlessness §8 G2 — snapshot the registry BEFORE the pull
|
|
132
|
+
// so we can apply the replay/freshness + unknown-key guard to whatever the
|
|
133
|
+
// pull brings in (a clean fast-forward can still deliver a stale or
|
|
134
|
+
// unknown-key registry that signed-commit verification alone won't catch).
|
|
135
|
+
let registryBefore = null;
|
|
136
|
+
try {
|
|
137
|
+
registryBefore = this.identityManager.loadRegistry();
|
|
138
|
+
}
|
|
139
|
+
catch { /* @silent-fallback-ok — reconcile skipped if snapshot unreadable */ }
|
|
130
140
|
// 0. Pre-flight: detect and fix stuck rebase / detached HEAD before attempting pull
|
|
131
141
|
let rebaseJustAborted = false;
|
|
132
142
|
try {
|
|
@@ -353,6 +363,16 @@ export class GitSyncManager {
|
|
|
353
363
|
// 2. Verify pulled commits
|
|
354
364
|
if (result.pulled) {
|
|
355
365
|
result.rejectedCommits = this.verifyPulledCommits();
|
|
366
|
+
// §8 G2 — apply the replay/freshness + unknown-key guard to the pulled
|
|
367
|
+
// registry, scrubbing any stale or unauthorized entries before we trust it.
|
|
368
|
+
if (registryBefore) {
|
|
369
|
+
try {
|
|
370
|
+
this.reconcilePulledRegistry(registryBefore);
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
console.warn(`[GitSync] registry reconcile warning: ${err instanceof Error ? err.message : String(err)}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
356
376
|
}
|
|
357
377
|
// 3. Push pending changes
|
|
358
378
|
if (this.autoPush) {
|
|
@@ -377,6 +397,63 @@ export class GitSyncManager {
|
|
|
377
397
|
});
|
|
378
398
|
return result;
|
|
379
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* §8 G2 — reconcile a freshly-pulled registry against the pre-pull snapshot,
|
|
402
|
+
* scrubbing any entry that fails the replay/freshness or unknown-key guard.
|
|
403
|
+
* Signed-commit verification (verifyPulledCommits) already rejected unsigned
|
|
404
|
+
* or revoked COMMITS; this rejects stale/unauthorized registry ENTRIES that a
|
|
405
|
+
* valid signature alone would let through (e.g. a re-keyed machine whose
|
|
406
|
+
* per-author sequence reset, or an unknown key asserting an awake role). The
|
|
407
|
+
* lease object itself is reconciled by the FencedLease layer (epoch CAS), not
|
|
408
|
+
* here. Returns the rejected machineIds (for the caller's diagnostics/tests).
|
|
409
|
+
*/
|
|
410
|
+
reconcilePulledRegistry(registryBefore) {
|
|
411
|
+
const afterPull = this.identityManager.loadRegistry();
|
|
412
|
+
const committedEpoch = registryBefore.lease?.epoch ?? 0;
|
|
413
|
+
const { accepted, rejected } = reconcileRegistryEntries({
|
|
414
|
+
localEntries: registryBefore.machines ?? {},
|
|
415
|
+
incomingEntries: afterPull.machines ?? {},
|
|
416
|
+
currentCommittedEpoch: committedEpoch,
|
|
417
|
+
});
|
|
418
|
+
if (rejected.length === 0)
|
|
419
|
+
return [];
|
|
420
|
+
// Rebuild a clean registry: start from the trusted pre-pull entries, then
|
|
421
|
+
// overlay the accepted incoming entries. Rejected entries fall back to the
|
|
422
|
+
// pre-pull value (or are omitted if they were never known locally).
|
|
423
|
+
const merged = { ...(registryBefore.machines ?? {}) };
|
|
424
|
+
for (const [id, entry] of Object.entries(accepted))
|
|
425
|
+
merged[id] = entry;
|
|
426
|
+
afterPull.machines = merged;
|
|
427
|
+
this.identityManager.saveRegistry(afterPull);
|
|
428
|
+
for (const r of rejected) {
|
|
429
|
+
this.securityLog.append({
|
|
430
|
+
event: 'registry_entry_rejected',
|
|
431
|
+
machineId: this.machineId,
|
|
432
|
+
rejectedMachine: r.machineId,
|
|
433
|
+
reason: r.reason,
|
|
434
|
+
});
|
|
435
|
+
console.warn(`[GitSync] Rejected pulled registry entry for ${r.machineId}: ${r.reason}`);
|
|
436
|
+
}
|
|
437
|
+
return rejected.map((r) => r.machineId);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Targeted pull-rebase (no auto-commit). Used by the lease CAS to minimize
|
|
441
|
+
* the push-reject window before writing a lease candidate. Returns true if
|
|
442
|
+
* the pull succeeded (HEAD may or may not have moved). Best-effort: returns
|
|
443
|
+
* false on any error rather than throwing.
|
|
444
|
+
*/
|
|
445
|
+
pullRebase() {
|
|
446
|
+
if (!this.isGitRepo())
|
|
447
|
+
return false;
|
|
448
|
+
try {
|
|
449
|
+
this.gitExec(['pull', '--rebase', '--autostash']);
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
// @silent-fallback-ok — lease CAS re-reads and retries on failure
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
380
457
|
/**
|
|
381
458
|
* Stage files and commit with machine signing.
|
|
382
459
|
*/
|