@pasko70/pibo 3.0.1 → 3.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/README.md +20 -6
- package/compute-image/Dockerfile +47 -0
- package/compute-image/Dockerfile.dockerignore +5 -0
- package/dist/agent-runtime/context-build.js +26 -2
- package/dist/agent-runtime/contract.js +78 -28
- package/dist/agent-runtime/portable-history.js +19 -9
- package/dist/agent-runtime/registry.js +8 -2
- package/dist/agent-runtime/routed-session.js +120 -15
- package/dist/agent-runtimes/codex-native/adapter.js +12 -0
- package/dist/agent-runtimes/codex-native/resource-delivery.js +35 -7
- package/dist/agent-runtimes/omp/adapter.js +3 -7
- package/dist/agent-runtimes/pi/adapter.js +5 -3
- package/dist/agent-runtimes/pi/routed-session.js +59 -12
- package/dist/agent-runtimes/pi/runtime.js +6 -0
- package/dist/apps/chat/agent-profiles.js +31 -2
- package/dist/apps/chat/agent-store.js +98 -6
- package/dist/apps/chat/chat-request-normalizers.js +31 -0
- package/dist/apps/chat/data/chat-data-mappers.js +61 -11
- package/dist/apps/chat/data/project-service.js +119 -15
- package/dist/apps/chat/data/room-service.js +21 -0
- package/dist/apps/chat/data/session-query-service.js +33 -10
- package/dist/apps/chat/data/timeline-query-service.js +60 -35
- package/dist/apps/chat/loop-api.js +10 -2
- package/dist/apps/chat/output-compactor.js +144 -21
- package/dist/apps/chat/output-event-policy.js +179 -2
- package/dist/apps/chat/stream.js +16 -7
- package/dist/apps/chat/trace-v2.js +1 -0
- package/dist/apps/chat/web-app.js +317 -86
- package/dist/apps/chat-ui/assets/{dist-BDIATCQt.js → dist-CUQJqggN.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-mqCviI-c.js → dist-ClUlQWYN.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-JnW4v8UE.js → dist-Djul9BmZ.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-lRiLP9rr.js → dist-GD0JGdCi.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BBDMeU5-.js → dist-W5HOqHym.js} +1 -1
- package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +228 -0
- package/dist/apps/chat-ui/assets/index-CmqRSbBU.css +1 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +43 -0
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/apps/cli-ui/InkSessionApp.js +111 -12
- package/dist/apps/cli-ui/cliSessionsCommand.js +34 -8
- package/dist/cli-session/localSessionSource.js +153 -161
- package/dist/cli.js +7 -11
- package/dist/compute/cli.js +35 -16
- package/dist/compute/docker.js +341 -94
- package/dist/config/config.js +3 -0
- package/dist/core/context-build.js +7 -16
- package/dist/core/output-persistence-retry.js +484 -0
- package/dist/core/output-render-sequence.js +331 -0
- package/dist/core/profiles.js +24 -0
- package/dist/core/provider-recovery.js +7 -1
- package/dist/core/session-model.js +37 -1
- package/dist/core/session-router.js +78 -110
- package/dist/data/ingest-service.js +91 -9
- package/dist/data/navigation-store.js +5 -5
- package/dist/data/pibo-store.js +8 -1
- package/dist/data/schema.js +243 -3
- package/dist/data/session-store.js +3 -3
- package/dist/debug/agents.js +91 -89
- package/dist/debug/index.js +46 -2
- package/dist/debug/pty.js +127 -48
- package/dist/gateway/cli.js +4 -0
- package/dist/gateway/web.js +8 -8
- package/dist/local/client.js +3 -2
- package/dist/loops/channel.js +3 -1
- package/dist/loops/cli.js +2 -1
- package/dist/loops/service.js +90 -11
- package/dist/loops/store.js +100 -74
- package/dist/mcp/agent-context.js +13 -26
- package/dist/mcp/commands/info.js +3 -1
- package/dist/mcp/config-command.js +46 -2
- package/dist/mcp/config.js +18 -4
- package/dist/plugins/context-files-store.js +36 -6
- package/dist/plugins/context-files.js +52 -1
- package/dist/plugins/registry.js +2 -0
- package/dist/plugins/user-profile-resources.js +22 -0
- package/dist/previews/base-url.js +34 -0
- package/dist/previews/config.js +2 -32
- package/dist/ralph/store.js +5 -0
- package/dist/reliability/store.js +378 -106
- package/dist/session-ui/terminalRows.js +8 -4
- package/dist/sessions/pibo-data-store.js +145 -0
- package/dist/sessions/store.js +42 -0
- package/dist/setup/cli.js +422 -6
- package/dist/setup/installation-profiles.js +464 -0
- package/dist/shared/deterministic-digest.js +94 -0
- package/dist/shared/trace-engine.js +54 -0
- package/dist/shared/trace-event-projection.js +124 -67
- package/dist/shared/trace-history.js +47 -12
- package/dist/shared/trace-live-reducer.js +23 -4
- package/dist/shared/trace-nodes.js +17 -28
- package/dist/shared/trace-order.js +41 -5
- package/dist/shared/trace-page-merge.js +44 -14
- package/dist/shared/trace-patch-nodes.js +2 -0
- package/dist/shared/trace-subagent-links.js +4 -1
- package/dist/shared/trace-tool-identity.js +15 -8
- package/dist/signals/projector.js +28 -6
- package/dist/subagents/observation-query.js +121 -0
- package/dist/subagents/tool.js +7 -3
- package/dist/tools/agent-browser-leases.js +58 -23
- package/dist/tools/agent-browser-wrapper.js +1 -1
- package/dist/tools/browser-use-leases.js +129 -16
- package/dist/tools/browser-use-wrapper.js +1 -1
- package/dist/tools/index.js +27 -11
- package/dist/tools/python-runtime.js +10 -3
- package/dist/tools/registry.js +1 -1
- package/dist/tools/runtime/node-backend.js +16 -5
- package/dist/tools/runtime/node-worker-source.js +112 -33
- package/dist/tools/runtime/python-backend.js +16 -5
- package/dist/tools/runtime/python-worker-source.js +167 -16
- package/dist/tools/runtime/registry.js +23 -10
- package/dist/user-skills/store.js +0 -1
- package/docs/ops/vscode-extension-release.md +9 -1
- package/npm-shrinkwrap.json +9 -2
- package/package.json +9 -2
- package/scripts/docker-entrypoint.sh +46 -0
- package/scripts/prepare-agent-browser-wrapper.sh +70 -0
- package/scripts/prepare-browser-use-wrapper.sh +255 -0
- package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +0 -228
- package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +0 -1
- package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +0 -43
- 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/compute/docker.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
import {
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { copyFile, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
5
7
|
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import dockerIgnoreModule from "@balena/dockerignore";
|
|
10
|
+
import { piboHomePath } from "../core/pibo-home.js";
|
|
6
11
|
import { COMPUTE_RESOURCE_POLICY_LABELS, buildComputeResourcePolicyLabels, buildDockerResourcePolicyArgs, resolveComputeResourcePolicy, } from "./resource-policy.js";
|
|
7
12
|
const execFileAsync = promisify(execFile);
|
|
13
|
+
const createDockerIgnore = dockerIgnoreModule;
|
|
8
14
|
export const IMAGE_NAME = "pibo:latest";
|
|
15
|
+
export const COMPUTE_IMAGE_ENV = "PIBO_COMPUTE_IMAGE";
|
|
16
|
+
export const PACKAGED_COMPUTE_DOCKERFILE = "compute-image/Dockerfile";
|
|
17
|
+
export const COMPUTE_PUBLISH_HOST = "127.0.0.1";
|
|
9
18
|
export const LABEL_ROLE = "pibo.compute.role";
|
|
10
19
|
export const LABEL_CREATED_AT = "pibo.compute.createdAt";
|
|
11
20
|
export const LABEL_HOLDER = "pibo.compute.holder";
|
|
@@ -19,9 +28,67 @@ export const LABEL_CLEANUP_STATE = "pibo.compute.cleanupState";
|
|
|
19
28
|
export const LABEL_DIRTY_REASON = "pibo.compute.dirtyReason";
|
|
20
29
|
export const LABEL_RALPH_JOB_ID = "pibo.ralph.jobId";
|
|
21
30
|
export const LABEL_RALPH_RUN_ID = "pibo.ralph.runId";
|
|
22
|
-
export
|
|
23
|
-
const
|
|
24
|
-
|
|
31
|
+
export function resolveComputeImageBuildConfig(workspaceDir, options = {}) {
|
|
32
|
+
const packageRoot = options.packageRoot ?? fileURLToPath(new URL("../../", import.meta.url));
|
|
33
|
+
const env = options.env ?? process.env;
|
|
34
|
+
const fileExists = options.fileExists ?? existsSync;
|
|
35
|
+
const readTextFile = options.readTextFile ?? ((candidate) => readFileSync(candidate, "utf8"));
|
|
36
|
+
const imageName = env[COMPUTE_IMAGE_ENV]?.trim() || IMAGE_NAME;
|
|
37
|
+
const workspaceDockerfile = path.join(workspaceDir, "Dockerfile");
|
|
38
|
+
const workspacePackageJson = path.join(workspaceDir, "package.json");
|
|
39
|
+
if (fileExists(workspaceDockerfile) && fileExists(workspacePackageJson)) {
|
|
40
|
+
try {
|
|
41
|
+
const manifest = JSON.parse(readTextFile(workspacePackageJson));
|
|
42
|
+
if (manifest.name === "@pasko70/pibo") {
|
|
43
|
+
return { imageName, buildContext: workspaceDir, source: "workspace" };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// An unrelated or malformed caller package is not a Pibo source checkout.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const dockerfile = path.join(packageRoot, PACKAGED_COMPUTE_DOCKERFILE);
|
|
51
|
+
if (!fileExists(dockerfile)) {
|
|
52
|
+
throw new Error(`No compute Dockerfile found in workspace ${workspaceDir} or installed package ${dockerfile}`);
|
|
53
|
+
}
|
|
54
|
+
return { imageName, buildContext: packageRoot, dockerfile, source: "package" };
|
|
55
|
+
}
|
|
56
|
+
export function buildDockerBuildArgs(config) {
|
|
57
|
+
return ["build", "-t", config.imageName, ...(config.dockerfile ? ["-f", config.dockerfile] : []), "."];
|
|
58
|
+
}
|
|
59
|
+
export async function dockerBuild(config) {
|
|
60
|
+
if (config.source === "workspace") {
|
|
61
|
+
await runDockerBuild(config);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!config.dockerfile)
|
|
65
|
+
throw new Error("Packaged compute image build requires a Dockerfile");
|
|
66
|
+
const stagedContext = await mkdtemp(path.join(tmpdir(), "pibo-compute-image-"));
|
|
67
|
+
try {
|
|
68
|
+
const { stdout } = await execFileAsync("npm", ["pack", "--json", "--ignore-scripts", "--pack-destination", stagedContext], {
|
|
69
|
+
cwd: config.buildContext,
|
|
70
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
71
|
+
});
|
|
72
|
+
const report = JSON.parse(stdout);
|
|
73
|
+
const filename = report[0]?.filename;
|
|
74
|
+
if (typeof filename !== "string" || !filename.endsWith(".tgz")) {
|
|
75
|
+
throw new Error("npm pack did not return a packaged Pibo archive");
|
|
76
|
+
}
|
|
77
|
+
await rename(path.join(stagedContext, filename), path.join(stagedContext, "pibo-package.tgz"));
|
|
78
|
+
await copyFile(config.dockerfile, path.join(stagedContext, "Dockerfile"));
|
|
79
|
+
await runDockerBuild({
|
|
80
|
+
imageName: config.imageName,
|
|
81
|
+
buildContext: stagedContext,
|
|
82
|
+
source: "workspace",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await rm(stagedContext, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function runDockerBuild(config) {
|
|
90
|
+
const { stderr } = await execFileAsync("docker", buildDockerBuildArgs(config), {
|
|
91
|
+
cwd: config.buildContext,
|
|
25
92
|
maxBuffer: 50 * 1024 * 1024,
|
|
26
93
|
});
|
|
27
94
|
if (stderr)
|
|
@@ -50,45 +117,83 @@ export async function getDependencyHash(workspaceDir) {
|
|
|
50
117
|
}
|
|
51
118
|
return hash.digest("hex");
|
|
52
119
|
}
|
|
53
|
-
export async function getSourceHash(workspaceDir) {
|
|
120
|
+
export async function getSourceHash(workspaceDir, dockerfile = path.join(workspaceDir, "Dockerfile")) {
|
|
54
121
|
const hash = createHash("sha256");
|
|
55
|
-
|
|
56
|
-
|
|
122
|
+
async function readOptionalFile(filePath) {
|
|
123
|
+
try {
|
|
124
|
+
return await readFile(filePath, "utf8");
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (error.code === "ENOENT")
|
|
128
|
+
return undefined;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const dockerfileIgnorePath = `${dockerfile}.dockerignore`;
|
|
133
|
+
const dockerfileIgnore = await readOptionalFile(dockerfileIgnorePath);
|
|
134
|
+
const rootIgnore = await readOptionalFile(path.join(workspaceDir, ".dockerignore"));
|
|
135
|
+
const activeIgnore = dockerfileIgnore ?? rootIgnore ?? "";
|
|
136
|
+
const requiredContextPaths = new Set([
|
|
137
|
+
path.relative(workspaceDir, dockerfile).split(path.sep).join("/"),
|
|
138
|
+
path.relative(workspaceDir, dockerfileIgnorePath).split(path.sep).join("/"),
|
|
139
|
+
".dockerignore",
|
|
140
|
+
]);
|
|
141
|
+
const contextIgnore = createDockerIgnore({ ignorecase: false }).add(activeIgnore
|
|
142
|
+
.split(/\r?\n/u)
|
|
143
|
+
.filter((line) => line.trim() !== ".")
|
|
144
|
+
.join("\n"));
|
|
145
|
+
const hasNegatedPattern = activeIgnore.split(/\r?\n/u).some((line) => /^\/*!/u.test(line.trim()));
|
|
146
|
+
const contextEntries = [];
|
|
147
|
+
function isIgnored(relativePath, directory) {
|
|
148
|
+
if (requiredContextPaths.has(relativePath))
|
|
149
|
+
return false;
|
|
150
|
+
return contextIgnore.ignores(directory ? `${relativePath}/` : relativePath);
|
|
151
|
+
}
|
|
152
|
+
async function walk(dir, relativeDir = "") {
|
|
153
|
+
let containsIncludedEntry = false;
|
|
57
154
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
58
155
|
for (const entry of entries) {
|
|
59
156
|
const fullPath = path.join(dir, entry.name);
|
|
157
|
+
const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
|
|
60
158
|
if (entry.isDirectory()) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
entry.name === "dist" ||
|
|
64
|
-
entry.name === ".pibo" ||
|
|
65
|
-
entry.name === "plans" ||
|
|
66
|
-
entry.name === "Reports") {
|
|
159
|
+
const ignored = isIgnored(relativePath, true);
|
|
160
|
+
if (ignored && !hasNegatedPattern)
|
|
67
161
|
continue;
|
|
162
|
+
const containsIncludedChild = await walk(fullPath, relativePath);
|
|
163
|
+
if (!ignored || containsIncludedChild) {
|
|
164
|
+
contextEntries.push({ relativePath, type: "directory" });
|
|
165
|
+
containsIncludedEntry = true;
|
|
68
166
|
}
|
|
69
|
-
await walk(fullPath);
|
|
70
167
|
}
|
|
71
|
-
else if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
entry.name === "package.json" ||
|
|
75
|
-
entry.name === "package-lock.json" ||
|
|
76
|
-
entry.name === "Dockerfile") {
|
|
77
|
-
files.push(fullPath);
|
|
78
|
-
}
|
|
168
|
+
else if (!isIgnored(relativePath, false)) {
|
|
169
|
+
contextEntries.push({ relativePath, type: entry.isFile() ? "file" : entry.isSymbolicLink() ? "symlink" : "other" });
|
|
170
|
+
containsIncludedEntry = true;
|
|
79
171
|
}
|
|
80
172
|
}
|
|
173
|
+
return containsIncludedEntry;
|
|
81
174
|
}
|
|
82
175
|
await walk(workspaceDir);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
hash.update(
|
|
176
|
+
contextEntries.sort((a, b) => (a.relativePath < b.relativePath ? -1 : a.relativePath > b.relativePath ? 1 : 0));
|
|
177
|
+
function update(value) {
|
|
178
|
+
const bytes = typeof value === "string" ? Buffer.from(value) : value;
|
|
179
|
+
hash.update(`${bytes.length}:`);
|
|
180
|
+
hash.update(bytes);
|
|
181
|
+
}
|
|
182
|
+
for (const entry of contextEntries) {
|
|
183
|
+
const fullPath = path.join(workspaceDir, entry.relativePath);
|
|
184
|
+
const stat = await lstat(fullPath);
|
|
185
|
+
update(entry.type);
|
|
186
|
+
update(entry.relativePath);
|
|
187
|
+
update(String(stat.mode & 0o777));
|
|
188
|
+
if (entry.type === "file")
|
|
189
|
+
update(await readFile(fullPath));
|
|
190
|
+
else if (entry.type === "symlink")
|
|
191
|
+
update(await readlink(fullPath));
|
|
87
192
|
}
|
|
88
193
|
return hash.digest("hex");
|
|
89
194
|
}
|
|
90
|
-
export async function shouldRebuild(workspaceDir, hashFile) {
|
|
91
|
-
const currentHash = await getSourceHash(workspaceDir);
|
|
195
|
+
export async function shouldRebuild(workspaceDir, hashFile, dockerfile) {
|
|
196
|
+
const currentHash = await getSourceHash(workspaceDir, dockerfile);
|
|
92
197
|
try {
|
|
93
198
|
const savedHash = await readFile(hashFile, "utf-8");
|
|
94
199
|
return savedHash.trim() !== currentHash;
|
|
@@ -107,8 +212,8 @@ export async function shouldRebuildDeps(workspaceDir, hashFile) {
|
|
|
107
212
|
return true;
|
|
108
213
|
}
|
|
109
214
|
}
|
|
110
|
-
export async function saveHash(workspaceDir, hashFile) {
|
|
111
|
-
const hash = await getSourceHash(workspaceDir);
|
|
215
|
+
export async function saveHash(workspaceDir, hashFile, dockerfile) {
|
|
216
|
+
const hash = await getSourceHash(workspaceDir, dockerfile);
|
|
112
217
|
await mkdir(path.dirname(hashFile), { recursive: true });
|
|
113
218
|
await writeFile(hashFile, hash, "utf-8");
|
|
114
219
|
}
|
|
@@ -119,6 +224,143 @@ export async function saveDepHash(workspaceDir, hashFile) {
|
|
|
119
224
|
}
|
|
120
225
|
const DEV_PORT_BASE = 4800;
|
|
121
226
|
const DEV_PORT_BLOCK_SIZE = 10;
|
|
227
|
+
const DEV_PORT_ALLOCATION_LOCK_TIMEOUT_MS = 30_000;
|
|
228
|
+
const DEV_PORT_ALLOCATION_LOCK_POLL_MS = 25;
|
|
229
|
+
const DEV_PORT_ALLOCATION_MALFORMED_STALE_MS = 5_000;
|
|
230
|
+
function devPortAllocationLockPath() {
|
|
231
|
+
return piboHomePath("compute", "dev-port-allocation.lock");
|
|
232
|
+
}
|
|
233
|
+
function isErrnoException(error, code) {
|
|
234
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
235
|
+
}
|
|
236
|
+
function processIsAlive(pid) {
|
|
237
|
+
try {
|
|
238
|
+
process.kill(pid, 0);
|
|
239
|
+
return "active";
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
if (isErrnoException(error, "ESRCH"))
|
|
243
|
+
return "dead";
|
|
244
|
+
return "ambiguous";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function processIdentity(pid) {
|
|
248
|
+
if (process.platform !== "linux")
|
|
249
|
+
return { liveness: processIsAlive(pid) };
|
|
250
|
+
let raw;
|
|
251
|
+
try {
|
|
252
|
+
raw = await readFile(`/proc/${pid}/stat`, "utf8");
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (isErrnoException(error, "ENOENT") || isErrnoException(error, "ESRCH"))
|
|
256
|
+
return { liveness: "dead" };
|
|
257
|
+
return { liveness: "ambiguous" };
|
|
258
|
+
}
|
|
259
|
+
const commandEnd = raw.lastIndexOf(")");
|
|
260
|
+
if (commandEnd < 2 || commandEnd + 2 >= raw.length)
|
|
261
|
+
return { liveness: "ambiguous" };
|
|
262
|
+
const fields = raw.slice(commandEnd + 2).trim().split(/\s+/);
|
|
263
|
+
if (fields[0] === "Z")
|
|
264
|
+
return { liveness: "dead" };
|
|
265
|
+
const startTicks = fields[19];
|
|
266
|
+
if (!startTicks || !/^[1-9][0-9]*$/.test(startTicks))
|
|
267
|
+
return { liveness: "ambiguous" };
|
|
268
|
+
return { liveness: "active", startId: `${pid}:${startTicks}` };
|
|
269
|
+
}
|
|
270
|
+
async function devPortAllocationLockOwnerLiveness(owner) {
|
|
271
|
+
const identity = await processIdentity(owner.pid);
|
|
272
|
+
if (identity.liveness !== "active")
|
|
273
|
+
return identity.liveness;
|
|
274
|
+
if (process.platform !== "linux")
|
|
275
|
+
return "active";
|
|
276
|
+
if (!owner.processStartId || !identity.startId)
|
|
277
|
+
return "ambiguous";
|
|
278
|
+
return owner.processStartId === identity.startId ? "active" : "dead";
|
|
279
|
+
}
|
|
280
|
+
async function removeAbandonedDevPortAllocationLock(lockPath) {
|
|
281
|
+
const raw = await readFile(lockPath, "utf8").catch(() => undefined);
|
|
282
|
+
if (raw === undefined)
|
|
283
|
+
return;
|
|
284
|
+
let owner = {};
|
|
285
|
+
try {
|
|
286
|
+
owner = JSON.parse(raw);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// A partially written lock can be reclaimed after a short grace period.
|
|
290
|
+
}
|
|
291
|
+
const validOwner = typeof owner.pid === "number"
|
|
292
|
+
&& Number.isSafeInteger(owner.pid)
|
|
293
|
+
&& owner.pid > 0
|
|
294
|
+
&& typeof owner.token === "string"
|
|
295
|
+
&& owner.token.length > 0
|
|
296
|
+
&& typeof owner.createdAt === "string"
|
|
297
|
+
&& (owner.processStartId === undefined || typeof owner.processStartId === "string");
|
|
298
|
+
if (!validOwner) {
|
|
299
|
+
const lockStat = await stat(lockPath).catch(() => undefined);
|
|
300
|
+
if (!lockStat || Date.now() - lockStat.mtimeMs <= DEV_PORT_ALLOCATION_MALFORMED_STALE_MS)
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
else if (await devPortAllocationLockOwnerLiveness(owner) !== "dead") {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if ((await readFile(lockPath, "utf8").catch(() => undefined)) === raw) {
|
|
307
|
+
await rm(lockPath, { force: true });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async function releaseDevPortAllocationLock(lockPath, token) {
|
|
311
|
+
try {
|
|
312
|
+
const owner = JSON.parse(await readFile(lockPath, "utf8"));
|
|
313
|
+
if (owner.token === token)
|
|
314
|
+
await rm(lockPath, { force: true });
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// The lock was already removed or replaced.
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function withDevPortAllocationLock(run) {
|
|
321
|
+
const lockPath = devPortAllocationLockPath();
|
|
322
|
+
const token = randomUUID();
|
|
323
|
+
const startedAt = Date.now();
|
|
324
|
+
const identity = await processIdentity(process.pid);
|
|
325
|
+
if (identity.liveness !== "active")
|
|
326
|
+
throw new Error("Cannot establish compute dev port allocation lock owner identity");
|
|
327
|
+
while (true) {
|
|
328
|
+
try {
|
|
329
|
+
await mkdir(path.dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
330
|
+
const handle = await open(lockPath, "wx", 0o600);
|
|
331
|
+
try {
|
|
332
|
+
await handle.writeFile(JSON.stringify({
|
|
333
|
+
pid: process.pid,
|
|
334
|
+
...(identity.startId ? { processStartId: identity.startId } : {}),
|
|
335
|
+
token,
|
|
336
|
+
createdAt: new Date().toISOString(),
|
|
337
|
+
}), "utf8");
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
await handle.close();
|
|
341
|
+
await rm(lockPath, { force: true });
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
await handle.close();
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
if (!isErrnoException(error, "EEXIST"))
|
|
349
|
+
throw error;
|
|
350
|
+
await removeAbandonedDevPortAllocationLock(lockPath);
|
|
351
|
+
if (Date.now() - startedAt >= DEV_PORT_ALLOCATION_LOCK_TIMEOUT_MS) {
|
|
352
|
+
throw new Error(`Timed out waiting for compute dev port allocation lock ${lockPath}`);
|
|
353
|
+
}
|
|
354
|
+
await new Promise((resolve) => setTimeout(resolve, DEV_PORT_ALLOCATION_LOCK_POLL_MS));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
return await run();
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
await releaseDevPortAllocationLock(lockPath, token);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
122
364
|
async function findNextPortBlock() {
|
|
123
365
|
const { stdout } = await execFileAsync("docker", [
|
|
124
366
|
"ps",
|
|
@@ -205,15 +447,15 @@ export function buildDevWorkerDockerRunArgs(options) {
|
|
|
205
447
|
"-e",
|
|
206
448
|
"PIBO_COMPUTE_WORKER_ROLE=dev",
|
|
207
449
|
"-p",
|
|
208
|
-
|
|
450
|
+
`${COMPUTE_PUBLISH_HOST}:${options.gatewayPort}:4789`,
|
|
209
451
|
"-p",
|
|
210
|
-
|
|
452
|
+
`${COMPUTE_PUBLISH_HOST}:${options.cdpPort}:56663`,
|
|
211
453
|
"-p",
|
|
212
|
-
|
|
454
|
+
`${COMPUTE_PUBLISH_HOST}:${options.webPort}:4788`,
|
|
213
455
|
"-p",
|
|
214
|
-
|
|
456
|
+
`${COMPUTE_PUBLISH_HOST}:${options.webUIPortChat}:4790`,
|
|
215
457
|
"-p",
|
|
216
|
-
|
|
458
|
+
`${COMPUTE_PUBLISH_HOST}:${options.webUIPortContext}:4791`,
|
|
217
459
|
"-v",
|
|
218
460
|
`${options.worktreePath}:/workspace`,
|
|
219
461
|
...(options.hostNodeModules ? ["-v", `${options.hostNodeModules}:/workspace/node_modules`] : []),
|
|
@@ -241,21 +483,14 @@ export function buildDevWorkerDockerRunArgs(options) {
|
|
|
241
483
|
...buildComputeResourcePolicyLabels(policy).flatMap((label) => ["--label", label]),
|
|
242
484
|
"--entrypoint",
|
|
243
485
|
"/bin/sh",
|
|
244
|
-
IMAGE_NAME,
|
|
486
|
+
options.imageName ?? IMAGE_NAME,
|
|
245
487
|
"-c",
|
|
246
|
-
"tail -f /dev/null",
|
|
488
|
+
"ln -sf /workspace/dist/bin/pibo.js /usr/local/bin/pibo && exec tail -f /dev/null",
|
|
247
489
|
];
|
|
248
490
|
}
|
|
249
491
|
export async function spawnDevWorker(options) {
|
|
250
492
|
const worktreePath = path.join(options.repoDir, ".worktrees", options.worktreeName);
|
|
251
493
|
await createWorktree(options.repoDir, options.worktreeName);
|
|
252
|
-
const block = await findNextPortBlock();
|
|
253
|
-
const base = DEV_PORT_BASE + block * DEV_PORT_BLOCK_SIZE;
|
|
254
|
-
const gatewayPort = base;
|
|
255
|
-
const cdpPort = base + 1;
|
|
256
|
-
const webPort = base + 2;
|
|
257
|
-
const webUIPortChat = base + 3;
|
|
258
|
-
const webUIPortContext = base + 4;
|
|
259
494
|
const id = `pibo-dev-${options.worktreeName}`;
|
|
260
495
|
const createdAt = new Date().toISOString();
|
|
261
496
|
// Mount host node_modules into the container so dependencies are immediately available
|
|
@@ -269,35 +504,41 @@ export async function spawnDevWorker(options) {
|
|
|
269
504
|
catch {
|
|
270
505
|
// host node_modules does not exist; skip mount
|
|
271
506
|
}
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
507
|
+
const ports = await withDevPortAllocationLock(async () => {
|
|
508
|
+
const block = await findNextPortBlock();
|
|
509
|
+
const base = DEV_PORT_BASE + block * DEV_PORT_BLOCK_SIZE;
|
|
510
|
+
const gatewayPort = base;
|
|
511
|
+
const cdpPort = base + 1;
|
|
512
|
+
const webPort = base + 2;
|
|
513
|
+
const webUIPortChat = base + 3;
|
|
514
|
+
const webUIPortContext = base + 4;
|
|
515
|
+
const args = buildDevWorkerDockerRunArgs({
|
|
516
|
+
id,
|
|
517
|
+
imageName: options.imageName,
|
|
518
|
+
worktreePath,
|
|
519
|
+
worktreeName: options.worktreeName,
|
|
520
|
+
block,
|
|
521
|
+
gatewayPort,
|
|
522
|
+
cdpPort,
|
|
523
|
+
webPort,
|
|
524
|
+
webUIPortChat,
|
|
525
|
+
webUIPortContext,
|
|
526
|
+
createdAt,
|
|
527
|
+
holder: options.holder,
|
|
528
|
+
ttlSeconds: options.ttlSeconds,
|
|
529
|
+
idleSeconds: options.idleSeconds,
|
|
530
|
+
ralphJobId: options.ralphJobId,
|
|
531
|
+
ralphRunId: options.ralphRunId,
|
|
532
|
+
hostNodeModules: nodeModulesMount,
|
|
533
|
+
});
|
|
534
|
+
await execFileAsync("docker", args, { cwd: options.repoDir });
|
|
535
|
+
return { gatewayPort, cdpPort, webPort, webUIPortChat, webUIPortContext };
|
|
289
536
|
});
|
|
290
|
-
await execFileAsync("docker", args, { cwd: options.repoDir });
|
|
291
|
-
const host = await detectHost();
|
|
292
537
|
return {
|
|
293
538
|
id,
|
|
294
|
-
image: IMAGE_NAME,
|
|
295
|
-
gatewayHost:
|
|
296
|
-
|
|
297
|
-
cdpPort,
|
|
298
|
-
webPort,
|
|
299
|
-
webUIPortChat,
|
|
300
|
-
webUIPortContext,
|
|
539
|
+
image: options.imageName ?? IMAGE_NAME,
|
|
540
|
+
gatewayHost: COMPUTE_PUBLISH_HOST,
|
|
541
|
+
...ports,
|
|
301
542
|
connect: `docker exec -it ${id} bash`,
|
|
302
543
|
worktree: worktreePath,
|
|
303
544
|
};
|
|
@@ -316,11 +557,11 @@ export function buildWorkerDockerRunArgs(options) {
|
|
|
316
557
|
"-e",
|
|
317
558
|
"PIBO_COMPUTE_WORKER_ROLE=worker",
|
|
318
559
|
"-p",
|
|
319
|
-
|
|
560
|
+
`${COMPUTE_PUBLISH_HOST}::4789`,
|
|
320
561
|
"-p",
|
|
321
|
-
|
|
562
|
+
`${COMPUTE_PUBLISH_HOST}::56663`,
|
|
322
563
|
"-p",
|
|
323
|
-
|
|
564
|
+
`${COMPUTE_PUBLISH_HOST}::4788`,
|
|
324
565
|
...buildComputeWorkerMetadataLabels({
|
|
325
566
|
role: "worker",
|
|
326
567
|
createdAt: options.createdAt,
|
|
@@ -335,7 +576,7 @@ export function buildWorkerDockerRunArgs(options) {
|
|
|
335
576
|
ralphRunId: options.ralphRunId,
|
|
336
577
|
}).flatMap((label) => ["--label", label]),
|
|
337
578
|
...buildComputeResourcePolicyLabels(policy).flatMap((label) => ["--label", label]),
|
|
338
|
-
IMAGE_NAME,
|
|
579
|
+
options.imageName ?? IMAGE_NAME,
|
|
339
580
|
"gateway:web",
|
|
340
581
|
];
|
|
341
582
|
}
|
|
@@ -345,6 +586,7 @@ export async function spawnWorker(options) {
|
|
|
345
586
|
const args = buildWorkerDockerRunArgs({
|
|
346
587
|
id,
|
|
347
588
|
createdAt,
|
|
589
|
+
imageName: options.imageName,
|
|
348
590
|
holder: options.holder,
|
|
349
591
|
worktreePath: options.workspaceDir,
|
|
350
592
|
ttlSeconds: options.ttlSeconds,
|
|
@@ -360,12 +602,10 @@ export async function spawnWorker(options) {
|
|
|
360
602
|
const gatewayPort = parseHostPort(port4789);
|
|
361
603
|
const cdpPort = parseHostPort(port56663);
|
|
362
604
|
const webPort = parseHostPort(port4788);
|
|
363
|
-
// Detect host IP
|
|
364
|
-
const host = await detectHost();
|
|
365
605
|
return {
|
|
366
606
|
id,
|
|
367
|
-
image: IMAGE_NAME,
|
|
368
|
-
gatewayHost:
|
|
607
|
+
image: options.imageName ?? IMAGE_NAME,
|
|
608
|
+
gatewayHost: COMPUTE_PUBLISH_HOST,
|
|
369
609
|
gatewayPort,
|
|
370
610
|
cdpPort,
|
|
371
611
|
webPort,
|
|
@@ -385,19 +625,6 @@ function parseHostPort(dockerPortOutput) {
|
|
|
385
625
|
}
|
|
386
626
|
return 0;
|
|
387
627
|
}
|
|
388
|
-
async function detectHost() {
|
|
389
|
-
try {
|
|
390
|
-
const { stdout } = await execFileAsync("hostname", ["-I"]);
|
|
391
|
-
const ips = stdout.trim().split(/\s+/);
|
|
392
|
-
const nonLocal = ips.find((ip) => !ip.startsWith("127."));
|
|
393
|
-
if (nonLocal)
|
|
394
|
-
return nonLocal;
|
|
395
|
-
return ips[0] || "127.0.0.1";
|
|
396
|
-
}
|
|
397
|
-
catch {
|
|
398
|
-
return "127.0.0.1";
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
628
|
export function parseDockerLabels(labelsStr) {
|
|
402
629
|
const labels = {};
|
|
403
630
|
for (const part of (labelsStr ?? "").split(",")) {
|
|
@@ -677,13 +904,33 @@ export function buildComputeWorkerReapPlan(workers, options = {}) {
|
|
|
677
904
|
};
|
|
678
905
|
}
|
|
679
906
|
export async function releaseWorker(id) {
|
|
907
|
+
let inspected;
|
|
908
|
+
try {
|
|
909
|
+
const { stdout } = await execFileAsync("docker", ["inspect", id]);
|
|
910
|
+
inspected = JSON.parse(stdout);
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
throw new Error(`Unable to inspect Docker container "${id}" before release. No container was changed. ` +
|
|
914
|
+
`Run "pibo compute list --all" to find Pibo compute workers and verify Docker is available.`);
|
|
915
|
+
}
|
|
916
|
+
const container = inspected[0];
|
|
917
|
+
const resolvedId = container?.Id?.trim();
|
|
918
|
+
if (!container || inspected.length !== 1 || !resolvedId) {
|
|
919
|
+
throw new Error(`Unable to resolve Docker container "${id}" before release. No container was changed.`);
|
|
920
|
+
}
|
|
921
|
+
const role = container.Config?.Labels?.[LABEL_ROLE];
|
|
922
|
+
if (role !== "worker" && role !== "dev") {
|
|
923
|
+
const identity = role === undefined ? `${LABEL_ROLE} is missing` : `${LABEL_ROLE}=${JSON.stringify(role)}`;
|
|
924
|
+
throw new Error(`Refusing to release Docker container "${id}": ${identity}; expected "worker" or "dev". ` +
|
|
925
|
+
`No container was changed. Run "pibo compute list --all" to find releasable workers.`);
|
|
926
|
+
}
|
|
680
927
|
try {
|
|
681
|
-
await execFileAsync("docker", ["stop", "-t", "10",
|
|
928
|
+
await execFileAsync("docker", ["stop", "-t", "10", resolvedId]);
|
|
682
929
|
}
|
|
683
930
|
catch {
|
|
684
931
|
// might already be stopped
|
|
685
932
|
}
|
|
686
|
-
await execFileAsync("docker", ["rm",
|
|
933
|
+
await execFileAsync("docker", ["rm", resolvedId]);
|
|
687
934
|
}
|
|
688
935
|
export async function applyComputeWorkerReapPlan(plan, options = {}) {
|
|
689
936
|
const release = options.release ?? releaseWorker;
|
package/dist/config/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { ensurePrivatePiboHomeForPath, piboHomePath } from "../core/pibo-home.js";
|
|
4
4
|
import { protectPrivateFileSync } from "../core/private-path.js";
|
|
5
|
+
import { parsePreviewBaseURL } from "../previews/base-url.js";
|
|
5
6
|
export const DEFAULT_PIBO_CONFIG_PATH = "config.json";
|
|
6
7
|
export function getDefaultPiboConfigPath() {
|
|
7
8
|
return piboHomePath(DEFAULT_PIBO_CONFIG_PATH);
|
|
@@ -137,6 +138,8 @@ function parseConfigValue(definition, value) {
|
|
|
137
138
|
if (definition.key === "auth.secret" && value.length < 32) {
|
|
138
139
|
throw new Error("auth.secret must be at least 32 characters");
|
|
139
140
|
}
|
|
141
|
+
if (definition.key === "preview.baseURL")
|
|
142
|
+
parsePreviewBaseURL(value);
|
|
140
143
|
if (definition.values && !definition.values.includes(value)) {
|
|
141
144
|
throw new Error(`${definition.key} must be one of: ${definition.values.join(", ")}`);
|
|
142
145
|
}
|
|
@@ -214,6 +214,7 @@ function withoutRequestedModels(profile) {
|
|
|
214
214
|
mcpServers: profile.mcpServers,
|
|
215
215
|
piPackages: profile.piPackages,
|
|
216
216
|
contextFiles: profile.contextFiles,
|
|
217
|
+
diagnostics: profile.diagnostics,
|
|
217
218
|
builtinTools: profile.builtinTools,
|
|
218
219
|
builtinToolNames: profile.builtinToolNames,
|
|
219
220
|
autoContextFiles: profile.autoContextFiles,
|
|
@@ -229,15 +230,15 @@ function toolDefinitionSchema(definition, toolInfo) {
|
|
|
229
230
|
}
|
|
230
231
|
function generatedOriginForTool(name, profile) {
|
|
231
232
|
if (name === "runtime")
|
|
232
|
-
return "
|
|
233
|
+
return "pibo-runtime";
|
|
233
234
|
if (name.startsWith("pibo_agents_"))
|
|
234
|
-
return "
|
|
235
|
+
return "pibo-subagents";
|
|
235
236
|
if (name.startsWith("pibo_run_"))
|
|
236
|
-
return "
|
|
237
|
+
return "pibo-run-control";
|
|
237
238
|
if (PIBO_GOAL_TOOL_NAMES.includes(name))
|
|
238
|
-
return "
|
|
239
|
+
return "pibo-goal-control";
|
|
239
240
|
if (name === "apply_patch" || name === "view_image")
|
|
240
|
-
return "
|
|
241
|
+
return "codex-compat";
|
|
241
242
|
if (profile.builtinToolNames.includes(name) || DEFAULT_BUILTIN_TOOL_NAMES.includes(name))
|
|
242
243
|
return undefined;
|
|
243
244
|
return undefined;
|
|
@@ -502,17 +503,6 @@ export async function inspectPiboContextBuild(options = {}) {
|
|
|
502
503
|
},
|
|
503
504
|
});
|
|
504
505
|
}
|
|
505
|
-
if (generatedOrigin) {
|
|
506
|
-
children.push({
|
|
507
|
-
id: `tools/${name}/generated-origin`,
|
|
508
|
-
kind: "metadata",
|
|
509
|
-
title: "Generated Origin",
|
|
510
|
-
source: "generated",
|
|
511
|
-
state: "active",
|
|
512
|
-
badges: ["GENERATED", "PIBO"],
|
|
513
|
-
hydratedText: generatedOrigin,
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
506
|
if (providerTool?.providerTool.kind === "web_search") {
|
|
517
507
|
children.push({
|
|
518
508
|
id: `tools/${name}/provider-payload`,
|
|
@@ -568,6 +558,7 @@ export async function inspectPiboContextBuild(options = {}) {
|
|
|
568
558
|
hasDefinition: Boolean(definition || info),
|
|
569
559
|
description: definition?.description ?? info?.description ?? providerTool?.description,
|
|
570
560
|
...(toolSource === "pibo" || toolSource === "generated" ? { owner: "pibo", deliveryMode: "direct" } : {}),
|
|
561
|
+
...(generatedOrigin ? { inspectorOrigin: { label: generatedOrigin, modelVisible: false } } : {}),
|
|
571
562
|
},
|
|
572
563
|
children,
|
|
573
564
|
};
|