@haywoodmarx/agent-pager 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/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/src/broker/address.d.ts +49 -0
- package/dist/src/broker/address.js +110 -0
- package/dist/src/broker/broker.d.ts +665 -0
- package/dist/src/broker/broker.js +2270 -0
- package/dist/src/broker/database.d.ts +81 -0
- package/dist/src/broker/database.js +69 -0
- package/dist/src/broker/operator-projection.d.ts +291 -0
- package/dist/src/broker/operator-projection.js +722 -0
- package/dist/src/broker/protocol.d.ts +237 -0
- package/dist/src/broker/protocol.js +311 -0
- package/dist/src/broker/schema.d.ts +94 -0
- package/dist/src/broker/schema.js +454 -0
- package/dist/src/broker/snapshot.d.ts +171 -0
- package/dist/src/broker/snapshot.js +78 -0
- package/dist/src/configuration-defaults.d.ts +64 -0
- package/dist/src/configuration-defaults.js +48 -0
- package/dist/src/configuration.d.ts +76 -0
- package/dist/src/configuration.js +156 -0
- package/dist/src/implementations/claude/environment.d.ts +8 -0
- package/dist/src/implementations/claude/environment.js +9 -0
- package/dist/src/implementations/claude/gateway-cli.d.ts +2 -0
- package/dist/src/implementations/claude/gateway-cli.js +136 -0
- package/dist/src/implementations/claude/gateway-identity.d.ts +134 -0
- package/dist/src/implementations/claude/gateway-identity.js +103 -0
- package/dist/src/implementations/claude/gateway.d.ts +148 -0
- package/dist/src/implementations/claude/gateway.js +236 -0
- package/dist/src/implementations/claude/instructions.d.ts +33 -0
- package/dist/src/implementations/claude/instructions.js +64 -0
- package/dist/src/implementations/claude/manifest.d.ts +33 -0
- package/dist/src/implementations/claude/manifest.js +108 -0
- package/dist/src/implementations/claude/plugin-directory.d.ts +80 -0
- package/dist/src/implementations/claude/plugin-directory.js +137 -0
- package/dist/src/implementations/claude/session-continuation.d.ts +25 -0
- package/dist/src/implementations/claude/session-continuation.js +125 -0
- package/dist/src/implementations/claude/session-identity.d.ts +25 -0
- package/dist/src/implementations/claude/session-identity.js +31 -0
- package/dist/src/implementations/claude/session-start-cli.d.ts +2 -0
- package/dist/src/implementations/claude/session-start-cli.js +16 -0
- package/dist/src/implementations/claude/socket-delivery.d.ts +52 -0
- package/dist/src/implementations/claude/socket-delivery.js +159 -0
- package/dist/src/implementations/model.d.ts +162 -0
- package/dist/src/implementations/model.js +1 -0
- package/dist/src/implementations/opencode/adapter.d.ts +71 -0
- package/dist/src/implementations/opencode/adapter.js +94 -0
- package/dist/src/implementations/opencode/environment.d.ts +8 -0
- package/dist/src/implementations/opencode/environment.js +9 -0
- package/dist/src/implementations/opencode/manifest.d.ts +30 -0
- package/dist/src/implementations/opencode/manifest.js +93 -0
- package/dist/src/implementations/opencode/plugin-api.d.ts +120 -0
- package/dist/src/implementations/opencode/plugin-api.js +1 -0
- package/dist/src/implementations/opencode/plugin.d.ts +38 -0
- package/dist/src/implementations/opencode/plugin.js +303 -0
- package/dist/src/implementations/opencode/self-close-tool.d.ts +9 -0
- package/dist/src/implementations/opencode/self-close-tool.js +31 -0
- package/dist/src/implementations/orca/terminal.d.ts +48 -0
- package/dist/src/implementations/orca/terminal.js +154 -0
- package/dist/src/implementations/pi/broker-client.d.ts +56 -0
- package/dist/src/implementations/pi/broker-client.js +156 -0
- package/dist/src/implementations/pi/environment.d.ts +8 -0
- package/dist/src/implementations/pi/environment.js +9 -0
- package/dist/src/implementations/pi/extension.d.ts +57 -0
- package/dist/src/implementations/pi/extension.js +253 -0
- package/dist/src/implementations/pi/host-slots.d.ts +18 -0
- package/dist/src/implementations/pi/host-slots.js +27 -0
- package/dist/src/implementations/pi/manifest.d.ts +11 -0
- package/dist/src/implementations/pi/manifest.js +61 -0
- package/dist/src/implementations/pi/self-close-tool.d.ts +7 -0
- package/dist/src/implementations/pi/self-close-tool.js +27 -0
- package/dist/src/implementations/registry.d.ts +47 -0
- package/dist/src/implementations/registry.js +157 -0
- package/dist/src/implementations/session-key.d.ts +13 -0
- package/dist/src/implementations/session-key.js +19 -0
- package/dist/src/index.d.ts +27 -0
- package/dist/src/index.js +26 -0
- package/dist/src/launch/broker-lock.d.ts +257 -0
- package/dist/src/launch/broker-lock.js +644 -0
- package/dist/src/launch/cli.d.ts +37 -0
- package/dist/src/launch/cli.js +317 -0
- package/dist/src/launch/environment.d.ts +45 -0
- package/dist/src/launch/environment.js +48 -0
- package/dist/src/launch/launch.d.ts +235 -0
- package/dist/src/launch/launch.js +223 -0
- package/dist/src/launch/on-demand.d.ts +202 -0
- package/dist/src/launch/on-demand.js +435 -0
- package/dist/src/launch/runtime-composition.d.ts +88 -0
- package/dist/src/launch/runtime-composition.js +253 -0
- package/dist/src/launch/runtime.d.ts +79 -0
- package/dist/src/launch/runtime.js +196 -0
- package/dist/src/launch/service-system.d.ts +11 -0
- package/dist/src/launch/service-system.js +39 -0
- package/dist/src/launch/service.d.ts +173 -0
- package/dist/src/launch/service.js +492 -0
- package/dist/src/launch/skill.d.ts +57 -0
- package/dist/src/launch/skill.js +81 -0
- package/dist/src/launch/terminal.d.ts +26 -0
- package/dist/src/launch/terminal.js +43 -0
- package/dist/src/launch/workspace-branch.d.ts +10 -0
- package/dist/src/launch/workspace-branch.js +29 -0
- package/dist/src/messaging/delivery-client.d.ts +144 -0
- package/dist/src/messaging/delivery-client.js +261 -0
- package/dist/src/messaging/delivery.d.ts +75 -0
- package/dist/src/messaging/delivery.js +22 -0
- package/dist/src/messaging/inbox-socket.d.ts +98 -0
- package/dist/src/messaging/inbox-socket.js +278 -0
- package/dist/src/messaging/mcp.d.ts +53 -0
- package/dist/src/messaging/mcp.js +501 -0
- package/dist/src/messaging/notifications.d.ts +231 -0
- package/dist/src/messaging/notifications.js +489 -0
- package/dist/src/messaging/push-delivery.d.ts +77 -0
- package/dist/src/messaging/push-delivery.js +187 -0
- package/dist/src/messaging/tools.d.ts +11 -0
- package/dist/src/messaging/tools.js +102 -0
- package/dist/src/messaging/wake.d.ts +23 -0
- package/dist/src/messaging/wake.js +39 -0
- package/dist/src/operator/agent-projection.d.ts +102 -0
- package/dist/src/operator/agent-projection.js +249 -0
- package/dist/src/operator/commands.d.ts +50 -0
- package/dist/src/operator/commands.js +199 -0
- package/dist/src/operator/diagnostics.d.ts +21 -0
- package/dist/src/operator/diagnostics.js +22 -0
- package/dist/src/operator/protocol.d.ts +511 -0
- package/dist/src/operator/protocol.js +49 -0
- package/dist/src/operator/repository-system.d.ts +14 -0
- package/dist/src/operator/repository-system.js +150 -0
- package/dist/src/operator/repository.d.ts +79 -0
- package/dist/src/operator/repository.js +72 -0
- package/dist/src/operator/traffic-projection.d.ts +19 -0
- package/dist/src/operator/traffic-projection.js +229 -0
- package/dist/src/operator/transport.d.ts +87 -0
- package/dist/src/operator/transport.js +549 -0
- package/dist/src/operator/update-store.d.ts +53 -0
- package/dist/src/operator/update-store.js +162 -0
- package/dist/src/operator/updates.d.ts +94 -0
- package/dist/src/operator/updates.js +280 -0
- package/dist/src/operator/validation.d.ts +114 -0
- package/dist/src/operator/validation.js +916 -0
- package/dist/src/shared/guards.d.ts +59 -0
- package/dist/src/shared/guards.js +119 -0
- package/dist/src/shared/http.d.ts +30 -0
- package/dist/src/shared/http.js +50 -0
- package/dist/src/shared/row.d.ts +2 -0
- package/dist/src/shared/row.js +1 -0
- package/dist/src/shared/shell.d.ts +6 -0
- package/dist/src/shared/shell.js +8 -0
- package/dist/src/tui/actions.d.ts +166 -0
- package/dist/src/tui/actions.js +253 -0
- package/dist/src/tui/agent-tree.d.ts +125 -0
- package/dist/src/tui/agent-tree.js +663 -0
- package/dist/src/tui/app.d.ts +38 -0
- package/dist/src/tui/app.js +378 -0
- package/dist/src/tui/chrome.d.ts +86 -0
- package/dist/src/tui/chrome.js +85 -0
- package/dist/src/tui/client.d.ts +41 -0
- package/dist/src/tui/client.js +279 -0
- package/dist/src/tui/composition.d.ts +41 -0
- package/dist/src/tui/composition.js +146 -0
- package/dist/src/tui/message-views.d.ts +94 -0
- package/dist/src/tui/message-views.js +515 -0
- package/dist/src/tui/model.d.ts +414 -0
- package/dist/src/tui/model.js +758 -0
- package/dist/src/tui/presentation.d.ts +350 -0
- package/dist/src/tui/presentation.js +541 -0
- package/dist/src/tui/production.d.ts +79 -0
- package/dist/src/tui/production.js +253 -0
- package/dist/src/tui/run-terminal.d.ts +32 -0
- package/dist/src/tui/run-terminal.js +54 -0
- package/dist/src/tui/runtime.d.ts +113 -0
- package/dist/src/tui/runtime.js +189 -0
- package/dist/src/tui/theme.d.ts +49 -0
- package/dist/src/tui/theme.js +55 -0
- package/dist/src/tui/transport.d.ts +31 -0
- package/dist/src/tui/transport.js +255 -0
- package/dist/src/version.d.ts +11 -0
- package/dist/src/version.js +52 -0
- package/package.json +142 -0
- package/skills/pager/SKILL.md +112 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { isAgentId } from "../../broker/protocol.js";
|
|
5
|
+
import { adapterHeartbeatIntervalMs } from "../../configuration-defaults.js";
|
|
6
|
+
import { ensureBrokerService } from "../../launch/on-demand.js";
|
|
7
|
+
import { readEmbeddedRuntimeEnvironment, resolveBunExecutable } from "../../launch/runtime.js";
|
|
8
|
+
import { MCP_TOOLS } from "../../messaging/tools.js";
|
|
9
|
+
import { isPlainObject } from "../../shared/guards.js";
|
|
10
|
+
import { openPagerInbox, pagerInboxGuidance, } from "../../messaging/inbox-socket.js";
|
|
11
|
+
import { PAGER_WAKE_TYPE } from "../../messaging/wake.js";
|
|
12
|
+
import { PiBrokerClient, registerPiAgent } from "./broker-client.js";
|
|
13
|
+
import { PI_AGENT_ID_ENVIRONMENT, PI_BROKER_URL_ENVIRONMENT, PI_DELIVERY_RESERVATION_ENVIRONMENT, PI_MESSAGING_RESERVATION_ENVIRONMENT, } from "./environment.js";
|
|
14
|
+
import { loadedApis, systemPiRuntimeSlot } from "./host-slots.js";
|
|
15
|
+
import { registerPiSelfCloseTool } from "./self-close-tool.js";
|
|
16
|
+
import { harnessSessionKey } from "../session-key.js";
|
|
17
|
+
/** Names the tool whose prompt guidelines carry the standing Pager guidance. */
|
|
18
|
+
const PI_ACK_TOOL = "ack_agent_messages";
|
|
19
|
+
/**
|
|
20
|
+
* Construct the Pager Pi extension around explicit host dependencies.
|
|
21
|
+
* @param dependencies - Service, HTTP, environment, and scheduling operations.
|
|
22
|
+
* @returns - Pi extension factory that registers broker tools and session lifecycle hooks.
|
|
23
|
+
*/
|
|
24
|
+
export function createPiExtension(dependencies) {
|
|
25
|
+
let runtime;
|
|
26
|
+
const sharedRuntime = () => {
|
|
27
|
+
if (runtime)
|
|
28
|
+
return runtime;
|
|
29
|
+
const existing = dependencies.runtimeSlot.get();
|
|
30
|
+
if (isPiExtensionRuntime(existing)) {
|
|
31
|
+
runtime = existing;
|
|
32
|
+
return runtime;
|
|
33
|
+
}
|
|
34
|
+
runtime = { directCwd: undefined, tasks: [] };
|
|
35
|
+
dependencies.runtimeSlot.set(runtime);
|
|
36
|
+
return runtime;
|
|
37
|
+
};
|
|
38
|
+
return (pi) => {
|
|
39
|
+
const apis = loadedApis();
|
|
40
|
+
if (apis.has(pi))
|
|
41
|
+
return;
|
|
42
|
+
apis.add(pi);
|
|
43
|
+
for (const tool of MCP_TOOLS) {
|
|
44
|
+
pi.registerTool({
|
|
45
|
+
name: tool.name,
|
|
46
|
+
label: tool.name,
|
|
47
|
+
description: tool.description,
|
|
48
|
+
...(tool.name === PI_ACK_TOOL ? { promptGuidelines: [...pagerInboxGuidance(PI_ACK_TOOL)] } : {}),
|
|
49
|
+
parameters: Type.Unsafe(tool.inputSchema),
|
|
50
|
+
execute: async (_toolCallId, params) => {
|
|
51
|
+
const client = sharedRuntime().client;
|
|
52
|
+
if (!client)
|
|
53
|
+
throw new Error("Pager is not connected");
|
|
54
|
+
const result = await client.callTool(tool.name, recordInput(params));
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
57
|
+
details: { result },
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
registerPiSelfCloseTool(pi, () => {
|
|
63
|
+
stopTasks(sharedRuntime());
|
|
64
|
+
});
|
|
65
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
66
|
+
stopTasks(sharedRuntime());
|
|
67
|
+
try {
|
|
68
|
+
const runtime = sharedRuntime();
|
|
69
|
+
if (!hasLaunchIdentity(dependencies.processEnv) &&
|
|
70
|
+
runtime.client &&
|
|
71
|
+
runtime.directCwd !== undefined &&
|
|
72
|
+
resolve(runtime.directCwd) === resolve(ctx.cwd)) {
|
|
73
|
+
startTasks(pi, ctx, dependencies, runtime);
|
|
74
|
+
await presentInbox(ctx, dependencies, runtime);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// The process-wide inbox follows this process to the identity it registers next, so the one it
|
|
78
|
+
// leaves must be detached first; otherwise the broker keeps pushing that identity's messages here,
|
|
79
|
+
// where they are claimed and cannot be acknowledged.
|
|
80
|
+
if (!hasLaunchIdentity(dependencies.processEnv) && runtime.client && runtime.directCwd !== undefined) {
|
|
81
|
+
const departing = runtime.client;
|
|
82
|
+
delete runtime.client;
|
|
83
|
+
delete runtime.connectionKey;
|
|
84
|
+
await departing.detach().catch(() => undefined);
|
|
85
|
+
}
|
|
86
|
+
const connection = await resolveConnection(dependencies, ctx);
|
|
87
|
+
// A direct-mode registration displaced whatever the identity held, so its fresh reservations
|
|
88
|
+
// are the only working ones.
|
|
89
|
+
if (!runtime.client || runtime.connectionKey !== connection.key || connection.directCwd !== undefined) {
|
|
90
|
+
runtime.client = await PiBrokerClient.connect(connection.brokerUrl, connection.identity, dependencies.fetch, connection.sessionKey);
|
|
91
|
+
runtime.connectionKey = connection.key;
|
|
92
|
+
runtime.directCwd = connection.directCwd;
|
|
93
|
+
}
|
|
94
|
+
startTasks(pi, ctx, dependencies, runtime);
|
|
95
|
+
await presentInbox(ctx, dependencies, runtime);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
notifyFailure(ctx, error);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
pi.on("session_shutdown", async (event) => {
|
|
102
|
+
const shared = sharedRuntime();
|
|
103
|
+
stopTasks(shared);
|
|
104
|
+
// Every other reason replaces the session inside this process, which reuses the connection.
|
|
105
|
+
if (event.reason !== "quit")
|
|
106
|
+
return;
|
|
107
|
+
const client = shared.client;
|
|
108
|
+
delete shared.client;
|
|
109
|
+
delete shared.connectionKey;
|
|
110
|
+
shared.inbox?.close();
|
|
111
|
+
delete shared.inbox;
|
|
112
|
+
// A failed notice leaves the lease to lapse, as after a crash.
|
|
113
|
+
await client?.detach().catch(() => undefined);
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function startTasks(pi, ctx, dependencies, runtime) {
|
|
118
|
+
runtime.inject = (content) => {
|
|
119
|
+
pi.sendMessage({ customType: PAGER_WAKE_TYPE, content, display: true }, { triggerTurn: true, ...(ctx.isIdle() ? {} : { deliverAs: "followUp" }) });
|
|
120
|
+
};
|
|
121
|
+
runtime.tasks.push({
|
|
122
|
+
cancel: dependencies.scheduler.repeat(adapterHeartbeatIntervalMs(dependencies.processEnv), () => {
|
|
123
|
+
void runtime.client?.heartbeat().catch((error) => {
|
|
124
|
+
notifyFailure(ctx, error);
|
|
125
|
+
});
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Open this process's inbox if it has none, present it on the current connection, and register it
|
|
131
|
+
* at once with a heartbeat.
|
|
132
|
+
*
|
|
133
|
+
* A failure to open is reported once and not retried: the session keeps its tools but receives no
|
|
134
|
+
* delivery, and its messages wait for a later session.
|
|
135
|
+
* @param ctx - Session context the failure is reported through.
|
|
136
|
+
* @param dependencies - Supplies the inbox opener.
|
|
137
|
+
* @param runtime - Shared runtime holding the connection and the inbox.
|
|
138
|
+
*/
|
|
139
|
+
async function presentInbox(ctx, dependencies, runtime) {
|
|
140
|
+
if (!runtime.inbox && !runtime.inboxFailed) {
|
|
141
|
+
try {
|
|
142
|
+
// eslint-disable-next-line @typescript-eslint/require-await -- async turns a throwing sendMessage into the rejection the inbox reports as a refused delivery.
|
|
143
|
+
runtime.inbox = await dependencies.openInbox(async (content) => {
|
|
144
|
+
if (!runtime.inject)
|
|
145
|
+
throw new Error("No Pi session is active");
|
|
146
|
+
runtime.inject(content);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
runtime.inboxFailed = true;
|
|
151
|
+
const cause = error instanceof Error ? error.message : String(error);
|
|
152
|
+
notifyFailure(ctx, new Error(`Pager inbox could not be opened, so this session receives no messages: ${cause}`));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (runtime.inbox)
|
|
156
|
+
runtime.client?.presentDeliveryAddress(runtime.inbox.address);
|
|
157
|
+
await runtime.client?.heartbeat();
|
|
158
|
+
}
|
|
159
|
+
async function resolveConnection(dependencies, ctx) {
|
|
160
|
+
const brokerUrl = dependencies.processEnv[PI_BROKER_URL_ENVIRONMENT];
|
|
161
|
+
const agentId = dependencies.processEnv[PI_AGENT_ID_ENVIRONMENT];
|
|
162
|
+
const messagingReservation = dependencies.processEnv[PI_MESSAGING_RESERVATION_ENVIRONMENT];
|
|
163
|
+
const deliveryReservation = dependencies.processEnv[PI_DELIVERY_RESERVATION_ENVIRONMENT];
|
|
164
|
+
const sessionKey = harnessSessionKey("pi", ctx.sessionManager.getSessionId());
|
|
165
|
+
const launchValues = [brokerUrl, agentId, messagingReservation, deliveryReservation];
|
|
166
|
+
if (launchValues.some(Boolean)) {
|
|
167
|
+
if (!brokerUrl || !isAgentId(agentId) || !messagingReservation || !deliveryReservation) {
|
|
168
|
+
throw new Error("Pager launch identity is incomplete");
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
brokerUrl,
|
|
172
|
+
identity: { agentId, messagingReservation, deliveryReservation },
|
|
173
|
+
key: `${brokerUrl}\u0000${agentId}`,
|
|
174
|
+
sessionKey,
|
|
175
|
+
directCwd: undefined,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const endpoints = await dependencies.ensureService(dependencies.readEnvironment());
|
|
179
|
+
const identity = await registerPiAgent({
|
|
180
|
+
request: dependencies.fetch,
|
|
181
|
+
brokerUrl: endpoints.mcpUrl,
|
|
182
|
+
workspacePath: ctx.cwd,
|
|
183
|
+
sessionName: ctx.sessionManager.getSessionId(),
|
|
184
|
+
sessionKey,
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
brokerUrl: endpoints.mcpUrl,
|
|
188
|
+
identity,
|
|
189
|
+
key: `${endpoints.mcpUrl}\u0000${identity.agentId}`,
|
|
190
|
+
sessionKey,
|
|
191
|
+
directCwd: ctx.cwd,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function stopTasks(runtime) {
|
|
195
|
+
delete runtime.inject;
|
|
196
|
+
for (const task of runtime.tasks.splice(0))
|
|
197
|
+
task.cancel();
|
|
198
|
+
}
|
|
199
|
+
function notifyFailure(ctx, error) {
|
|
200
|
+
if (ctx.hasUI)
|
|
201
|
+
ctx.ui.notify(error instanceof Error ? error.message : "Pager request failed", "warning");
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Recognise a runtime this build published, so a value from a differently shaped copy of the
|
|
205
|
+
* extension is replaced rather than adopted; whatever it scheduled keeps running with nothing
|
|
206
|
+
* holding it.
|
|
207
|
+
* @param value - Candidate runtime read from a shared runtime slot.
|
|
208
|
+
* @returns - True when every field this build reads is present and well formed.
|
|
209
|
+
*/
|
|
210
|
+
function isPiExtensionRuntime(value) {
|
|
211
|
+
return (isPlainObject(value) &&
|
|
212
|
+
"tasks" in value &&
|
|
213
|
+
Array.isArray(value.tasks) &&
|
|
214
|
+
value.tasks.every(isScheduledTask) &&
|
|
215
|
+
(!("inbox" in value) || value.inbox === undefined || isPagerInbox(value.inbox)));
|
|
216
|
+
}
|
|
217
|
+
function isPagerInbox(value) {
|
|
218
|
+
return isPlainObject(value) && typeof value.address === "string" && typeof value.close === "function";
|
|
219
|
+
}
|
|
220
|
+
function isScheduledTask(value) {
|
|
221
|
+
return isPlainObject(value) && typeof value.cancel === "function";
|
|
222
|
+
}
|
|
223
|
+
function recordInput(value) {
|
|
224
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
225
|
+
throw new Error("Pi tool arguments must be an object");
|
|
226
|
+
}
|
|
227
|
+
return Object.fromEntries(Object.entries(value));
|
|
228
|
+
}
|
|
229
|
+
function hasLaunchIdentity(environment) {
|
|
230
|
+
return [
|
|
231
|
+
environment[PI_BROKER_URL_ENVIRONMENT],
|
|
232
|
+
environment[PI_AGENT_ID_ENVIRONMENT],
|
|
233
|
+
environment[PI_MESSAGING_RESERVATION_ENVIRONMENT],
|
|
234
|
+
environment[PI_DELIVERY_RESERVATION_ENVIRONMENT],
|
|
235
|
+
].some(Boolean);
|
|
236
|
+
}
|
|
237
|
+
const defaultDependencies = {
|
|
238
|
+
readEnvironment: () => readEmbeddedRuntimeEnvironment(process.env, resolveBunExecutable),
|
|
239
|
+
ensureService: (environment) => ensureBrokerService(environment),
|
|
240
|
+
processEnv: process.env,
|
|
241
|
+
fetch: (url, init) => globalThis.fetch(url, init),
|
|
242
|
+
runtimeSlot: systemPiRuntimeSlot(),
|
|
243
|
+
openInbox: (accept) => openPagerInbox(accept, tmpdir()),
|
|
244
|
+
scheduler: {
|
|
245
|
+
repeat: (intervalMs, task) => {
|
|
246
|
+
const handle = setInterval(task, intervalMs);
|
|
247
|
+
return () => {
|
|
248
|
+
clearInterval(handle);
|
|
249
|
+
};
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
export default createPiExtension(defaultDependencies);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Shares one value across every loaded copy of the Pi extension within one host process. */
|
|
2
|
+
export type PiHostSlot<T> = {
|
|
3
|
+
/** Reads the value another loaded copy of the extension may already have published. */
|
|
4
|
+
get(): T | undefined;
|
|
5
|
+
/** Publishes a value for another loaded copy of the extension to adopt. */
|
|
6
|
+
set(value: T): void;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Build the production runtime slot backed by the shared host global.
|
|
10
|
+
* @returns - Slot reading and writing the process-wide runtime symbol.
|
|
11
|
+
*/
|
|
12
|
+
export declare function systemPiRuntimeSlot<T>(): PiHostSlot<T>;
|
|
13
|
+
/**
|
|
14
|
+
* Track which `pi` hosts this build has already registered tools on, across every loaded copy of the
|
|
15
|
+
* extension module.
|
|
16
|
+
* @returns - Set shared through the process-wide api-registration symbol.
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadedApis(): WeakSet<object>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const runtimeKey = Symbol.for("pager.pi.runtime");
|
|
2
|
+
const apiKey = Symbol.for("pager.pi.apis");
|
|
3
|
+
/**
|
|
4
|
+
* Build the production runtime slot backed by the shared host global.
|
|
5
|
+
* @returns - Slot reading and writing the process-wide runtime symbol.
|
|
6
|
+
*/
|
|
7
|
+
export function systemPiRuntimeSlot() {
|
|
8
|
+
return {
|
|
9
|
+
get: () => Reflect.get(globalThis, runtimeKey),
|
|
10
|
+
set: (value) => {
|
|
11
|
+
Reflect.set(globalThis, runtimeKey, value);
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Track which `pi` hosts this build has already registered tools on, across every loaded copy of the
|
|
17
|
+
* extension module.
|
|
18
|
+
* @returns - Set shared through the process-wide api-registration symbol.
|
|
19
|
+
*/
|
|
20
|
+
export function loadedApis() {
|
|
21
|
+
const existing = Reflect.get(globalThis, apiKey);
|
|
22
|
+
if (existing instanceof WeakSet)
|
|
23
|
+
return existing;
|
|
24
|
+
const apis = new WeakSet();
|
|
25
|
+
Reflect.set(globalThis, apiKey, apis);
|
|
26
|
+
return apis;
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type PagerInboxWriter } from "../../messaging/inbox-socket.js";
|
|
2
|
+
import type { ImplementationProvider } from "../model.js";
|
|
3
|
+
/**
|
|
4
|
+
* Construct the built-in Pi implementation provider.
|
|
5
|
+
* @param readVersion - Injected installed-version reader.
|
|
6
|
+
* @param writeInbox - Connection that writes one delivery into a session's inbox.
|
|
7
|
+
* @returns - Provider owning Pi launch and notification integration.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createPiImplementationProvider(readVersion: () => string | null, writeInbox: PagerInboxWriter): ImplementationProvider;
|
|
10
|
+
declare const _default: ImplementationProvider;
|
|
11
|
+
export default _default;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { deliverToPagerInbox, parsePagerInboxAddress, writePagerInbox, } from "../../messaging/inbox-socket.js";
|
|
4
|
+
import { implementationId } from "../registry.js";
|
|
5
|
+
import { PI_AGENT_ID_ENVIRONMENT, PI_BROKER_URL_ENVIRONMENT, PI_DELIVERY_RESERVATION_ENVIRONMENT, PI_MESSAGING_RESERVATION_ENVIRONMENT, } from "./environment.js";
|
|
6
|
+
import { harnessSessionKey } from "../session-key.js";
|
|
7
|
+
const extensionPath = fileURLToPath(new URL("./extension.js", import.meta.url));
|
|
8
|
+
/**
|
|
9
|
+
* Construct the built-in Pi implementation provider.
|
|
10
|
+
* @param readVersion - Injected installed-version reader.
|
|
11
|
+
* @param writeInbox - Connection that writes one delivery into a session's inbox.
|
|
12
|
+
* @returns - Provider owning Pi launch and notification integration.
|
|
13
|
+
*/
|
|
14
|
+
export function createPiImplementationProvider(readVersion, writeInbox) {
|
|
15
|
+
const version = readVersion();
|
|
16
|
+
return {
|
|
17
|
+
descriptor: {
|
|
18
|
+
id: implementationId("pi"),
|
|
19
|
+
displayName: "Pi",
|
|
20
|
+
version: version ? { known: true, value: version } : { known: false },
|
|
21
|
+
capabilities: ["launch", "notification", "session-identity"],
|
|
22
|
+
},
|
|
23
|
+
launch: {
|
|
24
|
+
supported: true,
|
|
25
|
+
buildPlan: (request) => Promise.resolve({
|
|
26
|
+
command: "pi",
|
|
27
|
+
args: ["--extension", extensionPath, ...request.passthroughArgs],
|
|
28
|
+
environment: {
|
|
29
|
+
[PI_AGENT_ID_ENVIRONMENT]: request.agentId,
|
|
30
|
+
[PI_BROKER_URL_ENVIRONMENT]: request.brokerUrl,
|
|
31
|
+
},
|
|
32
|
+
sensitiveEnvironment: {
|
|
33
|
+
[PI_MESSAGING_RESERVATION_ENVIRONMENT]: request.reservation,
|
|
34
|
+
[PI_DELIVERY_RESERVATION_ENVIRONMENT]: request.deliveryReservation,
|
|
35
|
+
},
|
|
36
|
+
workspacePath: request.workspacePath,
|
|
37
|
+
title: "Pager — Pi",
|
|
38
|
+
}),
|
|
39
|
+
},
|
|
40
|
+
sessionIdentity: {
|
|
41
|
+
supported: true,
|
|
42
|
+
sessionKey: ({ hostSessionId }) => harnessSessionKey("pi", hostSessionId),
|
|
43
|
+
},
|
|
44
|
+
hostRegistration: { supported: false },
|
|
45
|
+
notification: {
|
|
46
|
+
supported: true,
|
|
47
|
+
mode: "host-push",
|
|
48
|
+
parseAddress: parsePagerInboxAddress,
|
|
49
|
+
deliver: (address, wake) => deliverToPagerInbox(writeInbox, address, wake),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function installedVersion() {
|
|
54
|
+
try {
|
|
55
|
+
return execFileSync("pi", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export default createPiImplementationProvider(installedVersion, writePagerInbox);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
/**
|
|
3
|
+
* Register the Pi-local tool that requests an orderly process exit.
|
|
4
|
+
* @param pi - Pi extension API receiving the tool registration.
|
|
5
|
+
* @param beforeShutdown - Synchronous cleanup that prevents extension work from delaying exit.
|
|
6
|
+
*/
|
|
7
|
+
export declare function registerPiSelfCloseTool(pi: ExtensionAPI, beforeShutdown: () => void): void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
/**
|
|
3
|
+
* Register the Pi-local tool that requests an orderly process exit.
|
|
4
|
+
* @param pi - Pi extension API receiving the tool registration.
|
|
5
|
+
* @param beforeShutdown - Synchronous cleanup that prevents extension work from delaying exit.
|
|
6
|
+
*/
|
|
7
|
+
export function registerPiSelfCloseTool(pi, beforeShutdown) {
|
|
8
|
+
let shutdownRequested = false;
|
|
9
|
+
pi.registerTool({
|
|
10
|
+
name: "close_pi_session",
|
|
11
|
+
label: "Close Pi Session",
|
|
12
|
+
description: "Gracefully close this Pi session after assigned work is complete and no further response is needed.",
|
|
13
|
+
parameters: Type.Object({}, { additionalProperties: false }),
|
|
14
|
+
execute: (_toolCallId, _params, _signal, _onUpdate, ctx) => {
|
|
15
|
+
if (!shutdownRequested) {
|
|
16
|
+
beforeShutdown();
|
|
17
|
+
ctx.shutdown();
|
|
18
|
+
shutdownRequested = true;
|
|
19
|
+
}
|
|
20
|
+
return Promise.resolve({
|
|
21
|
+
content: [{ type: "text", text: "Graceful Pi shutdown requested." }],
|
|
22
|
+
details: { requested: true },
|
|
23
|
+
terminate: true,
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ImplementationDescriptor, ImplementationId, ImplementationProvider } from "./model.js";
|
|
2
|
+
/** Supplies generic filesystem and module-loading operations for provider discovery. */
|
|
3
|
+
export interface ImplementationDiscoveryPort {
|
|
4
|
+
/** Lists immediate implementation directories beneath the configured root. */
|
|
5
|
+
listDirectories(root: string): Promise<readonly string[]>;
|
|
6
|
+
/** Loads the provider exported by one conventionally named manifest. */
|
|
7
|
+
loadProvider(manifestPath: string): Promise<unknown>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Validate and brand an open implementation identifier.
|
|
11
|
+
* @param value - Untrusted identifier value to validate.
|
|
12
|
+
* @returns - Validated open implementation identifier.
|
|
13
|
+
*/
|
|
14
|
+
export declare function implementationId(value: unknown): ImplementationId;
|
|
15
|
+
/**
|
|
16
|
+
* Validate an implementation provider loaded across a manifest boundary.
|
|
17
|
+
* @param value - Untrusted manifest export to validate.
|
|
18
|
+
* @param manifestPath - Manifest path named in validation diagnostics.
|
|
19
|
+
*/
|
|
20
|
+
export declare function assertImplementationProvider(value: unknown, manifestPath: string): asserts value is ImplementationProvider;
|
|
21
|
+
/** Resolve open implementation ids through injected provider instances. */
|
|
22
|
+
export declare class ImplementationRegistry {
|
|
23
|
+
#private;
|
|
24
|
+
/**
|
|
25
|
+
* Construct a registry after validating every provider and duplicate id.
|
|
26
|
+
* @param providers - Provider instances to validate and index.
|
|
27
|
+
*/
|
|
28
|
+
constructor(providers: readonly ImplementationProvider[]);
|
|
29
|
+
/**
|
|
30
|
+
* Resolve one provider by opaque implementation id.
|
|
31
|
+
* @param id - Validated implementation identifier to resolve.
|
|
32
|
+
* @returns - Registered provider for the identifier.
|
|
33
|
+
*/
|
|
34
|
+
resolve(id: ImplementationId): ImplementationProvider;
|
|
35
|
+
/**
|
|
36
|
+
* List validated descriptors without exposing concrete provider types.
|
|
37
|
+
* @returns - Validated descriptors in registry insertion order.
|
|
38
|
+
*/
|
|
39
|
+
descriptors(): readonly ImplementationDescriptor[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Discover implementation manifests beneath a configured root by convention.
|
|
43
|
+
* @param root - Configured directory containing implementation subdirectories.
|
|
44
|
+
* @param port - Injected discovery and module-loading boundary.
|
|
45
|
+
* @returns - Registry containing every validated discovered provider.
|
|
46
|
+
*/
|
|
47
|
+
export declare function discoverImplementationRegistry(root: string, port: ImplementationDiscoveryPort): Promise<ImplementationRegistry>;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { ProtocolError, validateImplementationId } from "../broker/protocol.js";
|
|
3
|
+
import { isPlainObject as isRecord } from "../shared/guards.js";
|
|
4
|
+
const DISPLAY_NAME_MAX_LENGTH = 80;
|
|
5
|
+
/**
|
|
6
|
+
* Spells the capability vocabulary as the keys of an exhaustive record, so the compiler refuses a
|
|
7
|
+
* list that has drifted from {@link ImplementationCapabilityName}.
|
|
8
|
+
*/
|
|
9
|
+
const capabilityNames = Object.keys({
|
|
10
|
+
launch: true,
|
|
11
|
+
notification: true,
|
|
12
|
+
"session-identity": true,
|
|
13
|
+
"host-registration": true,
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Validate and brand an open implementation identifier.
|
|
17
|
+
* @param value - Untrusted identifier value to validate.
|
|
18
|
+
* @returns - Validated open implementation identifier.
|
|
19
|
+
*/
|
|
20
|
+
export function implementationId(value) {
|
|
21
|
+
return validateImplementationId(value);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Validate an implementation provider loaded across a manifest boundary.
|
|
25
|
+
* @param value - Untrusted manifest export to validate.
|
|
26
|
+
* @param manifestPath - Manifest path named in validation diagnostics.
|
|
27
|
+
*/
|
|
28
|
+
export function assertImplementationProvider(value, manifestPath) {
|
|
29
|
+
if (!isRecord(value))
|
|
30
|
+
invalidManifest(manifestPath, "must export an implementation provider");
|
|
31
|
+
const descriptor = value.descriptor;
|
|
32
|
+
if (!isRecord(descriptor))
|
|
33
|
+
invalidManifest(manifestPath, "must export a descriptor");
|
|
34
|
+
implementationId(descriptor.id);
|
|
35
|
+
if (typeof descriptor.displayName !== "string" ||
|
|
36
|
+
descriptor.displayName.length < 1 ||
|
|
37
|
+
descriptor.displayName.length > DISPLAY_NAME_MAX_LENGTH) {
|
|
38
|
+
invalidManifest(manifestPath, "has an invalid display name");
|
|
39
|
+
}
|
|
40
|
+
if (!isKnownVersion(descriptor.version))
|
|
41
|
+
invalidManifest(manifestPath, "has an invalid version value");
|
|
42
|
+
if (!Array.isArray(descriptor.capabilities))
|
|
43
|
+
invalidManifest(manifestPath, "must declare capabilities");
|
|
44
|
+
const declared = new Set();
|
|
45
|
+
for (const capability of descriptor.capabilities) {
|
|
46
|
+
if (!isCapabilityName(capability))
|
|
47
|
+
invalidManifest(manifestPath, "declares an unknown capability");
|
|
48
|
+
if (declared.has(capability))
|
|
49
|
+
invalidManifest(manifestPath, `declares duplicate capability ${capability}`);
|
|
50
|
+
declared.add(capability);
|
|
51
|
+
}
|
|
52
|
+
const capabilitySupport = {
|
|
53
|
+
launch: capabilitySupportState(value.launch, "buildPlan", manifestPath, "launch"),
|
|
54
|
+
notification: notificationSupportState(value.notification, manifestPath),
|
|
55
|
+
"session-identity": capabilitySupportState(value.sessionIdentity, "sessionKey", manifestPath, "session-identity"),
|
|
56
|
+
"host-registration": capabilitySupportState(value.hostRegistration, "registrationCommand", manifestPath, "host-registration"),
|
|
57
|
+
};
|
|
58
|
+
for (const capability of capabilityNames) {
|
|
59
|
+
if (declared.has(capability) !== capabilitySupport[capability]) {
|
|
60
|
+
invalidManifest(manifestPath, `descriptor and provider disagree about capability ${capability}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Resolve open implementation ids through injected provider instances. */
|
|
65
|
+
export class ImplementationRegistry {
|
|
66
|
+
#providers;
|
|
67
|
+
/**
|
|
68
|
+
* Construct a registry after validating every provider and duplicate id.
|
|
69
|
+
* @param providers - Provider instances to validate and index.
|
|
70
|
+
*/
|
|
71
|
+
constructor(providers) {
|
|
72
|
+
const indexed = new Map();
|
|
73
|
+
for (const provider of providers) {
|
|
74
|
+
assertImplementationProvider(provider, `provider:${String(provider.descriptor.id)}`);
|
|
75
|
+
if (indexed.has(provider.descriptor.id)) {
|
|
76
|
+
throw new ProtocolError("conflict", `duplicate implementation id: ${provider.descriptor.id}`);
|
|
77
|
+
}
|
|
78
|
+
indexed.set(provider.descriptor.id, provider);
|
|
79
|
+
}
|
|
80
|
+
this.#providers = indexed;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve one provider by opaque implementation id.
|
|
84
|
+
* @param id - Validated implementation identifier to resolve.
|
|
85
|
+
* @returns - Registered provider for the identifier.
|
|
86
|
+
*/
|
|
87
|
+
resolve(id) {
|
|
88
|
+
const provider = this.#providers.get(id);
|
|
89
|
+
if (!provider)
|
|
90
|
+
throw new ProtocolError("validation_failed", `unknown implementation id: ${id}`);
|
|
91
|
+
return provider;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* List validated descriptors without exposing concrete provider types.
|
|
95
|
+
* @returns - Validated descriptors in registry insertion order.
|
|
96
|
+
*/
|
|
97
|
+
descriptors() {
|
|
98
|
+
return [...this.#providers.values()].map((provider) => provider.descriptor);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Discover implementation manifests beneath a configured root by convention.
|
|
103
|
+
* @param root - Configured directory containing implementation subdirectories.
|
|
104
|
+
* @param port - Injected discovery and module-loading boundary.
|
|
105
|
+
* @returns - Registry containing every validated discovered provider.
|
|
106
|
+
*/
|
|
107
|
+
export async function discoverImplementationRegistry(root, port) {
|
|
108
|
+
const directories = [...(await port.listDirectories(root))].sort();
|
|
109
|
+
const providers = [];
|
|
110
|
+
for (const directory of directories) {
|
|
111
|
+
implementationId(directory);
|
|
112
|
+
const manifestPath = join(root, directory, "manifest.js");
|
|
113
|
+
const provider = await port.loadProvider(manifestPath);
|
|
114
|
+
assertImplementationProvider(provider, manifestPath);
|
|
115
|
+
if (provider.descriptor.id !== directory) {
|
|
116
|
+
invalidManifest(manifestPath, "descriptor id must match its implementation directory");
|
|
117
|
+
}
|
|
118
|
+
providers.push(provider);
|
|
119
|
+
}
|
|
120
|
+
return new ImplementationRegistry(providers);
|
|
121
|
+
}
|
|
122
|
+
function notificationSupportState(value, manifestPath) {
|
|
123
|
+
if (!isRecord(value) || typeof value.supported !== "boolean") {
|
|
124
|
+
invalidManifest(manifestPath, "has an invalid notification capability");
|
|
125
|
+
}
|
|
126
|
+
if (!value.supported)
|
|
127
|
+
return false;
|
|
128
|
+
if (value.mode !== "host-push")
|
|
129
|
+
invalidManifest(manifestPath, "has an invalid notification mode");
|
|
130
|
+
if (typeof value.parseAddress !== "function" || typeof value.deliver !== "function") {
|
|
131
|
+
invalidManifest(manifestPath, "has no parseAddress and deliver operations for notification mode host-push");
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
function capabilitySupportState(value, operation, manifestPath, capability) {
|
|
136
|
+
if (!isRecord(value) || typeof value.supported !== "boolean") {
|
|
137
|
+
invalidManifest(manifestPath, `has an invalid ${capability} capability`);
|
|
138
|
+
}
|
|
139
|
+
if (value.supported && typeof value[operation] !== "function") {
|
|
140
|
+
invalidManifest(manifestPath, `has no ${operation} operation for capability ${capability}`);
|
|
141
|
+
}
|
|
142
|
+
if (!value.supported && operation in value) {
|
|
143
|
+
invalidManifest(manifestPath, `exposes ${operation} while capability ${capability} is unsupported`);
|
|
144
|
+
}
|
|
145
|
+
return value.supported;
|
|
146
|
+
}
|
|
147
|
+
function isCapabilityName(value) {
|
|
148
|
+
return typeof value === "string" && capabilityNames.includes(value);
|
|
149
|
+
}
|
|
150
|
+
function isKnownVersion(value) {
|
|
151
|
+
if (!isRecord(value) || typeof value.known !== "boolean")
|
|
152
|
+
return false;
|
|
153
|
+
return value.known ? typeof value.value === "string" && value.value.length > 0 : !("value" in value);
|
|
154
|
+
}
|
|
155
|
+
function invalidManifest(manifestPath, reason) {
|
|
156
|
+
throw new ProtocolError("validation_failed", `invalid implementation manifest ${manifestPath}: ${reason}`);
|
|
157
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qualify a harness-supplied conversation identifier so keys cannot collide between implementations.
|
|
3
|
+
*
|
|
4
|
+
* The broker accepts a key as an opaque string, so an unqualified identifier from one harness could
|
|
5
|
+
* name another harness's conversation. A value the harness did not supply, or one the broker would
|
|
6
|
+
* refuse once qualified, yields no key: registering anew is correct, adopting a guess is not. The
|
|
7
|
+
* qualified form is what is checked, so the bound this applies is the broker's own rather than a
|
|
8
|
+
* second, tighter one that would refuse keys the broker accepts.
|
|
9
|
+
* @param implementationId - Validated implementation identity that owns the conversation.
|
|
10
|
+
* @param hostSessionId - Conversation identifier reported by the harness, when it reported one.
|
|
11
|
+
* @returns - Qualified session key, or nothing when no usable identifier was supplied.
|
|
12
|
+
*/
|
|
13
|
+
export declare function harnessSessionKey(implementationId: string, hostSessionId: string | undefined): string | undefined;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isSessionKey } from "../broker/protocol.js";
|
|
2
|
+
/**
|
|
3
|
+
* Qualify a harness-supplied conversation identifier so keys cannot collide between implementations.
|
|
4
|
+
*
|
|
5
|
+
* The broker accepts a key as an opaque string, so an unqualified identifier from one harness could
|
|
6
|
+
* name another harness's conversation. A value the harness did not supply, or one the broker would
|
|
7
|
+
* refuse once qualified, yields no key: registering anew is correct, adopting a guess is not. The
|
|
8
|
+
* qualified form is what is checked, so the bound this applies is the broker's own rather than a
|
|
9
|
+
* second, tighter one that would refuse keys the broker accepts.
|
|
10
|
+
* @param implementationId - Validated implementation identity that owns the conversation.
|
|
11
|
+
* @param hostSessionId - Conversation identifier reported by the harness, when it reported one.
|
|
12
|
+
* @returns - Qualified session key, or nothing when no usable identifier was supplied.
|
|
13
|
+
*/
|
|
14
|
+
export function harnessSessionKey(implementationId, hostSessionId) {
|
|
15
|
+
if (!hostSessionId)
|
|
16
|
+
return undefined;
|
|
17
|
+
const key = `${implementationId}:${hostSessionId}`;
|
|
18
|
+
return isSessionKey(key) ? key : undefined;
|
|
19
|
+
}
|