@ouro.bot/cli 0.1.0-alpha.892 → 0.1.0-alpha.893
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 +6 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/cmux-config.js +102 -0
- package/dist/heart/daemon/runtime-logging.js +1 -1
- package/dist/heart/daemon/sense-manager.js +23 -6
- package/dist/heart/identity.js +4 -1
- package/dist/heart/private-runtime/policy.js +8 -0
- package/dist/heart/sense-truth.js +2 -0
- package/dist/heart/turn-context.js +7 -0
- package/dist/mind/prompt.js +7 -0
- package/dist/repertoire/guardrails.js +6 -0
- package/dist/repertoire/tools-cmux.js +277 -0
- package/dist/repertoire/tools.js +4 -1
- package/dist/senses/cmux/attention.js +263 -0
- package/dist/senses/cmux/client.js +196 -0
- package/dist/senses/cmux/redact.js +37 -0
- package/dist/senses/cmux/sense.js +248 -0
- package/dist/senses/cmux-entry.js +77 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,196 @@
|
|
|
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.CmuxSocketError = void 0;
|
|
37
|
+
exports.createCmuxClient = createCmuxClient;
|
|
38
|
+
const node_crypto_1 = require("node:crypto");
|
|
39
|
+
const net = __importStar(require("node:net"));
|
|
40
|
+
const cmux_config_1 = require("../../heart/cmux-config");
|
|
41
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
42
|
+
class CmuxSocketError extends Error {
|
|
43
|
+
code;
|
|
44
|
+
constructor(code, message) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.code = code;
|
|
47
|
+
this.name = "CmuxSocketError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.CmuxSocketError = CmuxSocketError;
|
|
51
|
+
const ACCESS_REFUSED = /access denied|authentication required|invalid password|not authorized|unauthori[sz]ed/i;
|
|
52
|
+
function isRecord(value) {
|
|
53
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
54
|
+
}
|
|
55
|
+
function parseRecord(line) {
|
|
56
|
+
try {
|
|
57
|
+
const value = JSON.parse(line);
|
|
58
|
+
return isRecord(value) ? value : null;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function createCmuxClient(connection, options = {}) {
|
|
65
|
+
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
66
|
+
const repair = (0, cmux_config_1.cmuxRepairHint)(options.agentName ?? "<agent>");
|
|
67
|
+
const wrap = (line) => connection.auth.kind === "capability"
|
|
68
|
+
? `_cmux_capability_v1 ${connection.auth.token} ${line}`
|
|
69
|
+
: line;
|
|
70
|
+
const refusal = (text) => text.startsWith("ERROR:") && ACCESS_REFUSED.test(text)
|
|
71
|
+
? new CmuxSocketError("auth", `cmux refused the socket connection (${text.slice(0, 200)}); ${repair}`)
|
|
72
|
+
: null;
|
|
73
|
+
const connectError = (error) => new CmuxSocketError("unavailable", `cmux socket is not reachable at ${connection.socketPath} (${String(error.code)}); is cmux running?`);
|
|
74
|
+
/** Opens a connection, runs the password handshake when needed, and hands back a line reader. */
|
|
75
|
+
function open(onLine, onEnd) {
|
|
76
|
+
return new Promise((resolve) => {
|
|
77
|
+
const socket = net.createConnection(connection.socketPath);
|
|
78
|
+
let buffer = "";
|
|
79
|
+
let handshake = connection.auth.kind === "password";
|
|
80
|
+
socket.setEncoding("utf-8");
|
|
81
|
+
socket.on("error", (error) => onEnd(connectError(error)));
|
|
82
|
+
socket.on("close", () => onEnd());
|
|
83
|
+
socket.on("data", (chunk) => {
|
|
84
|
+
buffer += chunk;
|
|
85
|
+
let index = buffer.indexOf("\n");
|
|
86
|
+
while (index >= 0) {
|
|
87
|
+
const line = buffer.slice(0, index).trim();
|
|
88
|
+
buffer = buffer.slice(index + 1);
|
|
89
|
+
index = buffer.indexOf("\n");
|
|
90
|
+
if (!line)
|
|
91
|
+
continue;
|
|
92
|
+
if (handshake) {
|
|
93
|
+
handshake = false;
|
|
94
|
+
if (line.startsWith("ERROR:") && !line.includes("Unknown command 'auth'")) {
|
|
95
|
+
socket.destroy();
|
|
96
|
+
onEnd(new CmuxSocketError("auth", `cmux rejected the socket password (${line.slice(0, 200)}); ${repair}`));
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
resolve(socket);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
onLine(line);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
socket.on("connect", () => {
|
|
106
|
+
if (connection.auth.kind === "password")
|
|
107
|
+
socket.write(`auth ${connection.auth.password}\n`);
|
|
108
|
+
else
|
|
109
|
+
resolve(socket);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function request(line) {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
let settled = false;
|
|
116
|
+
let socket = null;
|
|
117
|
+
const finish = (error, value) => {
|
|
118
|
+
if (settled)
|
|
119
|
+
return;
|
|
120
|
+
settled = true;
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
socket?.destroy();
|
|
123
|
+
if (error)
|
|
124
|
+
reject(error);
|
|
125
|
+
else
|
|
126
|
+
resolve(value);
|
|
127
|
+
};
|
|
128
|
+
const timer = setTimeout(() => finish(new CmuxSocketError("timeout", `cmux did not answer within ${timeoutMs} ms`)), timeoutMs);
|
|
129
|
+
void open((response) => finish(refusal(response), response), (error) => finish(error ?? new CmuxSocketError("unavailable", "cmux closed the socket before answering"))).then((opened) => {
|
|
130
|
+
socket = opened;
|
|
131
|
+
opened.write(`${wrap(line)}\n`);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const v2Line = (method, params) => JSON.stringify({ id: (0, node_crypto_1.randomUUID)(), method, params });
|
|
136
|
+
function parseV2(text) {
|
|
137
|
+
const parsed = parseRecord(text);
|
|
138
|
+
if (!parsed)
|
|
139
|
+
throw new CmuxSocketError(text.startsWith("ERROR:") ? "command_failed" : "protocol", `unexpected cmux response: ${text.slice(0, 200)}`);
|
|
140
|
+
if (parsed.ok === true)
|
|
141
|
+
return isRecord(parsed.result) ? parsed.result : {};
|
|
142
|
+
const error = isRecord(parsed.error) ? parsed.error : {};
|
|
143
|
+
throw new CmuxSocketError(typeof error.code === "string" ? error.code : "error", typeof error.message === "string" && error.message ? error.message : "cmux error");
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
async call(method, params = {}) {
|
|
147
|
+
const text = await request(v2Line(method, params));
|
|
148
|
+
const result = parseV2(text);
|
|
149
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.cmux_call", message: "cmux socket call answered", meta: { method } });
|
|
150
|
+
return result;
|
|
151
|
+
},
|
|
152
|
+
async command(line) {
|
|
153
|
+
const text = await request(line);
|
|
154
|
+
if (text.startsWith("ERROR:"))
|
|
155
|
+
throw new CmuxSocketError("command_failed", text.slice(0, 500));
|
|
156
|
+
return text;
|
|
157
|
+
},
|
|
158
|
+
stream(params, handlers) {
|
|
159
|
+
let closed = false;
|
|
160
|
+
let socket = null;
|
|
161
|
+
const end = (error) => {
|
|
162
|
+
if (closed)
|
|
163
|
+
return;
|
|
164
|
+
closed = true;
|
|
165
|
+
socket?.destroy();
|
|
166
|
+
handlers.onClose(error);
|
|
167
|
+
};
|
|
168
|
+
void open((line) => {
|
|
169
|
+
if (closed)
|
|
170
|
+
return;
|
|
171
|
+
const refused = refusal(line);
|
|
172
|
+
if (refused)
|
|
173
|
+
return end(refused);
|
|
174
|
+
const frame = parseRecord(line);
|
|
175
|
+
if (!frame)
|
|
176
|
+
return end(new CmuxSocketError("protocol", `unexpected cmux event frame: ${line.slice(0, 200)}`));
|
|
177
|
+
if (frame.ok === false) {
|
|
178
|
+
try {
|
|
179
|
+
parseV2(line);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
return end(error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
handlers.onFrame(frame);
|
|
186
|
+
}, end).then((opened) => {
|
|
187
|
+
socket = opened;
|
|
188
|
+
if (closed)
|
|
189
|
+
opened.destroy();
|
|
190
|
+
else
|
|
191
|
+
opened.write(`${wrap(v2Line("events.stream", params))}\n`);
|
|
192
|
+
});
|
|
193
|
+
return { close: () => end() };
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.redactSecrets = redactSecrets;
|
|
4
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
5
|
+
/**
|
|
6
|
+
* Terminal screens and Feed tool input can show credentials. Everything the cmux sense hands to a
|
|
7
|
+
* model (screen text, request previews) goes through this first. It is a best-effort net for
|
|
8
|
+
* secret-shaped strings, not a guarantee, so callers also keep their output bounded.
|
|
9
|
+
*/
|
|
10
|
+
const REDACTED = "[redacted]";
|
|
11
|
+
const WHOLE_MATCH = [
|
|
12
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
13
|
+
/\bsk-[A-Za-z0-9_-]{16,}/g,
|
|
14
|
+
/\bgh[pousr]_[A-Za-z0-9]{20,}/g,
|
|
15
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}/g,
|
|
16
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
17
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,
|
|
18
|
+
/\bv1\.[A-Za-z0-9_-]{40,}\.[A-Za-z0-9_-]{40,}/g,
|
|
19
|
+
/\bxox[abprs]-[A-Za-z0-9-]{10,}/g,
|
|
20
|
+
];
|
|
21
|
+
/** Keep the label (group 1), redact the value. */
|
|
22
|
+
const LABELLED_VALUE = [
|
|
23
|
+
/(\b(?:Bearer|Basic)\s+)[A-Za-z0-9._~+/=-]{6,}/gi,
|
|
24
|
+
/(\b(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|client[_-]?secret)\s*[=:]\s*)(?:"[^"]*"|'[^']*'|[^\s'"&]+)/gi,
|
|
25
|
+
/([?&](?:token|key|api_key|access_token|sig|signature)=)[^&\s]+/gi,
|
|
26
|
+
];
|
|
27
|
+
function redactSecrets(text) {
|
|
28
|
+
let result = text;
|
|
29
|
+
for (const pattern of WHOLE_MATCH)
|
|
30
|
+
result = result.replace(pattern, REDACTED);
|
|
31
|
+
for (const pattern of LABELLED_VALUE)
|
|
32
|
+
result = result.replace(pattern, `$1${REDACTED}`);
|
|
33
|
+
if (result !== text) {
|
|
34
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.cmux_redacted", message: "redacted secret-shaped text from cmux output", meta: { removedChars: text.length - result.length } });
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
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_WAKE_TRIGGER = void 0;
|
|
37
|
+
exports.cmuxStatePath = cmuxStatePath;
|
|
38
|
+
exports.queueEscalation = queueEscalation;
|
|
39
|
+
exports.wakeForEscalations = wakeForEscalations;
|
|
40
|
+
exports.startCmuxSenseApp = startCmuxSenseApp;
|
|
41
|
+
const node_crypto_1 = require("node:crypto");
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
43
|
+
const cmux_config_1 = require("../../heart/cmux-config");
|
|
44
|
+
const socket_client_1 = require("../../heart/daemon/socket-client");
|
|
45
|
+
const identity_1 = require("../../heart/identity");
|
|
46
|
+
const runtime_credentials_1 = require("../../heart/runtime-credentials");
|
|
47
|
+
const pending_1 = require("../../mind/pending");
|
|
48
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
49
|
+
const attention_1 = require("./attention");
|
|
50
|
+
const client_1 = require("./client");
|
|
51
|
+
const RECONNECT_DELAYS_MS = [1_000, 2_000, 5_000, 10_000, 30_000];
|
|
52
|
+
const FEED_POLL_MS = 30_000;
|
|
53
|
+
const SAVE_DELAY_MS = 1_000;
|
|
54
|
+
function cmuxStatePath(agentName) {
|
|
55
|
+
return path.join((0, identity_1.getAgentRoot)(agentName), "state", "senses", "cmux", "state.json");
|
|
56
|
+
}
|
|
57
|
+
/** The private-turn trigger the daemon's policy accepts for a cmux escalation wake. */
|
|
58
|
+
exports.CMUX_WAKE_TRIGGER = "cmux-feed";
|
|
59
|
+
/** cmux waits about 120 seconds; a notice the agent has not read within 30 minutes is dropped unread. */
|
|
60
|
+
const ESCALATION_TTL_MS = 30 * 60_000;
|
|
61
|
+
/** Queues the escalation in the agent's private runtime, as the mail sense queues its notices. */
|
|
62
|
+
function queueEscalation(agent, escalation, nowMs) {
|
|
63
|
+
(0, pending_1.queuePendingMessage)((0, pending_1.getPrivateRuntimePendingDir)(agent), {
|
|
64
|
+
from: "cmux",
|
|
65
|
+
friendId: "self",
|
|
66
|
+
channel: "cmux",
|
|
67
|
+
key: "feed",
|
|
68
|
+
content: escalation.content,
|
|
69
|
+
timestamp: nowMs,
|
|
70
|
+
expiresAt: nowMs + ESCALATION_TTL_MS,
|
|
71
|
+
mode: "reflect",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** How many request ids one wake names in its origin references. */
|
|
75
|
+
const WAKE_REF_LIMIT = 20;
|
|
76
|
+
/**
|
|
77
|
+
* Asks the daemon for one private turn for everything a check pass queued: the turn reads every
|
|
78
|
+
* pending message at once, so a burst of Feed requests costs one turn, not one each. A refused or
|
|
79
|
+
* failed wake leaves the messages for the next private turn.
|
|
80
|
+
*/
|
|
81
|
+
async function wakeForEscalations(agent, requestIds) {
|
|
82
|
+
const key = requestIds.length === 1 ? requestIds[0] : `${requestIds.length}:${(0, node_crypto_1.createHash)("sha256").update(requestIds.join("\n")).digest("hex").slice(0, 32)}`;
|
|
83
|
+
try {
|
|
84
|
+
const response = await (0, socket_client_1.requestPrivateWake)(agent, undefined, {
|
|
85
|
+
reason: requestIds.length === 1 ? "cmux Feed request" : `${requestIds.length} cmux Feed requests`,
|
|
86
|
+
triggerSource: exports.CMUX_WAKE_TRIGGER,
|
|
87
|
+
budgetClass: "interactive",
|
|
88
|
+
idempotencyKey: `cmux-feed:${agent}:${key}`,
|
|
89
|
+
originRefs: [...requestIds.slice(0, WAKE_REF_LIMIT).map((id) => ({ kind: "cmux-feed", id })), { kind: "sense", id: "cmux" }],
|
|
90
|
+
});
|
|
91
|
+
if (response && !response.ok)
|
|
92
|
+
throw new Error(response.error ?? "the daemon refused the wake");
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.cmux_wake_error", message: "could not wake the private runtime for cmux escalations; they wait for the next private turn", meta: { agent, error: error.message } });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function startCmuxSenseApp(options) {
|
|
99
|
+
const agent = options.agentName;
|
|
100
|
+
const nowMs = options.now ?? Date.now;
|
|
101
|
+
const now = () => new Date(nowMs()).toISOString();
|
|
102
|
+
const machine = (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent);
|
|
103
|
+
const resolved = (0, cmux_config_1.resolveCmuxConnection)(agent, machine.ok ? machine.config : {});
|
|
104
|
+
if (!resolved.ok)
|
|
105
|
+
throw new Error(resolved.error);
|
|
106
|
+
const client = (options.createClient ?? ((connection) => (0, client_1.createCmuxClient)(connection, { agentName: agent })))(resolved.connection);
|
|
107
|
+
const escalate = options.escalate ?? ((escalation) => queueEscalation(agent, escalation, nowMs()));
|
|
108
|
+
const wake = options.wake ?? ((requestIds) => wakeForEscalations(agent, requestIds));
|
|
109
|
+
const schedule = options.schedule ?? ((fn, ms) => {
|
|
110
|
+
const timer = setTimeout(fn, ms);
|
|
111
|
+
return () => clearTimeout(timer);
|
|
112
|
+
});
|
|
113
|
+
const statePath = cmuxStatePath(agent);
|
|
114
|
+
const state = (0, attention_1.readCmuxState)(statePath, now());
|
|
115
|
+
state.connected = false;
|
|
116
|
+
let stopped = false;
|
|
117
|
+
let stream = null;
|
|
118
|
+
let attempt = 0;
|
|
119
|
+
let cancelReconnect = null;
|
|
120
|
+
let cancelPoll = null;
|
|
121
|
+
let cancelSave = null;
|
|
122
|
+
let feedChain = Promise.resolve();
|
|
123
|
+
let feedQueued = false;
|
|
124
|
+
let lastWritten = null;
|
|
125
|
+
/** Writes the state file only when something other than the timestamp changed. */
|
|
126
|
+
const save = () => {
|
|
127
|
+
cancelSave?.();
|
|
128
|
+
cancelSave = null;
|
|
129
|
+
const body = JSON.stringify({ ...state, updatedAt: null });
|
|
130
|
+
if (body === lastWritten)
|
|
131
|
+
return;
|
|
132
|
+
state.updatedAt = now();
|
|
133
|
+
// A failed write is retried by the next save; it never breaks the Feed check chain.
|
|
134
|
+
try {
|
|
135
|
+
(0, attention_1.writeCmuxState)(statePath, state);
|
|
136
|
+
lastWritten = body;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.cmux_state_write_error", message: "could not write the cmux sense state; the next save retries", meta: { agent, error: error.message } });
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const saveSoon = () => {
|
|
143
|
+
cancelSave ??= schedule(save, SAVE_DELAY_MS);
|
|
144
|
+
};
|
|
145
|
+
async function checkFeed() {
|
|
146
|
+
try {
|
|
147
|
+
const items = (0, attention_1.pendingFeedItems)(await client.call("feed.list", { pending_only: true }));
|
|
148
|
+
const queued = [];
|
|
149
|
+
for (const item of items.filter((entry) => !state.escalated.includes(entry.requestId))) {
|
|
150
|
+
// One failing item never blocks the others; it stays unremembered, so the next check retries it.
|
|
151
|
+
try {
|
|
152
|
+
const escalation = (0, attention_1.escalationMessage)(agent, item, (0, attention_1.surfaceForSession)(state, item.workstreamId));
|
|
153
|
+
await escalate(escalation);
|
|
154
|
+
queued.push(escalation.requestId);
|
|
155
|
+
(0, attention_1.rememberEscalation)(state, item.requestId);
|
|
156
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.cmux_escalated", message: "brought a cmux Feed request to the agent", meta: { agent, kind: item.kind, source: item.source } });
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.cmux_escalation_error", message: "could not record a cmux Feed request; the next check retries", meta: { agent, error: error.message } });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// One wake per check pass: the private turn reads every queued escalation at once.
|
|
163
|
+
if (queued.length > 0)
|
|
164
|
+
await wake(queued);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.cmux_feed_check_error", message: "cmux Feed check failed; the next check retries", meta: { agent, error: error.message } });
|
|
168
|
+
}
|
|
169
|
+
save();
|
|
170
|
+
}
|
|
171
|
+
/** Bursts of waiting events share one queued `feed.list` read instead of stacking reads. */
|
|
172
|
+
const requestFeedCheck = () => {
|
|
173
|
+
if (feedQueued)
|
|
174
|
+
return;
|
|
175
|
+
feedQueued = true;
|
|
176
|
+
feedChain = feedChain.then(() => {
|
|
177
|
+
feedQueued = false;
|
|
178
|
+
return checkFeed();
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
/** Records the cmux version once per connection; observing works on any version. */
|
|
182
|
+
async function identify() {
|
|
183
|
+
try {
|
|
184
|
+
const result = await client.call("system.identify", {});
|
|
185
|
+
state.cmuxVersion = typeof result.version === "string" ? result.version : null;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
state.cmuxVersion = null;
|
|
189
|
+
}
|
|
190
|
+
saveSoon();
|
|
191
|
+
}
|
|
192
|
+
const poll = () => {
|
|
193
|
+
cancelPoll = schedule(() => {
|
|
194
|
+
requestFeedCheck();
|
|
195
|
+
poll();
|
|
196
|
+
}, FEED_POLL_MS);
|
|
197
|
+
};
|
|
198
|
+
function connect() {
|
|
199
|
+
cancelReconnect = null;
|
|
200
|
+
stream = client.stream({ ...(state.seq !== null ? { after_seq: state.seq } : {}), categories: ["feed", "surface"] }, {
|
|
201
|
+
onFrame: (frame) => {
|
|
202
|
+
if (frame.type === "ack") {
|
|
203
|
+
attempt = 0;
|
|
204
|
+
const { reset } = (0, attention_1.applyAck)(state, frame);
|
|
205
|
+
state.connected = true;
|
|
206
|
+
state.lastError = null;
|
|
207
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.cmux_stream_connected", message: "cmux event stream connected", meta: { agent, reset } });
|
|
208
|
+
void identify();
|
|
209
|
+
requestFeedCheck();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (frame.type !== "event")
|
|
213
|
+
return;
|
|
214
|
+
if ((0, attention_1.applyEventFrame)(state, frame, now()).feedCheck)
|
|
215
|
+
requestFeedCheck();
|
|
216
|
+
saveSoon();
|
|
217
|
+
},
|
|
218
|
+
onClose: (error) => {
|
|
219
|
+
stream = null;
|
|
220
|
+
state.connected = false;
|
|
221
|
+
state.lastError = error?.message ?? null;
|
|
222
|
+
if (stopped)
|
|
223
|
+
return;
|
|
224
|
+
const delay = RECONNECT_DELAYS_MS[Math.min(attempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
225
|
+
attempt += 1;
|
|
226
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.cmux_stream_closed", message: "cmux event stream closed; reconnecting", meta: { agent, delay, error: error?.message ?? null } });
|
|
227
|
+
save();
|
|
228
|
+
cancelReconnect = schedule(connect, delay);
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
connect();
|
|
233
|
+
poll();
|
|
234
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.cmux_sense_started", message: "cmux sense started", meta: { agent, auth: resolved.connection.auth.kind } });
|
|
235
|
+
return {
|
|
236
|
+
statePath,
|
|
237
|
+
async stop() {
|
|
238
|
+
stopped = true;
|
|
239
|
+
cancelReconnect?.();
|
|
240
|
+
cancelPoll?.();
|
|
241
|
+
stream?.close();
|
|
242
|
+
await feedChain;
|
|
243
|
+
state.connected = false;
|
|
244
|
+
save();
|
|
245
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.cmux_sense_stopped", message: "cmux sense stopped", meta: { agent } });
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
const agentArgIndex = process.argv.indexOf("--agent");
|
|
37
|
+
const agentName = agentArgIndex >= 0 ? process.argv[agentArgIndex + 1] : undefined;
|
|
38
|
+
if (!agentName) {
|
|
39
|
+
// eslint-disable-next-line no-console -- pre-boot guard: --agent check before imports
|
|
40
|
+
console.error("Missing required --agent <name> argument.\nUsage: node dist/senses/cmux-entry.js --agent ouroboros");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const runtime_logging_1 = require("../heart/daemon/runtime-logging");
|
|
44
|
+
const runtime_1 = require("../nerves/runtime");
|
|
45
|
+
(0, runtime_logging_1.configureDaemonRuntimeLogger)("cmux");
|
|
46
|
+
(0, runtime_1.emitNervesEvent)({
|
|
47
|
+
component: "senses",
|
|
48
|
+
event: "senses.entry_boot",
|
|
49
|
+
message: "booting cmux entrypoint",
|
|
50
|
+
meta: { entry: "cmux", agentName },
|
|
51
|
+
});
|
|
52
|
+
Promise.resolve().then(() => __importStar(require("../heart/runtime-credentials"))).then(async ({ readMachineRuntimeCredentialConfig, refreshMachineRuntimeCredentialConfig, waitForRuntimeCredentialBootstrap }) => {
|
|
53
|
+
await waitForRuntimeCredentialBootstrap(agentName);
|
|
54
|
+
if (!readMachineRuntimeCredentialConfig(agentName).ok) {
|
|
55
|
+
const { loadOrCreateMachineIdentity } = await Promise.resolve().then(() => __importStar(require("../heart/machine-identity")));
|
|
56
|
+
await refreshMachineRuntimeCredentialConfig(agentName, loadOrCreateMachineIdentity().machineId, { preserveCachedOnFailure: true }).catch(() => undefined);
|
|
57
|
+
}
|
|
58
|
+
const { startCmuxSenseApp } = await Promise.resolve().then(() => __importStar(require("./cmux/sense")));
|
|
59
|
+
const app = await startCmuxSenseApp({ agentName: agentName });
|
|
60
|
+
const shutdown = () => {
|
|
61
|
+
void app.stop().finally(() => process.exit(0));
|
|
62
|
+
};
|
|
63
|
+
process.once("SIGTERM", shutdown);
|
|
64
|
+
process.once("SIGINT", shutdown);
|
|
65
|
+
})
|
|
66
|
+
.catch((error) => {
|
|
67
|
+
(0, runtime_1.emitNervesEvent)({
|
|
68
|
+
level: "error",
|
|
69
|
+
component: "senses",
|
|
70
|
+
event: "senses.entry_error",
|
|
71
|
+
message: "cmux entrypoint failed",
|
|
72
|
+
meta: { entry: "cmux", agentName, error: error instanceof Error ? error.message : String(error) },
|
|
73
|
+
});
|
|
74
|
+
// eslint-disable-next-line no-console -- fatal startup guard for sense process
|
|
75
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
});
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.893",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.893",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|