@ouro.bot/cli 0.1.0-alpha.547 → 0.1.0-alpha.549
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/changelog.json +14 -0
- package/dist/heart/auth/auth-flow.js +62 -10
- package/dist/heart/daemon/cli-defaults.js +10 -0
- package/dist/heart/daemon/cli-exec.js +15 -17
- package/dist/heart/daemon/daemon-runtime-sync.js +18 -0
- package/dist/heart/daemon/launchd.js +22 -5
- package/dist/heart/provider-credentials.js +1 -1
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.549",
|
|
6
|
+
"changes": [
|
|
7
|
+
"`ouro up` now unloads any already-loaded daemon LaunchAgent before replacing a drifted daemon runtime, preventing launchd KeepAlive from racing the manual replacement and leaving two daemon processes split across the same socket.",
|
|
8
|
+
"`ouro up` now rewrites the daemon boot plist as soon as the daemon is answering, so a degraded sense no longer prevents restart auto-start metadata from being refreshed."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"version": "0.1.0-alpha.548",
|
|
13
|
+
"changes": [
|
|
14
|
+
"OpenAI Codex auth now reuses an existing fresh local Codex login before starting browser login, letting `ouro auth --provider openai-codex` repair stale vault copies without a human OAuth round-trip when the local Codex CLI is already logged in.",
|
|
15
|
+
"Codex provider credentials now retain refresh metadata (`refreshToken` and `expiresAt`) alongside the access token, setting up proactive non-browser refresh handling for future hardening."
|
|
16
|
+
]
|
|
17
|
+
},
|
|
4
18
|
{
|
|
5
19
|
"version": "0.1.0-alpha.547",
|
|
6
20
|
"changes": [
|
|
@@ -52,6 +52,7 @@ const provider_credentials_1 = require("../provider-credentials");
|
|
|
52
52
|
const vault_unlock_1 = require("../../repertoire/vault-unlock");
|
|
53
53
|
const ANTHROPIC_SETUP_TOKEN_PREFIX = "sk-ant-oat01-";
|
|
54
54
|
const ANTHROPIC_SETUP_TOKEN_MIN_LENGTH = 80;
|
|
55
|
+
const CODEX_LOCAL_TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000;
|
|
55
56
|
function assertPersistentProviderCredentialsAllowed(agentName) {
|
|
56
57
|
if (agentName === "SerpentGuide") {
|
|
57
58
|
throw new Error("SerpentGuide uses provider credentials in memory during hatch bootstrap; persistent SerpentGuide auth is not supported.");
|
|
@@ -171,18 +172,64 @@ function writeAgentModel(agentName, facing, modelName, deps = {}) {
|
|
|
171
172
|
});
|
|
172
173
|
return { configPath, provider, previousModel };
|
|
173
174
|
}
|
|
174
|
-
function
|
|
175
|
+
function decodeJwtPayload(token) {
|
|
176
|
+
const parts = token.split(".");
|
|
177
|
+
if (parts.length < 2 || !parts[1])
|
|
178
|
+
return null;
|
|
179
|
+
try {
|
|
180
|
+
const base64 = parts[1]
|
|
181
|
+
.replace(/-/g, "+")
|
|
182
|
+
.replace(/_/g, "/")
|
|
183
|
+
.padEnd(Math.ceil(parts[1].length / 4) * 4, "=");
|
|
184
|
+
const parsed = JSON.parse(Buffer.from(base64, "base64").toString("utf8"));
|
|
185
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
186
|
+
return null;
|
|
187
|
+
return parsed;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function readJwtExpiresAt(token) {
|
|
194
|
+
const payload = decodeJwtPayload(token);
|
|
195
|
+
const exp = payload?.exp;
|
|
196
|
+
if (typeof exp !== "number" || !Number.isFinite(exp) || exp <= 0)
|
|
197
|
+
return undefined;
|
|
198
|
+
return Math.floor(exp * 1000);
|
|
199
|
+
}
|
|
200
|
+
function isFreshCodexToken(credentials, now) {
|
|
201
|
+
if (!credentials.oauthAccessToken)
|
|
202
|
+
return false;
|
|
203
|
+
if (typeof credentials.expiresAt !== "number")
|
|
204
|
+
return false;
|
|
205
|
+
return credentials.expiresAt > now.getTime() + CODEX_LOCAL_TOKEN_REFRESH_MARGIN_MS;
|
|
206
|
+
}
|
|
207
|
+
function readCodexLocalAuthCredentials(homeDir) {
|
|
175
208
|
const authPath = path.join(homeDir, ".codex", "auth.json");
|
|
176
209
|
try {
|
|
177
210
|
const raw = fs.readFileSync(authPath, "utf8");
|
|
178
211
|
const parsed = JSON.parse(raw);
|
|
179
|
-
const
|
|
180
|
-
|
|
212
|
+
const accessToken = typeof parsed.tokens?.access_token === "string" ? parsed.tokens.access_token.trim() : "";
|
|
213
|
+
if (!accessToken)
|
|
214
|
+
return {};
|
|
215
|
+
const refreshToken = typeof parsed.tokens?.refresh_token === "string" ? parsed.tokens.refresh_token.trim() : "";
|
|
216
|
+
const expiresAt = readJwtExpiresAt(accessToken);
|
|
217
|
+
return {
|
|
218
|
+
oauthAccessToken: accessToken,
|
|
219
|
+
...(refreshToken ? { refreshToken } : {}),
|
|
220
|
+
...(expiresAt ? { expiresAt } : {}),
|
|
221
|
+
};
|
|
181
222
|
}
|
|
182
223
|
catch {
|
|
183
|
-
return
|
|
224
|
+
return {};
|
|
184
225
|
}
|
|
185
226
|
}
|
|
227
|
+
function isCodexLoginStatusReady(result) {
|
|
228
|
+
if (result.error || result.status !== 0)
|
|
229
|
+
return false;
|
|
230
|
+
const output = `${typeof result.stdout === "string" ? result.stdout : ""}\n${typeof result.stderr === "string" ? result.stderr : ""}`;
|
|
231
|
+
return output.toLowerCase().includes("logged in");
|
|
232
|
+
}
|
|
186
233
|
function ensurePromptInput(promptInput, provider) {
|
|
187
234
|
if (promptInput)
|
|
188
235
|
return promptInput;
|
|
@@ -224,6 +271,7 @@ function validateAnthropicToken(token) {
|
|
|
224
271
|
async function collectRuntimeAuthCredentials(input, deps) {
|
|
225
272
|
const spawnSync = deps.spawnSync ?? child_process_1.spawnSync;
|
|
226
273
|
const homeDir = deps.homeDir ?? os.homedir();
|
|
274
|
+
const now = deps.now ?? (() => new Date());
|
|
227
275
|
if (input.provider === "github-copilot") {
|
|
228
276
|
let token = process.env.GH_TOKEN?.trim() || process.env.GITHUB_TOKEN?.trim() || "";
|
|
229
277
|
if (!token) {
|
|
@@ -267,9 +315,13 @@ async function collectRuntimeAuthCredentials(input, deps) {
|
|
|
267
315
|
return { githubToken: token, baseUrl };
|
|
268
316
|
}
|
|
269
317
|
if (input.provider === "openai-codex") {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
318
|
+
writeAuthProgress(input, "checking local Codex login...");
|
|
319
|
+
const localStatus = spawnSync("codex", ["login", "status"], { encoding: "utf8" });
|
|
320
|
+
const localCredentials = readCodexLocalAuthCredentials(homeDir);
|
|
321
|
+
if (isCodexLoginStatusReady(localStatus) && isFreshCodexToken(localCredentials, now())) {
|
|
322
|
+
writeAuthProgress(input, "using existing openai-codex local login...");
|
|
323
|
+
return localCredentials;
|
|
324
|
+
}
|
|
273
325
|
(0, runtime_1.emitNervesEvent)({
|
|
274
326
|
component: "daemon",
|
|
275
327
|
event: "daemon.auth_codex_login_start",
|
|
@@ -285,11 +337,11 @@ async function collectRuntimeAuthCredentials(input, deps) {
|
|
|
285
337
|
throw new Error(`'codex login' exited with status ${result.status}.`);
|
|
286
338
|
}
|
|
287
339
|
writeAuthProgress(input, "openai-codex login complete; reading local Codex token...");
|
|
288
|
-
const
|
|
289
|
-
if (!
|
|
340
|
+
const credentials = readCodexLocalAuthCredentials(homeDir);
|
|
341
|
+
if (!credentials.oauthAccessToken) {
|
|
290
342
|
throw new Error("Codex login completed but no token was found in ~/.codex/auth.json. Re-run `codex login` and try again.");
|
|
291
343
|
}
|
|
292
|
-
return
|
|
344
|
+
return credentials;
|
|
293
345
|
}
|
|
294
346
|
if (input.provider === "anthropic") {
|
|
295
347
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -251,6 +251,15 @@ function defaultEnsureDaemonBootPersistence(socketPath) {
|
|
|
251
251
|
envPath: process.env.PATH,
|
|
252
252
|
});
|
|
253
253
|
}
|
|
254
|
+
function defaultPrepareDaemonRuntimeReplacement() {
|
|
255
|
+
if (process.platform !== "darwin") {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
(0, launchd_1.bootoutLaunchAgentByLabel)({
|
|
259
|
+
exec: (cmd) => { (0, child_process_1.execSync)(cmd, { stdio: "ignore" }); },
|
|
260
|
+
userUid: process.getuid?.() ?? 0,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
254
263
|
async function defaultPromptInput(question) {
|
|
255
264
|
const readline = await Promise.resolve().then(() => __importStar(require("readline/promises")));
|
|
256
265
|
const rl = readline.createInterface({
|
|
@@ -615,6 +624,7 @@ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SO
|
|
|
615
624
|
syncGlobalOuroBotWrapper: ouro_bot_global_installer_1.syncGlobalOuroBotWrapper,
|
|
616
625
|
pruneDaemonLogs: logs_prune_1.pruneDaemonLogs,
|
|
617
626
|
ensureSkillManagement: skill_management_installer_1.ensureSkillManagement,
|
|
627
|
+
prepareDaemonRuntimeReplacement: defaultPrepareDaemonRuntimeReplacement,
|
|
618
628
|
ensureDaemonBootPersistence: defaultEnsureDaemonBootPersistence,
|
|
619
629
|
/* v8 ignore start -- dev-mode defaults: tests inject mocks for mode detection and binary resolution @preserve */
|
|
620
630
|
detectMode: () => (0, runtime_mode_1.detectRuntimeMode)((0, identity_1.getRepoRoot)()),
|
|
@@ -943,6 +943,7 @@ async function ensureDaemonRunning(deps, options = {}) {
|
|
|
943
943
|
stopDaemon: async () => {
|
|
944
944
|
await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
|
|
945
945
|
},
|
|
946
|
+
prepareDaemonRuntimeReplacement: deps.prepareDaemonRuntimeReplacement,
|
|
946
947
|
cleanupStaleSocket: deps.cleanupStaleSocket,
|
|
947
948
|
startDaemonProcess: deps.startDaemonProcess,
|
|
948
949
|
checkSocketAlive: deps.checkSocketAlive,
|
|
@@ -5901,6 +5902,20 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
5901
5902
|
return returnCliFailure(deps, daemonResult.message);
|
|
5902
5903
|
}
|
|
5903
5904
|
progress.completePhase("starting daemon", daemonProgressSummary(daemonResult));
|
|
5905
|
+
if (deps.ensureDaemonBootPersistence) {
|
|
5906
|
+
try {
|
|
5907
|
+
await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
|
|
5908
|
+
}
|
|
5909
|
+
catch (error) {
|
|
5910
|
+
(0, runtime_1.emitNervesEvent)({
|
|
5911
|
+
level: "warn",
|
|
5912
|
+
component: "daemon",
|
|
5913
|
+
event: "daemon.system_setup_launchd_error",
|
|
5914
|
+
message: "failed to persist daemon boot startup",
|
|
5915
|
+
meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
|
|
5916
|
+
});
|
|
5917
|
+
}
|
|
5918
|
+
}
|
|
5904
5919
|
progress.startPhase("provider checks");
|
|
5905
5920
|
const providerDegraded = await checkAlreadyRunningAgentProviders(deps, (msg) => progress.updateDetail(msg));
|
|
5906
5921
|
daemonResult.stability = mergeStartupStability(daemonResult.stability, providerDegraded);
|
|
@@ -6069,23 +6084,6 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
6069
6084
|
const driftAdvisories = await collectAgentDriftAdvisories(deps);
|
|
6070
6085
|
writeDriftAdvisorySummary(deps, driftAdvisories);
|
|
6071
6086
|
}
|
|
6072
|
-
// Persist boot startup AFTER daemon is running — bootstrap is safe now
|
|
6073
|
-
// because the daemon socket exists, so launchd's KeepAlive registers
|
|
6074
|
-
// for crash recovery without starting a competing process.
|
|
6075
|
-
if (deps.ensureDaemonBootPersistence) {
|
|
6076
|
-
try {
|
|
6077
|
-
await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
|
|
6078
|
-
}
|
|
6079
|
-
catch (error) {
|
|
6080
|
-
(0, runtime_1.emitNervesEvent)({
|
|
6081
|
-
level: "warn",
|
|
6082
|
-
component: "daemon",
|
|
6083
|
-
event: "daemon.system_setup_launchd_error",
|
|
6084
|
-
message: "failed to persist daemon boot startup",
|
|
6085
|
-
meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
|
|
6086
|
-
});
|
|
6087
|
-
}
|
|
6088
|
-
}
|
|
6089
6087
|
return daemonResult.message;
|
|
6090
6088
|
}
|
|
6091
6089
|
if (command.kind === "daemon.dev") {
|
|
@@ -107,6 +107,24 @@ async function ensureCurrentDaemonRuntime(deps) {
|
|
|
107
107
|
try {
|
|
108
108
|
deps.onProgress?.("stopping the older background service");
|
|
109
109
|
await deps.stopDaemon();
|
|
110
|
+
if (deps.prepareDaemonRuntimeReplacement) {
|
|
111
|
+
deps.onProgress?.("disabling daemon auto-restart during replacement");
|
|
112
|
+
try {
|
|
113
|
+
await Promise.resolve(deps.prepareDaemonRuntimeReplacement());
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
(0, runtime_1.emitNervesEvent)({
|
|
117
|
+
level: "warn",
|
|
118
|
+
component: "daemon",
|
|
119
|
+
event: "daemon.runtime_sync_replacement_prepare_error",
|
|
120
|
+
message: "daemon runtime replacement preparation failed",
|
|
121
|
+
meta: {
|
|
122
|
+
socketPath: deps.socketPath,
|
|
123
|
+
reason: formatErrorReason(error),
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
110
128
|
}
|
|
111
129
|
catch (error) {
|
|
112
130
|
const reason = formatErrorReason(error);
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.DAEMON_PLIST_LABEL = void 0;
|
|
37
|
+
exports.bootoutLaunchAgentByLabel = bootoutLaunchAgentByLabel;
|
|
37
38
|
exports.generateDaemonPlist = generateDaemonPlist;
|
|
38
39
|
exports.writeLaunchAgentPlist = writeLaunchAgentPlist;
|
|
39
40
|
exports.installLaunchAgent = installLaunchAgent;
|
|
@@ -48,6 +49,26 @@ function plistFilePath(homeDir) {
|
|
|
48
49
|
function userLaunchDomain(userUid) {
|
|
49
50
|
return `gui/${userUid}`;
|
|
50
51
|
}
|
|
52
|
+
function bootoutLaunchAgentByLabel(deps) {
|
|
53
|
+
const domain = userLaunchDomain(deps.userUid);
|
|
54
|
+
try {
|
|
55
|
+
deps.exec(`launchctl bootout ${domain}/${exports.DAEMON_PLIST_LABEL}`);
|
|
56
|
+
(0, runtime_1.emitNervesEvent)({
|
|
57
|
+
component: "daemon",
|
|
58
|
+
event: "daemon.launchd_label_bootout",
|
|
59
|
+
message: "booted out daemon launch agent by label",
|
|
60
|
+
meta: { label: exports.DAEMON_PLIST_LABEL, domain },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
(0, runtime_1.emitNervesEvent)({
|
|
65
|
+
component: "daemon",
|
|
66
|
+
event: "daemon.launchd_label_bootout_skipped",
|
|
67
|
+
message: "daemon launch agent label bootout skipped",
|
|
68
|
+
meta: { label: exports.DAEMON_PLIST_LABEL, domain, reason: error instanceof Error ? error.message : String(error) },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
51
72
|
function generateDaemonPlist(options) {
|
|
52
73
|
(0, runtime_1.emitNervesEvent)({
|
|
53
74
|
component: "daemon",
|
|
@@ -145,12 +166,8 @@ function uninstallLaunchAgent(deps) {
|
|
|
145
166
|
meta: {},
|
|
146
167
|
});
|
|
147
168
|
const fullPath = plistFilePath(deps.homeDir);
|
|
148
|
-
|
|
169
|
+
bootoutLaunchAgentByLabel(deps);
|
|
149
170
|
if (deps.existsFile(fullPath)) {
|
|
150
|
-
try {
|
|
151
|
-
deps.exec(`launchctl bootout ${domain} "${fullPath}"`);
|
|
152
|
-
}
|
|
153
|
-
catch { /* best effort */ }
|
|
154
171
|
deps.removeFile(fullPath);
|
|
155
172
|
}
|
|
156
173
|
(0, runtime_1.emitNervesEvent)({
|