@sentropic/h2a-runtime 0.98.7-rc.4 → 0.98.7-rc.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.
Files changed (89) hide show
  1. package/dist/agy-claude.d.ts +41 -1
  2. package/dist/agy-claude.d.ts.map +1 -1
  3. package/dist/agy-claude.js +147 -2
  4. package/dist/agy-claude.js.map +1 -1
  5. package/dist/central-mcp.d.ts +32 -0
  6. package/dist/central-mcp.d.ts.map +1 -1
  7. package/dist/central-mcp.js +186 -69
  8. package/dist/central-mcp.js.map +1 -1
  9. package/dist/conv-guard.d.ts.map +1 -1
  10. package/dist/conv-guard.js +6 -2
  11. package/dist/conv-guard.js.map +1 -1
  12. package/dist/file-lock.d.ts +4 -2
  13. package/dist/file-lock.d.ts.map +1 -1
  14. package/dist/file-lock.js +24 -10
  15. package/dist/file-lock.js.map +1 -1
  16. package/dist/gateway-host/config-file.d.ts +2 -0
  17. package/dist/gateway-host/config-file.d.ts.map +1 -1
  18. package/dist/gateway-host/config-file.js +5 -1
  19. package/dist/gateway-host/config-file.js.map +1 -1
  20. package/dist/gateway-host/daemon.d.ts.map +1 -1
  21. package/dist/gateway-host/daemon.js +1 -2
  22. package/dist/gateway-host/daemon.js.map +1 -1
  23. package/dist/identity-cull/cull.js +1 -1
  24. package/dist/identity-cull/cull.js.map +1 -1
  25. package/dist/identity-cull/identity-fence.d.ts +48 -0
  26. package/dist/identity-cull/identity-fence.d.ts.map +1 -0
  27. package/dist/identity-cull/identity-fence.js +221 -0
  28. package/dist/identity-cull/identity-fence.js.map +1 -0
  29. package/dist/identity-operator-break.d.ts +43 -0
  30. package/dist/identity-operator-break.d.ts.map +1 -0
  31. package/dist/identity-operator-break.js +85 -0
  32. package/dist/identity-operator-break.js.map +1 -0
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +59 -34
  36. package/dist/index.js.map +1 -1
  37. package/dist/llm-mesh-accounts.d.ts +2 -2
  38. package/dist/llm-mesh-accounts.d.ts.map +1 -1
  39. package/dist/llm-mesh-accounts.js +3 -5
  40. package/dist/llm-mesh-accounts.js.map +1 -1
  41. package/dist/native-host.d.ts +21 -0
  42. package/dist/native-host.d.ts.map +1 -1
  43. package/dist/native-host.js +163 -5
  44. package/dist/native-host.js.map +1 -1
  45. package/dist/native-terminal/fleet.d.ts +11 -0
  46. package/dist/native-terminal/fleet.d.ts.map +1 -1
  47. package/dist/native-terminal/fleet.js +58 -1
  48. package/dist/native-terminal/fleet.js.map +1 -1
  49. package/dist/native-terminal/host-scope.d.ts +18 -0
  50. package/dist/native-terminal/host-scope.d.ts.map +1 -0
  51. package/dist/native-terminal/host-scope.js +65 -0
  52. package/dist/native-terminal/host-scope.js.map +1 -0
  53. package/dist/native-terminal/host.d.ts +1 -0
  54. package/dist/native-terminal/host.d.ts.map +1 -1
  55. package/dist/native-terminal/host.js +4 -1
  56. package/dist/native-terminal/host.js.map +1 -1
  57. package/dist/native-terminal/op.d.ts +36 -1
  58. package/dist/native-terminal/op.d.ts.map +1 -1
  59. package/dist/native-terminal/op.js +190 -33
  60. package/dist/native-terminal/op.js.map +1 -1
  61. package/dist/native-terminal/process.d.ts.map +1 -1
  62. package/dist/native-terminal/process.js +10 -1
  63. package/dist/native-terminal/process.js.map +1 -1
  64. package/dist/native-terminal/screen.d.ts.map +1 -1
  65. package/dist/native-terminal/screen.js +1 -1
  66. package/dist/native-terminal/screen.js.map +1 -1
  67. package/dist/native-terminal/server.d.ts +12 -2
  68. package/dist/native-terminal/server.d.ts.map +1 -1
  69. package/dist/native-terminal/server.js +143 -42
  70. package/dist/native-terminal/server.js.map +1 -1
  71. package/dist/native-terminal/session-scope.d.ts +2 -0
  72. package/dist/native-terminal/session-scope.d.ts.map +1 -0
  73. package/dist/native-terminal/session-scope.js +32 -0
  74. package/dist/native-terminal/session-scope.js.map +1 -0
  75. package/dist/registry.d.ts +5 -12
  76. package/dist/registry.d.ts.map +1 -1
  77. package/dist/registry.js +20 -3
  78. package/dist/registry.js.map +1 -1
  79. package/dist/restore.d.ts.map +1 -1
  80. package/dist/restore.js +5 -37
  81. package/dist/restore.js.map +1 -1
  82. package/dist/run.d.ts.map +1 -1
  83. package/dist/run.js +3 -3
  84. package/dist/run.js.map +1 -1
  85. package/dist/tmux.d.ts +1 -1
  86. package/dist/tmux.d.ts.map +1 -1
  87. package/dist/tmux.js +34 -2
  88. package/dist/tmux.js.map +1 -1
  89. package/package.json +2 -2
