@ouro.bot/cli 0.1.0-alpha.894 → 0.1.0-alpha.897
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 +18 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/daemon/runtime-logging.js +1 -1
- package/dist/heart/daemon/sense-manager.js +20 -20
- package/dist/heart/identity.js +4 -4
- package/dist/heart/private-runtime/policy.js +4 -4
- package/dist/heart/sense-truth.js +2 -2
- package/dist/heart/{cmux-config.js → shepherd-config.js} +24 -24
- package/dist/heart/steward-policy.js +0 -26
- package/dist/heart/turn-context.js +5 -5
- package/dist/mind/prompt.js +5 -5
- package/dist/repertoire/bitwarden-store.js +153 -7
- package/dist/repertoire/credential-access.js +27 -23
- package/dist/repertoire/guardrails.js +4 -4
- package/dist/repertoire/tools-shepherd.js +181 -0
- package/dist/repertoire/tools.js +4 -4
- package/dist/repertoire/vault-unlock.js +59 -1
- package/dist/senses/{cmux → shepherd}/client.js +3 -3
- package/dist/senses/shepherd/cmux.js +332 -0
- package/dist/senses/shepherd/host.js +2 -0
- package/dist/senses/shepherd/judge.js +98 -0
- package/dist/senses/{cmux → shepherd}/redact.js +3 -3
- package/dist/senses/shepherd/returns.js +79 -0
- package/dist/senses/shepherd/sense.js +288 -0
- package/dist/senses/{cmux-entry.js → shepherd-entry.js} +10 -9
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/repertoire/tools-cmux.js +0 -396
- package/dist/senses/cmux/answer.js +0 -191
- package/dist/senses/cmux/attention.js +0 -265
- package/dist/senses/cmux/casebook.js +0 -416
- package/dist/senses/cmux/floor.js +0 -407
- package/dist/senses/cmux/principles.js +0 -72
- package/dist/senses/cmux/sense.js +0 -287
|
@@ -1,396 +0,0 @@
|
|
|
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.cmuxToolDefinitions = exports.cmuxCorrectToolDefinition = exports.cmuxReplyOnceToolDefinition = exports.cmuxSignalToolDefinition = exports.cmuxReadToolDefinition = exports.cmuxOverviewToolDefinition = exports.CMUX_STATUS_KEY = void 0;
|
|
37
|
-
exports.cmuxToolsEnabled = cmuxToolsEnabled;
|
|
38
|
-
const fs = __importStar(require("node:fs"));
|
|
39
|
-
const path = __importStar(require("node:path"));
|
|
40
|
-
const cmux_config_1 = require("../heart/cmux-config");
|
|
41
|
-
const runtime_credentials_1 = require("../heart/runtime-credentials");
|
|
42
|
-
const runtime_1 = require("../nerves/runtime");
|
|
43
|
-
const attention_1 = require("../senses/cmux/attention");
|
|
44
|
-
const answer_1 = require("../senses/cmux/answer");
|
|
45
|
-
const casebook_1 = require("../senses/cmux/casebook");
|
|
46
|
-
const client_1 = require("../senses/cmux/client");
|
|
47
|
-
const principles_1 = require("../senses/cmux/principles");
|
|
48
|
-
const redact_1 = require("../senses/cmux/redact");
|
|
49
|
-
/**
|
|
50
|
-
* The cmux sense's tools: who needs the human (`cmux_overview`), what a terminal shows
|
|
51
|
-
* (`cmux_read`), and a one-line status or desktop notification for the human (`cmux_signal`).
|
|
52
|
-
* They talk to cmux's control socket directly with this machine's cmux credential.
|
|
53
|
-
*/
|
|
54
|
-
exports.CMUX_STATUS_KEY = "ouro";
|
|
55
|
-
const STATUS_MAX_CHARS = 120;
|
|
56
|
-
const READ_MAX_CHARS = 16_000;
|
|
57
|
-
const REQUEST_PREVIEW_CHARS = 600;
|
|
58
|
-
function isRecord(value) {
|
|
59
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
60
|
-
}
|
|
61
|
-
function text(value) {
|
|
62
|
-
return typeof value === "string" ? value : "";
|
|
63
|
-
}
|
|
64
|
-
function list(value) {
|
|
65
|
-
return Array.isArray(value) ? value.filter(isRecord) : [];
|
|
66
|
-
}
|
|
67
|
-
function cmuxToolsEnabled(agentRoot) {
|
|
68
|
-
if (!agentRoot)
|
|
69
|
-
return false;
|
|
70
|
-
try {
|
|
71
|
-
const config = JSON.parse(fs.readFileSync(path.join(agentRoot, "agent.json"), "utf-8"));
|
|
72
|
-
return config.senses?.cmux?.enabled === true;
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
function clientFor(ctx) {
|
|
79
|
-
if (!ctx?.agentName || !ctx.agentRoot)
|
|
80
|
-
return "the cmux tools need an agent runtime";
|
|
81
|
-
const machine = (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(ctx.agentName);
|
|
82
|
-
const resolved = (0, cmux_config_1.resolveCmuxConnection)(ctx.agentName, machine.ok ? machine.config : {});
|
|
83
|
-
return resolved.ok ? (0, client_1.createCmuxClient)(resolved.connection, { agentName: ctx.agentName }) : resolved.error;
|
|
84
|
-
}
|
|
85
|
-
function workspacesFrom(tree) {
|
|
86
|
-
return list(tree.windows).flatMap((window) => list(window.workspaces)).map((workspace) => ({
|
|
87
|
-
id: text(workspace.id),
|
|
88
|
-
ref: text(workspace.ref),
|
|
89
|
-
title: text(workspace.title),
|
|
90
|
-
selected: workspace.selected === true,
|
|
91
|
-
surfaces: list(workspace.panes).flatMap((pane) => list(pane.surfaces)).map((surface) => ({
|
|
92
|
-
id: text(surface.id), ref: text(surface.ref), title: text(surface.title), type: text(surface.type),
|
|
93
|
-
})),
|
|
94
|
-
}));
|
|
95
|
-
}
|
|
96
|
-
async function readTree(client) {
|
|
97
|
-
return workspacesFrom(await client.call("system.tree", { all_windows: true }));
|
|
98
|
-
}
|
|
99
|
-
function failure(tool, error) {
|
|
100
|
-
const message = error.message;
|
|
101
|
-
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "repertoire", event: "repertoire.cmux_tool_error", message: "cmux tool call failed", meta: { tool, error: message } });
|
|
102
|
-
return JSON.stringify({ error: message });
|
|
103
|
-
}
|
|
104
|
-
async function overview(ctx) {
|
|
105
|
-
const client = clientFor(ctx);
|
|
106
|
-
if (typeof client === "string")
|
|
107
|
-
return JSON.stringify({ error: client });
|
|
108
|
-
const state = (0, attention_1.readCmuxState)(path.join(ctx.agentRoot, "state", "senses", "cmux", "state.json"), new Date().toISOString());
|
|
109
|
-
try {
|
|
110
|
-
const workspaces = await readTree(client);
|
|
111
|
-
const pending = (0, attention_1.pendingFeedItems)(await client.call("feed.list", { pending_only: true }));
|
|
112
|
-
const refFor = (surfaceId) => {
|
|
113
|
-
for (const workspace of workspaces) {
|
|
114
|
-
const surface = workspace.surfaces.find((entry) => entry.id === surfaceId);
|
|
115
|
-
if (surface)
|
|
116
|
-
return { workspace: workspace.ref, surface: surface.ref };
|
|
117
|
-
}
|
|
118
|
-
return { workspace: null, surface: null };
|
|
119
|
-
};
|
|
120
|
-
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.cmux_overview", message: "read cmux overview", meta: { workspaces: workspaces.length, waiting: pending.length } });
|
|
121
|
-
return JSON.stringify({
|
|
122
|
-
sense: { connected: state.connected, lastError: state.lastError, cmuxVersion: state.cmuxVersion },
|
|
123
|
-
principles: (0, principles_1.readCmuxPrinciples)(ctx.agentRoot),
|
|
124
|
-
waitingOnHuman: pending.map((item) => {
|
|
125
|
-
const where = refFor((0, attention_1.surfaceForSession)(state, item.workstreamId)?.surfaceId);
|
|
126
|
-
return {
|
|
127
|
-
requestId: item.requestId,
|
|
128
|
-
kind: item.kind,
|
|
129
|
-
agent: item.source,
|
|
130
|
-
tool: item.toolName,
|
|
131
|
-
cwd: item.cwd,
|
|
132
|
-
createdAt: item.createdAt,
|
|
133
|
-
workspace: where.workspace,
|
|
134
|
-
surface: where.surface,
|
|
135
|
-
request: item.toolInput ? (0, redact_1.redactSecrets)(item.toolInput).slice(0, REQUEST_PREVIEW_CHARS) : null,
|
|
136
|
-
requestTruncated: item.toolInputTruncated || (item.toolInput?.length ?? 0) > REQUEST_PREVIEW_CHARS,
|
|
137
|
-
};
|
|
138
|
-
}),
|
|
139
|
-
workspaces: workspaces.map((workspace) => ({
|
|
140
|
-
ref: workspace.ref,
|
|
141
|
-
title: workspace.title,
|
|
142
|
-
selected: workspace.selected,
|
|
143
|
-
surfaces: workspace.surfaces.map((surface) => {
|
|
144
|
-
const activity = state.surfaces[surface.id];
|
|
145
|
-
return {
|
|
146
|
-
ref: surface.ref,
|
|
147
|
-
title: surface.title,
|
|
148
|
-
type: surface.type,
|
|
149
|
-
...(activity ? { agent: activity.agent, lifecycle: activity.lifecycle, lastHook: activity.lastHook, lastTool: activity.lastTool, lastActivityAt: activity.at, cwd: activity.cwd } : {}),
|
|
150
|
-
};
|
|
151
|
-
}),
|
|
152
|
-
})),
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
catch (error) {
|
|
156
|
-
return failure("cmux_overview", error);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
exports.cmuxOverviewToolDefinition = {
|
|
160
|
-
tool: {
|
|
161
|
-
type: "function",
|
|
162
|
-
function: {
|
|
163
|
-
name: "cmux_overview",
|
|
164
|
-
description: "Who needs the human, across every coding agent running in cmux on this Mac. Returns the decisions cmux is holding open (permission requests, questions, plan approvals: which agent, tool, folder, terminal, and a redacted preview of the request), and every workspace and terminal with what its agent was last doing (working, idle = finished its turn and waiting for a prompt, waiting = blocked on the human, ended). Read-only.",
|
|
165
|
-
parameters: { type: "object", properties: {}, additionalProperties: false },
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
handler: (_args, ctx) => overview(ctx),
|
|
169
|
-
summaryKeys: [],
|
|
170
|
-
riskProfile: { mutates: "none", risk: "low" },
|
|
171
|
-
};
|
|
172
|
-
function boundedLines(value) {
|
|
173
|
-
const parsed = typeof value === "number" ? value : Number.parseInt(text(value), 10);
|
|
174
|
-
return Number.isFinite(parsed) ? Math.min(400, Math.max(1, Math.trunc(parsed))) : 60;
|
|
175
|
-
}
|
|
176
|
-
exports.cmuxReadToolDefinition = {
|
|
177
|
-
tool: {
|
|
178
|
-
type: "function",
|
|
179
|
-
function: {
|
|
180
|
-
name: "cmux_read",
|
|
181
|
-
description: "Read the text on one cmux terminal (what a coding agent session shows now, or with scrollback what it did recently). Secret-shaped strings are redacted and the text is bounded. Read-only. Take the surface ref (for example surface:12) from cmux_overview.",
|
|
182
|
-
parameters: {
|
|
183
|
-
type: "object",
|
|
184
|
-
properties: {
|
|
185
|
-
surface: { type: "string", description: "Surface ref (surface:12) or id from cmux_overview." },
|
|
186
|
-
lines: { type: "number", description: "How many lines, 1-400. Default 60." },
|
|
187
|
-
scrollback: { type: "boolean", description: "Include scrollback above the visible screen." },
|
|
188
|
-
},
|
|
189
|
-
required: ["surface"],
|
|
190
|
-
additionalProperties: false,
|
|
191
|
-
},
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
handler: async (args, ctx) => {
|
|
195
|
-
const surface = text(args.surface).trim();
|
|
196
|
-
if (!surface)
|
|
197
|
-
return JSON.stringify({ error: "name a surface ref or id from cmux_overview" });
|
|
198
|
-
const client = clientFor(ctx);
|
|
199
|
-
if (typeof client === "string")
|
|
200
|
-
return JSON.stringify({ error: client });
|
|
201
|
-
try {
|
|
202
|
-
const scrollback = args.scrollback === true || args.scrollback === "true";
|
|
203
|
-
const result = await client.call("surface.read_text", { surface_id: surface, lines: boundedLines(args.lines), scrollback });
|
|
204
|
-
const screen = (0, redact_1.redactSecrets)(text(result.text));
|
|
205
|
-
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.cmux_read", message: "read cmux terminal text", meta: { chars: screen.length, scrollback } });
|
|
206
|
-
return JSON.stringify({
|
|
207
|
-
surface: text(result.surface_ref) || surface,
|
|
208
|
-
workspace: text(result.workspace_ref) || null,
|
|
209
|
-
text: screen.length > READ_MAX_CHARS ? screen.slice(-READ_MAX_CHARS) : screen,
|
|
210
|
-
truncated: screen.length > READ_MAX_CHARS,
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
catch (error) {
|
|
214
|
-
return failure("cmux_read", error);
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
summaryKeys: ["surface"],
|
|
218
|
-
riskProfile: { mutates: "none", risk: "low" },
|
|
219
|
-
};
|
|
220
|
-
/** A v1 socket argument: quoted, with the characters cmux's tokenizer treats as special escaped. */
|
|
221
|
-
function socketQuote(value) {
|
|
222
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
223
|
-
}
|
|
224
|
-
exports.cmuxSignalToolDefinition = {
|
|
225
|
-
tool: {
|
|
226
|
-
type: "function",
|
|
227
|
-
function: {
|
|
228
|
-
name: "cmux_signal",
|
|
229
|
-
description: `Tell the human something through cmux on this Mac. status sets this agent's one-line status pill on a workspace's sidebar entry (at most ${STATUS_MAX_CHARS} characters; an empty string clears it). notify_title with notify_body posts a desktop notification tied to that workspace. Give at least one. It changes only what cmux shows; it never types into a terminal or answers an agent.`,
|
|
230
|
-
parameters: {
|
|
231
|
-
type: "object",
|
|
232
|
-
properties: {
|
|
233
|
-
workspace: { type: "string", description: "Workspace ref (workspace:3) or id from cmux_overview." },
|
|
234
|
-
status: { type: "string", description: "One line for the sidebar status, or an empty string to clear it." },
|
|
235
|
-
notify_title: { type: "string", description: "Desktop notification title." },
|
|
236
|
-
notify_body: { type: "string", description: "Desktop notification body." },
|
|
237
|
-
},
|
|
238
|
-
required: ["workspace"],
|
|
239
|
-
additionalProperties: false,
|
|
240
|
-
},
|
|
241
|
-
},
|
|
242
|
-
},
|
|
243
|
-
handler: async (args, ctx) => {
|
|
244
|
-
const target = text(args.workspace).trim();
|
|
245
|
-
const status = typeof args.status === "string" ? args.status.trim() : null;
|
|
246
|
-
const title = text(args.notify_title).trim();
|
|
247
|
-
if (!target)
|
|
248
|
-
return JSON.stringify({ error: "name a workspace ref or id from cmux_overview" });
|
|
249
|
-
if (status === null && !title)
|
|
250
|
-
return JSON.stringify({ error: "give a status, a notify_title, or both" });
|
|
251
|
-
if (status !== null && (status.length > STATUS_MAX_CHARS || /[\r\n]/.test(status)))
|
|
252
|
-
return JSON.stringify({ error: `status must be one line of at most ${STATUS_MAX_CHARS} characters` });
|
|
253
|
-
const client = clientFor(ctx);
|
|
254
|
-
if (typeof client === "string")
|
|
255
|
-
return JSON.stringify({ error: client });
|
|
256
|
-
try {
|
|
257
|
-
const workspace = (await readTree(client)).find((entry) => entry.ref === target || entry.id === target);
|
|
258
|
-
if (!workspace)
|
|
259
|
-
return JSON.stringify({ error: `no cmux workspace ${target}; see cmux_overview` });
|
|
260
|
-
const done = [];
|
|
261
|
-
if (status !== null) {
|
|
262
|
-
await client.command(status
|
|
263
|
-
? `set_status ${exports.CMUX_STATUS_KEY} ${socketQuote(status)} --tab=${workspace.id}`
|
|
264
|
-
: `clear_status ${exports.CMUX_STATUS_KEY} --tab=${workspace.id}`);
|
|
265
|
-
done.push(status ? "status set" : "status cleared");
|
|
266
|
-
}
|
|
267
|
-
if (title) {
|
|
268
|
-
await client.call("notification.create", { title, subtitle: "", body: text(args.notify_body), workspace_id: workspace.id });
|
|
269
|
-
done.push("notification sent");
|
|
270
|
-
}
|
|
271
|
-
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.cmux_signal", message: "signalled the human through cmux", meta: { done } });
|
|
272
|
-
return JSON.stringify({ workspace: workspace.ref, done });
|
|
273
|
-
}
|
|
274
|
-
catch (error) {
|
|
275
|
-
return failure("cmux_signal", error);
|
|
276
|
-
}
|
|
277
|
-
},
|
|
278
|
-
summaryKeys: ["workspace", "status", "notify_title"],
|
|
279
|
-
riskProfile: { mutates: "external_side_effect", risk: "high", reason: "changes what cmux shows the human (sidebar status or a desktop notification)" },
|
|
280
|
-
};
|
|
281
|
-
function answerContext(ctx, client) {
|
|
282
|
-
const stateDir = (0, answer_1.cmuxStateDir)(ctx.agentRoot);
|
|
283
|
-
return { agentRoot: ctx.agentRoot, stateDir, client, now: Date.now, cmuxVersion: () => (0, attention_1.readCmuxState)(path.join(stateDir, "state.json"), new Date().toISOString()).cmuxVersion };
|
|
284
|
-
}
|
|
285
|
-
exports.cmuxReplyOnceToolDefinition = {
|
|
286
|
-
tool: {
|
|
287
|
-
type: "function",
|
|
288
|
-
function: {
|
|
289
|
-
name: "cmux_reply_once",
|
|
290
|
-
description: "Record your judgment that a coding agent's pending permission request is routine and fine to allow once. Your judgment alone never sends anything: the reply goes out only when the code floor, the human's exact precedent and a standing owner grant all allow it, and then only as 'once'. Otherwise your judgment is logged in shadow for the human to review and the request stays with them. Questions, plan approvals, Codex and Copilot requests always stay with the human.",
|
|
291
|
-
parameters: {
|
|
292
|
-
type: "object",
|
|
293
|
-
properties: {
|
|
294
|
-
request_id: { type: "string", description: "The Feed request id from the escalation or cmux_overview." },
|
|
295
|
-
reasoning: { type: "string", description: "Why this is routine under the cmux principles, in one or two sentences." },
|
|
296
|
-
},
|
|
297
|
-
required: ["request_id", "reasoning"],
|
|
298
|
-
additionalProperties: false,
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
},
|
|
302
|
-
handler: async (args, ctx) => {
|
|
303
|
-
const requestId = text(args.request_id).trim();
|
|
304
|
-
const reasoning = text(args.reasoning).trim();
|
|
305
|
-
if (!requestId || !reasoning)
|
|
306
|
-
return JSON.stringify({ error: "give the request_id and your reasoning" });
|
|
307
|
-
const client = clientFor(ctx);
|
|
308
|
-
if (typeof client === "string")
|
|
309
|
-
return JSON.stringify({ error: client });
|
|
310
|
-
try {
|
|
311
|
-
const item = (0, attention_1.pendingFeedItems)(await client.call("feed.list", { pending_only: true })).find((entry) => entry.requestId === requestId);
|
|
312
|
-
if (!item)
|
|
313
|
-
return JSON.stringify({ error: `request ${requestId} is no longer pending` });
|
|
314
|
-
const answers = answerContext(ctx, client);
|
|
315
|
-
const judgment = (0, answer_1.judgeFeedItem)(answers, item);
|
|
316
|
-
if (judgment.reply) {
|
|
317
|
-
const outcome = await (0, answer_1.answerOnce)(answers, item, judgment);
|
|
318
|
-
return JSON.stringify({ sent: outcome === "replied_once", outcome, authority: judgment.authority, ...(outcome === "replied_once" || outcome === "race" ? {} : { next: "The request stays with the human; tell them with cmux_signal if it is urgent." }) });
|
|
319
|
-
}
|
|
320
|
-
(0, answer_1.recordDecision)(answers, item, judgment, "shadow", `agent would allow once: ${reasoning}`);
|
|
321
|
-
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.cmux_shadow_judgment", message: "logged the agent's would-be cmux answer in shadow", meta: { tool: item.toolName } });
|
|
322
|
-
return JSON.stringify({ sent: false, outcome: "shadow", reason: judgment.reason, next: "The request stays with the human; tell them with cmux_signal if it is urgent." });
|
|
323
|
-
}
|
|
324
|
-
catch (error) {
|
|
325
|
-
return failure("cmux_reply_once", error);
|
|
326
|
-
}
|
|
327
|
-
},
|
|
328
|
-
summaryKeys: ["request_id"],
|
|
329
|
-
riskProfile: { mutates: "external_side_effect", risk: "high", reason: "may answer a coding agent's permission request once when the floor, a precedent and a standing grant allow it" },
|
|
330
|
-
};
|
|
331
|
-
exports.cmuxCorrectToolDefinition = {
|
|
332
|
-
tool: {
|
|
333
|
-
type: "function",
|
|
334
|
-
function: {
|
|
335
|
-
name: "cmux_correct",
|
|
336
|
-
description: "Record the human's answer or correction for a coding-agent request the cmux sense judged, as an exact precedent on this machine. 'once' means the human is fine with exactly this request being allowed once next time, and is accepted only when cmux shows the human allowed that request themselves; 'ask' means always ask them about exactly this request. Use it only for what the human actually said, in conversation with them; it is refused in autonomous turns and while handling an external event. A precedent never overrides the code floor, and live replies still need a standing owner grant.",
|
|
337
|
-
parameters: {
|
|
338
|
-
type: "object",
|
|
339
|
-
properties: {
|
|
340
|
-
request_id: { type: "string", description: "The Feed request id the human answered or corrected." },
|
|
341
|
-
verdict: { type: "string", enum: ["once", "ask"], description: "once: fine to allow once next time. ask: always ask." },
|
|
342
|
-
note: { type: "string", description: "What the human said, briefly." },
|
|
343
|
-
},
|
|
344
|
-
required: ["request_id", "verdict", "note"],
|
|
345
|
-
additionalProperties: false,
|
|
346
|
-
},
|
|
347
|
-
},
|
|
348
|
-
},
|
|
349
|
-
handler: async (args, ctx) => {
|
|
350
|
-
const requestId = text(args.request_id).trim();
|
|
351
|
-
const verdict = args.verdict;
|
|
352
|
-
if (!requestId || (verdict !== "once" && verdict !== "ask"))
|
|
353
|
-
return JSON.stringify({ error: "give the request_id and a verdict of once or ask" });
|
|
354
|
-
if (!ctx?.agentRoot)
|
|
355
|
-
return JSON.stringify({ error: "the cmux tools need an agent runtime" });
|
|
356
|
-
if (ctx.currentExternalEvent || ctx.autonomousTurnKind)
|
|
357
|
-
return JSON.stringify({ error: "cmux_correct records what the human said in conversation; it cannot run in an autonomous turn or while handling an external event" });
|
|
358
|
-
const stateDir = (0, answer_1.cmuxStateDir)(ctx.agentRoot);
|
|
359
|
-
const decisions = (0, casebook_1.readDecisions)((0, casebook_1.cmuxDecisionLogPath)(stateDir)).filter((entry) => entry.requestId === requestId);
|
|
360
|
-
const decision = [...decisions].reverse().find((entry) => entry.shape);
|
|
361
|
-
if (!decision)
|
|
362
|
-
return JSON.stringify({ error: `no judged request ${requestId} with a precedent shape; the floor escalates it unconditionally or it was never seen` });
|
|
363
|
-
const shape = decision.shape;
|
|
364
|
-
if (!(0, casebook_1.isShape)(shape))
|
|
365
|
-
return JSON.stringify({ error: `the logged shape for ${requestId} is from an older format and cannot become a precedent; record it again after the sense judges the request anew` });
|
|
366
|
-
if (verdict === "once") {
|
|
367
|
-
// A "once" precedent turns into live authority later, so it must rest on the human's own answer in cmux, not on ours.
|
|
368
|
-
if (decisions.some((entry) => entry.outcome === "reply_sent" || entry.outcome === "replied_once"))
|
|
369
|
-
return JSON.stringify({ error: `the sense itself replied to ${requestId}; only a request the human answered can become a once precedent` });
|
|
370
|
-
const client = clientFor(ctx);
|
|
371
|
-
if (typeof client === "string")
|
|
372
|
-
return JSON.stringify({ error: client });
|
|
373
|
-
try {
|
|
374
|
-
const status = await (0, answer_1.feedItemStatus)(client, requestId);
|
|
375
|
-
if (status?.status !== "resolved" || status.kind !== "permission" || !status.mode || status.mode === "deny") {
|
|
376
|
-
return JSON.stringify({ error: `cmux does not show the human allowing ${requestId} (${status ? `${status.status}${status.mode ? `, ${status.mode}` : ""}` : "not listed"}); record 'ask' instead, or wait until they answer it in cmux` });
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
catch (error) {
|
|
380
|
-
return failure("cmux_correct", error);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
let entry;
|
|
384
|
-
try {
|
|
385
|
-
entry = (0, casebook_1.addCase)((0, casebook_1.cmuxCasebookPath)(stateDir), { verdict, shape, requestId, note: text(args.note), at: new Date().toISOString() });
|
|
386
|
-
}
|
|
387
|
-
catch (error) {
|
|
388
|
-
return failure("cmux_correct", error);
|
|
389
|
-
}
|
|
390
|
-
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.cmux_correct", message: "recorded the human's cmux precedent", meta: { verdict } });
|
|
391
|
-
return JSON.stringify({ case: entry.id, verdict, tool: entry.shape.tool, repoRoot: entry.shape.repoRoot });
|
|
392
|
-
},
|
|
393
|
-
summaryKeys: ["request_id", "verdict"],
|
|
394
|
-
riskProfile: { mutates: "durable_state_write", risk: "high", reason: "records a precedent that later decides whether a coding agent's request is answered" },
|
|
395
|
-
};
|
|
396
|
-
exports.cmuxToolDefinitions = [exports.cmuxOverviewToolDefinition, exports.cmuxReadToolDefinition, exports.cmuxSignalToolDefinition, exports.cmuxReplyOnceToolDefinition, exports.cmuxCorrectToolDefinition];
|
|
@@ -1,191 +0,0 @@
|
|
|
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.CMUX_MIN_ANSWER_VERSION = exports.CMUX_GRANT_ACTION = exports.CMUX_GRANT_KEY = void 0;
|
|
37
|
-
exports.cmuxStateDir = cmuxStateDir;
|
|
38
|
-
exports.judgeFeedItem = judgeFeedItem;
|
|
39
|
-
exports.recordDecision = recordDecision;
|
|
40
|
-
exports.feedItemStatus = feedItemStatus;
|
|
41
|
-
exports.answerOnce = answerOnce;
|
|
42
|
-
const path = __importStar(require("node:path"));
|
|
43
|
-
const steward_policy_1 = require("../../heart/steward-policy");
|
|
44
|
-
const runtime_1 = require("../../nerves/runtime");
|
|
45
|
-
const attention_1 = require("./attention");
|
|
46
|
-
const casebook_1 = require("./casebook");
|
|
47
|
-
const floor_1 = require("./floor");
|
|
48
|
-
/**
|
|
49
|
-
* Whether the sense may answer a pending Feed permission request itself, and the reply path that
|
|
50
|
-
* sends only `once`. A reply needs all of: the floor (allow, or soft with the human's exact
|
|
51
|
-
* precedent), no "ask me" precedent, cmux 0.65.0 or later, and a standing owner grant for the
|
|
52
|
-
* repository under its count cap. Anything else escalates to the human.
|
|
53
|
-
*/
|
|
54
|
-
exports.CMUX_GRANT_KEY = "cmux-feed-once";
|
|
55
|
-
exports.CMUX_GRANT_ACTION = "cmux.feed.once";
|
|
56
|
-
exports.CMUX_MIN_ANSWER_VERSION = "0.65.0";
|
|
57
|
-
/** The only reply mode this code can send. `always`, `all` and `bypass` would widen the coding agent's permissions. */
|
|
58
|
-
const REPLY_MODE = "once";
|
|
59
|
-
function cmuxStateDir(agentRoot) {
|
|
60
|
-
return path.join(agentRoot, "state", "senses", "cmux");
|
|
61
|
-
}
|
|
62
|
-
function inspectGrant(ctx, repoRoot) {
|
|
63
|
-
const nowMs = ctx.now();
|
|
64
|
-
const grant = (0, steward_policy_1.inspectStandingActionGrant)(ctx.agentRoot, {
|
|
65
|
-
key: exports.CMUX_GRANT_KEY,
|
|
66
|
-
action: exports.CMUX_GRANT_ACTION,
|
|
67
|
-
target: repoRoot,
|
|
68
|
-
now: new Date(nowMs).toISOString(),
|
|
69
|
-
usesInWindow: (windowMs) => (0, casebook_1.repliesInWindow)((0, casebook_1.readDecisionLog)((0, casebook_1.cmuxDecisionLogPath)(ctx.stateDir)), windowMs, nowMs),
|
|
70
|
-
});
|
|
71
|
-
// The decision log keeps reply records for REPLY_HISTORY_MS, so it cannot count a longer window.
|
|
72
|
-
if (grant.allowed && grant.windowMs > casebook_1.REPLY_HISTORY_MS)
|
|
73
|
-
return { allowed: false, reason: "the standing grant's window is longer than the 31 days of replies the sense keeps" };
|
|
74
|
-
return grant;
|
|
75
|
-
}
|
|
76
|
-
function judgeFeedItem(ctx, item) {
|
|
77
|
-
const floor = (0, floor_1.evaluateFloor)({ kind: item.kind, source: item.source, toolName: item.toolName, toolInput: item.toolInput, toolInputTruncated: item.toolInputTruncated, cwd: item.cwd }, ctx.fsx);
|
|
78
|
-
if (floor.verdict === "hard")
|
|
79
|
-
return { reply: false, floor, shape: null, precedent: null, reason: `floor: ${floor.reason}` };
|
|
80
|
-
const shape = floor.shape;
|
|
81
|
-
const precedent = (0, casebook_1.findPrecedent)((0, casebook_1.readCasebook)((0, casebook_1.cmuxCasebookPath)(ctx.stateDir)), shape);
|
|
82
|
-
if (precedent?.verdict === "ask")
|
|
83
|
-
return { reply: false, floor, shape, precedent, reason: "the human asked to be asked about this exact request" };
|
|
84
|
-
if (floor.verdict === "soft" && precedent?.verdict !== "once")
|
|
85
|
-
return { reply: false, floor, shape, precedent, reason: `floor: ${floor.reason}, and the human has not answered this exact request before` };
|
|
86
|
-
const basis = floor.verdict === "allow" ? `floor: ${floor.reason}` : `precedent ${precedent.id}`;
|
|
87
|
-
const version = ctx.cmuxVersion();
|
|
88
|
-
if (!(0, attention_1.cmuxVersionAtLeast)(version, exports.CMUX_MIN_ANSWER_VERSION)) {
|
|
89
|
-
return { reply: false, floor, shape, precedent, reason: `would answer once (${basis}), but cmux ${version ?? "of unknown version"} is older than ${exports.CMUX_MIN_ANSWER_VERSION}` };
|
|
90
|
-
}
|
|
91
|
-
const grant = inspectGrant(ctx, shape.repoRoot);
|
|
92
|
-
if (!grant.allowed)
|
|
93
|
-
return { reply: false, floor, shape, precedent, reason: `would answer once (${basis}), but ${grant.reason}` };
|
|
94
|
-
return { reply: true, floor, shape, precedent, authority: `standing grant v${grant.grantVersion} (${grant.maxCount} per ${Math.round(grant.windowMs / 60_000)} min); ${basis}` };
|
|
95
|
-
}
|
|
96
|
-
function recordDecision(ctx, item, judgment, outcome, detail) {
|
|
97
|
-
(0, casebook_1.appendDecision)((0, casebook_1.cmuxDecisionLogPath)(ctx.stateDir), {
|
|
98
|
-
at: new Date(ctx.now()).toISOString(),
|
|
99
|
-
requestId: item.requestId,
|
|
100
|
-
source: item.source,
|
|
101
|
-
tool: item.toolName,
|
|
102
|
-
cwd: item.cwd,
|
|
103
|
-
outcome,
|
|
104
|
-
floor: { verdict: judgment.floor.verdict, reason: judgment.floor.reason },
|
|
105
|
-
shape: judgment.shape ? (0, casebook_1.storeShape)(judgment.shape) : null,
|
|
106
|
-
precedent: judgment.precedent ? { id: judgment.precedent.id, verdict: judgment.precedent.verdict } : null,
|
|
107
|
-
authority: judgment.reply ? judgment.authority : "none",
|
|
108
|
-
detail,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
/** A Feed item's current status and decision, from the unfiltered `feed.list`, or null when cmux no longer lists it. */
|
|
112
|
-
async function feedItemStatus(client, requestId) {
|
|
113
|
-
const result = await client.call("feed.list", {});
|
|
114
|
-
const items = Array.isArray(result.items) ? result.items : [];
|
|
115
|
-
const item = items.find((entry) => entry && entry.request_id === requestId);
|
|
116
|
-
if (!item)
|
|
117
|
-
return null;
|
|
118
|
-
const decision = item.decision && typeof item.decision === "object" ? item.decision : {};
|
|
119
|
-
return {
|
|
120
|
-
status: typeof item.status === "string" ? item.status : "unknown",
|
|
121
|
-
mode: typeof decision.mode === "string" ? decision.mode : null,
|
|
122
|
-
kind: typeof decision.kind === "string" ? decision.kind : null,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Re-checks that the exact request is still pending, re-checks the grant and records `reply_sent`
|
|
127
|
-
* under the decision log's lock, sends `once`, then reads the item back. cmux reports a reply as
|
|
128
|
-
* delivered even when the waiter is gone, so only a `resolved` item with a `once` permission
|
|
129
|
-
* decision counts as `replied_once`; an item resolved some other way is a race (the human answered).
|
|
130
|
-
* A failed reply call, an item still pending or expired, or one cmux no longer lists is `unconfirmed`,
|
|
131
|
-
* and the caller escalates it.
|
|
132
|
-
*/
|
|
133
|
-
async function answerOnce(ctx, item, judgment) {
|
|
134
|
-
let current;
|
|
135
|
-
try {
|
|
136
|
-
current = (0, attention_1.pendingFeedItems)(await ctx.client.call("feed.list", { pending_only: true })).find((entry) => entry.requestId === item.requestId);
|
|
137
|
-
}
|
|
138
|
-
catch (error) {
|
|
139
|
-
recordDecision(ctx, item, judgment, "reply_failed", `could not re-check the request: ${error.message}`);
|
|
140
|
-
return "not_sent";
|
|
141
|
-
}
|
|
142
|
-
if (!current) {
|
|
143
|
-
recordDecision(ctx, item, judgment, "race", "the request was answered or withdrawn before the reply");
|
|
144
|
-
return "race";
|
|
145
|
-
}
|
|
146
|
-
if (current.toolName !== item.toolName || current.toolInput !== item.toolInput || current.cwd !== item.cwd || current.toolInputTruncated !== item.toolInputTruncated) {
|
|
147
|
-
recordDecision(ctx, item, judgment, "reply_failed", "the pending request changed after it was judged");
|
|
148
|
-
return "not_sent";
|
|
149
|
-
}
|
|
150
|
-
const reserved = (0, casebook_1.withDecisionLock)(ctx.stateDir, () => {
|
|
151
|
-
if ((0, casebook_1.readDecisions)((0, casebook_1.cmuxDecisionLogPath)(ctx.stateDir)).some((entry) => entry.requestId === item.requestId && entry.outcome === "reply_sent"))
|
|
152
|
-
return "the sense already sent a reply for this request";
|
|
153
|
-
const grant = inspectGrant(ctx, judgment.shape.repoRoot);
|
|
154
|
-
if (!grant.allowed)
|
|
155
|
-
return grant.reason;
|
|
156
|
-
recordDecision(ctx, item, judgment, "reply_sent", judgment.authority);
|
|
157
|
-
return null;
|
|
158
|
-
});
|
|
159
|
-
if (reserved !== null) {
|
|
160
|
-
recordDecision(ctx, item, judgment, "reply_failed", reserved);
|
|
161
|
-
return "not_sent";
|
|
162
|
-
}
|
|
163
|
-
try {
|
|
164
|
-
await ctx.client.call("feed.permission.reply", { request_id: item.requestId, mode: REPLY_MODE });
|
|
165
|
-
}
|
|
166
|
-
catch (error) {
|
|
167
|
-
// The call can fail after cmux applied the reply, so its fate is unknown and the human decides.
|
|
168
|
-
recordDecision(ctx, item, judgment, "reply_failed", `the reply call failed, so the reply may or may not have gone out: ${error.message}`);
|
|
169
|
-
return "unconfirmed";
|
|
170
|
-
}
|
|
171
|
-
let after;
|
|
172
|
-
try {
|
|
173
|
-
after = await feedItemStatus(ctx.client, item.requestId);
|
|
174
|
-
}
|
|
175
|
-
catch (error) {
|
|
176
|
-
recordDecision(ctx, item, judgment, "reply_failed", `could not confirm the reply: ${error.message}`);
|
|
177
|
-
return "unconfirmed";
|
|
178
|
-
}
|
|
179
|
-
if (after?.status === "resolved" && after.kind === "permission" && after.mode === REPLY_MODE) {
|
|
180
|
-
recordDecision(ctx, item, judgment, "replied_once", judgment.authority);
|
|
181
|
-
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.cmux_replied_once", message: "answered a coding agent's permission request once", meta: { requestId: item.requestId, tool: item.toolName } });
|
|
182
|
-
return "replied_once";
|
|
183
|
-
}
|
|
184
|
-
if (after === null || after.status !== "resolved") {
|
|
185
|
-
recordDecision(ctx, item, judgment, "reply_failed", after ? `the request is ${after.status} after the reply` : "cmux no longer lists the request");
|
|
186
|
-
return "unconfirmed";
|
|
187
|
-
}
|
|
188
|
-
recordDecision(ctx, item, judgment, "race", `after the reply the request was ${after.status}${after.mode ? ` (${after.mode})` : ""}`);
|
|
189
|
-
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.cmux_reply_race", message: "a cmux Feed request was resolved by someone else", meta: { requestId: item.requestId, status: after.status } });
|
|
190
|
-
return "race";
|
|
191
|
-
}
|