@ouro.bot/cli 0.1.0-alpha.195 → 0.1.0-alpha.197
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 +13 -0
- package/dist/heart/daemon/agent-config-check.js +149 -0
- package/dist/heart/daemon/daemon-cli.js +24 -1
- package/dist/heart/daemon/daemon-entry.js +8 -0
- package/dist/heart/daemon/daemon-runtime-sync.js +21 -2
- package/dist/heart/daemon/process-manager.js +18 -0
- package/dist/heart/identity.js +12 -1
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
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.197",
|
|
6
|
+
"changes": [
|
|
7
|
+
"fix(daemon): validate agent config before spawn \u2014 skips agents with missing credentials instead of crash-looping"
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.0-alpha.196",
|
|
12
|
+
"changes": [
|
|
13
|
+
"fix: ouro up verifies daemon is alive before reporting started (polls socket for 10s)",
|
|
14
|
+
"fix: daemon-runtime-sync also verifies after replacing a stale daemon"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
4
17
|
{
|
|
5
18
|
"version": "0.1.0-alpha.195",
|
|
6
19
|
"changes": [
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.checkAgentConfig = checkAgentConfig;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const identity_1 = require("../identity");
|
|
40
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
41
|
+
/**
|
|
42
|
+
* Pre-spawn validation: ensures agent.json exists and required secrets are present.
|
|
43
|
+
* Returns `{ ok: true }` when the agent is ready to run, or a descriptive error
|
|
44
|
+
* with an actionable fix message when something is missing.
|
|
45
|
+
*/
|
|
46
|
+
function checkAgentConfig(agentName, bundlesRoot, secretsRoot) {
|
|
47
|
+
const agentJsonPath = path.join(bundlesRoot, `${agentName}.ouro`, "agent.json");
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = fs.readFileSync(agentJsonPath, "utf-8");
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
error: `agent.json not found at ${agentJsonPath}`,
|
|
56
|
+
fix: `Run 'ouro hatch ${agentName}' to create the agent bundle, or verify that ${bundlesRoot}/${agentName}.ouro/ exists.`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(raw);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
error: `agent.json at ${agentJsonPath} contains invalid JSON`,
|
|
67
|
+
fix: `Open ${agentJsonPath} and fix the JSON syntax.`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// Disabled agents are valid — they just won't run
|
|
71
|
+
if (parsed.enabled === false) {
|
|
72
|
+
return { ok: true };
|
|
73
|
+
}
|
|
74
|
+
// Resolve provider: humanFacing.provider > config.provider
|
|
75
|
+
let provider;
|
|
76
|
+
const humanFacing = parsed.humanFacing;
|
|
77
|
+
if (humanFacing && typeof humanFacing.provider === "string") {
|
|
78
|
+
provider = humanFacing.provider;
|
|
79
|
+
}
|
|
80
|
+
else if (typeof parsed.provider === "string") {
|
|
81
|
+
provider = parsed.provider;
|
|
82
|
+
}
|
|
83
|
+
if (!provider) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
error: `agent.json for '${agentName}' has no provider configured`,
|
|
87
|
+
fix: `Add humanFacing.provider to ${agentJsonPath}. Valid providers: ${Object.keys(identity_1.PROVIDER_CREDENTIALS).join(", ")}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const requiredFields = identity_1.PROVIDER_CREDENTIALS[provider];
|
|
91
|
+
if (!requiredFields) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
error: `Unknown provider '${provider}' in agent.json for '${agentName}'`,
|
|
95
|
+
fix: `Set humanFacing.provider to one of: ${Object.keys(identity_1.PROVIDER_CREDENTIALS).join(", ")}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// Azure with managed identity does not require apiKey — skip secrets check
|
|
99
|
+
// when endpoint + deployment are present (managed identity auth)
|
|
100
|
+
const secretsJsonPath = path.join(secretsRoot, agentName, "secrets.json");
|
|
101
|
+
let secrets;
|
|
102
|
+
try {
|
|
103
|
+
const secretsRaw = fs.readFileSync(secretsJsonPath, "utf-8");
|
|
104
|
+
secrets = JSON.parse(secretsRaw);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
error: `secrets.json not found or unreadable at ${secretsJsonPath}`,
|
|
110
|
+
fix: `Run 'ouro auth ${agentName}' to configure credentials, or create ${secretsJsonPath} with providers.${provider} credentials.`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const providers = secrets.providers;
|
|
114
|
+
const providerSecrets = providers?.[provider];
|
|
115
|
+
if (!providerSecrets) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
error: `secrets.json for '${agentName}' is missing providers.${provider} section`,
|
|
119
|
+
fix: `Run 'ouro auth ${agentName}' to configure ${provider} credentials.`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// Azure special case: managed identity only needs endpoint + deployment
|
|
123
|
+
if (provider === "azure") {
|
|
124
|
+
const hasEndpoint = typeof providerSecrets.endpoint === "string" && providerSecrets.endpoint.length > 0;
|
|
125
|
+
const hasDeployment = typeof providerSecrets.deployment === "string" && providerSecrets.deployment.length > 0;
|
|
126
|
+
const hasManagedId = typeof providerSecrets.managedIdentityClientId === "string" && providerSecrets.managedIdentityClientId.length > 0;
|
|
127
|
+
if (hasEndpoint && hasDeployment && hasManagedId) {
|
|
128
|
+
return { ok: true };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const missing = requiredFields.filter((field) => {
|
|
132
|
+
const val = providerSecrets[field];
|
|
133
|
+
return typeof val !== "string" || val.length === 0;
|
|
134
|
+
});
|
|
135
|
+
if (missing.length > 0) {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
error: `secrets.json for '${agentName}' is missing required ${provider} credentials: ${missing.join(", ")}`,
|
|
139
|
+
fix: `Run 'ouro auth ${agentName}' to set up ${provider} credentials, or add the missing fields to providers.${provider} in ${secretsJsonPath}.`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
(0, runtime_1.emitNervesEvent)({
|
|
143
|
+
component: "daemon",
|
|
144
|
+
event: "daemon.agent_config_valid",
|
|
145
|
+
message: "agent config validation passed",
|
|
146
|
+
meta: { agent: agentName, provider },
|
|
147
|
+
});
|
|
148
|
+
return { ok: true };
|
|
149
|
+
}
|
|
@@ -266,15 +266,38 @@ async function ensureDaemonRunning(deps) {
|
|
|
266
266
|
},
|
|
267
267
|
cleanupStaleSocket: deps.cleanupStaleSocket,
|
|
268
268
|
startDaemonProcess: deps.startDaemonProcess,
|
|
269
|
+
checkSocketAlive: deps.checkSocketAlive,
|
|
269
270
|
});
|
|
270
271
|
}
|
|
271
272
|
deps.cleanupStaleSocket(deps.socketPath);
|
|
272
273
|
const started = await deps.startDaemonProcess(deps.socketPath);
|
|
274
|
+
const pid = started.pid ?? "unknown";
|
|
275
|
+
// Verify the daemon actually comes up before reporting success
|
|
276
|
+
const verified = await verifyDaemonAlive(deps.checkSocketAlive, deps.socketPath);
|
|
277
|
+
/* v8 ignore start -- daemon liveness failure: requires real daemon crash timing @preserve */
|
|
278
|
+
if (!verified) {
|
|
279
|
+
return {
|
|
280
|
+
alreadyRunning: false,
|
|
281
|
+
message: `daemon spawned (pid ${pid}) but failed to respond within 10s — check \`ouro status\` or daemon logs`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/* v8 ignore stop */
|
|
273
285
|
return {
|
|
274
286
|
alreadyRunning: false,
|
|
275
|
-
message: `daemon started (pid ${
|
|
287
|
+
message: `daemon started (pid ${pid})`,
|
|
276
288
|
};
|
|
277
289
|
}
|
|
290
|
+
/* v8 ignore start -- daemon liveness poll: real socket timing untestable in vitest @preserve */
|
|
291
|
+
async function verifyDaemonAlive(checkSocketAlive, socketPath, maxWaitMs = 10_000, pollIntervalMs = 500) {
|
|
292
|
+
const deadline = Date.now() + maxWaitMs;
|
|
293
|
+
while (Date.now() < deadline) {
|
|
294
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
295
|
+
if (await checkSocketAlive(socketPath))
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
/* v8 ignore stop */
|
|
278
301
|
/**
|
|
279
302
|
* Extract `--agent <name>` from an args array, returning the agent name and
|
|
280
303
|
* the remaining args with the flag pair removed.
|
|
@@ -54,6 +54,8 @@ const habit_migration_1 = require("./habit-migration");
|
|
|
54
54
|
const os_cron_deps_1 = require("./os-cron-deps");
|
|
55
55
|
const os_cron_1 = require("./os-cron");
|
|
56
56
|
const daemon_tombstone_1 = require("./daemon-tombstone");
|
|
57
|
+
const os = __importStar(require("os"));
|
|
58
|
+
const agent_config_check_1 = require("./agent-config-check");
|
|
57
59
|
function parseSocketPath(argv) {
|
|
58
60
|
const socketIndex = argv.indexOf("--socket");
|
|
59
61
|
if (socketIndex >= 0) {
|
|
@@ -92,6 +94,12 @@ const processManager = new process_manager_1.DaemonProcessManager({
|
|
|
92
94
|
autoStart: true,
|
|
93
95
|
})),
|
|
94
96
|
existsSync: fs.existsSync,
|
|
97
|
+
/* v8 ignore next 4 -- wiring: delegates to checkAgentConfig which has full unit tests @preserve */
|
|
98
|
+
configCheck: (agent) => {
|
|
99
|
+
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
100
|
+
const secretsRoot = path.join(os.homedir(), ".agentsecrets");
|
|
101
|
+
return (0, agent_config_check_1.checkAgentConfig)(agent, bundlesRoot, secretsRoot);
|
|
102
|
+
},
|
|
95
103
|
});
|
|
96
104
|
const scheduler = new task_scheduler_1.TaskDrivenScheduler({
|
|
97
105
|
agents: [...managedAgents],
|
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ensureCurrentDaemonRuntime = ensureCurrentDaemonRuntime;
|
|
4
4
|
const runtime_1 = require("../../nerves/runtime");
|
|
5
|
+
/* v8 ignore start -- daemon liveness poll: real socket timing untestable in vitest @preserve */
|
|
6
|
+
async function verifyDaemonStarted(deps) {
|
|
7
|
+
if (!deps.checkSocketAlive)
|
|
8
|
+
return true;
|
|
9
|
+
const maxWaitMs = 10_000;
|
|
10
|
+
const pollIntervalMs = 500;
|
|
11
|
+
const deadline = Date.now() + maxWaitMs;
|
|
12
|
+
while (Date.now() < deadline) {
|
|
13
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
14
|
+
if (await deps.checkSocketAlive(deps.socketPath))
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
/* v8 ignore stop */
|
|
5
20
|
function isKnownVersion(version) {
|
|
6
21
|
return version !== "unknown" && version.trim().length > 0;
|
|
7
22
|
}
|
|
@@ -116,11 +131,15 @@ async function ensureCurrentDaemonRuntime(deps) {
|
|
|
116
131
|
}
|
|
117
132
|
deps.cleanupStaleSocket(deps.socketPath);
|
|
118
133
|
const started = await deps.startDaemonProcess(deps.socketPath);
|
|
134
|
+
const pid = started.pid ?? "unknown";
|
|
135
|
+
const verified = await verifyDaemonStarted(deps);
|
|
136
|
+
/* v8 ignore next -- daemon liveness failure: requires real daemon crash timing @preserve */
|
|
137
|
+
const suffix = verified ? "" : " — but daemon failed to respond, check logs";
|
|
119
138
|
result = {
|
|
120
139
|
alreadyRunning: false,
|
|
121
140
|
message: includesVersionDrift
|
|
122
|
-
? `restarted stale daemon from ${runningVersion} to ${deps.localVersion} (pid ${
|
|
123
|
-
: `restarted drifted daemon (${driftSummary}) (pid ${
|
|
141
|
+
? `restarted stale daemon from ${runningVersion} to ${deps.localVersion} (pid ${pid})${suffix}`
|
|
142
|
+
: `restarted drifted daemon (${driftSummary}) (pid ${pid})${suffix}`,
|
|
124
143
|
};
|
|
125
144
|
(0, runtime_1.emitNervesEvent)({
|
|
126
145
|
component: "daemon",
|
|
@@ -54,6 +54,7 @@ class DaemonProcessManager {
|
|
|
54
54
|
cooldownRecoveryMs;
|
|
55
55
|
maxCooldownRetries;
|
|
56
56
|
existsSyncFn;
|
|
57
|
+
configCheckFn;
|
|
57
58
|
constructor(options) {
|
|
58
59
|
this.maxRestartsPerHour = options.maxRestartsPerHour ?? 10;
|
|
59
60
|
this.stabilityThresholdMs = options.stabilityThresholdMs ?? 60_000;
|
|
@@ -66,6 +67,7 @@ class DaemonProcessManager {
|
|
|
66
67
|
this.setTimeoutFn = options.setTimeoutFn ?? ((cb, delay) => setTimeout(cb, delay));
|
|
67
68
|
this.clearTimeoutFn = options.clearTimeoutFn ?? ((timer) => clearTimeout(timer));
|
|
68
69
|
this.existsSyncFn = options.existsSync ?? null;
|
|
70
|
+
this.configCheckFn = options.configCheck ?? null;
|
|
69
71
|
for (const agent of options.agents) {
|
|
70
72
|
this.agents.set(agent.name, {
|
|
71
73
|
config: agent,
|
|
@@ -104,6 +106,22 @@ class DaemonProcessManager {
|
|
|
104
106
|
this.clearRestartTimer(state);
|
|
105
107
|
state.stopRequested = false;
|
|
106
108
|
state.snapshot.status = "starting";
|
|
109
|
+
if (this.configCheckFn) {
|
|
110
|
+
const result = this.configCheckFn(agent);
|
|
111
|
+
if (!result.ok) {
|
|
112
|
+
state.snapshot.status = "crashed";
|
|
113
|
+
(0, runtime_1.emitNervesEvent)({
|
|
114
|
+
level: "error",
|
|
115
|
+
component: "daemon",
|
|
116
|
+
event: "daemon.agent_config_invalid",
|
|
117
|
+
message: result.error ?? "agent config validation failed",
|
|
118
|
+
meta: { agent, fix: result.fix ?? null },
|
|
119
|
+
});
|
|
120
|
+
process.stderr.write(`[daemon] ${agent}: ${result.error}\n` +
|
|
121
|
+
(result.fix ? ` Fix: ${result.fix}\n` : ""));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
107
125
|
const runCwd = (0, identity_1.getRepoRoot)();
|
|
108
126
|
const entryScript = path.join((0, identity_1.getRepoRoot)(), "dist", state.config.entry);
|
|
109
127
|
if (this.existsSyncFn && !this.existsSyncFn(entryScript)) {
|
package/dist/heart/identity.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.HARNESS_CANONICAL_REPO_URL = exports.DEFAULT_AGENT_SENSES = exports.DEFAULT_AGENT_PHRASES = exports.DEFAULT_AGENT_CONTEXT = void 0;
|
|
36
|
+
exports.HARNESS_CANONICAL_REPO_URL = exports.PROVIDER_CREDENTIALS = exports.DEFAULT_AGENT_SENSES = exports.DEFAULT_AGENT_PHRASES = exports.DEFAULT_AGENT_CONTEXT = void 0;
|
|
37
37
|
exports.buildDefaultAgentTemplate = buildDefaultAgentTemplate;
|
|
38
38
|
exports.getAgentName = getAgentName;
|
|
39
39
|
exports.getRepoRoot = getRepoRoot;
|
|
@@ -208,6 +208,17 @@ function resolveOptionalAgentName(agentName) {
|
|
|
208
208
|
function getAgentStateRoot(agentName) {
|
|
209
209
|
return path.join(getAgentRoot(resolveOptionalAgentName(agentName)), "state");
|
|
210
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Required credential fields per provider.
|
|
213
|
+
* Used by daemon config-check to validate secrets before spawning an agent.
|
|
214
|
+
*/
|
|
215
|
+
exports.PROVIDER_CREDENTIALS = {
|
|
216
|
+
azure: ["apiKey", "endpoint", "deployment"],
|
|
217
|
+
minimax: ["apiKey"],
|
|
218
|
+
anthropic: ["setupToken"],
|
|
219
|
+
"openai-codex": ["oauthAccessToken"],
|
|
220
|
+
"github-copilot": ["githubToken", "baseUrl"],
|
|
221
|
+
};
|
|
211
222
|
exports.HARNESS_CANONICAL_REPO_URL = "https://github.com/ouroborosbot/ouroboros.git";
|
|
212
223
|
function getAgentRepoWorkspacesRoot(agentName) {
|
|
213
224
|
return path.join(getAgentStateRoot(resolveOptionalAgentName(agentName)), "workspaces");
|