privateer-agent 0.12.8 → 0.12.10
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 +29 -10
- package/SECURITY.md +41 -5
- package/bin/privateer-launch.mjs +10 -0
- package/bin/privateer-verify.d.mts +10 -0
- package/bin/privateer-verify.mjs +289 -0
- package/extensions/privateer-brand.ts +8 -2
- package/extensions/privateer-hints.ts +8 -2
- package/package.json +25 -25
- package/patches/{@earendil-works+pi-coding-agent+0.80.3.patch → @earendil-works+pi-coding-agent+0.84.1.patch} +138 -70
- package/src/acp/run.ts +5 -4
- package/src/channels/run.ts +3 -1
- package/src/cli/chat.ts +7 -5
- package/src/harbor/index.ts +7 -6
- package/src/providers/account.ts +37 -29
- package/src/providers/piAuthStore.ts +129 -0
- package/src/remote/liveTaskSession.ts +6 -6
- package/src/session.ts +26 -9
package/src/cli/chat.ts
CHANGED
|
@@ -44,8 +44,10 @@ async function main() {
|
|
|
44
44
|
const {
|
|
45
45
|
accountPosture,
|
|
46
46
|
rememberAccountCredential,
|
|
47
|
+
persistAccountCredential,
|
|
47
48
|
dropPersistedAccountCredential,
|
|
48
49
|
} = await import("../providers/account.ts");
|
|
50
|
+
const { modelRegistryOf } = await import("../providers/piAuthStore.ts");
|
|
49
51
|
const { agentVersion } = await import("../config/version.ts");
|
|
50
52
|
const { pickerCatalog, hiddenAccountNotice, hiddenAccountTitleSuffix } = await import("../providers/modelCatalog.ts");
|
|
51
53
|
const { resolveDefaultModel, resolveSignedInModel, savedPiDefaultSpec } = await import("../providers/defaultModel.ts");
|
|
@@ -448,7 +450,7 @@ async function main() {
|
|
|
448
450
|
try { await priv.revokeLocalSessions(); } catch { /* best effort — server TTL is the fallback */ }
|
|
449
451
|
// Ownership-checked: auth.json is machine-global, so removing an entry another
|
|
450
452
|
// running terminal minted would strand it (see providers/account.ts).
|
|
451
|
-
try { dropPersistedAccountCredential(
|
|
453
|
+
try { await dropPersistedAccountCredential(); } catch { /* nothing persisted */ }
|
|
452
454
|
}
|
|
453
455
|
const onSignal = (): void => { void cleanup().finally(() => process.exit(0)); };
|
|
454
456
|
process.once("SIGINT", onSignal);
|
|
@@ -459,14 +461,14 @@ async function main() {
|
|
|
459
461
|
if (provider === "privateer") {
|
|
460
462
|
try {
|
|
461
463
|
const creds = await priv.acquireAccountCredential();
|
|
462
|
-
|
|
464
|
+
await persistAccountCredential(creds);
|
|
463
465
|
rememberAccountCredential(creds); // claim it, so cleanup drops OUR entry and only ours
|
|
464
466
|
} catch (e) {
|
|
465
467
|
console.log(`${RED}Account channel unavailable: ${(e as Error).message}${RESET}`);
|
|
466
468
|
}
|
|
467
469
|
}
|
|
468
470
|
|
|
469
|
-
const model = (services
|
|
471
|
+
const model = (modelRegistryOf(services) as any).find(provider, modelId);
|
|
470
472
|
if (!model) {
|
|
471
473
|
console.log(`${RED}Model ${provider}/${modelId} not found.${RESET} Set PRIVATEER_MODEL=provider/id and check the key is in .env.`);
|
|
472
474
|
rl.close();
|
|
@@ -633,7 +635,7 @@ async function main() {
|
|
|
633
635
|
// is filtered out until the credential is armed).
|
|
634
636
|
const SIGN_IN_HINT = "Run /login.";
|
|
635
637
|
async function modelCatalog() {
|
|
636
|
-
return pickerCatalog(services
|
|
638
|
+
return pickerCatalog(modelRegistryOf(services) as any);
|
|
637
639
|
}
|
|
638
640
|
|
|
639
641
|
// Switch the live session's model in place (history preserved — see
|
|
@@ -644,7 +646,7 @@ async function main() {
|
|
|
644
646
|
const at = sp.indexOf("/");
|
|
645
647
|
if (at < 0) { const m = "Usage: /model provider/id"; console.log(`${RED}${m}${RESET}`); if (remote) relay?.sendNotice(m); return; }
|
|
646
648
|
const p = sp.slice(0, at), id = sp.slice(at + 1);
|
|
647
|
-
const model = (session.modelRegistry as any).find?.(p, id) ?? (services
|
|
649
|
+
const model = (session.modelRegistry as any).find?.(p, id) ?? (modelRegistryOf(services) as any).find?.(p, id);
|
|
648
650
|
if (!model) { const m = `Model ${sp} not found — try /models.`; console.log(`${RED}${m}${RESET}`); if (remote) relay?.sendNotice(m); return; }
|
|
649
651
|
try {
|
|
650
652
|
await session.setModel(model);
|
package/src/harbor/index.ts
CHANGED
|
@@ -17,8 +17,11 @@ import { type GateController } from "../ext/permissionGate.ts";
|
|
|
17
17
|
import { moatResourceOptions } from "../config/moat.ts";
|
|
18
18
|
import {
|
|
19
19
|
rememberAccountCredential,
|
|
20
|
+
persistAccountCredential,
|
|
20
21
|
dropPersistedAccountCredential,
|
|
21
22
|
} from "../providers/account.ts";
|
|
23
|
+
// Pi-free static graph by design — see the header of piAuthStore.ts.
|
|
24
|
+
import { modelRegistryOf } from "../providers/piAuthStore.ts";
|
|
22
25
|
import { resolveDefaultModel } from "../providers/defaultModel.ts";
|
|
23
26
|
import { RelayClient, type TaskSpec } from "../remote/relayClient.ts";
|
|
24
27
|
import { createLiveTaskSession, type LiveTaskHandle } from "../remote/liveTaskSession.ts";
|
|
@@ -849,7 +852,7 @@ export class Harbor {
|
|
|
849
852
|
rmSync(join(agentDir(), "mcp-cache.json"), { force: true });
|
|
850
853
|
const services = await this.buildSessionServices(cwd, []);
|
|
851
854
|
const { provider, modelId } = parseSpec(modelSpec);
|
|
852
|
-
const model = (services
|
|
855
|
+
const model = (modelRegistryOf(services) as any).find(provider, modelId);
|
|
853
856
|
if (!model) return;
|
|
854
857
|
const { session } = await createAgentSessionFromServices({
|
|
855
858
|
services,
|
|
@@ -882,18 +885,16 @@ export class Harbor {
|
|
|
882
885
|
let out = "";
|
|
883
886
|
let status: "ok" | "error" = "ok";
|
|
884
887
|
let error: string | undefined;
|
|
885
|
-
let servicesRef: { authStorage?: { remove?: (p: string) => void; get?: (p: string) => unknown } } | null = null;
|
|
886
888
|
let spawnedAccount = false;
|
|
887
889
|
const { tools: allowedTools, directToolsEnv, notes } = await this.resolveRunTools(spec.tools, spec.cwd, spec.model);
|
|
888
890
|
try {
|
|
889
891
|
const services = await this.buildSessionServices(spec.cwd, directToolsEnv);
|
|
890
|
-
servicesRef = services as any;
|
|
891
892
|
|
|
892
893
|
const { provider, modelId } = parseSpec(spec.model);
|
|
893
894
|
if (provider === "privateer") {
|
|
894
895
|
try {
|
|
895
896
|
const creds = await acquireAccountCredential();
|
|
896
|
-
|
|
897
|
+
await persistAccountCredential(creds);
|
|
897
898
|
rememberAccountCredential(creds); // claim it, so the teardown drops OUR entry only
|
|
898
899
|
spawnedAccount = true;
|
|
899
900
|
} catch (e) {
|
|
@@ -901,7 +902,7 @@ export class Harbor {
|
|
|
901
902
|
}
|
|
902
903
|
}
|
|
903
904
|
|
|
904
|
-
const model = (services
|
|
905
|
+
const model = (modelRegistryOf(services) as any).find(provider, modelId);
|
|
905
906
|
if (!model) {
|
|
906
907
|
status = "error";
|
|
907
908
|
error = `model ${provider}/${modelId} not found`;
|
|
@@ -935,7 +936,7 @@ export class Harbor {
|
|
|
935
936
|
try { await revokeAccountSession(); } catch { /* best effort — server TTL is the fallback */ }
|
|
936
937
|
// Ownership-checked: an interactive terminal on this machine shares auth.json,
|
|
937
938
|
// and its entry must survive a harbor run's teardown (see providers/account.ts).
|
|
938
|
-
try { dropPersistedAccountCredential(
|
|
939
|
+
try { await dropPersistedAccountCredential(); } catch { /* nothing persisted */ }
|
|
939
940
|
}
|
|
940
941
|
}
|
|
941
942
|
return { out, status, error, notes };
|
package/src/providers/account.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { globalDir } from "../config/paths.ts";
|
|
|
28
28
|
import { canOpenBrowser, openInBrowser } from "../util/openBrowser.ts";
|
|
29
29
|
import { interpretReport, teePosture, tierFromTeePosture, type PrivacyTier } from "pi-privacy";
|
|
30
30
|
import { ACCOUNT_DEFAULT_MODEL_ID, ACCOUNT_NEAR_MODEL_ID, ensurePiDefaultModel } from "./defaultModel.ts";
|
|
31
|
+
import { piAuthStore } from "./piAuthStore.ts";
|
|
31
32
|
import {
|
|
32
33
|
sealedEnabled,
|
|
33
34
|
sealedProviderFor,
|
|
@@ -780,16 +781,11 @@ async function accountCredential(): Promise<AccountCredential> {
|
|
|
780
781
|
return slot.inFlight;
|
|
781
782
|
}
|
|
782
783
|
|
|
783
|
-
// `ctx` is Pi's ExtensionContext
|
|
784
|
-
//
|
|
784
|
+
// `ctx` is Pi's ExtensionContext. It used to carry the auth store too
|
|
785
|
+
// (ctx.modelRegistry.authStorage) — pi 0.84 removed it, and the surviving
|
|
786
|
+
// ModelRegistry facade has no credential surface at all, so the store is resolved
|
|
787
|
+
// from piAuthStore() instead and `ctx` is now only ever consulted for UI.
|
|
785
788
|
type SeedContext = {
|
|
786
|
-
modelRegistry?: {
|
|
787
|
-
authStorage?: {
|
|
788
|
-
set?: (provider: string, cred: unknown) => void;
|
|
789
|
-
get?: (provider: string) => unknown;
|
|
790
|
-
remove?: (provider: string) => void;
|
|
791
|
-
};
|
|
792
|
-
};
|
|
793
789
|
model?: { provider?: string; id?: string };
|
|
794
790
|
hasUI?: boolean;
|
|
795
791
|
ui?: { notify?: (message: string, level: string) => void };
|
|
@@ -809,10 +805,12 @@ export async function armAccountCredential(
|
|
|
809
805
|
opts: { notify?: boolean } = {},
|
|
810
806
|
): Promise<boolean> {
|
|
811
807
|
if (!hasCredentials()) return false;
|
|
812
|
-
const store = (ctx as SeedContext)?.modelRegistry?.authStorage;
|
|
813
|
-
if (typeof store?.set !== "function") return false;
|
|
814
808
|
try {
|
|
815
|
-
store
|
|
809
|
+
// `modify` is the store's only write path: it hands us the current entry and
|
|
810
|
+
// takes the replacement back. We ignore the current one — a freshly minted
|
|
811
|
+
// child credential always supersedes whatever is on disk.
|
|
812
|
+
const cred = await accountCredential();
|
|
813
|
+
await (await piAuthStore()).modify("privateer", async () => ({ type: "oauth", ...cred }));
|
|
816
814
|
return true;
|
|
817
815
|
} catch (e) {
|
|
818
816
|
// The account channel is NOT armed: a dead machine login (401 → credentials cleared
|
|
@@ -831,6 +829,17 @@ export async function armAccountCredential(
|
|
|
831
829
|
}
|
|
832
830
|
}
|
|
833
831
|
|
|
832
|
+
// Write an already-acquired account credential into Pi's auth store.
|
|
833
|
+
//
|
|
834
|
+
// The entrypoints that mint their own child credential (chat, acp, harbor, the live
|
|
835
|
+
// task session) used to poke `services.authStorage.set(...)` directly. pi 0.84 removed
|
|
836
|
+
// that property and made the store's only write path an async `modify`, so the poke
|
|
837
|
+
// lives here now — one place that knows the shape, next to the ownership rules that
|
|
838
|
+
// govern the same entry.
|
|
839
|
+
export async function persistAccountCredential(creds: AccountCredential): Promise<void> {
|
|
840
|
+
await (await piAuthStore()).modify("privateer", async () => ({ type: "oauth", ...creds }));
|
|
841
|
+
}
|
|
842
|
+
|
|
834
843
|
// Drop Pi's PERSISTED account credential (the `privateer` entry in auth.json) — but
|
|
835
844
|
// ONLY when it's the one this process minted. Every exit path pairs a session revoke
|
|
836
845
|
// with this drop (the contract in auth/privateer.ts): Pi reuses the persisted
|
|
@@ -849,31 +858,32 @@ export async function armAccountCredential(
|
|
|
849
858
|
// machine's whole token family — every terminal's session included. Those callers have
|
|
850
859
|
// also already wiped the local credentials, so the ownership memo is gone by then and an
|
|
851
860
|
// ownership-checked drop would refuse and strand a dead entry on disk.
|
|
852
|
-
export function dropPersistedAccountCredential(
|
|
853
|
-
|
|
861
|
+
export async function dropPersistedAccountCredential(
|
|
862
|
+
opts: { force?: boolean } = {},
|
|
863
|
+
): Promise<boolean> {
|
|
864
|
+
const store = await piAuthStore();
|
|
854
865
|
const mine = ownedAccountCredential()?.access;
|
|
855
866
|
// Every caller revokes this process's session immediately before calling us, so drop
|
|
856
867
|
// the memo whatever happens to the entry on disk — otherwise a later arm() in this
|
|
857
868
|
// process (a harbor run's session_start, say) could re-persist the revoked token.
|
|
858
869
|
armSlot().cred = undefined;
|
|
859
|
-
if (typeof store?.remove !== "function") return false;
|
|
860
870
|
try {
|
|
861
|
-
if (opts.force) {
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
871
|
+
if (!opts.force) {
|
|
872
|
+
// Not force: read the entry back and only drop it if it is ours. This read is
|
|
873
|
+
// the whole safety property — it is what sees the entry a CONCURRENT terminal
|
|
874
|
+
// wrote, and auth.json is re-read from disk when its revision changes, so this
|
|
875
|
+
// observes other processes rather than a stale in-process copy.
|
|
876
|
+
const persisted = (await store.read("privateer")) as { access?: unknown } | undefined;
|
|
865
877
|
if (!persisted) return false; // nothing persisted — nothing to drop
|
|
866
878
|
// Some other terminal's session. Leave it: it's the key that terminal is using.
|
|
867
879
|
if (typeof persisted.access === "string" && persisted.access !== mine) return false;
|
|
868
|
-
} else if (!mine) {
|
|
869
|
-
// Older Pi with no readable auth store AND we never minted anything — we have no
|
|
870
|
-
// basis to claim the entry, so leave it alone rather than break another terminal.
|
|
871
|
-
return false;
|
|
872
880
|
}
|
|
873
|
-
|
|
881
|
+
// force: the whole token family is revoked, so the entry is dead for every
|
|
882
|
+
// terminal and ownership is irrelevant.
|
|
883
|
+
await store.delete("privateer");
|
|
874
884
|
return true;
|
|
875
885
|
} catch {
|
|
876
|
-
return false; //
|
|
886
|
+
return false; // unwritable store — the server TTL is the fallback
|
|
877
887
|
}
|
|
878
888
|
}
|
|
879
889
|
|
|
@@ -889,12 +899,10 @@ export function dropPersistedAccountCredential(ctx: unknown, opts: { force?: boo
|
|
|
889
899
|
// refresh path (refreshToken), and pre-empting it would mint a second session row.
|
|
890
900
|
export async function ensureAccountArmed(ctx: unknown): Promise<void> {
|
|
891
901
|
if (!hasCredentials()) return;
|
|
892
|
-
const store = (ctx as SeedContext)?.modelRegistry?.authStorage;
|
|
893
|
-
if (typeof store?.get !== "function") return; // can't tell — leave it to session_start
|
|
894
902
|
try {
|
|
895
|
-
if (
|
|
903
|
+
if (await (await piAuthStore()).read("privateer")) return;
|
|
896
904
|
} catch {
|
|
897
|
-
return;
|
|
905
|
+
return; // can't tell — leave it to the next session_start
|
|
898
906
|
}
|
|
899
907
|
await armAccountCredential(ctx, { notify: false });
|
|
900
908
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Access to Pi's credential store and model registry after the 0.84 refactor.
|
|
2
|
+
//
|
|
3
|
+
// pi-coding-agent 0.84 folded credential storage and the model registry into a single
|
|
4
|
+
// `ModelRuntime` and removed both from `AgentSessionServices`. Two things that used to
|
|
5
|
+
// hang off `services` no longer do:
|
|
6
|
+
//
|
|
7
|
+
// services.authStorage -> gone; the runtime owns a private CredentialStore
|
|
8
|
+
// services.modelRegistry -> gone; superseded by services.modelRuntime
|
|
9
|
+
//
|
|
10
|
+
// This module is the single place that knows about that change, so the account channel
|
|
11
|
+
// and the entrypoints don't each grow their own workaround.
|
|
12
|
+
//
|
|
13
|
+
// ⚠️ NOTHING HERE MAY STATICALLY IMPORT PI. This module is pulled in by
|
|
14
|
+
// providers/account.ts, which is itself statically imported by extensions and by the
|
|
15
|
+
// harbor — and whose static graph is deliberately Pi-free, because Pi must not load
|
|
16
|
+
// before boot.ts has installed the env and the dispatcher (the same contract session.ts
|
|
17
|
+
// documents). So the one place a Pi class is genuinely needed loads it dynamically, and
|
|
18
|
+
// everything else is duck-typed off objects Pi already handed us.
|
|
19
|
+
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
|
|
22
|
+
import { agentDir as defaultAgentDir } from "../config/paths.ts";
|
|
23
|
+
|
|
24
|
+
/** The agent dir Pi is actually using — boot pins PI_CODING_AGENT_DIR to ours. */
|
|
25
|
+
function resolveAgentDir(agentDir?: string): string {
|
|
26
|
+
return agentDir ?? process.env.PI_CODING_AGENT_DIR ?? defaultAgentDir();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The subset of Pi's CredentialStore the account channel uses. All async in 0.84. */
|
|
30
|
+
export type PiCredentialStore = {
|
|
31
|
+
read(providerId: string): Promise<unknown>;
|
|
32
|
+
modify(providerId: string, fn: (current: unknown) => Promise<unknown>): Promise<unknown>;
|
|
33
|
+
delete(providerId: string): Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
let cached: { path: string; store: Promise<PiCredentialStore> } | undefined;
|
|
37
|
+
let override: Promise<PiCredentialStore> | undefined;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Pi's credential store for auth.json — the same file, format and class the ModelRuntime
|
|
41
|
+
* uses. `AuthStorage` IS that class: model-runtime.js defaults to
|
|
42
|
+
* `DefaultAuthStorage.create(authPath)`, where DefaultAuthStorage is this exact export,
|
|
43
|
+
* and it already implements the public CredentialStore interface. 0.84 merely stopped
|
|
44
|
+
* re-exporting it, so our pi patch puts the export back — which keeps us on Pi's own
|
|
45
|
+
* store and file format instead of reimplementing either.
|
|
46
|
+
*
|
|
47
|
+
* Why ModelRuntime isn't enough: it exposes login/logout/listCredentials but no way to
|
|
48
|
+
* WRITE an OAuth credential, and pi-ai exports only InMemoryCredentialStore. The account
|
|
49
|
+
* channel must write one, because Pi persists an OAuth credential only for a login IT
|
|
50
|
+
* drove — never for our device-code flow.
|
|
51
|
+
*
|
|
52
|
+
* Holding a second instance over the same path is safe and is not a shortcut: auth.json
|
|
53
|
+
* is machine-global and shared by every terminal on the box, so the store is already
|
|
54
|
+
* built for concurrent access (per-path shared read state, a file-revision check on
|
|
55
|
+
* read, serialized `modify` writes). A second instance in this process is just one more
|
|
56
|
+
* reader/writer, exactly like another terminal — which is the case the ownership check
|
|
57
|
+
* in account.ts exists to handle.
|
|
58
|
+
*/
|
|
59
|
+
export function piAuthStore(agentDir?: string): Promise<PiCredentialStore> {
|
|
60
|
+
if (override) return override;
|
|
61
|
+
const path = join(resolveAgentDir(agentDir), "auth.json");
|
|
62
|
+
if (cached?.path !== path) {
|
|
63
|
+
cached = {
|
|
64
|
+
path,
|
|
65
|
+
store: import("@earendil-works/pi-coding-agent").then((pi) => {
|
|
66
|
+
// `AuthStorage` reaches the index through OUR pi patch (0.84 stopped exporting
|
|
67
|
+
// it), and patches are best-effort — `bin/apply-patches.mjs` degrades to stock
|
|
68
|
+
// pi when it cannot write to node_modules, the `sudo npm i -g` case. This is
|
|
69
|
+
// the one place left that genuinely needs the patched export: only pi's own
|
|
70
|
+
// class knows auth.json's locking, revision checks and file format, and pi's
|
|
71
|
+
// package exports map blocks the deep path it still lives at. So say what
|
|
72
|
+
// happened. Unpatched, the alternative is `undefined.create is not a function`
|
|
73
|
+
// at whatever moment the account channel first writes a credential.
|
|
74
|
+
const ctor = (pi as { AuthStorage?: { create(p: string): PiCredentialStore } }).AuthStorage;
|
|
75
|
+
if (!ctor) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
"pi's AuthStorage export is missing — the privateer patch for " +
|
|
78
|
+
"@earendil-works/pi-coding-agent has not been applied to this install. " +
|
|
79
|
+
"Run `privateer` once with write access to node_modules (a root-owned " +
|
|
80
|
+
"global install needs sudo), or reinstall with a release bundle.",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return ctor.create(path);
|
|
84
|
+
}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return cached.store;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Substitute the store. Tests only.
|
|
92
|
+
*
|
|
93
|
+
* The account channel's ownership rules used to be testable by handing the functions a
|
|
94
|
+
* ctx with a fake store on it; the store is resolved internally now, so this is the seam
|
|
95
|
+
* that replaces it. Pass undefined to restore the real one.
|
|
96
|
+
*/
|
|
97
|
+
export function setPiAuthStoreForTests(store: PiCredentialStore | undefined): void {
|
|
98
|
+
override = store ? Promise.resolve(store) : undefined;
|
|
99
|
+
cached = undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The registry-shaped reads our callers make. Matches the old ModelRegistry facade. */
|
|
103
|
+
export type RegistryLike = {
|
|
104
|
+
find(provider: string, modelId: string): unknown;
|
|
105
|
+
getAll(): readonly unknown[];
|
|
106
|
+
getAvailable(): Promise<readonly unknown[]>;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A registry-shaped view of a session's services.
|
|
111
|
+
*
|
|
112
|
+
* 0.84 kept `ModelRegistry` as a synchronous facade over `ModelRuntime` (that is how
|
|
113
|
+
* extensions still see it) but stopped putting one on `services`. We deliberately do NOT
|
|
114
|
+
* construct one: importing the class would drag Pi into this module's static graph. The
|
|
115
|
+
* runtime is a live object Pi already gave us, so three delegating methods reproduce
|
|
116
|
+
* exactly the surface our callers used — `find` is `getModel` renamed.
|
|
117
|
+
*/
|
|
118
|
+
export function modelRegistryOf(services: { modelRuntime?: unknown } | null | undefined): RegistryLike {
|
|
119
|
+
const runtime = services?.modelRuntime as {
|
|
120
|
+
getModel(p: string, id: string): unknown;
|
|
121
|
+
getModels(): readonly unknown[];
|
|
122
|
+
getAvailable(): Promise<readonly unknown[]>;
|
|
123
|
+
} | undefined;
|
|
124
|
+
return {
|
|
125
|
+
find: (provider, modelId) => runtime?.getModel(provider, modelId),
|
|
126
|
+
getAll: () => runtime?.getModels() ?? [],
|
|
127
|
+
getAvailable: async () => (await runtime?.getAvailable()) ?? [],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -20,7 +20,9 @@ import { agentVersion } from "../config/version.ts";
|
|
|
20
20
|
import { createEngineEventAdapter } from "../bridge/engineAdapter.ts";
|
|
21
21
|
import { isRemoteUnsafeTool, type GateController } from "../ext/permissionGate.ts";
|
|
22
22
|
import { moatResourceOptions } from "../config/moat.ts";
|
|
23
|
-
import { rememberAccountCredential, dropPersistedAccountCredential } from "../providers/account.ts";
|
|
23
|
+
import { rememberAccountCredential, persistAccountCredential, dropPersistedAccountCredential } from "../providers/account.ts";
|
|
24
|
+
// Pi-free static graph by design — see the header of piAuthStore.ts.
|
|
25
|
+
import { modelRegistryOf } from "../providers/piAuthStore.ts";
|
|
24
26
|
import { RelayClient, type TaskSpec } from "./relayClient.ts";
|
|
25
27
|
import { RemoteBridge } from "./remoteBridge.ts";
|
|
26
28
|
import { AttachmentStore, type StoredAttachment } from "../util/attachmentStore.ts";
|
|
@@ -70,7 +72,6 @@ export async function createLiveTaskSession(spec: TaskSpec, deps: LiveTaskDeps):
|
|
|
70
72
|
let initialPromptSent = false;
|
|
71
73
|
let stopped = false;
|
|
72
74
|
let spawnedAccount = false;
|
|
73
|
-
let servicesRef: { authStorage?: { remove?: (p: string) => void; get?: (p: string) => unknown } } | null = null;
|
|
74
75
|
|
|
75
76
|
let attachTimer: ReturnType<typeof setTimeout> | undefined;
|
|
76
77
|
let lifeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -117,7 +118,7 @@ export async function createLiveTaskSession(spec: TaskSpec, deps: LiveTaskDeps):
|
|
|
117
118
|
try { await revokeAccountSession(); } catch { /* server TTL is the fallback */ }
|
|
118
119
|
// Ownership-checked: auth.json is shared machine-wide, so a live task's teardown
|
|
119
120
|
// must not delete an interactive terminal's entry (see providers/account.ts).
|
|
120
|
-
try { dropPersistedAccountCredential(
|
|
121
|
+
try { await dropPersistedAccountCredential(); } catch { /* nothing persisted */ }
|
|
121
122
|
}
|
|
122
123
|
deps.onClosed(termId);
|
|
123
124
|
deps.log(`live task ${termId} closed`);
|
|
@@ -218,13 +219,12 @@ export async function createLiveTaskSession(spec: TaskSpec, deps: LiveTaskDeps):
|
|
|
218
219
|
})) as any),
|
|
219
220
|
},
|
|
220
221
|
});
|
|
221
|
-
servicesRef = services as any;
|
|
222
222
|
|
|
223
223
|
const { provider, modelId } = deps.parseSpec(modelSpec);
|
|
224
224
|
if (provider === "privateer") {
|
|
225
225
|
try {
|
|
226
226
|
const creds = await spawnAccountCredentials();
|
|
227
|
-
|
|
227
|
+
await persistAccountCredential(creds);
|
|
228
228
|
rememberAccountCredential(creds); // claim it, so stop() drops OUR entry only
|
|
229
229
|
spawnedAccount = true;
|
|
230
230
|
} catch (e) {
|
|
@@ -236,7 +236,7 @@ export async function createLiveTaskSession(spec: TaskSpec, deps: LiveTaskDeps):
|
|
|
236
236
|
// just spawned + closes the relay), or a throw would leak an orphaned account "device"
|
|
237
237
|
// until its token TTL. Everything post-account-spawn runs under one guard.
|
|
238
238
|
try {
|
|
239
|
-
const model = (services
|
|
239
|
+
const model = (modelRegistryOf(services) as any).find(provider, modelId);
|
|
240
240
|
if (!model) {
|
|
241
241
|
throw new Error(`model ${provider}/${modelId} not found`);
|
|
242
242
|
}
|
package/src/session.ts
CHANGED
|
@@ -16,8 +16,18 @@ import {
|
|
|
16
16
|
SessionManager,
|
|
17
17
|
SettingsManager,
|
|
18
18
|
ModelRegistry,
|
|
19
|
-
|
|
19
|
+
ModelRuntime,
|
|
20
20
|
} from "@earendil-works/pi-coding-agent";
|
|
21
|
+
// From pi-ai, deliberately, not pi-coding-agent's `AuthStorage.inMemory()`. 0.84
|
|
22
|
+
// stopped exporting AuthStorage and our pi patch puts the export back — which is
|
|
23
|
+
// fine for the one caller that genuinely needs the file-backed store
|
|
24
|
+
// (providers/piAuthStore.ts) and wrong here, because patches are best-effort by
|
|
25
|
+
// design: `bin/apply-patches.mjs` degrades to stock pi when it cannot write to
|
|
26
|
+
// node_modules (the `sudo npm i -g` case it calls out). A static import of a
|
|
27
|
+
// patched-in export turns that graceful degradation into a hard ERR_IMPORT crash
|
|
28
|
+
// on every session start. pi-ai exports this store unpatched and it implements the
|
|
29
|
+
// same CredentialStore interface, so nothing about the session changes.
|
|
30
|
+
import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
|
|
21
31
|
|
|
22
32
|
import { agentDir as defaultAgentDir } from "./config/paths.ts";
|
|
23
33
|
import { createEngineEventAdapter } from "./bridge/engineAdapter.ts";
|
|
@@ -46,15 +56,23 @@ export interface PrivateerSession {
|
|
|
46
56
|
export async function createSession(opts: CreateSessionOptions): Promise<PrivateerSession> {
|
|
47
57
|
const AGENT_DIR = opts.agentDir ?? process.env.PI_CODING_AGENT_DIR ?? defaultAgentDir();
|
|
48
58
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
// pi 0.84: ModelRegistry.create(authStorage, modelsPath) is gone. The registry is now
|
|
60
|
+
// a synchronous facade over a ModelRuntime, and the runtime is what owns credentials —
|
|
61
|
+
// so the store this session used to hand to the registry is injected into the runtime
|
|
62
|
+
// instead. Still in-memory: a headless session must not touch the machine's auth.json.
|
|
63
|
+
const authStorage = new InMemoryCredentialStore();
|
|
64
|
+
const modelRuntime = await ModelRuntime.create({
|
|
65
|
+
credentials: authStorage,
|
|
66
|
+
modelsPath: `${AGENT_DIR}/models.json`,
|
|
67
|
+
});
|
|
68
|
+
const modelRegistry = new ModelRegistry(modelRuntime);
|
|
69
|
+
// Explicit, and awaited: find() below is synchronous, so models.json has to be loaded
|
|
70
|
+
// before it runs or the lookup fails on a cold cache.
|
|
71
|
+
await modelRegistry.refresh();
|
|
54
72
|
|
|
55
73
|
const model = modelRegistry.find(opts.provider, opts.modelId);
|
|
56
74
|
if (!model) {
|
|
57
|
-
const ids = (
|
|
75
|
+
const ids = (modelRegistry.getAll() ?? []).map(
|
|
58
76
|
(m: any) => `${m.provider}/${m.id}`,
|
|
59
77
|
);
|
|
60
78
|
throw new Error(
|
|
@@ -75,8 +93,7 @@ export async function createSession(opts: CreateSessionOptions): Promise<Private
|
|
|
75
93
|
cwd: opts.cwd,
|
|
76
94
|
agentDir: AGENT_DIR,
|
|
77
95
|
model,
|
|
78
|
-
|
|
79
|
-
modelRegistry,
|
|
96
|
+
modelRuntime,
|
|
80
97
|
settingsManager,
|
|
81
98
|
sessionManager: SessionManager.inMemory(opts.cwd),
|
|
82
99
|
resourceLoader: loader,
|