@ouro.bot/cli 0.1.0-alpha.804 → 0.1.0-alpha.805

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 CHANGED
@@ -1,6 +1,14 @@
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.805",
6
+ "changes": [
7
+ "Add the ACP frontend and daemon socket used by Ouro Workbench to start and supervise a selected boss agent.",
8
+ "Keep Workbench-owned session identity across prompts, approvals, cancellation, journaling, and session subscriptions.",
9
+ "Serialize identity-sensitive turns and per-turn runtime MCP cleanup so frontend state does not carry into the next turn."
10
+ ]
11
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.804",
6
14
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.804",
2
+ "runtimeVersion": "0.1.0-alpha.805",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-09-03T00:00:00.000Z"
5
5
  }
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0"?>
2
2
  <Container version="2">
3
3
  <Name>ouro-butler</Name>
4
- <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.804</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.805</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -967,7 +967,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
967
967
  const facing = (0, friends_1.channelToFacing)(channel);
968
968
  let providerRuntime = options?.providerRuntimeOverride ?? await getProviderRuntime(facing);
969
969
  const provider = providerRuntime.id;
970
- const toolChoiceRequired = options?.toolChoiceRequired ?? true;
970
+ const toolChoiceRequired = options?.hardDisableTools ? false : options?.toolChoiceRequired ?? true;
971
971
  const traceId = options?.traceId;
