@pasko70/pibo 2.5.2 → 3.0.1
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/context/pibo-native-tooling.md +1 -0
- package/dist/agent-runtime/contract.js +2 -1
- package/dist/agent-runtime/history.js +53 -1
- package/dist/agent-runtime/routed-session.js +100 -26
- package/dist/agent-runtimes/codex-native/adapter.js +304 -30
- package/dist/agent-runtimes/codex-native/client.js +22 -3
- package/dist/agent-runtimes/codex-native/first-use.js +294 -0
- package/dist/agent-runtimes/codex-native/history.js +26 -7
- package/dist/agent-runtimes/codex-native/process.js +14 -0
- package/dist/agent-runtimes/codex-native/thread.js +59 -25
- package/dist/agent-runtimes/codex-native/turn.js +27 -0
- package/dist/agent-runtimes/history-proof.js +8 -0
- package/dist/agent-runtimes/omp/adapter.js +61 -14
- package/dist/agent-runtimes/omp/history.js +189 -34
- package/dist/agent-runtimes/omp/thread.js +82 -36
- package/dist/agent-runtimes/pi/adapter.js +30 -3
- package/dist/agent-runtimes/pi/history.js +66 -5
- package/dist/agent-runtimes/pi/openai-response-observer.js +102 -0
- package/dist/agent-runtimes/pi/routed-session.js +344 -37
- package/dist/apps/chat/chat-api-routes.js +1 -1
- package/dist/apps/chat/chat-files.js +130 -9
- package/dist/apps/chat/chat-settings-routes.js +27 -2
- package/dist/apps/chat/chat-speech.js +100 -0
- package/dist/apps/chat/data/chat-data-mappers.js +25 -5
- package/dist/apps/chat/data/timeline-query-service.js +89 -1
- package/dist/apps/chat/trace-v2.js +210 -8
- package/dist/apps/chat/trace.js +3 -0
- package/dist/apps/chat/web-app.js +108 -3
- package/dist/apps/chat-ui/assets/{dist-DTWJf37A.js → dist-BBDMeU5-.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CkYWKnvy.js → dist-BDIATCQt.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DZ8Wpj-H.js → dist-JnW4v8UE.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Dmblz15N.js → dist-lRiLP9rr.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DjcwoyNY.js → dist-mqCviI-c.js} +1 -1
- package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +228 -0
- package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +1 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +43 -0
- package/dist/apps/chat-vscode-web/assets/{index-CF1-9PKU.css → index-SluZr_-r.css} +1 -1
- package/dist/apps/chat-vscode-web/index.html +2 -2
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.1.vsix +0 -0
- package/dist/cli.js +49 -5
- package/dist/config/config.js +59 -8
- package/dist/core/preview-server-settings.js +20 -0
- package/dist/core/session-router.js +464 -37
- package/dist/core/user-settings.js +10 -0
- package/dist/data/payload-store.js +57 -1
- package/dist/data/schema.js +18 -1
- package/dist/debug/trace.js +5 -0
- package/dist/gateway/client.js +368 -65
- package/dist/gateway/protocol.js +2 -0
- package/dist/gateway/server.js +8 -0
- package/dist/gateway/web.js +2 -0
- package/dist/index.js +1 -1
- package/dist/loops/store.js +6 -4
- package/dist/mcp/config.js +89 -17
- package/dist/mcp/daemon-client.js +473 -153
- package/dist/mcp/daemon.js +401 -37
- package/dist/plugins/builtin.js +2 -2
- package/dist/plugins/codex-native.js +2 -0
- package/dist/plugins/registry.js +259 -1
- package/dist/previews/cli.js +345 -0
- package/dist/previews/config.js +61 -0
- package/dist/previews/manager.js +644 -0
- package/dist/previews/network.js +198 -0
- package/dist/previews/plugin.js +11 -0
- package/dist/previews/process-supervisor.js +38 -0
- package/dist/previews/proxy.js +387 -0
- package/dist/previews/store.js +750 -0
- package/dist/previews/types.js +1 -0
- package/dist/previews/web-app.js +446 -0
- package/dist/runs/registry.js +22 -0
- package/dist/runs/tools.js +46 -7
- package/dist/session-ui/terminalRows.js +187 -13
- package/dist/sessions/pibo-data-store.js +37 -0
- package/dist/sessions/runtime-binding-persistence.js +34 -0
- package/dist/sessions/runtime-binding.js +6 -0
- package/dist/sessions/sqlite-store.js +37 -0
- package/dist/shared/trace-engine.js +12 -64
- package/dist/shared/trace-event-projection.js +16 -0
- package/dist/shared/trace-history.js +684 -184
- package/dist/shared/trace-limits.js +7 -0
- package/dist/shared/trace-live-patch.js +61 -0
- package/dist/shared/trace-tool-identity.js +43 -0
- package/dist/speech/openai-codex-realtime-call-proxy.js +248 -0
- package/dist/speech/openai-codex.js +407 -0
- package/dist/speech/types.js +8 -0
- package/dist/subagents/context.js +5 -3
- package/dist/subagents/observations.js +94 -6
- package/dist/subagents/tool.js +103 -20
- package/dist/tools/codex-image-artifacts.js +24 -0
- package/dist/tools/codex-image-generation.js +4 -22
- package/dist/web/channel.js +88 -4
- package/dist/web/http.js +2 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +0 -1
- package/dist/apps/chat-ui/assets/index-DsgTm1Aw.js +0 -228
- package/dist/apps/chat-vscode-web/assets/index-DvTSSvzN.js +0 -43
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.5.2.vsix +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { loadPiboConfig } from "../config/config.js";
|
|
2
|
+
import { PiboWebHttpError, responseHtml, responseJson } from "../web/http.js";
|
|
3
|
+
import { DEFAULT_PREVIEW_SESSION_TTL_MINUTES, DEFAULT_PREVIEW_TICKET_TTL_SECONDS, previewIdFromHostname, previewPublicURL, requirePreviewBaseURL, } from "./config.js";
|
|
4
|
+
import { createDefaultPreviewProcessController, reconcileManagedPreviews, startManagedPreview, stopManagedPreview, } from "./manager.js";
|
|
5
|
+
import { isPreviewTargetProcessCurrent, probePreviewTarget } from "./network.js";
|
|
6
|
+
import { cookieValue, DEFAULT_MAX_PREVIEW_PROXY_CONNECTIONS, DEFAULT_MAX_PREVIEW_PROXY_CONNECTIONS_PER_PREVIEW, PreviewProxyLimiter, previewSessionCookieName, proxyPreviewHttp, proxyPreviewWebSocket, } from "./proxy.js";
|
|
7
|
+
import { PreviewCapacityError, PreviewStore, createDefaultPreviewStore, previewExposureState } from "./store.js";
|
|
8
|
+
export const PREVIEW_WEB_APP_NAME = "pibo.session-live-previews";
|
|
9
|
+
export const PREVIEW_WEB_MOUNT_PATH = "/apps/previews";
|
|
10
|
+
export const PREVIEW_WEB_API_PREFIX = "/api/previews";
|
|
11
|
+
export const PREVIEW_SESSION_EXCHANGE_PATH = "/__pibo/session";
|
|
12
|
+
function escapeHtml(value) {
|
|
13
|
+
return value
|
|
14
|
+
.replaceAll("&", "&")
|
|
15
|
+
.replaceAll('"', """)
|
|
16
|
+
.replaceAll("<", "<")
|
|
17
|
+
.replaceAll(">", ">");
|
|
18
|
+
}
|
|
19
|
+
function sameOriginMutation(request) {
|
|
20
|
+
const origin = request.headers.get("origin");
|
|
21
|
+
if (!origin || origin !== new URL(request.url).origin) {
|
|
22
|
+
throw new PiboWebHttpError("Origin is not allowed", 403);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function withStore(path, action) {
|
|
26
|
+
const store = path ? new PreviewStore(path) : createDefaultPreviewStore();
|
|
27
|
+
try {
|
|
28
|
+
return action(store);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
store.close();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function withStoreAsync(path, action) {
|
|
35
|
+
const store = path ? new PreviewStore(path) : createDefaultPreviewStore();
|
|
36
|
+
try {
|
|
37
|
+
return await action(store);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
store.close();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function exposureHealth(exposure) {
|
|
44
|
+
const state = previewExposureState(exposure);
|
|
45
|
+
if (state !== "active")
|
|
46
|
+
return state;
|
|
47
|
+
if (exposure.managementMode === "managed") {
|
|
48
|
+
if (exposure.serverState === "starting")
|
|
49
|
+
return "starting";
|
|
50
|
+
if (exposure.serverState === "stopping")
|
|
51
|
+
return "stopping";
|
|
52
|
+
if (exposure.serverState === "stopped")
|
|
53
|
+
return "stopped";
|
|
54
|
+
if (exposure.serverState === "error")
|
|
55
|
+
return "error";
|
|
56
|
+
}
|
|
57
|
+
const processCurrent = isPreviewTargetProcessCurrent(exposure);
|
|
58
|
+
const online = processCurrent ? Boolean(await probePreviewTarget(exposure.targetPort, { timeoutMs: 500 })) : false;
|
|
59
|
+
return online ? "online" : "offline";
|
|
60
|
+
}
|
|
61
|
+
async function publicExposure(exposure, baseURL) {
|
|
62
|
+
const { workspace: _workspace, startCommand: _startCommand, targetHost: _targetHost, targetPort: _targetPort, managementMode: _managementMode, targetProcessId: _targetProcessId, targetProcessStartTicks: _targetProcessStartTicks, serverError: _serverError, serverGeneration: _serverGeneration, managerKind: _managerKind, managerId: _managerId, managerPid: _managerPid, managerProcessStartTicks: _managerProcessStartTicks, ...publicFields } = exposure;
|
|
63
|
+
return {
|
|
64
|
+
...publicFields,
|
|
65
|
+
managed: exposure.managementMode === "managed",
|
|
66
|
+
state: previewExposureState(exposure),
|
|
67
|
+
health: await exposureHealth(exposure),
|
|
68
|
+
publicUrl: previewPublicURL(exposure.id, baseURL).toString(),
|
|
69
|
+
openUrl: `${PREVIEW_WEB_API_PREFIX}/${encodeURIComponent(exposure.id)}/open`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function readBody(request, maxBytes) {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const contentLength = Number(request.headers["content-length"]);
|
|
75
|
+
if (Number.isFinite(contentLength) && contentLength > maxBytes) {
|
|
76
|
+
reject(new Error("Preview session exchange body is too large"));
|
|
77
|
+
request.destroy();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const chunks = [];
|
|
81
|
+
let total = 0;
|
|
82
|
+
request.on("data", (chunk) => {
|
|
83
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
84
|
+
total += buffer.length;
|
|
85
|
+
if (total > maxBytes) {
|
|
86
|
+
reject(new Error("Preview session exchange body is too large"));
|
|
87
|
+
request.destroy();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
chunks.push(buffer);
|
|
91
|
+
});
|
|
92
|
+
request.once("end", () => resolve(Buffer.concat(chunks)));
|
|
93
|
+
request.once("error", reject);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function nodeJson(response, status, payload) {
|
|
97
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
98
|
+
response.end(JSON.stringify(payload));
|
|
99
|
+
}
|
|
100
|
+
function nodePreviewUnavailable(response, status = 503) {
|
|
101
|
+
response.writeHead(status, {
|
|
102
|
+
"content-type": "text/html; charset=utf-8",
|
|
103
|
+
"cache-control": "no-store",
|
|
104
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'",
|
|
105
|
+
});
|
|
106
|
+
response.end("<!doctype html><title>Preview server stopped</title><p>Start this Preview server from Chat Web and reload.</p>");
|
|
107
|
+
}
|
|
108
|
+
function unauthorizedPreview(response) {
|
|
109
|
+
response.writeHead(401, {
|
|
110
|
+
"content-type": "text/html; charset=utf-8",
|
|
111
|
+
"cache-control": "no-store",
|
|
112
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'",
|
|
113
|
+
});
|
|
114
|
+
response.end("<!doctype html><title>Preview authentication required</title><p>Open this preview from an authenticated Pibo session.</p>");
|
|
115
|
+
}
|
|
116
|
+
function previewSessionCookie(token, requestURL, expiresAt) {
|
|
117
|
+
const maxAge = Math.max(0, Math.floor((Date.parse(expiresAt) - Date.now()) / 1000));
|
|
118
|
+
const secure = requestURL.protocol === "https:";
|
|
119
|
+
return [
|
|
120
|
+
`${previewSessionCookieName(requestURL.protocol)}=${encodeURIComponent(token)}`,
|
|
121
|
+
"Path=/",
|
|
122
|
+
"HttpOnly",
|
|
123
|
+
secure ? "Secure" : undefined,
|
|
124
|
+
secure ? "SameSite=None" : "SameSite=Strict",
|
|
125
|
+
secure ? "Partitioned" : undefined,
|
|
126
|
+
`Max-Age=${maxAge}`,
|
|
127
|
+
].filter(Boolean).join("; ");
|
|
128
|
+
}
|
|
129
|
+
function previewOpenHtml(exposure, ticket, baseURL) {
|
|
130
|
+
const action = new URL(PREVIEW_SESSION_EXCHANGE_PATH, previewPublicURL(exposure.id, baseURL)).toString();
|
|
131
|
+
const html = `<!doctype html>
|
|
132
|
+
<meta charset="utf-8">
|
|
133
|
+
<meta name="referrer" content="no-referrer">
|
|
134
|
+
<title>Opening ${escapeHtml(exposure.label)}</title>
|
|
135
|
+
<form method="post" action="${escapeHtml(action)}">
|
|
136
|
+
<input type="hidden" name="ticket" value="${escapeHtml(ticket)}">
|
|
137
|
+
<noscript><button type="submit">Open preview</button></noscript>
|
|
138
|
+
</form>
|
|
139
|
+
<script>document.forms[0].submit()</script>`;
|
|
140
|
+
return responseHtml(html, {
|
|
141
|
+
headers: {
|
|
142
|
+
"cache-control": "no-store",
|
|
143
|
+
"content-security-policy": `default-src 'none'; form-action ${new URL(action).origin}; script-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'self'`,
|
|
144
|
+
"referrer-policy": "no-referrer",
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async function proxyablePreviewExposure(store, previewId, managerOptions) {
|
|
149
|
+
await reconcileManagedPreviews(store, managerOptions);
|
|
150
|
+
const exposure = store.getExposure(previewId);
|
|
151
|
+
if (!exposure || previewExposureState(exposure) !== "active")
|
|
152
|
+
return undefined;
|
|
153
|
+
if (exposure.managementMode === "managed" && exposure.serverState !== "running")
|
|
154
|
+
return undefined;
|
|
155
|
+
if (!isPreviewTargetProcessCurrent(exposure)) {
|
|
156
|
+
if (exposure.managementMode === "external")
|
|
157
|
+
store.closeExposure(previewId);
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
return await probePreviewTarget(exposure.targetPort, { timeoutMs: 500 }) ? exposure : undefined;
|
|
161
|
+
}
|
|
162
|
+
async function authorizedProxyablePreviewExposure(store, token, expectedExposure, managerOptions) {
|
|
163
|
+
await reconcileManagedPreviews(store, managerOptions);
|
|
164
|
+
const exposure = store.authorizedBrowserSessionExposure(token, expectedExposure.id, expectedExposure);
|
|
165
|
+
if (!exposure)
|
|
166
|
+
return { status: "unauthorized" };
|
|
167
|
+
if (!isPreviewTargetProcessCurrent(expectedExposure)) {
|
|
168
|
+
if (expectedExposure.managementMode === "external")
|
|
169
|
+
store.closeExposure(expectedExposure.id);
|
|
170
|
+
return { status: "unavailable" };
|
|
171
|
+
}
|
|
172
|
+
const online = Boolean(await probePreviewTarget(expectedExposure.targetPort, { timeoutMs: 500 }));
|
|
173
|
+
if (!store.authorizedBrowserSessionExposure(token, expectedExposure.id, expectedExposure)) {
|
|
174
|
+
return { status: "unauthorized" };
|
|
175
|
+
}
|
|
176
|
+
return online
|
|
177
|
+
? { status: "authorized", exposure: expectedExposure }
|
|
178
|
+
: { status: "unavailable" };
|
|
179
|
+
}
|
|
180
|
+
function lifecycleErrorResponse(error) {
|
|
181
|
+
const message = error instanceof PreviewCapacityError
|
|
182
|
+
? error.message
|
|
183
|
+
: "Preview server operation failed";
|
|
184
|
+
return responseJson({ error: message }, { status: 409 });
|
|
185
|
+
}
|
|
186
|
+
export function createPreviewWebApp(options = {}) {
|
|
187
|
+
const configured = loadPiboConfig();
|
|
188
|
+
const baseURL = options.baseURL ? requirePreviewBaseURL(options.baseURL) : configured.preview?.baseURL ? requirePreviewBaseURL(configured.preview.baseURL) : undefined;
|
|
189
|
+
const databasePath = options.databasePath ?? configured.preview?.databasePath;
|
|
190
|
+
const piboOrigin = (() => {
|
|
191
|
+
const value = options.piboBaseURL ?? configured.auth?.baseURL;
|
|
192
|
+
if (!value)
|
|
193
|
+
return undefined;
|
|
194
|
+
try {
|
|
195
|
+
return new URL(value).origin;
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
})();
|
|
201
|
+
const ticketTtlSeconds = options.ticketTtlSeconds ?? DEFAULT_PREVIEW_TICKET_TTL_SECONDS;
|
|
202
|
+
const browserSessionTtlMinutes = options.browserSessionTtlMinutes ?? DEFAULT_PREVIEW_SESSION_TTL_MINUTES;
|
|
203
|
+
if (!Number.isInteger(ticketTtlSeconds) || ticketTtlSeconds < 1 || ticketTtlSeconds > 300) {
|
|
204
|
+
throw new Error("Preview ticket lifetime must be between 1 and 300 seconds");
|
|
205
|
+
}
|
|
206
|
+
if (!Number.isInteger(browserSessionTtlMinutes) || browserSessionTtlMinutes < 1 || browserSessionTtlMinutes > 24 * 60) {
|
|
207
|
+
throw new Error("Preview browser session lifetime must be between 1 minute and 24 hours");
|
|
208
|
+
}
|
|
209
|
+
const maxProxyConnections = options.maxProxyConnections ??
|
|
210
|
+
configured.preview?.maxProxyConnections ??
|
|
211
|
+
DEFAULT_MAX_PREVIEW_PROXY_CONNECTIONS;
|
|
212
|
+
const proxyLimiter = new PreviewProxyLimiter(maxProxyConnections, options.maxProxyConnectionsPerPreview ??
|
|
213
|
+
configured.preview?.maxProxyConnectionsPerPreview ??
|
|
214
|
+
Math.min(DEFAULT_MAX_PREVIEW_PROXY_CONNECTIONS_PER_PREVIEW, maxProxyConnections));
|
|
215
|
+
const managerOptions = {
|
|
216
|
+
...options.managerOptions,
|
|
217
|
+
controller: options.managerOptions?.controller ?? createDefaultPreviewProcessController(),
|
|
218
|
+
};
|
|
219
|
+
let reaperPromise;
|
|
220
|
+
const runReaper = () => {
|
|
221
|
+
if (reaperPromise)
|
|
222
|
+
return;
|
|
223
|
+
reaperPromise = withStoreAsync(databasePath, async (store) => {
|
|
224
|
+
await reconcileManagedPreviews(store, managerOptions);
|
|
225
|
+
store.prune();
|
|
226
|
+
})
|
|
227
|
+
.catch((error) => console.error(`Preview server reconciliation failed: ${error instanceof Error ? error.message : String(error)}`))
|
|
228
|
+
.finally(() => { reaperPromise = undefined; });
|
|
229
|
+
};
|
|
230
|
+
const reaperIntervalMs = !baseURL || options.reaperIntervalMs === false
|
|
231
|
+
? false
|
|
232
|
+
: Math.max(1_000, options.reaperIntervalMs ?? 5_000);
|
|
233
|
+
const reaperTimer = reaperIntervalMs === false ? undefined : setInterval(runReaper, reaperIntervalMs);
|
|
234
|
+
reaperTimer?.unref();
|
|
235
|
+
if (reaperTimer)
|
|
236
|
+
runReaper();
|
|
237
|
+
return {
|
|
238
|
+
name: PREVIEW_WEB_APP_NAME,
|
|
239
|
+
mountPath: PREVIEW_WEB_MOUNT_PATH,
|
|
240
|
+
apiPrefix: PREVIEW_WEB_API_PREFIX,
|
|
241
|
+
async dispose() {
|
|
242
|
+
if (reaperTimer)
|
|
243
|
+
clearInterval(reaperTimer);
|
|
244
|
+
await reaperPromise;
|
|
245
|
+
},
|
|
246
|
+
matchesHost(hostname) {
|
|
247
|
+
return baseURL ? previewIdFromHostname(hostname, baseURL) !== undefined : false;
|
|
248
|
+
},
|
|
249
|
+
async handleRequest(request, context) {
|
|
250
|
+
const url = new URL(request.url);
|
|
251
|
+
if (!url.pathname.startsWith(PREVIEW_WEB_API_PREFIX))
|
|
252
|
+
return undefined;
|
|
253
|
+
await context.requireSession({ request });
|
|
254
|
+
if (url.pathname === PREVIEW_WEB_API_PREFIX && request.method === "GET") {
|
|
255
|
+
const piboSessionId = url.searchParams.get("piboSessionId")?.trim();
|
|
256
|
+
if (!piboSessionId)
|
|
257
|
+
return responseJson({ error: "piboSessionId is required" }, { status: 400 });
|
|
258
|
+
if (!baseURL)
|
|
259
|
+
return responseJson({ configured: false, previews: [] });
|
|
260
|
+
const exposures = await withStoreAsync(databasePath, async (store) => {
|
|
261
|
+
await reconcileManagedPreviews(store, managerOptions);
|
|
262
|
+
return store.listExposures({ piboSessionId });
|
|
263
|
+
});
|
|
264
|
+
return responseJson({ configured: true, previews: await Promise.all(exposures.map((exposure) => publicExposure(exposure, baseURL))) });
|
|
265
|
+
}
|
|
266
|
+
if (!baseURL)
|
|
267
|
+
return responseJson({ error: "Live previews are not configured. Set preview.baseURL." }, { status: 503 });
|
|
268
|
+
const openMatch = url.pathname.match(/^\/api\/previews\/([^/]+)\/open$/);
|
|
269
|
+
if (openMatch && request.method === "GET") {
|
|
270
|
+
const id = decodeURIComponent(openMatch[1]);
|
|
271
|
+
const result = await withStoreAsync(databasePath, async (store) => {
|
|
272
|
+
const exposure = await proxyablePreviewExposure(store, id, managerOptions);
|
|
273
|
+
if (!exposure)
|
|
274
|
+
return undefined;
|
|
275
|
+
return { exposure, ticket: store.createTicket(id, ticketTtlSeconds).token };
|
|
276
|
+
});
|
|
277
|
+
if (!result)
|
|
278
|
+
return responseJson({ error: "Preview server is not running" }, { status: 409 });
|
|
279
|
+
return previewOpenHtml(result.exposure, result.ticket, baseURL);
|
|
280
|
+
}
|
|
281
|
+
const lifecycleMatch = url.pathname.match(/^\/api\/previews\/([^/]+)\/(start|stop)$/);
|
|
282
|
+
if (lifecycleMatch && request.method === "POST") {
|
|
283
|
+
sameOriginMutation(request);
|
|
284
|
+
const id = decodeURIComponent(lifecycleMatch[1]);
|
|
285
|
+
try {
|
|
286
|
+
const exposure = await withStoreAsync(databasePath, (store) => lifecycleMatch[2] === "start"
|
|
287
|
+
? startManagedPreview(store, id, managerOptions)
|
|
288
|
+
: stopManagedPreview(store, id, managerOptions));
|
|
289
|
+
return responseJson({ preview: await publicExposure(exposure, baseURL) });
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
return lifecycleErrorResponse(error);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const previewMatch = url.pathname.match(/^\/api\/previews\/([^/]+)$/);
|
|
296
|
+
if (previewMatch && request.method === "GET") {
|
|
297
|
+
const id = decodeURIComponent(previewMatch[1]);
|
|
298
|
+
const exposure = await withStoreAsync(databasePath, async (store) => {
|
|
299
|
+
await reconcileManagedPreviews(store, managerOptions);
|
|
300
|
+
return store.getExposure(id);
|
|
301
|
+
});
|
|
302
|
+
if (!exposure)
|
|
303
|
+
return responseJson({ error: "Preview not found" }, { status: 404 });
|
|
304
|
+
return responseJson({ preview: await publicExposure(exposure, baseURL) });
|
|
305
|
+
}
|
|
306
|
+
if (previewMatch && request.method === "DELETE") {
|
|
307
|
+
sameOriginMutation(request);
|
|
308
|
+
const id = decodeURIComponent(previewMatch[1]);
|
|
309
|
+
try {
|
|
310
|
+
const exposure = await withStoreAsync(databasePath, async (store) => {
|
|
311
|
+
const current = store.getExposure(id);
|
|
312
|
+
if (!current)
|
|
313
|
+
return undefined;
|
|
314
|
+
if (current.managementMode === "managed" && ["running", "starting", "stopping", "error"].includes(current.serverState ?? "")) {
|
|
315
|
+
await stopManagedPreview(store, id, managerOptions);
|
|
316
|
+
}
|
|
317
|
+
return store.closeExposure(id);
|
|
318
|
+
});
|
|
319
|
+
if (!exposure)
|
|
320
|
+
return responseJson({ error: "Preview not found" }, { status: 404 });
|
|
321
|
+
return responseJson({ removed: true, preview: await publicExposure(exposure, baseURL) });
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
return lifecycleErrorResponse(error);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return responseJson({ error: "Not found" }, { status: 404 });
|
|
328
|
+
},
|
|
329
|
+
async handleNodeRequest(request, response, _context, requestURL) {
|
|
330
|
+
if (!baseURL) {
|
|
331
|
+
nodeJson(response, 503, { error: "Live previews are not configured" });
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const previewId = previewIdFromHostname(requestURL.hostname, baseURL);
|
|
335
|
+
if (!previewId) {
|
|
336
|
+
nodeJson(response, 404, { error: "Preview not found" });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (requestURL.pathname === PREVIEW_SESSION_EXCHANGE_PATH) {
|
|
340
|
+
if (request.method !== "POST") {
|
|
341
|
+
response.writeHead(405, { allow: "POST", "cache-control": "no-store" });
|
|
342
|
+
response.end();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
if (!request.headers["content-type"]?.toLowerCase().startsWith("application/x-www-form-urlencoded")) {
|
|
347
|
+
response.writeHead(415, { "cache-control": "no-store" });
|
|
348
|
+
response.end();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const body = await readBody(request, 8 * 1024);
|
|
352
|
+
const ticket = new URLSearchParams(body.toString("utf8")).get("ticket") ?? "";
|
|
353
|
+
const browserSession = withStore(databasePath, (store) => store.exchangeTicketForBrowserSession(ticket, previewId, browserSessionTtlMinutes));
|
|
354
|
+
if (!browserSession) {
|
|
355
|
+
unauthorizedPreview(response);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
response.writeHead(303, {
|
|
359
|
+
location: "/",
|
|
360
|
+
"cache-control": "no-store",
|
|
361
|
+
"set-cookie": previewSessionCookie(browserSession.token, requestURL, browserSession.expiresAt),
|
|
362
|
+
});
|
|
363
|
+
response.end();
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
nodeJson(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
367
|
+
}
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const cookie = cookieValue(request.headers.cookie, previewSessionCookieName(requestURL.protocol));
|
|
371
|
+
const access = await withStoreAsync(databasePath, async (store) => {
|
|
372
|
+
const exposure = store.authorizedBrowserSessionExposure(cookie, previewId);
|
|
373
|
+
if (!exposure)
|
|
374
|
+
return { status: "unauthorized" };
|
|
375
|
+
return authorizedProxyablePreviewExposure(store, cookie, exposure, managerOptions);
|
|
376
|
+
});
|
|
377
|
+
if (access.status === "unauthorized") {
|
|
378
|
+
unauthorizedPreview(response);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (access.status === "unavailable") {
|
|
382
|
+
nodePreviewUnavailable(response);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const release = proxyLimiter.tryAcquire(previewId);
|
|
386
|
+
if (!release) {
|
|
387
|
+
nodeJson(response, 503, { error: "Preview connection limit reached" });
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
const exposure = withStore(databasePath, (store) => store.authorizedBrowserSessionExposure(cookie, previewId, access.exposure));
|
|
392
|
+
if (!exposure) {
|
|
393
|
+
unauthorizedPreview(response);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
await proxyPreviewHttp({ request, response, requestURL, exposure, piboOrigin });
|
|
397
|
+
}
|
|
398
|
+
finally {
|
|
399
|
+
release();
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
async handleUpgrade(request, socket, head, _context, requestURL) {
|
|
403
|
+
if (!baseURL) {
|
|
404
|
+
socket.end("HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n");
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const previewId = previewIdFromHostname(requestURL.hostname, baseURL);
|
|
408
|
+
const upgrade = Array.isArray(request.headers.upgrade) ? request.headers.upgrade[0] : request.headers.upgrade;
|
|
409
|
+
if (upgrade?.toLowerCase() !== "websocket") {
|
|
410
|
+
socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const cookie = cookieValue(request.headers.cookie, previewSessionCookieName(requestURL.protocol));
|
|
414
|
+
const access = previewId ? await withStoreAsync(databasePath, async (store) => {
|
|
415
|
+
const exposure = store.authorizedBrowserSessionExposure(cookie, previewId);
|
|
416
|
+
if (!exposure)
|
|
417
|
+
return { status: "unauthorized" };
|
|
418
|
+
return authorizedProxyablePreviewExposure(store, cookie, exposure, managerOptions);
|
|
419
|
+
}) : undefined;
|
|
420
|
+
if (!access || access.status === "unauthorized") {
|
|
421
|
+
socket.end("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (access.status === "unavailable") {
|
|
425
|
+
socket.end("HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n");
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const release = proxyLimiter.tryAcquire(previewId);
|
|
429
|
+
if (!release) {
|
|
430
|
+
socket.end("HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n");
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
const exposure = withStore(databasePath, (store) => store.authorizedBrowserSessionExposure(cookie, previewId, access.exposure));
|
|
435
|
+
if (!exposure) {
|
|
436
|
+
socket.end("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
await proxyPreviewWebSocket({ request, socket, head, requestURL, exposure });
|
|
440
|
+
}
|
|
441
|
+
finally {
|
|
442
|
+
release();
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
}
|
package/dist/runs/registry.js
CHANGED
|
@@ -373,6 +373,28 @@ export class PiboRunRegistry {
|
|
|
373
373
|
}
|
|
374
374
|
return notification;
|
|
375
375
|
}
|
|
376
|
+
releaseNotification(controllerPiboSessionId, notification) {
|
|
377
|
+
const released = [];
|
|
378
|
+
const items = [
|
|
379
|
+
...notification.completed,
|
|
380
|
+
...notification.failed,
|
|
381
|
+
...notification.timedOut,
|
|
382
|
+
...notification.cancelled,
|
|
383
|
+
...notification.running,
|
|
384
|
+
];
|
|
385
|
+
for (const item of items) {
|
|
386
|
+
const record = this.runs.get(item.runId);
|
|
387
|
+
if (!record || record.controllerPiboSessionId !== controllerPiboSessionId)
|
|
388
|
+
continue;
|
|
389
|
+
if (record.status !== item.status || record.notifiedStatus !== item.status)
|
|
390
|
+
continue;
|
|
391
|
+
record.notifiedStatus = undefined;
|
|
392
|
+
record.updatedAt = now();
|
|
393
|
+
this.options.store?.updateRun(record.runId, record);
|
|
394
|
+
released.push(snapshot(record));
|
|
395
|
+
}
|
|
396
|
+
return released;
|
|
397
|
+
}
|
|
376
398
|
hasPendingNotification(controllerPiboSessionId, options = {}) {
|
|
377
399
|
return [...this.runs.values()].some((record) => this.needsNotification(record, controllerPiboSessionId, options));
|
|
378
400
|
}
|
package/dist/runs/tools.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
+
import { Value } from "typebox/value";
|
|
2
3
|
import { piboStringEnum } from "../tools/schema.js";
|
|
3
4
|
import { definePiboTool, piboToolTerminalStatus, piboToolTimeoutPhase } from "../tools/contract.js";
|
|
4
5
|
import { foregroundServiceWarning, hasMeaningfulTimeoutOutput, isConfiguredTimeoutError, PiboRunCancellationError, PiboRunCancelledError, PiboRunExecutionTimeoutError, resolveRunTimeoutMs, waitForRunCancellationSettlement } from "./lifecycle.js";
|
|
@@ -36,18 +37,55 @@ function requireTool(tools, name) {
|
|
|
36
37
|
}
|
|
37
38
|
return tool;
|
|
38
39
|
}
|
|
40
|
+
function yieldedToolInputSchema(tool) {
|
|
41
|
+
const compatibilityTool = tool;
|
|
42
|
+
return compatibilityTool.inputSchema
|
|
43
|
+
?? compatibilityTool.parameters
|
|
44
|
+
?? Type.Any({ description: `Legacy yieldable tool "${tool.name}" has no declared input schema` });
|
|
45
|
+
}
|
|
46
|
+
function yieldedToolArgumentsSchema(tools) {
|
|
47
|
+
if (tools.length === 0)
|
|
48
|
+
return Type.Never({ description: "No yieldable tools are available" });
|
|
49
|
+
if (tools.length === 1)
|
|
50
|
+
return yieldedToolInputSchema(tools[0]);
|
|
51
|
+
return Type.Union(tools.map(yieldedToolInputSchema), {
|
|
52
|
+
description: "Arguments matching the input schema of the selected yieldable tool",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function validateYieldedToolArguments(tool, input) {
|
|
56
|
+
const inputSchema = yieldedToolInputSchema(tool);
|
|
57
|
+
if (!Value.Check(inputSchema, input)) {
|
|
58
|
+
const errors = [...Value.Errors(inputSchema, input)].slice(0, 5).map((error) => {
|
|
59
|
+
const location = error.path
|
|
60
|
+
?? error.instancePath
|
|
61
|
+
?? "/";
|
|
62
|
+
return `${location || "/"}: ${error.message}`;
|
|
63
|
+
});
|
|
64
|
+
throw new Error(`Invalid arguments for yielded tool "${tool.name}": ${errors.join("; ")}`);
|
|
65
|
+
}
|
|
66
|
+
if (!tool.prepareInput)
|
|
67
|
+
return input;
|
|
68
|
+
try {
|
|
69
|
+
return tool.prepareInput(input);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
73
|
+
throw new Error(`Invalid arguments for yielded tool "${tool.name}": ${reason}`, { cause: error });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
39
76
|
export function createRunToolDefinitions(yieldableTools, controller) {
|
|
40
77
|
const toolNames = yieldableTools.map((tool) => tool.name);
|
|
78
|
+
const argumentsSchema = yieldedToolArgumentsSchema(yieldableTools);
|
|
41
79
|
return [
|
|
42
80
|
definePiboTool({
|
|
43
81
|
name: "pibo_run_start",
|
|
44
82
|
title: "Pibo Run Start",
|
|
45
|
-
description: "Start a yieldable tool as a yielded run. The run records its configured timeout and classifies lifetime expiry separately from command failure. Use detached only for intentional fire-and-forget work.",
|
|
46
|
-
promptSnippet: "Use pibo_run_start to run a yieldable tool in the background. It returns a runId. Use pibo_run_read for completed results and pibo_run_wait/status/list/cancel/ack to manage runs.",
|
|
83
|
+
description: "Start a yieldable tool as a yielded run. arguments must match the selected toolName schema; invalid target arguments fail before run admission or persistence. The run records its configured timeout and classifies lifetime expiry separately from command failure. Use detached only for intentional fire-and-forget work.",
|
|
84
|
+
promptSnippet: "Use pibo_run_start to run a yieldable tool in the background. Provide arguments matching the selected toolName schema; invalid arguments create no run. It returns a runId. Use pibo_run_read for completed results and pibo_run_wait/status/list/cancel/ack to manage runs.",
|
|
47
85
|
executionMode: "parallel",
|
|
48
86
|
inputSchema: Type.Object({
|
|
49
87
|
toolName: piboStringEnum(toolNames, { description: "Yieldable tool name to start" }),
|
|
50
|
-
arguments:
|
|
88
|
+
arguments: argumentsSchema,
|
|
51
89
|
completionPolicy: Type.Optional(piboStringEnum(["tracked", "detached"], {
|
|
52
90
|
description: "tracked reminds this agent about completion; detached is fire-and-forget and creates no automatic reminders.",
|
|
53
91
|
default: "tracked",
|
|
@@ -55,9 +93,10 @@ export function createRunToolDefinitions(yieldableTools, controller) {
|
|
|
55
93
|
}),
|
|
56
94
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
57
95
|
const tool = requireTool(yieldableTools, params.toolName);
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
const
|
|
96
|
+
const toolArguments = validateYieldedToolArguments(tool, params.arguments);
|
|
97
|
+
const timeoutMs = resolveRunTimeoutMs(tool.name, toolArguments);
|
|
98
|
+
const serviceWarning = foregroundServiceWarning(tool.name, toolArguments, timeoutMs);
|
|
99
|
+
const prepared = prepareYieldedRunExecution(tool.name, toolArguments);
|
|
61
100
|
const runAbortController = new AbortController();
|
|
62
101
|
const runSignal = signal ? AbortSignal.any([signal, runAbortController.signal]) : runAbortController.signal;
|
|
63
102
|
let executionStarted = false;
|
|
@@ -69,7 +108,7 @@ export function createRunToolDefinitions(yieldableTools, controller) {
|
|
|
69
108
|
let cancellationFailure;
|
|
70
109
|
const run = controller.startToolRun({
|
|
71
110
|
toolName: tool.name,
|
|
72
|
-
params:
|
|
111
|
+
params: toolArguments,
|
|
73
112
|
completionPolicy: params.completionPolicy,
|
|
74
113
|
timeoutMs,
|
|
75
114
|
serviceWarning,
|