0xray 3.4.6 → 3.4.8
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/.xray/config.json +0 -1
- package/AGENTS-consumer.md +3 -1
- package/AGENTS.md +1 -1
- package/README.md +1 -1
- package/SKILLS.md +1 -1
- package/dist/AGENTS.md +1 -1
- package/dist/CHANGELOG.md +22 -0
- package/dist/README.md +1 -1
- package/dist/SKILLS.md +1 -1
- package/dist/governance/governance-service.js +3 -1
- package/dist/governance/llm-governance-provider.js +66 -10
- package/dist/mcps/governance.server.d.ts +1 -0
- package/dist/mcps/governance.server.js +79 -8
- package/package.json +1 -1
- package/scripts/node/consumer-install-smoke.mjs +84 -22
- package/scripts/node/postinstall.cjs +18 -3
- package/scripts/node/pre-publish-guard.js +2 -0
- package/scripts/node/release-gate.mjs +16 -11
- package/scripts/node/release.mjs +56 -22
- package/src/mcps/governance.server.ts +81 -8
package/.xray/config.json
CHANGED
package/AGENTS-consumer.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# 0xRay AI Agents
|
|
2
2
|
|
|
3
|
+
<!-- 0xray-managed -->
|
|
4
|
+
|
|
3
5
|
Quick reference for the 0xRay AI orchestration framework (shipped to your project on `npm install 0xray`).
|
|
4
6
|
|
|
5
|
-
**v3.4.
|
|
7
|
+
**v3.4.8** — 7 MCP servers · 45 skills · 68 codex terms · 4 platform bridges
|
|
6
8
|
|
|
7
9
|
## Postinstall (automatic)
|
|
8
10
|
|
package/AGENTS.md
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# xray — MCP-Centric AI Governance OS
|
|
2
2
|
|
|
3
|
-
**v3.4.
|
|
3
|
+
**v3.4.8** — 42 agents · 45 skills · 7 MCP servers · 68 codex terms · 3,226 tests
|
|
4
4
|
|
|
5
5
|
xray is the pure v2 three-subsystem AI orchestration framework — **MCP-centric**, governed by Dynamo, and autonomous via thinDispatch. Consumer `npm install 0xray` auto-wires all four platform bridges and seven MCP servers with zero manual config.
|
|
6
6
|
|
package/SKILLS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 0xray Skills
|
|
2
2
|
|
|
3
|
-
**v3.4.
|
|
3
|
+
**v3.4.8** — **45 skills** · invoked via `xray-skills` MCP · synced on postinstall
|
|
4
4
|
|
|
5
5
|
xray ships 45 knowledge skills (`src/skills/<name>/SKILL.md`) across the full software lifecycle. Lead-dev operating model lives in **`orchestrator`** skill + `multi_agent_orchestration.lead_dev_mode` in features.json. Skills load on demand via the **xray-skills** MCP server (`npx -y 0xray mcp skills`), agent `@mentions`, or platform skill directories after postinstall.
|
|
6
6
|
|
package/dist/AGENTS.md
CHANGED
package/dist/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Conventional Commits](https://www.conventionalcommits.org/).
|
|
6
6
|
|
|
7
|
+
## [3.4.8] - 2026-06-19
|
|
8
|
+
|
|
9
|
+
### 🐛 Bug Fixes
|
|
10
|
+
- **Hermes v2 OAuth** — governance LLM voters read `providers.xai-oauth.tokens` from `~/.hermes/auth.json` (npm consumers on modern Hermes no longer abstain)
|
|
11
|
+
- **Headless deliberation** — honor `XRAY_GOVERNANCE_IN_PROCESS=1` for in-process skill voters without MCP subprocesses
|
|
12
|
+
- **Governance MCP HTTP** — per-session Streamable HTTP transports on Railway
|
|
13
|
+
|
|
14
|
+
### ✅ Tests
|
|
15
|
+
- Unit coverage for Hermes v2 auth parsing and `XRAY_GOVERNANCE_IN_PROCESS` governance path
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## [3.4.7] - 2026-06-18
|
|
20
|
+
|
|
21
|
+
### 🐛 Bug Fixes
|
|
22
|
+
- **Release gate upgrade smoke** — `consumer-install-smoke` simulates consumer opt-in merge on every publish
|
|
23
|
+
- **Release pipeline** — artifacts sync before gate; idempotent `npm publish`; tag after publish
|
|
24
|
+
- **Postinstall** — skip customized `AGENTS.md`; hard-fail on bridge wiring errors
|
|
25
|
+
- **pre-publish-guard** — allow `.xray/config.json` and `.xray/state/` runtime dirt between gate steps
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
7
29
|
## [3.4.6] - 2026-06-18
|
|
8
30
|
|
|
9
31
|
### 🐛 Bug Fixes
|
package/dist/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# xray — MCP-Centric AI Governance OS
|
|
2
2
|
|
|
3
|
-
**v3.4.
|
|
3
|
+
**v3.4.8** — 42 agents · 45 skills · 7 MCP servers · 68 codex terms · 3,226 tests
|
|
4
4
|
|
|
5
5
|
xray is the pure v2 three-subsystem AI orchestration framework — **MCP-centric**, governed by Dynamo, and autonomous via thinDispatch. Consumer `npm install 0xray` auto-wires all four platform bridges and seven MCP servers with zero manual config.
|
|
6
6
|
|
package/dist/SKILLS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 0xray Skills
|
|
2
2
|
|
|
3
|
-
**v3.4.
|
|
3
|
+
**v3.4.8** — **45 skills** · invoked via `xray-skills` MCP · synced on postinstall
|
|
4
4
|
|
|
5
5
|
xray ships 45 knowledge skills (`src/skills/<name>/SKILL.md`) across the full software lifecycle. Lead-dev operating model lives in **`orchestrator`** skill + `multi_agent_orchestration.lead_dev_mode` in features.json. Skills load on demand via the **xray-skills** MCP server (`npx -y 0xray mcp skills`), agent `@mentions`, or platform skill directories after postinstall.
|
|
6
6
|
|
|
@@ -128,7 +128,9 @@ export class GovernanceService {
|
|
|
128
128
|
}
|
|
129
129
|
async callSkillServer(serverName, proposals, context) {
|
|
130
130
|
const votes = [];
|
|
131
|
-
const useInProcess = process.env.VERCEL === '1' ||
|
|
131
|
+
const useInProcess = process.env.VERCEL === '1' ||
|
|
132
|
+
!!process.env.RAILWAY_ENVIRONMENT ||
|
|
133
|
+
process.env.XRAY_GOVERNANCE_IN_PROCESS === '1';
|
|
132
134
|
for (const proposal of proposals) {
|
|
133
135
|
try {
|
|
134
136
|
let text = '';
|
|
@@ -42,6 +42,71 @@ DECISION: approve|reject|abstain
|
|
|
42
42
|
CONFIDENCE: <0.0-1.0>
|
|
43
43
|
REASONING: <2-4 sentence analysis of the proposal's broader project impact>`,
|
|
44
44
|
};
|
|
45
|
+
function resolveHermesExpiresAt(provider, tokens) {
|
|
46
|
+
if (typeof provider.expires_at === "number")
|
|
47
|
+
return provider.expires_at;
|
|
48
|
+
const expiresIn = tokens?.expires_in ?? provider.expires_in;
|
|
49
|
+
if (typeof expiresIn !== "number")
|
|
50
|
+
return undefined;
|
|
51
|
+
const refreshCandidates = [provider.last_refresh, tokens?.last_refresh];
|
|
52
|
+
for (const lastRefresh of refreshCandidates) {
|
|
53
|
+
if (typeof lastRefresh !== "string")
|
|
54
|
+
continue;
|
|
55
|
+
const base = Date.parse(lastRefresh);
|
|
56
|
+
if (!Number.isNaN(base)) {
|
|
57
|
+
return Math.floor((base + expiresIn * 1000) / 1000);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
function extractHermesOAuthEntry(data) {
|
|
63
|
+
if (!data || typeof data !== "object")
|
|
64
|
+
return undefined;
|
|
65
|
+
if (Array.isArray(data)) {
|
|
66
|
+
return data.find((e) => e.provider === "xai-oauth" || !!e.access_token);
|
|
67
|
+
}
|
|
68
|
+
const record = data;
|
|
69
|
+
const legacy = record["xai-oauth"];
|
|
70
|
+
if (legacy && typeof legacy === "object") {
|
|
71
|
+
const entry = legacy;
|
|
72
|
+
if (entry.access_token)
|
|
73
|
+
return entry;
|
|
74
|
+
}
|
|
75
|
+
if (typeof record.access_token === "string") {
|
|
76
|
+
return record;
|
|
77
|
+
}
|
|
78
|
+
for (const bucket of ["providers", "credential_pool"]) {
|
|
79
|
+
const pool = record[bucket];
|
|
80
|
+
if (!pool || typeof pool !== "object")
|
|
81
|
+
continue;
|
|
82
|
+
const provider = pool["xai-oauth"];
|
|
83
|
+
if (!provider || typeof provider !== "object")
|
|
84
|
+
continue;
|
|
85
|
+
const prov = provider;
|
|
86
|
+
const tokens = prov.tokens && typeof prov.tokens === "object"
|
|
87
|
+
? prov.tokens
|
|
88
|
+
: undefined;
|
|
89
|
+
if (tokens && typeof tokens.access_token === "string") {
|
|
90
|
+
const entry = {
|
|
91
|
+
access_token: tokens.access_token,
|
|
92
|
+
provider: "xai-oauth",
|
|
93
|
+
};
|
|
94
|
+
if (typeof tokens.refresh_token === "string") {
|
|
95
|
+
entry.refresh_token = tokens.refresh_token;
|
|
96
|
+
}
|
|
97
|
+
const expiresAt = resolveHermesExpiresAt(prov, tokens);
|
|
98
|
+
if (expiresAt !== undefined)
|
|
99
|
+
entry.expires_at = expiresAt;
|
|
100
|
+
if (typeof tokens.token_type === "string")
|
|
101
|
+
entry.token_type = tokens.token_type;
|
|
102
|
+
return entry;
|
|
103
|
+
}
|
|
104
|
+
if (typeof prov.access_token === "string") {
|
|
105
|
+
return prov;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
45
110
|
function readHermesOAuthToken() {
|
|
46
111
|
try {
|
|
47
112
|
const authPath = join(homedir(), ".hermes", "auth.json");
|
|
@@ -49,16 +114,7 @@ function readHermesOAuthToken() {
|
|
|
49
114
|
return null;
|
|
50
115
|
const raw = readFileSync(authPath, "utf-8");
|
|
51
116
|
const data = JSON.parse(raw);
|
|
52
|
-
|
|
53
|
-
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
54
|
-
entry = data["xai-oauth"];
|
|
55
|
-
if (!entry?.access_token && data.access_token) {
|
|
56
|
-
entry = data;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
if (!entry?.access_token && Array.isArray(data)) {
|
|
60
|
-
entry = data.find((e) => e.provider === "xai-oauth" || !!e.access_token);
|
|
61
|
-
}
|
|
117
|
+
const entry = extractHermesOAuthEntry(data);
|
|
62
118
|
if (!entry?.access_token) {
|
|
63
119
|
process.stderr.write("[llm] readHermesOAuthToken: no access_token in auth file\n");
|
|
64
120
|
return null;
|
|
@@ -29,6 +29,7 @@ declare class GovernanceServer extends XrayKnowledgeSkillBase {
|
|
|
29
29
|
connect(transport: Transport): Promise<void>;
|
|
30
30
|
/**
|
|
31
31
|
* Run as HTTP server using Streamable HTTP transport (for Grok CLI compatibility).
|
|
32
|
+
* Uses per-session transports so concurrent clients can each initialize independently.
|
|
32
33
|
*/
|
|
33
34
|
runHttp(port?: number): Promise<void>;
|
|
34
35
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
15
15
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
16
|
-
import { CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
16
|
+
import { CallToolRequestSchema, isInitializeRequest, } from "@modelcontextprotocol/sdk/types.js";
|
|
17
17
|
import { XrayKnowledgeSkillBase } from "./shared/knowledge-skill-base.js";
|
|
18
18
|
import { frameworkLogger } from "../core/framework-logger.js";
|
|
19
19
|
import { featuresConfigLoader } from "../core/features-config.js";
|
|
@@ -351,6 +351,7 @@ class GovernanceServer extends XrayKnowledgeSkillBase {
|
|
|
351
351
|
}
|
|
352
352
|
/**
|
|
353
353
|
* Run as HTTP server using Streamable HTTP transport (for Grok CLI compatibility).
|
|
354
|
+
* Uses per-session transports so concurrent clients can each initialize independently.
|
|
354
355
|
*/
|
|
355
356
|
async runHttp(port = parseInt(process.env.MCP_PORT ?? "3100", 10)) {
|
|
356
357
|
await this.initializeGovernance();
|
|
@@ -360,10 +361,7 @@ class GovernanceServer extends XrayKnowledgeSkillBase {
|
|
|
360
361
|
host: mcpHost,
|
|
361
362
|
...(allowedHosts ? { allowedHosts } : {}),
|
|
362
363
|
});
|
|
363
|
-
const
|
|
364
|
-
sessionIdGenerator: () => randomUUID(),
|
|
365
|
-
});
|
|
366
|
-
await this.server.connect(transport);
|
|
364
|
+
const transports = {};
|
|
367
365
|
const apiKey = process.env.GOVERNANCE_API_KEY;
|
|
368
366
|
if (apiKey) {
|
|
369
367
|
frameworkLogger.log("governance-mcp", "api-key-auth-enabled", "info", {});
|
|
@@ -383,8 +381,45 @@ class GovernanceServer extends XrayKnowledgeSkillBase {
|
|
|
383
381
|
}
|
|
384
382
|
next();
|
|
385
383
|
});
|
|
386
|
-
|
|
384
|
+
const mcpPostHandler = async (req, res) => {
|
|
387
385
|
try {
|
|
386
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
387
|
+
let transport;
|
|
388
|
+
if (sessionId && transports[sessionId]) {
|
|
389
|
+
transport = transports[sessionId];
|
|
390
|
+
}
|
|
391
|
+
else if (!sessionId && isInitializeRequest(req.body)) {
|
|
392
|
+
transport = new StreamableHTTPServerTransport({
|
|
393
|
+
sessionIdGenerator: () => randomUUID(),
|
|
394
|
+
onsessioninitialized: (sid) => {
|
|
395
|
+
if (transport) {
|
|
396
|
+
transports[sid] = transport;
|
|
397
|
+
frameworkLogger.log("governance-mcp", "session-initialized", "info", {
|
|
398
|
+
sessionId: sid,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
transport.onclose = () => {
|
|
404
|
+
const sid = transport?.sessionId;
|
|
405
|
+
if (sid && transports[sid]) {
|
|
406
|
+
delete transports[sid];
|
|
407
|
+
frameworkLogger.log("governance-mcp", "session-closed", "info", { sessionId: sid });
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
const sessionServer = new GovernanceServer();
|
|
411
|
+
await sessionServer.connect(transport);
|
|
412
|
+
await transport.handleRequest(req, res, req.body);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
res.status(400).json({
|
|
417
|
+
jsonrpc: "2.0",
|
|
418
|
+
error: { code: -32000, message: "Bad Request: No valid session ID provided" },
|
|
419
|
+
id: null,
|
|
420
|
+
});
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
388
423
|
await transport.handleRequest(req, res, req.body);
|
|
389
424
|
}
|
|
390
425
|
catch (error) {
|
|
@@ -393,15 +428,51 @@ class GovernanceServer extends XrayKnowledgeSkillBase {
|
|
|
393
428
|
res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal error" }, id: null });
|
|
394
429
|
}
|
|
395
430
|
}
|
|
431
|
+
};
|
|
432
|
+
app.post("/mcp", mcpPostHandler);
|
|
433
|
+
app.get("/mcp", async (req, res) => {
|
|
434
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
435
|
+
if (!sessionId || !transports[sessionId]) {
|
|
436
|
+
res.status(400).send("Invalid or missing session ID");
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
await transports[sessionId].handleRequest(req, res);
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
frameworkLogger.log("governance-mcp", "http-sse-error", "error", { error: String(error) });
|
|
444
|
+
if (!res.headersSent) {
|
|
445
|
+
res.status(500).send("Error processing SSE stream");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
app.delete("/mcp", async (req, res) => {
|
|
450
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
451
|
+
if (!sessionId || !transports[sessionId]) {
|
|
452
|
+
res.status(400).send("Invalid or missing session ID");
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
await transports[sessionId].handleRequest(req, res);
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
frameworkLogger.log("governance-mcp", "http-delete-error", "error", { error: String(error) });
|
|
460
|
+
if (!res.headersSent) {
|
|
461
|
+
res.status(500).send("Error processing session termination");
|
|
462
|
+
}
|
|
463
|
+
}
|
|
396
464
|
});
|
|
397
465
|
app.get("/health", (_req, res) => {
|
|
398
466
|
res.json({ status: "ok", server: "governance" });
|
|
399
467
|
});
|
|
468
|
+
const closeAllTransports = async () => {
|
|
469
|
+
await Promise.all(Object.values(transports).map((t) => t.close()));
|
|
470
|
+
};
|
|
400
471
|
app.listen(port, () => {
|
|
401
472
|
frameworkLogger.log("governance-mcp", "http-listening", "info", { port });
|
|
402
473
|
});
|
|
403
|
-
process.on("SIGINT", () => {
|
|
404
|
-
process.on("SIGTERM", () => {
|
|
474
|
+
process.on("SIGINT", () => { void closeAllTransports().then(() => process.exit(0)); });
|
|
475
|
+
process.on("SIGTERM", () => { void closeAllTransports().then(() => process.exit(0)); });
|
|
405
476
|
}
|
|
406
477
|
}
|
|
407
478
|
// Start the server if this file is run directly
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* consumer-install-smoke.mjs — Clean-room pack + install verification.
|
|
4
4
|
*
|
|
5
5
|
* Simulates what consumers receive from npm without publishing.
|
|
6
|
+
* Runs fresh install checks + upgrade merge simulation.
|
|
6
7
|
*
|
|
7
8
|
* Usage:
|
|
8
9
|
* node scripts/node/consumer-install-smoke.mjs
|
|
@@ -38,6 +39,85 @@ function run(cmd, cwd, { inherit = false } = {}) {
|
|
|
38
39
|
return inherit ? "" : String(result).trim();
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
function assertFreshInstallDefaults(tmpRoot, version) {
|
|
43
|
+
const consumerFeaturesPath = path.join(tmpRoot, ".xray", "features.json");
|
|
44
|
+
if (!fs.existsSync(consumerFeaturesPath)) {
|
|
45
|
+
throw new Error(".xray/features.json not deployed to consumer project by postinstall");
|
|
46
|
+
}
|
|
47
|
+
const features = JSON.parse(fs.readFileSync(consumerFeaturesPath, "utf-8"));
|
|
48
|
+
if (!features.memory_routing) {
|
|
49
|
+
throw new Error("Deployed features.json missing memory_routing block");
|
|
50
|
+
}
|
|
51
|
+
if (features.memory_routing.enabled !== false) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Fresh install must default memory_routing.enabled=false (got ${features.memory_routing.enabled})`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (!features.inference_governance) {
|
|
57
|
+
throw new Error("Deployed features.json missing inference_governance block");
|
|
58
|
+
}
|
|
59
|
+
if (features.inference_governance.enabled !== false) {
|
|
60
|
+
throw new Error("Fresh install must default inference_governance.enabled=false");
|
|
61
|
+
}
|
|
62
|
+
console.log(" ✅ .xray/features.json fresh-install defaults (opt-in off)");
|
|
63
|
+
return { consumerFeaturesPath, features };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function runUpgradeMergeSmoke(tmpRoot, nmRoot, version) {
|
|
67
|
+
console.log("\n📋 Upgrade merge smoke (consumer opt-ins preserved)...");
|
|
68
|
+
|
|
69
|
+
const { deployXrayConfig } = require(path.join(nmRoot, "scripts/node/install-bridges.cjs"));
|
|
70
|
+
const consumerFeaturesPath = path.join(tmpRoot, ".xray", "features.json");
|
|
71
|
+
|
|
72
|
+
fs.writeFileSync(
|
|
73
|
+
consumerFeaturesPath,
|
|
74
|
+
`${JSON.stringify(
|
|
75
|
+
{
|
|
76
|
+
version: "3.4.0",
|
|
77
|
+
memory_routing: {
|
|
78
|
+
enabled: true,
|
|
79
|
+
provider: "repertoire",
|
|
80
|
+
module_path: "dist/provider/memory-routing-provider.js",
|
|
81
|
+
},
|
|
82
|
+
inference_governance: { enabled: true },
|
|
83
|
+
consumer_custom_block: { kept: true },
|
|
84
|
+
},
|
|
85
|
+
null,
|
|
86
|
+
2,
|
|
87
|
+
)}\n`,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const shippedTemplate = JSON.parse(
|
|
91
|
+
fs.readFileSync(path.join(nmRoot, "xray/features.json"), "utf-8"),
|
|
92
|
+
);
|
|
93
|
+
const shippedMarker = shippedTemplate.token_optimization ? "token_optimization" : null;
|
|
94
|
+
|
|
95
|
+
deployXrayConfig(tmpRoot, nmRoot, () => {});
|
|
96
|
+
|
|
97
|
+
const merged = JSON.parse(fs.readFileSync(consumerFeaturesPath, "utf-8"));
|
|
98
|
+
|
|
99
|
+
if (merged.memory_routing?.enabled !== true) {
|
|
100
|
+
throw new Error(`Upgrade merge lost memory_routing.enabled (got ${merged.memory_routing?.enabled})`);
|
|
101
|
+
}
|
|
102
|
+
if (merged.memory_routing?.provider !== "repertoire") {
|
|
103
|
+
throw new Error(`Upgrade merge lost memory_routing.provider (got ${merged.memory_routing?.provider})`);
|
|
104
|
+
}
|
|
105
|
+
if (merged.inference_governance?.enabled !== true) {
|
|
106
|
+
throw new Error("Upgrade merge lost inference_governance.enabled");
|
|
107
|
+
}
|
|
108
|
+
if (merged.consumer_custom_block?.kept !== true) {
|
|
109
|
+
throw new Error("Upgrade merge dropped consumer-only keys");
|
|
110
|
+
}
|
|
111
|
+
if (merged.version !== version) {
|
|
112
|
+
throw new Error(`Upgrade merge version: expected ${version}, got ${merged.version}`);
|
|
113
|
+
}
|
|
114
|
+
if (shippedMarker && !merged[shippedMarker]) {
|
|
115
|
+
throw new Error(`Upgrade merge missing shipped template key: ${shippedMarker}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log(" ✅ upgrade merge preserves opt-ins + bumps features.version");
|
|
119
|
+
}
|
|
120
|
+
|
|
41
121
|
function main() {
|
|
42
122
|
console.log("\n=== Consumer Install Smoke ===\n");
|
|
43
123
|
|
|
@@ -94,7 +174,7 @@ function main() {
|
|
|
94
174
|
}
|
|
95
175
|
|
|
96
176
|
const bridges = require(path.join(nmRoot, "scripts/node/install-bridges.cjs"));
|
|
97
|
-
for (const exp of ["installAllBridges", "resolveConsumerTargetDir", "isConsumerInstall"]) {
|
|
177
|
+
for (const exp of ["installAllBridges", "resolveConsumerTargetDir", "isConsumerInstall", "deployXrayConfig"]) {
|
|
98
178
|
if (typeof bridges[exp] !== "function") {
|
|
99
179
|
throw new Error(`install-bridges missing export: ${exp}`);
|
|
100
180
|
}
|
|
@@ -106,7 +186,6 @@ function main() {
|
|
|
106
186
|
}
|
|
107
187
|
console.log(" ✅ consumer target resolution");
|
|
108
188
|
|
|
109
|
-
// Postinstall should have run; verify .mcp.json merge
|
|
110
189
|
const mcpPath = path.join(tmpRoot, ".mcp.json");
|
|
111
190
|
if (!fs.existsSync(mcpPath)) {
|
|
112
191
|
throw new Error(".mcp.json not created by postinstall");
|
|
@@ -124,26 +203,7 @@ function main() {
|
|
|
124
203
|
}
|
|
125
204
|
console.log(` ✅ .mcp.json has ${XRAY_MCP_NAMES.length} npx MCP servers`);
|
|
126
205
|
|
|
127
|
-
|
|
128
|
-
if (!fs.existsSync(consumerFeaturesPath)) {
|
|
129
|
-
throw new Error(".xray/features.json not deployed to consumer project by postinstall");
|
|
130
|
-
}
|
|
131
|
-
const features = JSON.parse(fs.readFileSync(consumerFeaturesPath, "utf-8"));
|
|
132
|
-
if (!features.memory_routing) {
|
|
133
|
-
throw new Error("Deployed features.json missing memory_routing block");
|
|
134
|
-
}
|
|
135
|
-
if (features.memory_routing.enabled !== false) {
|
|
136
|
-
throw new Error(
|
|
137
|
-
`Shipped template must default memory_routing.enabled=false (got ${features.memory_routing.enabled})`,
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
if (!features.inference_governance) {
|
|
141
|
-
throw new Error("Deployed features.json missing inference_governance block");
|
|
142
|
-
}
|
|
143
|
-
if (features.inference_governance.enabled !== false) {
|
|
144
|
-
throw new Error("Shipped template must default inference_governance.enabled=false");
|
|
145
|
-
}
|
|
146
|
-
console.log(" ✅ .xray/features.json deployed (memory_routing + inference_governance opt-in defaults)");
|
|
206
|
+
assertFreshInstallDefaults(tmpRoot, version);
|
|
147
207
|
|
|
148
208
|
const schemaInPackage = path.join(nmRoot, "xray", "features.schema.json");
|
|
149
209
|
if (!fs.existsSync(schemaInPackage)) {
|
|
@@ -151,6 +211,8 @@ function main() {
|
|
|
151
211
|
}
|
|
152
212
|
console.log(" ✅ xray/features.schema.json in package");
|
|
153
213
|
|
|
214
|
+
runUpgradeMergeSmoke(tmpRoot, nmRoot, version);
|
|
215
|
+
|
|
154
216
|
console.log("\n✅ Consumer install smoke passed\n");
|
|
155
217
|
} catch (err) {
|
|
156
218
|
console.error(`\n❌ Consumer smoke failed: ${err.message}\n`);
|
|
@@ -19,11 +19,24 @@ const targetDir = resolveConsumerTargetDir(packageRoot, process.env.PWD || proce
|
|
|
19
19
|
const resolvedPackage = path.resolve(packageRoot);
|
|
20
20
|
const resolvedTarget = path.resolve(targetDir);
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
const XRAY_MANAGED_AGENTS_MARKER = "<!-- 0xray-managed -->";
|
|
23
|
+
|
|
24
|
+
// Copy AGENTS-consumer.md → AGENTS.md (only if absent or still 0xray-managed)
|
|
23
25
|
const agentsConsumer = path.join(packageRoot, "AGENTS-consumer.md");
|
|
24
26
|
const agentsDest = path.join(targetDir, "AGENTS.md");
|
|
25
27
|
if (fs.existsSync(agentsConsumer) && isConsumerInstall(resolvedPackage, resolvedTarget)) {
|
|
26
|
-
|
|
28
|
+
const shouldDeployAgents =
|
|
29
|
+
!fs.existsSync(agentsDest) ||
|
|
30
|
+
fs.readFileSync(agentsDest, "utf8").includes(XRAY_MANAGED_AGENTS_MARKER);
|
|
31
|
+
if (shouldDeployAgents) {
|
|
32
|
+
let content = fs.readFileSync(agentsConsumer, "utf8");
|
|
33
|
+
if (!content.includes(XRAY_MANAGED_AGENTS_MARKER)) {
|
|
34
|
+
content = `${content.trimEnd()}\n\n${XRAY_MANAGED_AGENTS_MARKER}\n`;
|
|
35
|
+
}
|
|
36
|
+
fs.writeFileSync(agentsDest, content);
|
|
37
|
+
} else {
|
|
38
|
+
structuredLog("postinstall", "Skipped AGENTS.md (consumer-customized)", "info");
|
|
39
|
+
}
|
|
27
40
|
}
|
|
28
41
|
|
|
29
42
|
// Copy .gitignore.default → .gitignore (never overwrite existing)
|
|
@@ -43,7 +56,9 @@ if (isConsumerInstall(resolvedPackage, resolvedTarget)) {
|
|
|
43
56
|
log: structuredLog,
|
|
44
57
|
});
|
|
45
58
|
} catch (e) {
|
|
46
|
-
structuredLog("postinstall", "Bridge install failed", "
|
|
59
|
+
structuredLog("postinstall", "Bridge install failed", "error", { error: e.message });
|
|
60
|
+
console.error("\n❌ 0xRay postinstall failed — bridge wiring did not complete.\n");
|
|
61
|
+
process.exit(1);
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
structuredLog(
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* release-gate.mjs — Single release checkpoint.
|
|
4
4
|
*
|
|
5
5
|
* Modes:
|
|
6
|
-
* (default) build + test + consumer smoke
|
|
7
|
-
* --verify-only git + reconcile + release docs + consumer smoke — after push, before
|
|
6
|
+
* (default) build + test + consumer smoke (fresh + upgrade merge)
|
|
7
|
+
* --verify-only git + reconcile + release docs + consumer smoke — after push, before publish
|
|
8
8
|
* --skip-smoke skip consumer install smoke
|
|
9
9
|
* --skip-docs skip release docs validation (not recommended for publish)
|
|
10
10
|
*
|
|
@@ -36,21 +36,26 @@ function main() {
|
|
|
36
36
|
|
|
37
37
|
try {
|
|
38
38
|
if (verifyOnly) {
|
|
39
|
-
step("1/
|
|
39
|
+
step("1/5 Git + reconcile", "node scripts/node/pre-publish-guard.js --verify-only");
|
|
40
40
|
if (!skipDocs) {
|
|
41
|
-
step("2/
|
|
41
|
+
step("2/5 Release docs", "node scripts/node/validate-release-docs.mjs");
|
|
42
42
|
}
|
|
43
|
+
step(
|
|
44
|
+
"3/5 Plugin infrastructure",
|
|
45
|
+
"node scripts/test/validate-plugins-e2e.cjs --structural-only",
|
|
46
|
+
);
|
|
43
47
|
if (!skipSmoke) {
|
|
44
|
-
step("
|
|
48
|
+
step("4/5 Consumer install smoke", "node scripts/node/consumer-install-smoke.mjs");
|
|
45
49
|
}
|
|
46
50
|
} else {
|
|
47
|
-
step("1/
|
|
48
|
-
step("2/
|
|
51
|
+
step("1/5 Build", "npm run build");
|
|
52
|
+
step("2/5 Tests", "npm test");
|
|
53
|
+
step(
|
|
54
|
+
"3/5 Plugin infrastructure",
|
|
55
|
+
"node scripts/test/validate-plugins-e2e.cjs --structural-only",
|
|
56
|
+
);
|
|
49
57
|
if (!skipSmoke) {
|
|
50
|
-
step("
|
|
51
|
-
}
|
|
52
|
-
if (!skipDocs && process.argv.includes("--with-docs")) {
|
|
53
|
-
step("4/4 Release docs", "node scripts/node/validate-release-docs.mjs");
|
|
58
|
+
step("4/5 Consumer install smoke (fresh + upgrade)", "node scripts/node/consumer-install-smoke.mjs");
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
61
|
|
package/scripts/node/release.mjs
CHANGED
|
@@ -4,16 +4,15 @@
|
|
|
4
4
|
* Canonical 0xRay release script.
|
|
5
5
|
*
|
|
6
6
|
* 1. Bump version from npm registry baseline (reconcile-version)
|
|
7
|
-
* 2. Release
|
|
8
|
-
* 3.
|
|
7
|
+
* 2. Release artifacts (CHANGELOG / README / AGENTS / docs) — before gate so docs tests pass
|
|
8
|
+
* 3. Release gate (build + test + consumer smoke)
|
|
9
9
|
* 4. Commit release artifacts → push
|
|
10
10
|
* 5. Verify gate (reconcile + git + release docs + smoke)
|
|
11
|
-
* 6.
|
|
11
|
+
* 6. npm publish (idempotent — skip if version already on registry)
|
|
12
|
+
* 7. Tag → push tag (only after successful publish)
|
|
12
13
|
*
|
|
13
14
|
* Usage:
|
|
14
15
|
* npm run release:patch
|
|
15
|
-
* npm run release:minor
|
|
16
|
-
* npm run release:major
|
|
17
16
|
* node scripts/node/release.mjs patch --dry-run
|
|
18
17
|
*/
|
|
19
18
|
|
|
@@ -44,6 +43,32 @@ function currentBranch() {
|
|
|
44
43
|
return execSync("git rev-parse --abbrev-ref HEAD", { cwd: rootDir, encoding: "utf-8" }).trim();
|
|
45
44
|
}
|
|
46
45
|
|
|
46
|
+
function npmVersionPublished(version) {
|
|
47
|
+
try {
|
|
48
|
+
const out = execSync(`npm view 0xray@${version} version`, {
|
|
49
|
+
cwd: rootDir,
|
|
50
|
+
encoding: "utf-8",
|
|
51
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
52
|
+
}).trim();
|
|
53
|
+
return out === version;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function publishIdempotent(version) {
|
|
60
|
+
if (dryRun) {
|
|
61
|
+
console.log(` would: npm publish --access public (check 0xray@${version} first)`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (npmVersionPublished(version)) {
|
|
65
|
+
console.log(`ℹ️ 0xray@${version} already on npm — skipping publish`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
execSync("npm publish --access public", { cwd: rootDir, stdio: "inherit", encoding: "utf-8" });
|
|
69
|
+
console.log(`✅ Published 0xray@${version}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
47
72
|
async function main() {
|
|
48
73
|
if (!["major", "minor", "patch"].includes(releaseType)) {
|
|
49
74
|
console.error("Usage: node scripts/node/release.mjs [patch|minor|major] [--dry-run]");
|
|
@@ -70,14 +95,14 @@ async function main() {
|
|
|
70
95
|
: readVersion();
|
|
71
96
|
console.log(`📌 Release version: ${newVersion}`);
|
|
72
97
|
|
|
73
|
-
// 2.
|
|
74
|
-
console.log("\n📦 Step 2: Release
|
|
75
|
-
run("node scripts/node/release-gate.mjs", "release gate");
|
|
76
|
-
|
|
77
|
-
// 3. Changelog + doc artifacts
|
|
78
|
-
console.log("\n📦 Step 3: Release artifacts (CHANGELOG, README, AGENTS, docs)");
|
|
98
|
+
// 2. Artifacts BEFORE gate (docs vitest matches package.json version)
|
|
99
|
+
console.log("\n📦 Step 2: Release artifacts (CHANGELOG, README, AGENTS, docs)");
|
|
79
100
|
run("node scripts/node/version-manager.mjs --artifacts-only", "release artifacts");
|
|
80
101
|
|
|
102
|
+
// 3. Gate
|
|
103
|
+
console.log("\n📦 Step 3: Release gate (build + test + smoke)");
|
|
104
|
+
run("node scripts/node/release-gate.mjs", "release gate");
|
|
105
|
+
|
|
81
106
|
// 4. Commit + push
|
|
82
107
|
console.log("\n📦 Step 4: Commit & push");
|
|
83
108
|
const releaseFiles = getReleaseArtifactPaths();
|
|
@@ -97,24 +122,33 @@ async function main() {
|
|
|
97
122
|
const branch = currentBranch();
|
|
98
123
|
run(`git push origin ${branch}`, "git push");
|
|
99
124
|
|
|
100
|
-
// 5. Post-push verify before
|
|
101
|
-
console.log("\n📦 Step 5: Verify gate (reconcile + git + smoke)");
|
|
125
|
+
// 5. Post-push verify before publish
|
|
126
|
+
console.log("\n📦 Step 5: Verify gate (reconcile + git + docs + smoke)");
|
|
102
127
|
run("node scripts/node/release-gate.mjs --verify-only", "verify gate");
|
|
103
128
|
|
|
104
|
-
// 6.
|
|
105
|
-
console.log("\n📦 Step 6:
|
|
129
|
+
// 6. Publish (idempotent)
|
|
130
|
+
console.log("\n📦 Step 6: npm publish");
|
|
131
|
+
publishIdempotent(newVersion);
|
|
132
|
+
|
|
133
|
+
// 7. Tag after publish
|
|
134
|
+
console.log("\n📦 Step 7: Tag");
|
|
106
135
|
if (!dryRun) {
|
|
107
|
-
|
|
108
|
-
|
|
136
|
+
try {
|
|
137
|
+
execSync(`git tag -a v${newVersion} -m "Release v${newVersion}"`, { cwd: rootDir, stdio: "inherit" });
|
|
138
|
+
console.log(`✅ Created tag v${newVersion}`);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
const msg = String(e.stderr || e.message || "");
|
|
141
|
+
if (msg.includes("already exists")) {
|
|
142
|
+
console.log(`ℹ️ Tag v${newVersion} already exists`);
|
|
143
|
+
} else {
|
|
144
|
+
throw e;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
109
147
|
} else {
|
|
110
148
|
console.log(` would: git tag -a v${newVersion}`);
|
|
111
149
|
}
|
|
112
150
|
run(`git push origin v${newVersion}`, "push tag");
|
|
113
151
|
|
|
114
|
-
// 7. Publish
|
|
115
|
-
console.log("\n📦 Step 7: npm publish");
|
|
116
|
-
run("npm publish --access public", "npm publish");
|
|
117
|
-
|
|
118
152
|
console.log("\n╔════════════════════════════════════════════════════════╗");
|
|
119
153
|
console.log("║ ✅ Release Complete! ║");
|
|
120
154
|
console.log("╚════════════════════════════════════════════════════════╝");
|
|
@@ -125,7 +159,7 @@ async function main() {
|
|
|
125
159
|
fs.mkdirSync(path.join(rootDir, "tweets"), { recursive: true });
|
|
126
160
|
fs.writeFileSync(
|
|
127
161
|
tweetPath,
|
|
128
|
-
`🎉 0xRay v${newVersion} is LIVE -
|
|
162
|
+
`🎉 0xRay v${newVersion} is LIVE - consumer-safe upgrades!\n...\n\`\`\`\nnpm install 0xray@latest\n\`\`\`\n`,
|
|
129
163
|
);
|
|
130
164
|
console.log(`📝 Tweet template: tweets/v${newVersion}.md`);
|
|
131
165
|
}
|
|
@@ -18,6 +18,7 @@ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js
|
|
|
18
18
|
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
19
19
|
import {
|
|
20
20
|
CallToolRequestSchema,
|
|
21
|
+
isInitializeRequest,
|
|
21
22
|
type CallToolResult,
|
|
22
23
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
23
24
|
import { XrayKnowledgeSkillBase } from "./shared/knowledge-skill-base.js";
|
|
@@ -419,6 +420,7 @@ class GovernanceServer extends XrayKnowledgeSkillBase {
|
|
|
419
420
|
|
|
420
421
|
/**
|
|
421
422
|
* Run as HTTP server using Streamable HTTP transport (for Grok CLI compatibility).
|
|
423
|
+
* Uses per-session transports so concurrent clients can each initialize independently.
|
|
422
424
|
*/
|
|
423
425
|
async runHttp(port: number = parseInt(process.env.MCP_PORT ?? "3100", 10)): Promise<void> {
|
|
424
426
|
await this.initializeGovernance();
|
|
@@ -429,11 +431,7 @@ class GovernanceServer extends XrayKnowledgeSkillBase {
|
|
|
429
431
|
host: mcpHost,
|
|
430
432
|
...(allowedHosts ? { allowedHosts } : {}),
|
|
431
433
|
});
|
|
432
|
-
const
|
|
433
|
-
sessionIdGenerator: () => randomUUID(),
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
await this.server.connect(transport as any);
|
|
434
|
+
const transports: Record<string, StreamableHTTPServerTransport> = {};
|
|
437
435
|
|
|
438
436
|
const apiKey = process.env.GOVERNANCE_API_KEY;
|
|
439
437
|
if (apiKey) {
|
|
@@ -455,8 +453,45 @@ class GovernanceServer extends XrayKnowledgeSkillBase {
|
|
|
455
453
|
next();
|
|
456
454
|
});
|
|
457
455
|
|
|
458
|
-
|
|
456
|
+
const mcpPostHandler = async (req: any, res: any) => {
|
|
459
457
|
try {
|
|
458
|
+
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
459
|
+
let transport: StreamableHTTPServerTransport | undefined;
|
|
460
|
+
|
|
461
|
+
if (sessionId && transports[sessionId]) {
|
|
462
|
+
transport = transports[sessionId];
|
|
463
|
+
} else if (!sessionId && isInitializeRequest(req.body)) {
|
|
464
|
+
transport = new StreamableHTTPServerTransport({
|
|
465
|
+
sessionIdGenerator: () => randomUUID(),
|
|
466
|
+
onsessioninitialized: (sid) => {
|
|
467
|
+
if (transport) {
|
|
468
|
+
transports[sid] = transport;
|
|
469
|
+
frameworkLogger.log("governance-mcp", "session-initialized", "info", {
|
|
470
|
+
sessionId: sid,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
transport.onclose = () => {
|
|
476
|
+
const sid = transport?.sessionId;
|
|
477
|
+
if (sid && transports[sid]) {
|
|
478
|
+
delete transports[sid];
|
|
479
|
+
frameworkLogger.log("governance-mcp", "session-closed", "info", { sessionId: sid });
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
const sessionServer = new GovernanceServer();
|
|
483
|
+
await sessionServer.connect(transport as Transport);
|
|
484
|
+
await transport.handleRequest(req, res, req.body);
|
|
485
|
+
return;
|
|
486
|
+
} else {
|
|
487
|
+
res.status(400).json({
|
|
488
|
+
jsonrpc: "2.0",
|
|
489
|
+
error: { code: -32000, message: "Bad Request: No valid session ID provided" },
|
|
490
|
+
id: null,
|
|
491
|
+
});
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
460
495
|
await transport.handleRequest(req, res, req.body);
|
|
461
496
|
} catch (error) {
|
|
462
497
|
frameworkLogger.log("governance-mcp", "http-handler-error", "error", { error: String(error) });
|
|
@@ -464,18 +499,56 @@ class GovernanceServer extends XrayKnowledgeSkillBase {
|
|
|
464
499
|
res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal error" }, id: null });
|
|
465
500
|
}
|
|
466
501
|
}
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
app.post("/mcp", mcpPostHandler);
|
|
505
|
+
|
|
506
|
+
app.get("/mcp", async (req: any, res: any) => {
|
|
507
|
+
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
508
|
+
if (!sessionId || !transports[sessionId]) {
|
|
509
|
+
res.status(400).send("Invalid or missing session ID");
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
await transports[sessionId].handleRequest(req, res);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
frameworkLogger.log("governance-mcp", "http-sse-error", "error", { error: String(error) });
|
|
516
|
+
if (!res.headersSent) {
|
|
517
|
+
res.status(500).send("Error processing SSE stream");
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
app.delete("/mcp", async (req: any, res: any) => {
|
|
523
|
+
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
524
|
+
if (!sessionId || !transports[sessionId]) {
|
|
525
|
+
res.status(400).send("Invalid or missing session ID");
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
await transports[sessionId].handleRequest(req, res);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
frameworkLogger.log("governance-mcp", "http-delete-error", "error", { error: String(error) });
|
|
532
|
+
if (!res.headersSent) {
|
|
533
|
+
res.status(500).send("Error processing session termination");
|
|
534
|
+
}
|
|
535
|
+
}
|
|
467
536
|
});
|
|
468
537
|
|
|
469
538
|
app.get("/health", (_req: any, res: any) => {
|
|
470
539
|
res.json({ status: "ok", server: "governance" });
|
|
471
540
|
});
|
|
472
541
|
|
|
542
|
+
const closeAllTransports = async () => {
|
|
543
|
+
await Promise.all(Object.values(transports).map((t) => t.close()));
|
|
544
|
+
};
|
|
545
|
+
|
|
473
546
|
app.listen(port, () => {
|
|
474
547
|
frameworkLogger.log("governance-mcp", "http-listening", "info", { port });
|
|
475
548
|
});
|
|
476
549
|
|
|
477
|
-
process.on("SIGINT", () => {
|
|
478
|
-
process.on("SIGTERM", () => {
|
|
550
|
+
process.on("SIGINT", () => { void closeAllTransports().then(() => process.exit(0)); });
|
|
551
|
+
process.on("SIGTERM", () => { void closeAllTransports().then(() => process.exit(0)); });
|
|
479
552
|
}
|
|
480
553
|
}
|
|
481
554
|
|