@ouro.bot/cli 0.1.0-alpha.935 → 0.1.0-alpha.937
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 +21 -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-Dbska1tZ.js +1 -0
- package/dist/mailbox-ui/assets/index-DsiKaPyH.css +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/judge-evaluation-command.js +180 -0
- package/dist/senses/shepherd/judge-evaluation-entry.js +13 -0
- package/dist/senses/shepherd/judge-evaluation.js +150 -0
- package/dist/senses/shepherd/judge.js +12 -5
- 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 +14 -9
- package/dist/senses/shepherd/shepherd-judge-corpus-v1.json +167 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
- package/dist/mailbox-ui/assets/index-Cyv4mw3Y.js +0 -1
- package/dist/mailbox-ui/assets/index-Du_9G9WO.css +0 -1
|
@@ -0,0 +1,352 @@
|
|
|
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.readShepherdMandates = readShepherdMandates;
|
|
37
|
+
exports.updateShepherdMandate = updateShepherdMandate;
|
|
38
|
+
exports.resolveShepherdMandate = resolveShepherdMandate;
|
|
39
|
+
const node_crypto_1 = require("node:crypto");
|
|
40
|
+
const fs = __importStar(require("node:fs"));
|
|
41
|
+
const path = __importStar(require("node:path"));
|
|
42
|
+
const file_lock_1 = require("../file-lock");
|
|
43
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
44
|
+
const MAX_BYTES = 2 * 1024 * 1024;
|
|
45
|
+
const MAX_LIFETIME = 7 * 86_400_000;
|
|
46
|
+
const empty = () => ({ version: 1, revision: 0, mandates: [], requests: [] });
|
|
47
|
+
const digest = (value) => (0, node_crypto_1.createHash)("sha256").update(value).digest("hex");
|
|
48
|
+
const mandateId = (actorId, requestId) => `mandate:${digest(JSON.stringify([actorId, requestId]))}`;
|
|
49
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
50
|
+
const integer = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
51
|
+
const text = (value, max) => typeof value === "string" && value.trim().length > 0 && value.length <= max && !/[\x00-\x08\x0b-\x1f\x7f-\x9f]/.test(value);
|
|
52
|
+
const identity = (value) => text(value, 200) && value === value.trim() && !/[\r\n\t]/.test(value);
|
|
53
|
+
const iso = (value) => typeof value === "string" && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
54
|
+
const exactKeys = (value, required, optional = []) => required.every(key => Object.hasOwn(value, key)) && Object.keys(value).every(key => required.includes(key) || optional.includes(key));
|
|
55
|
+
const canonicalProjectShape = (value) => typeof value === "string" && path.isAbsolute(value) && path.normalize(value) === value
|
|
56
|
+
&& (value === path.parse(value).root || !value.endsWith(path.sep)) && !/[\x00-\x1f\x7f-\x9f]/.test(value);
|
|
57
|
+
function validContext(value) {
|
|
58
|
+
return object(value) && exactKeys(value, ["kind", "actorId", "requestId"]) && value.kind === "local_owner_administration"
|
|
59
|
+
&& identity(value.actorId) && identity(value.requestId);
|
|
60
|
+
}
|
|
61
|
+
function validProvenance(value) {
|
|
62
|
+
return object(value) && exactKeys(value, ["kind", "actorId", "requestId", "at"])
|
|
63
|
+
&& validContext({ kind: value.kind, actorId: value.actorId, requestId: value.requestId }) && iso(value.at);
|
|
64
|
+
}
|
|
65
|
+
function validTarget(value) {
|
|
66
|
+
return object(value) && exactKeys(value, ["project"], ["host", "session", "instance"]) && canonicalProjectShape(value.project)
|
|
67
|
+
&& ["host", "session", "instance"].every(key => !Object.hasOwn(value, key) || identity(value[key]))
|
|
68
|
+
&& (value.session === undefined || value.host !== undefined)
|
|
69
|
+
&& (value.instance === undefined || value.session !== undefined);
|
|
70
|
+
}
|
|
71
|
+
function validRequest(value) {
|
|
72
|
+
if (!object(value) || !validContext(value.context) || !integer(value.expectedRevision))
|
|
73
|
+
return false;
|
|
74
|
+
return value.operation === "grant"
|
|
75
|
+
? exactKeys(value, ["operation", "context", "expectedRevision", "target", "task", "expiresAt"])
|
|
76
|
+
&& validTarget(value.target) && text(value.task, 2000) && iso(value.expiresAt)
|
|
77
|
+
: value.operation === "revoke" && exactKeys(value, ["operation", "context", "expectedRevision", "mandateId"])
|
|
78
|
+
&& identity(value.mandateId);
|
|
79
|
+
}
|
|
80
|
+
function validMandate(value) {
|
|
81
|
+
return object(value) && exactKeys(value, ["id", "revision", "target", "task", "action", "createdAt", "expiresAt", "status", "provenance", "revoked"])
|
|
82
|
+
&& typeof value.id === "string" && /^mandate:[a-f0-9]{64}$/.test(value.id) && integer(value.revision) && value.revision > 0
|
|
83
|
+
&& validTarget(value.target) && text(value.task, 2000) && value.action === "continue_text"
|
|
84
|
+
&& iso(value.createdAt) && iso(value.expiresAt) && Date.parse(value.expiresAt) > Date.parse(value.createdAt)
|
|
85
|
+
&& Date.parse(value.expiresAt) - Date.parse(value.createdAt) <= MAX_LIFETIME
|
|
86
|
+
&& validProvenance(value.provenance) && value.provenance.at === value.createdAt
|
|
87
|
+
&& value.id === mandateId(value.provenance.actorId, value.provenance.requestId)
|
|
88
|
+
&& (value.status === "active" ? value.revoked === null : value.status === "revoked" && validProvenance(value.revoked)
|
|
89
|
+
&& Date.parse(value.revoked.at) >= Date.parse(value.createdAt));
|
|
90
|
+
}
|
|
91
|
+
function validReceipt(value) {
|
|
92
|
+
return object(value) && exactKeys(value, ["revision", "operation", "mandateId", "provenance"])
|
|
93
|
+
&& integer(value.revision) && value.revision > 0 && (value.operation === "grant" || value.operation === "revoke")
|
|
94
|
+
&& typeof value.mandateId === "string" && /^mandate:[a-f0-9]{64}$/.test(value.mandateId) && validProvenance(value.provenance);
|
|
95
|
+
}
|
|
96
|
+
function validState(value, nowMs = Date.now()) {
|
|
97
|
+
if (!object(value) || !exactKeys(value, ["version", "revision", "mandates", "requests"])
|
|
98
|
+
|| value.version !== 1 || !integer(value.revision) || !Array.isArray(value.mandates) || !Array.isArray(value.requests)
|
|
99
|
+
|| !value.mandates.every(validMandate) || value.revision !== value.requests.length)
|
|
100
|
+
return false;
|
|
101
|
+
const ids = new Set(value.mandates.map(mandate => mandate.id));
|
|
102
|
+
if (ids.size !== value.mandates.length
|
|
103
|
+
|| value.mandates.filter(mandate => mandate.status === "active" && Date.parse(mandate.expiresAt) > nowMs).length > 100)
|
|
104
|
+
return false;
|
|
105
|
+
const requests = new Set();
|
|
106
|
+
const grants = new Map();
|
|
107
|
+
const revocations = new Map();
|
|
108
|
+
for (const [index, request] of value.requests.entries()) {
|
|
109
|
+
if (!object(request) || !exactKeys(request, ["actorId", "requestId", "hash", "receipt"])
|
|
110
|
+
|| !identity(request.actorId) || !identity(request.requestId) || typeof request.hash !== "string" || !/^[a-f0-9]{64}$/.test(request.hash)
|
|
111
|
+
|| !validReceipt(request.receipt) || request.receipt.revision !== index + 1
|
|
112
|
+
|| request.actorId !== request.receipt.provenance.actorId || request.requestId !== request.receipt.provenance.requestId
|
|
113
|
+
|| !ids.has(request.receipt.mandateId))
|
|
114
|
+
return false;
|
|
115
|
+
const key = JSON.stringify([request.actorId, request.requestId]);
|
|
116
|
+
const operations = request.receipt.operation === "grant" ? grants : revocations;
|
|
117
|
+
if (requests.has(key) || operations.has(request.receipt.mandateId))
|
|
118
|
+
return false;
|
|
119
|
+
requests.add(key);
|
|
120
|
+
operations.set(request.receipt.mandateId, request.receipt);
|
|
121
|
+
}
|
|
122
|
+
return value.mandates.every(mandate => {
|
|
123
|
+
const grant = grants.get(mandate.id);
|
|
124
|
+
const revoked = revocations.get(mandate.id);
|
|
125
|
+
return grant && canonical(grant.provenance) === canonical(mandate.provenance)
|
|
126
|
+
&& (mandate.status === "active" ? !revoked && mandate.revision === grant.revision
|
|
127
|
+
: revoked && revoked.revision > grant.revision && revoked.revision === mandate.revision
|
|
128
|
+
&& canonical(revoked.provenance) === canonical(mandate.revoked));
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function owned(metadata) {
|
|
132
|
+
if (!process.getuid || metadata.uid !== process.getuid())
|
|
133
|
+
throw new Error("Shepherd mandates require current-user ownership");
|
|
134
|
+
}
|
|
135
|
+
function directory(candidate, privateMode, create) {
|
|
136
|
+
let metadata = fs.lstatSync(candidate, { throwIfNoEntry: false });
|
|
137
|
+
if (!metadata && create) {
|
|
138
|
+
try {
|
|
139
|
+
fs.mkdirSync(candidate, { mode: 0o700 });
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error.code !== "EEXIST")
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
metadata = fs.lstatSync(candidate);
|
|
146
|
+
}
|
|
147
|
+
if (!metadata)
|
|
148
|
+
return false;
|
|
149
|
+
owned(metadata);
|
|
150
|
+
if (!metadata.isDirectory())
|
|
151
|
+
throw new Error("Shepherd mandate directory must be a directory, not a link");
|
|
152
|
+
if (privateMode ? (metadata.mode & 0o7777) !== 0o700 : (metadata.mode & 0o022) !== 0) {
|
|
153
|
+
throw new Error("Shepherd mandate directory requires 0700 mode and non-writable ancestors");
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
function storeDirectory(root, create = false) {
|
|
158
|
+
const base = path.resolve(root);
|
|
159
|
+
if (!directory(base, false, false))
|
|
160
|
+
throw new Error("Shepherd mandate bundle directory is missing");
|
|
161
|
+
let current = base;
|
|
162
|
+
for (const name of ["state", "senses", "shepherd"]) {
|
|
163
|
+
current = path.join(current, name);
|
|
164
|
+
if (!directory(current, name === "shepherd", create))
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
return current;
|
|
168
|
+
}
|
|
169
|
+
function regularFile(metadata) {
|
|
170
|
+
owned(metadata);
|
|
171
|
+
if (!metadata.isFile())
|
|
172
|
+
throw new Error("Shepherd mandates require a regular file, not a link");
|
|
173
|
+
if ((metadata.mode & 0o7777) !== 0o600)
|
|
174
|
+
throw new Error("Shepherd mandate file requires 0600 mode");
|
|
175
|
+
if (metadata.nlink !== 1)
|
|
176
|
+
throw new Error("Shepherd mandate file requires a single link");
|
|
177
|
+
if (metadata.size > MAX_BYTES)
|
|
178
|
+
throw new Error("Shepherd mandates exceed 2 MiB");
|
|
179
|
+
}
|
|
180
|
+
/** Missing is empty, not a grant. No read creates, repairs, or changes any filesystem entry. */
|
|
181
|
+
function readShepherdMandates(agentRoot) {
|
|
182
|
+
const dir = storeDirectory(agentRoot);
|
|
183
|
+
if (!dir)
|
|
184
|
+
return empty();
|
|
185
|
+
const file = path.join(dir, "mandates.json");
|
|
186
|
+
const metadata = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
187
|
+
if (!metadata)
|
|
188
|
+
return empty();
|
|
189
|
+
regularFile(metadata);
|
|
190
|
+
const handle = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
191
|
+
let raw;
|
|
192
|
+
try {
|
|
193
|
+
const opened = fs.fstatSync(handle);
|
|
194
|
+
regularFile(opened);
|
|
195
|
+
if (opened.ino !== metadata.ino || opened.dev !== metadata.dev)
|
|
196
|
+
throw new Error("Shepherd mandate file changed during read");
|
|
197
|
+
const bytes = Buffer.alloc(MAX_BYTES + 1);
|
|
198
|
+
let count = 0;
|
|
199
|
+
while (count < bytes.length) {
|
|
200
|
+
const received = fs.readSync(handle, bytes, count, bytes.length - count, null);
|
|
201
|
+
if (received === 0)
|
|
202
|
+
break;
|
|
203
|
+
count += received;
|
|
204
|
+
}
|
|
205
|
+
if (count > MAX_BYTES)
|
|
206
|
+
throw new Error("Shepherd mandates exceed 2 MiB");
|
|
207
|
+
raw = bytes.toString("utf8", 0, count);
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
fs.closeSync(handle);
|
|
211
|
+
}
|
|
212
|
+
let state;
|
|
213
|
+
try {
|
|
214
|
+
state = JSON.parse(raw);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
throw new Error("Shepherd mandates contain malformed JSON");
|
|
218
|
+
}
|
|
219
|
+
if (!validState(state))
|
|
220
|
+
throw new Error("Shepherd mandates contain an invalid or unsupported schema");
|
|
221
|
+
return state;
|
|
222
|
+
}
|
|
223
|
+
function requireCanonicalProject(project) {
|
|
224
|
+
try {
|
|
225
|
+
if (canonicalProjectShape(project) && fs.realpathSync.native(project) === project && fs.statSync(project).isDirectory())
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (!["ENOENT", "ENOTDIR", "ELOOP"].includes(error.code ?? ""))
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
throw new Error("Shepherd mandates require an exact existing canonical project directory");
|
|
233
|
+
}
|
|
234
|
+
function canonical(value) {
|
|
235
|
+
return JSON.stringify(value, (_key, item) => object(item)
|
|
236
|
+
? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item);
|
|
237
|
+
}
|
|
238
|
+
function persist(dir, state) {
|
|
239
|
+
const snapshot = JSON.stringify(state);
|
|
240
|
+
if (Buffer.byteLength(snapshot) > MAX_BYTES)
|
|
241
|
+
throw new Error("Shepherd mandates exceed 2 MiB; no change was written");
|
|
242
|
+
const temporary = path.join(dir, `mandates.${(0, node_crypto_1.randomUUID)()}.tmp`);
|
|
243
|
+
const handle = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
|
|
244
|
+
try {
|
|
245
|
+
fs.writeFileSync(handle, snapshot, "utf8");
|
|
246
|
+
fs.fsyncSync(handle);
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
fs.closeSync(handle);
|
|
250
|
+
}
|
|
251
|
+
fs.renameSync(temporary, path.join(dir, "mandates.json"));
|
|
252
|
+
const directoryHandle = fs.openSync(dir, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
|
|
253
|
+
try {
|
|
254
|
+
fs.fsyncSync(directoryHandle);
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
fs.closeSync(directoryHandle);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** Caller has already validated the local-family owner; this is not exposed as an agent/browser grant tool. */
|
|
261
|
+
async function updateShepherdMandate(agentRoot, request) {
|
|
262
|
+
if (!validRequest(request))
|
|
263
|
+
throw new Error("Invalid Shepherd mandate administrative request: requires canonical project, bounded task and expiry");
|
|
264
|
+
// Never hold a caller-owned mutable object across the asynchronous lock acquisition.
|
|
265
|
+
request = structuredClone(request);
|
|
266
|
+
const bodyHash = digest(canonical(request));
|
|
267
|
+
// Read before creating state so malformed existing authority is never repaired by a mutation.
|
|
268
|
+
const initial = readShepherdMandates(agentRoot);
|
|
269
|
+
const existingDir = storeDirectory(agentRoot);
|
|
270
|
+
const now = Date.now();
|
|
271
|
+
// A replay remains a replay after expiry; expiry is checked only for a new grant under the lock.
|
|
272
|
+
if (!existingDir && request.operation === "grant" && (Date.parse(request.expiresAt) <= now || Date.parse(request.expiresAt) - now > MAX_LIFETIME)) {
|
|
273
|
+
throw new Error("Shepherd mandate expiry must be in the future and within seven days");
|
|
274
|
+
}
|
|
275
|
+
if (!existingDir) {
|
|
276
|
+
if (request.operation === "revoke")
|
|
277
|
+
throw new Error("Shepherd mandate not found");
|
|
278
|
+
if (request.expectedRevision !== initial.revision)
|
|
279
|
+
throw new Error("Shepherd mandate revision conflict");
|
|
280
|
+
requireCanonicalProject(request.target.project);
|
|
281
|
+
}
|
|
282
|
+
const dir = storeDirectory(agentRoot, true);
|
|
283
|
+
return (0, file_lock_1.withFileLock)(dir, "mandates", () => {
|
|
284
|
+
const state = readShepherdMandates(agentRoot);
|
|
285
|
+
const prior = state.requests.find(item => item.actorId === request.context.actorId && item.requestId === request.context.requestId);
|
|
286
|
+
if (prior) {
|
|
287
|
+
if (prior.hash !== bodyHash)
|
|
288
|
+
throw new Error("Shepherd mandate request ID was already used with different arguments");
|
|
289
|
+
return prior.receipt;
|
|
290
|
+
}
|
|
291
|
+
if (state.revision !== request.expectedRevision)
|
|
292
|
+
throw new Error(`Shepherd mandate revision conflict: expected ${request.expectedRevision}, current ${state.revision}`);
|
|
293
|
+
const committedAt = Date.now();
|
|
294
|
+
const provenance = { ...request.context, at: new Date(committedAt).toISOString() };
|
|
295
|
+
const revision = state.revision + 1;
|
|
296
|
+
let id;
|
|
297
|
+
if (request.operation === "grant") {
|
|
298
|
+
if (Date.parse(request.expiresAt) <= committedAt || Date.parse(request.expiresAt) - committedAt > MAX_LIFETIME) {
|
|
299
|
+
throw new Error("Shepherd mandate expiry must be in the future and within seven days");
|
|
300
|
+
}
|
|
301
|
+
if (state.mandates.filter(item => item.status === "active" && Date.parse(item.expiresAt) > committedAt).length >= 100) {
|
|
302
|
+
throw new Error("Shepherd already has 100 active mandates; revoke one explicitly");
|
|
303
|
+
}
|
|
304
|
+
requireCanonicalProject(request.target.project);
|
|
305
|
+
id = mandateId(request.context.actorId, request.context.requestId);
|
|
306
|
+
state.mandates.push({
|
|
307
|
+
id, revision, target: { ...request.target }, task: request.task, action: "continue_text",
|
|
308
|
+
createdAt: provenance.at, expiresAt: request.expiresAt, status: "active", provenance, revoked: null,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
const mandate = state.mandates.find(item => item.id === request.mandateId);
|
|
313
|
+
if (!mandate)
|
|
314
|
+
throw new Error("Shepherd mandate not found");
|
|
315
|
+
if (mandate.status === "revoked")
|
|
316
|
+
throw new Error("Shepherd mandate is already revoked");
|
|
317
|
+
if (committedAt < Date.parse(mandate.createdAt))
|
|
318
|
+
throw new Error("Shepherd mandate clock moved before grant time; no change was written");
|
|
319
|
+
id = mandate.id;
|
|
320
|
+
mandate.status = "revoked";
|
|
321
|
+
mandate.revoked = provenance;
|
|
322
|
+
mandate.revision = revision;
|
|
323
|
+
}
|
|
324
|
+
const receipt = { revision, operation: request.operation, mandateId: id, provenance };
|
|
325
|
+
state.revision = revision;
|
|
326
|
+
state.requests.push({ actorId: request.context.actorId, requestId: request.context.requestId, hash: bodyHash, receipt });
|
|
327
|
+
persist(dir, state);
|
|
328
|
+
(0, runtime_1.emitNervesEvent)({ component: "heart", event: "heart.shepherd_mandate_mutated", message: "committed a trusted Shepherd mandate revision", meta: { operation: request.operation, revision } });
|
|
329
|
+
return receipt;
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
function resolveShepherdMandate(state, target, nowMs) {
|
|
333
|
+
if (!validState(state, nowMs) || !Number.isFinite(nowMs))
|
|
334
|
+
return { state: "withheld", reason: "invalid_mandate_state" };
|
|
335
|
+
if (!validTarget(target))
|
|
336
|
+
return { state: "withheld", reason: "invalid_project" };
|
|
337
|
+
try {
|
|
338
|
+
requireCanonicalProject(target.project);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
if (error.message !== "Shepherd mandates require an exact existing canonical project directory")
|
|
342
|
+
throw error;
|
|
343
|
+
return { state: "withheld", reason: "invalid_project" };
|
|
344
|
+
}
|
|
345
|
+
const matches = state.mandates.filter(mandate => mandate.status === "active"
|
|
346
|
+
&& Date.parse(mandate.createdAt) <= nowMs && Date.parse(mandate.expiresAt) > nowMs
|
|
347
|
+
&& mandate.target.project === target.project
|
|
348
|
+
&& ["host", "session", "instance"].every(key => mandate.target[key] === undefined || mandate.target[key] === target[key]));
|
|
349
|
+
if (matches.length !== 1)
|
|
350
|
+
return { state: "withheld", reason: matches.length ? "ambiguous_mandates" : "no_applicable_mandate" };
|
|
351
|
+
return { state: "authorized", mandate: matches[0] };
|
|
352
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.shepherdInspectionCommand = shepherdInspectionCommand;
|
|
4
|
+
exports.shepherdRetirementCommand = shepherdRetirementCommand;
|
|
5
|
+
const safeOwner = (agent) => /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(agent);
|
|
6
|
+
const claimId = (id) => /^[a-f0-9]{64}$/.test(id);
|
|
7
|
+
/** Display-only local commands; no browser action or observation is an authorization grant. */
|
|
8
|
+
function shepherdInspectionCommand(agent, id) {
|
|
9
|
+
if (!safeOwner(agent) || (id !== undefined && !claimId(id)))
|
|
10
|
+
return null;
|
|
11
|
+
return id === undefined
|
|
12
|
+
? `ouro shepherd status --agent '${agent}' --json`
|
|
13
|
+
: `ouro shepherd inspect --agent '${agent}' --id '${id}' --json`;
|
|
14
|
+
}
|
|
15
|
+
function shepherdRetirementCommand(agent, id, revision) {
|
|
16
|
+
if (!safeOwner(agent) || !claimId(id) || revision === null || !Number.isSafeInteger(revision) || revision < 0)
|
|
17
|
+
return null;
|
|
18
|
+
return `ouro shepherd reconcile --agent '${agent}' --id '${id}' --resolution retired --expected-revision ${revision} --request-id 'supervision-retire-${id}-r${revision}' --json`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
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.SHEPHERD_ADMIN_USAGE = void 0;
|
|
37
|
+
exports.parseShepherdOperation = parseShepherdOperation;
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
exports.SHEPHERD_ADMIN_USAGE = "Usage: ouro shepherd status|inspect|pause|resume|mandate grant|mandate revoke|reconcile|maintenance --agent <name> [operation options]; ouro shepherd chat --agent <name> [--session <UUID>] [--web]";
|
|
40
|
+
const nonempty = (value) => !!value && value === value.trim() && value.length <= 2000 && !/[\x00-\x1f\x7f-\x9f]/.test(value);
|
|
41
|
+
const absolute = (value) => path.isAbsolute(value) && path.normalize(value) === value;
|
|
42
|
+
function parseShepherdOperation(args) {
|
|
43
|
+
const fail = () => { throw new Error(exports.SHEPHERD_ADMIN_USAGE); };
|
|
44
|
+
let operation = args[0];
|
|
45
|
+
let offset = 1;
|
|
46
|
+
if (operation === "mandate") {
|
|
47
|
+
operation += `.${args[1]}`;
|
|
48
|
+
offset = 2;
|
|
49
|
+
}
|
|
50
|
+
const mutation = !["status", "inspect"].includes(operation ?? "");
|
|
51
|
+
const operationFlags = {
|
|
52
|
+
status: [], inspect: ["id", "cursor", "limit"],
|
|
53
|
+
pause: ["host", "session"], resume: ["host", "session"],
|
|
54
|
+
"mandate.grant": ["project", "task", "expires-at", "host", "session", "instance"],
|
|
55
|
+
"mandate.revoke": ["id"], reconcile: ["id", "resolution"], maintenance: ["destination"],
|
|
56
|
+
};
|
|
57
|
+
if (!operation || !Object.hasOwn(operationFlags, operation))
|
|
58
|
+
return fail();
|
|
59
|
+
const allowed = new Set(["agent", "json", ...operationFlags[operation], ...(mutation ? ["expected-revision", "request-id"] : [])]);
|
|
60
|
+
const values = new Map();
|
|
61
|
+
for (let i = offset; i < args.length; i++) {
|
|
62
|
+
const flag = args[i];
|
|
63
|
+
const key = flag.slice(2);
|
|
64
|
+
if (!flag.startsWith("--") || !allowed.has(key) || values.has(key))
|
|
65
|
+
return fail();
|
|
66
|
+
if (key === "json") {
|
|
67
|
+
values.set(key, "true");
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const value = args[++i];
|
|
71
|
+
if (!nonempty(value) || value.startsWith("--"))
|
|
72
|
+
return fail();
|
|
73
|
+
values.set(key, value);
|
|
74
|
+
}
|
|
75
|
+
const required = (key) => values.get(key) ?? fail();
|
|
76
|
+
const integer = (key, fallback) => {
|
|
77
|
+
const value = values.get(key);
|
|
78
|
+
if (value === undefined && fallback !== undefined)
|
|
79
|
+
return fallback;
|
|
80
|
+
if (!value || !/^(0|[1-9]\d*)$/.test(value) || !Number.isSafeInteger(Number(value)))
|
|
81
|
+
return fail();
|
|
82
|
+
return Number(value);
|
|
83
|
+
};
|
|
84
|
+
const agent = required("agent");
|
|
85
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(agent))
|
|
86
|
+
return fail();
|
|
87
|
+
const common = { agent, ...(values.has("json") ? { json: true } : {}) };
|
|
88
|
+
const host = () => {
|
|
89
|
+
const value = required("host");
|
|
90
|
+
return value === "cmux" || value === "herdr" ? value : fail();
|
|
91
|
+
};
|
|
92
|
+
if (operation === "status")
|
|
93
|
+
return { kind: "shepherd.status", ...common };
|
|
94
|
+
const id = () => {
|
|
95
|
+
const value = required("id");
|
|
96
|
+
if (!(operation === "mandate.revoke" ? /^mandate:[a-f0-9]{64}$|^[a-f0-9]{64}$/ : /^[a-f0-9]{64}$/).test(value))
|
|
97
|
+
return fail();
|
|
98
|
+
return value;
|
|
99
|
+
};
|
|
100
|
+
if (operation === "inspect") {
|
|
101
|
+
const limit = integer("limit", 50);
|
|
102
|
+
if (limit < 1 || limit > 200 || (values.has("id") && values.has("cursor")))
|
|
103
|
+
return fail();
|
|
104
|
+
return { kind: "shepherd.inspect", ...common, limit, ...(values.has("id") ? { id: id() } : {}), ...(values.has("cursor") ? { cursor: required("cursor") } : {}) };
|
|
105
|
+
}
|
|
106
|
+
const admin = { ...common, expectedRevision: integer("expected-revision"), requestId: required("request-id") };
|
|
107
|
+
if (admin.requestId.length > 200)
|
|
108
|
+
return fail();
|
|
109
|
+
if (operation === "pause" || operation === "resume")
|
|
110
|
+
return { kind: `shepherd.${operation}`, ...admin, host: host(), session: required("session") };
|
|
111
|
+
if (operation === "mandate.revoke")
|
|
112
|
+
return { kind: "shepherd.mandate.revoke", ...admin, id: id() };
|
|
113
|
+
if (operation === "reconcile") {
|
|
114
|
+
const resolution = required("resolution");
|
|
115
|
+
if (resolution !== "delivered" && resolution !== "retired")
|
|
116
|
+
return fail();
|
|
117
|
+
return { kind: "shepherd.reconcile", ...admin, id: id(), resolution };
|
|
118
|
+
}
|
|
119
|
+
if (operation === "maintenance") {
|
|
120
|
+
const destination = required("destination");
|
|
121
|
+
if (!absolute(destination))
|
|
122
|
+
return fail();
|
|
123
|
+
return { kind: "shepherd.maintenance", ...admin, destination };
|
|
124
|
+
}
|
|
125
|
+
const project = required("project");
|
|
126
|
+
const expiresAt = required("expires-at");
|
|
127
|
+
if (!absolute(project) || !Number.isFinite(Date.parse(expiresAt)) || new Date(expiresAt).toISOString() !== expiresAt
|
|
128
|
+
|| (values.has("session") && !values.has("host")) || (values.has("instance") && !values.has("session")))
|
|
129
|
+
return fail();
|
|
130
|
+
return {
|
|
131
|
+
kind: "shepherd.mandate.grant", ...admin, project, task: required("task"), expiresAt,
|
|
132
|
+
...(values.has("host") ? { host: host() } : {}), ...(values.has("session") ? { session: required("session") } : {}),
|
|
133
|
+
...(values.has("instance") ? { instance: required("instance") } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SHEPHERD_SUPERVISION_SOURCES = exports.SHEPHERD_ALPHA_AUTOMATION_WARNING = exports.SHEPHERD_OBSERVATION_MAX_BYTES = exports.SHEPHERD_OBSERVATION_FILE = exports.SHEPHERD_SUPERVISION_STALE_MS = exports.SHEPHERD_SUPERVISION_LIMIT = void 0;
|
|
4
|
+
exports.buildShepherdSupervisionView = buildShepherdSupervisionView;
|
|
5
|
+
exports.isShepherdSupervisionSnapshot = isShepherdSupervisionSnapshot;
|
|
6
|
+
exports.SHEPHERD_SUPERVISION_LIMIT = 200;
|
|
7
|
+
exports.SHEPHERD_SUPERVISION_STALE_MS = 30_000;
|
|
8
|
+
exports.SHEPHERD_OBSERVATION_FILE = "supervision-observation.json";
|
|
9
|
+
exports.SHEPHERD_OBSERVATION_MAX_BYTES = 2 * 1024 * 1024;
|
|
10
|
+
exports.SHEPHERD_ALPHA_AUTOMATION_WARNING = "This alpha retains unqualified legacy automation. Legacy immediate/network automation may still type text without mandate enforcement. Automatic keys are refused. Absent or revoked mandates do not disable those text paths. Disable Shepherd to stop legacy automation.";
|
|
11
|
+
exports.SHEPHERD_SUPERVISION_SOURCES = ["host", "guidance", "mandates", "returns", "custody", "notices"];
|
|
12
|
+
function freshness(at, nowMs) {
|
|
13
|
+
const ageMs = at === null ? NaN : nowMs - Date.parse(at);
|
|
14
|
+
return !Number.isFinite(ageMs) || ageMs < 0
|
|
15
|
+
? { status: "unknown", observedAt: at, ageMs: null }
|
|
16
|
+
: { status: ageMs > exports.SHEPHERD_SUPERVISION_STALE_MS ? "stale" : "fresh", observedAt: at, ageMs };
|
|
17
|
+
}
|
|
18
|
+
/** Observation is not authority; aging may only weaken current claims, never promote them. */
|
|
19
|
+
function buildShepherdSupervisionView(snapshot, nowMs) {
|
|
20
|
+
const current = freshness(snapshot.observedAt, nowMs);
|
|
21
|
+
const hostAvailable = snapshot.sourceHealth.host === "ok" && current.status !== "unknown";
|
|
22
|
+
const sessions = snapshot.sessions.items.map(row => {
|
|
23
|
+
const observed = freshness(row.observedAt, nowMs);
|
|
24
|
+
const fresh = hostAvailable && current.status === "fresh" && observed.status === "fresh";
|
|
25
|
+
return {
|
|
26
|
+
...row,
|
|
27
|
+
freshness: observed,
|
|
28
|
+
activity: fresh ? row.activity : "unknown",
|
|
29
|
+
authority: fresh && snapshot.sourceHealth.mandates === "ok" && snapshot.sourceHealth.guidance === "ok"
|
|
30
|
+
? row.authority : "unknown",
|
|
31
|
+
capability: fresh ? row.capability : "unknown",
|
|
32
|
+
lastDecisionLabel: row.lastDecision === "done" ? "Agent reports finished" : row.lastDecision,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
const counts = hostAvailable ? { working: 0, idle: 0, shell: 0, unknown: 0 } : null;
|
|
36
|
+
if (counts)
|
|
37
|
+
for (const row of sessions)
|
|
38
|
+
counts[row.activity] += 1;
|
|
39
|
+
return {
|
|
40
|
+
schemaVersion: 1,
|
|
41
|
+
agentName: snapshot.agentName,
|
|
42
|
+
freshness: current,
|
|
43
|
+
health: snapshot.sourceErrors.length || Object.values(snapshot.sourceHealth).some(value => value !== "ok") ? "degraded" : "ok",
|
|
44
|
+
sourceHealth: { ...snapshot.sourceHealth },
|
|
45
|
+
counts,
|
|
46
|
+
sessions: { ...snapshot.sessions, items: sessions },
|
|
47
|
+
timeline: { ...snapshot.timeline, items: snapshot.timeline.items.map(event => ({
|
|
48
|
+
...event, summary: event.kind === "judgment" && event.summary === "done" ? "Agent reports finished" : event.summary,
|
|
49
|
+
})) },
|
|
50
|
+
custody: { ...snapshot.custody, claims: { ...snapshot.custody.claims, items: snapshot.custody.claims.items.map(claim => ({ ...claim })) } },
|
|
51
|
+
sourceErrors: snapshot.sourceErrors.map(error => ({ ...error })),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
55
|
+
const exact = (value, keys) => Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
56
|
+
const text = (value) => typeof value === "string" && value.length > 0 && value.length <= 2000 && !/[\x00-\x1f\x7f-\x9f\u2028\u2029]/.test(value);
|
|
57
|
+
const nullableText = (value) => value === null || text(value);
|
|
58
|
+
const date = (value) => typeof value === "string" && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
59
|
+
const oneOf = (value, values) => typeof value === "string" && values.includes(value);
|
|
60
|
+
const count = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
61
|
+
function validPage(value, validItem) {
|
|
62
|
+
return object(value) && exact(value, ["totalCount", "truncated", "items"])
|
|
63
|
+
&& (value.totalCount === null || count(value.totalCount)) && typeof value.truncated === "boolean"
|
|
64
|
+
&& Array.isArray(value.items) && value.items.length <= exports.SHEPHERD_SUPERVISION_LIMIT && value.items.every(validItem)
|
|
65
|
+
&& (value.totalCount === null ? value.truncated : value.totalCount >= value.items.length
|
|
66
|
+
&& value.truncated === (value.totalCount > value.items.length));
|
|
67
|
+
}
|
|
68
|
+
function validSession(value) {
|
|
69
|
+
return object(value) && exact(value, ["host", "session", "instance", "label", "harness", "project", "activity",
|
|
70
|
+
"observedAt", "lastDecision", "lastAction", "lastReason", "lastActionAt", "withheldReason", "authority", "capability"])
|
|
71
|
+
&& text(value.host) && text(value.session) && nullableText(value.instance) && text(value.label)
|
|
72
|
+
&& nullableText(value.harness) && nullableText(value.project) && nullableText(value.lastDecision) && nullableText(value.withheldReason)
|
|
73
|
+
&& (value.lastAction === null || oneOf(value.lastAction, ["respond", "let_through"])) && nullableText(value.lastReason)
|
|
74
|
+
&& (value.lastActionAt === null || date(value.lastActionAt))
|
|
75
|
+
&& (value.observedAt === null || date(value.observedAt)) && oneOf(value.activity, ["working", "idle", "shell", "unknown"])
|
|
76
|
+
&& oneOf(value.authority, ["active", "absent", "paused", "ambiguous", "unknown"])
|
|
77
|
+
&& oneOf(value.capability, ["supported", "unsupported", "unknown"]);
|
|
78
|
+
}
|
|
79
|
+
function validEvent(value) {
|
|
80
|
+
return object(value) && exact(value, ["id", "host", "session", "instance", "at", "kind", "summary"])
|
|
81
|
+
&& text(value.id) && text(value.host) && text(value.session) && nullableText(value.instance) && date(value.at) && text(value.summary)
|
|
82
|
+
&& oneOf(value.kind, ["observation", "judgment", "input_admitted", "legacy_input_reported", "input_unknown", "notification_queued",
|
|
83
|
+
"notification_delivered", "notification_unknown", "recovery"]);
|
|
84
|
+
}
|
|
85
|
+
function validClaim(value) {
|
|
86
|
+
return object(value) && exact(value, ["id", "host", "session", "instance", "state", "claimedAt", "reason", "retired"])
|
|
87
|
+
&& typeof value.id === "string" && /^[a-f0-9]{64}$/.test(value.id)
|
|
88
|
+
&& text(value.host) && text(value.session) && nullableText(value.instance) && date(value.claimedAt)
|
|
89
|
+
&& nullableText(value.reason) && typeof value.retired === "boolean" && oneOf(value.state, ["claimed", "sent", "not_sent", "unknown"]);
|
|
90
|
+
}
|
|
91
|
+
function isShepherdSupervisionSnapshot(value) {
|
|
92
|
+
if (!object(value) || !exact(value, ["schemaVersion", "agentName", "observedAt", "sourceHealth", "sessions", "timeline", "custody", "sourceErrors"])
|
|
93
|
+
|| value.schemaVersion !== 1 || typeof value.agentName !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(value.agentName)
|
|
94
|
+
|| !date(value.observedAt) || !object(value.sourceHealth)
|
|
95
|
+
|| !exact(value.sourceHealth, [...exports.SHEPHERD_SUPERVISION_SOURCES])
|
|
96
|
+
|| !Object.values(value.sourceHealth).every(health => oneOf(health, ["ok", "missing", "error"]))
|
|
97
|
+
|| !validPage(value.sessions, validSession) || !validPage(value.timeline, validEvent)
|
|
98
|
+
|| !object(value.custody) || !exact(value.custody, ["revision", "bytes", "capacity", "claims"])
|
|
99
|
+
|| !(value.custody.revision === null || count(value.custody.revision))
|
|
100
|
+
|| !(value.custody.bytes === null || count(value.custody.bytes))
|
|
101
|
+
|| !oneOf(value.custody.capacity, ["normal", "warning", "refused", "unknown"])
|
|
102
|
+
|| !validPage(value.custody.claims, validClaim)
|
|
103
|
+
|| !Array.isArray(value.sourceErrors) || value.sourceErrors.length > 50
|
|
104
|
+
|| !value.sourceErrors.every(error => object(error) && exact(error, ["source", "code", "detail"])
|
|
105
|
+
&& oneOf(error.source, [...exports.SHEPHERD_SUPERVISION_SOURCES, "replica"]) && text(error.code) && text(error.detail)))
|
|
106
|
+
return false;
|
|
107
|
+
const sessions = value.sessions;
|
|
108
|
+
const timeline = value.timeline;
|
|
109
|
+
const observedAt = value.observedAt;
|
|
110
|
+
return new Set(sessions.items.map(row => JSON.stringify([row.host, row.session]))).size === sessions.items.length
|
|
111
|
+
&& new Set(timeline.items.map(event => event.id)).size === timeline.items.length
|
|
112
|
+
&& new Set(value.custody.claims.items.map(claim => claim.id)).size === value.custody.claims.items.length
|
|
113
|
+
&& sessions.items.every(row => row.observedAt === null || Date.parse(row.observedAt) <= Date.parse(observedAt));
|
|
114
|
+
}
|