@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
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
4
|
+
const CODE_SERVER_VERSION = "4.135.0";
|
|
5
|
+
const BROWSER_USE_VERSION = "0.12.6";
|
|
6
|
+
const AGENT_BROWSER_VERSION = "0.27.0";
|
|
7
|
+
const CHROME_DEVTOOLS_MCP_VERSION = "1.8.0";
|
|
8
|
+
const FILESYSTEM_MCP_VERSION = "2026.7.10";
|
|
9
|
+
const CODE_SERVER_SHA256 = {
|
|
10
|
+
x64: "300ef4e37e469e6368a4673c6a623e1c9ba8a34f42b394fb49c431a8900bc7d1",
|
|
11
|
+
arm64: "fe6561798415e709109cb902dca2a57a687240af7d8220f6fa1d01cd2ae0541e",
|
|
12
|
+
};
|
|
13
|
+
const COMPONENTS = {
|
|
14
|
+
core: { name: "core", version: "package", description: "Pibo gateway and Chat Web", optional: false },
|
|
15
|
+
"vscode-web": { name: "vscode-web", version: CODE_SERVER_VERSION, description: "Embedded code-server workspace", optional: true, sha256: { "linux-amd64": CODE_SERVER_SHA256.x64, "linux-arm64": CODE_SERVER_SHA256.arm64 } },
|
|
16
|
+
"browser-tools": { name: "browser-tools", version: `browser-use ${BROWSER_USE_VERSION}; agent-browser ${AGENT_BROWSER_VERSION}`, description: "Curated browser automation CLIs", optional: true, integrity: { "browser-use-wheel-sha256": "f969aa1f895cbf44525e13b5743d78282bee589e68cc5d0ee238666b8b0d0b13", "agent-browser-npm-sri": "sha512-mmHzVsYFVA6nshNNGJzg83aVMgKpf4h98ytY3pvtJB1Cot0ZyA2bfnkbSngGD56Azkj+GlhVH6qx9DfKOVE0yg==" } },
|
|
17
|
+
"managed-browser": { name: "managed-browser", version: "system chromium", description: "Managed Chromium/CDP runtime prerequisites", optional: true },
|
|
18
|
+
"web-annotations": { name: "web-annotations", version: "package", description: "Pibo Web Annotations integration", optional: true },
|
|
19
|
+
"mcp-defaults": { name: "mcp-defaults", version: `chrome-devtools-mcp ${CHROME_DEVTOOLS_MCP_VERSION}; filesystem ${FILESYSTEM_MCP_VERSION}`, description: "Allowlisted Chrome DevTools MCP integration", optional: true, integrity: { "chrome-devtools-mcp-npm-sri": "sha512-Wrm9z0/5WbVs778apjWgYRkpe9bvYQWjK2zVRwqoPAtz1IHQ5+GvotM07UGXJcfrA0rj6Gt1Pnn5+w/Tf1nU4w==", "filesystem-npm-sri": "sha512-Mmjg4anFBD5OzbPnGJOA0jPPN8645ERhQk38HQLpSenx1ox9bfdPkmAzUnNjeQtqQGFLtKe13J20RtLBmUKMZA==" } },
|
|
20
|
+
};
|
|
21
|
+
const PROFILE_COMPONENTS = {
|
|
22
|
+
vanilla: ["core"],
|
|
23
|
+
"batteries-included": ["core", "vscode-web", "browser-tools", "managed-browser", "web-annotations", "mcp-defaults"],
|
|
24
|
+
};
|
|
25
|
+
function sha256(content) {
|
|
26
|
+
return createHash("sha256").update(content).digest("hex");
|
|
27
|
+
}
|
|
28
|
+
function shellQuote(value) {
|
|
29
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
30
|
+
}
|
|
31
|
+
function normalizeContent(content) {
|
|
32
|
+
return content.endsWith("\n") ? content : `${content}\n`;
|
|
33
|
+
}
|
|
34
|
+
export function installationManifestPath(piboHome) {
|
|
35
|
+
return join(piboHome, "setup", "installation.json");
|
|
36
|
+
}
|
|
37
|
+
function gatewayService(options) {
|
|
38
|
+
const integrationEnvironment = [
|
|
39
|
+
...(options.hasVscode ? ["Environment=PIBO_VSCODE_WEB_URL=/apps/vscode/", `Environment=PIBO_VSCODE_WORKSPACE_ROOT=${options.workspaceRoot}`] : []),
|
|
40
|
+
...(options.hasMcpDefaults ? [`Environment=MCP_CONFIG_PATH=${options.piboHome}/setup/mcp-defaults.json`] : []),
|
|
41
|
+
];
|
|
42
|
+
return `[Unit]
|
|
43
|
+
Description=Pibo web gateway
|
|
44
|
+
After=network-online.target
|
|
45
|
+
Wants=network-online.target
|
|
46
|
+
|
|
47
|
+
[Service]
|
|
48
|
+
Type=simple
|
|
49
|
+
User=root
|
|
50
|
+
WorkingDirectory=/root
|
|
51
|
+
Environment=HOME=/root
|
|
52
|
+
Environment=PIBO_HOME=${options.piboHome}
|
|
53
|
+
Environment=NODE_ENV=production
|
|
54
|
+
Environment=PIBO_GATEWAY_WEB_PORT=4788
|
|
55
|
+
${integrationEnvironment.length > 0 ? `${integrationEnvironment.join("\n")}\n` : ""}ExecStart=${options.piboCommand} gateway:web --web-host 127.0.0.1 --web-port 4788
|
|
56
|
+
Restart=always
|
|
57
|
+
RestartSec=5
|
|
58
|
+
KillSignal=SIGTERM
|
|
59
|
+
TimeoutStopSec=30
|
|
60
|
+
|
|
61
|
+
[Install]
|
|
62
|
+
WantedBy=multi-user.target
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
function codeServerService(workspaceRoot) {
|
|
66
|
+
return `[Unit]
|
|
67
|
+
Description=Pibo VS Code Web
|
|
68
|
+
After=network-online.target
|
|
69
|
+
Wants=network-online.target
|
|
70
|
+
|
|
71
|
+
[Service]
|
|
72
|
+
Type=simple
|
|
73
|
+
User=pibo-code
|
|
74
|
+
Group=pibo-code
|
|
75
|
+
WorkingDirectory=${workspaceRoot}
|
|
76
|
+
Environment=HOME=/var/lib/pibo-code
|
|
77
|
+
Environment=XDG_DATA_HOME=/var/lib/pibo-code/.local/share
|
|
78
|
+
Environment=XDG_CONFIG_HOME=/var/lib/pibo-code/.config
|
|
79
|
+
ExecStart=/opt/pibo/code-server/${CODE_SERVER_VERSION}/bin/code-server --bind-addr 127.0.0.1:4790 --auth none --disable-telemetry --disable-update-check --disable-workspace-trust ${workspaceRoot}
|
|
80
|
+
Restart=always
|
|
81
|
+
RestartSec=5
|
|
82
|
+
NoNewPrivileges=true
|
|
83
|
+
PrivateTmp=true
|
|
84
|
+
ProtectSystem=strict
|
|
85
|
+
ProtectHome=read-only
|
|
86
|
+
ReadWritePaths=${workspaceRoot} /var/lib/pibo-code
|
|
87
|
+
|
|
88
|
+
[Install]
|
|
89
|
+
WantedBy=multi-user.target
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
function proxyConfig(domain, batteriesIncluded) {
|
|
93
|
+
const site = domain ?? "http://127.0.0.1:8080";
|
|
94
|
+
const vscode = batteriesIncluded
|
|
95
|
+
? `\tredir /apps/vscode /apps/vscode/ 308
|
|
96
|
+
\thandle_path /apps/vscode/* {
|
|
97
|
+
\t\tforward_auth 127.0.0.1:4788 {
|
|
98
|
+
\t\t\turi /api/chat/bootstrap
|
|
99
|
+
\t\t\theader_up -Connection
|
|
100
|
+
\t\t\theader_up -Upgrade
|
|
101
|
+
\t\t}
|
|
102
|
+
\t\treverse_proxy 127.0.0.1:4790 {
|
|
103
|
+
\t\t\theader_down -X-Frame-Options
|
|
104
|
+
\t\t\theader_down +Content-Security-Policy "frame-ancestors 'self'"
|
|
105
|
+
\t\t}
|
|
106
|
+
\t}
|
|
107
|
+
`
|
|
108
|
+
: "";
|
|
109
|
+
return `${site} {
|
|
110
|
+
\tencode zstd gzip
|
|
111
|
+
${vscode}\thandle {
|
|
112
|
+
\t\treverse_proxy 127.0.0.1:4788
|
|
113
|
+
\t}
|
|
114
|
+
}
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
function mcpDefaults(piboHome, workspaceRoot) {
|
|
118
|
+
const binDir = join(piboHome, "setup", "mcp-runtime", "node_modules", ".bin");
|
|
119
|
+
return `${JSON.stringify({
|
|
120
|
+
mcpServers: {
|
|
121
|
+
"chrome-devtools": {
|
|
122
|
+
command: join(piboHome, "setup", "bin", "chrome-devtools-mcp"),
|
|
123
|
+
allowedTools: ["list_pages", "select_page", "navigate_page", "take_screenshot", "take_snapshot", "evaluate_script", "list_console_messages", "list_network_requests", "get_network_request"],
|
|
124
|
+
pibo: { description: "Inspect the Pibo-managed Chromium instance without broad host access", descriptionSource: "registry" },
|
|
125
|
+
},
|
|
126
|
+
filesystem: {
|
|
127
|
+
command: join(binDir, "mcp-server-filesystem"),
|
|
128
|
+
args: [workspaceRoot],
|
|
129
|
+
allowedTools: ["read_text_file", "read_media_file", "read_multiple_files", "list_directory", "list_directory_with_sizes", "directory_tree", "search_files", "get_file_info", "list_allowed_directories"],
|
|
130
|
+
pibo: { description: "Read-only workspace inspection restricted to the configured Pibo workspace root", descriptionSource: "registry" },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
}, null, 2)}\n`;
|
|
134
|
+
}
|
|
135
|
+
function chromeDevtoolsMcpWrapper(piboHome, piboCommand) {
|
|
136
|
+
const executable = join(piboHome, "setup", "mcp-runtime", "node_modules", ".bin", "chrome-devtools-mcp");
|
|
137
|
+
return `#!/bin/sh
|
|
138
|
+
set -eu
|
|
139
|
+
export PIBO_HOME=${shellQuote(piboHome)}
|
|
140
|
+
eval "$(${piboCommand} tools env browser-use)"
|
|
141
|
+
cdp_url=$(browser-use --pibo-ensure-chrome | tail -n 1)
|
|
142
|
+
exec ${shellQuote(executable)} --browserUrl "$cdp_url" --no-usage-statistics --no-performance-crux "$@"
|
|
143
|
+
`;
|
|
144
|
+
}
|
|
145
|
+
function installPackagesCommand(packages) {
|
|
146
|
+
const names = packages.map(shellQuote).join(" ");
|
|
147
|
+
return `if command -v apt-get >/dev/null 2>&1; then apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y ${names}; elif command -v dnf >/dev/null 2>&1; then dnf install -y ${names}; elif command -v pacman >/dev/null 2>&1; then pacman -Sy --needed --noconfirm ${names}; else echo "Supported package manager not found (apt-get, dnf, or pacman)" >&2; exit 2; fi`;
|
|
148
|
+
}
|
|
149
|
+
function codeServerInstallCommand() {
|
|
150
|
+
return `set -eu; arch=$(uname -m); case "$arch" in x86_64|amd64) asset=amd64; sha=${CODE_SERVER_SHA256.x64};; aarch64|arm64) asset=arm64; sha=${CODE_SERVER_SHA256.arm64};; *) echo "Unsupported architecture: $arch" >&2; exit 2;; esac; tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; curl -fsSL "https://github.com/coder/code-server/releases/download/v${CODE_SERVER_VERSION}/code-server-${CODE_SERVER_VERSION}-linux-$asset.tar.gz" -o "$tmp/code-server.tgz"; echo "$sha $tmp/code-server.tgz" | sha256sum -c -; mkdir -p /opt/pibo/code-server/${CODE_SERVER_VERSION}; tar -xzf "$tmp/code-server.tgz" --strip-components=1 -C /opt/pibo/code-server/${CODE_SERVER_VERSION}`;
|
|
151
|
+
}
|
|
152
|
+
function protectedRouteProbe(url, websocket = false) {
|
|
153
|
+
const headers = websocket ? " --http1.1 -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: cGliby1zZXR1cC1wcm9iZQ=='" : "";
|
|
154
|
+
return `status=$(curl -sS -o /dev/null -w '%{http_code}'${headers} ${shellQuote(url)}); case "$status" in 302|303|307|308|401|403) ;; *) echo "Expected authenticated route to reject anonymous request, got HTTP $status" >&2; exit 1;; esac`;
|
|
155
|
+
}
|
|
156
|
+
function availableRouteProbe(url) {
|
|
157
|
+
return `status=$(curl -sS -o /dev/null -w '%{http_code}' ${shellQuote(url)}); case "$status" in 200|204|302|303|307|308|401|403) ;; *) echo "Expected maintained integration route to be available, got HTTP $status" >&2; exit 1;; esac`;
|
|
158
|
+
}
|
|
159
|
+
export function createInstallationPlan(options) {
|
|
160
|
+
const piboHome = options.piboHome ?? "/root/.pibo";
|
|
161
|
+
const workspaceRoot = options.workspaceRoot ?? "/srv/pibo-workspaces";
|
|
162
|
+
const piboCommand = options.piboCommand ?? "/usr/bin/pibo";
|
|
163
|
+
const requestedComponents = [...PROFILE_COMPONENTS[options.profile], ...(options.additionalComponents ?? [])];
|
|
164
|
+
if (requestedComponents.includes("mcp-defaults"))
|
|
165
|
+
requestedComponents.push("browser-tools");
|
|
166
|
+
if (requestedComponents.includes("browser-tools"))
|
|
167
|
+
requestedComponents.push("managed-browser");
|
|
168
|
+
const componentNames = [...new Set(requestedComponents)];
|
|
169
|
+
const components = componentNames.map((name) => name === "core" && options.piboVersion ? { ...COMPONENTS.core, version: options.piboVersion } : COMPONENTS[name]);
|
|
170
|
+
const hasVscode = componentNames.includes("vscode-web");
|
|
171
|
+
const hasBrowserTools = componentNames.includes("browser-tools");
|
|
172
|
+
const hasManagedBrowser = componentNames.includes("managed-browser");
|
|
173
|
+
const hasMcpDefaults = componentNames.includes("mcp-defaults");
|
|
174
|
+
const hasWebAnnotations = componentNames.includes("web-annotations");
|
|
175
|
+
const files = [
|
|
176
|
+
{ path: "/etc/systemd/system/pibo-web.service", purpose: "Pibo gateway service", content: gatewayService({ piboHome, workspaceRoot, hasVscode, hasMcpDefaults, piboCommand }) },
|
|
177
|
+
{ path: "/etc/caddy/Caddyfile", purpose: hasVscode ? "Authenticated same-origin Pibo and VS Code Web proxy" : "Pibo HTTPS proxy", content: proxyConfig(options.domain, hasVscode) },
|
|
178
|
+
];
|
|
179
|
+
if (componentNames.includes("vscode-web")) {
|
|
180
|
+
files.push({ path: "/etc/systemd/system/pibo-code-server.service", purpose: "Loopback VS Code Web service", content: codeServerService(workspaceRoot) }, { path: "/etc/pibo/code-server-default-settings.json", purpose: "Initial Pibo-compatible VS Code Web defaults", content: `${JSON.stringify({ "workbench.colorTheme": "Default Dark Modern", "window.autoDetectColorScheme": false, "telemetry.telemetryLevel": "off" }, null, 2)}\n`, mode: 0o600 }, { path: `/opt/pibo/code-server/${CODE_SERVER_VERSION}/.pibo-owned`, purpose: "Ownership marker for the pinned VS Code Web binary", content: `code-server ${CODE_SERVER_VERSION}\n`, mode: 0o600 });
|
|
181
|
+
}
|
|
182
|
+
if (componentNames.includes("mcp-defaults")) {
|
|
183
|
+
files.push({ path: join(piboHome, "setup", "mcp-defaults.json"), purpose: "Allowlisted MCP defaults", content: mcpDefaults(piboHome, workspaceRoot), mode: 0o600 }, { path: join(piboHome, "setup", "bin", "chrome-devtools-mcp"), purpose: "Dynamic managed-CDP launcher for Chrome DevTools MCP", content: chromeDevtoolsMcpWrapper(piboHome, piboCommand), mode: 0o755 }, { path: join(piboHome, "setup", "mcp-runtime", ".pibo-owned"), purpose: "Ownership marker for isolated curated MCP packages", content: `chrome-devtools-mcp ${CHROME_DEVTOOLS_MCP_VERSION}\n@modelcontextprotocol/server-filesystem ${FILESYSTEM_MCP_VERSION}\n`, mode: 0o600 });
|
|
184
|
+
}
|
|
185
|
+
const browserUseProfiles = join(piboHome, "tools/browser-use/home/chrome-profiles");
|
|
186
|
+
const browserUseAuthPool = join(piboHome, "tools/browser-use/home/auth-pool");
|
|
187
|
+
const browserUseProcessPool = join(piboHome, "tools/browser-use/home/pibo-browser-pool");
|
|
188
|
+
const agentBrowserProfiles = join(piboHome, "tools/agent-browser/home/profiles");
|
|
189
|
+
const browserHealthCommand = `PIBO_HOME=${shellQuote(piboHome)} ${piboCommand} tools browser-use health --json | grep -q '\"overall\": \"ok\"' && PIBO_HOME=${shellQuote(piboHome)} ${piboCommand} tools agent-browser health --json | grep -q '\"overall\": \"ok\"'`;
|
|
190
|
+
const mcpHealthCommand = `test -x ${shellQuote(join(piboHome, "setup", "mcp-runtime", "node_modules", ".bin", "chrome-devtools-mcp"))} && test -x ${shellQuote(join(piboHome, "setup", "mcp-runtime", "node_modules", ".bin", "mcp-server-filesystem"))} && ${shellQuote(join(piboHome, "setup", "mcp-runtime", "node_modules", ".bin", "chrome-devtools-mcp"))} --help >/dev/null`;
|
|
191
|
+
const actions = [
|
|
192
|
+
{ id: "host-packages", description: "Install core reverse-proxy prerequisites", command: installPackagesCommand(["ca-certificates", "caddy"]), checkCommand: "command -v caddy >/dev/null 2>&1", privileged: true },
|
|
193
|
+
];
|
|
194
|
+
if (hasVscode)
|
|
195
|
+
actions.push({ id: "vscode-packages", description: "Install VS Code Web download prerequisites", command: installPackagesCommand(["curl"]), checkCommand: "command -v curl >/dev/null 2>&1", privileged: true }, { id: "service-account", description: "Create the dedicated VS Code Web account and workspace", command: `id -u pibo-code >/dev/null 2>&1 || { nologin=$(command -v nologin || printf /usr/sbin/nologin); useradd --system --home /var/lib/pibo-code --create-home --shell "$nologin" pibo-code; }; settings=/var/lib/pibo-code/.local/share/code-server/User/settings.json; mkdir -p ${shellQuote(workspaceRoot)} "$(dirname "$settings")"; test -e "$settings" || install -m 600 -o pibo-code -g pibo-code /etc/pibo/code-server-default-settings.json "$settings"; chown -R pibo-code:pibo-code ${shellQuote(workspaceRoot)} /var/lib/pibo-code`, checkCommand: `id -u pibo-code >/dev/null 2>&1 && test -d ${shellQuote(workspaceRoot)} && test -f /var/lib/pibo-code/.local/share/code-server/User/settings.json`, privileged: true }, { id: "code-server", description: `Install pinned code-server ${CODE_SERVER_VERSION}`, command: codeServerInstallCommand(), checkCommand: `test -x /opt/pibo/code-server/${CODE_SERVER_VERSION}/bin/code-server`, privileged: true });
|
|
196
|
+
if (hasManagedBrowser)
|
|
197
|
+
actions.push({ id: "chromium", description: "Install managed Chromium prerequisites", command: installPackagesCommand(["chromium"]), checkCommand: "command -v chromium >/dev/null 2>&1 || command -v chromium-browser >/dev/null 2>&1", privileged: true });
|
|
198
|
+
if (hasBrowserTools)
|
|
199
|
+
actions.push({ id: "browser-profiles", description: "Create private managed browser-profile infrastructure", command: `install -d -m 700 ${shellQuote(browserUseProfiles)} ${shellQuote(browserUseAuthPool)} ${shellQuote(browserUseProcessPool)} ${shellQuote(agentBrowserProfiles)} ${shellQuote(join(agentBrowserProfiles, "auth-template"))} ${shellQuote(join(agentBrowserProfiles, "leases"))}`, checkCommand: `test "$(stat -c %a ${shellQuote(browserUseProfiles)})" = 700 && test "$(stat -c %a ${shellQuote(browserUseAuthPool)})" = 700 && test "$(stat -c %a ${shellQuote(browserUseProcessPool)})" = 700 && test "$(stat -c %a ${shellQuote(agentBrowserProfiles)})" = 700`, privileged: true }, { id: "browser-use", description: `Install browser-use ${BROWSER_USE_VERSION} through the curated Pibo tool registry`, command: `PIBO_HOME=${shellQuote(piboHome)} ${piboCommand} tools install browser-use && PIBO_HOME=${shellQuote(piboHome)} ${piboCommand} tools env browser-use >/dev/null`, checkCommand: `PIBO_HOME=${shellQuote(piboHome)} ${piboCommand} tools browser-use health --json | grep -q '\"overall\": \"ok\"'`, privileged: false }, { id: "agent-browser", description: `Install agent-browser ${AGENT_BROWSER_VERSION} through the curated Pibo tool registry`, command: `PIBO_HOME=${shellQuote(piboHome)} ${piboCommand} tools install agent-browser`, checkCommand: `PIBO_HOME=${shellQuote(piboHome)} ${piboCommand} tools agent-browser health --json | grep -q '\"overall\": \"ok\"'`, privileged: false }, { id: "browser-health", description: "Verify curated browser runtimes and managed profile infrastructure", command: browserHealthCommand, checkCommand: browserHealthCommand, privileged: false });
|
|
200
|
+
if (hasMcpDefaults)
|
|
201
|
+
actions.push({ id: "mcp-defaults", description: "Install pinned curated MCP packages in an isolated runtime", command: `npm install --prefix ${shellQuote(join(piboHome, "setup", "mcp-runtime"))} chrome-devtools-mcp@${CHROME_DEVTOOLS_MCP_VERSION} @modelcontextprotocol/server-filesystem@${FILESYSTEM_MCP_VERSION}`, checkCommand: mcpHealthCommand, privileged: false }, { id: "mcp-health", description: "Verify curated MCP executables and command startup", command: mcpHealthCommand, checkCommand: mcpHealthCommand, privileged: false });
|
|
202
|
+
const healthCommand = [
|
|
203
|
+
`PIBO_HOME=${shellQuote(piboHome)} ${piboCommand} gateway web doctor`,
|
|
204
|
+
...(hasVscode ? ["curl -fsS http://127.0.0.1:4790/healthz >/dev/null"] : []),
|
|
205
|
+
...(hasWebAnnotations ? [availableRouteProbe("http://127.0.0.1:4788/apps/web-annotations/overlay.js?pibo-setup-probe=1")] : []),
|
|
206
|
+
...(hasVscode && options.domain ? [protectedRouteProbe(`https://${options.domain}/apps/vscode/?pibo-setup-probe=1`), protectedRouteProbe(`https://${options.domain}/apps/vscode/ws?pibo-setup-probe=1`, true)] : []),
|
|
207
|
+
].join(" && ");
|
|
208
|
+
actions.push({ id: "systemd", description: "Reload and enable setup-managed services", command: `systemctl daemon-reload && systemctl enable pibo-web${hasVscode ? " && systemctl enable --now pibo-code-server" : ""}`, checkCommand: `systemctl is-enabled --quiet pibo-web${hasVscode ? " && systemctl is-enabled --quiet pibo-code-server && systemctl is-active --quiet pibo-code-server" : ""}`, privileged: true, restartEffect: hasVscode ? "Starts or restarts the loopback VS Code Web service" : "Registers the Pibo gateway service" }, { id: "gateway", description: "Use the Pibo-owned safe gateway lifecycle", command: `PIBO_HOME=${shellQuote(piboHome)} ${piboCommand} gateway web restart`, checkCommand: "systemctl is-active --quiet pibo-web", privileged: true, restartEffect: "May be blocked while production sessions are active" }, { id: "caddy", description: "Validate and reload the public proxy", command: "caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy", checkCommand: "caddy validate --config /etc/caddy/Caddyfile >/dev/null 2>&1 && systemctl is-active --quiet caddy", privileged: true, restartEffect: "Reloads Caddy without dropping established connections" }, { id: "health", description: "Verify gateway, optional VS Code Web, public auth, and WebSocket gates", command: healthCommand, checkCommand: healthCommand, privileged: false });
|
|
209
|
+
const warnings = options.domain ? [] : ["No domain was provided. Caddy remains loopback-only on http://127.0.0.1:8080 and cannot provision trusted TLS."];
|
|
210
|
+
return {
|
|
211
|
+
schemaVersion: 1,
|
|
212
|
+
profileVersion: 1,
|
|
213
|
+
profile: options.profile,
|
|
214
|
+
summary: options.profile === "batteries-included" ? "Complete supported Pibo workstation with embedded IDE, browser tooling, annotations, and curated MCP defaults." : "Minimal Pibo gateway and Chat Web installation without optional integrations.",
|
|
215
|
+
piboHome,
|
|
216
|
+
workspaceRoot,
|
|
217
|
+
piboCommand,
|
|
218
|
+
domain: options.domain,
|
|
219
|
+
components,
|
|
220
|
+
hostPackages: ["node >=24", "npm", "caddy", "ca-certificates", ...(hasVscode ? ["curl"] : []), ...(hasManagedBrowser ? ["chromium"] : [])],
|
|
221
|
+
downloads: hasVscode ? [{
|
|
222
|
+
name: "code-server",
|
|
223
|
+
version: CODE_SERVER_VERSION,
|
|
224
|
+
urls: {
|
|
225
|
+
"linux-amd64": `https://github.com/coder/code-server/releases/download/v${CODE_SERVER_VERSION}/code-server-${CODE_SERVER_VERSION}-linux-amd64.tar.gz`,
|
|
226
|
+
"linux-arm64": `https://github.com/coder/code-server/releases/download/v${CODE_SERVER_VERSION}/code-server-${CODE_SERVER_VERSION}-linux-arm64.tar.gz`,
|
|
227
|
+
},
|
|
228
|
+
sha256: { "linux-amd64": CODE_SERVER_SHA256.x64, "linux-arm64": CODE_SERVER_SHA256.arm64 },
|
|
229
|
+
}] : [],
|
|
230
|
+
services: [
|
|
231
|
+
{ name: "pibo-web", bind: "127.0.0.1:4788", public: false, user: "root" },
|
|
232
|
+
...(hasVscode ? [{ name: "pibo-code-server", bind: "127.0.0.1:4790", public: false, user: "pibo-code" }] : []),
|
|
233
|
+
],
|
|
234
|
+
ports: [
|
|
235
|
+
{ name: "chat-web", host: "127.0.0.1", port: 4788, exposure: options.domain ? "public-via-proxy" : "loopback" },
|
|
236
|
+
...(hasVscode ? [{ name: "vscode-web", host: "127.0.0.1", port: 4790, exposure: options.domain ? "public-via-proxy" : "loopback" }] : []),
|
|
237
|
+
options.domain ? { name: "https-proxy", host: options.domain, port: 443, exposure: "public" } : { name: "local-proxy", host: "127.0.0.1", port: 8080, exposure: "loopback" },
|
|
238
|
+
],
|
|
239
|
+
files,
|
|
240
|
+
actions,
|
|
241
|
+
securityBoundaries: [
|
|
242
|
+
"The Pibo gateway binds only to 127.0.0.1:4788.",
|
|
243
|
+
"Caddy is the only public listener and terminates TLS when a domain is configured.",
|
|
244
|
+
...(hasVscode ? ["VS Code Web binds only to 127.0.0.1:4790.", "Caddy authenticates /apps/vscode/* through the Pibo Chat bootstrap endpoint.", "The IDE runs as pibo-code and can write only its data directory and the configured workspace root."] : []),
|
|
245
|
+
...(hasMcpDefaults ? ["MCP defaults use explicit tool allowlists and the Pibo-managed loopback CDP endpoint."] : []),
|
|
246
|
+
],
|
|
247
|
+
warnings,
|
|
248
|
+
repairCommand: `${piboCommand} setup install --profile ${options.profile} --pibo-home ${shellQuote(piboHome)}${options.domain ? ` --domain ${shellQuote(options.domain)}` : ""} --apply --yes`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function ownedDirectoryMarkerParent(path) {
|
|
252
|
+
return /(?:\/opt\/pibo\/code-server\/[^/]+|\/setup\/mcp-runtime)\/\.pibo-owned$/.test(path) ? dirname(path) : undefined;
|
|
253
|
+
}
|
|
254
|
+
function outputPath(path, root) {
|
|
255
|
+
if (!root)
|
|
256
|
+
return path;
|
|
257
|
+
return isAbsolute(path) ? join(root, path.replace(/^\/+/, "")) : join(root, path);
|
|
258
|
+
}
|
|
259
|
+
function atomicWrite(path, content, mode) {
|
|
260
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
261
|
+
const tempPath = `${path}.pibo-setup-${process.pid}.tmp`;
|
|
262
|
+
writeFileSync(tempPath, content, { mode: mode ?? 0o644 });
|
|
263
|
+
renameSync(tempPath, path);
|
|
264
|
+
}
|
|
265
|
+
export function validateInstallationPlanTargets(plan, options = {}) {
|
|
266
|
+
const manifestPath = outputPath(installationManifestPath(plan.piboHome), options.root);
|
|
267
|
+
const previous = readInstallationManifest(manifestPath);
|
|
268
|
+
for (const file of plan.files) {
|
|
269
|
+
const destination = outputPath(file.path, options.root);
|
|
270
|
+
if (!existsSync(destination))
|
|
271
|
+
continue;
|
|
272
|
+
const currentDigest = sha256(readFileSync(destination, "utf8"));
|
|
273
|
+
const targetDigest = sha256(normalizeContent(file.content));
|
|
274
|
+
if (currentDigest === targetDigest)
|
|
275
|
+
continue;
|
|
276
|
+
const wasOwned = previous?.ownedFiles.find((entry) => entry.path === file.path);
|
|
277
|
+
if (!wasOwned || currentDigest !== wasOwned.sha256)
|
|
278
|
+
throw new Error(`Refusing to overwrite unmanaged or modified file: ${destination}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
export function materializeInstallationPlan(plan, options = {}) {
|
|
282
|
+
validateInstallationPlanTargets(plan, options);
|
|
283
|
+
const manifestPath = outputPath(installationManifestPath(plan.piboHome), options.root);
|
|
284
|
+
const previous = readInstallationManifest(manifestPath);
|
|
285
|
+
const written = [];
|
|
286
|
+
const unchanged = [];
|
|
287
|
+
const removed = [];
|
|
288
|
+
const preserved = [];
|
|
289
|
+
const ownedFiles = [];
|
|
290
|
+
for (const file of plan.files) {
|
|
291
|
+
const destination = outputPath(file.path, options.root);
|
|
292
|
+
const content = normalizeContent(file.content);
|
|
293
|
+
const digest = sha256(content);
|
|
294
|
+
if (existsSync(destination)) {
|
|
295
|
+
const current = readFileSync(destination, "utf8");
|
|
296
|
+
if (sha256(current) === digest) {
|
|
297
|
+
if (process.platform !== "win32" && file.mode !== undefined && (statSync(destination).mode & 0o777) !== file.mode) {
|
|
298
|
+
chmodSync(destination, file.mode);
|
|
299
|
+
written.push(destination);
|
|
300
|
+
}
|
|
301
|
+
else
|
|
302
|
+
unchanged.push(destination);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
const wasOwned = previous?.ownedFiles.find((entry) => entry.path === file.path);
|
|
306
|
+
if (!wasOwned || sha256(current) !== wasOwned.sha256)
|
|
307
|
+
throw new Error(`Refusing to overwrite unmanaged or modified file: ${destination}`);
|
|
308
|
+
atomicWrite(destination, content, file.mode);
|
|
309
|
+
written.push(destination);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
atomicWrite(destination, content, file.mode);
|
|
314
|
+
written.push(destination);
|
|
315
|
+
}
|
|
316
|
+
ownedFiles.push({ path: file.path, sha256: digest, mode: file.mode, purpose: file.purpose });
|
|
317
|
+
}
|
|
318
|
+
const plannedPaths = new Set(ownedFiles.map((file) => file.path));
|
|
319
|
+
for (const stale of previous?.ownedFiles ?? []) {
|
|
320
|
+
if (plannedPaths.has(stale.path))
|
|
321
|
+
continue;
|
|
322
|
+
const path = outputPath(stale.path, options.root);
|
|
323
|
+
if (!existsSync(path))
|
|
324
|
+
continue;
|
|
325
|
+
if (sha256(readFileSync(path, "utf8")) !== stale.sha256) {
|
|
326
|
+
preserved.push(path);
|
|
327
|
+
ownedFiles.push(stale);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
rmSync(path);
|
|
331
|
+
removed.push(path);
|
|
332
|
+
const ownedDirectory = ownedDirectoryMarkerParent(path);
|
|
333
|
+
if (ownedDirectory) {
|
|
334
|
+
rmSync(ownedDirectory, { recursive: true, force: true });
|
|
335
|
+
removed.push(ownedDirectory);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const now = options.now ?? new Date().toISOString();
|
|
339
|
+
const manifestContent = (updatedAt, completedActions) => ({
|
|
340
|
+
schemaVersion: 1,
|
|
341
|
+
profileVersion: plan.profileVersion,
|
|
342
|
+
profile: plan.profile,
|
|
343
|
+
components: plan.components,
|
|
344
|
+
piboHome: plan.piboHome,
|
|
345
|
+
workspaceRoot: plan.workspaceRoot,
|
|
346
|
+
piboCommand: plan.piboCommand,
|
|
347
|
+
domain: plan.domain,
|
|
348
|
+
installedAt: previous?.installedAt ?? now,
|
|
349
|
+
updatedAt,
|
|
350
|
+
ownedFiles,
|
|
351
|
+
completedActions,
|
|
352
|
+
});
|
|
353
|
+
const comparable = (manifest) => JSON.stringify({ ...manifest, installedAt: "", updatedAt: "", completedActions: [] });
|
|
354
|
+
const previousActions = previous?.completedActions ?? [];
|
|
355
|
+
const unchangedManifest = previous !== undefined && comparable(previous) === comparable(manifestContent(previous.updatedAt, previousActions));
|
|
356
|
+
if (!unchangedManifest)
|
|
357
|
+
atomicWrite(manifestPath, `${JSON.stringify(manifestContent(now, []), null, 2)}\n`, 0o600);
|
|
358
|
+
else if (process.platform !== "win32" && (statSync(manifestPath).mode & 0o777) !== 0o600) {
|
|
359
|
+
chmodSync(manifestPath, 0o600);
|
|
360
|
+
written.push(manifestPath);
|
|
361
|
+
}
|
|
362
|
+
return { manifestPath, written, unchanged, removed, preserved };
|
|
363
|
+
}
|
|
364
|
+
export function readInstallationManifest(path) {
|
|
365
|
+
if (!existsSync(path))
|
|
366
|
+
return undefined;
|
|
367
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
368
|
+
if (parsed.schemaVersion !== 1)
|
|
369
|
+
throw new Error(`Unsupported installation manifest schema: ${String(parsed.schemaVersion)}`);
|
|
370
|
+
parsed.completedActions ??= [];
|
|
371
|
+
parsed.piboCommand ??= "/usr/bin/pibo";
|
|
372
|
+
return parsed;
|
|
373
|
+
}
|
|
374
|
+
export function runPendingInstallationActions(plan, manifestPath, run, options = {}) {
|
|
375
|
+
const manifest = readInstallationManifest(manifestPath);
|
|
376
|
+
if (!manifest)
|
|
377
|
+
throw new Error(`No installation manifest found at ${manifestPath}`);
|
|
378
|
+
const completed = [];
|
|
379
|
+
const skipped = [];
|
|
380
|
+
for (const action of plan.actions) {
|
|
381
|
+
const fingerprint = sha256(JSON.stringify({ command: action.command, checkCommand: action.checkCommand }));
|
|
382
|
+
const recorded = manifest.completedActions.some((entry) => entry.id === action.id && entry.fingerprint === fingerprint);
|
|
383
|
+
if (recorded && (options.isComplete === undefined || options.isComplete(action))) {
|
|
384
|
+
skipped.push(action.id);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
run(action);
|
|
388
|
+
manifest.completedActions = manifest.completedActions.filter((entry) => entry.id !== action.id);
|
|
389
|
+
manifest.completedActions.push({ id: action.id, fingerprint, completedAt: options.now?.() ?? new Date().toISOString() });
|
|
390
|
+
manifest.updatedAt = options.now?.() ?? new Date().toISOString();
|
|
391
|
+
atomicWrite(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 0o600);
|
|
392
|
+
completed.push(action.id);
|
|
393
|
+
}
|
|
394
|
+
return { completed, skipped };
|
|
395
|
+
}
|
|
396
|
+
export function inspectInstallation(options) {
|
|
397
|
+
const manifestPath = outputPath(installationManifestPath(options.piboHome), options.root);
|
|
398
|
+
const manifest = readInstallationManifest(manifestPath);
|
|
399
|
+
if (!manifest)
|
|
400
|
+
return { installed: false, manifestPath, components: [], checks: [{ name: "manifest", status: "fail", detail: "No setup installation manifest found" }], repairCommand: "pibo setup install --profile batteries-included --apply --yes" };
|
|
401
|
+
const manifestModeOk = process.platform === "win32" || (statSync(manifestPath).mode & 0o777) === 0o600;
|
|
402
|
+
const checks = [{ name: "manifest", status: manifestModeOk ? "ok" : "fail", detail: manifestModeOk ? `${manifest.profile} profile version ${manifest.profileVersion}` : `Installation manifest mode is ${(statSync(manifestPath).mode & 0o777).toString(8)}; expected 600` }];
|
|
403
|
+
for (const file of manifest.ownedFiles) {
|
|
404
|
+
const path = outputPath(file.path, options.root);
|
|
405
|
+
if (!existsSync(path))
|
|
406
|
+
checks.push({ name: `file:${file.path}`, status: "fail", detail: "Owned file is missing" });
|
|
407
|
+
else if (sha256(readFileSync(path, "utf8")) !== file.sha256)
|
|
408
|
+
checks.push({ name: `file:${file.path}`, status: "warn", detail: "Owned file was modified; setup will preserve it until explicitly reconciled" });
|
|
409
|
+
else if (process.platform !== "win32" && file.mode !== undefined && (statSync(path).mode & 0o777) !== file.mode)
|
|
410
|
+
checks.push({ name: `file:${file.path}`, status: "fail", detail: `Owned file mode is ${(statSync(path).mode & 0o777).toString(8)}; expected ${file.mode.toString(8)}` });
|
|
411
|
+
else
|
|
412
|
+
checks.push({ name: `file:${file.path}`, status: "ok", detail: "Owned file matches the installation manifest" });
|
|
413
|
+
}
|
|
414
|
+
if (!options.root) {
|
|
415
|
+
const expectedPlan = createInstallationPlan({ profile: manifest.profile, piboHome: manifest.piboHome, workspaceRoot: manifest.workspaceRoot, piboCommand: manifest.piboCommand, domain: manifest.domain, additionalComponents: manifest.components.map((component) => component.name) });
|
|
416
|
+
for (const action of expectedPlan.actions) {
|
|
417
|
+
const fingerprint = sha256(JSON.stringify({ command: action.command, checkCommand: action.checkCommand }));
|
|
418
|
+
const complete = manifest.completedActions.some((entry) => entry.id === action.id && entry.fingerprint === fingerprint);
|
|
419
|
+
checks.push({ name: `action:${action.id}`, status: complete ? "ok" : "fail", detail: complete ? `${action.id} completed with the current plan fingerprint` : `${action.id} is incomplete or stale` });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return { installed: true, manifestPath, profile: manifest.profile, profileVersion: manifest.profileVersion, components: manifest.components, checks, repairCommand: `${manifest.piboCommand} setup upgrade --pibo-home ${shellQuote(manifest.piboHome)} --apply --yes` };
|
|
423
|
+
}
|
|
424
|
+
export function uninstallInstallation(options) {
|
|
425
|
+
const manifestPath = outputPath(installationManifestPath(options.piboHome), options.root);
|
|
426
|
+
const manifest = readInstallationManifest(manifestPath);
|
|
427
|
+
if (!manifest)
|
|
428
|
+
throw new Error(`No installation manifest found at ${manifestPath}`);
|
|
429
|
+
const removed = [];
|
|
430
|
+
const preserved = [];
|
|
431
|
+
for (const file of manifest.ownedFiles) {
|
|
432
|
+
const path = outputPath(file.path, options.root);
|
|
433
|
+
if (!existsSync(path))
|
|
434
|
+
continue;
|
|
435
|
+
if (sha256(readFileSync(path, "utf8")) !== file.sha256) {
|
|
436
|
+
preserved.push(path);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
rmSync(path);
|
|
440
|
+
removed.push(path);
|
|
441
|
+
}
|
|
442
|
+
const ownedDirectoryMarkers = manifest.ownedFiles.filter((file) => ownedDirectoryMarkerParent(file.path) !== undefined);
|
|
443
|
+
for (const marker of ownedDirectoryMarkers) {
|
|
444
|
+
const markerPath = outputPath(marker.path, options.root);
|
|
445
|
+
if (!removed.includes(markerPath))
|
|
446
|
+
continue;
|
|
447
|
+
const ownedDirectory = ownedDirectoryMarkerParent(markerPath);
|
|
448
|
+
rmSync(ownedDirectory, { recursive: true, force: true });
|
|
449
|
+
removed.push(ownedDirectory);
|
|
450
|
+
}
|
|
451
|
+
rmSync(manifestPath);
|
|
452
|
+
removed.push(manifestPath);
|
|
453
|
+
return { removed, preserved, manifestPath };
|
|
454
|
+
}
|
|
455
|
+
export function parseInstallationProfile(value) {
|
|
456
|
+
if (value === "batteries-included" || value === "vanilla")
|
|
457
|
+
return value;
|
|
458
|
+
throw new Error("Profile must be 'batteries-included' or 'vanilla'");
|
|
459
|
+
}
|
|
460
|
+
export function parseInstallationComponent(value) {
|
|
461
|
+
if (value in COMPONENTS)
|
|
462
|
+
return value;
|
|
463
|
+
throw new Error(`Unknown setup component: ${value}`);
|
|
464
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export function deterministicDigest(value) {
|
|
2
|
+
return sha256Hex(new TextEncoder().encode(canonicalJson(value)));
|
|
3
|
+
}
|
|
4
|
+
export function canonicalJson(value) {
|
|
5
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
6
|
+
return JSON.stringify(value);
|
|
7
|
+
if (typeof value === "number")
|
|
8
|
+
return Number.isFinite(value) ? JSON.stringify(value) : "null";
|
|
9
|
+
if (typeof value === "bigint")
|
|
10
|
+
return JSON.stringify(value.toString());
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
13
|
+
if (typeof value !== "object")
|
|
14
|
+
return "null";
|
|
15
|
+
const record = value;
|
|
16
|
+
const entries = Object.keys(record)
|
|
17
|
+
.filter((key) => record[key] !== undefined)
|
|
18
|
+
.sort()
|
|
19
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`);
|
|
20
|
+
return `{${entries.join(",")}}`;
|
|
21
|
+
}
|
|
22
|
+
function sha256Hex(bytes) {
|
|
23
|
+
const bitLength = bytes.length * 8;
|
|
24
|
+
const paddedLength = Math.ceil((bytes.length + 9) / 64) * 64;
|
|
25
|
+
const padded = new Uint8Array(paddedLength);
|
|
26
|
+
padded.set(bytes);
|
|
27
|
+
padded[bytes.length] = 0x80;
|
|
28
|
+
const view = new DataView(padded.buffer);
|
|
29
|
+
const high = Math.floor(bitLength / 0x1_0000_0000);
|
|
30
|
+
const low = bitLength >>> 0;
|
|
31
|
+
view.setUint32(paddedLength - 8, high);
|
|
32
|
+
view.setUint32(paddedLength - 4, low);
|
|
33
|
+
const constants = [];
|
|
34
|
+
const hash = [];
|
|
35
|
+
for (let candidate = 2; constants.length < 64; candidate += 1) {
|
|
36
|
+
let prime = true;
|
|
37
|
+
for (let divisor = 2; divisor * divisor <= candidate; divisor += 1) {
|
|
38
|
+
if (candidate % divisor === 0) {
|
|
39
|
+
prime = false;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!prime)
|
|
44
|
+
continue;
|
|
45
|
+
if (hash.length < 8)
|
|
46
|
+
hash.push(fractionalBits(Math.sqrt(candidate)));
|
|
47
|
+
const cubeRoot = Math.cbrt ? Math.cbrt(candidate) : candidate ** (1 / 3);
|
|
48
|
+
constants.push(fractionalBits(cubeRoot));
|
|
49
|
+
}
|
|
50
|
+
const words = new Uint32Array(64);
|
|
51
|
+
for (let offset = 0; offset < paddedLength; offset += 64) {
|
|
52
|
+
for (let index = 0; index < 16; index += 1)
|
|
53
|
+
words[index] = view.getUint32(offset + index * 4);
|
|
54
|
+
for (let index = 16; index < 64; index += 1) {
|
|
55
|
+
const left = words[index - 15];
|
|
56
|
+
const right = words[index - 2];
|
|
57
|
+
const sigma0 = rotateRight(left, 7) ^ rotateRight(left, 18) ^ (left >>> 3);
|
|
58
|
+
const sigma1 = rotateRight(right, 17) ^ rotateRight(right, 19) ^ (right >>> 10);
|
|
59
|
+
words[index] = (words[index - 16] + sigma0 + words[index - 7] + sigma1) >>> 0;
|
|
60
|
+
}
|
|
61
|
+
let [a, b, c, d, e, f, g, h] = hash;
|
|
62
|
+
for (let index = 0; index < 64; index += 1) {
|
|
63
|
+
const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
|
|
64
|
+
const choice = (e & f) ^ (~e & g);
|
|
65
|
+
const temporary1 = (h + sum1 + choice + constants[index] + words[index]) >>> 0;
|
|
66
|
+
const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
|
|
67
|
+
const majority = (a & b) ^ (a & c) ^ (b & c);
|
|
68
|
+
const temporary2 = (sum0 + majority) >>> 0;
|
|
69
|
+
h = g;
|
|
70
|
+
g = f;
|
|
71
|
+
f = e;
|
|
72
|
+
e = (d + temporary1) >>> 0;
|
|
73
|
+
d = c;
|
|
74
|
+
c = b;
|
|
75
|
+
b = a;
|
|
76
|
+
a = (temporary1 + temporary2) >>> 0;
|
|
77
|
+
}
|
|
78
|
+
hash[0] = (hash[0] + a) >>> 0;
|
|
79
|
+
hash[1] = (hash[1] + b) >>> 0;
|
|
80
|
+
hash[2] = (hash[2] + c) >>> 0;
|
|
81
|
+
hash[3] = (hash[3] + d) >>> 0;
|
|
82
|
+
hash[4] = (hash[4] + e) >>> 0;
|
|
83
|
+
hash[5] = (hash[5] + f) >>> 0;
|
|
84
|
+
hash[6] = (hash[6] + g) >>> 0;
|
|
85
|
+
hash[7] = (hash[7] + h) >>> 0;
|
|
86
|
+
}
|
|
87
|
+
return hash.map((word) => word.toString(16).padStart(8, "0")).join("");
|
|
88
|
+
}
|
|
89
|
+
function fractionalBits(value) {
|
|
90
|
+
return Math.floor((value - Math.floor(value)) * 0x1_0000_0000) >>> 0;
|
|
91
|
+
}
|
|
92
|
+
function rotateRight(value, bits) {
|
|
93
|
+
return (value >>> bits) | (value << (32 - bits));
|
|
94
|
+
}
|
|
@@ -9,6 +9,8 @@ export { patchTraceViewWithEvent, patchTraceViewWithEvents } from "./trace-live-
|
|
|
9
9
|
export { assistantMessageNodeId, dedupeTraceEvents, latestTraceStreamId, messageTurnNodeId, thinkingNodeId, traceEventDedupeKey, } from "./trace-event-projection.js";
|
|
10
10
|
export { traceNodesFromHistoryEntries } from "./trace-history.js";
|
|
11
11
|
export { compareTraceNodes, flattenTraceNodes, mapTraceNodesById, nestTraceNodes, sortTraceNodes, } from "./trace-nodes.js";
|
|
12
|
+
// ── buildTraceViewFromEvents ─────────────────────────────────────
|
|
13
|
+
const SERVICE_TURN_HISTORY_MATCH_MAX_DISTANCE_MS = 5 * 60 * 1_000;
|
|
12
14
|
export function buildTraceViewFromEvents(input) {
|
|
13
15
|
const sessionStatus = input.status ?? "idle";
|
|
14
16
|
const events = dedupeTraceEvents(input.events);
|
|
@@ -23,6 +25,7 @@ export function buildTraceViewFromEvents(input) {
|
|
|
23
25
|
const historyTurnTimings = timingOverflow ? [...suppliedTurnTimings, ...eventTurnTimings] : turnTimings;
|
|
24
26
|
const entries = projectHistoryEntries(allEntries, sessionStatus, openHistoryEventIds, historyTurnTimings, input.historyReconciliationProof, input.historyReconciliationAuthoritative);
|
|
25
27
|
const nodes = traceNodesFromHistoryEntries(input.session.id, entries, historyTurnTimings, input.historyReconciliationProof, input.historyReconciliationAuthoritative);
|
|
28
|
+
suppressServiceTurnHistory(nodes, events);
|
|
26
29
|
reconcileTranscriptUserMessages(nodes, events, turnTimings);
|
|
27
30
|
const byId = mapTraceNodesById(nodes);
|
|
28
31
|
const childByParent = mapTraceChildSessionsByParent(input.sessions ?? []);
|
|
@@ -53,3 +56,54 @@ export function buildTraceViewFromEvents(input) {
|
|
|
53
56
|
: [],
|
|
54
57
|
};
|
|
55
58
|
}
|
|
59
|
+
function suppressServiceTurnHistory(nodes, events) {
|
|
60
|
+
// Native history does not retain the product-level service source. Match the unique nearby
|
|
61
|
+
// user entry, then let the event log provide the canonical prompt-free service turn.
|
|
62
|
+
const hiddenNativeTurnIds = new Set();
|
|
63
|
+
const serviceStarts = events.flatMap((storedEvent) => {
|
|
64
|
+
const event = storedEvent.payload;
|
|
65
|
+
if (event.type !== "message_started" || event.source !== "service" || !event.text.trim())
|
|
66
|
+
return [];
|
|
67
|
+
return [{ text: normalizeTraceText(event.text), createdAt: storedEvent.createdAt, eventId: event.eventId }];
|
|
68
|
+
});
|
|
69
|
+
for (const serviceStart of serviceStarts) {
|
|
70
|
+
const candidates = nodes
|
|
71
|
+
.filter((node) => node.type === "user.message"
|
|
72
|
+
&& (node.source === "transcript" || node.source === "product-history")
|
|
73
|
+
&& normalizeTraceText(traceNodeText(node)) === serviceStart.text
|
|
74
|
+
&& (!node.nativeTurnId || !hiddenNativeTurnIds.has(node.nativeTurnId)))
|
|
75
|
+
.map((node) => ({ node, distance: timestampDistance(node.startedAt, serviceStart.createdAt) }))
|
|
76
|
+
.filter((candidate) => candidate.distance !== undefined && candidate.distance <= SERVICE_TURN_HISTORY_MATCH_MAX_DISTANCE_MS)
|
|
77
|
+
.sort((left, right) => left.distance - right.distance);
|
|
78
|
+
const identityMatch = candidates.find(({ node }) => node.eventId === serviceStart.eventId
|
|
79
|
+
|| node.entryId === serviceStart.eventId
|
|
80
|
+
|| node.nativeTurnId === serviceStart.eventId);
|
|
81
|
+
const matched = identityMatch ?? (candidates.length > 0 && (candidates.length === 1 || candidates[0].distance !== candidates[1].distance)
|
|
82
|
+
? candidates[0]
|
|
83
|
+
: undefined);
|
|
84
|
+
if (matched?.node.nativeTurnId)
|
|
85
|
+
hiddenNativeTurnIds.add(matched.node.nativeTurnId);
|
|
86
|
+
}
|
|
87
|
+
if (hiddenNativeTurnIds.size === 0)
|
|
88
|
+
return;
|
|
89
|
+
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
|
90
|
+
const node = nodes[index];
|
|
91
|
+
if (node.nativeTurnId && hiddenNativeTurnIds.has(node.nativeTurnId))
|
|
92
|
+
nodes.splice(index, 1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function traceNodeText(node) {
|
|
96
|
+
return typeof node.output === "string" ? node.output : typeof node.summary === "string" ? node.summary : "";
|
|
97
|
+
}
|
|
98
|
+
function normalizeTraceText(value) {
|
|
99
|
+
return value.replace(/\s+/g, " ").trim();
|
|
100
|
+
}
|
|
101
|
+
function timestampDistance(left, right) {
|
|
102
|
+
if (!left || !right)
|
|
103
|
+
return undefined;
|
|
104
|
+
const leftMs = Date.parse(left);
|
|
105
|
+
const rightMs = Date.parse(right);
|
|
106
|
+
if (!Number.isFinite(leftMs) || !Number.isFinite(rightMs))
|
|
107
|
+
return undefined;
|
|
108
|
+
return Math.abs(leftMs - rightMs);
|
|
109
|
+
}
|