972
972
  (0, runtime_1.emitNervesEvent)({
973
973
  event: "engine.turn_start",
@@ -1253,15 +1253,17 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1253
1253
  const ordinaryActiveTools = relationshipToolNames
1254
1254
  ? unscopedOrdinaryActiveTools.filter((tool) => relationshipToolNames.includes(tool.function.name))
1255
1255
  : unscopedOrdinaryActiveTools;
1256
- const candidateActiveTools = options?.toolProfile === "sanctuary-health-private"
1257
- ? (() => {
1258
- const sendTools = baseTools.filter((tool) => tool.function.name === "send_message");
1259
- if (channel !== "inner" || sendTools.length !== 1 || baseTools.length !== 1) {
1260
- throw new Error("sanctuary-health-private requires inner channel with exactly one canonical send_message definition");
1261
- }
1262
- return [sendTools[0], tools_1.restTool];
1263
- })()
1264
- : ordinaryActiveTools;
1256
+ const candidateActiveTools = options?.hardDisableTools
1257
+ ? []
1258
+ : options?.toolProfile === "sanctuary-health-private"
1259
+ ? (() => {
1260
+ const sendTools = baseTools.filter((tool) => tool.function.name === "send_message");
1261
+ if (channel !== "inner" || sendTools.length !== 1 || baseTools.length !== 1) {
1262
+ throw new Error("sanctuary-health-private requires inner channel with exactly one canonical send_message definition");
1263
+ }
1264
+ return [sendTools[0], tools_1.restTool];
1265
+ })()
1266
+ : ordinaryActiveTools;
1265
1267
  const candidateToolNames = new Set(candidateActiveTools.map((tool) => tool.function.name));
1266
1268
  const unadvertisedRequiredTool = requiredToolCallNames.find((name) => !candidateToolNames.has(name));
1267
1269
  if (unadvertisedRequiredTool) {
@@ -1328,7 +1330,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1328
1330
  callbacks: turnCallbackBufferRef.current?.callbacks ?? callbacks,
1329
1331
  signal,
1330
1332
  traceId,
1331
- toolChoiceRequired: forcingHistoricalEffect || toolChoiceRequired,
1333
+ toolChoiceRequired: options?.hardDisableTools ? false : forcingHistoricalEffect || toolChoiceRequired,
1332
1334
  reasoningEffort: currentReasoningEffort,
1333
1335
  eagerSettleStreaming: true,
1334
1336
  systemPrompt: structuredSystemPrompt,
@@ -2028,6 +2030,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
2028
2030
  policyDigest,
2029
2031
  policyId: protectedCall.policy.policyId,
2030
2032
  actionClass: protectedCall.policy.actionClass,
2033
+ liveToolContext: augmentedToolContext,
2031
2034
  });
2032
2035
  suspension = {
2033
2036
  approvalId: committed.approvalId,
@@ -40,6 +40,7 @@ var __importStar = (this && this.__importStar) || (function () {
40
40
  })();
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.defaultStartDaemonProcess = defaultStartDaemonProcess;
43
+ exports.defaultStartDaemonForFrontend = defaultStartDaemonForFrontend;
43
44
  exports.readFirstBundleMetaVersion = readFirstBundleMetaVersion;
44
45
  exports.isDaemonLaunchAgentLoaded = isDaemonLaunchAgentLoaded;
45
46
  exports.waitForBootstrappedDaemonSocket = waitForBootstrappedDaemonSocket;
@@ -80,10 +81,7 @@ const provider_credentials_1 = require("../provider-credentials");
80
81
  const startup_tui_1 = require("./startup-tui");
81
82
  const daemon_tombstone_1 = require("./daemon-tombstone");
82
83
  // ── Default implementations ──
83
- async function defaultStartDaemonProcess(socketPath) {
84
- const launchdStarted = await startDaemonProcessViaLaunchd(socketPath);
85
- if (launchdStarted)
86
- return launchdStarted;
84
+ function spawnDetachedDaemonProcess(socketPath, environment = process.env) {
87
85
  const entry = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
88
86
  // Redirect stdio to /dev/null via file descriptors — using 'ignore' causes EPIPE
89
87
  // when the daemon's logging system writes to stderr after the parent exits.
@@ -92,11 +90,30 @@ async function defaultStartDaemonProcess(socketPath) {
92
90
  const child = (0, child_process_1.spawn)(process.execPath, [entry, "--socket", socketPath], {
93
91
  detached: true,
94
92
  stdio: ["ignore", outFd, errFd],
93
+ env: environment,
95
94
  });
96
95
  child.unref();
97
96
  // Don't close fds — the child process needs them. They'll be cleaned up when the parent exits.
98
97
  return { pid: child.pid ?? null };
99
98
  }
99
+ async function defaultStartDaemonProcess(socketPath) {
100
+ const launchdStarted = await startDaemonProcessViaLaunchd(socketPath);
101
+ if (launchdStarted)
102
+ return launchdStarted;
103
+ return spawnDetachedDaemonProcess(socketPath);
104
+ }
105
+ async function defaultStartDaemonForFrontend(socketPath, agent) {
106
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(agent)) {
107
+ throw new Error("frontend daemon startup requires a safe agent name");
108
+ }
109
+ if (process.platform === "darwin") {
110
+ bootoutDaemonLaunchAgentLabel(currentUserUid());
111
+ }
112
+ return spawnDetachedDaemonProcess(socketPath, {
113
+ ...process.env,
114
+ OURO_DAEMON_REQUIRED_AGENT: agent,
115
+ });
116
+ }
100
117
  function defaultWriteStdout(text) {
101
118
  process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
102
119
  }
@@ -711,6 +728,7 @@ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SO
711
728
  : socket_client_1.DEFAULT_DAEMON_COMMAND_TIMEOUT_MS,
712
729
  }),
713
730
  startDaemonProcess: defaultStartDaemonProcess,
731
+ startDaemonForFrontend: defaultStartDaemonForFrontend,
714
732
  writeStdout: defaultWriteStdout,
715
733
  setExitCode: (code) => {
716
734
  const current = typeof process.exitCode === "number" ? process.exitCode : 0;
@@ -48,6 +48,7 @@ exports.ensureDaemonRunning = ensureDaemonRunning;
48
48
  exports.listGithubCopilotModels = listGithubCopilotModels;
49
49
  exports.checkManualCloneBundles = checkManualCloneBundles;
50
50
  exports.resolveWorkbenchRuntimeMcp = resolveWorkbenchRuntimeMcp;
51
+ exports.resolveFrontendFriendId = resolveFrontendFriendId;
51
52
  exports.setupBlueBubblesHostForConnect = setupBlueBubblesHostForConnect;
52
53
  exports.resolveMailImportFilePath = resolveMailImportFilePath;
53
54
  exports.runOuroCli = runOuroCli;
@@ -2921,17 +2922,49 @@ function findInstalledWorkbenchMcp(deps, preferred) {
2921
2922
  * - a string path → use it if it exists, otherwise fall back to discovery
2922
2923
  * (treating the string as the preferred candidate).
2923
2924
  *
2924
- * Returns `{ ouro_workbench: { command, args: [] } }` so the daemon merges it
2925
- * into the boss agent's toolset for the turn without writing to agent.json.
2925
+ * Returns the command plus the exact Workbench runtime coordinates needed by
2926
+ * the daemon-spawned MCP process, without writing them to agent.json.
2926
2927
  */
2927
- function resolveWorkbenchRuntimeMcp(flag, deps) {
2928
+ function resolveWorkbenchRuntimeMcp(flag, deps, environment = process.env) {
2928
2929
  if (flag === undefined)
2929
2930
  return null;
2930
2931
  const preferred = typeof flag === "string" ? flag : null;
2931
2932
  const command = findInstalledWorkbenchMcp(deps, preferred);
2932
2933
  if (!command)
2933
2934
  return null;
2934
- return { ouro_workbench: { command, args: [] } };
2935
+ const keys = [
2936
+ "BUN_BIN",
2937
+ "CMUX_BUNDLED_CLI_PATH",
2938
+ "CMUX_SOCKET_PATH",
2939
+ "CMUX_SOCKET_CAPABILITY",
2940
+ ];
2941
+ const env = Object.fromEntries(keys.map((key) => [key, environment[key]?.trim()]));
2942
+ if (Object.values(env).some((value) => !value))
2943
+ return null;
2944
+ return { ouro_workbench: { command, args: [], env: env } };
2945
+ }
2946
+ async function resolveFrontendFriendId(input) {
2947
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(input.agent))
2948
+ throw new Error("frontend requires a safe agent name");
2949
+ const agentRoot = path.join(input.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(), `${input.agent}.ouro`);
2950
+ if (!input.store)
2951
+ (0, auth_flow_1.readAgentConfigForAgent)(input.agent, input.bundlesRoot);
2952
+ const store = input.store ?? new friends_1.FileFriendStore(path.join(agentRoot, "friends"));
2953
+ if (input.explicitFriendId) {
2954
+ const existing = await store.get(input.explicitFriendId);
2955
+ if (!existing)
2956
+ throw new Error(`friend not found: ${input.explicitFriendId}`);
2957
+ return existing.id;
2958
+ }
2959
+ const ownerUsername = input.ownerUsername === undefined ? (0, friends_1.machineOwnerUsername)() : input.ownerUsername;
2960
+ if (!ownerUsername)
2961
+ throw new Error("local machine owner identity is unavailable");
2962
+ return (await new friends_1.FriendResolver(store, {
2963
+ provider: "local",
2964
+ externalId: ownerUsername,
2965
+ displayName: ownerUsername,
2966
+ channel: "mcp",
2967
+ }).resolve()).friend.id;
2935
2968
  }
2936
2969
  function removeWorkbenchMcpRegistration(agent, deps) {
2937
2970
  const { configPath } = (0, auth_flow_1.readAgentConfigForAgent)(agent, deps.bundlesRoot);
@@ -5128,7 +5161,7 @@ async function executeConnectWorkbench(agent, deps) {
5128
5161
  workbenchHasStaleBundleEntry
5129
5162
  ? "agent.json: removed stale senses.workbench / mcpServers.ouro_workbench entries"
5130
5163
  : "agent.json: no Workbench entries needed or written",
5131
- `boss launch path: ouro mcp-serve --agent ${agent} --workbench-mcp ${executablePath}`,
5164
+ `boss launch path: ouro acp-serve --agent ${agent} --workbench-mcp ${executablePath}`,
5132
5165
  "Workbench tools are injected per served turn; provider secrets stay in the agent vault.",
5133
5166
  ...(syncSummary ? [syncSummary] : []),
5134
5167
  ].join("\n");
@@ -7485,7 +7518,10 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7485
7518
  // bridge on every senseTurn and merged per-turn in the daemon — nothing is
7486
7519
  // written to agent.json.
7487
7520
  const workbenchMcpFlag = command.workbenchMcp;
7488
- const runtimeMcp = resolveWorkbenchRuntimeMcp(workbenchMcpFlag, deps);
7521
+ const runtimeMcp = resolveWorkbenchRuntimeMcp(workbenchMcpFlag, deps, deps.workbenchMcpEnvironment ?? process.env);
7522
+ if (workbenchMcpFlag !== undefined && !runtimeMcp) {
7523
+ throw new Error("Workbench MCP runtime coordinates are unavailable");
7524
+ }
7489
7525
  const server = createMcpServer({
7490
7526
  agent: command.agent,
7491
7527
  friendId,
@@ -7503,6 +7539,59 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7503
7539
  await runMcpServeCliLifecycle(server, mcpInput);
7504
7540
  return "";
7505
7541
  }
7542
+ if (command.kind === "acp-serve") {
7543
+ const createAcpServer = deps.createAcpServer ?? (await Promise.resolve().then(() => __importStar(require("../../senses/acp-server")))).createAcpServer;
7544
+ const resolveFriendId = deps.resolveFrontendFriendId ?? resolveFrontendFriendId;
7545
+ const friendId = await resolveFriendId({
7546
+ agent: command.agent,
7547
+ ...(command.friendId ? { explicitFriendId: command.friendId } : {}),
7548
+ ...(deps.bundlesRoot ? { bundlesRoot: deps.bundlesRoot } : {}),
7549
+ });
7550
+ const commandSocketPath = command.socketOverride ?? deps.socketPath;
7551
+ const { frontendSocketPathForDaemon } = await Promise.resolve().then(() => __importStar(require("../frontend-socket")));
7552
+ const input = deps.acpServeInput ?? process.stdin;
7553
+ const output = deps.acpServeOutput ?? process.stdout;
7554
+ const runtimeMcpServers = command.observeOnly
7555
+ ? undefined
7556
+ : resolveWorkbenchRuntimeMcp(command.workbenchMcp, deps, deps.workbenchMcpEnvironment ?? process.env);
7557
+ if (!command.observeOnly && command.workbenchMcp !== undefined && !runtimeMcpServers) {
7558
+ throw new Error("Workbench MCP runtime coordinates are unavailable");
7559
+ }
7560
+ const daemonResult = deps.ensureDaemonForFrontend
7561
+ ? await deps.ensureDaemonForFrontend(command.agent)
7562
+ : await ensureDaemonRunning({
7563
+ ...deps,
7564
+ startDaemonProcess: (socketPath) => deps.startDaemonForFrontend
7565
+ ? deps.startDaemonForFrontend(socketPath, command.agent)
7566
+ : deps.startDaemonProcess(socketPath),
7567
+ reportDaemonStartupPhase: () => { },
7568
+ });
7569
+ if (!daemonResult.ok) {
7570
+ throw new Error(daemonResult.message);
7571
+ }
7572
+ const server = createAcpServer({
7573
+ agent: command.agent,
7574
+ friendId,
7575
+ frontendSocketPath: frontendSocketPathForDaemon(commandSocketPath),
7576
+ stdin: input,
7577
+ stdout: output,
7578
+ ...(command.observeOnly ? { disableTools: true } : {}),
7579
+ ...(runtimeMcpServers ? { runtimeMcpServers } : {}),
7580
+ });
7581
+ (0, runtime_1.emitNervesEvent)({
7582
+ component: "daemon",
7583
+ event: "daemon.acp_serve_started",
7584
+ message: "ACP frontend server started via CLI",
7585
+ meta: {
7586
+ agent: command.agent,
7587
+ friendId,
7588
+ observeOnly: command.observeOnly === true,
7589
+ workbenchRuntimeMcp: !!runtimeMcpServers,
7590
+ },
7591
+ });
7592
+ await runMcpServeCliLifecycle(server, input);
7593
+ return "";
7594
+ }
7506
7595
  if (command.kind === "private.decisions") {
7507
7596
  let response;
7508
7597
  try {
@@ -345,6 +345,12 @@ exports.COMMAND_REGISTRY = {
345
345
  usage: "ouro mcp-serve --agent <name> [--friend <id>]",
346
346
  example: "ouro mcp-serve --agent ouroboros",
347
347
  },
348
+ "acp-serve": {
349
+ category: "System",
350
+ description: "Start the ACP frontend adapter for an Ouro agent",
351
+ usage: "ouro acp-serve --agent <name> [--friend-id <id>] [--workbench-mcp [path]] [--observe-only]",
352
+ example: "ouro acp-serve --agent ouroboros --workbench-mcp",
353
+ },
348
354
  setup: {
349
355
  category: "System",
350
356
  description: "Register MCP server and hooks for a dev tool",
@@ -13,6 +13,7 @@ exports.isAgentProvider = isAgentProvider;
13
13
  exports.usage = usage;
14
14
  exports.inferAgentNameFromRemote = inferAgentNameFromRemote;
15
15
  exports.parseMcpServeCommand = parseMcpServeCommand;
16
+ exports.parseAcpServeCommand = parseAcpServeCommand;
16
17
  exports.parseOuroCommand = parseOuroCommand;
17
18
  const friends_1 = require("@ouro.bot/friends");
18
19
  const cli_help_1 = require("./cli-help");
@@ -1686,6 +1687,55 @@ function parseMcpServeCommand(args) {
1686
1687
  ...(workbenchMcp !== undefined ? { workbenchMcp } : {}),
1687
1688
  };
1688
1689
  }
1690
+ function parseAcpServeCommand(args) {
1691
+ const usage = "Usage: ouro acp-serve --agent <name> [--friend-id <id>] [--socket <path>] [--workbench-mcp [path]] [--observe-only]";
1692
+ let agent;
1693
+ let friendId;
1694
+ let socketOverride;
1695
+ let workbenchMcp;
1696
+ let observeOnly = false;
1697
+ for (let i = 0; i < args.length; i++) {
1698
+ const token = args[i];
1699
+ if (token === "--workbench-mcp") {
1700
+ const next = args[i + 1];
1701
+ if (next && !next.startsWith("--"))
1702
+ workbenchMcp = args[++i];
1703
+ else
1704
+ workbenchMcp = true;
1705
+ continue;
1706
+ }
1707
+ if (token === "--observe-only") {
1708
+ observeOnly = true;
1709
+ continue;
1710
+ }
1711
+ const next = args[i + 1];
1712
+ if (token === "--agent" || token === "--friend-id" || token === "--socket") {
1713
+ if (!next || next.startsWith("--"))
1714
+ throw new Error(`${token} requires a value\n${usage}`);
1715
+ if (token === "--agent")
1716
+ agent = next;
1717
+ else if (token === "--friend-id")
1718
+ friendId = next;
1719
+ else
1720
+ socketOverride = next;
1721
+ i += 1;
1722
+ continue;
1723
+ }
1724
+ throw new Error(usage);
1725
+ }
1726
+ if (!agent)
1727
+ throw new Error(`acp-serve requires --agent <name>\n${usage}`);
1728
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(agent))
1729
+ throw new Error("acp-serve requires a safe agent name");
1730
+ return {
1731
+ kind: "acp-serve",
1732
+ agent,
1733
+ ...(friendId ? { friendId } : {}),
1734
+ ...(socketOverride ? { socketOverride } : {}),
1735
+ ...(workbenchMcp !== undefined ? { workbenchMcp } : {}),
1736
+ ...(observeOnly ? { observeOnly: true } : {}),
1737
+ };
1738
+ }
1689
1739
  function parseSetupCommand(args) {
1690
1740
  let tool;
1691
1741
  let agent;
@@ -2675,6 +2725,8 @@ function parseOuroCommand(args) {
2675
2725
  return parseNervesReviewCommand(args.slice(1));
2676
2726
  if (head === "link")
2677
2727
  return parseLinkCommand(args.slice(1));
2728
+ if (head === "acp-serve")
2729
+ return parseAcpServeCommand(args.slice(1));
2678
2730
  if (head === "mcp-serve")
2679
2731
  return parseMcpServeCommand(args.slice(1));
2680
2732
  if (head === "setup")
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PUBLIC_DAEMON_STARTUP_FAILURE_REASON = void 0;
4
+ exports.providerReadinessAgents = providerReadinessAgents;
4
5
  exports.createProviderReadinessPreparationFailure = createProviderReadinessPreparationFailure;
5
6
  exports.createSanctuaryBundlePreparationFailure = createSanctuaryBundlePreparationFailure;
6
7
  exports.failFastContainerCredentialBootstrapStartup = failFastContainerCredentialBootstrapStartup;
@@ -20,6 +21,13 @@ exports.PUBLIC_DAEMON_STARTUP_FAILURE_REASON = "startupFailurePublic";
20
21
  class DaemonPreparationFailure extends Error {
21
22
  name = "DaemonPreparationFailure";
22
23
  }
24
+ function providerReadinessAgents(managedAgents, requiredAgent) {
25
+ const candidate = requiredAgent?.trim();
26
+ if (!candidate || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(candidate)) {
27
+ return [...managedAgents];
28
+ }
29
+ return managedAgents.includes(candidate) ? [candidate] : [...managedAgents];
30
+ }
23
31
  function createProviderReadinessPreparationFailure(issues) {
24
32
  const lines = ["Provider checks need attention"];
25
33
  for (const issue of issues) {
@@ -94,6 +94,7 @@ const socketPath = parseSocketPath(process.argv);
94
94
  const entryPath = path.resolve(__dirname, "daemon-entry.js");
95
95
  const mode = (0, runtime_mode_1.detectRuntimeMode)((0, identity_1.getRepoRoot)());
96
96
  const managedAgents = (0, agent_discovery_1.listEnabledBundleAgents)();
97
+ const readinessAgents = (0, daemon_bootstrap_startup_1.providerReadinessAgents)(managedAgents, process.env.OURO_DAEMON_REQUIRED_AGENT);
97
98
  const sanctuaryPackageManagement = (0, sanctuary_package_management_1.resolveSanctuaryPackageManagementActivation)({ mode, argv: process.argv, managedAgents, repoRoot: (0, identity_1.getRepoRoot)(), bundlesRoot: (0, identity_1.getAgentBundlesRoot)(), runtimePackageVersion: (0, bundle_manifest_1.getPackageVersion)() });
98
99
  try {
99
100
  (0, sanctuary_package_management_1.requireSanctuaryPackageManagementDecision)(sanctuaryPackageManagement);
@@ -598,7 +599,7 @@ function writeStopCommandHealthState() {
598
599
  }
599
600
  async function prepareProviderRuntime() {
600
601
  const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
601
- const readiness = await Promise.all(managedAgents.map(async (agent) => {
602
+ const readiness = await Promise.all(readinessAgents.map(async (agent) => {
602
603
  try {
603
604
  await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agent, { preserveCachedOnFailure: true });
604
605
  const result = await (0, agent_config_check_1.checkAgentConfigWithProviderHealth)(agent, bundlesRoot);
@@ -82,6 +82,11 @@ const router_1 = require("../external-events/router");
82
82
  const habit_policy_1 = require("../../rsvp/habit-policy");
83
83
  const container_runtime_1 = require("./container-runtime");
84
84
  const habit_session_1 = require("../habits/habit-session");
85
+ const turn_execution_lease_1 = require("../turn-execution-lease");
86
+ const frontend_session_service_1 = require("../frontend-session-service");
87
+ const frontend_socket_1 = require("../frontend-socket");
88
+ const frontend_journal_1 = require("../frontend-journal");
89
+ const frontend_approval_runtime_1 = require("../frontend-approval-runtime");
85
90
  const PIDFILE_PATH = path.join(os.homedir(), ".ouro-cli", "daemon.pids");
86
91
  /**
87
92
  * Defense-in-depth: detect if we're running under vitest. The pidfile lives
@@ -556,33 +561,35 @@ function taskIdFromTaskPokePrivateWakeCommand(command) {
556
561
  * restarts the daemon process (fresh module cache).
557
562
  */
558
563
  async function handleAgentSenseTurn(command, runtime) {
559
- try {
560
- const { setAgentName } = await Promise.resolve().then(() => __importStar(require("../identity")));
561
- setAgentName(command.agent);
562
- const { runSenseTurn } = await Promise.resolve().then(() => __importStar(require("../../senses/shared-turn")));
563
- const result = await runSenseTurn({
564
- agentName: command.agent,
565
- channel: command.channel,
566
- sessionKey: command.sessionKey,
567
- friendId: command.friendId,
568
- userMessage: command.message,
569
- ...(runtime?.socketPath ? { toolContext: { daemonSocketPath: runtime.socketPath } } : {}),
570
- // Per-turn, per-agent runtime MCP injection (e.g. Workbench's ouro_workbench).
571
- // Scoped to THIS turn only — never stored as module state, so a concurrent
572
- // turn for a different agent cannot inherit these servers.
573
- ...(command.runtimeMcp ? { runtimeMcpServers: command.runtimeMcp } : {}),
574
- });
575
- return {
576
- ok: true,
577
- message: result.response,
578
- data: { ponderDeferred: result.ponderDeferred },
579
- };
580
- }
581
- catch (error) {
582
- /* v8 ignore next -- branch: String(error) fallback only for non-Error throws @preserve */
583
- const errorMessage = error instanceof Error ? error.message : String(error);
584
- return { ok: false, error: `sense turn failed: ${errorMessage}` };
585
- }
564
+ return (0, turn_execution_lease_1.withTurnExecutionLease)(async () => {
565
+ try {
566
+ const { setAgentName } = await Promise.resolve().then(() => __importStar(require("../identity")));
567
+ setAgentName(command.agent);
568
+ const { runSenseTurn } = await Promise.resolve().then(() => __importStar(require("../../senses/shared-turn")));
569
+ const result = await runSenseTurn({
570
+ agentName: command.agent,
571
+ channel: command.channel,
572
+ sessionKey: command.sessionKey,
573
+ friendId: command.friendId,
574
+ userMessage: command.message,
575
+ ...(runtime?.socketPath ? { toolContext: { daemonSocketPath: runtime.socketPath } } : {}),
576
+ // Per-turn, per-agent runtime MCP injection (e.g. Workbench's ouro_workbench).
577
+ // Scoped to THIS turn only — never stored as module state, so a concurrent
578
+ // turn for a different agent cannot inherit these servers.
579
+ ...(command.runtimeMcp ? { runtimeMcpServers: command.runtimeMcp } : {}),
580
+ });
581
+ return {
582
+ ok: true,
583
+ message: result.response,
584
+ data: { ponderDeferred: result.ponderDeferred },
585
+ };
586
+ }
587
+ catch (error) {
588
+ /* v8 ignore next -- branch: String(error) fallback only for non-Error throws @preserve */
589
+ const errorMessage = error instanceof Error ? error.message : String(error);
590
+ return { ok: false, error: `sense turn failed: ${errorMessage}` };
591
+ }
592
+ });
586
593
  }
587
594
  async function handleAgentAskTurn(command, runtime) {
588
595
  /* v8 ignore start -- ask command parameter defaults are legacy MCP compatibility; send_message shares the primary path @preserve */
@@ -614,12 +621,16 @@ class OuroDaemon {
614
621
  mode;
615
622
  server = null;
616
623
  mailboxServer = null;
624
+ frontendSocketServer = null;
617
625
  socketIdentity = null;
618
626
  senseAutostartTimer = null;
619
627
  externalEventReconcileTimer = null;
620
628
  externalEventReconcileRunning = false;
621
629
  constructedAtMs = Date.now();
622
630
  mailboxServerFactory;
631
+ frontendSocketPath;
632
+ frontendSessionService;
633
+ frontendSocketServerFactory;
623
634
  privateRuntimePolicyDeps;
624
635
  onStopCommandComplete;
625
636
  externalEventRoot;
@@ -644,6 +655,17 @@ class OuroDaemon {
644
655
  this.bundlesRoot = options.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
645
656
  this.mode = options.mode ?? "production";
646
657
  this.mailboxServerFactory = options.mailboxServerFactory ?? this.createDefaultMailboxServer.bind(this);
658
+ this.frontendSocketPath = options.frontendSocketPath ?? (0, frontend_socket_1.frontendSocketPathForDaemon)(this.socketPath);
659
+ /* v8 ignore next -- path-only default adapter; frontend service tests inject and verify the same root contract @preserve */
660
+ const frontendAgentRoot = (agent) => path.join(this.bundlesRoot, `${agent}.ouro`);
661
+ this.frontendSessionService = options.frontendSessionService ?? new frontend_session_service_1.FrontendSessionService({
662
+ journal: new frontend_journal_1.FrontendJournalStore({ agentRoot: frontendAgentRoot }),
663
+ authority: (0, frontend_approval_runtime_1.createFrontendApprovalRuntime)({
664
+ agentRoot: frontendAgentRoot,
665
+ settleApproval: frontend_approval_runtime_1.settleFrontendApproval,
666
+ }),
667
+ });
668
+ this.frontendSocketServerFactory = options.frontendSocketServerFactory ?? frontend_socket_1.startFrontendSocketServer;
647
669
  this.privateRuntimePolicyDeps = options.privateRuntimePolicyDeps ?? {};
648
670
  this.onStopCommandComplete = options.onStopCommandComplete ?? null;
649
671
  this.externalEventRoot = options.externalEventRoot ?? null;
@@ -848,6 +870,21 @@ class OuroDaemon {
848
870
  meta: { port: mailbox_types_1.MAILBOX_DEFAULT_PORT },
849
871
  });
850
872
  }
873
+ try {
874
+ this.frontendSocketServer = await this.frontendSocketServerFactory({
875
+ socketPath: this.frontendSocketPath,
876
+ service: this.frontendSessionService,
877
+ });
878
+ }
879
+ catch (error) {
880
+ (0, runtime_1.emitNervesEvent)({
881
+ level: "warn",
882
+ component: "daemon",
883
+ event: "daemon.frontend_socket_start_failed",
884
+ message: `Frontend socket failed to start: ${String(error)}`,
885
+ meta: { socketPath: this.frontendSocketPath },
886
+ });
887
+ }
851
888
  }
852
889
  triggerAutoStartAgents() {
853
890
  if (this.processManager.triggerAutoStartAgents) {
@@ -1117,6 +1154,12 @@ class OuroDaemon {
1117
1154
  message: "stopping daemon server",
1118
1155
  meta: { socketPath: this.socketPath },
1119
1156
  });
1157
+ this.frontendSessionService.cancelAllTurns();
1158
+ if (this.frontendSocketServer) {
1159
+ await this.frontendSocketServer.stop();
1160
+ this.frontendSocketServer = null;
1161
+ }
1162
+ this.frontendSessionService.close?.();
1120
1163
  (0, update_checker_1.stopUpdateChecker)();
1121
1164
  (0, mcp_manager_1.shutdownSharedMcpManager)();
1122
1165
  this.scheduler.stop?.();
@@ -2402,28 +2445,32 @@ class OuroDaemon {
2402
2445
  return this.handlePrivateRuntimeWake(this.buildAwaitPrivateWakeCommand(command));
2403
2446
  }
2404
2447
  case "mcp.list": {
2405
- (0, identity_1.setAgentName)(command.agent ?? "default");
2406
- const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)();
2407
- if (!mcpManager) {
2408
- return { ok: true, data: [], message: "no MCP servers configured" };
2409
- }
2410
- return { ok: true, data: mcpManager.listAllTools() };
2448
+ return (0, turn_execution_lease_1.withTurnExecutionLease)(async () => {
2449
+ (0, identity_1.setAgentName)(command.agent ?? "default");
2450
+ const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)();
2451
+ if (!mcpManager) {
2452
+ return { ok: true, data: [], message: "no MCP servers configured" };
2453
+ }
2454
+ return { ok: true, data: mcpManager.listAllTools() };
2455
+ });
2411
2456
  }
2412
2457
  case "mcp.call": {
2413
- (0, identity_1.setAgentName)(command.agent ?? "default");
2414
- const mcpCallManager = await (0, mcp_manager_1.getSharedMcpManager)();
2415
- if (!mcpCallManager) {
2416
- return { ok: false, error: "no MCP servers configured" };
2417
- }
2418
- try {
2419
- const parsedArgs = command.args ? JSON.parse(command.args) : {};
2420
- const result = await mcpCallManager.callTool(command.server, command.tool, parsedArgs);
2421
- return { ok: true, data: result };
2422
- }
2423
- catch (error) {
2424
- /* v8 ignore next -- defensive: callTool errors are always Error instances @preserve */
2425
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
2426
- }
2458
+ return (0, turn_execution_lease_1.withTurnExecutionLease)(async () => {
2459
+ (0, identity_1.setAgentName)(command.agent ?? "default");
2460
+ const mcpCallManager = await (0, mcp_manager_1.getSharedMcpManager)();
2461
+ if (!mcpCallManager) {
2462
+ return { ok: false, error: "no MCP servers configured" };
2463
+ }
2464
+ try {
2465
+ const parsedArgs = command.args ? JSON.parse(command.args) : {};
2466
+ const result = await mcpCallManager.callTool(command.server, command.tool, parsedArgs);
2467
+ return { ok: true, data: result };
2468
+ }
2469
+ catch (error) {
2470
+ /* v8 ignore next -- defensive: callTool errors are always Error instances @preserve */
2471
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
2472
+ }
2473
+ });
2427
2474
  }
2428
2475
  case "hatch.start":
2429
2476
  return {
@@ -359,7 +359,7 @@ function senseRepairHint(agent, sense) {
359
359
  }
360
360
  /* v8 ignore next -- Workbench is deliberately not daemon-managed, so getSenseInventory never asks the daemon manager for a repair hint @preserve */
361
361
  if (sense === "workbench") {
362
- return `Agent-runnable: install Ouro Workbench.app, then launch the boss through Workbench or run 'ouro mcp-serve --agent ${agent} --workbench-mcp'; 'ouro connect workbench --agent ${agent}' only cleans stale bundle entries.`;
362
+ return `Agent-runnable: install Ouro Workbench.app, then launch the boss through Workbench or run 'ouro acp-serve --agent ${agent} --workbench-mcp'; 'ouro connect workbench --agent ${agent}' only cleans stale bundle entries.`;
363
363
  }
364
364
  return `Run 'ouro connect bluebubbles --agent ${agent}' to attach BlueBubbles on this machine; then run 'ouro up' again.`;
365
365
  }