mcp-pacemaker 1.3.1 → 2.0.2
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/CHANGELOG.md +85 -27
- package/CONTRIBUTING.md +123 -47
- package/README.md +117 -62
- package/SECURITY.md +34 -24
- package/bin/mcp-bridge.mjs +91 -42
- package/bin/pooling-batch-scheduler.mjs +129 -0
- package/bin/pooling-batches.mjs +270 -0
- package/bin/pooling-config.mjs +413 -389
- package/bin/pooling-editor.mjs +128 -0
- package/bin/pooling-errors.mjs +23 -0
- package/bin/pooling-execution.mjs +19 -6
- package/bin/pooling-files.mjs +541 -0
- package/bin/pooling-trace.mjs +218 -0
- package/bin/pooling-writer.mjs +87 -14
- package/bin/prewarm.mjs +87 -6
- package/bin/windows/PoolingSecurityHelper.build.json +7 -4
- package/bin/windows/PoolingSecurityHelper.exe +0 -0
- package/bin/windows/src/PoolingNativeFiles.cs +678 -0
- package/bin/windows/src/PoolingSecurityHelper.cs +18 -5
- package/package.json +2 -2
- package/tools/windows-security-helper/README.md +146 -3
- package/tools/windows-security-helper/build.ps1 +2 -2
- package/ui/dist/assets/index-CaLPg0c-.css +1 -0
- package/ui/dist/assets/index-CgPruWYi.js +9 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/third-party-manifest.json +37 -19
- package/ui/dist/assets/index-CIy7uxfd.css +0 -1
- package/ui/dist/assets/index-CtaaQsB9.js +0 -8
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { PoolingConfigError } from './pooling-errors.mjs';
|
|
2
|
+
|
|
3
|
+
// Span editing intentionally keeps unrequested bytes, spelling and whitespace.
|
|
4
|
+
export class JsonSpans {
|
|
5
|
+
constructor(bytes) {
|
|
6
|
+
this.text = bytes.toString('utf8');
|
|
7
|
+
this.offset = 0;
|
|
8
|
+
try {
|
|
9
|
+
if (!Buffer.from(this.text).equals(bytes)) throw new Error();
|
|
10
|
+
this.servers = JSON.parse(this.text);
|
|
11
|
+
if (!this.servers ||
|
|
12
|
+
Array.isArray(this.servers) ||
|
|
13
|
+
typeof this.servers !== 'object') throw new Error();
|
|
14
|
+
this.root = this.value(0);
|
|
15
|
+
} catch {
|
|
16
|
+
throw new PoolingConfigError(400, 'INVALID_CONFIG',
|
|
17
|
+
'Config must be a UTF-8 JSON object without duplicate keys or excessive nesting.');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
whitespace() {
|
|
22
|
+
while (' \t\r\n'.includes(this.text[this.offset] ?? '\0')) this.offset++;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
string() {
|
|
26
|
+
const start = this.offset++;
|
|
27
|
+
while (this.text[this.offset] !== '"') {
|
|
28
|
+
this.offset += this.text[this.offset] === '\\' ? 2 : 1;
|
|
29
|
+
}
|
|
30
|
+
this.offset++;
|
|
31
|
+
return JSON.parse(this.text.slice(start, this.offset));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
value(depth) {
|
|
35
|
+
if (depth > 128) throw new Error();
|
|
36
|
+
this.whitespace();
|
|
37
|
+
const start = this.offset;
|
|
38
|
+
const token = this.text[this.offset];
|
|
39
|
+
const properties = new Map();
|
|
40
|
+
if (token === '{') {
|
|
41
|
+
this.offset++;
|
|
42
|
+
this.whitespace();
|
|
43
|
+
while (this.text[this.offset] !== '}') {
|
|
44
|
+
const keyStart = this.offset;
|
|
45
|
+
const key = this.string();
|
|
46
|
+
const keyEnd = this.offset;
|
|
47
|
+
if (properties.has(key)) throw new Error();
|
|
48
|
+
this.whitespace();
|
|
49
|
+
this.offset++;
|
|
50
|
+
const value = this.value(depth + 1);
|
|
51
|
+
properties.set(key, { keyStart, keyEnd, value });
|
|
52
|
+
this.whitespace();
|
|
53
|
+
if (this.text[this.offset] !== ',') break;
|
|
54
|
+
this.offset++;
|
|
55
|
+
this.whitespace();
|
|
56
|
+
}
|
|
57
|
+
this.offset++;
|
|
58
|
+
} else if (token === '[') {
|
|
59
|
+
this.offset++;
|
|
60
|
+
this.whitespace();
|
|
61
|
+
while (this.text[this.offset] !== ']') {
|
|
62
|
+
this.value(depth + 1);
|
|
63
|
+
this.whitespace();
|
|
64
|
+
if (this.text[this.offset] !== ',') break;
|
|
65
|
+
this.offset++;
|
|
66
|
+
}
|
|
67
|
+
this.offset++;
|
|
68
|
+
} else if (token === '"') {
|
|
69
|
+
this.string();
|
|
70
|
+
} else {
|
|
71
|
+
while (this.offset < this.text.length &&
|
|
72
|
+
!' \t\r\n,}]'.includes(this.text[this.offset])) this.offset++;
|
|
73
|
+
}
|
|
74
|
+
return { start, end: this.offset, properties };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
update(name, changes) {
|
|
78
|
+
const server = this.root.properties.get(name).value;
|
|
79
|
+
const entries = [...server.properties.values()];
|
|
80
|
+
const edits = [];
|
|
81
|
+
const additions = [];
|
|
82
|
+
for (const [key, value] of changes) {
|
|
83
|
+
const property = server.properties.get(key);
|
|
84
|
+
if (!property) {
|
|
85
|
+
if (value !== undefined) additions.push([key, value]);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (value === undefined) {
|
|
89
|
+
const index = entries.indexOf(property);
|
|
90
|
+
const previous = entries[index - 1];
|
|
91
|
+
const next = entries[index + 1];
|
|
92
|
+
edits.push({
|
|
93
|
+
start: previous ? previous.value.end : property.keyStart,
|
|
94
|
+
end: previous ? property.value.end : next.keyStart,
|
|
95
|
+
text: '',
|
|
96
|
+
});
|
|
97
|
+
} else if (this.servers[name][key] !== value) {
|
|
98
|
+
edits.push({ start: property.value.start, end: property.value.end, text: JSON.stringify(value) });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (additions.length) {
|
|
102
|
+
const first = entries[0];
|
|
103
|
+
const last = entries.at(-1);
|
|
104
|
+
const spacing = this.text.slice(server.start + 1, first.keyStart);
|
|
105
|
+
const colon = this.text.slice(first.keyEnd, first.value.start);
|
|
106
|
+
const text = additions.map(([key, value]) =>
|
|
107
|
+
`,${spacing}${JSON.stringify(key)}${colon}${JSON.stringify(value)}`).join('');
|
|
108
|
+
edits.push({ start: last.value.end, end: last.value.end, text });
|
|
109
|
+
}
|
|
110
|
+
let text = this.text;
|
|
111
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) {
|
|
112
|
+
text = text.slice(0, edit.start) + edit.text + text.slice(edit.end);
|
|
113
|
+
}
|
|
114
|
+
return Buffer.from(text);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
validateComplete() {
|
|
118
|
+
for (const server of Object.values(this.servers)) {
|
|
119
|
+
if (!server ||
|
|
120
|
+
Array.isArray(server) ||
|
|
121
|
+
typeof server !== 'object' ||
|
|
122
|
+
(!server.command && !server.url)) {
|
|
123
|
+
throw new PoolingConfigError(400, 'INVALID_CONFIG',
|
|
124
|
+
'Every server definition must be an object with a command or URL.');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export class PoolingConfigError extends Error {
|
|
2
|
+
constructor(statusCode, code, message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'PoolingConfigError';
|
|
5
|
+
this.statusCode = statusCode;
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function poolingIOError(error) {
|
|
11
|
+
if (error instanceof PoolingConfigError) return error;
|
|
12
|
+
return new PoolingConfigError(500, 'IO_ERROR', 'Unable to access config transaction files.');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function poolingConflict() {
|
|
16
|
+
return new PoolingConfigError(409, 'REVISION_CONFLICT',
|
|
17
|
+
'Config content, file identity, or permissions changed. Reread the config.');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function recoveryError() {
|
|
21
|
+
return new PoolingConfigError(409, 'RECOVERY_REQUIRED',
|
|
22
|
+
'Config transaction state could not be verified. No recovery changes were authorized.');
|
|
23
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PoolingConfigError } from './pooling-
|
|
1
|
+
import { PoolingConfigError } from './pooling-errors.mjs';
|
|
2
2
|
|
|
3
3
|
export const POOLING_BUDGET_MS = 9000;
|
|
4
4
|
|
|
@@ -54,8 +54,15 @@ export class PoolingExecution {
|
|
|
54
54
|
if (Atomics.compareExchange(this.state, 0, ACTIVE, FINISHED) !== ACTIVE) this.check();
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
fail() {
|
|
58
|
-
|
|
57
|
+
fail(commitState = 'not-committed') {
|
|
58
|
+
if (commitState === 'unknown') {
|
|
59
|
+
Atomics.store(this.state, 0, COMMITTING);
|
|
60
|
+
} else if (commitState === 'committed') {
|
|
61
|
+
Atomics.store(this.state, 0, process.hrtime.bigint() >= this.deadline
|
|
62
|
+
? FINISHED_LATE : FINISHED_COMMITTED);
|
|
63
|
+
} else {
|
|
64
|
+
Atomics.store(this.state, 0, FINISHED);
|
|
65
|
+
}
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
cancel(expired = false) {
|
|
@@ -64,8 +71,10 @@ export class PoolingExecution {
|
|
|
64
71
|
state === FINISHED_COMMITTED) return undefined;
|
|
65
72
|
if (state === FINISHED_LATE) return this.committedLate();
|
|
66
73
|
if (state === COMMITTING) {
|
|
67
|
-
|
|
74
|
+
const error = new PoolingConfigError(504, 'WRITER_OUTCOME_UNKNOWN',
|
|
68
75
|
'Config commit already started. Its outcome is not yet known. Do not replay this change; reread the config.');
|
|
76
|
+
error.commitState = 'unknown';
|
|
77
|
+
return error;
|
|
69
78
|
}
|
|
70
79
|
try {
|
|
71
80
|
this.check();
|
|
@@ -75,8 +84,10 @@ export class PoolingExecution {
|
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
committedLate() {
|
|
78
|
-
|
|
87
|
+
const error = new PoolingConfigError(504, 'WRITER_DEADLINE_COMMITTED',
|
|
79
88
|
'Config commit finished after its deadline. Do not replay this change; reread the config.');
|
|
89
|
+
error.commitState = 'committed';
|
|
90
|
+
return error;
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
workerFailure() {
|
|
@@ -84,8 +95,10 @@ export class PoolingExecution {
|
|
|
84
95
|
if (state === FINISHED_LATE) return this.committedLate();
|
|
85
96
|
if (state === COMMITTING ||
|
|
86
97
|
state === FINISHED_COMMITTED) {
|
|
87
|
-
|
|
98
|
+
const error = new PoolingConfigError(500, 'WRITER_OUTCOME_UNKNOWN',
|
|
88
99
|
'Pooling writer stopped after config commit started. Its outcome is unknown. Do not replay this change; reread the config.');
|
|
100
|
+
error.commitState = state === FINISHED_COMMITTED ? 'committed' : 'unknown';
|
|
101
|
+
return error;
|
|
89
102
|
}
|
|
90
103
|
}
|
|
91
104
|
}
|