@pasko70/pibo 2.5.2 → 3.0.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.
Files changed (100) hide show
  1. package/context/pibo-native-tooling.md +1 -0
  2. package/dist/agent-runtime/contract.js +2 -1
  3. package/dist/agent-runtime/history.js +53 -1
  4. package/dist/agent-runtime/routed-session.js +100 -26
  5. package/dist/agent-runtimes/codex-native/adapter.js +304 -30
  6. package/dist/agent-runtimes/codex-native/client.js +22 -3
  7. package/dist/agent-runtimes/codex-native/first-use.js +294 -0
  8. package/dist/agent-runtimes/codex-native/history.js +26 -7
  9. package/dist/agent-runtimes/codex-native/process.js +14 -0
  10. package/dist/agent-runtimes/codex-native/thread.js +59 -25
  11. package/dist/agent-runtimes/codex-native/turn.js +27 -0
  12. package/dist/agent-runtimes/history-proof.js +8 -0
  13. package/dist/agent-runtimes/omp/adapter.js +61 -14
  14. package/dist/agent-runtimes/omp/history.js +189 -34
  15. package/dist/agent-runtimes/omp/thread.js +82 -36
  16. package/dist/agent-runtimes/pi/adapter.js +30 -3
  17. package/dist/agent-runtimes/pi/history.js +66 -5
  18. package/dist/agent-runtimes/pi/openai-response-observer.js +102 -0
  19. package/dist/agent-runtimes/pi/routed-session.js +344 -37
  20. package/dist/apps/chat/chat-api-routes.js +1 -1
  21. package/dist/apps/chat/chat-files.js +130 -9
  22. package/dist/apps/chat/chat-settings-routes.js +27 -2
  23. package/dist/apps/chat/chat-speech.js +100 -0
  24. package/dist/apps/chat/data/chat-data-mappers.js +25 -5
  25. package/dist/apps/chat/data/timeline-query-service.js +89 -1
  26. package/dist/apps/chat/trace-v2.js +210 -8
  27. package/dist/apps/chat/trace.js +3 -0
  28. package/dist/apps/chat/web-app.js +108 -3
  29. package/dist/apps/chat-ui/assets/{dist-DTWJf37A.js → dist-BBDMeU5-.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{dist-CkYWKnvy.js → dist-BDIATCQt.js} +1 -1
  31. package/dist/apps/chat-ui/assets/{dist-DZ8Wpj-H.js → dist-JnW4v8UE.js} +1 -1
  32. package/dist/apps/chat-ui/assets/{dist-Dmblz15N.js → dist-lRiLP9rr.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-DjcwoyNY.js → dist-mqCviI-c.js} +1 -1
  34. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +228 -0
  35. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +1 -0
  36. package/dist/apps/chat-ui/index.html +2 -2
  37. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +43 -0
  38. package/dist/apps/chat-vscode-web/assets/{index-CF1-9PKU.css → index-SluZr_-r.css} +1 -1
  39. package/dist/apps/chat-vscode-web/index.html +2 -2
  40. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  41. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.0.vsix +0 -0
  42. package/dist/cli.js +49 -5
  43. package/dist/config/config.js +59 -8
  44. package/dist/core/preview-server-settings.js +20 -0
  45. package/dist/core/session-router.js +457 -37
  46. package/dist/core/user-settings.js +10 -0
  47. package/dist/data/payload-store.js +57 -1
  48. package/dist/data/schema.js +18 -1
  49. package/dist/debug/trace.js +5 -0
  50. package/dist/gateway/client.js +368 -65
  51. package/dist/gateway/protocol.js +2 -0
  52. package/dist/gateway/server.js +8 -0
  53. package/dist/gateway/web.js +2 -0
  54. package/dist/index.js +1 -1
  55. package/dist/loops/store.js +6 -4
  56. package/dist/mcp/config.js +89 -17
  57. package/dist/mcp/daemon-client.js +473 -153
  58. package/dist/mcp/daemon.js +401 -37
  59. package/dist/plugins/builtin.js +2 -2
  60. package/dist/plugins/codex-native.js +2 -0
  61. package/dist/plugins/registry.js +259 -1
  62. package/dist/previews/cli.js +345 -0
  63. package/dist/previews/config.js +61 -0
  64. package/dist/previews/manager.js +644 -0
  65. package/dist/previews/network.js +198 -0
  66. package/dist/previews/plugin.js +11 -0
  67. package/dist/previews/process-supervisor.js +38 -0
  68. package/dist/previews/proxy.js +387 -0
  69. package/dist/previews/store.js +750 -0
  70. package/dist/previews/types.js +1 -0
  71. package/dist/previews/web-app.js +446 -0
  72. package/dist/runs/registry.js +22 -0
  73. package/dist/runs/tools.js +46 -7
  74. package/dist/session-ui/terminalRows.js +187 -13
  75. package/dist/sessions/pibo-data-store.js +37 -0
  76. package/dist/sessions/runtime-binding-persistence.js +34 -0
  77. package/dist/sessions/runtime-binding.js +6 -0
  78. package/dist/sessions/sqlite-store.js +37 -0
  79. package/dist/shared/trace-engine.js +12 -64
  80. package/dist/shared/trace-event-projection.js +16 -0
  81. package/dist/shared/trace-history.js +684 -184
  82. package/dist/shared/trace-limits.js +7 -0
  83. package/dist/shared/trace-live-patch.js +61 -0
  84. package/dist/shared/trace-tool-identity.js +43 -0
  85. package/dist/speech/openai-codex-realtime-call-proxy.js +248 -0
  86. package/dist/speech/openai-codex.js +407 -0
  87. package/dist/speech/types.js +8 -0
  88. package/dist/subagents/context.js +5 -3
  89. package/dist/subagents/observations.js +94 -6
  90. package/dist/subagents/tool.js +103 -20
  91. package/dist/tools/codex-image-artifacts.js +24 -0
  92. package/dist/tools/codex-image-generation.js +4 -22
  93. package/dist/web/channel.js +88 -4
  94. package/dist/web/http.js +2 -1
  95. package/npm-shrinkwrap.json +2 -2
  96. package/package.json +1 -1
  97. package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +0 -1
  98. package/dist/apps/chat-ui/assets/index-DsgTm1Aw.js +0 -228
  99. package/dist/apps/chat-vscode-web/assets/index-DvTSSvzN.js +0 -43
  100. package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.5.2.vsix +0 -0
@@ -8,6 +8,7 @@ export const PIBO_AGENT_TOOL_NAMES = [
8
8
  "pibo_agents_observe",
9
9
  "pibo_agents_kill",
10
10
  ];
11
+ export const PIBO_AGENT_SESSION_NAME_MAX_LENGTH = 40;
11
12
  function availableAgentDescription(subagent) {
12
13
  return subagent.description?.trim() || `Targets profile ${subagent.targetProfile}.`;
13
14
  }
@@ -30,6 +31,58 @@ export function formatAvailableAgentsForPrompt(subagents) {
30
31
  function resultText(prefix, value) {
31
32
  return `${prefix}\n${JSON.stringify(value, null, 2)}`;
32
33
  }
34
+ export function normalizePiboAgentSessionName(value) {
35
+ if (typeof value !== "string")
36
+ throw new Error("Agent session name is required.");
37
+ if ([...value].length > PIBO_AGENT_SESSION_NAME_MAX_LENGTH) {
38
+ throw new Error(`Agent session name must be at most ${PIBO_AGENT_SESSION_NAME_MAX_LENGTH} characters.`);
39
+ }
40
+ const normalized = value.trim();
41
+ if (!normalized)
42
+ throw new Error("Agent session name must not be empty.");
43
+ return normalized;
44
+ }
45
+ function preparePiboAgentSessionNameInput(input) {
46
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
47
+ throw new Error("Agent session name is required.");
48
+ }
49
+ return {
50
+ ...input,
51
+ sessionName: normalizePiboAgentSessionName(input.sessionName),
52
+ };
53
+ }
54
+ function preparePiboAgentToolInput(input) {
55
+ return preparePiboAgentSessionNameInput(input);
56
+ }
57
+ function preparePiboDeprecatedSubagentToolInput(input) {
58
+ return preparePiboAgentSessionNameInput(input);
59
+ }
60
+ export function formatAgentObservationsForModel(result) {
61
+ const includeTools = result.filters.includeTools === true;
62
+ const toolDetail = result.filters.toolDetail ?? "summary";
63
+ const lines = [
64
+ `Agent observations (${result.observations.length}; tools=${includeTools ? toolDetail : "hidden"}; order=${result.filters.order ?? "desc"}; limit=${result.filters.limit ?? 20})`,
65
+ `nextAfterSequence=${result.nextAfterSequence}; truncated=${result.truncated}`,
66
+ ];
67
+ if (result.observations.length === 0) {
68
+ lines.push("", "No delegated-agent observations matched the filters.");
69
+ return lines.join("\n");
70
+ }
71
+ for (const observation of result.observations) {
72
+ const scope = [
73
+ observation.name,
74
+ observation.threadKey ? `thread=${observation.threadKey}` : undefined,
75
+ observation.requestId ? `request=${observation.requestId}` : undefined,
76
+ ].filter(Boolean).join("; ");
77
+ const event = observation.kind === "tool"
78
+ ? `${observation.eventType}${observation.toolName ? ` ${observation.toolName}` : ""}`
79
+ : observation.eventType;
80
+ lines.push("", `#${observation.sequence} ${scope} — ${event}${observation.isError ? " [error]" : ""}`);
81
+ if (observation.text)
82
+ lines.push(observation.text);
83
+ }
84
+ return lines.join("\n");
85
+ }
33
86
  function normalizeAgentSendMessageResult(result, fallbackRequestId) {
34
87
  return {
35
88
  ...result,
@@ -55,20 +108,27 @@ export function createAgentToolDefinitions(subagents, controller) {
55
108
  name: "pibo_agents_send_message",
56
109
  title: "Pibo Agents Send Message",
57
110
  description: [
58
- "Yielded-only delegated send. Start this tool through pibo_run_start; bounded waits do not limit the child lifetime.",
111
+ "Yielded-only delegated send with a required sessionName. It must be a nonblank string of at most 40 Unicode code points and is trimmed before use. name selects the configured agent, sessionName is the human-readable child-session title, and threadKey controls conversation reuse. Invalid arguments fail before a run or child session is created. Start this tool through pibo_run_start; bounded waits do not limit the child lifetime.",
59
112
  "Available agents:",
60
113
  catalog,
61
114
  ].join("\n"),
62
- promptSnippet: "Start pibo_agents_send_message through pibo_run_start. Reuse threadKey to continue its child session, and use run wait/status/read/cancel plus agent observe for lifecycle control.",
115
+ promptSnippet: "Start pibo_agents_send_message through pibo_run_start. Provide a nonblank sessionName of at most 40 Unicode code points on every call; Pibo trims it and rejects invalid input before creating a run. Follow-up calls update the reused child title without changing identity. Reuse threadKey to continue its child session, and use run wait/status/read/cancel plus agent observe for lifecycle control.",
63
116
  executionMode: "parallel",
64
117
  inputSchema: Type.Object({
65
- name: piboStringEnum(names, { description: "Available delegated agent name" }),
118
+ name: piboStringEnum(names, { description: "Configured delegated-agent selector; not the child title or reuse key" }),
119
+ sessionName: Type.String({
120
+ description: "Required nonblank human-readable child Pibo Session title, trimmed before use and limited to 40 Unicode code points. Follow-up calls update the title without changing thread identity.",
121
+ minLength: 1,
122
+ maxLength: PIBO_AGENT_SESSION_NAME_MAX_LENGTH,
123
+ pattern: "\\S",
124
+ }),
66
125
  message: Type.String({ description: "Message to send to the delegated agent" }),
67
126
  threadKey: Type.Optional(Type.String({
68
- description: "Stable key for continuing one delegated-agent conversation. Omit it to create a new child session.",
127
+ description: "Stable reuse key for one delegated-agent conversation; independent of sessionName. Omit it to create a new child session.",
69
128
  maxLength: 256,
70
129
  })),
71
130
  }),
131
+ prepareInput: preparePiboAgentToolInput,
72
132
  async execute(toolCallId, params, signal, _onUpdate, context) {
73
133
  const subagent = byName.get(params.name);
74
134
  if (!subagent)
@@ -76,10 +136,12 @@ export function createAgentToolDefinitions(subagents, controller) {
76
136
  if (!context.yieldedRunId) {
77
137
  throw new Error("pibo_agents_send_message is yielded-only. Start it through pibo_run_start.");
78
138
  }
139
+ const preparedParams = preparePiboAgentToolInput(params);
79
140
  const result = normalizeAgentSendMessageResult(await controller.sendMessage({
80
141
  subagent,
81
- message: params.message,
82
- threadKey: params.threadKey,
142
+ sessionName: preparedParams.sessionName,
143
+ message: preparedParams.message,
144
+ threadKey: preparedParams.threadKey,
83
145
  toolCallId,
84
146
  requestId: context.yieldedRunId,
85
147
  parentProvenance: context.getActiveMessage?.()?.provenance,
@@ -105,8 +167,8 @@ export function createAgentToolDefinitions(subagents, controller) {
105
167
  definePiboTool({
106
168
  name: "pibo_agents_list_agents",
107
169
  title: "Pibo Agents List Agents",
108
- description: "List available delegated-agent profiles and child agent instances owned by this session.",
109
- promptSnippet: "List available delegated agents and existing child instances with their agentId, thread, profile, and running, idle, or killed status.",
170
+ description: "List available delegated-agent profiles and child agent instances owned by this session, including each current sessionName when available.",
171
+ promptSnippet: "List available delegated agents and existing child instances with their agentId, current sessionName, threadKey, profile, and running, idle, or killed status.",
110
172
  executionMode: "parallel",
111
173
  annotations: { readOnly: true },
112
174
  inputSchema: Type.Object({}),
@@ -121,8 +183,12 @@ export function createAgentToolDefinitions(subagents, controller) {
121
183
  definePiboTool({
122
184
  name: "pibo_agents_observe",
123
185
  title: "Pibo Agents Observe",
124
- description: "Read bounded delegated-agent observations with exact agent, thread, event, kind, time, text, cursor, order, and limit filters.",
125
- promptSnippet: "Observe child-agent activity. Array filters use OR within a field and different fields combine with AND. For cursor polling, pass afterSequence from the previous result; pages consume the oldest unseen observations even when order is desc.",
186
+ description: [
187
+ "Read completed delegated-agent messages with bounded cursor, identity, event, time, text, order, and limit filters.",
188
+ "Default: the newest 20 completed assistant messages, with streaming deltas and tools hidden.",
189
+ "Set includeTools=true to add compact tool calls and terminal results; set toolDetail=full only for bounded diagnostic inspection.",
190
+ ].join("\n"),
191
+ promptSnippet: "Observe child-agent progress through completed assistant messages. Defaults: newest 20 messages, no streaming deltas, no duplicate tool progress events, no tools. Set includeTools=true for compact tool call/result summaries, toolDetail=full for bounded raw tool text, or eventTypes/kinds for explicit progress diagnostics. Pass afterSequence from the previous result for cursor polling; cursor pages consume the oldest unseen matches even when order is desc.",
126
192
  executionMode: "parallel",
127
193
  annotations: { readOnly: true },
128
194
  inputSchema: Type.Object({
@@ -130,21 +196,23 @@ export function createAgentToolDefinitions(subagents, controller) {
130
196
  agentIds: Type.Optional(Type.Array(Type.String({ description: "Owned child agentId" }), { maxItems: 50 })),
131
197
  names: Type.Optional(Type.Array(piboStringEnum(names), { maxItems: 50 })),
132
198
  threadKeys: Type.Optional(Type.Array(Type.String(), { maxItems: 50 })),
133
- eventTypes: Type.Optional(Type.Array(Type.String({ description: "Exact Pibo output event type" }), { maxItems: 50 })),
134
- kinds: Type.Optional(Type.Array(piboStringEnum(["message", "thinking", "tool", "error", "lifecycle", "event"]), { maxItems: 6 })),
199
+ eventTypes: Type.Optional(Type.Array(Type.String({ description: "Exact Pibo output event type. Explicit filters can retrieve progress events hidden by the default view." }), { maxItems: 50 })),
200
+ kinds: Type.Optional(Type.Array(piboStringEnum(["message", "thinking", "tool", "error", "lifecycle", "event"], { description: "Optional broad event kinds, including progress events. Omit eventTypes and kinds for the compact default view." }), { maxItems: 6 })),
135
201
  roles: Type.Optional(Type.Array(Type.String({ description: "Exact normalized role, for example assistant" }), { maxItems: 20 })),
136
202
  since: Type.Optional(Type.String({ description: "Inclusive ISO-8601 lower timestamp bound" })),
137
203
  until: Type.Optional(Type.String({ description: "Inclusive ISO-8601 upper timestamp bound" })),
138
204
  textContains: Type.Optional(Type.String({ description: "Case-insensitive substring match against normalized observation text" })),
139
- afterSequence: Type.Optional(Type.Integer({ description: "Exclusive live observation cursor. Cursor pages consume the oldest unseen observations; desc reverses only the returned page.", minimum: 0 })),
140
- order: Type.Optional(piboStringEnum(["asc", "desc"], { default: "asc" })),
141
- limit: Type.Optional(Type.Integer({ description: "Maximum observations to return", minimum: 1, maximum: 200, default: 50 })),
142
- includeDetails: Type.Optional(Type.Boolean({ description: "Include the normalized source event in each observation" })),
205
+ afterSequence: Type.Optional(Type.Integer({ description: "Exclusive live observation cursor. Cursor pages consume the oldest unseen matches; desc reverses only the returned page.", minimum: 0 })),
206
+ order: Type.Optional(piboStringEnum(["asc", "desc"], { default: "desc", description: "Newest first by default when no cursor is supplied" })),
207
+ limit: Type.Optional(Type.Integer({ description: "Maximum completed messages or activity records to return. Use 50 explicitly when needed.", minimum: 1, maximum: 200, default: 20 })),
208
+ includeTools: Type.Optional(Type.Boolean({ description: "Include tools. Default false. With the default message view, true adds tool_call and tool_execution_finished records.", default: false })),
209
+ toolDetail: Type.Optional(piboStringEnum(["summary", "full"], { default: "summary", description: "Tool text detail when tools are included. summary is compact; full remains bounded to the observation text limit." })),
210
+ includeDetails: Type.Optional(Type.Boolean({ description: "Include the normalized source event in structured details. Default false; use only for diagnostics.", default: false })),
143
211
  }),
144
212
  async execute(_toolCallId, params) {
145
213
  const result = controller.observe(params);
146
214
  return {
147
- content: [{ type: "text", text: resultText("Agent observations:", result) }],
215
+ content: [{ type: "text", text: formatAgentObservationsForModel(result) }],
148
216
  details: result,
149
217
  };
150
218
  },
@@ -198,18 +266,33 @@ export function createSubagentToolDefinitions(subagents, runner) {
198
266
  definitions.push(definePiboTool({
199
267
  name: toolName,
200
268
  title: `Pibo Subagent ${subagent.name}`,
201
- description: subagent.description ?? `Send a message to the ${subagent.name} subagent. Use threadKey to continue the same subagent session.`,
202
- promptSnippet: subagent.description ?? `Send a message to the ${subagent.name} subagent. Pass the same threadKey when you want to continue the same subagent session.`,
269
+ description: `${subagent.description ?? `Send a message to the ${subagent.name} subagent.`} sessionName is required, trimmed, nonblank, and limited to 40 Unicode code points.`,
270
+ promptSnippet: `Provide a nonblank sessionName of at most 40 Unicode code points. ${subagent.description ?? `Send a message to the ${subagent.name} subagent.`} Pass the same threadKey when you want to continue the same subagent session.`,
203
271
  executionMode: "parallel",
204
272
  inputSchema: Type.Object({
273
+ sessionName: Type.String({
274
+ description: "Required human-readable delegated-session title, trimmed before use and limited to 40 Unicode code points.",
275
+ minLength: 1,
276
+ maxLength: PIBO_AGENT_SESSION_NAME_MAX_LENGTH,
277
+ pattern: "\\S",
278
+ }),
205
279
  message: Type.String({ description: "Message to send to the subagent" }),
206
280
  threadKey: Type.Optional(Type.String({
207
281
  description: "Stable key for continuing a previous subagent conversation. Omit it to create a new subagent session.",
208
282
  maxLength: 256,
209
283
  })),
210
284
  }),
285
+ prepareInput: preparePiboDeprecatedSubagentToolInput,
211
286
  async execute(toolCallId, params, signal) {
212
- const result = await runner.runSubagent({ subagent, message: params.message, threadKey: params.threadKey, toolCallId, signal });
287
+ const preparedParams = preparePiboDeprecatedSubagentToolInput(params);
288
+ const result = await runner.runSubagent({
289
+ subagent,
290
+ sessionName: preparedParams.sessionName,
291
+ message: preparedParams.message,
292
+ threadKey: preparedParams.threadKey,
293
+ toolCallId,
294
+ signal,
295
+ });
213
296
  return { content: [{ type: "text", text: result.reply.text }], details: result };
214
297
  },
215
298
  }));
@@ -0,0 +1,24 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { getPiboHome } from "../core/pibo-home.js";
4
+ export function codexImageArtifactRoot() {
5
+ return join(getPiboHome(), "generated_images");
6
+ }
7
+ export function codexImageArtifactPath(sessionId, toolCallId) {
8
+ const safeSessionId = sanitizePathPart(sessionId?.trim() || "local");
9
+ const safeToolCallId = sanitizePathPart(toolCallId || `image_${Date.now()}`);
10
+ const artifactId = `${safeSessionId}/${safeToolCallId}.png`;
11
+ return {
12
+ artifactId,
13
+ savedPath: join(codexImageArtifactRoot(), safeSessionId, `${safeToolCallId}.png`),
14
+ };
15
+ }
16
+ export async function saveCodexGeneratedImage(sessionId, toolCallId, b64Json) {
17
+ const target = codexImageArtifactPath(sessionId, toolCallId);
18
+ await mkdir(dirname(target.savedPath), { recursive: true });
19
+ await writeFile(target.savedPath, Buffer.from(b64Json.trim(), "base64"));
20
+ return target;
21
+ }
22
+ function sanitizePathPart(value) {
23
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 120) || "unknown";
24
+ }
@@ -1,10 +1,10 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { readFile } from "node:fs/promises";
2
2
  import { platform, release, arch } from "node:os";
3
- import { dirname, extname, isAbsolute, join, resolve } from "node:path";
3
+ import { extname, isAbsolute, resolve } from "node:path";
4
4
  import { Type } from "typebox";
5
5
  import { readPiCredential, resolvePiProviderAuth, } from "../agent-runtimes/pi/credentials.js";
6
6
  import { definePiboTool } from "./contract.js";
7
- import { getPiboHome } from "../core/pibo-home.js";
7
+ import { saveCodexGeneratedImage } from "./codex-image-artifacts.js";
8
8
  const OPENAI_CODEX_PROVIDER = "openai-codex";
9
9
  const DEFAULT_CODEX_BACKEND_BASE_URL = "https://chatgpt.com/backend-api";
10
10
  const CODEX_IMAGE_MODEL = "gpt-image-2";
@@ -141,24 +141,6 @@ function validateImageArgs(params) {
141
141
  }
142
142
  }
143
143
  }
144
- function sanitizePathPart(value) {
145
- return value.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 120) || "unknown";
146
- }
147
- function artifactPath(sessionId, toolCallId) {
148
- const safeSessionId = sanitizePathPart(sessionId?.trim() || "local");
149
- const safeToolCallId = sanitizePathPart(toolCallId || `image_${Date.now()}`);
150
- const artifactId = `${safeSessionId}/${safeToolCallId}.png`;
151
- return {
152
- artifactId,
153
- savedPath: join(getPiboHome(), "generated_images", safeSessionId, `${safeToolCallId}.png`),
154
- };
155
- }
156
- async function saveGeneratedImage(sessionId, toolCallId, b64Json) {
157
- const target = artifactPath(sessionId, toolCallId);
158
- await mkdir(dirname(target.savedPath), { recursive: true });
159
- await writeFile(target.savedPath, Buffer.from(b64Json.trim(), "base64"));
160
- return target;
161
- }
162
144
  function truncateErrorBody(text) {
163
145
  const compact = text.replace(/\s+/g, " ").trim();
164
146
  return compact.length > 1000 ? `${compact.slice(0, 1000)}…` : compact;
@@ -240,7 +222,7 @@ export function createCodexImageGenerationToolDefinition(context = {}, options =
240
222
  const auth = await getCodexImageAuth();
241
223
  const response = await postCodexImageRequest(endpoint, createRequest(params.prompt, images), auth, signal, options.baseUrl);
242
224
  const result = firstImageB64(response);
243
- const saved = await saveGeneratedImage(context.piboSessionId, toolCallId, result);
225
+ const saved = await saveCodexGeneratedImage(context.piboSessionId, toolCallId, result);
244
226
  const details = {
245
227
  provider: OPENAI_CODEX_PROVIDER,
246
228
  api: "codex-chatgpt-images",
@@ -40,6 +40,43 @@ function firstHeaderValue(value) {
40
40
  const raw = Array.isArray(value) ? value[0] : value;
41
41
  return raw?.split(",")[0]?.trim() || undefined;
42
42
  }
43
+ function strictAuthorityHostname(rawAuthority) {
44
+ if (!rawAuthority)
45
+ return undefined;
46
+ const authority = rawAuthority.trim();
47
+ if (!authority || authority.includes(",") || /[\\/@?#\s]/.test(authority))
48
+ return undefined;
49
+ try {
50
+ return new URL(`http://${authority}`).hostname.toLowerCase();
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ }
56
+ function rawRequestHostname(request) {
57
+ const rawAuthority = request.headers.host;
58
+ if (Array.isArray(rawAuthority))
59
+ return undefined;
60
+ const directHostname = strictAuthorityHostname(rawAuthority);
61
+ if (!directHostname)
62
+ return undefined;
63
+ const forwardedHost = request.headers["x-forwarded-host"];
64
+ if (Array.isArray(forwardedHost) || forwardedHost?.includes(","))
65
+ return undefined;
66
+ const forwardedProto = request.headers["x-forwarded-proto"];
67
+ if (Array.isArray(forwardedProto) || forwardedProto?.includes(","))
68
+ return undefined;
69
+ if (Boolean(forwardedHost) !== Boolean(forwardedProto))
70
+ return undefined;
71
+ if (forwardedProto && forwardedProto !== "http" && forwardedProto !== "https")
72
+ return undefined;
73
+ if (isLoopbackAddress(request.socket.remoteAddress) &&
74
+ forwardedHost &&
75
+ forwardedProto) {
76
+ return strictAuthorityHostname(forwardedHost);
77
+ }
78
+ return directHostname;
79
+ }
43
80
  function isLoopbackAddress(address) {
44
81
  return address === "::1" || address === "127.0.0.1" || address?.startsWith("127.") === true || address?.startsWith("::ffff:127.") === true;
45
82
  }
@@ -240,9 +277,28 @@ export function createWebHostChannel(options = {}) {
240
277
  }
241
278
  };
242
279
  const handleRequest = async (nodeRequest, nodeResponse) => {
280
+ const requestController = new AbortController();
281
+ const abortRequest = () => requestController.abort(new Error("HTTP client disconnected"));
282
+ const abortPrematureResponse = () => {
283
+ if (!nodeResponse.writableFinished)
284
+ abortRequest();
285
+ };
286
+ nodeRequest.once("aborted", abortRequest);
287
+ nodeResponse.once("close", abortPrematureResponse);
243
288
  try {
244
289
  const baseURL = createRequestBaseURL(nodeRequest, host, port, options.canonicalBaseURL);
245
- const baseRequest = await nodeRequestToWebRequest(nodeRequest, baseURL);
290
+ const requestURL = new URL(nodeRequest.url ?? "/", baseURL);
291
+ const requestHostname = rawRequestHostname(nodeRequest);
292
+ const ctx = requireContext();
293
+ const apps = ctx.getWebApps();
294
+ const hostApp = requestHostname === requestURL.hostname.toLowerCase()
295
+ ? apps.find((candidate) => candidate.matchesHost?.(requestHostname))
296
+ : undefined;
297
+ if (hostApp?.handleNodeRequest) {
298
+ await hostApp.handleNodeRequest(nodeRequest, nodeResponse, createAppContext(ctx), requestURL);
299
+ return;
300
+ }
301
+ const baseRequest = await nodeRequestToWebRequest(nodeRequest, baseURL, requestController.signal);
246
302
  // Inject the TCP socket peer into every request so the local auth
247
303
  // plugin can apply the same loopback predicate from `getSession`
248
304
  // regardless of whether the call came from a browser cookie, the
@@ -271,13 +327,11 @@ export function createWebHostChannel(options = {}) {
271
327
  await sendResponse(nodeResponse, authResponse);
272
328
  return;
273
329
  }
274
- const ctx = requireContext();
275
330
  const simpleApiResponse = await handleSimpleAgentApiRequest(request, ctx);
276
331
  if (simpleApiResponse) {
277
332
  await sendResponse(nodeResponse, simpleApiResponse);
278
333
  return;
279
334
  }
280
- const apps = ctx.getWebApps();
281
335
  const app = apps.find((candidate) => matchPrefix(url.pathname, candidate.mountPath) || matchPrefix(url.pathname, candidate.apiPrefix));
282
336
  if (app) {
283
337
  const response = await app.handleRequest(request, createAppContext(ctx));
@@ -302,7 +356,34 @@ export function createWebHostChannel(options = {}) {
302
356
  }
303
357
  catch (error) {
304
358
  const status = error instanceof PiboAuthError || error instanceof PiboWebHttpError ? error.statusCode : 500;
305
- await sendResponse(nodeResponse, responseJson({ error: error instanceof Error ? error.message : String(error) }, { status }));
359
+ if (!nodeResponse.destroyed) {
360
+ await sendResponse(nodeResponse, responseJson({ error: error instanceof Error ? error.message : String(error) }, { status }));
361
+ }
362
+ }
363
+ finally {
364
+ nodeRequest.removeListener("aborted", abortRequest);
365
+ nodeResponse.removeListener("close", abortPrematureResponse);
366
+ }
367
+ };
368
+ const handleUpgrade = async (nodeRequest, socket, head) => {
369
+ try {
370
+ const ctx = requireContext();
371
+ const requestURL = new URL(nodeRequest.url ?? "/", createRequestBaseURL(nodeRequest, host, port, options.canonicalBaseURL));
372
+ const requestHostname = rawRequestHostname(nodeRequest);
373
+ const app = requestHostname === requestURL.hostname.toLowerCase()
374
+ ? ctx.getWebApps().find((candidate) => candidate.matchesHost?.(requestHostname))
375
+ : undefined;
376
+ if (!app?.handleUpgrade) {
377
+ socket.end("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
378
+ return;
379
+ }
380
+ await app.handleUpgrade(nodeRequest, socket, head, createAppContext(ctx), requestURL);
381
+ }
382
+ catch (error) {
383
+ if (!socket.destroyed) {
384
+ const unauthorized = error instanceof PiboAuthError || error instanceof PiboWebHttpError;
385
+ socket.end(`HTTP/1.1 ${unauthorized ? 401 : 502} ${unauthorized ? "Unauthorized" : "Bad Gateway"}\r\nConnection: close\r\n\r\n`);
386
+ }
306
387
  }
307
388
  };
308
389
  return {
@@ -320,6 +401,9 @@ export function createWebHostChannel(options = {}) {
320
401
  server = createServer((request, response) => {
321
402
  void handleRequest(request, response);
322
403
  });
404
+ server.on("upgrade", (request, socket, head) => {
405
+ void handleUpgrade(request, socket, head);
406
+ });
323
407
  server.on("connection", (socket) => {
324
408
  sockets.add(socket);
325
409
  socket.once("close", () => sockets.delete(socket));
package/dist/web/http.js CHANGED
@@ -44,7 +44,7 @@ export async function readJsonBody(request) {
44
44
  throw new PiboWebHttpError("Invalid JSON body", 400);
45
45
  }
46
46
  }
47
- export async function nodeRequestToWebRequest(request, baseURL) {
47
+ export async function nodeRequestToWebRequest(request, baseURL, signal) {
48
48
  const url = new URL(request.url ?? "/", baseURL);
49
49
  const headers = new Headers();
50
50
  for (const [key, value] of Object.entries(request.headers)) {
@@ -74,6 +74,7 @@ export async function nodeRequestToWebRequest(request, baseURL) {
74
74
  method: request.method,
75
75
  headers,
76
76
  body,
77
+ signal,
77
78
  });
78
79
  }
79
80
  export async function sendWebResponse(response, webResponse, options = {}) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "2.5.2",
3
+ "version": "3.0.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@pasko70/pibo",
9
- "version": "2.5.2",
9
+ "version": "3.0.0",
10
10
  "workspaces": [
11
11
  "packages/workflows"
12
12
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "2.5.2",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  "packages/workflows"