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.
- 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/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.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
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RegistrySyncDebouncer — G2 automated state sync (durable half).
|
|
3
|
+
*
|
|
4
|
+
* Spec: docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §8 G2.
|
|
5
|
+
*
|
|
6
|
+
* The Phase-0 root cause: a running server changed its registry (role/lease)
|
|
7
|
+
* but NOTHING pushed that change to git, so cross-machine state only ever
|
|
8
|
+
* propagated by hand. This component closes that: a marked-dirty registry is
|
|
9
|
+
* debounced and committed+pushed to git — but ONLY the authoritative machine
|
|
10
|
+
* (lease holder / awake) writes durable authority-bearing state (single-writer,
|
|
11
|
+
* removing the O(N) thundering-herd of every machine pushing a corrected
|
|
12
|
+
* registry).
|
|
13
|
+
*
|
|
14
|
+
* This is the DURABLE path only. High-frequency liveness/heartbeat (ephemeral)
|
|
15
|
+
* never comes through here — it travels over the tunnel and stays out of git
|
|
16
|
+
* history, so steady-state healthy operation produces ~0 commits (the
|
|
17
|
+
* ephemeral-vs-durable split, spec §Design Principles).
|
|
18
|
+
*
|
|
19
|
+
* Push contention: commitAndPush performs `git push` which fails (returns
|
|
20
|
+
* false) on a non-fast-forward — we never force-push. Repeated failures past a
|
|
21
|
+
* threshold emit a sync-health signal so the lease layer can self-suspend
|
|
22
|
+
* ingress (a machine that cannot prove it is visible must not serve). The
|
|
23
|
+
* pull/rebase reconciliation itself lives in the periodic GitSync.sync cycle.
|
|
24
|
+
*/
|
|
25
|
+
export interface SyncHealthState {
|
|
26
|
+
healthy: boolean;
|
|
27
|
+
consecutiveFailures: number;
|
|
28
|
+
lastError?: string;
|
|
29
|
+
lastPushAt?: string;
|
|
30
|
+
lastAttemptAt?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface RegistrySyncDeps {
|
|
33
|
+
/** Commit + push the given paths. Returns true if a commit was pushed. */
|
|
34
|
+
commitAndPush: (message: string, paths: string[]) => boolean;
|
|
35
|
+
/** Absolute path of the registry file to stage. */
|
|
36
|
+
registryAbsPath: string;
|
|
37
|
+
/**
|
|
38
|
+
* Single-writer gate: only flush when THIS machine has authority to write
|
|
39
|
+
* durable registry state (holds the lease / is awake). A standby marking
|
|
40
|
+
* dirty is a no-op push (it still updates its own liveness elsewhere).
|
|
41
|
+
*/
|
|
42
|
+
isAuthoritative: () => boolean;
|
|
43
|
+
/** Debounce window (registrySyncDebounceMs). */
|
|
44
|
+
debounceMs: number;
|
|
45
|
+
/** Failures before the sync-health signal flips unhealthy. Default 3. */
|
|
46
|
+
maxConsecutiveFailures?: number;
|
|
47
|
+
/** Sync-health signal consumer (lease layer self-suspends ingress on unhealthy). */
|
|
48
|
+
onSyncHealth?: (s: SyncHealthState) => void;
|
|
49
|
+
logger?: (msg: string) => void;
|
|
50
|
+
now?: () => number;
|
|
51
|
+
}
|
|
52
|
+
export declare class RegistrySyncDebouncer {
|
|
53
|
+
private readonly deps;
|
|
54
|
+
private readonly maxFailures;
|
|
55
|
+
private timer;
|
|
56
|
+
private pendingReasons;
|
|
57
|
+
private flushing;
|
|
58
|
+
private health;
|
|
59
|
+
private stopped;
|
|
60
|
+
constructor(deps: RegistrySyncDeps);
|
|
61
|
+
private now;
|
|
62
|
+
private log;
|
|
63
|
+
/**
|
|
64
|
+
* Mark the registry as needing a durable push. Debounced — many rapid marks
|
|
65
|
+
* within the window coalesce into a single commit (coarse, not per-tick).
|
|
66
|
+
*/
|
|
67
|
+
markRegistryDirty(reason: string): void;
|
|
68
|
+
/**
|
|
69
|
+
* Flush immediately (also used by stop()). Returns true if a commit was
|
|
70
|
+
* pushed. A non-authoritative machine returns false without pushing
|
|
71
|
+
* (single-writer). Re-entrancy guarded.
|
|
72
|
+
*/
|
|
73
|
+
flush(): Promise<boolean>;
|
|
74
|
+
getHealth(): SyncHealthState;
|
|
75
|
+
stop(): void;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=RegistrySyncDebouncer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RegistrySyncDebouncer.d.ts","sourceRoot":"","sources":["../../src/core/RegistrySyncDebouncer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC;IAC7D,mDAAmD;IACnD,eAAe,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,eAAe,EAAE,MAAM,OAAO,CAAC;IAC/B,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,oFAAoF;IACpF,YAAY,CAAC,EAAE,CAAC,CAAC,EAAE,eAAe,KAAK,IAAI,CAAC;IAC5C,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAmB;IACxC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAA8D;IAC5E,OAAO,CAAC,OAAO,CAAS;gBAEZ,IAAI,EAAE,gBAAgB;IAKlC,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,GAAG;IAIX;;;OAGG;IACH,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAWvC;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,OAAO,CAAC;IAqD/B,SAAS,IAAI,eAAe;IAI5B,IAAI,IAAI,IAAI;CAOb"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RegistrySyncDebouncer — G2 automated state sync (durable half).
|
|
3
|
+
*
|
|
4
|
+
* Spec: docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §8 G2.
|
|
5
|
+
*
|
|
6
|
+
* The Phase-0 root cause: a running server changed its registry (role/lease)
|
|
7
|
+
* but NOTHING pushed that change to git, so cross-machine state only ever
|
|
8
|
+
* propagated by hand. This component closes that: a marked-dirty registry is
|
|
9
|
+
* debounced and committed+pushed to git — but ONLY the authoritative machine
|
|
10
|
+
* (lease holder / awake) writes durable authority-bearing state (single-writer,
|
|
11
|
+
* removing the O(N) thundering-herd of every machine pushing a corrected
|
|
12
|
+
* registry).
|
|
13
|
+
*
|
|
14
|
+
* This is the DURABLE path only. High-frequency liveness/heartbeat (ephemeral)
|
|
15
|
+
* never comes through here — it travels over the tunnel and stays out of git
|
|
16
|
+
* history, so steady-state healthy operation produces ~0 commits (the
|
|
17
|
+
* ephemeral-vs-durable split, spec §Design Principles).
|
|
18
|
+
*
|
|
19
|
+
* Push contention: commitAndPush performs `git push` which fails (returns
|
|
20
|
+
* false) on a non-fast-forward — we never force-push. Repeated failures past a
|
|
21
|
+
* threshold emit a sync-health signal so the lease layer can self-suspend
|
|
22
|
+
* ingress (a machine that cannot prove it is visible must not serve). The
|
|
23
|
+
* pull/rebase reconciliation itself lives in the periodic GitSync.sync cycle.
|
|
24
|
+
*/
|
|
25
|
+
export class RegistrySyncDebouncer {
|
|
26
|
+
deps;
|
|
27
|
+
maxFailures;
|
|
28
|
+
timer = null;
|
|
29
|
+
pendingReasons = [];
|
|
30
|
+
flushing = false;
|
|
31
|
+
health = { healthy: true, consecutiveFailures: 0 };
|
|
32
|
+
stopped = false;
|
|
33
|
+
constructor(deps) {
|
|
34
|
+
this.deps = deps;
|
|
35
|
+
this.maxFailures = deps.maxConsecutiveFailures ?? 3;
|
|
36
|
+
}
|
|
37
|
+
now() {
|
|
38
|
+
return (this.deps.now ?? Date.now)();
|
|
39
|
+
}
|
|
40
|
+
log(msg) {
|
|
41
|
+
this.deps.logger?.(`[registry-sync] ${msg}`);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Mark the registry as needing a durable push. Debounced — many rapid marks
|
|
45
|
+
* within the window coalesce into a single commit (coarse, not per-tick).
|
|
46
|
+
*/
|
|
47
|
+
markRegistryDirty(reason) {
|
|
48
|
+
if (this.stopped)
|
|
49
|
+
return;
|
|
50
|
+
this.pendingReasons.push(reason);
|
|
51
|
+
if (this.timer)
|
|
52
|
+
return; // already scheduled
|
|
53
|
+
this.timer = setTimeout(() => {
|
|
54
|
+
this.timer = null;
|
|
55
|
+
void this.flush();
|
|
56
|
+
}, this.deps.debounceMs);
|
|
57
|
+
if (this.timer.unref)
|
|
58
|
+
this.timer.unref();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Flush immediately (also used by stop()). Returns true if a commit was
|
|
62
|
+
* pushed. A non-authoritative machine returns false without pushing
|
|
63
|
+
* (single-writer). Re-entrancy guarded.
|
|
64
|
+
*/
|
|
65
|
+
async flush() {
|
|
66
|
+
if (this.flushing)
|
|
67
|
+
return false;
|
|
68
|
+
if (this.pendingReasons.length === 0)
|
|
69
|
+
return false;
|
|
70
|
+
this.flushing = true;
|
|
71
|
+
const reasons = this.pendingReasons.slice(0, 8);
|
|
72
|
+
this.pendingReasons = [];
|
|
73
|
+
this.health.lastAttemptAt = new Date(this.now()).toISOString();
|
|
74
|
+
try {
|
|
75
|
+
if (!this.deps.isAuthoritative()) {
|
|
76
|
+
// Single-writer: a standby never writes durable authority-bearing state.
|
|
77
|
+
this.log(`skip flush — not authoritative (${reasons.length} pending reason(s) dropped)`);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const message = `chore(mesh): registry sync — ${reasons.join('; ').slice(0, 200)}`;
|
|
81
|
+
const pushed = this.deps.commitAndPush(message, [this.deps.registryAbsPath]);
|
|
82
|
+
if (pushed) {
|
|
83
|
+
this.health = {
|
|
84
|
+
healthy: true,
|
|
85
|
+
consecutiveFailures: 0,
|
|
86
|
+
lastPushAt: new Date(this.now()).toISOString(),
|
|
87
|
+
lastAttemptAt: this.health.lastAttemptAt,
|
|
88
|
+
};
|
|
89
|
+
this.deps.onSyncHealth?.(this.health);
|
|
90
|
+
this.log(`pushed: ${message}`);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
// commitAndPush returns false either when nothing changed (benign) OR on
|
|
94
|
+
// a push failure. We can't distinguish here, so a "nothing to push" is
|
|
95
|
+
// treated as success (no failure increment) only if a prior push exists.
|
|
96
|
+
// To stay conservative we treat false as a non-failure unless we can see
|
|
97
|
+
// the working tree is dirty — handled by the caller's periodic sync.
|
|
98
|
+
this.log(`commitAndPush returned false (no-op or push declined): ${message}`);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
const lastError = err instanceof Error ? err.message : String(err);
|
|
103
|
+
this.health = {
|
|
104
|
+
healthy: this.health.consecutiveFailures + 1 < this.maxFailures,
|
|
105
|
+
consecutiveFailures: this.health.consecutiveFailures + 1,
|
|
106
|
+
lastError,
|
|
107
|
+
lastPushAt: this.health.lastPushAt,
|
|
108
|
+
lastAttemptAt: this.health.lastAttemptAt,
|
|
109
|
+
};
|
|
110
|
+
this.deps.onSyncHealth?.(this.health);
|
|
111
|
+
this.log(`push failed (${this.health.consecutiveFailures}/${this.maxFailures}): ${lastError}`);
|
|
112
|
+
// Re-queue the reasons so the next mark/flush retries them.
|
|
113
|
+
this.pendingReasons.unshift(...reasons);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
this.flushing = false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
getHealth() {
|
|
121
|
+
return { ...this.health };
|
|
122
|
+
}
|
|
123
|
+
stop() {
|
|
124
|
+
this.stopped = true;
|
|
125
|
+
if (this.timer) {
|
|
126
|
+
clearTimeout(this.timer);
|
|
127
|
+
this.timer = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=RegistrySyncDebouncer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RegistrySyncDebouncer.js","sourceRoot":"","sources":["../../src/core/RegistrySyncDebouncer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* liveTailRedaction — versioned secret-redaction for the live tail (spec §8 G3c).
|
|
3
|
+
*
|
|
4
|
+
* The live tail carries the most sensitive data in the system (user messages,
|
|
5
|
+
* tool outputs, incidentally-present secrets). Before any tail content leaves
|
|
6
|
+
* this machine it is scrubbed of credential-shaped material. The category set
|
|
7
|
+
* is a NAMED, VERSIONED enum (not an ad-hoc inline regex) so it can be extended
|
|
8
|
+
* as new token/credential shapes appear — combined with carry-by-reference for
|
|
9
|
+
* large tool output (§8 G3b), the tail minimizes raw sensitive bytes
|
|
10
|
+
* structurally rather than relying on pattern-matching alone.
|
|
11
|
+
*
|
|
12
|
+
* Bump REDACTION_CATEGORY_VERSION when the category set changes, so a receiver
|
|
13
|
+
* can record which version scrubbed a flush.
|
|
14
|
+
*/
|
|
15
|
+
export declare const REDACTION_CATEGORY_VERSION = 1;
|
|
16
|
+
export declare enum RedactionCategory {
|
|
17
|
+
BearerToken = "bearer-token",
|
|
18
|
+
ApiKey = "api-key",
|
|
19
|
+
PrivateKeyBlock = "private-key-block",
|
|
20
|
+
AwsAccessKey = "aws-access-key",
|
|
21
|
+
SecretAssignment = "secret-assignment",
|
|
22
|
+
JwtLike = "jwt-like"
|
|
23
|
+
}
|
|
24
|
+
export interface RedactionResult {
|
|
25
|
+
text: string;
|
|
26
|
+
redactedCount: number;
|
|
27
|
+
categories: RedactionCategory[];
|
|
28
|
+
version: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Redact credential-shaped material from a string. Returns the scrubbed text,
|
|
32
|
+
* how many redactions occurred, and which categories fired (for the flush's
|
|
33
|
+
* redaction metadata).
|
|
34
|
+
*/
|
|
35
|
+
export declare function redactForLiveTail(input: string): RedactionResult;
|
|
36
|
+
//# sourceMappingURL=liveTailRedaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"liveTailRedaction.d.ts","sourceRoot":"","sources":["../../src/core/liveTailRedaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAE5C,oBAAY,iBAAiB;IAC3B,WAAW,iBAAiB;IAC5B,MAAM,YAAY;IAClB,eAAe,sBAAsB;IACrC,YAAY,mBAAmB;IAC/B,gBAAgB,sBAAsB;IACtC,OAAO,aAAa;CACrB;AAuBD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,iBAAiB,EAAE,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe,CAYhE"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* liveTailRedaction — versioned secret-redaction for the live tail (spec §8 G3c).
|
|
3
|
+
*
|
|
4
|
+
* The live tail carries the most sensitive data in the system (user messages,
|
|
5
|
+
* tool outputs, incidentally-present secrets). Before any tail content leaves
|
|
6
|
+
* this machine it is scrubbed of credential-shaped material. The category set
|
|
7
|
+
* is a NAMED, VERSIONED enum (not an ad-hoc inline regex) so it can be extended
|
|
8
|
+
* as new token/credential shapes appear — combined with carry-by-reference for
|
|
9
|
+
* large tool output (§8 G3b), the tail minimizes raw sensitive bytes
|
|
10
|
+
* structurally rather than relying on pattern-matching alone.
|
|
11
|
+
*
|
|
12
|
+
* Bump REDACTION_CATEGORY_VERSION when the category set changes, so a receiver
|
|
13
|
+
* can record which version scrubbed a flush.
|
|
14
|
+
*/
|
|
15
|
+
export const REDACTION_CATEGORY_VERSION = 1;
|
|
16
|
+
export var RedactionCategory;
|
|
17
|
+
(function (RedactionCategory) {
|
|
18
|
+
RedactionCategory["BearerToken"] = "bearer-token";
|
|
19
|
+
RedactionCategory["ApiKey"] = "api-key";
|
|
20
|
+
RedactionCategory["PrivateKeyBlock"] = "private-key-block";
|
|
21
|
+
RedactionCategory["AwsAccessKey"] = "aws-access-key";
|
|
22
|
+
RedactionCategory["SecretAssignment"] = "secret-assignment";
|
|
23
|
+
RedactionCategory["JwtLike"] = "jwt-like";
|
|
24
|
+
})(RedactionCategory || (RedactionCategory = {}));
|
|
25
|
+
// Conservative, high-precision patterns — false positives (over-redaction) are
|
|
26
|
+
// cheaper than leaking a secret over the wire. Each is global + case-insensitive
|
|
27
|
+
// where appropriate.
|
|
28
|
+
const RULES = [
|
|
29
|
+
{ category: RedactionCategory.PrivateKeyBlock, pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g },
|
|
30
|
+
{ category: RedactionCategory.BearerToken, pattern: /\bBearer\s+[A-Za-z0-9._\-]{12,}/gi },
|
|
31
|
+
{ category: RedactionCategory.AwsAccessKey, pattern: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
32
|
+
{ category: RedactionCategory.JwtLike, pattern: /\beyJ[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]{8,}\b/g },
|
|
33
|
+
// key/secret/token/password = "value" or : value
|
|
34
|
+
{ category: RedactionCategory.SecretAssignment, pattern: /\b(?:api[_-]?key|secret|token|password|passwd|client[_-]?secret)\b\s*[:=]\s*["']?[A-Za-z0-9._\-]{8,}["']?/gi },
|
|
35
|
+
// Generic long opaque api-key-ish tokens (sk-, ghp_, xoxb-, etc.)
|
|
36
|
+
{ category: RedactionCategory.ApiKey, pattern: /\b(?:sk|pk|rk|ghp|gho|ghs|xox[bpoa])[_-][A-Za-z0-9]{16,}\b/g },
|
|
37
|
+
];
|
|
38
|
+
const PLACEHOLDER = (c) => `[redacted:${c}]`;
|
|
39
|
+
/**
|
|
40
|
+
* Redact credential-shaped material from a string. Returns the scrubbed text,
|
|
41
|
+
* how many redactions occurred, and which categories fired (for the flush's
|
|
42
|
+
* redaction metadata).
|
|
43
|
+
*/
|
|
44
|
+
export function redactForLiveTail(input) {
|
|
45
|
+
let text = input;
|
|
46
|
+
let redactedCount = 0;
|
|
47
|
+
const categories = new Set();
|
|
48
|
+
for (const { category, pattern } of RULES) {
|
|
49
|
+
text = text.replace(pattern, () => {
|
|
50
|
+
redactedCount++;
|
|
51
|
+
categories.add(category);
|
|
52
|
+
return PLACEHOLDER(category);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return { text, redactedCount, categories: [...categories], version: REDACTION_CATEGORY_VERSION };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=liveTailRedaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"liveTailRedaction.js","sourceRoot":"","sources":["../../src/core/liveTailRedaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC;AAE5C,MAAM,CAAN,IAAY,iBAOX;AAPD,WAAY,iBAAiB;IAC3B,iDAA4B,CAAA;IAC5B,uCAAkB,CAAA;IAClB,0DAAqC,CAAA;IACrC,oDAA+B,CAAA;IAC/B,2DAAsC,CAAA;IACtC,yCAAoB,CAAA;AACtB,CAAC,EAPW,iBAAiB,KAAjB,iBAAiB,QAO5B;AAOD,+EAA+E;AAC/E,iFAAiF;AACjF,qBAAqB;AACrB,MAAM,KAAK,GAAmB;IAC5B,EAAE,QAAQ,EAAE,iBAAiB,CAAC,eAAe,EAAE,OAAO,EAAE,6EAA6E,EAAE;IACvI,EAAE,QAAQ,EAAE,iBAAiB,CAAC,WAAW,EAAE,OAAO,EAAE,mCAAmC,EAAE;IACzF,EAAE,QAAQ,EAAE,iBAAiB,CAAC,YAAY,EAAE,OAAO,EAAE,uBAAuB,EAAE;IAC9E,EAAE,QAAQ,EAAE,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,oEAAoE,EAAE;IACtH,iDAAiD;IACjD,EAAE,QAAQ,EAAE,iBAAiB,CAAC,gBAAgB,EAAE,OAAO,EAAE,6GAA6G,EAAE;IACxK,kEAAkE;IAClE,EAAE,QAAQ,EAAE,iBAAiB,CAAC,MAAM,EAAE,OAAO,EAAE,6DAA6D,EAAE;CAC/G,CAAC;AAEF,MAAM,WAAW,GAAG,CAAC,CAAoB,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC;AAShE;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,IAAI,IAAI,GAAG,KAAK,CAAC;IACjB,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,MAAM,UAAU,GAAG,IAAI,GAAG,EAAqB,CAAC;IAChD,KAAK,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,KAAK,EAAE,CAAC;QAC1C,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE;YAChC,aAAa,EAAE,CAAC;YAChB,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACzB,OAAO,WAAW,CAAC,QAAQ,CAAC,CAAC;QAC/B,CAAC,CAAC,CAAC;IACL,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE,CAAC,GAAG,UAAU,CAAC,EAAE,OAAO,EAAE,0BAA0B,EAAE,CAAC;AACnG,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* registryReplayGuard — G2 replay/freshness + unknown-key constraint (spec §8 G2).
|
|
3
|
+
*
|
|
4
|
+
* Validates an incoming (pulled) registry entry before it is applied locally.
|
|
5
|
+
* Signed-commit verification (v3) already rejects unsigned/revoked commits;
|
|
6
|
+
* this guard adds the content-level checks a signature alone cannot give:
|
|
7
|
+
*
|
|
8
|
+
* 1. Replay/freshness — a pulled entry whose per-author `syncSequence` is not
|
|
9
|
+
* strictly greater than the last applied for its author is discarded.
|
|
10
|
+
* 2. Epoch floor — a pulled entry whose `authoredUnderEpoch` is below the
|
|
11
|
+
* current committed epoch is discarded. This catches the case a per-author
|
|
12
|
+
* sequence cannot: a machine that wiped/restored local state (sequence
|
|
13
|
+
* resets to 0) or re-keyed cannot smuggle in an authority-bearing write,
|
|
14
|
+
* because its stale leaseEpoch is rejected regardless of a locally-monotonic
|
|
15
|
+
* sequence.
|
|
16
|
+
* 3. Unknown-key first commit — the FIRST commit from a previously-unseen
|
|
17
|
+
* machineId is accepted ONLY if it is role:standby + rejoined:true (or a
|
|
18
|
+
* pairing-join record). Any unknown-key first commit asserting an awake
|
|
19
|
+
* role or a lease claim is rejected — a rejoining machine must pull-and-read
|
|
20
|
+
* before writing, never assert its stale prior role.
|
|
21
|
+
*
|
|
22
|
+
* Pure logic, unit-tested on both sides of every boundary.
|
|
23
|
+
*/
|
|
24
|
+
import type { MachineRegistryEntry } from './types.js';
|
|
25
|
+
export interface ReplayGuardInput {
|
|
26
|
+
machineId: string;
|
|
27
|
+
/** The incoming (pulled) entry for this machine. */
|
|
28
|
+
incoming: MachineRegistryEntry;
|
|
29
|
+
/** The locally-known entry for this machine (undefined = previously unseen). */
|
|
30
|
+
local?: MachineRegistryEntry;
|
|
31
|
+
/** The current committed lease epoch (registry.lease?.epoch ?? 0). */
|
|
32
|
+
currentCommittedEpoch: number;
|
|
33
|
+
/** Whether this incoming entry carries a valid pairing-join record (allows an awake-less first write). */
|
|
34
|
+
hasValidJoinRecord?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface ReplayDecision {
|
|
37
|
+
accept: boolean;
|
|
38
|
+
reason: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function evaluateRegistryEntry(input: ReplayGuardInput): ReplayDecision;
|
|
41
|
+
/**
|
|
42
|
+
* Reconcile a full incoming registry against the local one, entry by entry.
|
|
43
|
+
* Returns the entries to apply (accepted) and the rejected ones with reasons
|
|
44
|
+
* (logged, never applied). The lease object is reconciled separately by the
|
|
45
|
+
* FencedLease layer (epoch CAS), not here.
|
|
46
|
+
*/
|
|
47
|
+
export declare function reconcileRegistryEntries(opts: {
|
|
48
|
+
localEntries: Record<string, MachineRegistryEntry>;
|
|
49
|
+
incomingEntries: Record<string, MachineRegistryEntry>;
|
|
50
|
+
currentCommittedEpoch: number;
|
|
51
|
+
joinRecords?: ReadonlySet<string>;
|
|
52
|
+
}): {
|
|
53
|
+
accepted: Record<string, MachineRegistryEntry>;
|
|
54
|
+
rejected: Array<{
|
|
55
|
+
machineId: string;
|
|
56
|
+
reason: string;
|
|
57
|
+
}>;
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=registryReplayGuard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registryReplayGuard.d.ts","sourceRoot":"","sources":["../../src/core/registryReplayGuard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,gFAAgF;IAChF,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,sEAAsE;IACtE,qBAAqB,EAAE,MAAM,CAAC;IAC9B,0GAA0G;IAC1G,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,gBAAgB,GAAG,cAAc,CAuC7E;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC7C,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;IACnD,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;IACtD,qBAAqB,EAAE,MAAM,CAAC;IAC9B,WAAW,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACnC,GAAG;IACF,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;IAC/C,QAAQ,EAAE,KAAK,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxD,CAoBA"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* registryReplayGuard — G2 replay/freshness + unknown-key constraint (spec §8 G2).
|
|
3
|
+
*
|
|
4
|
+
* Validates an incoming (pulled) registry entry before it is applied locally.
|
|
5
|
+
* Signed-commit verification (v3) already rejects unsigned/revoked commits;
|
|
6
|
+
* this guard adds the content-level checks a signature alone cannot give:
|
|
7
|
+
*
|
|
8
|
+
* 1. Replay/freshness — a pulled entry whose per-author `syncSequence` is not
|
|
9
|
+
* strictly greater than the last applied for its author is discarded.
|
|
10
|
+
* 2. Epoch floor — a pulled entry whose `authoredUnderEpoch` is below the
|
|
11
|
+
* current committed epoch is discarded. This catches the case a per-author
|
|
12
|
+
* sequence cannot: a machine that wiped/restored local state (sequence
|
|
13
|
+
* resets to 0) or re-keyed cannot smuggle in an authority-bearing write,
|
|
14
|
+
* because its stale leaseEpoch is rejected regardless of a locally-monotonic
|
|
15
|
+
* sequence.
|
|
16
|
+
* 3. Unknown-key first commit — the FIRST commit from a previously-unseen
|
|
17
|
+
* machineId is accepted ONLY if it is role:standby + rejoined:true (or a
|
|
18
|
+
* pairing-join record). Any unknown-key first commit asserting an awake
|
|
19
|
+
* role or a lease claim is rejected — a rejoining machine must pull-and-read
|
|
20
|
+
* before writing, never assert its stale prior role.
|
|
21
|
+
*
|
|
22
|
+
* Pure logic, unit-tested on both sides of every boundary.
|
|
23
|
+
*/
|
|
24
|
+
export function evaluateRegistryEntry(input) {
|
|
25
|
+
const { incoming, local, currentCommittedEpoch, hasValidJoinRecord } = input;
|
|
26
|
+
// ── Unknown-key first commit constraint ───────────────────────────
|
|
27
|
+
if (!local) {
|
|
28
|
+
if (hasValidJoinRecord) {
|
|
29
|
+
return { accept: true, reason: 'unknown-key-with-valid-join-record' };
|
|
30
|
+
}
|
|
31
|
+
const isStandby = incoming.role === 'standby';
|
|
32
|
+
const isRejoin = incoming.rejoined === true;
|
|
33
|
+
if (isStandby && isRejoin) {
|
|
34
|
+
return { accept: true, reason: 'unknown-key-standby-rejoin' };
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
accept: false,
|
|
38
|
+
reason: `unknown-key-first-commit must be standby+rejoined (got role=${incoming.role}, rejoined=${incoming.rejoined})`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// ── Replay/freshness (per-author monotonic sequence) ──────────────
|
|
42
|
+
const localSeq = local.syncSequence ?? -1;
|
|
43
|
+
const incomingSeq = incoming.syncSequence ?? -1;
|
|
44
|
+
if (incomingSeq <= localSeq) {
|
|
45
|
+
return {
|
|
46
|
+
accept: false,
|
|
47
|
+
reason: `stale-sync-sequence (incoming ${incomingSeq} <= local ${localSeq})`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// ── Epoch floor (catches wiped/re-keyed sequence reset) ───────────
|
|
51
|
+
const authoredUnder = incoming.authoredUnderEpoch ?? 0;
|
|
52
|
+
if (authoredUnder < currentCommittedEpoch) {
|
|
53
|
+
return {
|
|
54
|
+
accept: false,
|
|
55
|
+
reason: `below-epoch-floor (authoredUnderEpoch ${authoredUnder} < committed ${currentCommittedEpoch})`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return { accept: true, reason: 'fresh' };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Reconcile a full incoming registry against the local one, entry by entry.
|
|
62
|
+
* Returns the entries to apply (accepted) and the rejected ones with reasons
|
|
63
|
+
* (logged, never applied). The lease object is reconciled separately by the
|
|
64
|
+
* FencedLease layer (epoch CAS), not here.
|
|
65
|
+
*/
|
|
66
|
+
export function reconcileRegistryEntries(opts) {
|
|
67
|
+
const accepted = {};
|
|
68
|
+
const rejected = [];
|
|
69
|
+
for (const [machineId, incoming] of Object.entries(opts.incomingEntries)) {
|
|
70
|
+
const decision = evaluateRegistryEntry({
|
|
71
|
+
machineId,
|
|
72
|
+
incoming,
|
|
73
|
+
local: opts.localEntries[machineId],
|
|
74
|
+
currentCommittedEpoch: opts.currentCommittedEpoch,
|
|
75
|
+
hasValidJoinRecord: opts.joinRecords?.has(machineId) ?? false,
|
|
76
|
+
});
|
|
77
|
+
if (decision.accept) {
|
|
78
|
+
accepted[machineId] = incoming;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
rejected.push({ machineId, reason: decision.reason });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { accepted, rejected };
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=registryReplayGuard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registryReplayGuard.js","sourceRoot":"","sources":["../../src/core/registryReplayGuard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAqBH,MAAM,UAAU,qBAAqB,CAAC,KAAuB;IAC3D,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,GAAG,KAAK,CAAC;IAE7E,qEAAqE;IACrE,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,IAAI,kBAAkB,EAAE,CAAC;YACvB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,oCAAoC,EAAE,CAAC;QACxE,CAAC;QACD,MAAM,SAAS,GAAG,QAAQ,CAAC,IAAI,KAAK,SAAS,CAAC;QAC9C,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,KAAK,IAAI,CAAC;QAC5C,IAAI,SAAS,IAAI,QAAQ,EAAE,CAAC;YAC1B,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,4BAA4B,EAAE,CAAC;QAChE,CAAC;QACD,OAAO;YACL,MAAM,EAAE,KAAK;YACb,MAAM,EAAE,+DAA+D,QAAQ,CAAC,IAAI,cAAc,QAAQ,CAAC,QAAQ,GAAG;SACvH,CAAC;IACJ,CAAC;IAED,qEAAqE;IACrE,MAAM,QAAQ,GAAG,KAAK,CAAC,YAAY,IAAI,CAAC,CAAC,CAAC;IAC1C,MAAM,WAAW,GAAG,QAAQ,CAAC,YAAY,IAAI,CAAC,CAAC,CAAC;IAChD,IAAI,WAAW,IAAI,QAAQ,EAAE,CAAC;QAC5B,OAAO;YACL,MAAM,EAAE,KAAK;YACb,MAAM,EAAE,iCAAiC,WAAW,aAAa,QAAQ,GAAG;SAC7E,CAAC;IACJ,CAAC;IAED,qEAAqE;IACrE,MAAM,aAAa,GAAG,QAAQ,CAAC,kBAAkB,IAAI,CAAC,CAAC;IACvD,IAAI,aAAa,GAAG,qBAAqB,EAAE,CAAC;QAC1C,OAAO;YACL,MAAM,EAAE,KAAK;YACb,MAAM,EAAE,yCAAyC,aAAa,gBAAgB,qBAAqB,GAAG;SACvG,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;AAC3C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAKxC;IAIC,MAAM,QAAQ,GAAyC,EAAE,CAAC;IAC1D,MAAM,QAAQ,GAAiD,EAAE,CAAC;IAElE,KAAK,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,CAAC;QACzE,MAAM,QAAQ,GAAG,qBAAqB,CAAC;YACrC,SAAS;YACT,QAAQ;YACR,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC;YACnC,qBAAqB,EAAE,IAAI,CAAC,qBAAqB;YACjD,kBAAkB,EAAE,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,SAAS,CAAC,IAAI,KAAK;SAC9D,CAAC,CAAC;QACH,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;YACpB,QAAQ,CAAC,SAAS,CAAC,GAAG,QAAQ,CAAC;QACjC,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AAChC,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-Machine Seamlessness — config resolution + invariant validation.
|
|
3
|
+
*
|
|
4
|
+
* Spec: docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §9 (Tunability).
|
|
5
|
+
*
|
|
6
|
+
* Resolves the optional `multiMachine` seamlessness knobs to concrete values
|
|
7
|
+
* with sane 2-machine-personal-use defaults, auto-derives the standby pull
|
|
8
|
+
* cadence, and enforces the cross-knob invariants. A violating config is
|
|
9
|
+
* REJECTED with a clear message rather than degrading silently — so a user
|
|
10
|
+
* widening `ingressHeartbeatMs` (which widens `leaseTtlMs`) cannot quietly
|
|
11
|
+
* invalidate the RPO bound the spec promises (§3).
|
|
12
|
+
*/
|
|
13
|
+
import type { MultiMachineConfig } from './types.js';
|
|
14
|
+
/**
|
|
15
|
+
* The mesh protocol version this build speaks. A machine below this version
|
|
16
|
+
* is ineligible for the awake lease during a seamless handoff (spec §11
|
|
17
|
+
* partial-migration safety). Bump only on a breaking coordination change.
|
|
18
|
+
*/
|
|
19
|
+
export declare const SEAMLESSNESS_PROTOCOL_VERSION = 1;
|
|
20
|
+
/** Fully-resolved seamlessness knobs (every field concrete). */
|
|
21
|
+
export interface ResolvedSeamlessnessConfig {
|
|
22
|
+
ingressHeartbeatMs: number;
|
|
23
|
+
registrySyncDebounceMs: number;
|
|
24
|
+
standbyPullIntervalMs: number;
|
|
25
|
+
failoverThresholdMs: number;
|
|
26
|
+
leaseTtlMs: number;
|
|
27
|
+
liveTailTransport: 'tunnel' | 'git';
|
|
28
|
+
liveTailMaxStalenessMs: number;
|
|
29
|
+
liveTailPushRateMs: number;
|
|
30
|
+
liveTailOutOfOrderTimeoutMs: number;
|
|
31
|
+
liveTailMaxBytesPerTopic: number;
|
|
32
|
+
handoffAckTimeoutMs: number;
|
|
33
|
+
minHandoffIntervalMs: number;
|
|
34
|
+
splitBrainEscalationCooldownMs: number;
|
|
35
|
+
handoffBar: 'near-instant' | 'relaxed';
|
|
36
|
+
maxProcessingMs: number;
|
|
37
|
+
protocolVersion: number;
|
|
38
|
+
}
|
|
39
|
+
export declare class SeamlessnessConfigError extends Error {
|
|
40
|
+
constructor(message: string);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the failover threshold (ms) from the existing config field.
|
|
44
|
+
* `failoverTimeoutMinutes` is the canonical knob; default 15 min.
|
|
45
|
+
* Nullish-coalescing (not ||) so an explicit 0 isn't clobbered — but 0 is
|
|
46
|
+
* itself invalid and caught by validation.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveFailoverThresholdMs(mm?: MultiMachineConfig): number;
|
|
49
|
+
/**
|
|
50
|
+
* Resolve all seamlessness knobs to concrete values. Auto-derives
|
|
51
|
+
* `standbyPullIntervalMs` (failoverThresholdMs/4) and the tail/lease-linked
|
|
52
|
+
* defaults. Does NOT validate — call validateSeamlessnessInvariants() after.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveSeamlessnessConfig(mm?: MultiMachineConfig): ResolvedSeamlessnessConfig;
|
|
55
|
+
/**
|
|
56
|
+
* Validate the cross-knob invariants (spec §9). Returns the list of
|
|
57
|
+
* violations (empty = valid). Callers that want hard-rejection use
|
|
58
|
+
* assertSeamlessnessInvariants().
|
|
59
|
+
*
|
|
60
|
+
* Invariants:
|
|
61
|
+
* 1. standbyPullIntervalMs < failoverThresholdMs / 3
|
|
62
|
+
* — a standby must pull often enough to notice a dead holder well
|
|
63
|
+
* before the failover threshold, or failover races stale data.
|
|
64
|
+
* 2. standbyPullIntervalMs < leaseTtlMs
|
|
65
|
+
* — a standby must refresh its git-committed epoch view at least once
|
|
66
|
+
* per lease lifetime, or it decides max(tunnel,git) on a stale git floor.
|
|
67
|
+
* 3. liveTailPushRateMs ≤ liveTailMaxStalenessMs
|
|
68
|
+
* — the holder must push tail flushes at least as often as the RPO bound,
|
|
69
|
+
* or the promised RPO is unachievable.
|
|
70
|
+
* Plus basic positivity (a 0/negative cadence is never valid).
|
|
71
|
+
*/
|
|
72
|
+
export declare function validateSeamlessnessInvariants(c: ResolvedSeamlessnessConfig): string[];
|
|
73
|
+
/**
|
|
74
|
+
* Resolve + validate in one step, throwing SeamlessnessConfigError on any
|
|
75
|
+
* invariant violation. Use at server startup so a bad config is rejected
|
|
76
|
+
* loudly rather than degrading silently.
|
|
77
|
+
*/
|
|
78
|
+
export declare function assertSeamlessnessInvariants(mm?: MultiMachineConfig): ResolvedSeamlessnessConfig;
|
|
79
|
+
//# sourceMappingURL=seamlessnessConfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seamlessnessConfig.d.ts","sourceRoot":"","sources":["../../src/core/seamlessnessConfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,IAAI,CAAC;AAE/C,gEAAgE;AAChE,MAAM,WAAW,0BAA0B;IACzC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,QAAQ,GAAG,KAAK,CAAC;IACpC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,2BAA2B,EAAE,MAAM,CAAC;IACpC,wBAAwB,EAAE,MAAM,CAAC;IACjC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,8BAA8B,EAAE,MAAM,CAAC;IACvC,UAAU,EAAE,cAAc,GAAG,SAAS,CAAC;IACvC,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,EAAE,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAG1E;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,EAAE,CAAC,EAAE,kBAAkB,GAAG,0BAA0B,CA+B7F;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,8BAA8B,CAAC,CAAC,EAAE,0BAA0B,GAAG,MAAM,EAAE,CAqCtF;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,EAAE,CAAC,EAAE,kBAAkB,GAAG,0BAA0B,CAUhG"}
|