@zieglar/pi-chat-exchange 0.0.0-stage → 0.2.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/CHANGELOG.md +70 -0
- package/LICENSE +21 -0
- package/README.md +89 -2
- package/dist/bin/mcp-stdio.js +196 -0
- package/dist/control/debug-log.js +67 -0
- package/dist/control/mcp/debug-transport.js +48 -0
- package/dist/control/mcp/messaging-tools.js +278 -0
- package/dist/control/mcp/ping.js +43 -0
- package/dist/control/mcp/server.js +153 -0
- package/dist/core/authorization.js +32 -0
- package/dist/core/reference.js +32 -0
- package/dist/core/rules.js +468 -0
- package/dist/endpoints/pi/contract.js +2 -0
- package/dist/endpoints/pi/session-host.js +39 -0
- package/dist/host/bound-endpoint.js +176 -0
- package/dist/host/endpoint-host.js +839 -0
- package/dist/host/mcp-binding-config.js +209 -0
- package/dist/host/ownership-recovery.js +257 -0
- package/dist/index.js +11 -0
- package/dist/protocol/capabilities.js +46 -0
- package/dist/protocol/endpoint-capabilities.js +15 -0
- package/dist/protocol/entities.js +185 -0
- package/dist/protocol/formats.js +13 -0
- package/dist/protocol/idempotency.js +93 -0
- package/dist/protocol/identity.js +130 -0
- package/dist/protocol/index.js +8 -0
- package/dist/protocol/lifecycle.js +125 -0
- package/dist/protocol/outcomes.js +55 -0
- package/dist/runtime/desktop-manifest.js +65 -0
- package/dist/runtime/diagnostics.js +34 -0
- package/dist/runtime/executables.js +222 -0
- package/dist/storage/capability.js +221 -0
- package/dist/storage/connection.js +94 -0
- package/dist/storage/creation-request.js +176 -0
- package/dist/storage/creation.js +553 -0
- package/dist/storage/current-schema.js +397 -0
- package/dist/storage/delivery-ack.js +177 -0
- package/dist/storage/errors.js +9 -0
- package/dist/storage/filesystem.js +116 -0
- package/dist/storage/inbox-discovery.js +135 -0
- package/dist/storage/messaging.js +156 -0
- package/dist/storage/migration.js +258 -0
- package/dist/storage/ownership.js +789 -0
- package/dist/storage/participant-identity.js +127 -0
- package/dist/storage/participant.js +700 -0
- package/dist/storage/recovery.js +99 -0
- package/dist/storage/runtime.js +113 -0
- package/dist/storage/schema.js +556 -0
- package/dist/storage/session-state.js +75 -0
- package/dist/storage/store.js +450 -0
- package/dist/storage/transaction.js +199 -0
- package/dist/storage/trust-root.js +164 -0
- package/dist/tunnel/background-manager.js +1727 -0
- package/dist/tunnel/codes.js +79 -0
- package/dist/tunnel/health.js +75 -0
- package/dist/tunnel/keychain.js +526 -0
- package/dist/tunnel/managed-binding.js +454 -0
- package/dist/tunnel/manager-panel.js +249 -0
- package/dist/tunnel/manager-ui.js +341 -0
- package/dist/tunnel/manager.js +372 -0
- package/dist/tunnel/messaging-activation-ui.js +85 -0
- package/dist/tunnel/messaging-config.js +246 -0
- package/dist/tunnel/multi-session-admission.js +31 -0
- package/dist/tunnel/multi-session-policy.js +314 -0
- package/dist/tunnel/offline-client.js +104 -0
- package/dist/tunnel/profile.js +319 -0
- package/dist/tunnel/redact.js +9 -0
- package/dist/tunnel/secure-input.js +318 -0
- package/dist/tunnel/session-process-mutex.js +338 -0
- package/dist/tunnel/setup-ui.js +443 -0
- package/dist/tunnel/setup.js +734 -0
- package/dist/tunnel/state.js +127 -0
- package/docs/architecture.md +170 -0
- package/docs/chatgpt-installation.md +149 -0
- package/docs/delivery-ack-contract.md +86 -0
- package/docs/independent-install.md +156 -0
- package/docs/multi-session-usage.md +77 -0
- package/docs/ownership-contract.md +429 -0
- package/docs/protocol-v2.md +269 -0
- package/docs/releases/0.2.0.md +59 -0
- package/docs/storage-contract.md +1241 -0
- package/docs/storage-security.md +138 -0
- package/docs/tunnel-manager-background.md +169 -0
- package/docs/tunnel-setup.md +214 -0
- package/native/tunnel-keychain.swift +432 -0
- package/package.json +77 -4
- package/pi-extension/README.md +125 -0
- package/pi-extension/allowlist.example.json +11 -0
- package/pi-extension/index.ts +2312 -0
- package/pi-extension/multi-session-commands.ts +93 -0
- package/pi-extension/multi-session-runtime.ts +75 -0
- package/pi-extension/tui-keys.ts +40 -0
- package/scripts/reconcile-session-process-mutex.mjs +262 -0
- package/scripts/recover-stale-ownership.mjs +91 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HANDOFF-853274 — one host-owned Endpoint bound for a single-instance
|
|
3
|
+
* control surface (the local MCP server).
|
|
4
|
+
*
|
|
5
|
+
* This is the object injected into the MCP server as its messaging port. It
|
|
6
|
+
* carries NO endpoint parameter on any call: the Endpoint is fixed at
|
|
7
|
+
* construction from the trusted launch configuration. All authority still
|
|
8
|
+
* flows through `CeEndpointHost` (and from there through Storage); this class
|
|
9
|
+
* only adds admission gating and the opt-in lease lifecycle:
|
|
10
|
+
*
|
|
11
|
+
* - start: `beginSession({ requireUnowned: true })` — an existing ownership row
|
|
12
|
+
* (live or expired, e.g. after a crash) refuses before any durable write.
|
|
13
|
+
* This runtime never adopts or takes over; the separate operator-only recovery
|
|
14
|
+
* caller must clear one exact expired owner before a normal restart.
|
|
15
|
+
* - heartbeat: `host.renew` on a fixed interval. BUSY rolled back and is
|
|
16
|
+
* retried only at the next tick; UNCERTAIN or any authority refusal stops
|
|
17
|
+
* the heartbeat permanently and blocks new sends (no blind retry).
|
|
18
|
+
* - shutdown: stop admitting calls, wait for in-flight calls, stop the
|
|
19
|
+
* heartbeat, then `host.end` (release, then detach) exactly once. An unknown
|
|
20
|
+
* outcome is reported, never retried.
|
|
21
|
+
*/
|
|
22
|
+
import { CeHostRefusal, } from "./endpoint-host.js";
|
|
23
|
+
const defaultTimers = {
|
|
24
|
+
setInterval: (callback, ms) => {
|
|
25
|
+
const handle = setInterval(callback, ms);
|
|
26
|
+
// A heartbeat alone must never keep the process alive.
|
|
27
|
+
handle.unref();
|
|
28
|
+
return handle;
|
|
29
|
+
},
|
|
30
|
+
clearInterval: (handle) => clearInterval(handle),
|
|
31
|
+
};
|
|
32
|
+
export class CeBoundEndpoint {
|
|
33
|
+
#admission;
|
|
34
|
+
#host;
|
|
35
|
+
#endpointId;
|
|
36
|
+
#timers;
|
|
37
|
+
#onEvent;
|
|
38
|
+
#state = "ACTIVE";
|
|
39
|
+
#blockedReason;
|
|
40
|
+
#heartbeat;
|
|
41
|
+
#inFlight = 0;
|
|
42
|
+
#shutdown;
|
|
43
|
+
constructor(host, endpointId, options) {
|
|
44
|
+
this.#admission = options.admission;
|
|
45
|
+
this.#host = host;
|
|
46
|
+
this.#endpointId = endpointId;
|
|
47
|
+
this.#timers = options.timers ?? defaultTimers;
|
|
48
|
+
this.#onEvent = options.onEvent;
|
|
49
|
+
}
|
|
50
|
+
/** Bind the fixed Endpoint (refusing any existing ownership before writes)
|
|
51
|
+
* and start the renewal heartbeat. Throws the sanitized host refusal. */
|
|
52
|
+
static start(host, endpointId, options) {
|
|
53
|
+
if (!Number.isSafeInteger(options.renewIntervalMs) || options.renewIntervalMs < 1) {
|
|
54
|
+
throw new CeHostRefusal("CE_HOST_INVALID_REQUEST");
|
|
55
|
+
}
|
|
56
|
+
options.admission?.({ kind: "begin" });
|
|
57
|
+
host.beginSession(endpointId, { requireUnowned: true });
|
|
58
|
+
const bound = new CeBoundEndpoint(host, endpointId, options);
|
|
59
|
+
bound.#heartbeat = bound.#timers.setInterval(() => bound.#renew(), options.renewIntervalMs);
|
|
60
|
+
return bound;
|
|
61
|
+
}
|
|
62
|
+
get state() {
|
|
63
|
+
return this.#state;
|
|
64
|
+
}
|
|
65
|
+
#emit(event) {
|
|
66
|
+
try {
|
|
67
|
+
this.#onEvent?.(event);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Observation must never affect authority or lifecycle.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
#stopHeartbeat() {
|
|
74
|
+
if (this.#heartbeat !== undefined) {
|
|
75
|
+
this.#timers.clearInterval(this.#heartbeat);
|
|
76
|
+
this.#heartbeat = undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
#renew() {
|
|
80
|
+
if (this.#state !== "ACTIVE")
|
|
81
|
+
return;
|
|
82
|
+
try {
|
|
83
|
+
this.#admission?.({ kind: "renew" });
|
|
84
|
+
this.#host.renew(this.#endpointId);
|
|
85
|
+
this.#emit({ kind: "renewed" });
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
const refusal = error instanceof CeHostRefusal ? error : new CeHostRefusal("CE_HOST_UNAVAILABLE");
|
|
89
|
+
if (refusal.code === "CE_HOST_BUSY") {
|
|
90
|
+
// Rolled back as a whole: the next tick may try again before expiry.
|
|
91
|
+
this.#emit({ kind: "renewal_deferred", code: refusal.code });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// UNCERTAIN, authority loss, or any other refusal: never retry blindly.
|
|
95
|
+
this.#stopHeartbeat();
|
|
96
|
+
this.#state = "WRITE_BLOCKED";
|
|
97
|
+
this.#blockedReason = refusal.code;
|
|
98
|
+
this.#emit({ kind: "renewal_stopped", code: refusal.code, reason: refusal.reason });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
#admit(write) {
|
|
102
|
+
if (this.#state === "CLOSING" || this.#state === "CLOSED") {
|
|
103
|
+
throw new CeHostRefusal("CE_HOST_UNAVAILABLE", false, "ENDPOINT_CLOSING");
|
|
104
|
+
}
|
|
105
|
+
if (write && this.#state === "WRITE_BLOCKED") {
|
|
106
|
+
throw new CeHostRefusal(this.#blockedReason === "CE_HOST_UNCERTAIN" ? "CE_HOST_UNCERTAIN" : "CE_HOST_AUTHORITY_ABSENT", false, "LEASE_RENEWAL_STOPPED");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
#run(write, request, operation) {
|
|
110
|
+
this.#admit(write);
|
|
111
|
+
this.#admission?.(request);
|
|
112
|
+
this.#inFlight += 1;
|
|
113
|
+
try {
|
|
114
|
+
return operation();
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
this.#inFlight -= 1;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
send(input) {
|
|
121
|
+
return this.#run(true, { kind: "send", to: input.to }, () => this.#host.send(this.#endpointId, { to: input.to, text: input.text, idempotencyKey: input.idempotencyKey }));
|
|
122
|
+
}
|
|
123
|
+
listInbox(input) {
|
|
124
|
+
const request = {};
|
|
125
|
+
if (input.afterAuditSequence !== undefined)
|
|
126
|
+
request.afterAuditSequence = input.afterAuditSequence;
|
|
127
|
+
if (input.pageSize !== undefined)
|
|
128
|
+
request.pageSize = input.pageSize;
|
|
129
|
+
if (input.rawScanLimit !== undefined)
|
|
130
|
+
request.rawScanLimit = input.rawScanLimit;
|
|
131
|
+
return this.#run(false, { kind: "inbox" }, () => this.#host.listInbox(this.#endpointId, request));
|
|
132
|
+
}
|
|
133
|
+
readPayload(input) {
|
|
134
|
+
return this.#run(false, { kind: "readPayload" }, () => this.#host.readPayload(this.#endpointId, { messageId: input.messageId }));
|
|
135
|
+
}
|
|
136
|
+
/** HANDOFF-CE-E2B-01 delivery ACK. It is a durable WRITE, so it runs through
|
|
137
|
+
* the same admission gate as `send`: a permanently stopped heartbeat or a
|
|
138
|
+
* closing endpoint refuses it before the host is reached. The four metadata
|
|
139
|
+
* fields are copied by the caller from an authorized read; no endpoint,
|
|
140
|
+
* session, or capability parameter exists on this call. */
|
|
141
|
+
acknowledge(input) {
|
|
142
|
+
return this.#run(true, { kind: "ack" }, () => this.#host.acknowledge(this.#endpointId, {
|
|
143
|
+
messageId: input.messageId,
|
|
144
|
+
payloadDigest: input.payloadDigest,
|
|
145
|
+
payloadLength: input.payloadLength,
|
|
146
|
+
messageCreatedAuditSequence: input.messageCreatedAuditSequence,
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
/** Graceful, idempotent shutdown. Calls after the first return the first
|
|
150
|
+
* outcome; the release is attempted at most once per process. */
|
|
151
|
+
shutdown(drainTimeoutMs = 5_000) {
|
|
152
|
+
if (this.#shutdown !== undefined)
|
|
153
|
+
return this.#shutdown;
|
|
154
|
+
this.#state = "CLOSING";
|
|
155
|
+
this.#shutdown = (async () => {
|
|
156
|
+
const deadline = Date.now() + drainTimeoutMs;
|
|
157
|
+
while (this.#inFlight > 0 && Date.now() < deadline) {
|
|
158
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
159
|
+
}
|
|
160
|
+
this.#stopHeartbeat();
|
|
161
|
+
try {
|
|
162
|
+
const ended = this.#host.end(this.#endpointId);
|
|
163
|
+
return Object.freeze({ released: ended.released, detached: ended.detached, code: undefined, reason: undefined });
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
const refusal = error instanceof CeHostRefusal ? error : new CeHostRefusal("CE_HOST_UNAVAILABLE");
|
|
167
|
+
return Object.freeze({ released: false, detached: false, code: refusal.code, reason: refusal.reason });
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
this.#state = "CLOSED";
|
|
171
|
+
}
|
|
172
|
+
})();
|
|
173
|
+
return this.#shutdown;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=bound-endpoint.js.map
|