@@ -1,2 +1,42 @@
1
- export * from "@sentropic/h2a/agy-claude";
1
+ export declare const AGY_CLAUDE_QUOTA_CODE = "agy-claude-quota-exhausted";
2
+ export type AgyEffort = "low" | "medium" | "high";
3
+ export declare class AgyEffortError extends Error {
4
+ readonly code = "agy-effort-unsupported";
5
+ }
6
+ export declare function normalizeAgyEffort(effort: string | undefined): AgyEffort | undefined;
7
+ export declare function isAgyClaudeModel(model: string | undefined): boolean;
8
+ export declare function resolveAgyModel(model: string | undefined, effort?: string): string | undefined;
9
+ export declare class AgyClaudeBuildOptInError extends Error {
10
+ readonly code = "agy-claude-build-requires-opt-in";
11
+ }
12
+ export declare function assertAgyClaudeBuildAllowed(options: {
13
+ profile: string;
14
+ model?: string | undefined;
15
+ role?: string | undefined;
16
+ allowClaudeBuild?: boolean | undefined;
17
+ }): void;
18
+ export type AgyClaudeQuotaStatus = {
19
+ status: "unknown" | "exhausted";
20
+ observedAt?: string;
21
+ resetsAt?: string;
22
+ signature?: string;
23
+ };
24
+ export declare function detectAgyClaudeQuotaExhaustion(output: string, now?: number): {
25
+ exhausted: boolean;
26
+ signature?: string;
27
+ resetsAt?: string;
28
+ };
29
+ export declare class AgyClaudeQuotaExhaustedError extends Error {
30
+ readonly signature: string;
31
+ readonly code = "agy-claude-quota-exhausted";
32
+ readonly retrySafe = false;
33
+ readonly retryable = false;
34
+ readonly quota: AgyClaudeQuotaStatus;
35
+ constructor(signature: string, output: string, now?: number);
36
+ get resetsAt(): string | undefined;
37
+ }
38
+ export declare function agyClaudeQuotaError(signature: string, output: string, now?: number): AgyClaudeQuotaExhaustedError;
39
+ export declare function agyQuotaStatusPath(): string;
40
+ export declare function readAgyClaudeQuotaStatus(path?: string): AgyClaudeQuotaStatus;
41
+ export declare function writeAgyClaudeQuotaStatus(status: AgyClaudeQuotaStatus, path?: string): void;
2
42
  //# sourceMappingURL=agy-claude.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agy-claude.d.ts","sourceRoot":"","sources":["../src/agy-claude.ts"],"names":[],"mappings":"AACA,cAAc,2BAA2B,CAAC"}
1
+ {"version":3,"file":"agy-claude.d.ts","sourceRoot":"","sources":["../src/agy-claude.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,qBAAqB,+BAA+B,CAAC;AAClE,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAClD,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,4BAA4B;CAC1C;AACD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAKpF;AACD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEnE;AACD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAU9F;AACD,qBAAa,wBAAyB,SAAQ,KAAK;IACjD,QAAQ,CAAC,IAAI,sCAAsC;CACpD;AACD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE;IACnD,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAChH,GAAG,IAAI,CAOP;AACD,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,SAAS,GAAG,WAAW,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AACF,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG;IAChF,SAAS,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3D,CAyCA;AACD,qBAAa,4BAA6B,SAAQ,KAAK;IAKzC,QAAQ,CAAC,SAAS,EAAE,MAAM;IAJtC,QAAQ,CAAC,IAAI,gCAAyB;IACtC,QAAQ,CAAC,SAAS,SAAS;IAC3B,QAAQ,CAAC,SAAS,SAAS;IAC3B,QAAQ,CAAC,KAAK,EAAE,oBAAoB,CAAC;gBAChB,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,SAAa;IAMxE,IAAI,QAAQ,IAAI,MAAM,GAAG,SAAS,CAAgC;CACnE;AACD,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,4BAA4B,CAErH;AACD,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AACD,wBAAgB,wBAAwB,CAAC,IAAI,SAAuB,GAAG,oBAAoB,CAU1F;AACD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,oBAAoB,EAAE,IAAI,SAAuB,GAAG,IAAI,CAKzG"}
@@ -1,3 +1,148 @@
1
- // Small shared contract; importing it must not load either CLI entry point.
2
- export * from "@sentropic/h2a/agy-claude";
1
+ // AGY/Claude shared contract — VENDORED copy of
2
+ // `packages/h2a/src/runtime/agy-claude.ts` (see agy-claude-vendor-sync.test.ts).
3
+ // Why vendored instead of imported: this module is a leaf (node builtins
4
+ // only) that both the runtime and the core need; a cross-package import in
5
+ // either direction creates an undeclared edge that breaks isolated installs
6
+ // of this package (MODULE_NOT_FOUND at host startup) or an unorderable
7
+ // typecheck cycle. Edit the CORE copy first, then sync this file exactly
8
+ // (the sync test fails closed on any drift).
9
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { dirname, join } from "node:path";
12
+ export const AGY_CLAUDE_QUOTA_CODE = "agy-claude-quota-exhausted";
13
+ export class AgyEffortError extends Error {
14
+ code = "agy-effort-unsupported";
15
+ }
16
+ export function normalizeAgyEffort(effort) {
17
+ if (effort === undefined)
18
+ return undefined;
19
+ if (effort === "max")
20
+ return "high";
21
+ if (effort === "low" || effort === "medium" || effort === "high")
22
+ return effort;
23
+ throw new AgyEffortError(`invalid AGY effort ${effort}; AGY effort must be low|medium|high (max maps to high)`);
24
+ }
25
+ export function isAgyClaudeModel(model) {
26
+ return model?.startsWith("claude-") ?? false;
27
+ }
28
+ export function resolveAgyModel(model, effort) {
29
+ const level = normalizeAgyEffort(effort);
30
+ const claude = model && /^(claude-(?:opus|sonnet)-5-5)(?:-(.*))?$/.exec(model);
31
+ if (claude) {
32
+ // Validate a supplied suffix even when a separate effort overrides it.
33
+ const suffix = normalizeAgyEffort(claude[2]);
34
+ const resolved = level ?? suffix;
35
+ return resolved ? `${claude[1]}-${resolved}` : model;
36
+ }
37
+ return model;
38
+ }
39
+ export class AgyClaudeBuildOptInError extends Error {
40
+ code = "agy-claude-build-requires-opt-in";
41
+ }
42
+ export function assertAgyClaudeBuildAllowed(options) {
43
+ if (options.role !== undefined && !["review", "challenge", "build"].includes(options.role)) {
44
+ throw new Error("invalid agy run role: expected review|challenge|build");
45
+ }
46
+ if (options.profile === "agy" && isAgyClaudeModel(options.model) && options.role === "build" && options.allowClaudeBuild !== true) {
47
+ throw new AgyClaudeBuildOptInError("AGY Claude credits are reserved for reviews and adversarial challenges; build requires --allow-claude-build");
48
+ }
49
+ }
50
+ export function detectAgyClaudeQuotaExhaustion(output, now = Date.now()) {
51
+ let preliminary = { exhausted: false };
52
+ const lines = output.split(/\r?\n/);
53
+ for (let index = 0; index < lines.length; index++) {
54
+ let line = lines[index];
55
+ // Provider diagnostics only: prose mentioning 429/quota is not evidence.
56
+ if (!/^\s*(?:AGY_ERROR\s*\{|(?:API )?error\s*:)/i.test(line))
57
+ continue;
58
+ let structured = /AGY_ERROR\s*(\{.*)/.exec(line)?.[1];
59
+ if (structured) {
60
+ // Native/tmux screen captures insert newlines at visual column wraps,
61
+ // including inside JSON strings. Reassemble only an anchored diagnostic,
62
+ // require complete JSON, and never consume the next provider diagnostic.
63
+ let end = index;
64
+ for (;;) {
65
+ try {
66
+ JSON.parse(structured);
67
+ break;
68
+ }
69
+ catch {
70
+ const next = lines[end + 1];
71
+ if (next === undefined || /^\s*(?:AGY_ERROR\s*\{|(?:API )?error\s*:)/i.test(next))
72
+ break;
73
+ structured += next;
74
+ end++;
75
+ }
76
+ }
77
+ try {
78
+ JSON.parse(structured);
79
+ }
80
+ catch {
81
+ continue;
82
+ } // Partial chunks still prove no complete reset.
83
+ line = `AGY_ERROR ${structured}`;
84
+ index = end;
85
+ }
86
+ let signature;
87
+ if (/\b429\b/.test(line))
88
+ signature = "agy-claude:http-429";
89
+ else if (/RESOURCE_EXHAUSTED/i.test(line))
90
+ signature = "agy-claude:resource-exhausted";
91
+ else if (/quota (?:exceeded|reached)|individual quota/i.test(line))
92
+ signature = "agy-claude:quota-exceeded";
93
+ if (!signature)
94
+ continue;
95
+ const duration = /Resets in\s+((?:\d+(?:\.\d+)?\s*[dhms]\s*)+)/i.exec(line)?.[1];
96
+ let ms = 0;
97
+ if (duration)
98
+ for (const part of duration.matchAll(/(\d+(?:\.\d+)?)\s*([dhms])/gi)) {
99
+ ms += Number(part[1]) * ({ d: 86_400_000, h: 3_600_000, m: 60_000, s: 1_000 }[part[2].toLowerCase()] ?? 0);
100
+ }
101
+ const resetsAt = duration && Number.isFinite(now + ms) && ms < 8.64e15 - now
102
+ ? new Date(now + ms).toISOString() : undefined;
103
+ if (resetsAt)
104
+ return { exhausted: true, signature, resetsAt };
105
+ preliminary = { exhausted: true, signature };
106
+ }
107
+ return preliminary;
108
+ }
109
+ export class AgyClaudeQuotaExhaustedError extends Error {
110
+ signature;
111
+ code = AGY_CLAUDE_QUOTA_CODE;
112
+ retrySafe = false;
113
+ retryable = false;
114
+ quota;
115
+ constructor(signature, output, now = Date.now()) {
116
+ super("AGY Claude quota exhausted; no fallback or automatic retry. The caller chooses the next action.");
117
+ this.signature = signature;
118
+ const detected = detectAgyClaudeQuotaExhaustion(output, now);
119
+ this.quota = { status: "exhausted", signature, observedAt: new Date(now).toISOString(),
120
+ ...(detected.resetsAt ? { resetsAt: detected.resetsAt } : {}) };
121
+ }
122
+ get resetsAt() { return this.quota.resetsAt; }
123
+ }
124
+ export function agyClaudeQuotaError(signature, output, now = Date.now()) {
125
+ return new AgyClaudeQuotaExhaustedError(signature, output, now);
126
+ }
127
+ export function agyQuotaStatusPath() {
128
+ return join(process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), "h2a", "agy-claude-quota.json");
129
+ }
130
+ export function readAgyClaudeQuotaStatus(path = agyQuotaStatusPath()) {
131
+ try {
132
+ const value = JSON.parse(readFileSync(path, "utf8"));
133
+ if (value.status === "exhausted" && typeof value.observedAt === "string" && Number.isFinite(Date.parse(value.observedAt))) {
134
+ return { status: value.status, observedAt: value.observedAt,
135
+ ...(typeof value.signature === "string" ? { signature: value.signature } : {}),
136
+ ...(typeof value.resetsAt === "string" && Number.isFinite(Date.parse(value.resetsAt)) ? { resetsAt: value.resetsAt } : {}) };
137
+ }
138
+ }
139
+ catch { /* No provider observation: never invent availability. */ }
140
+ return { status: "unknown" };
141
+ }
142
+ export function writeAgyClaudeQuotaStatus(status, path = agyQuotaStatusPath()) {
143
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
144
+ const temporary = `${path}.${process.pid}.tmp`;
145
+ writeFileSync(temporary, JSON.stringify(status), { mode: 0o600 });
146
+ renameSync(temporary, path);
147
+ }
3
148
  //# sourceMappingURL=agy-claude.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"agy-claude.js","sourceRoot":"","sources":["../src/agy-claude.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,cAAc,2BAA2B,CAAC"}
1
+ {"version":3,"file":"agy-claude.js","sourceRoot":"","sources":["../src/agy-claude.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,17 @@
1
+ type CentralMarker = Readonly<{
2
+ endpoint: string;
3
+ generation: string;
4
+ pid: number;
5
+ startedAt: string;
6
+ }>;
7
+ type CentralPing = Readonly<{
8
+ kind: "generation";
9
+ generation: string;
10
+ }> | Readonly<{
11
+ kind: "dead";
12
+ }> | Readonly<{
13
+ kind: "ambiguous";
14
+ }>;
1
15
  export type CentralMcpPreparation = Readonly<{
2
16
  status: "central";
3
17
  endpoint: string;
@@ -6,6 +20,20 @@ export type CentralMcpPreparation = Readonly<{
6
20
  status: "degraded";
7
21
  reason: string;
8
22
  }>;
23
+ /** Test seam mirroring the core marker-path options. Production always uses
24
+ * `/run/user/<uid>`; the path is never env-derived. */
25
+ export interface CentralMcpPaths {
26
+ readonly runtimeBase?: string;
27
+ }
28
+ export declare const H2A_MCP_CENTRAL_ENV = "H2A_MCP_CENTRAL";
29
+ export declare const H2A_MCP_CENTRAL_ENDPOINT_ENV = "H2A_MCP_CENTRAL_ENDPOINT";
30
+ export declare function centralMcpPing(endpoint: string, timeoutMs?: number): Promise<CentralPing>;
31
+ /**
32
+ * Read the protected central marker without exposing its token to callers
33
+ * that only need liveness/generation metadata. A missing marker is the sole
34
+ * non-error absence case; malformed or insecure state remains a hard failure.
35
+ */
36
+ export declare function readCentralMcpMarker(paths?: CentralMcpPaths): CentralMarker | undefined;
9
37
  /** Raw (unstructured) central MCP server output. Read by `h2a logs`, rotated at spawn. */
10
38
  export declare function resolveCentralMcpLogPath(): string;
11
39
  /**
@@ -15,6 +43,7 @@ export declare function resolveCentralMcpLogPath(): string;
15
43
  */
16
44
  export declare function ensureCentralMcp(options?: Readonly<{
17
45
  root?: string;
46
+ paths?: CentralMcpPaths;
18
47
  }>): Promise<Readonly<{
19
48
  endpoint: string;
20
49
  generation: string;
@@ -24,9 +53,12 @@ export declare function prepareCentralMcpForLaunch(options: Readonly<{
24
53
  root?: string;
25
54
  profile: string;
26
55
  cwd: string;
56
+ paths?: CentralMcpPaths;
27
57
  }>): Promise<CentralMcpPreparation | undefined>;
28
58
  /** Restore launches inherit these values; each re-entered run rewrites its host config. */
29
59
  export declare function prepareCentralMcpForRestore(options?: Readonly<{
30
60
  root?: string;
61
+ paths?: CentralMcpPaths;
31
62
  }>): Promise<CentralMcpPreparation | undefined>;
63
+ export {};
32
64
  //# sourceMappingURL=central-mcp.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"central-mcp.d.ts","sourceRoot":"","sources":["../src/central-mcp.ts"],"names":[],"mappings":"AA4DA,MAAM,MAAM,qBAAqB,GAC7B,QAAQ,CAAC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,GACrE,QAAQ,CAAC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAuDrD,0FAA0F;AAC1F,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAuFD;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,GAAE,QAAQ,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAM,GACxC,OAAO,CAAC,QAAQ,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,SAAS,CAAC,CA6DzE;AA+ED,mFAAmF;AACnF,wBAAsB,0BAA0B,CAAC,OAAO,EAAE,QAAQ,CAAC;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC,GAAG,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAiC9C;AAED,2FAA2F;AAC3F,wBAAsB,2BAA2B,CAC/C,OAAO,GAAE,QAAQ,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAM,GACxC,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAU5C"}
1
+ {"version":3,"file":"central-mcp.d.ts","sourceRoot":"","sources":["../src/central-mcp.ts"],"names":[],"mappings":"AA0CA,KAAK,aAAa,GAAG,QAAQ,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC,CAAC;AAEH,KAAK,WAAW,GACZ,QAAQ,CAAC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,GACpD,QAAQ,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GAC1B,QAAQ,CAAC;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC,CAAC;AAEpC,MAAM,MAAM,qBAAqB,GAC7B,QAAQ,CAAC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,GACrE,QAAQ,CAAC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAMrD;wDACwD;AACxD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAID,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AACrD,eAAO,MAAM,4BAA4B,6BAA6B,CAAC;AA0DvE,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,SAA8B,GAAG,OAAO,CAAC,WAAW,CAAC,CAyBpH;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,GAAE,eAAoB,GAAG,aAAa,GAAG,SAAS,CAuC3F;AA4BD,0FAA0F;AAC1F,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAuFD;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,GAAE,QAAQ,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,eAAe,CAAA;CAAE,CAAM,GACjE,OAAO,CAAC,QAAQ,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,SAAS,CAAC,CA6DzE;AAoFD,mFAAmF;AACnF,wBAAsB,0BAA0B,CAAC,OAAO,EAAE,QAAQ,CAAC;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB,CAAC,GAAG,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAyB9C;AAED,2FAA2F;AAC3F,wBAAsB,2BAA2B,CAC/C,OAAO,GAAE,QAAQ,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,eAAe,CAAA;CAAE,CAAM,GACjE,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAU5C"}
@@ -1,11 +1,20 @@
1
1
  /**
2
- * Runtime-side central MCP lifecycle. The core package remains the sole owner
3
- * of marker validation, reclaim, and server startup; this module only selects
4
- * a persisted endpoint, asks core for protected liveness facts, and starts the
5
- * existing foreground command as a detached child when necessary.
2
+ * Runtime-side central MCP lifecycle. This module owns its end of the
3
+ * central contract: endpoint selection, marker read/validation, HTTP
4
+ * liveness ping, and starting the existing foreground serve command as a
5
+ * detached child when necessary. It deliberately never names the core
6
+ * package: host-config rendering stays a core command invoked over the
7
+ * `h2a` binary (the same spawn pattern used for `mcp-central-serve`
8
+ * below), so isolated installs of this package keep working and degrade
9
+ * cleanly where core is absent.
10
+ *
11
+ * Cross-package constants below (env names, marker directory/file, ping
12
+ * path, file mode) mirror the core contract values. They are pinned by the
13
+ * vitest suite in this directory, which exercises them against real
14
+ * markers and a real HTTP peer.
6
15
  */
7
- import { spawn } from "node:child_process";
8
- import { chmodSync, closeSync, constants, existsSync, mkdirSync, openSync, readFileSync, readSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
16
+ import { spawn, spawnSync } from "node:child_process";
17
+ import { chmodSync, closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
9
18
  import { homedir } from "node:os";
10
19
  import { dirname, join } from "node:path";
11
20
  import { getH2aConfig, resolveConfigPath, setH2aConfig } from "./config.js";
@@ -14,24 +23,135 @@ const CENTRAL_PORT_BASE = 47_000;
14
23
  const CENTRAL_PORT_SPAN = 10_000;
15
24
  const CENTRAL_START_TIMEOUT_MS = 10_000;
16
25
  const CENTRAL_START_POLL_MS = 100;
17
- let coreCentralMcp;
18
- function loadCoreCentralMcp() {
19
- // Keep the core/runtime boundary dynamic. Core already reaches runtime via a
20
- // string-specifier seam for central marker ownership checks, so a static
21
- // runtime dependency here would turn that boundary into a hard cycle.
22
- const corePackage = "@sentropic/h2a";
23
- coreCentralMcp ??= import(corePackage).then((core) => {
24
- const candidate = core;
25
- if (typeof candidate.H2A_MCP_CENTRAL_ENV !== "string" ||
26
- typeof candidate.H2A_MCP_CENTRAL_ENDPOINT_ENV !== "string" ||
27
- typeof candidate.centralMcpPing !== "function" ||
28
- typeof candidate.readCentralMcpMarker !== "function" ||
29
- typeof candidate.runCli !== "function") {
30
- throw new Error("@sentropic/h2a does not expose the central MCP runtime surface");
26
+ // Stable cross-package contract values (mirrored from core; pinned by the
27
+ // vitest suite in this directory against real markers and a real HTTP peer).
28
+ export const H2A_MCP_CENTRAL_ENV = "H2A_MCP_CENTRAL";
29
+ export const H2A_MCP_CENTRAL_ENDPOINT_ENV = "H2A_MCP_CENTRAL_ENDPOINT";
30
+ const CENTRAL_RUNTIME_DIRECTORY = "h2a-mcp-central";
31
+ const CENTRAL_MARKER_FILE = "marker.json";
32
+ const CENTRAL_PING_PATH = "/_h2a-central/ping";
33
+ const CENTRAL_LIVENESS_TIMEOUT_MS = 1_500;
34
+ const CENTRAL_HOST_SETUP_TIMEOUT_MS = 60_000;
35
+ function currentUid() {
36
+ if (typeof process.getuid !== "function") {
37
+ throw new Error(`${H2A_MCP_CENTRAL_ENDPOINT_ENV} requires a current uid`);
38
+ }
39
+ return process.getuid();
40
+ }
41
+ function centralMcpMarkerPath(paths = {}) {
42
+ const base = paths.runtimeBase ?? join("/run/user", String(currentUid()));
43
+ return join(base, CENTRAL_RUNTIME_DIRECTORY, CENTRAL_MARKER_FILE);
44
+ }
45
+ function isLoopbackHostname(hostname) {
46
+ return hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]" || hostname === "localhost";
47
+ }
48
+ function parseCentralMcpEndpoint(value) {
49
+ if (!value || value.trim().length === 0) {
50
+ throw new Error(`${H2A_MCP_CENTRAL_ENDPOINT_ENV} must be a non-empty absolute http URL`);
51
+ }
52
+ let endpoint;
53
+ try {
54
+ endpoint = new URL(value);
55
+ }
56
+ catch {
57
+ throw new Error(`${H2A_MCP_CENTRAL_ENDPOINT_ENV} must be a non-empty absolute http URL`);
58
+ }
59
+ if (endpoint.protocol !== "http:" ||
60
+ !isLoopbackHostname(endpoint.hostname) ||
61
+ !endpoint.port ||
62
+ Number(endpoint.port) < 1 ||
63
+ Number(endpoint.port) > 65_535 ||
64
+ endpoint.username ||
65
+ endpoint.password ||
66
+ endpoint.search ||
67
+ endpoint.hash) {
68
+ throw new Error(`${H2A_MCP_CENTRAL_ENDPOINT_ENV} must be an absolute http URL with an explicit port`);
69
+ }
70
+ return endpoint.href;
71
+ }
72
+ function errorHasCode(error, expectedCode) {
73
+ let current = error;
74
+ for (let depth = 0; depth < 4 && current && typeof current === "object"; depth += 1) {
75
+ if (current.code === expectedCode)
76
+ return true;
77
+ current = current.cause;
78
+ }
79
+ return false;
80
+ }
81
+ export async function centralMcpPing(endpoint, timeoutMs = CENTRAL_LIVENESS_TIMEOUT_MS) {
82
+ const canonicalEndpoint = parseCentralMcpEndpoint(endpoint);
83
+ const controller = new AbortController();
84
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
85
+ const pingUrl = new URL(CENTRAL_PING_PATH, canonicalEndpoint).href;
86
+ return fetch(pingUrl, { signal: controller.signal }).then(async (response) => {
87
+ clearTimeout(timeout);
88
+ if (!response.ok)
89
+ return { kind: "dead" };
90
+ try {
91
+ const body = (await response.json());
92
+ return typeof body.generation === "string" && body.generation.length > 0
93
+ ? { kind: "generation", generation: body.generation }
94
+ : { kind: "dead" };
31
95
  }
32
- return candidate;
96
+ catch {
97
+ return { kind: "dead" };
98
+ }
99
+ }, (error) => {
100
+ clearTimeout(timeout);
101
+ // ECONNREFUSED is the only transport failure that positively proves no
102
+ // listener owns this registration. Timeouts and all other errors are not.
103
+ return (errorHasCode(error, "ECONNREFUSED") ? { kind: "dead" } : { kind: "ambiguous" });
33
104
  });
34
- return coreCentralMcp;
105
+ }
106
+ /**
107
+ * Read the protected central marker without exposing its token to callers
108
+ * that only need liveness/generation metadata. A missing marker is the sole
109
+ * non-error absence case; malformed or insecure state remains a hard failure.
110
+ */
111
+ export function readCentralMcpMarker(paths = {}) {
112
+ const path = centralMcpMarkerPath(paths);
113
+ let info;
114
+ try {
115
+ info = lstatSync(path);
116
+ }
117
+ catch (error) {
118
+ if (error.code === "ENOENT")
119
+ return undefined;
120
+ throw error;
121
+ }
122
+ if (!info.isFile())
123
+ throw new Error(`central MCP marker is not a regular file: ${path}`);
124
+ if (info.uid !== currentUid())
125
+ throw new Error(`central MCP marker is not owned by the current user: ${path}`);
126
+ if ((info.mode & 0o777) !== 0o600)
127
+ throw new Error(`central MCP marker must have mode 0600: ${path}`);
128
+ let value;
129
+ try {
130
+ value = JSON.parse(readFileSync(path, "utf8"));
131
+ }
132
+ catch {
133
+ throw new Error(`central MCP marker is malformed: ${path}`);
134
+ }
135
+ if (typeof value.endpoint !== "string" ||
136
+ typeof value.generation !== "string" ||
137
+ value.generation.length === 0 ||
138
+ typeof value.pid !== "number" ||
139
+ typeof value.startedAt !== "string" ||
140
+ typeof value.token !== "string" ||
141
+ value.token.length === 0) {
142
+ throw new Error(`central MCP marker is malformed: ${path}`);
143
+ }
144
+ try {
145
+ return {
146
+ endpoint: parseCentralMcpEndpoint(value.endpoint),
147
+ generation: value.generation,
148
+ pid: value.pid,
149
+ startedAt: value.startedAt,
150
+ };
151
+ }
152
+ catch {
153
+ throw new Error(`central MCP marker is malformed: ${path}`);
154
+ }
35
155
  }
36
156
  function delay(milliseconds) {
37
157
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
@@ -127,7 +247,7 @@ function centralLogFile() {
127
247
  catch { /* rotation is best-effort; the spawn below still proceeds */ }
128
248
  return path;
129
249
  }
130
- async function waitForCentralStart(core, endpoint, spawnFailure) {
250
+ async function waitForCentralStart(paths, endpoint, spawnFailure) {
131
251
  const deadline = Date.now() + CENTRAL_START_TIMEOUT_MS;
132
252
  while (Date.now() < deadline) {
133
253
  const failure = spawnFailure();
@@ -136,9 +256,9 @@ async function waitForCentralStart(core, endpoint, spawnFailure) {
136
256
  appendCrashLog("mcp-server", { event: "startupError", reason: "spawn-failed", detail: failure.message });
137
257
  throw failure;
138
258
  }
139
- const ping = await core.centralMcpPing(endpoint, CENTRAL_START_POLL_MS);
259
+ const ping = await centralMcpPing(endpoint, CENTRAL_START_POLL_MS);
140
260
  if (ping.kind === "generation") {
141
- const marker = core.readCentralMcpMarker();
261
+ const marker = readCentralMcpMarker(paths);
142
262
  if (marker?.endpoint === endpoint && marker.generation === ping.generation) {
143
263
  return { endpoint, generation: ping.generation };
144
264
  }
@@ -159,10 +279,10 @@ export async function ensureCentralMcp(options = {}) {
159
279
  const endpoint = configuredCentralEndpoint();
160
280
  if (!endpoint)
161
281
  return undefined;
162
- const core = await loadCoreCentralMcp();
163
- const marker = core.readCentralMcpMarker();
282
+ const paths = options.paths ?? {};
283
+ const marker = readCentralMcpMarker(paths);
164
284
  if (marker) {
165
- const ping = await core.centralMcpPing(marker.endpoint);
285
+ const ping = await centralMcpPing(marker.endpoint);
166
286
  if (ping.kind === "ambiguous") {
167
287
  appendCrashLog("mcp-server", { event: "refusal", instanceId: marker.generation, reason: "ambiguous-liveness", detail: marker.endpoint });
168
288
  throw new Error(`central MCP liveness at ${marker.endpoint} is ambiguous; refusing to start another server`);
@@ -185,8 +305,8 @@ export async function ensureCentralMcp(options = {}) {
185
305
  stdio: ["ignore", logFd, logFd],
186
306
  env: {
187
307
  ...process.env,
188
- [core.H2A_MCP_CENTRAL_ENV]: "1",
189
- [core.H2A_MCP_CENTRAL_ENDPOINT_ENV]: endpoint,
308
+ [H2A_MCP_CENTRAL_ENV]: "1",
309
+ [H2A_MCP_CENTRAL_ENDPOINT_ENV]: endpoint,
190
310
  },
191
311
  });
192
312
  child.once("error", (error) => {
@@ -211,7 +331,7 @@ export async function ensureCentralMcp(options = {}) {
211
331
  finally {
212
332
  closeSync(logFd);
213
333
  }
214
- return waitForCentralStart(core, endpoint, () => spawnError);
334
+ return waitForCentralStart(paths, endpoint, () => spawnError);
215
335
  }
216
336
  function profileMcpConfig(profile, cwd) {
217
337
  switch (profile) {
@@ -235,33 +355,9 @@ function profileMcpConfig(profile, cwd) {
235
355
  return undefined;
236
356
  }
237
357
  }
238
- function coherentHostSetupReport(host) {
239
- return {
240
- ok: true,
241
- repair: false,
242
- dryRun: false,
243
- sessionFreshnessGuarantee: "runtime central MCP writer",
244
- nativeCommandFailureLimit: "runtime central MCP writer",
245
- version: "runtime-central-mcp",
246
- hosts: [{
247
- host,
248
- ok: true,
249
- findings: [],
250
- diagnostics: [],
251
- changed: [],
252
- preserved: [],
253
- failures: [],
254
- unverifiable: [],
255
- unrepaired: [],
256
- coherencePaths: [],
257
- plannedActions: [],
258
- repairMarkerPath: "",
259
- }],
260
- };
261
- }
262
- function activateCentralMcpEnvironment(core, endpoint) {
263
- process.env[core.H2A_MCP_CENTRAL_ENV] = "1";
264
- process.env[core.H2A_MCP_CENTRAL_ENDPOINT_ENV] = endpoint;
358
+ function activateCentralMcpEnvironment(endpoint) {
359
+ process.env[H2A_MCP_CENTRAL_ENV] = "1";
360
+ process.env[H2A_MCP_CENTRAL_ENDPOINT_ENV] = endpoint;
265
361
  }
266
362
  function deactivateCentralMcpEnvironment() {
267
363
  delete process.env.H2A_MCP_CENTRAL;
@@ -284,25 +380,46 @@ function restoreHostConfig(path, snapshot) {
284
380
  function errorMessage(error) {
285
381
  return error instanceof Error ? error.message : String(error);
286
382
  }
383
+ /**
384
+ * Render the host config through the core CLI over the `h2a` binary (never an
385
+ * in-process core import). Output stays captured (quiet launch path); a
386
+ * nonzero exit degrades to the per-session sidecar exactly like before. The
387
+ * real installation doctor runs here instead of the former always-ok stub, so
388
+ * a genuinely incoherent host setup now surfaces as degraded-with-reason
389
+ * rather than a masked central — still fail-closed, never a crash.
390
+ */
391
+ function writeHostConfig(target, cwd) {
392
+ let status = null;
393
+ let spawnError;
394
+ try {
395
+ const result = spawnSync("h2a", ["host", "setup", "--host", target.host, "--write", target.path], { cwd, stdio: "pipe", encoding: "utf8", timeout: CENTRAL_HOST_SETUP_TIMEOUT_MS });
396
+ status = result.status;
397
+ spawnError = result.error;
398
+ }
399
+ catch (error) {
400
+ spawnError = error instanceof Error ? error : new Error(String(error));
401
+ }
402
+ if (status !== 0) {
403
+ throw new Error(`central MCP could not write the ${target.host} host config at ${target.path}` +
404
+ (spawnError ? ` (${spawnError.message})` : ""));
405
+ }
406
+ }
287
407
  /** Ensure central MCP and rewrite the launched host's existing MCP config path. */
288
408
  export async function prepareCentralMcpForLaunch(options) {
289
409
  let hostConfig;
290
410
  try {
291
- const central = await ensureCentralMcp({ ...(options.root ? { root: options.root } : {}) });
411
+ const central = await ensureCentralMcp({
412
+ ...(options.root ? { root: options.root } : {}),
413
+ ...(options.paths ? { paths: options.paths } : {}),
414
+ });
292
415
  if (!central)
293
416
  return undefined;
294
- const core = await loadCoreCentralMcp();
295
417
  const target = profileMcpConfig(options.profile, options.cwd);
296
418
  if (target)
297
419
  hostConfig = { path: target.path, snapshot: captureHostConfig(target.path) };
298
- activateCentralMcpEnvironment(core, central.endpoint);
299
- if (target) {
300
- const quiet = { stdout: { write: () => true }, stderr: { write: () => true }, cwd: () => options.cwd };
301
- const code = core.runCli(["host", "setup", "--host", target.host, "--write", target.path], quiet, { doctorHostInstallations: () => coherentHostSetupReport(target.host) });
302
- if (code !== 0) {
303
- throw new Error(`central MCP could not write the ${target.host} host config at ${target.path}`);
304
- }
305
- }
420
+ activateCentralMcpEnvironment(central.endpoint);
421
+ if (target)
422
+ writeHostConfig(target, options.cwd);
306
423
  return { status: "central", ...central };
307
424
  }
308
425
  catch (error) {
@@ -325,7 +442,7 @@ export async function prepareCentralMcpForRestore(options = {}) {
325
442
  const central = await ensureCentralMcp(options);
326
443
  if (!central)
327
444
  return undefined;
328
- activateCentralMcpEnvironment(await loadCoreCentralMcp(), central.endpoint);
445
+ activateCentralMcpEnvironment(central.endpoint);
329
446
  return { status: "central", ...central };
330
447
  }
331
448
  catch (error) {