@bivy/bivy 0.0.0 → 0.1.0-staging.2
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/LICENSE +105 -0
- package/README.md +265 -5
- package/bin/acp-shim.mjs +298 -0
- package/bin/agent-manifest.json +277 -0
- package/bin/bivy.mjs +4100 -0
- package/bin/codex-app-server-shim.mjs +447 -0
- package/bin/patch-pi-dependencies.mjs +44 -0
- package/bin/prune-sessions.mjs +52 -0
- package/bin/sessions-list.mjs +27 -0
- package/bin/shim-path.mjs +126 -0
- package/bin/uninstall-paths.mjs +48 -0
- package/dist/approval.js +87 -0
- package/dist/attach.js +248 -0
- package/dist/auth.js +258 -0
- package/dist/bivy-login.js +180 -0
- package/dist/browser-open.js +50 -0
- package/dist/control-plane-tasks.js +236 -0
- package/dist/data-dir.js +25 -0
- package/dist/device-registry.js +201 -0
- package/dist/e2e.js +70 -0
- package/dist/ephemeral-exec.js +109 -0
- package/dist/exec.js +209 -0
- package/dist/git-auth.js +155 -0
- package/dist/github-app-auth.js +107 -0
- package/dist/github-app-connect.js +235 -0
- package/dist/github-app-manifest.js +82 -0
- package/dist/github-app-sync-cli.js +93 -0
- package/dist/github-app-vault.js +106 -0
- package/dist/github-apps.js +121 -0
- package/dist/github-connect-repo.js +74 -0
- package/dist/github-device-auth.js +109 -0
- package/dist/github-tasks.js +650 -0
- package/dist/guard.js +109 -0
- package/dist/harness/cache-evict.js +88 -0
- package/dist/harness/checkpoint.js +0 -0
- package/dist/harness/cow-clone.js +84 -0
- package/dist/harness/dep-cache.js +78 -0
- package/dist/harness/disk-admission.js +46 -0
- package/dist/harness/egress.js +30 -0
- package/dist/harness/manager.js +97 -0
- package/dist/harness/mcp-config-formats.js +164 -0
- package/dist/harness/mcp-config.js +111 -0
- package/dist/harness/mcp-inject.js +134 -0
- package/dist/harness/mcp-proxy-cli.js +88 -0
- package/dist/harness/mcp-proxy.js +150 -0
- package/dist/harness/net-proxy.js +120 -0
- package/dist/harness/sandbox.js +96 -0
- package/dist/history-sync.js +26 -0
- package/dist/hosted-endpoints.d.mts +14 -0
- package/dist/hosted-endpoints.mjs +35 -0
- package/dist/identity.js +153 -0
- package/dist/integrations/index.js +4 -0
- package/dist/integrations/manager.js +279 -0
- package/dist/integrations/oauth.js +78 -0
- package/dist/integrations/registry.js +239 -0
- package/dist/integrations/store.js +54 -0
- package/dist/integrations/types.js +1 -0
- package/dist/linear-tasks.js +49 -0
- package/dist/metadata.js +226 -0
- package/dist/multiplexer.js +79 -0
- package/dist/native-pi.js +38 -0
- package/dist/node-stats.js +237 -0
- package/dist/pairing-crypto.js +105 -0
- package/dist/policy/conditions.js +103 -0
- package/dist/policy/policy-engine.js +20 -0
- package/dist/policy/risk.js +18 -0
- package/dist/policy/ruleset.js +113 -0
- package/dist/policy/run-policy.js +108 -0
- package/dist/policy/session-reroute.js +96 -0
- package/dist/pty-runner.py +95 -0
- package/dist/question.js +146 -0
- package/dist/redact.js +97 -0
- package/dist/relay-attach.js +345 -0
- package/dist/relay-chunk.js +73 -0
- package/dist/relay-cli-crypto.js +70 -0
- package/dist/relay-client.js +344 -0
- package/dist/relay-setup.js +262 -0
- package/dist/repo-workspace.js +208 -0
- package/dist/runtime/adoption.js +45 -0
- package/dist/runtime/agent-service-bin.js +149 -0
- package/dist/runtime/agent-service.js +439 -0
- package/dist/runtime/ansi.js +27 -0
- package/dist/runtime/anthropic-preflight.js +80 -0
- package/dist/runtime/claude-code.js +1364 -0
- package/dist/runtime/cli-parsers.js +647 -0
- package/dist/runtime/codex-auth.js +168 -0
- package/dist/runtime/codex-preflight.js +60 -0
- package/dist/runtime/codex-sessions.js +229 -0
- package/dist/runtime/control-plane-location.js +74 -0
- package/dist/runtime/credential-ingest.js +122 -0
- package/dist/runtime/credential-provisioning.js +79 -0
- package/dist/runtime/credential-store.js +435 -0
- package/dist/runtime/credentials.js +153 -0
- package/dist/runtime/host.js +153 -0
- package/dist/runtime/index.js +1548 -0
- package/dist/runtime/local-model-store.js +194 -0
- package/dist/runtime/location-registry.js +28 -0
- package/dist/runtime/model-catalog.js +97 -0
- package/dist/runtime/model-namer.js +85 -0
- package/dist/runtime/native-process-scan.js +102 -0
- package/dist/runtime/native-session-discovery.js +103 -0
- package/dist/runtime/normalize.js +75 -0
- package/dist/runtime/oauth/model-oauth-providers.js +75 -0
- package/dist/runtime/oauth/model-oauth.js +324 -0
- package/dist/runtime/opencode-preflight.js +55 -0
- package/dist/runtime/pi-auth.js +82 -0
- package/dist/runtime/pi-oauth.js +52 -0
- package/dist/runtime/pi-session-discovery.js +42 -0
- package/dist/runtime/pi.js +518 -0
- package/dist/runtime/process.js +499 -0
- package/dist/runtime/protocol.js +630 -0
- package/dist/runtime/remote.js +541 -0
- package/dist/runtime/rpc-protocol.js +56 -0
- package/dist/runtime/ruleset-store.js +117 -0
- package/dist/runtime/session-location.js +50 -0
- package/dist/runtime/types.js +17 -0
- package/dist/secrets-cli.js +134 -0
- package/dist/secrets.js +264 -0
- package/dist/server.js +9411 -0
- package/dist/session/bivy-session.js +1 -0
- package/dist/session/checkpoint-pack.js +133 -0
- package/dist/session/event-log.js +340 -0
- package/dist/session/fork-dirty.js +73 -0
- package/dist/session/fork-prereqs.js +61 -0
- package/dist/session/fork.js +57 -0
- package/dist/session/native-import.js +56 -0
- package/dist/session/reconnect.js +168 -0
- package/dist/session/replication-service.js +236 -0
- package/dist/session/replication.js +106 -0
- package/dist/session/replicator.js +140 -0
- package/dist/session/session-new-dedupe.js +42 -0
- package/dist/session/sibling-client.js +201 -0
- package/dist/session/transcript-merge.js +131 -0
- package/dist/session/transcript-normal.js +130 -0
- package/dist/session/workspace-context.js +1 -0
- package/dist/session-event-coalescer.js +50 -0
- package/dist/session-identity.js +34 -0
- package/dist/session-ref.js +65 -0
- package/dist/stt-cli.js +131 -0
- package/dist/stt.js +168 -0
- package/dist/terminal.js +409 -0
- package/dist/wire-format.js +67 -0
- package/dist/worktree-provision.js +118 -0
- package/dist/worktree.js +117 -0
- package/package.json +40 -6
- package/public/qr.js +464 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// SPDX-License-Identifier: FSL-1.1-ALv2
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
// Codex credential materialization.
|
|
4
|
+
//
|
|
5
|
+
// A ChatGPT/Codex subscription a user connects *inside Bivy* lands in the shared
|
|
6
|
+
// node vault as an `openai-codex` OAuth record. The Codex CLI can't
|
|
7
|
+
// read that vault — it authenticates from `OPENAI_API_KEY` or its own
|
|
8
|
+
// `$CODEX_HOME/auth.json` (the file `codex login` writes). This module bridges the
|
|
9
|
+
// gap: it mints the file Codex expects from the vault record, so a subscription
|
|
10
|
+
// connected in Bivy "just works" for Codex with no separate `codex login`.
|
|
11
|
+
//
|
|
12
|
+
// Why it's sound: Pi's `openai-codex` OAuth app IS the Codex CLI's own OAuth app
|
|
13
|
+
// (identical client_id, token endpoint, and scopes — verified against the codex
|
|
14
|
+
// binary), so tokens minted through Bivy are accepted by Codex's backend. The
|
|
15
|
+
// vault stores {access, refresh, expires, accountId} but NOT the `id_token`
|
|
16
|
+
// Codex's auth.json requires; we recover it with a `refresh_token` grant. The
|
|
17
|
+
// original grant used `scope=openid`, so the refresh response carries a fresh
|
|
18
|
+
// `id_token` — the same call Codex itself makes to refresh.
|
|
19
|
+
//
|
|
20
|
+
// Rotation note: OpenAI rotates the refresh token on every grant, so we persist
|
|
21
|
+
// the rotated token back to the vault. We mint only when no auth.json exists yet
|
|
22
|
+
// (Codex then owns and self-refreshes it), which keeps churn to a single grant.
|
|
23
|
+
// The residual edge case — Codex self-refreshing later invalidates the vault's
|
|
24
|
+
// copy for *other* `openai-codex` consumers — is documented; for the common case
|
|
25
|
+
// (the subscription was connected for Codex) there are no other consumers.
|
|
26
|
+
import fs from "node:fs";
|
|
27
|
+
import os from "node:os";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import { createCredentialVault } from "./credential-store.js";
|
|
30
|
+
// Codex's own OAuth app — verified identical to the app Pi's `openai-codex` login
|
|
31
|
+
// uses, so a refresh grant minted here is honored by Codex's backend.
|
|
32
|
+
const CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
33
|
+
const CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
34
|
+
/** Resolve Codex's home dir exactly as the CLI (and our rollout reader) does. */
|
|
35
|
+
export function resolveCodexHome() {
|
|
36
|
+
return process.env.CODEX_HOME?.trim() || path.join(os.homedir(), ".codex");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Pre-trust a workspace in Codex's `config.toml` so a `bivy run codex` launched
|
|
40
|
+
* there doesn't stall on the first-run "Do you trust the contents of this
|
|
41
|
+
* directory?" gate — which, unanswered, blocks Codex before it writes a rollout
|
|
42
|
+
* (and so blocks "continue as chat" takeover, which resumes by that rollout id).
|
|
43
|
+
*
|
|
44
|
+
* We only ADD a missing `[projects."<path>"]` trust entry; we never rewrite or
|
|
45
|
+
* downgrade an existing one (a user may have deliberately set a different level).
|
|
46
|
+
* Best-effort and idempotent: any failure just leaves the interactive prompt in
|
|
47
|
+
* place, exactly as before. Mirrors what choosing "Yes, continue" would persist.
|
|
48
|
+
*/
|
|
49
|
+
export function ensureCodexTrusted(workspace) {
|
|
50
|
+
const dir = workspace?.trim();
|
|
51
|
+
if (!dir)
|
|
52
|
+
return;
|
|
53
|
+
try {
|
|
54
|
+
const codexHome = resolveCodexHome();
|
|
55
|
+
const configFile = path.join(codexHome, "config.toml");
|
|
56
|
+
let existing = "";
|
|
57
|
+
try {
|
|
58
|
+
existing = fs.readFileSync(configFile, "utf8");
|
|
59
|
+
}
|
|
60
|
+
catch { /* no config yet — we'll create one */ }
|
|
61
|
+
// TOML basic-string key: escape backslashes and double quotes.
|
|
62
|
+
const escaped = dir.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
63
|
+
const header = `[projects."${escaped}"]`;
|
|
64
|
+
// Already has a table header for this exact path → leave its trust_level be.
|
|
65
|
+
if (existing.split(/\r?\n/).some((line) => line.trim() === header))
|
|
66
|
+
return;
|
|
67
|
+
const prefix = existing.length && !existing.endsWith("\n") ? "\n" : "";
|
|
68
|
+
const block = `${prefix}\n${header}\ntrust_level = "trusted"\n`;
|
|
69
|
+
fs.mkdirSync(codexHome, { recursive: true, mode: 0o700 });
|
|
70
|
+
fs.appendFileSync(configFile, block);
|
|
71
|
+
}
|
|
72
|
+
catch { /* best effort — an untrusted prompt is a soft failure, not a hard one */ }
|
|
73
|
+
}
|
|
74
|
+
/** Exchange a refresh token for a fresh token set (incl. the `id_token`). */
|
|
75
|
+
async function refreshCodexTokens(refreshToken) {
|
|
76
|
+
let res;
|
|
77
|
+
try {
|
|
78
|
+
res = await fetch(CODEX_TOKEN_URL, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
81
|
+
body: new URLSearchParams({
|
|
82
|
+
grant_type: "refresh_token",
|
|
83
|
+
refresh_token: refreshToken,
|
|
84
|
+
client_id: CODEX_OAUTH_CLIENT_ID,
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
if (!res.ok)
|
|
92
|
+
return undefined;
|
|
93
|
+
const j = (await res.json().catch(() => ({})));
|
|
94
|
+
const idToken = typeof j.id_token === "string" ? j.id_token : "";
|
|
95
|
+
const accessToken = typeof j.access_token === "string" ? j.access_token : "";
|
|
96
|
+
const refreshTok = typeof j.refresh_token === "string" ? j.refresh_token : "";
|
|
97
|
+
const expiresIn = typeof j.expires_in === "number" ? j.expires_in : 0;
|
|
98
|
+
if (!idToken || !accessToken || !refreshTok)
|
|
99
|
+
return undefined;
|
|
100
|
+
return { idToken, accessToken, refreshToken: refreshTok, expiresIn };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Ensure the Codex CLI has a usable credential file, minting one from Bivy's
|
|
104
|
+
* vault when needed. Returns the resolved `CODEX_HOME` (so the caller can pin it
|
|
105
|
+
* on the subprocess env) or `undefined` when there's nothing to do / no vault
|
|
106
|
+
* credential — in which case the caller's preflight surfaces the actionable
|
|
107
|
+
* "no credential" error unchanged.
|
|
108
|
+
*
|
|
109
|
+
* Idempotent and low-churn: if an `auth.json` already exists (a native
|
|
110
|
+
* `codex login` or a prior materialization) it is left untouched — Codex owns and
|
|
111
|
+
* self-refreshes it — so we mint at most once. We write to the *default* Codex
|
|
112
|
+
* home (never a throwaway dir) so Codex's rollouts stay where the session reader
|
|
113
|
+
* looks.
|
|
114
|
+
*/
|
|
115
|
+
export async function ensureCodexAuth(credsDir) {
|
|
116
|
+
const codexHome = resolveCodexHome();
|
|
117
|
+
const authFile = path.join(codexHome, "auth.json");
|
|
118
|
+
// Never clobber an existing login (native or previously materialized); Codex
|
|
119
|
+
// owns and refreshes it. An OPENAI_API_KEY, if present, is handled by preflight.
|
|
120
|
+
if (fs.existsSync(authFile))
|
|
121
|
+
return codexHome;
|
|
122
|
+
if (process.env.OPENAI_API_KEY?.trim())
|
|
123
|
+
return undefined;
|
|
124
|
+
const store = createCredentialVault(credsDir);
|
|
125
|
+
const cred = await store.read("openai-codex").catch(() => undefined);
|
|
126
|
+
if (!cred || cred.type !== "oauth" || typeof cred.refresh !== "string" || !cred.refresh)
|
|
127
|
+
return undefined;
|
|
128
|
+
const refreshed = await refreshCodexTokens(cred.refresh);
|
|
129
|
+
if (!refreshed)
|
|
130
|
+
return undefined;
|
|
131
|
+
// Persist the rotated refresh token back to the vault FIRST — OpenAI rotates it
|
|
132
|
+
// on every grant, so the previous one is now dead. If we can't persist it, bail
|
|
133
|
+
// rather than strand the vault on a token we've just invalidated.
|
|
134
|
+
try {
|
|
135
|
+
await store.modify("openai-codex", async (current) => ({
|
|
136
|
+
...(current ?? cred),
|
|
137
|
+
type: "oauth",
|
|
138
|
+
access: refreshed.accessToken,
|
|
139
|
+
refresh: refreshed.refreshToken,
|
|
140
|
+
expires: Date.now() + refreshed.expiresIn * 1000,
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
const accountId = typeof cred.accountId === "string" ? cred.accountId : undefined;
|
|
147
|
+
const authJson = {
|
|
148
|
+
auth_mode: "chatgpt",
|
|
149
|
+
OPENAI_API_KEY: null,
|
|
150
|
+
tokens: {
|
|
151
|
+
id_token: refreshed.idToken,
|
|
152
|
+
access_token: refreshed.accessToken,
|
|
153
|
+
refresh_token: refreshed.refreshToken,
|
|
154
|
+
account_id: accountId,
|
|
155
|
+
},
|
|
156
|
+
last_refresh: new Date().toISOString(),
|
|
157
|
+
};
|
|
158
|
+
try {
|
|
159
|
+
fs.mkdirSync(codexHome, { recursive: true, mode: 0o700 });
|
|
160
|
+
const tmp = `${authFile}.tmp`;
|
|
161
|
+
fs.writeFileSync(tmp, `${JSON.stringify(authJson, null, 2)}\n`, { mode: 0o600 });
|
|
162
|
+
fs.renameSync(tmp, authFile);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
return codexHome;
|
|
168
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// SPDX-License-Identifier: FSL-1.1-ALv2
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
// Codex credential preflight.
|
|
4
|
+
//
|
|
5
|
+
// The Codex CLI authenticates one of two ways: an OpenAI API key read from the
|
|
6
|
+
// `OPENAI_API_KEY` environment variable, or tokens in `CODEX_HOME/auth.json`
|
|
7
|
+
// (default `~/.codex/auth.json`). Bivy's shared credential vault forwards *API
|
|
8
|
+
// keys* to every agent (see credentials.ts), and `codex-auth.ts` mints the
|
|
9
|
+
// auth.json from a connected ChatGPT/Codex *subscription* — so both sign-in kinds
|
|
10
|
+
// now reach Codex.
|
|
11
|
+
//
|
|
12
|
+
// This preflight is the backstop: if neither an API key nor an auth.json is
|
|
13
|
+
// present after that handoff (e.g. no OpenAI sign-in at all, or a saved
|
|
14
|
+
// subscription that could not be refreshed), `codex exec` would fail with the
|
|
15
|
+
// opaque upstream error `unexpected status 401 Unauthorized: Missing bearer or
|
|
16
|
+
// basic authentication in header`. We detect that up front and return a clear,
|
|
17
|
+
// actionable message instead of letting the subprocess spawn and die on a 401.
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
/** Path to the Codex CLI's own auth file (`CODEX_HOME/auth.json`, default `~/.codex`). */
|
|
22
|
+
export function codexAuthFile(deps = {}) {
|
|
23
|
+
const codexHome = (deps.codexHome ?? process.env.CODEX_HOME)?.trim();
|
|
24
|
+
const base = codexHome ? codexHome : path.join(deps.home ?? os.homedir(), ".codex");
|
|
25
|
+
return path.join(base, "auth.json");
|
|
26
|
+
}
|
|
27
|
+
/** The user-facing message shown when Codex has no OpenAI credential. */
|
|
28
|
+
export const CODEX_NO_CREDENTIAL_MESSAGE = [
|
|
29
|
+
"Codex has no OpenAI credential on this node, so it can't start a turn — it would fail with `401 Unauthorized: Missing bearer or basic authentication in header`.",
|
|
30
|
+
"",
|
|
31
|
+
"Fix it any of these ways:",
|
|
32
|
+
"• Connect ChatGPT Plus/Pro (Codex) under Keys & OAuth — Bivy mints Codex's auth file from it automatically, or",
|
|
33
|
+
"• Add an OpenAI API key under Keys & OAuth — Bivy passes it to Codex as OPENAI_API_KEY, or",
|
|
34
|
+
"• Run `codex login` on this node to sign in directly.",
|
|
35
|
+
"",
|
|
36
|
+
"If you already connected the ChatGPT/Codex subscription in Bivy and still see this, the saved login could not be refreshed — reconnect it under Keys & OAuth.",
|
|
37
|
+
].join("\n");
|
|
38
|
+
/**
|
|
39
|
+
* Returns a human-readable error when the Codex CLI has no usable OpenAI
|
|
40
|
+
* credential (no `OPENAI_API_KEY` in the resolved environment and no
|
|
41
|
+
* `codex login` auth file), or undefined when a credential is present so the
|
|
42
|
+
* turn should proceed. `env` is the environment the Codex subprocess will run
|
|
43
|
+
* with (ambient process env + Bivy's vault handoff), so this catches both an
|
|
44
|
+
* OpenAI API key stored in Bivy and one already present on the node.
|
|
45
|
+
*/
|
|
46
|
+
export function codexCredentialPreflight(env, deps = {}) {
|
|
47
|
+
if (env.OPENAI_API_KEY?.trim())
|
|
48
|
+
return undefined;
|
|
49
|
+
const exists = deps.fileExists ?? ((p) => {
|
|
50
|
+
try {
|
|
51
|
+
return fs.existsSync(p);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (exists(codexAuthFile(deps)))
|
|
58
|
+
return undefined;
|
|
59
|
+
return CODEX_NO_CREDENTIAL_MESSAGE;
|
|
60
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// SPDX-License-Identifier: FSL-1.1-ALv2
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
// Codex on-disk session ("rollout") reader — the read side of adopting a Codex
|
|
4
|
+
// session that was started outside Bivy (e.g. a bare `codex` in a terminal).
|
|
5
|
+
//
|
|
6
|
+
// The Codex CLI ("@openai/codex") persists each session as a JSONL rollout under
|
|
7
|
+
// $CODEX_HOME/sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl
|
|
8
|
+
// (CODEX_HOME defaults to ~/.codex). The first line carries session metadata
|
|
9
|
+
// (id, cwd, timestamp); subsequent lines are response/event items.
|
|
10
|
+
//
|
|
11
|
+
// IMPORTANT — best-effort, and NOT yet verified against a live Codex here:
|
|
12
|
+
// Codex's rollout record shape has changed across versions, so every read is
|
|
13
|
+
// defensive (unknown/malformed lines are skipped, never thrown), and both a
|
|
14
|
+
// wrapped (`{type,payload}`) and a flat record layout are tolerated. This module
|
|
15
|
+
// only *reconstructs history* and *locates* sessions; a governed live resume of a
|
|
16
|
+
// Codex session additionally needs the Codex runtime to gain resume +
|
|
17
|
+
// toolInterception (today it is a non-governing process runtime — see
|
|
18
|
+
// docs/agent-runtimes.md and src/runtime/index.ts `cliAgentInfo`). Unlike Claude,
|
|
19
|
+
// Codex has no stable launch-time session-id flag to pin, so adoption relies on
|
|
20
|
+
// locating the rollout a run produced (see discoverCodexSessionForCwd).
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import os from "node:os";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import { hasLiveProcessForCwd } from "./native-process-scan.js";
|
|
25
|
+
/** Binary names a live Codex process could be running under (see
|
|
26
|
+
* native-process-scan.ts's best-effort cwd match). */
|
|
27
|
+
const CODEX_BIN_NAMES = ["codex"];
|
|
28
|
+
export function codexHome() {
|
|
29
|
+
return process.env.CODEX_HOME?.trim() || path.join(os.homedir(), ".codex");
|
|
30
|
+
}
|
|
31
|
+
export function codexSessionsDir() {
|
|
32
|
+
return path.join(codexHome(), "sessions");
|
|
33
|
+
}
|
|
34
|
+
/** Unwrap a `{ type, payload }` envelope, else return the record as-is. */
|
|
35
|
+
function inner(rec) {
|
|
36
|
+
const r = (rec ?? {});
|
|
37
|
+
return r.payload && typeof r.payload === "object" ? r.payload : r;
|
|
38
|
+
}
|
|
39
|
+
function toEpoch(value) {
|
|
40
|
+
if (value == null)
|
|
41
|
+
return undefined;
|
|
42
|
+
const ms = new Date(value).getTime();
|
|
43
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
44
|
+
}
|
|
45
|
+
/** Recursively collect `rollout-*.jsonl` files under a Codex sessions dir. */
|
|
46
|
+
function findRolloutFiles(root) {
|
|
47
|
+
const out = [];
|
|
48
|
+
const walk = (dir) => {
|
|
49
|
+
let entries;
|
|
50
|
+
try {
|
|
51
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return; // missing/unreadable — best-effort
|
|
55
|
+
}
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const full = path.join(dir, entry.name);
|
|
58
|
+
if (entry.isDirectory())
|
|
59
|
+
walk(full);
|
|
60
|
+
else if (entry.isFile() && /^rollout-.*\.jsonl$/.test(entry.name))
|
|
61
|
+
out.push(full);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
walk(root);
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/** Session metadata from a rollout's first line (id / cwd / start time). */
|
|
68
|
+
function metaFromFirstLine(line) {
|
|
69
|
+
try {
|
|
70
|
+
const rec = JSON.parse(line);
|
|
71
|
+
const p = inner(rec);
|
|
72
|
+
const id = typeof p.id === "string" ? p.id : typeof p.session_id === "string" ? p.session_id : undefined;
|
|
73
|
+
const cwd = typeof p.cwd === "string" ? p.cwd : undefined;
|
|
74
|
+
const createdAt = toEpoch(p.timestamp ?? rec.timestamp);
|
|
75
|
+
return { id, cwd, createdAt };
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Extract plain text from a Codex content field (string or content-block array). */
|
|
82
|
+
function textOf(content) {
|
|
83
|
+
if (typeof content === "string")
|
|
84
|
+
return content;
|
|
85
|
+
if (!Array.isArray(content))
|
|
86
|
+
return "";
|
|
87
|
+
return content
|
|
88
|
+
.map((block) => {
|
|
89
|
+
const b = (block ?? {});
|
|
90
|
+
if (typeof b.text === "string")
|
|
91
|
+
return b.text;
|
|
92
|
+
return "";
|
|
93
|
+
})
|
|
94
|
+
.join("")
|
|
95
|
+
.trim();
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Reconstruct a session's conversation from its rollout file as normalized
|
|
99
|
+
* RuntimeMessages (role + content + timestamp) — the same shape loadClaudeTranscript
|
|
100
|
+
* produces, so a reopened Codex session paints prior history the same way. Skips
|
|
101
|
+
* meta/system/tooling records; keeps user and assistant turns. Best-effort.
|
|
102
|
+
*/
|
|
103
|
+
export function loadCodexTranscriptFile(file) {
|
|
104
|
+
const messages = [];
|
|
105
|
+
let raw;
|
|
106
|
+
try {
|
|
107
|
+
raw = fs.readFileSync(file, "utf8");
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return messages;
|
|
111
|
+
}
|
|
112
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
113
|
+
if (!line.trim())
|
|
114
|
+
continue;
|
|
115
|
+
let rec;
|
|
116
|
+
try {
|
|
117
|
+
rec = JSON.parse(line);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const p = inner(rec);
|
|
123
|
+
// A message item: either {role, content} directly or an item with type "message".
|
|
124
|
+
const role = p.role ?? (p.type === "message" ? p.role : undefined);
|
|
125
|
+
if (role !== "user" && role !== "assistant")
|
|
126
|
+
continue;
|
|
127
|
+
const content = p.content ?? p.text;
|
|
128
|
+
const text = textOf(content);
|
|
129
|
+
if (!text)
|
|
130
|
+
continue;
|
|
131
|
+
messages.push({ role, content: text, timestamp: toEpoch(p.timestamp ?? rec.timestamp) ?? Date.now() });
|
|
132
|
+
}
|
|
133
|
+
return messages;
|
|
134
|
+
}
|
|
135
|
+
/** Enumerate Codex sessions on disk, newest first. Best-effort. */
|
|
136
|
+
export function listCodexSessions() {
|
|
137
|
+
const sessions = [];
|
|
138
|
+
for (const file of findRolloutFiles(codexSessionsDir())) {
|
|
139
|
+
let firstLine = "";
|
|
140
|
+
let mtime;
|
|
141
|
+
try {
|
|
142
|
+
mtime = fs.statSync(file).mtimeMs;
|
|
143
|
+
const fd = fs.readFileSync(file, "utf8");
|
|
144
|
+
firstLine = fd.split(/\r?\n/, 1)[0] ?? "";
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const meta = metaFromFirstLine(firstLine);
|
|
150
|
+
const transcript = loadCodexTranscriptFile(file);
|
|
151
|
+
// Codex prepends synthetic context turns as the first "user" messages —
|
|
152
|
+
// `<environment_context>`, `<recommended_plugins>`, `<user_instructions>`,
|
|
153
|
+
// etc. — which make useless list labels. Prefer the first turn that isn't one
|
|
154
|
+
// of these XML-ish blocks (i.e. the user's real prompt), falling back to the
|
|
155
|
+
// first turn only if that's genuinely all there is.
|
|
156
|
+
const userTurns = transcript.filter((m) => m.role === "user");
|
|
157
|
+
const contentOf = (m) => String(m?.content ?? "");
|
|
158
|
+
const isInjectedBlock = (text) => /^<[a-z][\w-]*>/i.test(text.trimStart());
|
|
159
|
+
const firstUser = userTurns.find((m) => !isInjectedBlock(contentOf(m))) ?? userTurns[0];
|
|
160
|
+
const firstMessage = firstUser ? contentOf(firstUser).slice(0, 200) : undefined;
|
|
161
|
+
sessions.push({
|
|
162
|
+
id: meta.id,
|
|
163
|
+
file,
|
|
164
|
+
cwd: meta.cwd,
|
|
165
|
+
createdAt: meta.createdAt ?? mtime,
|
|
166
|
+
firstMessage,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return sessions.sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
|
|
170
|
+
}
|
|
171
|
+
/** Load a Codex session's transcript by its session id (searches the store). */
|
|
172
|
+
export function loadCodexTranscript(sessionId) {
|
|
173
|
+
const match = listCodexSessions().find((s) => s.id === sessionId);
|
|
174
|
+
return match ? loadCodexTranscriptFile(match.file) : [];
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Delete a Codex session's rollout file by id so a user-initiated delete in the
|
|
178
|
+
* app actually removes it from Codex's store (`$CODEX_HOME/sessions/**`) — the
|
|
179
|
+
* read-side counterpart to loadCodexTranscript. Returns true if a rollout was
|
|
180
|
+
* unlinked. Best-effort: a missing store / already-gone file is not an error.
|
|
181
|
+
*/
|
|
182
|
+
export function deleteCodexSession(sessionId) {
|
|
183
|
+
const match = listCodexSessions().find((s) => s.id === sessionId);
|
|
184
|
+
if (!match)
|
|
185
|
+
return false;
|
|
186
|
+
try {
|
|
187
|
+
fs.unlinkSync(match.file);
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (error?.code === "ENOENT")
|
|
192
|
+
return false;
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Locate the Codex session a run produced, for adoption when there is no pinned
|
|
198
|
+
* id: the newest rollout whose recorded cwd matches `cwd` and that started at/after
|
|
199
|
+
* `since` (the run-terminal's creation time, minus a small skew). Returns the
|
|
200
|
+
* session summary or undefined. Best-effort — Codex assigns the id itself, so this
|
|
201
|
+
* is how a `bivy run codex` session is mapped back to its on-disk rollout.
|
|
202
|
+
*/
|
|
203
|
+
export function discoverCodexSessionForCwd(cwd, since = 0) {
|
|
204
|
+
const target = path.resolve(cwd);
|
|
205
|
+
const skewMs = 5_000;
|
|
206
|
+
return listCodexSessions().find((s) => s.cwd != null && path.resolve(s.cwd) === target && (s.createdAt ?? 0) >= since - skewMs);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Enumerate Codex's on-disk sessions as bounded discovery metadata (issue
|
|
210
|
+
* #156's runtime-agnostic discovery/adoption flow — see
|
|
211
|
+
* native-session-discovery.ts). Every Codex session Codex itself assigned an
|
|
212
|
+
* id to is resumable via `codex exec resume <id>`, so `resumable` is true
|
|
213
|
+
* whenever an id was recorded; `active` is a best-effort live-process check
|
|
214
|
+
* scoped to this session's own cwd, never its transcript content.
|
|
215
|
+
*/
|
|
216
|
+
export function discoverNativeCodexSessions(hasLiveProcess = (cwd) => hasLiveProcessForCwd(cwd, CODEX_BIN_NAMES)) {
|
|
217
|
+
return listCodexSessions()
|
|
218
|
+
.filter((s) => Boolean(s.id))
|
|
219
|
+
.map((s) => ({
|
|
220
|
+
runtimeId: "codex-approvals",
|
|
221
|
+
ref: s.id,
|
|
222
|
+
file: s.file,
|
|
223
|
+
cwd: s.cwd,
|
|
224
|
+
updatedAt: s.createdAt,
|
|
225
|
+
title: s.firstMessage,
|
|
226
|
+
active: Boolean(s.cwd) && hasLiveProcess(s.cwd),
|
|
227
|
+
resumable: true,
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// SPDX-License-Identifier: FSL-1.1-ALv2
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
export class ControlPlaneSessionLocationRegistry {
|
|
4
|
+
options;
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
}
|
|
8
|
+
/** Map a durable row to a routable location, or undefined if not adoptable. */
|
|
9
|
+
toLocation(row) {
|
|
10
|
+
if (!row.sessionId || !row.agentServiceAddress)
|
|
11
|
+
return undefined;
|
|
12
|
+
const runtimeId = this.options.resolveRuntimeId(row.sessionId);
|
|
13
|
+
if (!runtimeId)
|
|
14
|
+
return undefined; // can't route an attach without the runtime id
|
|
15
|
+
return { sessionId: row.sessionId, agentServiceAddress: row.agentServiceAddress, runtimeId, nodeId: this.options.nodeId };
|
|
16
|
+
}
|
|
17
|
+
async lookup(sessionId) {
|
|
18
|
+
if (!sessionId)
|
|
19
|
+
return undefined;
|
|
20
|
+
const rows = await this.options.fetchNodeSessions().catch(() => []);
|
|
21
|
+
const row = rows.find((r) => r.sessionId === sessionId);
|
|
22
|
+
return row ? this.toLocation(row) : undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Every adoptable session this node currently owns in the control plane (has a
|
|
26
|
+
* host address AND a resolvable runtime id). Startup adoption iterates this and
|
|
27
|
+
* re-attaches to each. Never throws — a fetch failure yields `[]`.
|
|
28
|
+
*/
|
|
29
|
+
async listNode() {
|
|
30
|
+
const rows = await this.options.fetchNodeSessions().catch(() => []);
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const row of rows) {
|
|
33
|
+
const location = this.toLocation(row);
|
|
34
|
+
if (location)
|
|
35
|
+
out.push(location);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
// Writes are owned by advertiseSessions (the replace-all POST to /node/sessions
|
|
40
|
+
// is the single writer of session_index); this is a read-through view.
|
|
41
|
+
async record(_location) {
|
|
42
|
+
/* no-op: advertise owns control-plane writes */
|
|
43
|
+
}
|
|
44
|
+
async forget(_sessionId) {
|
|
45
|
+
/* no-op: advertise owns control-plane writes */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Layers two registries so a lookup prefers the fast, authoritative in-memory
|
|
50
|
+
* entry (this daemon's own live sessions) and falls through to a shared,
|
|
51
|
+
* durable one (the control plane) only on a miss — exactly the "in-memory first,
|
|
52
|
+
* then control-plane" ordering Stage 3 needs so nothing regresses when the
|
|
53
|
+
* process already knows a session. Writes (`record`/`forget`) go to the primary
|
|
54
|
+
* (in-memory) layer; the control-plane layer's writes are no-ops by design.
|
|
55
|
+
*/
|
|
56
|
+
export class LayeredSessionLocationRegistry {
|
|
57
|
+
primary;
|
|
58
|
+
fallback;
|
|
59
|
+
constructor(primary, fallback) {
|
|
60
|
+
this.primary = primary;
|
|
61
|
+
this.fallback = fallback;
|
|
62
|
+
}
|
|
63
|
+
async record(location) {
|
|
64
|
+
await this.primary.record(location);
|
|
65
|
+
await this.fallback.record(location);
|
|
66
|
+
}
|
|
67
|
+
async lookup(sessionId) {
|
|
68
|
+
return (await this.primary.lookup(sessionId)) ?? (await this.fallback.lookup(sessionId));
|
|
69
|
+
}
|
|
70
|
+
async forget(sessionId) {
|
|
71
|
+
await this.primary.forget(sessionId);
|
|
72
|
+
await this.fallback.forget(sessionId);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// SPDX-License-Identifier: FSL-1.1-ALv2
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
// Ingest credentials FROM an agent's own store INTO Bivy's vault.
|
|
4
|
+
//
|
|
5
|
+
// The reverse of provisioning: when a user logs in inside an agent's native CLI
|
|
6
|
+
// (e.g. `codex login`, or a login done in Pi's TUI), Bivy folds that credential
|
|
7
|
+
// back into its vault so the one login is unified everywhere. Merge is
|
|
8
|
+
// rotation-safe (freshest `expires` wins), so it's safe even when both Bivy and
|
|
9
|
+
// the agent have touched the same credential.
|
|
10
|
+
//
|
|
11
|
+
// Triggered on `bivy run <agent>` terminal exit (server.ts) and by the auth.json
|
|
12
|
+
// watcher; each agent contributes a small format adapter here.
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { createCredentialVault } from "./credential-store.js";
|
|
16
|
+
import { resolveCodexHome } from "./codex-auth.js";
|
|
17
|
+
import { claudeCredentialFiles } from "./anthropic-preflight.js";
|
|
18
|
+
/** Read the `exp` (seconds) claim from a JWT access token → absolute epoch ms. */
|
|
19
|
+
function jwtExpiryMs(token) {
|
|
20
|
+
const parts = token.split(".");
|
|
21
|
+
if (parts.length < 2 || !parts[1])
|
|
22
|
+
return undefined;
|
|
23
|
+
try {
|
|
24
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
25
|
+
const exp = Number(payload.exp);
|
|
26
|
+
return Number.isFinite(exp) && exp > 0 ? exp * 1000 : undefined;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Map Codex's `~/.codex/auth.json` into a Bivy credential (provider id → credential). */
|
|
33
|
+
export function codexAuthToCredential(raw) {
|
|
34
|
+
if (!raw || typeof raw !== "object")
|
|
35
|
+
return undefined;
|
|
36
|
+
const record = raw;
|
|
37
|
+
// API-key mode: `codex login --api-key` writes OPENAI_API_KEY.
|
|
38
|
+
if (typeof record.OPENAI_API_KEY === "string" && record.OPENAI_API_KEY.trim()) {
|
|
39
|
+
return { providerId: "openai", credential: { type: "api_key", key: record.OPENAI_API_KEY.trim() } };
|
|
40
|
+
}
|
|
41
|
+
// ChatGPT subscription: the OAuth token set the CLI persists.
|
|
42
|
+
const tokens = record.tokens;
|
|
43
|
+
const access = typeof tokens?.access_token === "string" ? tokens.access_token : "";
|
|
44
|
+
const refresh = typeof tokens?.refresh_token === "string" ? tokens.refresh_token : "";
|
|
45
|
+
if (!access || !refresh)
|
|
46
|
+
return undefined;
|
|
47
|
+
const accountId = typeof tokens?.account_id === "string" ? tokens.account_id : undefined;
|
|
48
|
+
const expires = jwtExpiryMs(access) ?? Date.now() + 60 * 60 * 1000;
|
|
49
|
+
return {
|
|
50
|
+
providerId: "openai-codex",
|
|
51
|
+
credential: { type: "oauth", access, refresh, expires, ...(accountId ? { accountId } : {}) },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Fold a login/refresh done in the Codex CLI back into Bivy's vault. Returns imported count. */
|
|
55
|
+
export async function ingestCodexCredentials(credsDir) {
|
|
56
|
+
let raw;
|
|
57
|
+
try {
|
|
58
|
+
raw = JSON.parse(fs.readFileSync(path.join(resolveCodexHome(), "auth.json"), "utf8"));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return 0; // no Codex login on disk
|
|
62
|
+
}
|
|
63
|
+
const mapped = codexAuthToCredential(raw);
|
|
64
|
+
if (!mapped)
|
|
65
|
+
return 0;
|
|
66
|
+
return createCredentialVault(credsDir).importAll({ [mapped.providerId]: mapped.credential });
|
|
67
|
+
}
|
|
68
|
+
/** Map the Claude CLI's `.credentials.json` (`claudeAiOauth`) into a Bivy credential. */
|
|
69
|
+
export function claudeAuthToCredential(raw) {
|
|
70
|
+
if (!raw || typeof raw !== "object")
|
|
71
|
+
return undefined;
|
|
72
|
+
const oauth = raw.claudeAiOauth;
|
|
73
|
+
const access = typeof oauth?.accessToken === "string" ? oauth.accessToken : "";
|
|
74
|
+
const refresh = typeof oauth?.refreshToken === "string" ? oauth.refreshToken : "";
|
|
75
|
+
if (!access || !refresh)
|
|
76
|
+
return undefined;
|
|
77
|
+
// Claude stores `expiresAt` as an absolute epoch-ms timestamp already.
|
|
78
|
+
const expires = Number(oauth?.expiresAt) || Date.now() + 60 * 60 * 1000;
|
|
79
|
+
return { providerId: "anthropic", credential: { type: "oauth", access, refresh, expires } };
|
|
80
|
+
}
|
|
81
|
+
/** Fold a login done in the Claude CLI (`claude /login`) back into Bivy's vault. */
|
|
82
|
+
export async function ingestClaudeCredentials(credsDir) {
|
|
83
|
+
for (const file of claudeCredentialFiles()) {
|
|
84
|
+
let raw;
|
|
85
|
+
try {
|
|
86
|
+
raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
continue; // not this path (or macOS Keychain — no file to read)
|
|
90
|
+
}
|
|
91
|
+
const mapped = claudeAuthToCredential(raw);
|
|
92
|
+
if (mapped)
|
|
93
|
+
return createCredentialVault(credsDir).importAll({ [mapped.providerId]: mapped.credential });
|
|
94
|
+
}
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Ingest an agent's native credential store into Bivy's vault. Called when a
|
|
99
|
+
* `bivy run <agent>` terminal exits, so a login done inside that agent's CLI is
|
|
100
|
+
* captured centrally. Best-effort and per-agent; unknown agents are a no-op.
|
|
101
|
+
*
|
|
102
|
+
* Gemini is intentionally absent: its `~/.gemini` login is a Google account
|
|
103
|
+
* (Code Assist) token, a different auth model from an api key / subscription
|
|
104
|
+
* OAuth Bivy can provision back — folding it in would be misleading.
|
|
105
|
+
*/
|
|
106
|
+
export async function ingestAgentCredentials(agentId, credsDir, piDir) {
|
|
107
|
+
switch ((agentId ?? "").toLowerCase()) {
|
|
108
|
+
case "codex":
|
|
109
|
+
case "codex-approvals":
|
|
110
|
+
return ingestCodexCredentials(credsDir);
|
|
111
|
+
case "claude":
|
|
112
|
+
case "claude-code":
|
|
113
|
+
case "claude-code-sdk":
|
|
114
|
+
return ingestClaudeCredentials(credsDir);
|
|
115
|
+
case "pi":
|
|
116
|
+
// Pi's TUI writes a plaintext auth.json in its own dir; fold it back into
|
|
117
|
+
// the shared vault.
|
|
118
|
+
return createCredentialVault(credsDir, piDir).ingestPlaintext();
|
|
119
|
+
default:
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
}
|