instar 1.3.18 → 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.
Files changed (95) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +104 -6
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/FencedLease.d.ts +126 -0
  5. package/dist/core/FencedLease.d.ts.map +1 -0
  6. package/dist/core/FencedLease.js +189 -0
  7. package/dist/core/FencedLease.js.map +1 -0
  8. package/dist/core/GitLeaseStore.d.ts +46 -0
  9. package/dist/core/GitLeaseStore.d.ts.map +1 -0
  10. package/dist/core/GitLeaseStore.js +79 -0
  11. package/dist/core/GitLeaseStore.js.map +1 -0
  12. package/dist/core/GitSync.d.ts +19 -0
  13. package/dist/core/GitSync.d.ts.map +1 -1
  14. package/dist/core/GitSync.js +77 -0
  15. package/dist/core/GitSync.js.map +1 -1
  16. package/dist/core/HandoffSentinel.d.ts +80 -0
  17. package/dist/core/HandoffSentinel.d.ts.map +1 -0
  18. package/dist/core/HandoffSentinel.js +141 -0
  19. package/dist/core/HandoffSentinel.js.map +1 -0
  20. package/dist/core/LeaseCoordinator.d.ts +141 -0
  21. package/dist/core/LeaseCoordinator.d.ts.map +1 -0
  22. package/dist/core/LeaseCoordinator.js +226 -0
  23. package/dist/core/LeaseCoordinator.js.map +1 -0
  24. package/dist/core/LiveTailBuffer.d.ts +64 -0
  25. package/dist/core/LiveTailBuffer.d.ts.map +1 -0
  26. package/dist/core/LiveTailBuffer.js +122 -0
  27. package/dist/core/LiveTailBuffer.js.map +1 -0
  28. package/dist/core/MultiMachineCoordinator.d.ts +49 -0
  29. package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
  30. package/dist/core/MultiMachineCoordinator.js +136 -0
  31. package/dist/core/MultiMachineCoordinator.js.map +1 -1
  32. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  33. package/dist/core/PostUpdateMigrator.js +24 -0
  34. package/dist/core/PostUpdateMigrator.js.map +1 -1
  35. package/dist/core/RegistrySyncDebouncer.d.ts +77 -0
  36. package/dist/core/RegistrySyncDebouncer.d.ts.map +1 -0
  37. package/dist/core/RegistrySyncDebouncer.js +131 -0
  38. package/dist/core/RegistrySyncDebouncer.js.map +1 -0
  39. package/dist/core/liveTailRedaction.d.ts +36 -0
  40. package/dist/core/liveTailRedaction.d.ts.map +1 -0
  41. package/dist/core/liveTailRedaction.js +57 -0
  42. package/dist/core/liveTailRedaction.js.map +1 -0
  43. package/dist/core/registryReplayGuard.d.ts +59 -0
  44. package/dist/core/registryReplayGuard.d.ts.map +1 -0
  45. package/dist/core/registryReplayGuard.js +86 -0
  46. package/dist/core/registryReplayGuard.js.map +1 -0
  47. package/dist/core/seamlessnessConfig.d.ts +79 -0
  48. package/dist/core/seamlessnessConfig.d.ts.map +1 -0
  49. package/dist/core/seamlessnessConfig.js +131 -0
  50. package/dist/core/seamlessnessConfig.js.map +1 -0
  51. package/dist/core/types.d.ts +117 -0
  52. package/dist/core/types.d.ts.map +1 -1
  53. package/dist/core/types.js.map +1 -1
  54. package/dist/core/wireRegistrySync.d.ts +32 -0
  55. package/dist/core/wireRegistrySync.d.ts.map +1 -0
  56. package/dist/core/wireRegistrySync.js +31 -0
  57. package/dist/core/wireRegistrySync.js.map +1 -0
  58. package/dist/feedback-factory/dispatch/dispatch.d.ts +47 -0
  59. package/dist/feedback-factory/dispatch/dispatch.d.ts.map +1 -0
  60. package/dist/feedback-factory/dispatch/dispatch.js +72 -0
  61. package/dist/feedback-factory/dispatch/dispatch.js.map +1 -0
  62. package/dist/messaging/FencedOutbox.d.ts +42 -0
  63. package/dist/messaging/FencedOutbox.d.ts.map +1 -0
  64. package/dist/messaging/FencedOutbox.js +60 -0
  65. package/dist/messaging/FencedOutbox.js.map +1 -0
  66. package/dist/messaging/MessageProcessingLedger.d.ts +105 -0
  67. package/dist/messaging/MessageProcessingLedger.d.ts.map +1 -0
  68. package/dist/messaging/MessageProcessingLedger.js +206 -0
  69. package/dist/messaging/MessageProcessingLedger.js.map +1 -0
  70. package/dist/messaging/TelegramAdapter.d.ts +18 -1
  71. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  72. package/dist/messaging/TelegramAdapter.js +50 -0
  73. package/dist/messaging/TelegramAdapter.js.map +1 -1
  74. package/dist/scaffold/templates.d.ts.map +1 -1
  75. package/dist/scaffold/templates.js +7 -0
  76. package/dist/scaffold/templates.js.map +1 -1
  77. package/dist/server/AgentServer.d.ts.map +1 -1
  78. package/dist/server/AgentServer.js +1 -0
  79. package/dist/server/AgentServer.js.map +1 -1
  80. package/dist/server/routes.d.ts +2 -0
  81. package/dist/server/routes.d.ts.map +1 -1
  82. package/dist/server/routes.js +6 -0
  83. package/dist/server/routes.js.map +1 -1
  84. package/package.json +1 -1
  85. package/src/data/builtin-manifest.json +64 -64
  86. package/src/scaffold/templates.ts +7 -0
  87. package/upgrades/1.3.19.md +23 -0
  88. package/upgrades/side-effects/PR-BODY-cross-machine-seamlessness.md +63 -0
  89. package/upgrades/side-effects/cross-machine-seamlessness-g1-lease.md +71 -0
  90. package/upgrades/side-effects/cross-machine-seamlessness-g2-foundation.md +75 -0
  91. package/upgrades/side-effects/cross-machine-seamlessness-g3-telegram-contract.md +36 -0
  92. package/upgrades/side-effects/cross-machine-seamlessness-g3a-ledger-handoff.md +56 -0
  93. package/upgrades/side-effects/cross-machine-seamlessness-g3bc-livetail-outbox.md +51 -0
  94. package/upgrades/side-effects/cross-machine-seamlessness-p4-awareness-observability.md +54 -0
  95. package/upgrades/side-effects/feedback-factory-dispatch.md +31 -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":"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"}
@@ -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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"}
@@ -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":"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"}
1
+ {"version":3,"file":"GitSync.d.ts","sourceRoot":"","sources":["../../src/core/GitSync.ts"],"names":[],"mappings":"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"}
@@ -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
  */