privateer-agent 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +167 -442
- package/bin/privateer-tui +86 -0
- package/bin/privateer.mjs +17 -6
- package/bin/pv +28 -0
- package/package.json +26 -23
- package/src/auth/privateer.ts +57 -0
- package/src/boot.ts +43 -0
- package/src/bridge/engineAdapter.ts +182 -0
- package/src/cli/chat.ts +243 -0
- package/src/config/paths.ts +24 -46
- package/src/config/permissionMode.ts +5 -0
- package/src/crypto/outboxSeal.ts +61 -0
- package/src/daemon/index.ts +204 -121
- package/src/daemon/ipc.ts +1 -1
- package/src/engine/errors.ts +85 -45
- package/src/engine/router.ts +11 -165
- package/src/ext/permissionGate.ts +216 -0
- package/src/main.ts +32 -0
- package/src/permissions/classify.ts +172 -0
- package/src/permissions/gate.ts +11 -14
- package/src/permissions/mode.ts +6 -2
- package/src/permissions/{uiGate.ts → modeGate.ts} +18 -3
- package/src/providers/account.ts +170 -0
- package/src/providers/catalog.ts +77 -61
- package/src/providers/genModelsJson.ts +97 -0
- package/src/remote/relayClient.ts +15 -0
- package/src/remote/remoteBridge.ts +152 -0
- package/src/routines/delivery.ts +118 -9
- package/src/routines/schema.ts +24 -5
- package/src/routines/store.ts +44 -1
- package/src/routines/toolSelect.ts +3 -1
- package/src/session.ts +81 -273
- package/src/tools/routine.ts +120 -101
- package/src/tools/saveAttachment.ts +39 -42
- package/src/tools/sendFile.ts +75 -0
- package/src/util/attachmentStore.ts +18 -35
- package/src/util/redact.ts +33 -3
- package/LICENSE +0 -21
- package/src/agents/loader.ts +0 -49
- package/src/commands/custom.ts +0 -123
- package/src/commands/registry.ts +0 -618
- package/src/components/AgentGroupView.tsx +0 -104
- package/src/components/App.tsx +0 -1626
- package/src/components/ApprovalPrompt.tsx +0 -49
- package/src/components/Banner.tsx +0 -78
- package/src/components/Markdown.tsx +0 -183
- package/src/components/ModeHint.tsx +0 -40
- package/src/components/ModelPicker.tsx +0 -302
- package/src/components/Onboarding.tsx +0 -203
- package/src/components/OptionPicker.tsx +0 -134
- package/src/components/PlanConfirm.tsx +0 -37
- package/src/components/PrivateerLogin.tsx +0 -109
- package/src/components/PromptInput.tsx +0 -602
- package/src/components/RewindPicker.tsx +0 -69
- package/src/components/Root.tsx +0 -116
- package/src/components/SessionPicker.tsx +0 -64
- package/src/components/StatusBar.tsx +0 -131
- package/src/components/TodoPanel.tsx +0 -36
- package/src/components/ToolCallView.tsx +0 -113
- package/src/components/Transcript.tsx +0 -210
- package/src/components/figures.ts +0 -14
- package/src/components/promptModel.ts +0 -73
- package/src/components/spinnerVerbs.ts +0 -46
- package/src/components/theme.ts +0 -57
- package/src/components/types.ts +0 -34
- package/src/components/useTeeShield.ts +0 -104
- package/src/components/useTerminalWidth.ts +0 -24
- package/src/components/useZdrShield.ts +0 -126
- package/src/config/load.ts +0 -115
- package/src/config/schema.ts +0 -94
- package/src/context/outputStyles.ts +0 -42
- package/src/context/projectInfo.ts +0 -59
- package/src/context/systemPrompt.ts +0 -176
- package/src/engine/QueryEngine.ts +0 -399
- package/src/hooks/engine.ts +0 -155
- package/src/main.tsx +0 -209
- package/src/mcp/client.ts +0 -251
- package/src/mcp/oauth.ts +0 -245
- package/src/memory/auto.ts +0 -146
- package/src/memory/checkpoints.ts +0 -227
- package/src/memory/store.ts +0 -127
- package/src/providers/attestation.ts +0 -149
- package/src/providers/capabilities.ts +0 -104
- package/src/providers/models.ts +0 -183
- package/src/providers/registry.ts +0 -71
- package/src/providers/resolve.ts +0 -78
- package/src/skills/installer.ts +0 -222
- package/src/skills/loader.ts +0 -88
- package/src/tools/askUser.ts +0 -92
- package/src/tools/bash.ts +0 -98
- package/src/tools/context.ts +0 -128
- package/src/tools/edit.ts +0 -67
- package/src/tools/exec.ts +0 -60
- package/src/tools/glob.ts +0 -39
- package/src/tools/grep.ts +0 -86
- package/src/tools/index.ts +0 -83
- package/src/tools/memory.ts +0 -53
- package/src/tools/processRegistry.ts +0 -77
- package/src/tools/read.ts +0 -42
- package/src/tools/sendFileToClient.ts +0 -55
- package/src/tools/skill.ts +0 -44
- package/src/tools/task.ts +0 -52
- package/src/tools/todo.ts +0 -36
- package/src/tools/todoStore.ts +0 -31
- package/src/tools/walk.ts +0 -44
- package/src/tools/web.ts +0 -145
- package/src/tools/worktree.ts +0 -145
- package/src/tools/write.ts +0 -40
- package/src/util/images.ts +0 -378
- package/src/util/limit.ts +0 -32
- package/src/version.ts +0 -13
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Launch Pi's full interactive TUI with the Privateer moat + tool packs. The moat is
|
|
3
|
+
# installed as re-export SHIMS in the agent dir's extensions/, so BOTH this TUI and
|
|
4
|
+
# any subagents it spawns (which run as child `pi` processes reading the agent dir)
|
|
5
|
+
# load the identical set — including our permission gate. So subagent tool calls are
|
|
6
|
+
# gated too (the gate runs bypass-within-restricted-tools headlessly; danger.ts still
|
|
7
|
+
# blocks destructive shell). One source of truth via discovery — no -e (which would
|
|
8
|
+
# double-load vs discovery).
|
|
9
|
+
#
|
|
10
|
+
# Runs in the current directory. Model via PRIVATEER_MODEL=provider/id.
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
# Resolve through symlinks so REPO points at the real repo, not the symlink dir.
|
|
13
|
+
SOURCE="${BASH_SOURCE[0]}"
|
|
14
|
+
while [ -h "$SOURCE" ]; do
|
|
15
|
+
DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
|
16
|
+
SOURCE="$(readlink "$SOURCE")"
|
|
17
|
+
[ "${SOURCE#/}" = "$SOURCE" ] && SOURCE="$DIR/$SOURCE"
|
|
18
|
+
done
|
|
19
|
+
REPO="$(cd -P "$(dirname "$SOURCE")/.." && pwd)"
|
|
20
|
+
|
|
21
|
+
pick_node() {
|
|
22
|
+
if command -v node >/dev/null 2>&1 \
|
|
23
|
+
&& node -e 'process.exit((+process.versions.node.split(".")[0]) >= 22 ? 0 : 1)' 2>/dev/null; then
|
|
24
|
+
command -v node; return
|
|
25
|
+
fi
|
|
26
|
+
for c in "$HOME"/.nvm/versions/node/v22*/bin/node; do
|
|
27
|
+
[ -x "$c" ] && { echo "$c"; return; }
|
|
28
|
+
done
|
|
29
|
+
echo "node"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
AGENT_DIR="${PRIVATEER_HOME:-$HOME/.privateer}/agent"
|
|
33
|
+
EXT_DIR="$AGENT_DIR/extensions"
|
|
34
|
+
mkdir -p "$EXT_DIR"
|
|
35
|
+
|
|
36
|
+
# Install/refresh the moat + tool-pack shims. Each shim re-exports its target by
|
|
37
|
+
# ABSOLUTE path so the target's own relative imports resolve from the repo (a plain
|
|
38
|
+
# symlink would resolve them relative to the shim's location and break). We remove
|
|
39
|
+
# any shim we previously managed first, so a dropped package can't linger and reload.
|
|
40
|
+
MANAGED="privateer-brand privateer-gate privateer-account privateer-posture privateer-tools privateer-privacy pi-privacy pi-web-access rpiv-web-tools pi-mcp-adapter pi-hypa pi-subagents"
|
|
41
|
+
for name in $MANAGED; do rm -f "$EXT_DIR/$name.ts"; done
|
|
42
|
+
shim() { printf 'export { default } from "%s";\n' "$2" > "$EXT_DIR/$1.ts"; }
|
|
43
|
+
# Branding + the account sign-in surface (banner, ⚓ badge, /signin /signout).
|
|
44
|
+
shim privateer-brand "$REPO/extensions/privateer-brand.ts"
|
|
45
|
+
shim privateer-gate "$REPO/extensions/privateer-gate.ts"
|
|
46
|
+
shim privateer-account "$REPO/extensions/privateer-account.ts"
|
|
47
|
+
shim privateer-posture "$REPO/extensions/privateer-posture.ts"
|
|
48
|
+
shim privateer-tools "$REPO/extensions/privateer-tools.ts"
|
|
49
|
+
# pi-privacy wrapped with the account-channel tier resolver (privateer/near… = TEE),
|
|
50
|
+
# so it replaces loading pi-privacy's default entry.
|
|
51
|
+
shim privateer-privacy "$REPO/extensions/privateer-privacy.ts"
|
|
52
|
+
# Privacy-aligned web tools: pluggable backends (SearXNG self-hosted = private; no
|
|
53
|
+
# WebView). Pick the backend with /web-tools, or set SEARXNG_URL. Replaces
|
|
54
|
+
# pi-web-access (opened a WebView + defaulted to Exa, a 3rd-party search).
|
|
55
|
+
shim rpiv-web-tools "$REPO/node_modules/@juicesharp/rpiv-web-tools/index.ts"
|
|
56
|
+
shim pi-mcp-adapter "$REPO/node_modules/pi-mcp-adapter/index.ts"
|
|
57
|
+
shim pi-hypa "$REPO/node_modules/@hypabolic/pi-hypa/extensions/index.ts"
|
|
58
|
+
shim pi-subagents "$REPO/node_modules/pi-subagents/src/extension/index.ts"
|
|
59
|
+
|
|
60
|
+
CLI="$REPO/node_modules/@earendil-works/pi-coding-agent/dist/cli.js"
|
|
61
|
+
export PI_CODING_AGENT_DIR="$AGENT_DIR"
|
|
62
|
+
NODE_BIN="$(pick_node)"
|
|
63
|
+
|
|
64
|
+
# Quiet Pi's built-in startup resource listing ([Extensions]/[Context]/[Skills]/…) by
|
|
65
|
+
# default — our banner is the intended startup surface, and the extension list is noise.
|
|
66
|
+
# Only set it when unset, so a later `--verbose` or a user's own settings toggle sticks.
|
|
67
|
+
"$NODE_BIN" -e '
|
|
68
|
+
const fs = require("fs"), p = process.argv[1];
|
|
69
|
+
let s = {}; try { s = JSON.parse(fs.readFileSync(p, "utf8")); } catch {}
|
|
70
|
+
if (s.quietStartup === undefined) { s.quietStartup = true; fs.writeFileSync(p, JSON.stringify(s, null, 2) + "\n"); }
|
|
71
|
+
' "$AGENT_DIR/settings.json" 2>/dev/null || true
|
|
72
|
+
|
|
73
|
+
# Default model: when signed in to a Privateer account, default to GLM 5.1 on the
|
|
74
|
+
# account's NEAR confidential-compute (TEE) channel — attestable, strongest privacy
|
|
75
|
+
# tier. Otherwise (BYO key, no account) fall back to a cheap OpenRouter model. An
|
|
76
|
+
# explicit PRIVATEER_MODEL always wins.
|
|
77
|
+
CRED="${PRIVATEER_HOME:-$HOME/.privateer}/credentials.json"
|
|
78
|
+
if [ -n "${PRIVATEER_MODEL:-}" ]; then
|
|
79
|
+
MODEL="$PRIVATEER_MODEL"
|
|
80
|
+
elif [ -f "$CRED" ]; then
|
|
81
|
+
MODEL="privateer/near/zai-org/GLM-5.1-FP8"
|
|
82
|
+
else
|
|
83
|
+
MODEL="openrouter/openai/gpt-4o-mini"
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
exec "$NODE_BIN" --env-file="$REPO/.env" "$CLI" --model "$MODEL" "$@"
|
package/bin/privateer.mjs
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// Launcher for the privateer 0.3 agent REPL. It runs in the DIRECTORY YOU INVOKE
|
|
3
|
+
// IT FROM (process.cwd() — that's the agent's working dir), while resolving the
|
|
4
|
+
// code and dev keys from the repo. Prefer the `bin/pv` wrapper, which also picks a
|
|
5
|
+
// Node >= 22 (the Pi stack's floor).
|
|
6
|
+
import { register } from "tsx/esm/api";
|
|
4
7
|
import { fileURLToPath } from "node:url";
|
|
5
8
|
import { dirname, resolve } from "node:path";
|
|
6
|
-
import { register } from "tsx/esm/api";
|
|
7
9
|
|
|
8
|
-
const
|
|
10
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const repo = resolve(here, "..");
|
|
12
|
+
|
|
13
|
+
// Dev convenience: load provider keys from the repo's .env if present, WITHOUT
|
|
14
|
+
// changing cwd. (A real install would rely on env vars / ~/.privateer instead.)
|
|
15
|
+
try {
|
|
16
|
+
process.loadEnvFile(resolve(repo, ".env"));
|
|
17
|
+
} catch {
|
|
18
|
+
/* no .env — fall back to the ambient environment */
|
|
19
|
+
}
|
|
9
20
|
|
|
10
|
-
register();
|
|
11
|
-
await import(resolve(
|
|
21
|
+
register(); // resolves the repo's tsx regardless of the invocation cwd
|
|
22
|
+
await import(resolve(repo, "src/cli/chat.ts"));
|
package/bin/pv
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Launch the privateer 0.3 dev agent in the CURRENT directory. Picks a Node >= 22
|
|
3
|
+
# (the Pi stack's floor): the active node if new enough, else an nvm-installed v22.
|
|
4
|
+
# Symlink this onto your PATH under whatever name you like (it does NOT have to be
|
|
5
|
+
# `pv`), e.g.: ln -s "$PWD/bin/pv" /usr/local/bin/pv
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
# Resolve through symlinks so REPO points at the real repo, not the symlink dir.
|
|
8
|
+
SOURCE="${BASH_SOURCE[0]}"
|
|
9
|
+
while [ -h "$SOURCE" ]; do
|
|
10
|
+
DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
|
11
|
+
SOURCE="$(readlink "$SOURCE")"
|
|
12
|
+
[ "${SOURCE#/}" = "$SOURCE" ] && SOURCE="$DIR/$SOURCE"
|
|
13
|
+
done
|
|
14
|
+
REPO="$(cd -P "$(dirname "$SOURCE")/.." && pwd)"
|
|
15
|
+
|
|
16
|
+
pick_node() {
|
|
17
|
+
if command -v node >/dev/null 2>&1 \
|
|
18
|
+
&& node -e 'process.exit((+process.versions.node.split(".")[0]) >= 22 ? 0 : 1)' 2>/dev/null; then
|
|
19
|
+
command -v node
|
|
20
|
+
return
|
|
21
|
+
fi
|
|
22
|
+
for c in "$HOME"/.nvm/versions/node/v22*/bin/node; do
|
|
23
|
+
[ -x "$c" ] && { echo "$c"; return; }
|
|
24
|
+
done
|
|
25
|
+
echo "node" # last resort — errors clearly if it's too old
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
exec "$(pick_node)" "$REPO/bin/privateer.mjs" "$@"
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "privateer-agent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Privateer — a provider-agnostic terminal coding agent. Bring your own model
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Privateer — a provider-agnostic, safe-by-default terminal coding agent with TEE/Tinfoil attestation, rebuilt on the Pi toolkit. Bring your own model across 20 providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Patrick (zahnno)",
|
|
@@ -15,15 +15,17 @@
|
|
|
15
15
|
"cli",
|
|
16
16
|
"tui",
|
|
17
17
|
"terminal",
|
|
18
|
+
"pi",
|
|
19
|
+
"tee",
|
|
20
|
+
"confidential-computing",
|
|
21
|
+
"attestation",
|
|
22
|
+
"tinfoil",
|
|
18
23
|
"openrouter",
|
|
19
24
|
"anthropic",
|
|
20
25
|
"openai",
|
|
21
26
|
"ollama",
|
|
22
27
|
"nearai",
|
|
23
|
-
"
|
|
24
|
-
"confidential-computing",
|
|
25
|
-
"ai-sdk",
|
|
26
|
-
"ink"
|
|
28
|
+
"venice"
|
|
27
29
|
],
|
|
28
30
|
"bin": {
|
|
29
31
|
"privateer": "./bin/privateer.mjs"
|
|
@@ -38,37 +40,38 @@
|
|
|
38
40
|
"LICENSE"
|
|
39
41
|
],
|
|
40
42
|
"scripts": {
|
|
41
|
-
"start": "tsx src/main.
|
|
42
|
-
"
|
|
43
|
+
"start": "tsx src/main.ts",
|
|
44
|
+
"chat": "node --env-file=.env --import tsx src/cli/chat.ts",
|
|
45
|
+
"dev": "tsx watch src/main.ts",
|
|
43
46
|
"typecheck": "tsc --noEmit",
|
|
44
47
|
"test": "for f in tests/*.test.ts; do node --import tsx --test \"$f\" || exit 1; done"
|
|
45
48
|
},
|
|
46
49
|
"engines": {
|
|
47
|
-
"node": ">=
|
|
50
|
+
"node": ">=22.19.0"
|
|
48
51
|
},
|
|
49
52
|
"dependencies": {
|
|
50
|
-
"@
|
|
51
|
-
"@
|
|
52
|
-
"@
|
|
53
|
-
"@
|
|
54
|
-
"@
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
53
|
+
"@earendil-works/pi-ai": "^0.80.3",
|
|
54
|
+
"@earendil-works/pi-coding-agent": "^0.80.3",
|
|
55
|
+
"@earendil-works/pi-tui": "^0.80.3",
|
|
56
|
+
"@hypabolic/pi-hypa": "^0.1.6",
|
|
57
|
+
"@juicesharp/rpiv-web-tools": "^1.20.0",
|
|
58
|
+
"@noble/ciphers": "^2.1.1",
|
|
59
|
+
"@noble/curves": "^1.9.7",
|
|
60
|
+
"@noble/hashes": "^1.7.1",
|
|
61
|
+
"pi-mcp-adapter": "^2.11.0",
|
|
62
|
+
"pi-privacy": "^0.3.0",
|
|
63
|
+
"pi-subagents": "^0.34.0",
|
|
61
64
|
"picomatch": "^4.0.4",
|
|
62
|
-
"react": "^18.3.1",
|
|
63
65
|
"tsx": "^4.16.0",
|
|
66
|
+
"typebox": "^1.3.4",
|
|
67
|
+
"undici": "^7.28.0",
|
|
64
68
|
"ws": "^8.21.0",
|
|
65
69
|
"zod": "^4.0.16"
|
|
66
70
|
},
|
|
67
71
|
"devDependencies": {
|
|
68
72
|
"@types/node": "^20.14.0",
|
|
69
|
-
"@types/
|
|
73
|
+
"@types/picomatch": "^4.0.3",
|
|
70
74
|
"@types/ws": "^8.18.1",
|
|
71
|
-
"ink-testing-library": "^4.0.0",
|
|
72
75
|
"typescript": "^5.5.0"
|
|
73
76
|
}
|
|
74
77
|
}
|
package/src/auth/privateer.ts
CHANGED
|
@@ -448,6 +448,63 @@ export async function logout(): Promise<void> {
|
|
|
448
448
|
clearCredentials();
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
+
// ── Account provider (Pi OAuth) credential helpers ───────────────────────────
|
|
452
|
+
// The `privateer` account inference provider registers with Pi via the OAuth path
|
|
453
|
+
// (ProviderConfigInput.oauth). Pi treats a credential as { access, refresh, expires }
|
|
454
|
+
// and refreshes when Date.now() >= expires. We map a per-terminal CHILD session onto
|
|
455
|
+
// that shape: `access` = child access JWT, `refresh` = child refresh token, `expires`
|
|
456
|
+
// = the JWT's exp (so Pi refreshes just before the server would reject it). These are
|
|
457
|
+
// independent of authedFetch's in-memory _child (Pi owns this credential's lifecycle).
|
|
458
|
+
|
|
459
|
+
interface AccountCredential {
|
|
460
|
+
access: string;
|
|
461
|
+
refresh: string;
|
|
462
|
+
expires: number; // ms epoch
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Decode a JWT's `exp` (seconds) → ms epoch minus a safety skew, or a short fallback
|
|
466
|
+
// so an undecodable token still gets refreshed frequently rather than never.
|
|
467
|
+
function jwtExpMs(token: string, fallbackMs = 5 * 60_000): number {
|
|
468
|
+
try {
|
|
469
|
+
const seg = token.split(".")[1];
|
|
470
|
+
const payload = JSON.parse(Buffer.from(seg, "base64").toString("utf8")) as { exp?: unknown };
|
|
471
|
+
if (typeof payload.exp === "number") return payload.exp * 1000 - 30_000; // 30s skew
|
|
472
|
+
} catch {
|
|
473
|
+
/* not a decodable JWT */
|
|
474
|
+
}
|
|
475
|
+
return Date.now() + fallbackMs;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Spawn a fresh child session (from the parent machine login) as an account credential.
|
|
479
|
+
export async function spawnAccountCredentials(): Promise<AccountCredential> {
|
|
480
|
+
const parent = loadCredentials();
|
|
481
|
+
if (!parent) throw new Error("Not logged in to Privateer. Run /login.");
|
|
482
|
+
const res = await postJson(
|
|
483
|
+
serverBaseUrl(),
|
|
484
|
+
"/auth/session/spawn",
|
|
485
|
+
{ refreshToken: parent.refreshToken, deviceLabel: defaultDeviceLabel() },
|
|
486
|
+
{ headers: { Authorization: `Bearer ${parent.accessToken}` } },
|
|
487
|
+
);
|
|
488
|
+
if (!res.ok) {
|
|
489
|
+
if (res.status === 401) {
|
|
490
|
+
clearCredentials();
|
|
491
|
+
notifySessionExpired();
|
|
492
|
+
}
|
|
493
|
+
throw new Error("Your Privateer session expired. Run /login to sign in again.");
|
|
494
|
+
}
|
|
495
|
+
const { accessToken, refreshToken } = (await res.json()) as { accessToken: string; refreshToken: string };
|
|
496
|
+
return { access: accessToken, refresh: refreshToken, expires: jwtExpMs(accessToken) };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Rotate this account credential's own refresh token; caller falls back to a fresh
|
|
500
|
+
// spawn if this throws (expired/reused child token).
|
|
501
|
+
export async function refreshAccountCredentials(refresh: string): Promise<AccountCredential> {
|
|
502
|
+
const res = await postJson(serverBaseUrl(), "/auth/refresh", { refreshToken: refresh });
|
|
503
|
+
if (!res.ok) throw new Error(`account refresh failed (${res.status})`);
|
|
504
|
+
const { accessToken, refreshToken } = (await res.json()) as { accessToken: string; refreshToken: string };
|
|
505
|
+
return { access: accessToken, refresh: refreshToken, expires: jwtExpMs(accessToken) };
|
|
506
|
+
}
|
|
507
|
+
|
|
451
508
|
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
452
509
|
return new Promise((resolve, reject) => {
|
|
453
510
|
const t = setTimeout(resolve, ms);
|
package/src/boot.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// BOOT — the first module loaded, before any Pi module exists in the graph.
|
|
2
|
+
//
|
|
3
|
+
// Two load-bearing side effects, both of which MUST happen before Pi is
|
|
4
|
+
// imported/evaluated (see docs/pi-migration-plan.md §0 and §Appendix A.3):
|
|
5
|
+
//
|
|
6
|
+
// 1. Pin PI_CODING_AGENT_DIR to $PRIVATEER_HOME/agent. Many Pi internals
|
|
7
|
+
// (settings-manager, auth-storage, model-registry, migrations, sdk) call
|
|
8
|
+
// getAgentDir() directly at import/eval time and IGNORE the agentDir option,
|
|
9
|
+
// so the env var — not the option — is the real lever. Set it here, once.
|
|
10
|
+
//
|
|
11
|
+
// 2. Install the process-wide undici attestation dispatcher, so Pi's provider
|
|
12
|
+
// TLS handshakes flow through a connector we can inspect (the TEE/Tinfoil
|
|
13
|
+
// SPKI capture). Pi's extension hooks can't reach the TLS layer; this can.
|
|
14
|
+
//
|
|
15
|
+
// ORDERING CONTRACT: this module imports ONLY node builtins + our own paths and
|
|
16
|
+
// dispatcher modules. It must NEVER import anything from @earendil-works/pi-*.
|
|
17
|
+
// Entrypoints do `import "./boot.ts";` and then DYNAMICALLY import all
|
|
18
|
+
// Pi-touching code, so these side effects are guaranteed to run first regardless
|
|
19
|
+
// of ESM hoisting. Keep this file dependency-light and import-order-safe.
|
|
20
|
+
|
|
21
|
+
import { agentDir } from "./config/paths.ts";
|
|
22
|
+
// The attestation dispatcher now lives in the pi-privacy package (the publishable
|
|
23
|
+
// moat). boot still installs it pre-Pi as belt-and-suspenders; the package's
|
|
24
|
+
// extension would also install it at extension-init (idempotent — same module
|
|
25
|
+
// instance, so the captured-cert map is shared).
|
|
26
|
+
import { installAttestationDispatcher } from "pi-privacy/attest";
|
|
27
|
+
|
|
28
|
+
// (1) Pin Pi's agent dir under the privateer home. `??=` so an explicit
|
|
29
|
+
// PI_CODING_AGENT_DIR from the environment still wins (tests, power users).
|
|
30
|
+
//
|
|
31
|
+
// NOTE: confirm the literal env var name against the installed Pi constant —
|
|
32
|
+
// ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`, which is
|
|
33
|
+
// "PI_CODING_AGENT_DIR" for APP_NAME="pi". Pinned here as a constant so a Pi
|
|
34
|
+
// rename surfaces as one edit, not a silent config-home split.
|
|
35
|
+
export const PI_AGENT_DIR_ENV = "PI_CODING_AGENT_DIR";
|
|
36
|
+
process.env[PI_AGENT_DIR_ENV] ??= agentDir();
|
|
37
|
+
|
|
38
|
+
// (2) Install the attestation dispatcher (idempotent).
|
|
39
|
+
installAttestationDispatcher();
|
|
40
|
+
|
|
41
|
+
// Marker other modules can assert on to catch an accidental "imported Pi before
|
|
42
|
+
// boot" regression during development.
|
|
43
|
+
export const BOOTED = true;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// The load-bearing bridge: translate Pi's session.subscribe() events into the
|
|
2
|
+
// privateer `EngineEvent` vocabulary that RelayClient.sendEvent() already speaks.
|
|
3
|
+
// This is the ENTIRE coupling between Pi and the preserved connection layer — the
|
|
4
|
+
// relay/app see EngineEvents and never learn the loop underneath changed.
|
|
5
|
+
//
|
|
6
|
+
// Promoted from ../../ pi-spike/adapter.mjs (spike-B proven) and HARDENED per
|
|
7
|
+
// Phase 1 of docs/pi-migration-plan.md:
|
|
8
|
+
// - stateful so `usage` carries BOTH the cumulative session total and this
|
|
9
|
+
// turn's delta (the real EngineEvent shape, unlike the stateless spike),
|
|
10
|
+
// - `finish` carries usage + finishReason,
|
|
11
|
+
// - compaction / auto-retry / agent-end-error mapped through.
|
|
12
|
+
//
|
|
13
|
+
// Field names for the compaction/retry/error events are best-effort against Pi
|
|
14
|
+
// 0.80 and marked TODO(verify) — Phase 1's test (`tests/engine.test.ts`, ported
|
|
15
|
+
// from tree-cli) asserts the mapping against a real event stream and pins them.
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
addUsage,
|
|
19
|
+
emptyUsage,
|
|
20
|
+
type EngineEvent,
|
|
21
|
+
type UsageTotals,
|
|
22
|
+
} from "../engine/events.ts";
|
|
23
|
+
|
|
24
|
+
// Minimal structural view of the Pi session events we read. Kept loose (not the
|
|
25
|
+
// full Pi union) so this module type-checks without pinning to Pi's internals;
|
|
26
|
+
// the runtime shape is what the spike observed.
|
|
27
|
+
export interface PiSessionEvent {
|
|
28
|
+
type: string;
|
|
29
|
+
[k: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface PiUsage {
|
|
33
|
+
input?: number;
|
|
34
|
+
output?: number;
|
|
35
|
+
cacheRead?: number;
|
|
36
|
+
totalTokens?: number;
|
|
37
|
+
cost?: { total?: number };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normUsage(u: PiUsage | undefined): UsageTotals {
|
|
41
|
+
const input = u?.input ?? 0;
|
|
42
|
+
const output = u?.output ?? 0;
|
|
43
|
+
return {
|
|
44
|
+
inputTokens: input,
|
|
45
|
+
outputTokens: output,
|
|
46
|
+
cachedInputTokens: u?.cacheRead ?? 0,
|
|
47
|
+
totalTokens: u?.totalTokens ?? input + output,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function textOf(result: unknown): string {
|
|
52
|
+
if (typeof result === "string") return result;
|
|
53
|
+
const parts = (result as any)?.content;
|
|
54
|
+
if (Array.isArray(parts)) return parts.map((p: any) => p.text ?? `[${p.type}]`).join("");
|
|
55
|
+
try {
|
|
56
|
+
return JSON.stringify(result);
|
|
57
|
+
} catch {
|
|
58
|
+
return String(result);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function num(v: unknown, fallback = 0): number {
|
|
63
|
+
return typeof v === "number" && Number.isFinite(v) ? v : fallback;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Create a stateful adapter. One instance per session (it accumulates the
|
|
67
|
+
// running usage total). `toEngineEvents` returns zero-or-more EngineEvents for
|
|
68
|
+
// each Pi event; feed each through in `session.subscribe`.
|
|
69
|
+
export function createEngineEventAdapter() {
|
|
70
|
+
let sessionTotal: UsageTotals = emptyUsage();
|
|
71
|
+
|
|
72
|
+
function toEngineEvents(ev: PiSessionEvent): EngineEvent[] {
|
|
73
|
+
switch (ev.type) {
|
|
74
|
+
case "message_update": {
|
|
75
|
+
const a = ev.assistantMessageEvent as any;
|
|
76
|
+
if (a?.type === "text_delta") return [{ type: "text", text: a.delta }];
|
|
77
|
+
if (a?.type === "thinking_delta") return [{ type: "reasoning", text: a.delta }];
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case "tool_execution_start":
|
|
82
|
+
return [
|
|
83
|
+
{
|
|
84
|
+
type: "tool-call",
|
|
85
|
+
id: ev.toolCallId as string,
|
|
86
|
+
name: ev.toolName as string,
|
|
87
|
+
input: ev.args,
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
case "tool_execution_end":
|
|
92
|
+
return ev.isError
|
|
93
|
+
? [
|
|
94
|
+
{
|
|
95
|
+
type: "tool-error",
|
|
96
|
+
id: ev.toolCallId as string,
|
|
97
|
+
name: ev.toolName as string,
|
|
98
|
+
error: textOf(ev.result),
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
: [
|
|
102
|
+
{
|
|
103
|
+
type: "tool-result",
|
|
104
|
+
id: ev.toolCallId as string,
|
|
105
|
+
name: ev.toolName as string,
|
|
106
|
+
output: textOf(ev.result),
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
case "turn_end": {
|
|
111
|
+
const turn = normUsage((ev.message as any)?.usage);
|
|
112
|
+
sessionTotal = addUsage(sessionTotal, turn);
|
|
113
|
+
const finishReason =
|
|
114
|
+
(ev.finishReason as string) ??
|
|
115
|
+
((ev.message as any)?.stopReason as string) ??
|
|
116
|
+
"stop";
|
|
117
|
+
return [
|
|
118
|
+
{ type: "usage", usage: sessionTotal, turn },
|
|
119
|
+
{ type: "finish", usage: sessionTotal, finishReason },
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Compaction: Pi collapses history to free context. TODO(verify) field
|
|
124
|
+
// names for before/after token counts against a real compaction event.
|
|
125
|
+
case "compaction_start":
|
|
126
|
+
case "compaction_end":
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
type: "compacted",
|
|
130
|
+
before: num(ev.beforeTokens ?? (ev as any).before),
|
|
131
|
+
after: num(ev.afterTokens ?? (ev as any).after),
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
// Automatic transient-failure retry before any output streamed.
|
|
136
|
+
// TODO(verify) Pi's auto-retry event name + fields.
|
|
137
|
+
case "auto_retry":
|
|
138
|
+
case "auto_retry_start":
|
|
139
|
+
return [
|
|
140
|
+
{
|
|
141
|
+
type: "retrying",
|
|
142
|
+
attempt: num(ev.attempt, 1),
|
|
143
|
+
max: num(ev.max ?? (ev as any).maxAttempts, 1),
|
|
144
|
+
delayMs: num(ev.delayMs ?? (ev as any).delay, 0),
|
|
145
|
+
reason: String(ev.reason ?? (ev as any).error ?? "transient failure"),
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
// Terminal error surfaced at the end of an agent run. TODO(verify) shape;
|
|
150
|
+
// Phase 1 re-points errors/errors.ts (describeError) here.
|
|
151
|
+
case "agent_end": {
|
|
152
|
+
const err = (ev as any).error;
|
|
153
|
+
if (!err) return [];
|
|
154
|
+
return [
|
|
155
|
+
{
|
|
156
|
+
type: "error",
|
|
157
|
+
error: typeof err === "string" ? err : (err.message ?? String(err)),
|
|
158
|
+
retryable: Boolean((ev as any).retryable),
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
case "abort":
|
|
164
|
+
case "aborted":
|
|
165
|
+
return [{ type: "aborted" }];
|
|
166
|
+
|
|
167
|
+
default:
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
toEngineEvents,
|
|
174
|
+
// Current running total, for callers that want to snapshot session usage
|
|
175
|
+
// outside the event stream.
|
|
176
|
+
get sessionUsage(): UsageTotals {
|
|
177
|
+
return sessionTotal;
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type EngineEventAdapter = ReturnType<typeof createEngineEventAdapter>;
|