char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Somauth: error out instead of running random commandsd76f269

main
3.9 KiB111 linesraw
1// A CredentialStore over pi's auth.json, locked the way pi locks it, so berry and pi share logins and
2// never double-refresh a rotating oauth token.
3
4import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
5import { homedir } from "node:os";
6import { dirname, join } from "node:path";
7import lockfile from "proper-lockfile";
8import type { Credential, CredentialStore } from "@earendil-works/pi-ai";
9
10type AuthData = Record<string, Credential>;
11
12export const PI_AUTH_PATH = join(homedir(), ".pi", "agent", "auth.json");
13
14/** pi's config values: commands are rejected; `$VAR`/`${VAR}` interpolate, `$$` and `$!` escape. */
15export function resolveConfigValue(
16  config: string,
17  env?: Record<string, string>,
18): string | undefined {
19  if (config.startsWith("!")) {
20    throw new Error(
21      "Command-based API keys are disabled; use a literal key or an environment variable.",
22    );
23  }
24  let resolved = "";
25  for (
26    const [text, braced, bare] of config.matchAll(
27      /\$[$!]|\$\{([A-Za-z_]\w*)\}|\$([A-Za-z_]\w*)|[^$]+|\$/g,
28    )
29  ) {
30    const name = braced ?? bare;
31    if (name === undefined) {
32      resolved += text.length === 2 && text[0] === "$" ? text[1] : text;
33      continue;
34    }
35    const value = env?.[name] || Deno.env.get(name);
36    if (!value) return undefined;
37    resolved += value;
38  }
39  return resolved;
40}
41
42export function piAuthStore(path = PI_AUTH_PATH): CredentialStore {
43  async function locked<T>(
44    use: (
45      data: AuthData,
46    ) => { result: T; next?: AuthData } | Promise<{ result: T; next?: AuthData }>,
47    signal?: AbortSignal,
48  ): Promise<T> {
49    signal?.throwIfAborted();
50    await mkdir(dirname(path), { recursive: true, mode: 0o700 });
51    await writeFile(path, "{}", { mode: 0o600, flag: "wx" }).catch((error) => {
52      if (error.code !== "EEXIST") throw error;
53    });
54    const release = await lockfile.lock(path, {
55      realpath: false,
56      stale: 30_000,
57      retries: { retries: 30, factor: 1.5, minTimeout: 10, maxTimeout: 1_000 },
58    });
59    try {
60      signal?.throwIfAborted();
61      const text = (await readFile(path, "utf8")).replace(/^\uFEFF/, "");
62      const { result, next } = await use(text.trim() ? JSON.parse(text) : {});
63      signal?.throwIfAborted();
64      if (next) await writeFile(path, JSON.stringify(next, null, 2), { mode: 0o600 });
65      return result;
66    } finally {
67      await release().catch(() => {});
68    }
69  }
70
71  // reads take the lock only when the file changed, as pi's do: listing models reads every provider at once
72  let cached: { revision: string; data: Promise<AuthData> } | undefined;
73  async function current(signal?: AbortSignal): Promise<AuthData> {
74    const info = await stat(path).catch(() => undefined);
75    const revision = info ? `${info.mtimeMs}:${info.size}` : "absent";
76    if (cached?.revision !== revision) {
77      const data = locked((data) => ({ result: data }), signal);
78      cached = { revision, data };
79      data.catch(() => cached?.data === data && (cached = undefined));
80    }
81    return await cached.data;
82  }
83
84  return {
85    read: async (provider, options) => {
86      const credential = (await current(options?.signal))[provider];
87      if (credential?.type !== "api_key" || credential.key === undefined) return credential;
88      return { ...credential, key: resolveConfigValue(credential.key, credential.env) };
89    },
90
91    list: async (options) =>
92      Object.entries(await current(options?.signal)).map(([providerId, { type }]) => ({
93        providerId,
94        type,
95      })),
96
97    modify: (provider, change, options) =>
98      locked(async (data) => {
99        const next = await change(data[provider]);
100        return next === undefined
101          ? { result: data[provider] }
102          : { result: next, next: { ...data, [provider]: next } };
103      }, options?.signal),
104
105    delete: (provider, options) =>
106      locked((data) => {
107        const { [provider]: _, ...rest } = data;
108        return { result: undefined, next: rest };
109      }, options?.signal),
110  };
111}