privateer-agent 0.4.0 → 0.4.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.
|
@@ -139,7 +139,7 @@ function shortCwd(): string {
|
|
|
139
139
|
// - signed in → "connected as <account>"
|
|
140
140
|
// - signed out AND the current model bills to a Privateer account → it can't run
|
|
141
141
|
// until they sign in, so say so plainly (warning)
|
|
142
|
-
// - signed out on their own key → a quiet tease that /
|
|
142
|
+
// - signed out on their own key → a quiet tease that /login adds more
|
|
143
143
|
function accountLine(modelProvider?: string): string {
|
|
144
144
|
const u = priv.currentUser();
|
|
145
145
|
if (u) {
|
|
@@ -147,9 +147,9 @@ function accountLine(modelProvider?: string): string {
|
|
|
147
147
|
return `${GREEN}connected${DIM} as ${RESET}${OCEAN_LIGHT}${label}${RESET}`;
|
|
148
148
|
}
|
|
149
149
|
if (modelProvider === "privateer") {
|
|
150
|
-
return `${YELLOW}not signed in · /
|
|
150
|
+
return `${YELLOW}not signed in · /login to use this model${RESET}`;
|
|
151
151
|
}
|
|
152
|
-
return `${DIM}not signed in · ${OCEAN_LIGHT}/
|
|
152
|
+
return `${DIM}not signed in · ${OCEAN_LIGHT}/login${DIM} to connect your account${RESET}`;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// Is dotted version `a` newer than `b`? Plain numeric compare of major.minor.patch —
|
|
@@ -430,7 +430,7 @@ export default function privateerBrand(pi: any): void {
|
|
|
430
430
|
ctx?.ui?.setTitle?.("Privateer");
|
|
431
431
|
refresh(ctx);
|
|
432
432
|
// No startup notify here: the banner's account line already surfaces the
|
|
433
|
-
// "not signed in · /
|
|
433
|
+
// "not signed in · /login" prompt, so a second line would just be noise.
|
|
434
434
|
});
|
|
435
435
|
|
|
436
436
|
// Keep the header's account line in sync with the picked model (the "this model
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "privateer-agent",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
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",
|
package/src/channels/run.ts
CHANGED
|
@@ -110,6 +110,7 @@ async function main() {
|
|
|
110
110
|
type GateController = import("../ext/permissionGate.ts").GateController;
|
|
111
111
|
const { makePiPrivacyExtension } = await import("pi-privacy");
|
|
112
112
|
const { makeAccountProvider } = await import("../providers/account.ts");
|
|
113
|
+
const { resolveDefaultModel } = await import("../providers/defaultModel.ts");
|
|
113
114
|
const { agentDir, configPath, globalDir } = await import("../config/paths.ts");
|
|
114
115
|
const { redactText, collectSecrets } = await import("../util/redact.ts");
|
|
115
116
|
const { MessagingBridge } = await import("./bridge.ts");
|
|
@@ -130,7 +131,7 @@ async function main() {
|
|
|
130
131
|
process.exit(1);
|
|
131
132
|
}
|
|
132
133
|
const ch = cfg.channels ?? {};
|
|
133
|
-
const defaultModel: string = ch.model ?? cfg.defaultModel
|
|
134
|
+
const defaultModel: string = resolveDefaultModel({ explicit: ch.model ?? cfg.defaultModel });
|
|
134
135
|
const defaultTools: string[] = Array.isArray(ch.tools) && ch.tools.length ? ch.tools : SAFE_TOOLS;
|
|
135
136
|
const defaultPosture: Posture = normalizePosture(ch.posture) ?? "approve";
|
|
136
137
|
const cwd: string = ch.cwd ?? process.cwd();
|
package/src/cli/chat.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// the EngineEvent adapter, and Pi's real session.
|
|
5
5
|
//
|
|
6
6
|
// Run: nvm use && node --env-file=.env --import tsx src/cli/chat.ts
|
|
7
|
-
// Model: PRIVATEER_MODEL=provider/id (default
|
|
7
|
+
// Model: PRIVATEER_MODEL=provider/id (else the account default when signed in, or a
|
|
8
|
+
// BYO-keyed provider — see providers/defaultModel.ts)
|
|
8
9
|
// e.g. tinfoil/llama3-3-70b to watch TEE posture go green.
|
|
9
10
|
|
|
10
11
|
import "../boot.ts"; // env + attestation dispatcher, before any Pi import
|
|
@@ -33,8 +34,11 @@ async function main() {
|
|
|
33
34
|
const priv = await import("../auth/privateer.ts");
|
|
34
35
|
const { makeAccountProvider, accountPosture } = await import("../providers/account.ts");
|
|
35
36
|
const { agentVersion } = await import("../config/version.ts");
|
|
37
|
+
const { resolveDefaultModel } = await import("../providers/defaultModel.ts");
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
// resolveDefaultModel() already honours PRIVATEER_MODEL first, then the account
|
|
40
|
+
// default when signed in, then a BYO key — one source of truth (defaultModel.ts).
|
|
41
|
+
const spec = resolveDefaultModel();
|
|
38
42
|
const slash = spec.indexOf("/");
|
|
39
43
|
const provider = spec.slice(0, slash);
|
|
40
44
|
const modelId = spec.slice(slash + 1);
|
package/src/daemon/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { createEngineEventAdapter } from "../bridge/engineAdapter.ts";
|
|
|
15
15
|
import { makePermissionGate, type GateController } from "../ext/permissionGate.ts";
|
|
16
16
|
import { makePiPrivacyExtension } from "pi-privacy";
|
|
17
17
|
import { makeAccountProvider } from "../providers/account.ts";
|
|
18
|
+
import { resolveDefaultModel } from "../providers/defaultModel.ts";
|
|
18
19
|
import { RelayClient, type TaskSpec } from "../remote/relayClient.ts";
|
|
19
20
|
import { createLiveTaskSession, type LiveTaskHandle } from "../remote/liveTaskSession.ts";
|
|
20
21
|
import { makeRoutinesControl } from "../remote/routinesControl.ts";
|
|
@@ -76,12 +77,14 @@ function loadDaemonConfig(): DaemonConfig {
|
|
|
76
77
|
try {
|
|
77
78
|
const raw = JSON.parse(readFileSync(configPath(), "utf8"));
|
|
78
79
|
return {
|
|
79
|
-
|
|
80
|
+
// config.defaultModel is the explicit choice; absent it, resolve (account default
|
|
81
|
+
// when signed in, else BYO) rather than assuming a BYO OpenRouter key.
|
|
82
|
+
defaultModel: resolveDefaultModel({ explicit: typeof raw.defaultModel === "string" ? raw.defaultModel : undefined }),
|
|
80
83
|
webhooks: raw.webhooks,
|
|
81
84
|
providers: raw.providers,
|
|
82
85
|
};
|
|
83
86
|
} catch {
|
|
84
|
-
return { defaultModel:
|
|
87
|
+
return { defaultModel: resolveDefaultModel() };
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
|
package/src/providers/account.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
notifySignedIn,
|
|
21
21
|
} from "../auth/privateer.ts";
|
|
22
22
|
import { interpretReport, teePosture, tierFromTeePosture, type PrivacyTier } from "pi-privacy";
|
|
23
|
+
import { ACCOUNT_DEFAULT_MODEL_ID, ensurePiDefaultModel } from "./defaultModel.ts";
|
|
23
24
|
|
|
24
25
|
// Seed/fallback catalog: registered synchronously so the account provider has real
|
|
25
26
|
// models the instant it loads (before the live /api/models fetch resolves) — in
|
|
@@ -28,7 +29,7 @@ import { interpretReport, teePosture, tierFromTeePosture, type PrivacyTier } fro
|
|
|
28
29
|
// confidential-compute (TEE, attestable) model — the strongest privacy tier. Also the
|
|
29
30
|
// fallback list if the live listing can't be reached.
|
|
30
31
|
const DEFAULT_MODELS = [
|
|
31
|
-
|
|
32
|
+
ACCOUNT_DEFAULT_MODEL_ID,
|
|
32
33
|
"anthropic/claude-sonnet-4.6",
|
|
33
34
|
"openai/gpt-5.5",
|
|
34
35
|
"deepseek/deepseek-v4-flash",
|
|
@@ -101,6 +102,11 @@ export const privateerOAuthProvider = {
|
|
|
101
102
|
}
|
|
102
103
|
if (cb.signal?.aborted) throw new Error("Login cancelled");
|
|
103
104
|
const creds = await spawnAccountCredentials();
|
|
105
|
+
// Seed Pi's saved model default to the account channel, so the next launch resolves
|
|
106
|
+
// to a billable subscription model instead of falling through to a keyless built-in
|
|
107
|
+
// (the "No API key found for openrouter" trap). No-op if the user already has a
|
|
108
|
+
// chosen default. See providers/defaultModel.ts.
|
|
109
|
+
ensurePiDefaultModel();
|
|
104
110
|
// The fresh path already fired notifySignedIn (pollForToken); fire here for the
|
|
105
111
|
// already-linked path so the header re-renders to "connected" on this terminal too.
|
|
106
112
|
if (wasLinked) notifySignedIn();
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// The single source of truth for "which model do we default to?" — shared by every
|
|
2
|
+
// entry point that has to pick a model when the user hasn't named one: the REPL
|
|
3
|
+
// (cli/chat.ts), the daemon (routines), the channels runner, and the login-time hook
|
|
4
|
+
// that seeds Pi's TUI default (ensurePiDefaultModel).
|
|
5
|
+
//
|
|
6
|
+
// The bug this fixes: each of those sites used to hardcode `openrouter/openai/gpt-4o-
|
|
7
|
+
// mini`, which assumes a BYO OpenRouter key. A user who is ONLY signed into their
|
|
8
|
+
// Privateer subscription has no such key, so the runtime resolved to OpenRouter and
|
|
9
|
+
// then failed at request time with "No API key found for openrouter". Being signed in
|
|
10
|
+
// never nominated a model. resolveDefaultModel() makes the account channel the default
|
|
11
|
+
// the moment credentials exist, and keeps the legacy BYO behaviour otherwise.
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { hasCredentials } from "../auth/privateer.ts";
|
|
16
|
+
import { agentDir } from "../config/paths.ts";
|
|
17
|
+
|
|
18
|
+
// The signed-in default: a NEAR confidential-compute (TEE, attestable) model — the
|
|
19
|
+
// strongest privacy tier, and the same id the app shows first. Kept here as the one
|
|
20
|
+
// definition; providers/account.ts imports it so its seed catalog can't drift.
|
|
21
|
+
export const ACCOUNT_DEFAULT_MODEL_ID = "near/zai-org/GLM-5.1-FP8";
|
|
22
|
+
export const ACCOUNT_DEFAULT_SPEC = `privateer/${ACCOUNT_DEFAULT_MODEL_ID}`;
|
|
23
|
+
|
|
24
|
+
// Last-resort BYO default, preserved from the pre-resolver code so a user who set an
|
|
25
|
+
// OpenRouter key (and isn't signed in) keeps the old behaviour. If they have no key
|
|
26
|
+
// either, this still surfaces the familiar "No API key found for openrouter" — a clear
|
|
27
|
+
// signal to run /login or set a key, which is better than an empty/undefined model.
|
|
28
|
+
export const LEGACY_BYO_FALLBACK = "openrouter/openai/gpt-4o-mini";
|
|
29
|
+
|
|
30
|
+
// BYO providers we can positively detect from the environment, in preference order.
|
|
31
|
+
// Each model id matches Pi's own defaultModelPerProvider so it actually resolves once
|
|
32
|
+
// the key is present. OpenRouter stays on the legacy cheap default for continuity.
|
|
33
|
+
const BYO_BY_KEY: Array<{ env: string; spec: string }> = [
|
|
34
|
+
{ env: "ANTHROPIC_API_KEY", spec: "anthropic/claude-opus-4-8" },
|
|
35
|
+
{ env: "OPENAI_API_KEY", spec: "openai/gpt-5.5" },
|
|
36
|
+
{ env: "OPENROUTER_API_KEY", spec: LEGACY_BYO_FALLBACK },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export interface ResolveDefaultModelOptions {
|
|
40
|
+
// An explicit, user-chosen spec (e.g. config.defaultModel, a channel's `model`).
|
|
41
|
+
// Wins over everything when non-empty — it's a deliberate choice, not a fallback.
|
|
42
|
+
explicit?: string | null;
|
|
43
|
+
// Override for testing / non-process callers. Defaults to process.env.
|
|
44
|
+
env?: NodeJS.ProcessEnv;
|
|
45
|
+
// Override the signed-in check (testing). Defaults to hasCredentials().
|
|
46
|
+
signedIn?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Resolve the model spec ("provider/id") to use when no model is named. Pure and
|
|
50
|
+
// synchronous (only reads env + the credentials file), so it's safe to call from any
|
|
51
|
+
// entry point at startup. Precedence:
|
|
52
|
+
// 1. explicit user choice (config/channel) — deliberate, always wins
|
|
53
|
+
// 2. PRIVATEER_MODEL env — dev/global override
|
|
54
|
+
// 3. signed into Privateer → the account default — the fix: subscription users
|
|
55
|
+
// 4. a BYO provider whose key is present — anthropic, openai, openrouter
|
|
56
|
+
// 5. LEGACY_BYO_FALLBACK — familiar "add a key" signal
|
|
57
|
+
export function resolveDefaultModel(opts: ResolveDefaultModelOptions = {}): string {
|
|
58
|
+
const env = opts.env ?? process.env;
|
|
59
|
+
|
|
60
|
+
const explicit = opts.explicit?.trim();
|
|
61
|
+
if (explicit) return explicit;
|
|
62
|
+
|
|
63
|
+
const fromEnv = env.PRIVATEER_MODEL?.trim();
|
|
64
|
+
if (fromEnv) return fromEnv;
|
|
65
|
+
|
|
66
|
+
const signedIn = opts.signedIn ?? hasCredentials();
|
|
67
|
+
if (signedIn) return ACCOUNT_DEFAULT_SPEC;
|
|
68
|
+
|
|
69
|
+
for (const { env: keyName, spec } of BYO_BY_KEY) {
|
|
70
|
+
if (env[keyName]?.trim()) return spec;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return LEGACY_BYO_FALLBACK;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Split a "provider/id" spec on its first slash (model ids themselves contain "/", so
|
|
77
|
+
// only the first delimiter separates provider from model). Returns null for a spec
|
|
78
|
+
// with no provider prefix.
|
|
79
|
+
function splitSpec(spec: string): { provider: string; modelId: string } | null {
|
|
80
|
+
const slash = spec.indexOf("/");
|
|
81
|
+
if (slash <= 0 || slash === spec.length - 1) return null;
|
|
82
|
+
return { provider: spec.slice(0, slash), modelId: spec.slice(slash + 1) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The TUI consumer. Pi's own model resolution (findInitialModel) checks its saved
|
|
86
|
+
// settings default BEFORE it falls through to a keyless built-in, but nothing ever
|
|
87
|
+
// pointed that default at the account channel — Pi's provider-default table has no
|
|
88
|
+
// `privateer` entry, so a signed-in-only user landed on OpenRouter and errored. On a
|
|
89
|
+
// successful login we seed Pi's global settings.json (agentDir/settings.json — the
|
|
90
|
+
// same file its SettingsManager reads) with the account default, so the NEXT launch
|
|
91
|
+
// resolves cleanly.
|
|
92
|
+
//
|
|
93
|
+
// Guarded: we only write when the user has NOT already chosen a default (no
|
|
94
|
+
// `defaultModel` key), so a deliberate /model choice is never stomped. Best-effort —
|
|
95
|
+
// any read/parse/write failure is swallowed; a missing seed just means the user picks
|
|
96
|
+
// a model once via /model. Returns the spec written, or null if we left it alone.
|
|
97
|
+
export function ensurePiDefaultModel(spec: string = ACCOUNT_DEFAULT_SPEC): string | null {
|
|
98
|
+
const parts = splitSpec(spec);
|
|
99
|
+
if (!parts) return null;
|
|
100
|
+
const settingsPath = join(agentDir(), "settings.json");
|
|
101
|
+
try {
|
|
102
|
+
let settings: Record<string, unknown> = {};
|
|
103
|
+
if (existsSync(settingsPath)) {
|
|
104
|
+
const raw = readFileSync(settingsPath, "utf8").trim();
|
|
105
|
+
if (raw) settings = JSON.parse(raw) as Record<string, unknown>;
|
|
106
|
+
}
|
|
107
|
+
// Respect an existing choice — presence of the key means the user (or Pi) already
|
|
108
|
+
// has a default; don't override it.
|
|
109
|
+
if (typeof settings.defaultModel === "string" && settings.defaultModel.trim()) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
settings.defaultProvider = parts.provider;
|
|
113
|
+
settings.defaultModel = parts.modelId;
|
|
114
|
+
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
115
|
+
return spec;
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|