@ouro.bot/cli 0.1.0-alpha.934 → 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.
Files changed (40) hide show
  1. package/changelog.json +18 -0
  2. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
  3. package/deploy/unraid/sanctuary.xml +1 -1
  4. package/dist/heart/daemon/cli-defaults.js +5 -0
  5. package/dist/heart/daemon/cli-exec.js +9 -0
  6. package/dist/heart/daemon/cli-help.js +2 -2
  7. package/dist/heart/daemon/cli-parse.js +2 -1
  8. package/dist/heart/daemon/daemon.js +14 -0
  9. package/dist/heart/daemon/ouro-entry.js +4 -1
  10. package/dist/heart/daemon/runtime-logging.js +1 -1
  11. package/dist/heart/daemon/sense-manager.js +53 -0
  12. package/dist/heart/mailbox/mailbox-http-hooks.js +1 -0
  13. package/dist/heart/mailbox/mailbox-http-routes.js +8 -0
  14. package/dist/heart/mailbox/mailbox-read.js +3 -1
  15. package/dist/heart/mailbox/mailbox-supervision.js +131 -0
  16. package/dist/heart/shepherd/custody-types.js +2 -0
  17. package/dist/heart/shepherd/mandates.js +352 -0
  18. package/dist/heart/shepherd/operation-commands.js +19 -0
  19. package/dist/heart/shepherd/operations.js +135 -0
  20. package/dist/heart/shepherd/supervision.js +114 -0
  21. package/dist/heart/shepherd/types.js +2 -0
  22. package/dist/mailbox-ui/assets/index-DsiKaPyH.css +1 -0
  23. package/dist/mailbox-ui/assets/index-xU5krmwW.js +1 -0
  24. package/dist/mailbox-ui/index.html +2 -2
  25. package/dist/nerves/coverage/file-completeness.js +4 -0
  26. package/dist/repertoire/tools-shell.js +9 -4
  27. package/dist/senses/shepherd/checkin-store.js +209 -41
  28. package/dist/senses/shepherd/cmux.js +69 -23
  29. package/dist/senses/shepherd/guidance.js +5 -2
  30. package/dist/senses/shepherd/herdr.js +84 -2
  31. package/dist/senses/shepherd/observation-publisher.js +320 -0
  32. package/dist/senses/shepherd/operations-defaults.js +86 -0
  33. package/dist/senses/shepherd/operations.js +159 -0
  34. package/dist/senses/shepherd/recovery.js +543 -0
  35. package/dist/senses/shepherd/sense.js +8 -0
  36. package/dist/senses/teams.js +13 -1
  37. package/npm-shrinkwrap.json +2 -2
  38. package/package.json +1 -1
  39. package/dist/mailbox-ui/assets/index-Cyv4mw3Y.js +0 -1
  40. package/dist/mailbox-ui/assets/index-Du_9G9WO.css +0 -1
