@apnex/network-adapter 0.1.0
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/hub-error.d.ts +42 -0
- package/dist/hub-error.js +74 -0
- package/dist/hub-error.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/agent-client.d.ts +192 -0
- package/dist/kernel/agent-client.js +44 -0
- package/dist/kernel/agent-client.js.map +1 -0
- package/dist/kernel/event-router.d.ts +49 -0
- package/dist/kernel/event-router.js +150 -0
- package/dist/kernel/event-router.js.map +1 -0
- package/dist/kernel/handshake.d.ts +161 -0
- package/dist/kernel/handshake.js +257 -0
- package/dist/kernel/handshake.js.map +1 -0
- package/dist/kernel/instance.d.ts +40 -0
- package/dist/kernel/instance.js +79 -0
- package/dist/kernel/instance.js.map +1 -0
- package/dist/kernel/mcp-agent-client.d.ts +139 -0
- package/dist/kernel/mcp-agent-client.js +505 -0
- package/dist/kernel/mcp-agent-client.js.map +1 -0
- package/dist/kernel/poll-backstop.d.ts +108 -0
- package/dist/kernel/poll-backstop.js +243 -0
- package/dist/kernel/poll-backstop.js.map +1 -0
- package/dist/kernel/session-claim.d.ts +67 -0
- package/dist/kernel/session-claim.js +106 -0
- package/dist/kernel/session-claim.js.map +1 -0
- package/dist/kernel/state-sync.d.ts +43 -0
- package/dist/kernel/state-sync.js +85 -0
- package/dist/kernel/state-sync.js.map +1 -0
- package/dist/logger.d.ts +82 -0
- package/dist/logger.js +114 -0
- package/dist/logger.js.map +1 -0
- package/dist/notification-log.d.ts +29 -0
- package/dist/notification-log.js +66 -0
- package/dist/notification-log.js.map +1 -0
- package/dist/prompt-format.d.ts +38 -0
- package/dist/prompt-format.js +220 -0
- package/dist/prompt-format.js.map +1 -0
- package/dist/tool-manager/dispatcher.d.ts +180 -0
- package/dist/tool-manager/dispatcher.js +379 -0
- package/dist/tool-manager/dispatcher.js.map +1 -0
- package/dist/tool-manager/tool-catalog-cache.d.ts +85 -0
- package/dist/tool-manager/tool-catalog-cache.js +137 -0
- package/dist/tool-manager/tool-catalog-cache.js.map +1 -0
- package/dist/wire/mcp-transport.d.ts +120 -0
- package/dist/wire/mcp-transport.js +447 -0
- package/dist/wire/mcp-transport.js.map +1 -0
- package/dist/wire/transport.d.ts +174 -0
- package/dist/wire/transport.js +43 -0
- package/dist/wire/transport.js.map +1 -0
- package/package.json +32 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enriched register_role handshake.
|
|
3
|
+
*
|
|
4
|
+
* After the bare `register_role({role})` call in `McpAgentClient.runHandshake`
|
|
5
|
+
* (which proves the wire is alive and the session is bound), the agent
|
|
6
|
+
* performs this enriched handshake on entry to the `synchronizing` state. It:
|
|
7
|
+
*
|
|
8
|
+
* 1. Re-calls `register_role` with full payload (globalInstanceId,
|
|
9
|
+
* clientMetadata, advisoryTags).
|
|
10
|
+
* 2. Parses the response for `agentId`/`sessionEpoch`/`wasCreated`.
|
|
11
|
+
* 3. Tracks epoch displacement across reconnects.
|
|
12
|
+
* 4. Halts on fatal codes (`agent_thrashing_detected`, `role_mismatch`).
|
|
13
|
+
*
|
|
14
|
+
* Fatal halt is delegated to a caller-provided `onFatalHalt` callback so
|
|
15
|
+
* each engineer can implement its own shutdown path (Claude: stdio drain +
|
|
16
|
+
* `process.exit(2)`; OpenCode: OpenCode lifecycle shutdown).
|
|
17
|
+
*/
|
|
18
|
+
import type { ILogger, LegacyStringLogger } from "../logger.js";
|
|
19
|
+
export declare const FATAL_CODES: ReadonlySet<string>;
|
|
20
|
+
export interface HandshakeClientMetadata {
|
|
21
|
+
clientName: string;
|
|
22
|
+
clientVersion: string;
|
|
23
|
+
proxyName: string;
|
|
24
|
+
proxyVersion: string;
|
|
25
|
+
transport: string;
|
|
26
|
+
sdkVersion: string;
|
|
27
|
+
hostname?: string;
|
|
28
|
+
platform?: string;
|
|
29
|
+
pid?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface HandshakeAdvisoryTags {
|
|
32
|
+
/** Best-effort, drift-prone. Hub MUST NOT route on this. */
|
|
33
|
+
llmModel?: string;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
}
|
|
36
|
+
export interface HandshakePayload {
|
|
37
|
+
role: string;
|
|
38
|
+
globalInstanceId: string;
|
|
39
|
+
clientMetadata: HandshakeClientMetadata;
|
|
40
|
+
advisoryTags: HandshakeAdvisoryTags;
|
|
41
|
+
labels?: Record<string, string>;
|
|
42
|
+
}
|
|
43
|
+
export interface HandshakeResponse {
|
|
44
|
+
agentId: string;
|
|
45
|
+
sessionEpoch: number;
|
|
46
|
+
wasCreated: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface HandshakeFatalError {
|
|
49
|
+
code: string;
|
|
50
|
+
message: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Parse an MCP CallTool result for a structured handshake error payload.
|
|
54
|
+
* Returns the fatal error if the result matches `{isError:true, content:[{text: <json>}]}`
|
|
55
|
+
* and the JSON body has a `code` in `FATAL_CODES`. Returns null otherwise.
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseHandshakeError(result: unknown): HandshakeFatalError | null;
|
|
58
|
+
/**
|
|
59
|
+
* Parse a successful handshake response. The adapter's executeTool returns
|
|
60
|
+
* parsed JSON directly, but some code paths deliver the raw `{content:[{text}]}`
|
|
61
|
+
* envelope — handle both.
|
|
62
|
+
*
|
|
63
|
+
* mission-63 W3: reads canonical envelope per Design v1.0 §3.1 + ADR-028 —
|
|
64
|
+
* `body.agent.id` + `body.session.epoch` (not `body.agentId` + `body.sessionEpoch`).
|
|
65
|
+
* The Hub-side flat-field shape is gone post-mission-63 W1+W2 (anti-goal §8.1
|
|
66
|
+
* clean cutover). Adapter parses the canonical shape only.
|
|
67
|
+
*/
|
|
68
|
+
export declare function parseHandshakeResponse(result: unknown): HandshakeResponse | null;
|
|
69
|
+
export interface HandshakeConfig {
|
|
70
|
+
role: string;
|
|
71
|
+
globalInstanceId: string;
|
|
72
|
+
clientInfo: {
|
|
73
|
+
name: string;
|
|
74
|
+
version: string;
|
|
75
|
+
};
|
|
76
|
+
proxyName: string;
|
|
77
|
+
proxyVersion: string;
|
|
78
|
+
transport: string;
|
|
79
|
+
sdkVersion: string;
|
|
80
|
+
llmModel?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Mission-19 routing labels. Forwarded as the `labels` arg on the
|
|
83
|
+
* enriched register_role call. Hub persists them on the Agent entity
|
|
84
|
+
* (immutable after first create — INV-AG1), and subsequent
|
|
85
|
+
* `task.labels` / dispatch selectors inherit from the Agent. Omit to
|
|
86
|
+
* keep legacy broadcast semantics (labels = {}).
|
|
87
|
+
*/
|
|
88
|
+
labels?: Record<string, string>;
|
|
89
|
+
/**
|
|
90
|
+
* ADR-017: optional durable-wake HTTP endpoint. When set, the Hub
|
|
91
|
+
* POSTs here on queue-deadline miss to cold-start scaled-to-zero
|
|
92
|
+
* agents. For Cloud Run architects, this is the service URL. Absent
|
|
93
|
+
* for interactive CLI agents — watchdog skips Stage 1 re-dispatch and
|
|
94
|
+
* escalates directly to Director notification.
|
|
95
|
+
*/
|
|
96
|
+
wakeEndpoint?: string;
|
|
97
|
+
/**
|
|
98
|
+
* ADR-017: optional per-agent receipt-SLA override in milliseconds.
|
|
99
|
+
* When omitted, Hub uses DEFAULT_AGENT_RECEIPT_SLA_MS (30000).
|
|
100
|
+
*/
|
|
101
|
+
receiptSla?: number;
|
|
102
|
+
}
|
|
103
|
+
export interface HandshakeContext {
|
|
104
|
+
/** Tool executor — typically `transport.request.bind(transport)`. */
|
|
105
|
+
executeTool: (name: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
106
|
+
config: HandshakeConfig;
|
|
107
|
+
/** Previous session epoch for displacement detection. 0 if unknown. */
|
|
108
|
+
previousEpoch: number;
|
|
109
|
+
/**
|
|
110
|
+
* Structured logger for diagnostics + the canonical `[Handshake] Registered as …`
|
|
111
|
+
* line. A legacy `(msg: string) => void` is auto-bridged for tests and
|
|
112
|
+
* shims that haven't migrated to `ILogger` yet.
|
|
113
|
+
*/
|
|
114
|
+
log: ILogger | LegacyStringLogger;
|
|
115
|
+
/** Invoked on fatal codes. Implementations should terminate the process. */
|
|
116
|
+
onFatalHalt?: (err: HandshakeFatalError) => void;
|
|
117
|
+
}
|
|
118
|
+
export interface HandshakeResult {
|
|
119
|
+
/** Parsed response, or null if the handshake failed (non-fatally). */
|
|
120
|
+
response: HandshakeResponse | null;
|
|
121
|
+
/** New epoch to persist for the next reconnect's displacement check. */
|
|
122
|
+
epoch: number;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* bug-17 fix: resolve clientName / clientVersion with proxy-fallback.
|
|
126
|
+
*
|
|
127
|
+
* The MCP `initialize` handshake is supposed to carry the host's `clientInfo`
|
|
128
|
+
* (name + version), but some hosts — notably the claude-plugin dev-channel
|
|
129
|
+
* load path (`claude --dangerously-load-development-channels`) — don't
|
|
130
|
+
* forward clientInfo through the stdio transport. The Hub then persists
|
|
131
|
+
* `clientName: "unknown"` + `clientVersion: "0.0.0"` into the Agent record,
|
|
132
|
+
* losing the identity signal entirely.
|
|
133
|
+
*
|
|
134
|
+
* Fallback policy: when either field is missing/empty/sentinel, substitute
|
|
135
|
+
* the proxy identity (`@apnex/claude-plugin` / `@apnex/vertex-cloudrun` / etc.)
|
|
136
|
+
* which is authoritative at the adapter layer. The Agent record then
|
|
137
|
+
* surfaces a meaningful identity even when MCP clientInfo is absent.
|
|
138
|
+
*
|
|
139
|
+
* Exported for unit-test access.
|
|
140
|
+
*/
|
|
141
|
+
export declare function resolveClientName(raw: string | undefined, proxyName: string): string;
|
|
142
|
+
export declare function resolveClientVersion(raw: string | undefined, proxyVersion: string): string;
|
|
143
|
+
/**
|
|
144
|
+
* Build the enriched register_role payload.
|
|
145
|
+
*/
|
|
146
|
+
export declare function buildHandshakePayload(config: HandshakeConfig): HandshakePayload;
|
|
147
|
+
/**
|
|
148
|
+
* Perform the enriched handshake. Never throws on tool-call failure — a
|
|
149
|
+
* transport or session error is logged and returned as
|
|
150
|
+
* `{response:null, epoch:previousEpoch}` so the caller can continue state
|
|
151
|
+
* sync. A fatal code (`agent_thrashing_detected`, `role_mismatch`) triggers
|
|
152
|
+
* `onFatalHalt` and then also returns null.
|
|
153
|
+
*/
|
|
154
|
+
export declare function performHandshake(ctx: HandshakeContext): Promise<HandshakeResult>;
|
|
155
|
+
/**
|
|
156
|
+
* Build a fatal-halt function with a stdio drain delay. Engineers that use
|
|
157
|
+
* stdio transports (Claude) MUST use this to avoid losing the halt message
|
|
158
|
+
* to an unflushed buffer. Engineers that do not (OpenCode) can implement
|
|
159
|
+
* their own `onFatalHalt` directly.
|
|
160
|
+
*/
|
|
161
|
+
export declare function makeStdioFatalHalt(log: (msg: string) => void, exit?: (code: number) => void, drainMs?: number): (err: HandshakeFatalError) => void;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enriched register_role handshake.
|
|
3
|
+
*
|
|
4
|
+
* After the bare `register_role({role})` call in `McpAgentClient.runHandshake`
|
|
5
|
+
* (which proves the wire is alive and the session is bound), the agent
|
|
6
|
+
* performs this enriched handshake on entry to the `synchronizing` state. It:
|
|
7
|
+
*
|
|
8
|
+
* 1. Re-calls `register_role` with full payload (globalInstanceId,
|
|
9
|
+
* clientMetadata, advisoryTags).
|
|
10
|
+
* 2. Parses the response for `agentId`/`sessionEpoch`/`wasCreated`.
|
|
11
|
+
* 3. Tracks epoch displacement across reconnects.
|
|
12
|
+
* 4. Halts on fatal codes (`agent_thrashing_detected`, `role_mismatch`).
|
|
13
|
+
*
|
|
14
|
+
* Fatal halt is delegated to a caller-provided `onFatalHalt` callback so
|
|
15
|
+
* each engineer can implement its own shutdown path (Claude: stdio drain +
|
|
16
|
+
* `process.exit(2)`; OpenCode: OpenCode lifecycle shutdown).
|
|
17
|
+
*/
|
|
18
|
+
import { hostname, platform as osPlatform } from "node:os";
|
|
19
|
+
import { normalizeToILogger } from "../logger.js";
|
|
20
|
+
export const FATAL_CODES = new Set([
|
|
21
|
+
"agent_thrashing_detected",
|
|
22
|
+
"role_mismatch",
|
|
23
|
+
]);
|
|
24
|
+
/**
|
|
25
|
+
* Parse an MCP CallTool result for a structured handshake error payload.
|
|
26
|
+
* Returns the fatal error if the result matches `{isError:true, content:[{text: <json>}]}`
|
|
27
|
+
* and the JSON body has a `code` in `FATAL_CODES`. Returns null otherwise.
|
|
28
|
+
*/
|
|
29
|
+
export function parseHandshakeError(result) {
|
|
30
|
+
try {
|
|
31
|
+
const r = result;
|
|
32
|
+
if (!r || r.isError !== true || !Array.isArray(r.content) || !r.content[0]?.text) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const body = JSON.parse(r.content[0].text);
|
|
36
|
+
if (body && typeof body.code === "string" && FATAL_CODES.has(body.code)) {
|
|
37
|
+
return { code: body.code, message: String(body.message ?? "") };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
/* not a structured error — fall through */
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse a successful handshake response. The adapter's executeTool returns
|
|
47
|
+
* parsed JSON directly, but some code paths deliver the raw `{content:[{text}]}`
|
|
48
|
+
* envelope — handle both.
|
|
49
|
+
*
|
|
50
|
+
* mission-63 W3: reads canonical envelope per Design v1.0 §3.1 + ADR-028 —
|
|
51
|
+
* `body.agent.id` + `body.session.epoch` (not `body.agentId` + `body.sessionEpoch`).
|
|
52
|
+
* The Hub-side flat-field shape is gone post-mission-63 W1+W2 (anti-goal §8.1
|
|
53
|
+
* clean cutover). Adapter parses the canonical shape only.
|
|
54
|
+
*/
|
|
55
|
+
export function parseHandshakeResponse(result) {
|
|
56
|
+
try {
|
|
57
|
+
const r = result;
|
|
58
|
+
let body;
|
|
59
|
+
if (Array.isArray(r.content)) {
|
|
60
|
+
const text = r.content[0]?.text;
|
|
61
|
+
body = text ? JSON.parse(text) : {};
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
body = r;
|
|
65
|
+
}
|
|
66
|
+
const agent = body.agent;
|
|
67
|
+
const session = body.session;
|
|
68
|
+
if (agent &&
|
|
69
|
+
typeof agent.id === "string" &&
|
|
70
|
+
session &&
|
|
71
|
+
typeof session.epoch === "number") {
|
|
72
|
+
return {
|
|
73
|
+
agentId: agent.id,
|
|
74
|
+
sessionEpoch: session.epoch,
|
|
75
|
+
wasCreated: Boolean(body.wasCreated),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* fall through */
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* bug-17 fix: resolve clientName / clientVersion with proxy-fallback.
|
|
86
|
+
*
|
|
87
|
+
* The MCP `initialize` handshake is supposed to carry the host's `clientInfo`
|
|
88
|
+
* (name + version), but some hosts — notably the claude-plugin dev-channel
|
|
89
|
+
* load path (`claude --dangerously-load-development-channels`) — don't
|
|
90
|
+
* forward clientInfo through the stdio transport. The Hub then persists
|
|
91
|
+
* `clientName: "unknown"` + `clientVersion: "0.0.0"` into the Agent record,
|
|
92
|
+
* losing the identity signal entirely.
|
|
93
|
+
*
|
|
94
|
+
* Fallback policy: when either field is missing/empty/sentinel, substitute
|
|
95
|
+
* the proxy identity (`@apnex/claude-plugin` / `@apnex/vertex-cloudrun` / etc.)
|
|
96
|
+
* which is authoritative at the adapter layer. The Agent record then
|
|
97
|
+
* surfaces a meaningful identity even when MCP clientInfo is absent.
|
|
98
|
+
*
|
|
99
|
+
* Exported for unit-test access.
|
|
100
|
+
*/
|
|
101
|
+
export function resolveClientName(raw, proxyName) {
|
|
102
|
+
if (!raw || raw === "unknown")
|
|
103
|
+
return proxyName;
|
|
104
|
+
return raw;
|
|
105
|
+
}
|
|
106
|
+
export function resolveClientVersion(raw, proxyVersion) {
|
|
107
|
+
if (!raw || raw === "0.0.0")
|
|
108
|
+
return proxyVersion;
|
|
109
|
+
return raw;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Build the enriched register_role payload.
|
|
113
|
+
*/
|
|
114
|
+
export function buildHandshakePayload(config) {
|
|
115
|
+
const payload = {
|
|
116
|
+
role: config.role,
|
|
117
|
+
globalInstanceId: config.globalInstanceId,
|
|
118
|
+
clientMetadata: {
|
|
119
|
+
clientName: resolveClientName(config.clientInfo.name, config.proxyName),
|
|
120
|
+
clientVersion: resolveClientVersion(config.clientInfo.version, config.proxyVersion),
|
|
121
|
+
proxyName: config.proxyName,
|
|
122
|
+
proxyVersion: config.proxyVersion,
|
|
123
|
+
transport: config.transport,
|
|
124
|
+
sdkVersion: config.sdkVersion,
|
|
125
|
+
hostname: hostname(),
|
|
126
|
+
platform: osPlatform(),
|
|
127
|
+
pid: process.pid,
|
|
128
|
+
},
|
|
129
|
+
advisoryTags: {
|
|
130
|
+
llmModel: config.llmModel ?? "unknown",
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
if (config.labels)
|
|
134
|
+
payload.labels = config.labels;
|
|
135
|
+
if (config.wakeEndpoint)
|
|
136
|
+
payload.wakeEndpoint = config.wakeEndpoint;
|
|
137
|
+
if (typeof config.receiptSla === "number")
|
|
138
|
+
payload.receiptSla = config.receiptSla;
|
|
139
|
+
return payload;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Perform the enriched handshake. Never throws on tool-call failure — a
|
|
143
|
+
* transport or session error is logged and returned as
|
|
144
|
+
* `{response:null, epoch:previousEpoch}` so the caller can continue state
|
|
145
|
+
* sync. A fatal code (`agent_thrashing_detected`, `role_mismatch`) triggers
|
|
146
|
+
* `onFatalHalt` and then also returns null.
|
|
147
|
+
*/
|
|
148
|
+
export async function performHandshake(ctx) {
|
|
149
|
+
const log = normalizeToILogger(ctx.log, "Handshake");
|
|
150
|
+
const payload = buildHandshakePayload(ctx.config);
|
|
151
|
+
let result;
|
|
152
|
+
try {
|
|
153
|
+
result = await ctx.executeTool("register_role", payload);
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
log.log("agent.handshake.tool_call_failed", { error: String(err) }, `[Handshake] tool-call failed (non-fatal, retry on reconnect): ${err}`);
|
|
157
|
+
return { response: null, epoch: ctx.previousEpoch };
|
|
158
|
+
}
|
|
159
|
+
const fatal = parseHandshakeError(result);
|
|
160
|
+
if (fatal) {
|
|
161
|
+
log.log("agent.handshake.fatal", { code: fatal.code, message: fatal.message }, `[Handshake] FATAL ${fatal.code}: ${fatal.message}`);
|
|
162
|
+
if (ctx.onFatalHalt)
|
|
163
|
+
ctx.onFatalHalt(fatal);
|
|
164
|
+
return { response: null, epoch: ctx.previousEpoch };
|
|
165
|
+
}
|
|
166
|
+
const response = parseHandshakeResponse(result);
|
|
167
|
+
if (!response) {
|
|
168
|
+
// mission-63 W3 diagnostic — capture envelope + body shape for parse
|
|
169
|
+
// failures. Post-mission-63 the canonical shape is `body.agent.id` +
|
|
170
|
+
// `body.session.epoch`; legacy flat fields (body.agentId + body.sessionEpoch)
|
|
171
|
+
// surfacing here would indicate the Hub is on a pre-mission-63 build.
|
|
172
|
+
let envelope = "unknown";
|
|
173
|
+
let bodyKeys = "none";
|
|
174
|
+
let nestedAgentKeys = "none";
|
|
175
|
+
let nestedSessionKeys = "none";
|
|
176
|
+
let legacyAgentIdType = "absent";
|
|
177
|
+
let legacySessionEpochType = "absent";
|
|
178
|
+
try {
|
|
179
|
+
const r = result;
|
|
180
|
+
let body = {};
|
|
181
|
+
if (Array.isArray(r.content)) {
|
|
182
|
+
envelope = "content-array";
|
|
183
|
+
const text = r.content[0]
|
|
184
|
+
?.text;
|
|
185
|
+
body = text ? JSON.parse(text) : {};
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
envelope = "raw-object";
|
|
189
|
+
body = r;
|
|
190
|
+
}
|
|
191
|
+
bodyKeys = Object.keys(body).sort().join(",");
|
|
192
|
+
if (body.agent && typeof body.agent === "object" && body.agent !== null) {
|
|
193
|
+
nestedAgentKeys = Object.keys(body.agent)
|
|
194
|
+
.sort()
|
|
195
|
+
.join(",");
|
|
196
|
+
}
|
|
197
|
+
if (body.session && typeof body.session === "object" && body.session !== null) {
|
|
198
|
+
nestedSessionKeys = Object.keys(body.session)
|
|
199
|
+
.sort()
|
|
200
|
+
.join(",");
|
|
201
|
+
}
|
|
202
|
+
legacyAgentIdType =
|
|
203
|
+
body.agentId === undefined
|
|
204
|
+
? "undefined"
|
|
205
|
+
: body.agentId === null
|
|
206
|
+
? "null"
|
|
207
|
+
: typeof body.agentId;
|
|
208
|
+
legacySessionEpochType =
|
|
209
|
+
body.sessionEpoch === undefined
|
|
210
|
+
? "undefined"
|
|
211
|
+
: body.sessionEpoch === null
|
|
212
|
+
? "null"
|
|
213
|
+
: typeof body.sessionEpoch;
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
envelope = `parse-error:${err?.message ?? String(err)}`;
|
|
217
|
+
}
|
|
218
|
+
log.log("agent.handshake.parse_failed", { envelope, bodyKeys, nestedAgentKeys, nestedSessionKeys, legacyAgentIdType, legacySessionEpochType }, "[Handshake] response parse failed (non-fatal)");
|
|
219
|
+
return { response: null, epoch: ctx.previousEpoch };
|
|
220
|
+
}
|
|
221
|
+
// M-Session-Claim-Separation (mission-40) T3 HC #1: post-T2 register_role
|
|
222
|
+
// is pure identity assertion — does NOT increment sessionEpoch. The
|
|
223
|
+
// previousEpoch vs response.sessionEpoch comparison no longer detects
|
|
224
|
+
// "I just took over" (that signal lives on claim_session's response
|
|
225
|
+
// shape: sessionClaimed + displacedPriorSession). It still detects
|
|
226
|
+
// "someone else claimed our identity between our last register_role
|
|
227
|
+
// and this one": any positive delta means an out-of-band claim happened.
|
|
228
|
+
// Pre-T2 the threshold was `> 1` (because register_role itself bumped
|
|
229
|
+
// by 1 on every call). Post-T2 the threshold is `> 0`.
|
|
230
|
+
//
|
|
231
|
+
// Takeover detection inside the adapter (claude-plugin shim.ts T3) keys
|
|
232
|
+
// on the claim_session response fields, NOT on this delta — see
|
|
233
|
+
// mission-40 brief §3 T3 + anti-goal §7.5.
|
|
234
|
+
if (ctx.previousEpoch > 0 && response.sessionEpoch - ctx.previousEpoch > 0) {
|
|
235
|
+
log.log("agent.handshake.epoch_jump", { from: ctx.previousEpoch, to: response.sessionEpoch }, `[Handshake] sessionEpoch advanced from ${ctx.previousEpoch} to ${response.sessionEpoch} between register_role calls — an external claim_session has displaced our prior session; in-flight RPCs from prior epoch may be abandoned`);
|
|
236
|
+
}
|
|
237
|
+
log.log("agent.handshake.registered", {
|
|
238
|
+
agentId: response.agentId,
|
|
239
|
+
epoch: response.sessionEpoch,
|
|
240
|
+
wasCreated: response.wasCreated,
|
|
241
|
+
}, `[Handshake] Registered as ${response.agentId} (epoch=${response.sessionEpoch}${response.wasCreated ? ", newly created" : ""})`);
|
|
242
|
+
return { response, epoch: response.sessionEpoch };
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Build a fatal-halt function with a stdio drain delay. Engineers that use
|
|
246
|
+
* stdio transports (Claude) MUST use this to avoid losing the halt message
|
|
247
|
+
* to an unflushed buffer. Engineers that do not (OpenCode) can implement
|
|
248
|
+
* their own `onFatalHalt` directly.
|
|
249
|
+
*/
|
|
250
|
+
export function makeStdioFatalHalt(log, exit = process.exit.bind(process), drainMs = 100) {
|
|
251
|
+
return (err) => {
|
|
252
|
+
const text = `[FATAL:${err.code}] ${err.message}`;
|
|
253
|
+
log(text);
|
|
254
|
+
setTimeout(() => exit(2), drainMs);
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=handshake.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handshake.js","sourceRoot":"","sources":["../../src/kernel/handshake.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* globalInstanceId bootstrap.
|
|
3
|
+
*
|
|
4
|
+
* Identity is decoupled from authentication: the Hub token grants *access*
|
|
5
|
+
* and *role*; the globalInstanceId grants *identity*. This lets tokens be
|
|
6
|
+
* rotated without orphaning Agent entities.
|
|
7
|
+
*
|
|
8
|
+
* Storage: ~/.ois/instance.json (owner-private, 0600). NOT workspace-local —
|
|
9
|
+
* workspace scoping fails on multi-terminal, clone, and accidental git commit.
|
|
10
|
+
*
|
|
11
|
+
* First call generates a new UUID v4. Subsequent calls read it. If the file
|
|
12
|
+
* is deleted or corrupted, a new UUID is generated → new fingerprint → Hub
|
|
13
|
+
* creates a new Agent. The old Agent remains append-only with its queue
|
|
14
|
+
* intact; use `migrate_agent_queue` to recover pending work.
|
|
15
|
+
*/
|
|
16
|
+
export interface LoadInstanceOptions {
|
|
17
|
+
/** Override the default ~/.ois/instance.json location (for tests). */
|
|
18
|
+
instanceFile?: string;
|
|
19
|
+
/** Optional logger for diagnostics (regeneration on corruption, etc.). */
|
|
20
|
+
log?: (msg: string) => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Load an existing globalInstanceId from disk or create a new one.
|
|
24
|
+
* Returns the UUID (or the env-provided string override). Idempotent
|
|
25
|
+
* across calls within a process.
|
|
26
|
+
*
|
|
27
|
+
* Precedence: `OIS_INSTANCE_ID` env var wins outright — used when the
|
|
28
|
+
* operator wants a stable, human-meaningful identity (e.g. "greg" or
|
|
29
|
+
* "kate") and is responsible for uniqueness themselves. Absent that,
|
|
30
|
+
* falls back to the file-persisted UUID (original behaviour). The env
|
|
31
|
+
* path does NOT touch the file, so the two schemes coexist cleanly:
|
|
32
|
+
* toggling the env var off restores the file-derived UUID untouched.
|
|
33
|
+
*
|
|
34
|
+
* This is a pragmatic escape hatch that unblocks multi-agent
|
|
35
|
+
* co-location on a single user account (two Claude instances on one
|
|
36
|
+
* laptop). A richer design — separate agentName metadata plus an
|
|
37
|
+
* agentId derived from multiple inputs — is queued for the Entity
|
|
38
|
+
* Registry SSOT mission.
|
|
39
|
+
*/
|
|
40
|
+
export declare function loadOrCreateGlobalInstanceId(opts?: LoadInstanceOptions): string;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* globalInstanceId bootstrap.
|
|
3
|
+
*
|
|
4
|
+
* Identity is decoupled from authentication: the Hub token grants *access*
|
|
5
|
+
* and *role*; the globalInstanceId grants *identity*. This lets tokens be
|
|
6
|
+
* rotated without orphaning Agent entities.
|
|
7
|
+
*
|
|
8
|
+
* Storage: ~/.ois/instance.json (owner-private, 0600). NOT workspace-local —
|
|
9
|
+
* workspace scoping fails on multi-terminal, clone, and accidental git commit.
|
|
10
|
+
*
|
|
11
|
+
* First call generates a new UUID v4. Subsequent calls read it. If the file
|
|
12
|
+
* is deleted or corrupted, a new UUID is generated → new fingerprint → Hub
|
|
13
|
+
* creates a new Agent. The old Agent remains append-only with its queue
|
|
14
|
+
* intact; use `migrate_agent_queue` to recover pending work.
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
const DEFAULT_INSTANCE_DIR = join(homedir(), ".ois");
|
|
21
|
+
const DEFAULT_INSTANCE_FILE = join(DEFAULT_INSTANCE_DIR, "instance.json");
|
|
22
|
+
/**
|
|
23
|
+
* Load an existing globalInstanceId from disk or create a new one.
|
|
24
|
+
* Returns the UUID (or the env-provided string override). Idempotent
|
|
25
|
+
* across calls within a process.
|
|
26
|
+
*
|
|
27
|
+
* Precedence: `OIS_INSTANCE_ID` env var wins outright — used when the
|
|
28
|
+
* operator wants a stable, human-meaningful identity (e.g. "greg" or
|
|
29
|
+
* "kate") and is responsible for uniqueness themselves. Absent that,
|
|
30
|
+
* falls back to the file-persisted UUID (original behaviour). The env
|
|
31
|
+
* path does NOT touch the file, so the two schemes coexist cleanly:
|
|
32
|
+
* toggling the env var off restores the file-derived UUID untouched.
|
|
33
|
+
*
|
|
34
|
+
* This is a pragmatic escape hatch that unblocks multi-agent
|
|
35
|
+
* co-location on a single user account (two Claude instances on one
|
|
36
|
+
* laptop). A richer design — separate agentName metadata plus an
|
|
37
|
+
* agentId derived from multiple inputs — is queued for the Entity
|
|
38
|
+
* Registry SSOT mission.
|
|
39
|
+
*/
|
|
40
|
+
export function loadOrCreateGlobalInstanceId(opts = {}) {
|
|
41
|
+
const log = opts.log ?? (() => { });
|
|
42
|
+
const envOverride = process.env.OIS_INSTANCE_ID?.trim();
|
|
43
|
+
if (envOverride) {
|
|
44
|
+
log(`[instance] Using OIS_INSTANCE_ID from env: ${envOverride}`);
|
|
45
|
+
return envOverride;
|
|
46
|
+
}
|
|
47
|
+
const file = opts.instanceFile ?? DEFAULT_INSTANCE_FILE;
|
|
48
|
+
const dir = file.substring(0, file.lastIndexOf("/")) || DEFAULT_INSTANCE_DIR;
|
|
49
|
+
if (!existsSync(dir)) {
|
|
50
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
51
|
+
}
|
|
52
|
+
if (existsSync(file)) {
|
|
53
|
+
try {
|
|
54
|
+
const raw = JSON.parse(readFileSync(file, "utf-8"));
|
|
55
|
+
if (raw.globalInstanceId && typeof raw.globalInstanceId === "string") {
|
|
56
|
+
return raw.globalInstanceId;
|
|
57
|
+
}
|
|
58
|
+
log(`[instance] ${file} missing globalInstanceId — regenerating`);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
log(`[instance] Failed to parse ${file}: ${err} — regenerating`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const globalInstanceId = randomUUID();
|
|
65
|
+
const body = {
|
|
66
|
+
globalInstanceId,
|
|
67
|
+
createdAt: new Date().toISOString(),
|
|
68
|
+
};
|
|
69
|
+
writeFileSync(file, JSON.stringify(body, null, 2), { mode: 0o600 });
|
|
70
|
+
try {
|
|
71
|
+
chmodSync(file, 0o600);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
/* best effort — filesystem may not support chmod */
|
|
75
|
+
}
|
|
76
|
+
log(`[instance] Generated new globalInstanceId → ${file}`);
|
|
77
|
+
return globalInstanceId;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=instance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instance.js","sourceRoot":"","sources":["../../src/kernel/instance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACxF,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,oBAAoB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,CAAC,CAAC;AACrD,MAAM,qBAAqB,GAAG,IAAI,CAAC,oBAAoB,EAAE,eAAe,CAAC,CAAC;AAc1E;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,4BAA4B,CAC1C,OAA4B,EAAE;IAE9B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,GAAgB,CAAC,CAAC,CAAC;IAEjD,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,EAAE,CAAC;IACxD,IAAI,WAAW,EAAE,CAAC;QAChB,GAAG,CAAC,8CAA8C,WAAW,EAAE,CAAC,CAAC;QACjE,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,IAAI,qBAAqB,CAAC;IACxD,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,oBAAoB,CAAC;IAE7E,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACrB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACnD,CAAC;IAED,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACrB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAiB,CAAC;YACpE,IAAI,GAAG,CAAC,gBAAgB,IAAI,OAAO,GAAG,CAAC,gBAAgB,KAAK,QAAQ,EAAE,CAAC;gBACrE,OAAO,GAAG,CAAC,gBAAgB,CAAC;YAC9B,CAAC;YACD,GAAG,CAAC,cAAc,IAAI,0CAA0C,CAAC,CAAC;QACpE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,8BAA8B,IAAI,KAAK,GAAG,iBAAiB,CAAC,CAAC;QACnE,CAAC;IACH,CAAC;IAED,MAAM,gBAAgB,GAAG,UAAU,EAAE,CAAC;IACtC,MAAM,IAAI,GAAiB;QACzB,gBAAgB;QAChB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACpC,CAAC;IACF,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACpE,IAAI,CAAC;QACH,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACzB,CAAC;IAAC,MAAM,CAAC;QACP,oDAAoD;IACtD,CAAC;IACD,GAAG,CAAC,+CAA+C,IAAI,EAAE,CAAC,CAAC;IAC3D,OAAO,gBAAgB,CAAC;AAC1B,CAAC"}
|