@ouro.bot/cli 0.1.0-alpha.935 → 0.1.0-alpha.936
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 +11 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/daemon/cli-defaults.js +5 -0
- package/dist/heart/daemon/cli-exec.js +9 -0
- package/dist/heart/daemon/cli-help.js +2 -2
- package/dist/heart/daemon/cli-parse.js +2 -1
- package/dist/heart/daemon/daemon.js +14 -0
- package/dist/heart/daemon/ouro-entry.js +4 -1
- package/dist/heart/daemon/runtime-logging.js +1 -1
- package/dist/heart/daemon/sense-manager.js +53 -0
- package/dist/heart/mailbox/mailbox-http-hooks.js +1 -0
- package/dist/heart/mailbox/mailbox-http-routes.js +8 -0
- package/dist/heart/mailbox/mailbox-read.js +3 -1
- package/dist/heart/mailbox/mailbox-supervision.js +131 -0
- package/dist/heart/shepherd/custody-types.js +2 -0
- package/dist/heart/shepherd/mandates.js +352 -0
- package/dist/heart/shepherd/operation-commands.js +19 -0
- package/dist/heart/shepherd/operations.js +135 -0
- package/dist/heart/shepherd/supervision.js +114 -0
- package/dist/heart/shepherd/types.js +2 -0
- package/dist/mailbox-ui/assets/index-DsiKaPyH.css +1 -0
- package/dist/mailbox-ui/assets/index-xU5krmwW.js +1 -0
- package/dist/mailbox-ui/index.html +2 -2
- package/dist/nerves/coverage/file-completeness.js +4 -0
- package/dist/senses/shepherd/checkin-store.js +209 -41
- package/dist/senses/shepherd/cmux.js +69 -23
- package/dist/senses/shepherd/guidance.js +5 -2
- package/dist/senses/shepherd/herdr.js +84 -2
- package/dist/senses/shepherd/observation-publisher.js +320 -0
- package/dist/senses/shepherd/operations-defaults.js +86 -0
- package/dist/senses/shepherd/operations.js +159 -0
- package/dist/senses/shepherd/recovery.js +543 -0
- package/dist/senses/shepherd/sense.js +8 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/mailbox-ui/assets/index-Cyv4mw3Y.js +0 -1
- package/dist/mailbox-ui/assets/index-Du_9G9WO.css +0 -1
|
@@ -114,12 +114,26 @@ function herdrRequest(socketPath, method, params, timeoutMs = TIMEOUT_MS) {
|
|
|
114
114
|
function createHerdrHost(connection, options = {}) {
|
|
115
115
|
const schedule = options.schedule ?? cmux_1.realSchedule;
|
|
116
116
|
const call = (method, params) => herdrRequest(connection.socketPath, method, params);
|
|
117
|
-
async function panes() {
|
|
117
|
+
async function panes(strict = false) {
|
|
118
118
|
const result = await call("pane.list", {});
|
|
119
|
+
if (strict) {
|
|
120
|
+
if (!Array.isArray(result.panes))
|
|
121
|
+
throw new Error("invalid Herdr terminal inventory");
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
for (const pane of result.panes) {
|
|
124
|
+
if (!isRecord(pane) || typeof pane.pane_id !== "string" || !pane.pane_id.trim() || seen.has(pane.pane_id)) {
|
|
125
|
+
throw new Error("invalid Herdr terminal inventory identity");
|
|
126
|
+
}
|
|
127
|
+
seen.add(pane.pane_id);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
119
130
|
return Array.isArray(result.panes) ? result.panes.filter(isRecord) : [];
|
|
120
131
|
}
|
|
121
132
|
return {
|
|
122
133
|
name: "herdr",
|
|
134
|
+
async retirementInventory() {
|
|
135
|
+
return (await panes(true)).map(pane => pane.pane_id);
|
|
136
|
+
},
|
|
123
137
|
async list() {
|
|
124
138
|
return (await panes()).map((pane) => ({
|
|
125
139
|
id: str(pane.pane_id) ?? "",
|
|
@@ -187,6 +201,50 @@ function createHerdrHost(connection, options = {}) {
|
|
|
187
201
|
let refresh = false;
|
|
188
202
|
const status = new Map();
|
|
189
203
|
const pending = new Map();
|
|
204
|
+
const inventory = new Map();
|
|
205
|
+
let cancelObservation = null;
|
|
206
|
+
let inventoryGeneration = 0;
|
|
207
|
+
const publishObservation = (error) => {
|
|
208
|
+
handlers.observed?.({ at: Date.now(), sessions: [...inventory.values()], ...(error ? { error } : {}) });
|
|
209
|
+
};
|
|
210
|
+
const inventoryRow = (pane) => {
|
|
211
|
+
const id = str(pane.pane_id);
|
|
212
|
+
if (!id)
|
|
213
|
+
return;
|
|
214
|
+
const state = str(pane.agent_status);
|
|
215
|
+
inventory.set(id, {
|
|
216
|
+
session: { id, ref: id, workspace: str(pane.workspace_id),
|
|
217
|
+
title: str(pane.title) ?? str(pane.terminal_title_stripped) ?? str(pane.label) ?? "",
|
|
218
|
+
agent: str(pane.agent), cwd: str(pane.foreground_cwd) ?? str(pane.cwd), focused: pane.focused === true },
|
|
219
|
+
activity: state === "working" ? "working" : RETURN_STATES.has(state ?? "") ? "idle" : "unknown",
|
|
220
|
+
observedAt: Date.now(),
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
const replaceInventory = (current) => {
|
|
224
|
+
const ids = new Set(current.flatMap(pane => str(pane.pane_id) ?? []));
|
|
225
|
+
for (const id of inventory.keys())
|
|
226
|
+
if (!ids.has(id))
|
|
227
|
+
inventory.delete(id);
|
|
228
|
+
for (const pane of current)
|
|
229
|
+
inventoryRow(pane);
|
|
230
|
+
publishObservation();
|
|
231
|
+
};
|
|
232
|
+
// Read-only refresh belongs to this host watcher, never to the judge or the browser.
|
|
233
|
+
const refreshObservation = async () => {
|
|
234
|
+
cancelObservation = null;
|
|
235
|
+
const generation = ++inventoryGeneration;
|
|
236
|
+
try {
|
|
237
|
+
const current = await panes(true);
|
|
238
|
+
if (!closed && generation === inventoryGeneration)
|
|
239
|
+
replaceInventory(current);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
if (!closed && generation === inventoryGeneration)
|
|
243
|
+
publishObservation("host_unavailable");
|
|
244
|
+
}
|
|
245
|
+
if (!closed && generation === inventoryGeneration && !cancelObservation)
|
|
246
|
+
cancelObservation = schedule(() => void refreshObservation(), 5_000);
|
|
247
|
+
};
|
|
190
248
|
const cancelPending = (paneId) => {
|
|
191
249
|
pending.get(paneId)?.();
|
|
192
250
|
pending.delete(paneId);
|
|
@@ -263,12 +321,22 @@ function createHerdrHost(connection, options = {}) {
|
|
|
263
321
|
if (name === "pane_closed") {
|
|
264
322
|
cancelPending(paneId);
|
|
265
323
|
status.delete(paneId);
|
|
324
|
+
inventory.delete(paneId);
|
|
266
325
|
refresh = true;
|
|
267
326
|
stream?.close();
|
|
268
327
|
return;
|
|
269
328
|
}
|
|
270
329
|
if (name !== "pane_agent_status_changed")
|
|
271
330
|
return;
|
|
331
|
+
const row = inventory.get(paneId);
|
|
332
|
+
if (row) {
|
|
333
|
+
const next = str(data.agent_status);
|
|
334
|
+
row.activity = next === "working" ? "working" : RETURN_STATES.has(next ?? "") ? "idle" : "unknown";
|
|
335
|
+
row.observedAt = Date.now();
|
|
336
|
+
publishObservation();
|
|
337
|
+
if (closed)
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
272
340
|
observed(paneId, str(data.agent_status) ?? "unknown");
|
|
273
341
|
};
|
|
274
342
|
const reconnect = (error) => {
|
|
@@ -282,13 +350,17 @@ function createHerdrHost(connection, options = {}) {
|
|
|
282
350
|
const delay = RECONNECT_DELAYS_MS[Math.min(attempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
283
351
|
attempt += 1;
|
|
284
352
|
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.shepherd_herdr_closed", message: "Herdr event subscription closed; resubscribing", meta: { delay, error: error?.message ?? null } });
|
|
353
|
+
publishObservation("host_unavailable");
|
|
354
|
+
if (closed)
|
|
355
|
+
return;
|
|
285
356
|
cancelReconnect = schedule(() => void subscribe(), delay);
|
|
286
357
|
};
|
|
287
358
|
const subscribe = async () => {
|
|
288
359
|
cancelReconnect = null;
|
|
289
360
|
let current;
|
|
361
|
+
const generation = ++inventoryGeneration;
|
|
290
362
|
try {
|
|
291
|
-
current = await panes();
|
|
363
|
+
current = await panes(!!handlers.observed);
|
|
292
364
|
}
|
|
293
365
|
catch (error) {
|
|
294
366
|
return reconnect(error);
|
|
@@ -314,11 +386,19 @@ function createHerdrHost(connection, options = {}) {
|
|
|
314
386
|
else if (next !== "unknown")
|
|
315
387
|
status.set(id, next);
|
|
316
388
|
}
|
|
389
|
+
if (generation === inventoryGeneration)
|
|
390
|
+
replaceInventory(current);
|
|
391
|
+
if (closed)
|
|
392
|
+
return;
|
|
393
|
+
if (handlers.observed && !cancelObservation)
|
|
394
|
+
cancelObservation = schedule(() => void refreshObservation(), 5_000);
|
|
317
395
|
const subscriptions = [
|
|
318
396
|
{ type: "pane.created" }, { type: "pane.closed" }, { type: "pane.focused" }, { type: "pane.moved" },
|
|
319
397
|
...current.flatMap((pane) => str(pane.pane_id) ? [{ type: "pane.agent_status_changed", pane_id: pane.pane_id }] : []),
|
|
320
398
|
];
|
|
321
399
|
stream = connectLines(connection.socketPath, (line) => {
|
|
400
|
+
if (closed)
|
|
401
|
+
return;
|
|
322
402
|
if (isRecord(line.result) && line.result.type === "subscription_started") {
|
|
323
403
|
attempt = 0;
|
|
324
404
|
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_herdr_connected", message: "Herdr event subscription started", meta: { panes: subscriptions.length - 4 } });
|
|
@@ -334,6 +414,8 @@ function createHerdrHost(connection, options = {}) {
|
|
|
334
414
|
return {
|
|
335
415
|
close() {
|
|
336
416
|
closed = true;
|
|
417
|
+
inventoryGeneration += 1;
|
|
418
|
+
cancelObservation?.();
|
|
337
419
|
cancelReconnect?.();
|
|
338
420
|
for (const cancel of pending.values())
|
|
339
421
|
cancel();
|
|
@@ -0,0 +1,320 @@
|
|
|
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.createShepherdObservationPublisher = createShepherdObservationPublisher;
|
|
37
|
+
const node_crypto_1 = require("node:crypto");
|
|
38
|
+
const fs = __importStar(require("node:fs"));
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const mandates_1 = require("../../heart/shepherd/mandates");
|
|
41
|
+
const supervision_1 = require("../../heart/shepherd/supervision");
|
|
42
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
43
|
+
const guidance_1 = require("./guidance");
|
|
44
|
+
const notice_outbox_1 = require("./notice-outbox");
|
|
45
|
+
const recovery_1 = require("./recovery");
|
|
46
|
+
const checkin_store_1 = require("./checkin-store");
|
|
47
|
+
const returns_1 = require("./returns");
|
|
48
|
+
const redact_1 = require("./redact");
|
|
49
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
50
|
+
const kinds = new Set(["premature", "gate", "done", "working", "waiting", "unclear", "error", "loop_guard"]);
|
|
51
|
+
const safe = (value) => (0, redact_1.redactSecrets)(value).replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029]+/g, " ").trim().slice(0, 2000) || "(unnamed)";
|
|
52
|
+
const idFor = (value) => (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(value)).digest("hex");
|
|
53
|
+
const iso = (at) => {
|
|
54
|
+
if (!Number.isFinite(Date.parse(at)))
|
|
55
|
+
throw new Error("Invalid observation source timestamp");
|
|
56
|
+
return new Date(at).toISOString();
|
|
57
|
+
};
|
|
58
|
+
const emptyPage = () => ({ totalCount: 0, truncated: false, items: [] });
|
|
59
|
+
const unknownCustody = () => ({
|
|
60
|
+
revision: null, bytes: null, capacity: "unknown", claims: { totalCount: null, truncated: true, items: [] },
|
|
61
|
+
});
|
|
62
|
+
function page(events, totalCount) {
|
|
63
|
+
const unique = [...new Map(events.map(event => [event.id, event])).values()];
|
|
64
|
+
const count = totalCount === undefined ? unique.length : totalCount;
|
|
65
|
+
const items = unique.sort((a, b) => b.at.localeCompare(a.at) || a.id.localeCompare(b.id)).slice(0, supervision_1.SHEPHERD_SUPERVISION_LIMIT);
|
|
66
|
+
return { items, totalCount: count, truncated: count === null || count > items.length };
|
|
67
|
+
}
|
|
68
|
+
/** Unlike the legacy convenience reader, a malformed row never becomes a healthy empty history. */
|
|
69
|
+
function readReturnSummaries(root) {
|
|
70
|
+
const file = (0, returns_1.returnsLogPath)(root);
|
|
71
|
+
const metadata = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
72
|
+
if (!metadata)
|
|
73
|
+
return null;
|
|
74
|
+
if (!metadata.isFile() || metadata.nlink !== 1 || metadata.size > supervision_1.SHEPHERD_OBSERVATION_MAX_BYTES)
|
|
75
|
+
throw new Error("Invalid returns source");
|
|
76
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
77
|
+
let raw;
|
|
78
|
+
try {
|
|
79
|
+
const opened = fs.fstatSync(fd);
|
|
80
|
+
if (opened.ino !== metadata.ino || opened.dev !== metadata.dev)
|
|
81
|
+
throw new Error("Returns source changed");
|
|
82
|
+
const bytes = Buffer.alloc(supervision_1.SHEPHERD_OBSERVATION_MAX_BYTES + 1);
|
|
83
|
+
let size = 0;
|
|
84
|
+
while (size < bytes.length) {
|
|
85
|
+
const received = fs.readSync(fd, bytes, size, bytes.length - size, null);
|
|
86
|
+
if (!received)
|
|
87
|
+
break;
|
|
88
|
+
size += received;
|
|
89
|
+
}
|
|
90
|
+
if (size > supervision_1.SHEPHERD_OBSERVATION_MAX_BYTES)
|
|
91
|
+
throw new Error("Returns source exceeds limit");
|
|
92
|
+
raw = bytes.toString("utf8", 0, size);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
fs.closeSync(fd);
|
|
96
|
+
}
|
|
97
|
+
return raw.split("\n").filter(line => line.trim()).map(line => {
|
|
98
|
+
const value = JSON.parse(line);
|
|
99
|
+
if (!object(value) || typeof value.at !== "string" || typeof value.host !== "string" || typeof value.session !== "string"
|
|
100
|
+
|| typeof value.transition !== "string" || typeof value.kind !== "string" || !kinds.has(value.kind)
|
|
101
|
+
|| !value.host || !value.session || !value.transition || (value.action !== "respond" && value.action !== "let_through")
|
|
102
|
+
|| typeof value.reason !== "string")
|
|
103
|
+
throw new Error("Invalid returns source schema");
|
|
104
|
+
return { at: iso(value.at), host: safe(value.host), session: safe(value.session), transition: value.transition, kind: value.kind,
|
|
105
|
+
action: value.action, reason: safe(value.reason) };
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function createShepherdObservationPublisher(options) {
|
|
109
|
+
const now = options.now ?? Date.now;
|
|
110
|
+
let stopped = false;
|
|
111
|
+
let observed = null;
|
|
112
|
+
let returns = [];
|
|
113
|
+
let custody = unknownCustody();
|
|
114
|
+
const events = { returns: emptyPage(), custody: emptyPage(), notices: emptyPage() };
|
|
115
|
+
let observations = emptyPage();
|
|
116
|
+
function projectAndPublish() {
|
|
117
|
+
const at = now();
|
|
118
|
+
const health = {
|
|
119
|
+
host: observed && !observed.error ? "ok" : "missing", guidance: "ok", mandates: "ok", returns: "ok", custody: "ok", notices: "ok",
|
|
120
|
+
};
|
|
121
|
+
const errors = [];
|
|
122
|
+
const fail = (source) => {
|
|
123
|
+
health[source] = "error";
|
|
124
|
+
errors.push({ source, code: "read_failed", detail: `The ${source} source could not be read. Any retained history is not current.` });
|
|
125
|
+
};
|
|
126
|
+
const read = (source, fn) => {
|
|
127
|
+
try {
|
|
128
|
+
return fn();
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
fail(source);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const guidance = read("guidance", () => (0, guidance_1.readGuidance)(options.agentRoot));
|
|
136
|
+
const mandates = read("mandates", () => (0, mandates_1.readShepherdMandates)(options.agentRoot));
|
|
137
|
+
try {
|
|
138
|
+
const current = readReturnSummaries(options.agentRoot);
|
|
139
|
+
if (current === null)
|
|
140
|
+
health.returns = "missing";
|
|
141
|
+
else {
|
|
142
|
+
returns = current;
|
|
143
|
+
events.returns = page(current.map(record => ({
|
|
144
|
+
id: `return:${idFor([record.host, record.session, record.transition, record.at])}`,
|
|
145
|
+
host: record.host, session: record.session, instance: null, at: record.at, kind: "judgment", summary: record.kind,
|
|
146
|
+
})));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
fail("returns");
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const current = (0, recovery_1.readCheckInSnapshot)((0, checkin_store_1.checkInStorePath)(options.agentRoot), { limit: supervision_1.SHEPHERD_SUPERVISION_LIMIT });
|
|
154
|
+
if (current.state === "missing")
|
|
155
|
+
health.custody = "missing";
|
|
156
|
+
else {
|
|
157
|
+
custody = {
|
|
158
|
+
revision: current.revision, bytes: current.bytes, capacity: current.capacity,
|
|
159
|
+
claims: { totalCount: current.totalCount, truncated: current.nextCursor !== null, items: current.entries.map(entry => ({
|
|
160
|
+
id: entry.id, host: safe(entry.host), session: safe(entry.session), instance: entry.instance === null ? null : safe(entry.instance),
|
|
161
|
+
state: entry.state, claimedAt: iso(entry.claimedAt), reason: entry.reason === null ? null : safe(entry.reason), retired: entry.retired,
|
|
162
|
+
})) },
|
|
163
|
+
};
|
|
164
|
+
events.custody = page(current.entries.flatMap(entry => {
|
|
165
|
+
const base = { host: safe(entry.host), session: safe(entry.session), instance: entry.instance === null ? null : safe(entry.instance) };
|
|
166
|
+
const boundAdmission = entry.receipt?.requestId === entry.id && entry.receipt.surfaceId === entry.session
|
|
167
|
+
&& entry.receipt.processInstance === entry.instance && entry.receipt.pasteAdmitted && entry.receipt.submitAdmitted;
|
|
168
|
+
return [{
|
|
169
|
+
...base, id: `input:${entry.id}`, at: iso(entry.claimedAt),
|
|
170
|
+
kind: entry.state === "sent" ? boundAdmission ? "input_admitted" : "legacy_input_reported"
|
|
171
|
+
: entry.state === "not_sent" ? "observation" : "input_unknown",
|
|
172
|
+
summary: entry.state === "sent" ? boundAdmission ? "Bound host receipt reports paste and submit admission; not proof of task completion"
|
|
173
|
+
: "Legacy dispatcher reported sent; no qualified bound paste-and-submit receipt. Not proof of input admission or task completion."
|
|
174
|
+
: entry.state === "not_sent" ? "Input refused; no terminal input"
|
|
175
|
+
: "Claim held; input outcome is unknown. Reconcile evidence, do not retry blindly.",
|
|
176
|
+
}, ...entry.reconciliations.map(receipt => ({
|
|
177
|
+
...base, id: `recovery:${entry.id}:${receipt.revision}`, at: iso(receipt.at),
|
|
178
|
+
kind: "recovery", summary: `Owner acknowledged recovery: ${receipt.resolution}; original state ${receipt.originalState}`,
|
|
179
|
+
}))];
|
|
180
|
+
}), current.nextCursor === null ? undefined : null);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
fail("custody");
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
events.notices = page((0, notice_outbox_1.readNoticeOutbox)(options.agentRoot).flatMap(entry => {
|
|
188
|
+
const base = { host: safe(entry.record.host), session: safe(entry.record.session), instance: null };
|
|
189
|
+
return [{
|
|
190
|
+
...base, id: `notice:${entry.id}`, at: iso(entry.record.at), kind: "notification_queued", summary: "Owner notification queued",
|
|
191
|
+
}, ...Object.entries(entry.deliveries).map(([recipient, delivery]) => ({
|
|
192
|
+
...base, id: `notice:${entry.id}:${idFor(recipient)}`, at: iso(delivery.at),
|
|
193
|
+
kind: delivery.state === "sent" ? "notification_delivered"
|
|
194
|
+
: delivery.state === "unknown" ? "notification_unknown" : "notification_queued",
|
|
195
|
+
summary: delivery.state === "sent" ? "Owner notification accepted by transport; not proof the owner read it"
|
|
196
|
+
: delivery.state === "unknown" ? "Owner notification delivery is unknown" : "Owner notification pending transport",
|
|
197
|
+
}))];
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
fail("notices");
|
|
202
|
+
}
|
|
203
|
+
if (observed?.error)
|
|
204
|
+
errors.push({ source: "host", code: observed.error, detail: observed.error === "sense_stopped"
|
|
205
|
+
? "The Shepherd sense is stopped. Retained observations are historical."
|
|
206
|
+
: "The terminal host is unavailable. Retained observations are historical." });
|
|
207
|
+
const sessions = (observed?.sessions ?? []).slice(0, supervision_1.SHEPHERD_SUPERVISION_LIMIT).map(row => {
|
|
208
|
+
const session = row.session;
|
|
209
|
+
let authority = "unknown";
|
|
210
|
+
let withheldReason = row.error ? "Terminal could not be read; no current policy assessment" : "Guarded automatic input is not qualified for this host";
|
|
211
|
+
if (row.error && errors.length < 50)
|
|
212
|
+
errors.push({ source: "host", code: "row_read_failed", detail: `Terminal ${safe(session.id)} could not be read` });
|
|
213
|
+
if (guidance && mandates) {
|
|
214
|
+
if ((0, guidance_1.applicableGuidance)(guidance, { host: options.host, session: session.id, cwd: session.cwd ?? null, task: null }).paused)
|
|
215
|
+
authority = "paused";
|
|
216
|
+
else if (session.cwd) {
|
|
217
|
+
const resolution = (0, mandates_1.resolveShepherdMandate)(mandates, {
|
|
218
|
+
project: session.cwd, host: options.host, session: session.id, ...(session.instance ? { instance: session.instance } : {}),
|
|
219
|
+
}, at);
|
|
220
|
+
authority = resolution.state === "authorized" ? "active" : resolution.reason === "ambiguous_mandates" ? "ambiguous" : "absent";
|
|
221
|
+
}
|
|
222
|
+
else
|
|
223
|
+
authority = "absent";
|
|
224
|
+
if (authority === "paused")
|
|
225
|
+
withheldReason = "Stored terminal pause is set";
|
|
226
|
+
else if (authority === "absent")
|
|
227
|
+
withheldReason = "No applicable stored mandate; legacy automatic paths are not uniformly mandate-enforced";
|
|
228
|
+
else if (authority === "ambiguous")
|
|
229
|
+
withheldReason = "Multiple stored mandates apply; policy scope is ambiguous";
|
|
230
|
+
}
|
|
231
|
+
const last = returns.filter(record => record.host === options.host && record.session === safe(session.id)).sort((a, b) => b.at.localeCompare(a.at))[0];
|
|
232
|
+
return {
|
|
233
|
+
host: safe(options.host), session: safe(session.id), instance: session.instance ? safe(session.instance) : null,
|
|
234
|
+
label: safe(session.title || session.ref || session.id), harness: session.agent ? safe(session.agent) : null,
|
|
235
|
+
project: session.cwd ? safe(session.cwd) : null, activity: row.activity,
|
|
236
|
+
observedAt: row.observedAt === null ? null : new Date(row.observedAt).toISOString(),
|
|
237
|
+
lastDecision: last?.kind ?? null, lastAction: last?.action ?? null, lastReason: last?.reason ?? null,
|
|
238
|
+
lastActionAt: last?.at ?? null, withheldReason, authority, capability: "unsupported",
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
const allEvents = [...Object.values(events), observations];
|
|
242
|
+
const total = allEvents.some(value => value.totalCount === null) ? null : allEvents.reduce((sum, value) => sum + value.totalCount, 0);
|
|
243
|
+
const snapshot = {
|
|
244
|
+
schemaVersion: 1, agentName: options.agentName, observedAt: new Date(observed?.at ?? at).toISOString(),
|
|
245
|
+
sourceHealth: health, sourceErrors: errors,
|
|
246
|
+
sessions: { totalCount: observed ? observed.sessions.length : null,
|
|
247
|
+
truncated: !observed || observed.sessions.length > sessions.length, items: sessions },
|
|
248
|
+
timeline: page(allEvents.flatMap(value => value.items), total), custody,
|
|
249
|
+
};
|
|
250
|
+
try {
|
|
251
|
+
if (!(0, supervision_1.isShepherdSupervisionSnapshot)(snapshot))
|
|
252
|
+
throw new Error("Invalid bounded observation projection");
|
|
253
|
+
const encoded = JSON.stringify(snapshot);
|
|
254
|
+
if (Buffer.byteLength(encoded) > supervision_1.SHEPHERD_OBSERVATION_MAX_BYTES)
|
|
255
|
+
throw new Error("Observation exceeds publication limit");
|
|
256
|
+
const dir = (0, returns_1.shepherdStateDir)(options.agentRoot);
|
|
257
|
+
(0, checkin_store_1.validateCustodyDirectory)(dir, true);
|
|
258
|
+
const temporary = path.join(dir, `${supervision_1.SHEPHERD_OBSERVATION_FILE}.${(0, node_crypto_1.randomUUID)()}.tmp`);
|
|
259
|
+
const fd = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
|
|
260
|
+
try {
|
|
261
|
+
fs.writeFileSync(fd, encoded);
|
|
262
|
+
fs.fsyncSync(fd);
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
fs.closeSync(fd);
|
|
266
|
+
}
|
|
267
|
+
fs.renameSync(temporary, path.join(dir, supervision_1.SHEPHERD_OBSERVATION_FILE));
|
|
268
|
+
const directory = fs.openSync(dir, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
|
|
269
|
+
try {
|
|
270
|
+
fs.fsyncSync(directory);
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
fs.closeSync(directory);
|
|
274
|
+
}
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.shepherd_observation_failed",
|
|
279
|
+
message: "Could not publish a safe Shepherd observation; the previous replica must age out", meta: { agent: options.agentName } });
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function publish() {
|
|
284
|
+
try {
|
|
285
|
+
return projectAndPublish();
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.shepherd_observation_failed",
|
|
289
|
+
message: "Could not assemble a Shepherd observation; the previous replica must age out", meta: { agent: options.agentName } });
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
observe(observation) {
|
|
295
|
+
if (stopped)
|
|
296
|
+
return false;
|
|
297
|
+
observed = structuredClone(observation);
|
|
298
|
+
if (!observation.error) {
|
|
299
|
+
const current = observation.sessions.slice(0, supervision_1.SHEPHERD_SUPERVISION_LIMIT).map(row => ({
|
|
300
|
+
id: `observation:${idFor([options.host, row.session.id, row.session.instance ?? null, observation.at])}`,
|
|
301
|
+
host: safe(options.host), session: safe(row.session.id), instance: row.session.instance ? safe(row.session.instance) : null,
|
|
302
|
+
at: new Date(observation.at).toISOString(), kind: "observation",
|
|
303
|
+
summary: row.error ? "Terminal observation failed" : `Observed terminal activity: ${row.activity}`,
|
|
304
|
+
}));
|
|
305
|
+
const unique = new Map([...current, ...observations.items].map(event => [event.id, event]));
|
|
306
|
+
observations = page([...unique.values()], observations.totalCount
|
|
307
|
+
+ current.filter(event => !observations.items.some(previous => previous.id === event.id)).length);
|
|
308
|
+
}
|
|
309
|
+
return publish();
|
|
310
|
+
},
|
|
311
|
+
refresh() { return !stopped && publish(); },
|
|
312
|
+
stop() {
|
|
313
|
+
if (stopped)
|
|
314
|
+
return;
|
|
315
|
+
observed = { ...(observed ?? { at: 0, sessions: [] }), error: "sense_stopped" };
|
|
316
|
+
publish();
|
|
317
|
+
stopped = true;
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
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.createDefaultShepherdOperationDeps = createDefaultShepherdOperationDeps;
|
|
37
|
+
const path = __importStar(require("node:path"));
|
|
38
|
+
const fs = __importStar(require("node:fs"));
|
|
39
|
+
const identity_1 = require("../../heart/identity");
|
|
40
|
+
const runtime_credentials_1 = require("../../heart/runtime-credentials");
|
|
41
|
+
const shepherd_config_1 = require("../../heart/shepherd-config");
|
|
42
|
+
const host_1 = require("./host");
|
|
43
|
+
const surface_1 = require("./surface");
|
|
44
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
45
|
+
function createDefaultShepherdOperationDeps(lifecycle) {
|
|
46
|
+
return {
|
|
47
|
+
agentRoot: agent => path.join((0, identity_1.getAgentBundlesRoot)(), `${agent}.ouro`),
|
|
48
|
+
resolveOwner: async (agent) => {
|
|
49
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(agent))
|
|
50
|
+
throw new Error("existing local owner requires a safe agent name");
|
|
51
|
+
const root = path.join((0, identity_1.getAgentBundlesRoot)(), `${agent}.ouro`);
|
|
52
|
+
if (!fs.lstatSync(root, { throwIfNoEntry: false })?.isDirectory()
|
|
53
|
+
|| !fs.lstatSync(path.join(root, "friends"), { throwIfNoEntry: false })?.isDirectory()) {
|
|
54
|
+
throw new Error("existing local owner linking is required; no bundle or friends directory was created");
|
|
55
|
+
}
|
|
56
|
+
return { actorId: await (0, surface_1.resolveExistingLocalOwner)((0, surface_1.localShepherdFriendStore)(agent)) };
|
|
57
|
+
},
|
|
58
|
+
host: async (agent) => {
|
|
59
|
+
const machine = (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent);
|
|
60
|
+
if (!machine.ok)
|
|
61
|
+
throw new Error("machine Shepherd connection configuration is unavailable");
|
|
62
|
+
const connection = (0, shepherd_config_1.resolveShepherdConnection)(agent, machine.config);
|
|
63
|
+
if (!connection.ok)
|
|
64
|
+
throw new Error(connection.error);
|
|
65
|
+
return (0, host_1.createShepherdHost)(connection.connection, { agentName: agent });
|
|
66
|
+
},
|
|
67
|
+
senseStopped: async (agent) => {
|
|
68
|
+
const response = await lifecycle(agent);
|
|
69
|
+
if (!response.ok)
|
|
70
|
+
throw new Error(response.error ?? "daemon lifecycle unavailable");
|
|
71
|
+
const value = response.data;
|
|
72
|
+
if (value === null || typeof value !== "object" || !("kind" in value) || value.kind !== "daemon_lifecycle"
|
|
73
|
+
|| !("enabled" in value) || value.enabled !== false || !("processState" in value) || value.processState !== "stopped"
|
|
74
|
+
|| !("checkedAt" in value) || typeof value.checkedAt !== "string" || !Number.isFinite(Date.parse(value.checkedAt))
|
|
75
|
+
|| Date.parse(value.checkedAt) > Date.now() || Date.now() - Date.parse(value.checkedAt) > 30_000) {
|
|
76
|
+
throw new Error("fresh disabled and stopped daemon lifecycle proof is unavailable");
|
|
77
|
+
}
|
|
78
|
+
(0, runtime_1.emitNervesEvent)({
|
|
79
|
+
component: "senses", event: "senses.shepherd_maintenance_quiescence_confirmed",
|
|
80
|
+
message: "validated fresh daemon proof that Shepherd is disabled and stopped",
|
|
81
|
+
meta: { agent, processState: "stopped" },
|
|
82
|
+
});
|
|
83
|
+
return { kind: "daemon_lifecycle", enabled: false, processState: "stopped", checkedAt: value.checkedAt };
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|