@@ -0,0 +1,543 @@
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.readCheckInSnapshot = readCheckInSnapshot;
37
+ exports.readMaintenanceCheckInSnapshot = readMaintenanceCheckInSnapshot;
38
+ exports.reconcileShepherdDispatch = reconcileShepherdDispatch;
39
+ exports.maintainShepherdCustody = maintainShepherdCustody;
40
+ const node_crypto_1 = require("node:crypto");
41
+ const fs = __importStar(require("node:fs"));
42
+ const path = __importStar(require("node:path"));
43
+ const file_lock_1 = require("../../heart/file-lock");
44
+ const runtime_1 = require("../../nerves/runtime");
45
+ const checkin_store_1 = require("./checkin-store");
46
+ const redact_1 = require("./redact");
47
+ const refuse = checkin_store_1.refuseCustody;
48
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
49
+ const identity = (value) => typeof value === "string" && value.length > 0 && value.length <= 256 && value === value.trim() && !/[\x00-\x1f\x7f-\x9f]/.test(value);
50
+ const timestamp = (value) => typeof value === "string" && Number.isFinite(Date.parse(value));
51
+ const digest = (value) => (0, node_crypto_1.createHash)("sha256").update(value).digest("hex");
52
+ function canonical(value) {
53
+ return JSON.stringify(value, (_name, item) => object(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item);
54
+ }
55
+ function validActor(request) {
56
+ return object(request.actor) && request.actor.kind === "local_owner_administration" && identity(request.actor.actorId)
57
+ && identity(request.requestId) && typeof request.expectedRevision === "number" && Number.isSafeInteger(request.expectedRevision) && request.expectedRevision >= 0;
58
+ }
59
+ function fresh(value) {
60
+ return timestamp(value) && Date.parse(value) <= Date.now() && Date.now() - Date.parse(value) <= 30_000;
61
+ }
62
+ function validRetirementShape(value) {
63
+ return object(value) && value.kind === "host_process_inventory" && identity(value.host) && identity(value.session)
64
+ && (value.instance === null || identity(value.instance)) && timestamp(value.checkedAt) && value.gone === true && typeof value.wholeSessionRetired === "boolean";
65
+ }
66
+ const validRetirement = (value) => validRetirementShape(value) && fresh(value.checkedAt);
67
+ function copyRetirement(proof) {
68
+ const { kind, host, session, instance, checkedAt, gone, wholeSessionRetired } = proof;
69
+ return { kind, host, session, instance, checkedAt, gone, wholeSessionRetired };
70
+ }
71
+ function parseJson(value, maxBytes = 8192) {
72
+ if (Buffer.byteLength(value) > maxBytes)
73
+ refuse("recovery record byte limit");
74
+ return JSON.parse(value);
75
+ }
76
+ function validRecoveryReceipt(value) {
77
+ return object(value) && Number.isSafeInteger(value.revision) && Number(value.revision) > 0 && identity(value.dispatchId)
78
+ && ["acknowledged_delivery", "retired_instance"].includes(String(value.resolution))
79
+ && ["claimed", "sent", "unknown"].includes(String(value.originalState)) && timestamp(value.at) && identity(value.actorId) && identity(value.requestId);
80
+ }
81
+ function auditRow(data) {
82
+ const parsed = parseJson(data);
83
+ if (!object(parsed) || !validRecoveryReceipt(parsed.receipt))
84
+ refuse("invalid recovery audit schema");
85
+ return { receipt: parsed.receipt, evidence: parsed.evidence };
86
+ }
87
+ function reconciliationRows(db, claim) {
88
+ const dispatchId = claim.id;
89
+ const count = db.prepare("SELECT COUNT(*) AS count FROM reconciliations WHERE dispatch_id=?").get(dispatchId);
90
+ const rows = db.prepare("SELECT data FROM reconciliations WHERE dispatch_id=? ORDER BY rowid DESC LIMIT 20").all(dispatchId);
91
+ let receipt = null;
92
+ const receipts = rows.map(row => {
93
+ const audit = auditRow(row.data);
94
+ if (audit.receipt.dispatchId !== dispatchId)
95
+ refuse("recovery dispatch identity mismatch");
96
+ if (audit.receipt.resolution === "acknowledged_delivery") {
97
+ if (!matchesReceipt(claim, audit.evidence, false))
98
+ refuse("invalid recovery host receipt schema");
99
+ receipt ??= (0, checkin_store_1.copyCustodyInputReceipt)(audit.evidence.receipt);
100
+ }
101
+ else if (!validRetirementShape(audit.evidence) || audit.evidence.host !== claim.host || audit.evidence.session !== claim.session
102
+ || audit.evidence.instance !== claim.instance || audit.evidence.instance === null && !audit.evidence.wholeSessionRetired) {
103
+ refuse("invalid recovery retirement proof schema");
104
+ }
105
+ return audit.receipt;
106
+ });
107
+ return { receipts, count: count.count, receipt };
108
+ }
109
+ function readCheckInSnapshot(file, options) {
110
+ return readCustodySnapshot(file, options, "read");
111
+ }
112
+ /** Recovery may inspect a held marker only after trusted daemon quiescence; this never opens a writer. */
113
+ function readMaintenanceCheckInSnapshot(file, options, sense) {
114
+ requireStoppedSense({ sense });
115
+ return readCustodySnapshot(file, options, "maintenance-read");
116
+ }
117
+ function readCustodySnapshot(file, options, mode) {
118
+ if (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > 200)
119
+ refuse("inspection limit must be 1..200");
120
+ if (options.id !== undefined && (!/^[a-f0-9]{64}$/.test(options.id) || options.cursor !== undefined))
121
+ refuse("invalid inspection ID or conflicting cursor");
122
+ let cursor = null;
123
+ if (options.cursor !== undefined) {
124
+ let parsed;
125
+ try {
126
+ parsed = JSON.parse(Buffer.from(options.cursor, "base64url").toString("utf8"));
127
+ }
128
+ catch {
129
+ refuse("invalid inspection cursor");
130
+ }
131
+ if (options.cursor.length > 200 || !object(parsed) || typeof parsed.after !== "string" || !/^[a-f0-9]{64}$/.test(parsed.after)
132
+ || typeof parsed.revision !== "number" || !Number.isSafeInteger(parsed.revision) || parsed.revision < 0)
133
+ refuse("invalid inspection cursor");
134
+ cursor = { after: parsed.after, revision: parsed.revision };
135
+ }
136
+ const db = (0, checkin_store_1.openCustodyDatabase)(file, mode);
137
+ if (!db)
138
+ return {
139
+ state: "missing", schemaVersion: null, revision: null, bytes: null, capacity: "unknown",
140
+ totalCount: null, noticeCount: null, counts: null, entries: [], nextCursor: null,
141
+ };
142
+ try {
143
+ return db.transaction(() => {
144
+ const schemaVersion = Number(db.pragma("user_version", { simple: true }));
145
+ const revision = schemaVersion === 2 ? (0, checkin_store_1.custodyRevision)(db) : 0;
146
+ if (cursor && cursor.revision !== revision)
147
+ refuse("inspection cursor revision conflict");
148
+ const capacity = (0, checkin_store_1.custodyCapacity)(db);
149
+ const total = db.prepare("SELECT COUNT(*) AS count FROM dispatches").get();
150
+ const notices = db.prepare("SELECT COUNT(*) AS count FROM notices").get();
151
+ const grouped = db.prepare("SELECT json_extract(data,'$.state') AS state,COUNT(*) AS count FROM dispatches GROUP BY json_extract(data,'$.state')").all();
152
+ const counts = { claimed: 0, sent: 0, not_sent: 0, unknown: 0 };
153
+ for (const group of grouped) {
154
+ if (!["claimed", "sent", "not_sent", "unknown"].includes(group.state))
155
+ refuse("invalid custody state schema");
156
+ counts[group.state] = group.count;
157
+ }
158
+ const rows = (options.id !== undefined
159
+ ? db.prepare("SELECT id,host,session,instance,data FROM dispatches WHERE id=?").all(options.id)
160
+ : db.prepare("SELECT id,host,session,instance,data FROM dispatches WHERE id>? ORDER BY id LIMIT ?").all(cursor?.after ?? "", options.limit + 1));
161
+ const page = rows.slice(0, options.limit);
162
+ const entries = page.map(row => {
163
+ const claim = (0, checkin_store_1.parseCustodyDispatch)(row);
164
+ const audit = schemaVersion === 2 ? reconciliationRows(db, claim) : { receipts: [], count: 0, receipt: null };
165
+ const retired = schemaVersion === 2 && (0, checkin_store_1.hasRetiredCustodyInstance)(db, claim);
166
+ const entry = {
167
+ id: claim.id, host: claim.host, session: claim.session, instance: claim.instance, state: claim.state, trigger: claim.trigger,
168
+ claimedAt: claim.at, reason: claim.reason === undefined ? null : (0, redact_1.redactSecrets)(claim.reason), retired,
169
+ receipt: claim.receipt ? (0, checkin_store_1.copyCustodyInputReceipt)(claim.receipt) : audit.receipt, reconciliations: audit.receipts, reconciliationCount: audit.count,
170
+ };
171
+ return entry;
172
+ });
173
+ return {
174
+ state: "ready", schemaVersion, revision, ...capacity, totalCount: total.count, noticeCount: notices.count, counts, entries,
175
+ nextCursor: rows.length > options.limit ? Buffer.from(JSON.stringify({ after: page.at(-1).id, revision })).toString("base64url") : null,
176
+ };
177
+ })();
178
+ }
179
+ finally {
180
+ db.close();
181
+ }
182
+ }
183
+ function priorRequest(db, request, hash, validate) {
184
+ const row = db.prepare("SELECT hash,receipt FROM custody_requests WHERE actor_id=? AND request_id=?").get(request.actor.actorId, request.requestId);
185
+ if (!row)
186
+ return null;
187
+ if (row.hash !== hash)
188
+ refuse("owner request ID was already used with different arguments");
189
+ const receipt = parseJson(row.receipt);
190
+ if (!validate(receipt))
191
+ refuse("invalid owner request receipt schema");
192
+ return receipt;
193
+ }
194
+ function rememberRequest(db, request, hash, receipt) {
195
+ const serialized = JSON.stringify(receipt);
196
+ db.prepare("INSERT INTO custody_requests(actor_id,request_id,hash,receipt) VALUES(?,?,?,?)").run(request.actor.actorId, request.requestId, hash, serialized);
197
+ }
198
+ function retireInstance(db, proof, provenance, archive) {
199
+ if (proof.instance === null && !proof.wholeSessionRetired)
200
+ refuse("null instance requires whole session retirement proof");
201
+ const prior = (0, checkin_store_1.readRetiredCustodyRecord)(db, proof);
202
+ if (prior && !archive)
203
+ return;
204
+ if (prior?.archive !== undefined && canonical(prior.archive) !== canonical(archive))
205
+ refuse("retired instance archive reference already assigned; manual maintenance required");
206
+ if (Buffer.byteLength(JSON.stringify({ ...(prior ?? { proof, provenance }), ...(archive === undefined ? {} : { archive }) })) > 4096)
207
+ refuse("retired instance index capacity");
208
+ const data = JSON.stringify({ ...(prior ?? { proof: copyRetirement(proof), provenance }), ...(archive === undefined ? {} : { archive }) });
209
+ db.prepare(`INSERT INTO retired_instances(key,host,session,instance,data) VALUES(?,?,?,?,?)
210
+ ON CONFLICT(key) DO UPDATE SET data=excluded.data`).run((0, checkin_store_1.retiredCustodyKey)(proof), proof.host, proof.session, proof.instance, data);
211
+ }
212
+ function matchesReceipt(claim, evidence, requireFresh = true) {
213
+ return object(evidence) && evidence.kind === "guarded_host_receipt_v1" && timestamp(evidence.checkedAt) && (!requireFresh || fresh(evidence.checkedAt))
214
+ && evidence.host === claim.host && (0, checkin_store_1.validCustodyInputReceipt)(evidence.receipt)
215
+ && evidence.receipt.requestId === claim.id && evidence.receipt.surfaceId === claim.session
216
+ && evidence.receipt.processInstance === claim.instance && evidence.receipt.pasteAdmitted && evidence.receipt.submitAdmitted;
217
+ }
218
+ async function reconcileShepherdDispatch(root, request) {
219
+ if (!object(request) || !validActor(request) || !identity(request.id))
220
+ refuse("invalid owner reconciliation request");
221
+ request = structuredClone(request);
222
+ const hash = digest(canonical({ operation: "reconcile", request: { ...request, evidence: { ...request.evidence, checkedAt: undefined } } }));
223
+ const file = (0, checkin_store_1.checkInStorePath)(root);
224
+ // Do not turn a missing ledger into a new source of authority.
225
+ const inspection = readCheckInSnapshot(file, { limit: 1 });
226
+ if (inspection.state === "missing")
227
+ refuse("custody store is missing");
228
+ return (0, file_lock_1.withFileLock)(path.dirname(file), "operations", () => {
229
+ const db = (0, checkin_store_1.openCustodyDatabase)(file, "write");
230
+ try {
231
+ let mutated = false;
232
+ const result = db.transaction(() => {
233
+ (0, checkin_store_1.custodyCapacity)(db, true);
234
+ const prior = priorRequest(db, request, hash, validRecoveryReceipt);
235
+ if (prior)
236
+ return prior;
237
+ if ((0, checkin_store_1.custodyRevision)(db) !== request.expectedRevision)
238
+ refuse("reconciliation revision conflict");
239
+ const row = db.prepare("SELECT id,host,session,instance,data FROM dispatches WHERE id=?").get(request.id);
240
+ if (!row)
241
+ refuse("dispatch not found");
242
+ const claim = (0, checkin_store_1.parseCustodyDispatch)(row);
243
+ if (claim.state === "not_sent")
244
+ refuse("definite not-sent dispatch requires no reconciliation");
245
+ let resolution;
246
+ let retirement = null;
247
+ let verifiedEvidence;
248
+ if (matchesReceipt(claim, request.evidence)) {
249
+ resolution = "acknowledged_delivery";
250
+ verifiedEvidence = {
251
+ kind: request.evidence.kind, host: request.evidence.host, checkedAt: request.evidence.checkedAt,
252
+ receipt: (0, checkin_store_1.copyCustodyInputReceipt)(request.evidence.receipt),
253
+ };
254
+ }
255
+ else if (validRetirement(request.evidence) && request.evidence.host === claim.host
256
+ && request.evidence.session === claim.session && request.evidence.instance === claim.instance) {
257
+ resolution = "retired_instance";
258
+ retirement = request.evidence;
259
+ verifiedEvidence = copyRetirement(request.evidence);
260
+ }
261
+ else
262
+ refuse("matching confirmed host receipt or fresh exact gone-instance proof required");
263
+ const receipt = {
264
+ revision: (0, checkin_store_1.advanceCustodyRevision)(db), dispatchId: claim.id, resolution, originalState: claim.state,
265
+ at: new Date(Date.now()).toISOString(), actorId: request.actor.actorId, requestId: request.requestId,
266
+ };
267
+ if (retirement)
268
+ retireInstance(db, retirement, receipt);
269
+ if (Buffer.byteLength(JSON.stringify({ receipt, evidence: request.evidence })) > 8192)
270
+ refuse("recovery record byte limit");
271
+ const audit = JSON.stringify({ receipt, evidence: verifiedEvidence });
272
+ db.prepare("INSERT INTO reconciliations(id,dispatch_id,data) VALUES(?,?,?)").run(digest(JSON.stringify([request.actor.actorId, request.requestId])), claim.id, audit);
273
+ rememberRequest(db, request, hash, receipt);
274
+ mutated = true;
275
+ return receipt;
276
+ }).immediate();
277
+ if (mutated)
278
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_dispatch_reconciled", message: "appended custody reconciliation without rewriting original outcome", meta: { resolution: result.resolution, revision: result.revision } });
279
+ return result;
280
+ }
281
+ finally {
282
+ db.close();
283
+ }
284
+ });
285
+ }
286
+ function syncFile(file) {
287
+ const handle = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
288
+ try {
289
+ fs.fsyncSync(handle);
290
+ }
291
+ finally {
292
+ fs.closeSync(handle);
293
+ }
294
+ }
295
+ function syncDirectory(dir) {
296
+ const handle = fs.openSync(dir, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
297
+ try {
298
+ fs.fsyncSync(handle);
299
+ }
300
+ finally {
301
+ fs.closeSync(handle);
302
+ }
303
+ }
304
+ function privateRegular(file, maxBytes) {
305
+ const stat = fs.lstatSync(file);
306
+ if (!stat.isFile() || stat.nlink !== 1 || stat.uid !== process.getuid() || (stat.mode & 0o7777) !== 0o600 || stat.size > maxBytes)
307
+ refuse("maintenance private file ownership, mode, link or byte limit");
308
+ }
309
+ function fileDigest(file) {
310
+ privateRegular(file, 64 * 1024 * 1024);
311
+ const handle = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
312
+ try {
313
+ const hash = (0, node_crypto_1.createHash)("sha256");
314
+ const buffer = Buffer.alloc(65536);
315
+ for (;;) {
316
+ const count = fs.readSync(handle, buffer, 0, buffer.length, null);
317
+ if (!count)
318
+ break;
319
+ hash.update(buffer.subarray(0, count));
320
+ }
321
+ return hash.digest("hex");
322
+ }
323
+ finally {
324
+ fs.closeSync(handle);
325
+ }
326
+ }
327
+ function writePrivateJson(file, value) {
328
+ const temp = `${file}.${(0, node_crypto_1.randomUUID)()}.tmp`;
329
+ fs.writeFileSync(temp, JSON.stringify(value), { flag: "wx", mode: 0o600 });
330
+ syncFile(temp);
331
+ fs.renameSync(temp, file);
332
+ syncDirectory(path.dirname(file));
333
+ }
334
+ function maintenanceDirectory(dir, create) {
335
+ if (!path.isAbsolute(dir) || path.normalize(dir) !== dir)
336
+ refuse("maintenance destination must be an exact absolute path");
337
+ (0, checkin_store_1.validateCustodyDirectory)(dir, create);
338
+ }
339
+ function validMaintenanceReceipt(value) {
340
+ return object(value) && Number.isSafeInteger(value.revision) && Number(value.revision) > 0 && identity(value.requestId) && identity(value.actorId)
341
+ && Number.isSafeInteger(value.archivedCount) && Number(value.archivedCount) >= 0 && Number.isSafeInteger(value.retainedCount) && Number(value.retainedCount) >= 0
342
+ && typeof value.archive === "string" && path.isAbsolute(value.archive) && typeof value.archiveDigest === "string" && /^[a-f0-9]{64}$/.test(value.archiveDigest) && timestamp(value.at);
343
+ }
344
+ function readIntent(file, hash, archive, replacementPrefix) {
345
+ if (!fs.lstatSync(file, { throwIfNoEntry: false }))
346
+ return null;
347
+ privateRegular(file, 8192);
348
+ const value = parseJson(fs.readFileSync(file, "utf8"));
349
+ if (!object(value) || value.version !== 1 || value.hash !== hash || typeof value.sourceRevision !== "number" || !Number.isSafeInteger(value.sourceRevision) || value.sourceRevision < 0
350
+ || typeof value.sourceDigest !== "string" || !/^[a-f0-9]{64}$/.test(value.sourceDigest) || value.archive !== archive
351
+ || typeof value.replacement !== "string" || !value.replacement.startsWith(replacementPrefix)
352
+ || path.dirname(value.replacement) !== path.dirname(replacementPrefix)
353
+ || !["copying", "prepared"].includes(String(value.stage)))
354
+ refuse("interrupted maintenance belongs to a different request or has invalid schema");
355
+ const base = { version: 1, hash, sourceDigest: value.sourceDigest, sourceRevision: value.sourceRevision, archive, replacement: value.replacement };
356
+ if (value.stage === "prepared") {
357
+ if (!validMaintenanceReceipt(value.receipt) || value.archiveDigest !== value.receipt.archiveDigest
358
+ || typeof value.replacementDigest !== "string" || !/^[a-f0-9]{64}$/.test(value.replacementDigest))
359
+ refuse("invalid prepared maintenance schema");
360
+ return { ...base, stage: "prepared", archiveDigest: value.receipt.archiveDigest, replacementDigest: value.replacementDigest, receipt: value.receipt };
361
+ }
362
+ if (value.archiveDigest !== null || value.replacementDigest !== null || value.receipt !== null)
363
+ refuse("invalid copying maintenance schema");
364
+ return { ...base, stage: "copying", archiveDigest: null, replacementDigest: null, receipt: null };
365
+ }
366
+ function targetsClaim(proof, claim) {
367
+ return proof.host === claim.host && proof.session === claim.session && (proof.instance === null && proof.wholeSessionRetired || proof.instance === claim.instance);
368
+ }
369
+ function archiveCandidates(db, proofs) {
370
+ const ids = [];
371
+ for (const row of db.prepare("SELECT id,host,session,instance,data FROM dispatches").iterate()) {
372
+ const claim = (0, checkin_store_1.parseCustodyDispatch)(row);
373
+ if (!proofs.some(proof => targetsClaim(proof, claim)))
374
+ continue;
375
+ const retired = (0, checkin_store_1.hasRetiredCustodyInstance)(db, claim);
376
+ if ((claim.state === "claimed" || claim.state === "unknown") && !retired)
377
+ refuse("unresolved attempt must be reconciled before archive");
378
+ ids.push(claim.id);
379
+ }
380
+ return ids;
381
+ }
382
+ function validateArchive(file, expectedCount) {
383
+ const db = (0, checkin_store_1.openCustodyDatabase)(file, "read");
384
+ try {
385
+ if (db.pragma("integrity_check", { simple: true }) !== "ok")
386
+ refuse("archive integrity check failed");
387
+ const count = db.prepare("SELECT COUNT(*) AS count FROM dispatches").get();
388
+ if (count.count !== expectedCount)
389
+ refuse("archive dispatch count mismatch");
390
+ for (const row of db.prepare("SELECT id,host,session,instance,data FROM dispatches").iterate())
391
+ (0, checkin_store_1.parseCustodyDispatch)(row);
392
+ }
393
+ finally {
394
+ db.close();
395
+ }
396
+ }
397
+ function verifyPrepared(intent) {
398
+ if (intent.stage !== "prepared" || !intent.receipt || fileDigest(intent.archive) !== intent.archiveDigest
399
+ || fileDigest(intent.replacement) !== intent.replacementDigest)
400
+ refuse("prepared maintenance payload digest mismatch");
401
+ const manifest = `${intent.archive}.json`;
402
+ privateRegular(manifest, 8192);
403
+ const value = parseJson(fs.readFileSync(manifest, "utf8"));
404
+ if (!object(value) || value.archiveDigest !== intent.archiveDigest || value.requestHash !== intent.hash
405
+ || canonical(value.receipt) !== canonical(intent.receipt))
406
+ refuse("archive manifest mismatch");
407
+ }
408
+ function requireStoppedSense(request) {
409
+ if (!object(request.sense) || request.sense.kind !== "daemon_lifecycle" || request.sense.enabled !== false || request.sense.processState !== "stopped" || !fresh(request.sense.checkedAt)) {
410
+ refuse("maintenance requires a fresh disabled and stopped sense proof");
411
+ }
412
+ }
413
+ /** Fresh lifecycle observations are preconditions, not a change to the operator's archival intent. */
414
+ async function maintainShepherdCustody(root, request) {
415
+ if (!object(request) || !validActor(request) || typeof request.destination !== "string" || request.destination.length > 2000
416
+ || !Array.isArray(request.retiredInstances) || request.retiredInstances.length > 200
417
+ || request.requestFingerprint !== undefined && !/^[a-f0-9]{64}$/.test(request.requestFingerprint))
418
+ refuse("invalid owner maintenance request");
419
+ request = structuredClone(request);
420
+ const hash = request.requestFingerprint ?? digest(canonical({ operation: "maintenance", request: {
421
+ ...request, sense: undefined, retiredInstances: request.retiredInstances.map(proof => ({ ...proof, checkedAt: undefined })),
422
+ } }));
423
+ const file = (0, checkin_store_1.checkInStorePath)(root);
424
+ const marker = `${file}.maintenance.json`;
425
+ const archive = path.join(request.destination, `custody-${hash}.sqlite`);
426
+ const replacementPrefix = `${file}.replacement-${hash}-`;
427
+ maintenanceDirectory(request.destination, false);
428
+ if (!fs.lstatSync(file, { throwIfNoEntry: false }))
429
+ refuse("custody store is missing");
430
+ return (0, file_lock_1.withFileLock)(path.dirname(file), "operations", async () => {
431
+ let intent = readIntent(marker, hash, archive, replacementPrefix);
432
+ const db = (0, checkin_store_1.openCustodyDatabase)(file, "maintenance");
433
+ try {
434
+ const prior = priorRequest(db, request, hash, validMaintenanceReceipt);
435
+ if (prior) {
436
+ if (intent) {
437
+ if (intent.stage !== "prepared" || fileDigest(file) !== intent.replacementDigest || fileDigest(archive) !== prior.archiveDigest)
438
+ refuse("completed maintenance does not match recovery marker");
439
+ fs.unlinkSync(marker);
440
+ syncDirectory(path.dirname(file));
441
+ }
442
+ return prior;
443
+ }
444
+ requireStoppedSense(request);
445
+ if (!request.retiredInstances.length || !request.retiredInstances.every(validRetirement)
446
+ || request.retiredInstances.some(proof => proof.instance === null && !proof.wholeSessionRetired))
447
+ refuse("maintenance requires fresh exact retired-instance proofs");
448
+ if ((0, checkin_store_1.custodyRevision)(db) !== request.expectedRevision)
449
+ refuse("maintenance revision conflict");
450
+ if (intent) {
451
+ if (intent.sourceRevision !== (0, checkin_store_1.custodyRevision)(db) || fileDigest(file) !== intent.sourceDigest)
452
+ refuse("maintenance source changed; manual recovery required");
453
+ if (intent.stage === "prepared") {
454
+ verifyPrepared(intent);
455
+ requireStoppedSense(request);
456
+ db.close();
457
+ fs.renameSync(intent.replacement, file);
458
+ syncDirectory(path.dirname(file));
459
+ fs.unlinkSync(marker);
460
+ syncDirectory(path.dirname(file));
461
+ return intent.receipt;
462
+ }
463
+ }
464
+ const ids = archiveCandidates(db, request.retiredInstances);
465
+ if (!ids.length)
466
+ refuse("no settled retired-instance payloads are eligible for archive");
467
+ const total = db.prepare("SELECT COUNT(*) AS count FROM dispatches").get();
468
+ maintenanceDirectory(request.destination, true);
469
+ if (fs.lstatSync(archive, { throwIfNoEntry: false })) {
470
+ if (!intent || intent.stage !== "copying")
471
+ refuse("archive destination already exists; resume its exact prepared request");
472
+ privateRegular(archive, 64 * 1024 * 1024);
473
+ fs.renameSync(archive, `${archive}.interrupted-${(0, node_crypto_1.randomUUID)()}`);
474
+ if (fs.lstatSync(`${archive}.json`, { throwIfNoEntry: false })) {
475
+ privateRegular(`${archive}.json`, 8192);
476
+ fs.renameSync(`${archive}.json`, `${archive}.json.interrupted-${(0, node_crypto_1.randomUUID)()}`);
477
+ }
478
+ syncDirectory(request.destination);
479
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.shepherd_custody_archive_resumed", message: "preserved an interrupted archive publication before rebuilding from unchanged custody", meta: { stage: "copying" } });
480
+ }
481
+ intent = {
482
+ version: 1, hash, sourceDigest: fileDigest(file), sourceRevision: (0, checkin_store_1.custodyRevision)(db), stage: "copying", archive,
483
+ replacement: `${replacementPrefix}${(0, node_crypto_1.randomUUID)()}.sqlite`, archiveDigest: null, replacementDigest: null, receipt: null,
484
+ };
485
+ writePrivateJson(marker, intent);
486
+ const archiveTemp = `${archive}.${(0, node_crypto_1.randomUUID)()}.tmp`;
487
+ fs.writeFileSync(archiveTemp, "", { flag: "wx", mode: 0o600 });
488
+ await db.backup(archiveTemp);
489
+ syncFile(archiveTemp);
490
+ validateArchive(archiveTemp, total.count);
491
+ const archiveDigest = fileDigest(archiveTemp);
492
+ fs.writeFileSync(intent.replacement, "", { flag: "wx", mode: 0o600 });
493
+ await db.backup(intent.replacement);
494
+ const replacement = (0, checkin_store_1.openCustodyDatabase)(intent.replacement, "write");
495
+ let receipt;
496
+ try {
497
+ receipt = replacement.transaction(() => {
498
+ for (const id of ids) {
499
+ replacement.prepare("DELETE FROM reconciliations WHERE dispatch_id=?").run(id);
500
+ replacement.prepare("DELETE FROM dispatches WHERE id=?").run(id);
501
+ }
502
+ for (const proof of request.retiredInstances)
503
+ retireInstance(replacement, proof, { actorId: request.actor.actorId, requestId: request.requestId }, { path: archive, digest: archiveDigest });
504
+ const result = {
505
+ revision: (0, checkin_store_1.advanceCustodyRevision)(replacement), requestId: request.requestId, actorId: request.actor.actorId,
506
+ archivedCount: ids.length, retainedCount: total.count - ids.length, archive, archiveDigest, at: new Date(Date.now()).toISOString(),
507
+ };
508
+ rememberRequest(replacement, request, hash, result);
509
+ return result;
510
+ }).immediate();
511
+ replacement.exec("VACUUM");
512
+ (0, checkin_store_1.custodyCapacity)(replacement, true);
513
+ if (replacement.pragma("integrity_check", { simple: true }) !== "ok")
514
+ refuse("replacement custody integrity check failed");
515
+ }
516
+ finally {
517
+ replacement.close();
518
+ }
519
+ syncFile(intent.replacement);
520
+ validateArchive(intent.replacement, receipt.retainedCount);
521
+ fs.renameSync(archiveTemp, archive);
522
+ syncDirectory(request.destination);
523
+ writePrivateJson(`${archive}.json`, { version: 1, requestHash: hash, archiveDigest, receipt });
524
+ intent = { ...intent, stage: "prepared", archiveDigest, replacementDigest: fileDigest(intent.replacement), receipt };
525
+ writePrivateJson(marker, intent);
526
+ verifyPrepared(intent);
527
+ requireStoppedSense(request);
528
+ if (fileDigest(file) !== intent.sourceDigest)
529
+ refuse("maintenance source changed before swap");
530
+ db.close();
531
+ fs.renameSync(intent.replacement, file);
532
+ syncDirectory(path.dirname(file));
533
+ fs.unlinkSync(marker);
534
+ syncDirectory(path.dirname(file));
535
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_custody_archived", message: "archived settled retired custody and retained replay tombstones", meta: { archivedCount: receipt.archivedCount, retainedCount: receipt.retainedCount, revision: receipt.revision } });
536
+ return receipt;
537
+ }
538
+ finally {
539
+ if (db.open)
540
+ db.close();
541
+ }
542
+ });
543
+ }
@@ -65,6 +65,7 @@ const agent_presentation_1 = require("./agent-presentation");
65
65
  const feedback_worker_1 = require("./feedback-worker");
66
66
  const checkin_config_1 = require("./checkin-config");
67
67
  const checkin_store_1 = require("./checkin-store");
68
+ const observation_publisher_1 = require("./observation-publisher");
68
69
  /**
69
70
  * Ouro Shepherd: every time a coding agent in a terminal hands control back, a cheap judge decides
70
71
  * whether it should have. A premature return is answered in the terminal, visibly, as
@@ -212,6 +213,8 @@ async function startShepherdSenseApp(options) {
212
213
  const notifier = options.notifyOwner ? null : (0, owner_notice_1.createOwnerNotifier)({ agent, agentRoot, policy: noticePolicy(agent, agentRoot), schedule, listSessions: () => host.list() });
213
214
  const notifyOwner = options.notifyOwner ?? ((record) => notifier.notify(record));
214
215
  const custody = (options.openCheckInStore ?? checkin_store_1.openCheckInStore)((0, checkin_store_1.checkInStorePath)(agentRoot));
216
+ const observations = (0, observation_publisher_1.createShepherdObservationPublisher)({ agentRoot, agentName: agent, host: host.name, now });
217
+ observations.refresh();
215
218
  const dispatchOwner = (0, node_crypto_1.randomUUID)();
216
219
  const feedback = (options.feedbackWorker ?? feedback_worker_1.startFeedbackWorker)({ agent, agentRoot, now, schedule });
217
220
  /**
@@ -684,9 +687,13 @@ async function startShepherdSenseApp(options) {
684
687
  catch (error) {
685
688
  (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.shepherd_log_error", message: "could not log a Shepherd judgment", meta: { agent, error: error.message } });
686
689
  }
690
+ if (!stopped)
691
+ observations.refresh();
687
692
  (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_return", message: "handled a returned control", meta: { agent, host: host.name, kind: record.kind, action: record.action } });
688
693
  }
689
694
  const watcher = host.watch({
695
+ observed: observation => { if (!stopped)
696
+ observations.observe(observation); },
690
697
  returned: (event) => {
691
698
  if (stopped)
692
699
  return;
@@ -712,6 +719,7 @@ async function startShepherdSenseApp(options) {
712
719
  if (stopped)
713
720
  return;
714
721
  stopped = true;
722
+ observations.stop();
715
723
  networkWaiting.clear();
716
724
  watcher.close();
717
725
  cancelWake?.();
@@ -1248,7 +1248,19 @@ function registerBotHandlers(app, label) {
1248
1248
  // Use the SDK's single-attempt, awaitable send instead of that hidden retry loop.
1249
1249
  const stream = {
1250
1250
  emit: value => sendText(typeof value === "string" ? value : value.text),
1251
- update: () => ctx.send({ type: "typing" }),
1251
+ update: async () => {
1252
+ try {
1253
+ await ctx.send({ type: "typing" });
1254
+ }
1255
+ catch (error) {
1256
+ (0, runtime_1.emitNervesEvent)({
1257
+ level: "warn", component: "channels", event: "channel.teams_typing_failed",
1258
+ message: "Teams rejected an advisory typing update; the reply is still pending",
1259
+ meta: { conversationId: convId, inboundActivityId: activity.id,
1260
+ error: error instanceof Error ? error.message : String(error) },
1261
+ });
1262
+ }
1263
+ },
1252
1264
  close: async () => undefined,
1253
1265
  };
1254
1266
  (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.message_received", component: "channels", message: `[${label}] incoming teams message`, meta: { stage: "received", inboundActivityId: activity.id, botId: app.id, tenantId: requestContext.tenantId, userId: userId.slice(0, 12), conversationId: convId } });
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.934",
3
+ "version": "0.1.0-alpha.936",
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.934",
9
+ "version": "0.1.0-alpha.936",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.78.0",
12
12
  "@azure/identity": "^4.13.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.934",
3
+ "version": "0.1.0-alpha.936",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },