@ouro.bot/cli 0.1.0-alpha.815 → 0.1.0-alpha.817

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 (63) hide show
  1. package/assets/sanctuary-host-launcher.sh +16 -0
  2. package/changelog.json +18 -0
  3. package/deploy/unraid/Dockerfile +6 -0
  4. package/deploy/unraid/README.txt +305 -365
  5. package/deploy/unraid/docker-man-template-transaction.mjs +192 -7
  6. package/deploy/unraid/sanctuary-acceptance-adapter.sh +1 -1
  7. package/deploy/unraid/sanctuary-acceptance-contract.json +7 -7
  8. package/deploy/unraid/sanctuary-authority-installation.json +36 -0
  9. package/deploy/unraid/sanctuary-authority-service.sh +30 -0
  10. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +62 -9
  11. package/deploy/unraid/sanctuary-unit16-run.sh +15 -15
  12. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
  13. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +2 -2
  14. package/deploy/unraid/sanctuary.xml +2 -1
  15. package/dist/heart/approval-store.js +11 -1
  16. package/dist/heart/core.js +2 -1
  17. package/dist/heart/daemon/container-spec-auditor-main.js +9 -8
  18. package/dist/heart/daemon/container-spec-auditor.js +17 -24
  19. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +95 -91
  20. package/dist/heart/daemon/sanctuary-acceptance-harness.js +95 -169
  21. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +4 -5
  22. package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
  23. package/dist/heart/daemon/sanctuary-authority-epoch.js +256 -0
  24. package/dist/heart/daemon/sanctuary-authority-installation.js +140 -0
  25. package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
  26. package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +780 -0
  27. package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
  28. package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
  29. package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +494 -0
  30. package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
  31. package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
  32. package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
  33. package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
  34. package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +445 -0
  35. package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
  36. package/dist/heart/daemon/sanctuary-telegram-authority-service.js +615 -0
  37. package/dist/heart/daemon/sense-manager.js +20 -10
  38. package/dist/heart/external-events/router.js +31 -15
  39. package/dist/heart/steward-policy.js +374 -58
  40. package/dist/heart/tool-approval.js +8 -1
  41. package/dist/repertoire/relationship-authorization.js +128 -0
  42. package/dist/repertoire/tools-base.js +9 -13
  43. package/dist/repertoire/tools-sanctuary-host.js +202 -0
  44. package/dist/repertoire/tools-steward-policy.js +34 -10
  45. package/dist/repertoire/tools-unraid.js +79 -32
  46. package/dist/repertoire/tools.js +37 -25
  47. package/dist/repertoire/unraid-restart.js +179 -52
  48. package/dist/senses/private-runtime.js +27 -12
  49. package/dist/senses/root-host-approval-port.js +345 -0
  50. package/dist/senses/root-host-approval-runtime.js +393 -0
  51. package/dist/senses/sanctuary-authority-resident.js +102 -0
  52. package/dist/senses/sanctuary-health-runner.js +0 -1
  53. package/dist/senses/sanctuary-media-catalog-contract.js +4 -1
  54. package/dist/senses/sanctuary-runtime.js +2 -0
  55. package/dist/senses/telegram-admission.js +7 -0
  56. package/dist/senses/telegram-approval-runtime.js +130 -12
  57. package/dist/senses/telegram-attachments.js +5 -1
  58. package/dist/senses/telegram-authority-transport.js +231 -0
  59. package/dist/senses/telegram-client.js +31 -8
  60. package/dist/senses/telegram-entry.js +1 -1
  61. package/dist/senses/telegram.js +174 -28
  62. package/npm-shrinkwrap.json +2 -2
  63. package/package.json +1 -1
@@ -38,14 +38,119 @@ exports.updateStewardPolicy = updateStewardPolicy;
38
38
  exports.readRoutineActionReceipts = readRoutineActionReceipts;
39
39
  exports.inspectRoutineActionGrant = inspectRoutineActionGrant;
40
40
  exports.consumeRoutineActionGrant = consumeRoutineActionGrant;
41
+ exports.withStewardPolicyLease = withStewardPolicyLease;
42
+ exports.withRoutineActionAttempt = withRoutineActionAttempt;
41
43
  exports.transitionRoutineActionReceipt = transitionRoutineActionReceipt;
42
44
  exports.recoverRoutineActionReceipts = recoverRoutineActionReceipts;
43
45
  const fs = __importStar(require("node:fs"));
44
46
  const path = __importStar(require("node:path"));
45
47
  const node_crypto_1 = require("node:crypto");
48
+ const node_util_1 = require("node:util");
46
49
  const runtime_1 = require("../nerves/runtime");
47
50
  const session_transaction_1 = require("../mind/session-transaction");
48
51
  const EMPTY_POLICY = { schemaVersion: 1, version: 0, desiredStates: {}, routineActionGrants: {}, updatedAt: null };
52
+ const MAX_POLICY_BYTES = 1024 * 1024;
53
+ const MAX_AUDIT_ROW_BYTES = 16 * MAX_POLICY_BYTES;
54
+ function sha256(bytes) {
55
+ return (0, node_crypto_1.createHash)("sha256").update(bytes, "utf8").digest("hex");
56
+ }
57
+ function record(value) {
58
+ return value !== null && typeof value === "object" && !Array.isArray(value);
59
+ }
60
+ function exactKeys(value, required, optional = []) {
61
+ return required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
62
+ }
63
+ function text(value) {
64
+ return typeof value === "string" && value.length > 0 && value.trim() === value;
65
+ }
66
+ function canonicalTime(value) {
67
+ return typeof value === "string" && Number.isFinite(Date.parse(value)) && new Date(Date.parse(value)).toISOString() === value;
68
+ }
69
+ function positiveInteger(value) {
70
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
71
+ }
72
+ function textArray(value) {
73
+ return Array.isArray(value) && value.every(text) && new Set(value).size === value.length;
74
+ }
75
+ function ownerAuthorization(value) {
76
+ return record(value) && exactKeys(value, ["profileId", "profileVersion", "requestId", "sessionKey", "receiptId"])
77
+ && value.profileId === "sanctuary-owner" && positiveInteger(value.profileVersion)
78
+ && text(value.requestId) && text(value.sessionKey) && text(value.receiptId);
79
+ }
80
+ function desiredEntry(value, version) {
81
+ return record(value) && exactKeys(value, ["value", "provenance", "version", "source"], ["expiresAt"])
82
+ && text(value.value) && text(value.source) && positiveInteger(value.version) && value.version <= version
83
+ && (value.provenance === "stated" || value.provenance === "observed" || value.provenance === "default")
84
+ && (value.expiresAt === undefined || canonicalTime(value.expiresAt));
85
+ }
86
+ function grantEntry(value, version) {
87
+ return record(value) && exactKeys(value, ["action", "targets", "maxCount", "windowMs", "verificationRequired", "exclusions", "provenance", "issuer", "authorizedAt", "authorizingSessionEvent", "version"], ["expiresAt"])
88
+ && text(value.action) && textArray(value.targets) && value.targets.length > 0 && textArray(value.exclusions)
89
+ && positiveInteger(value.maxCount) && positiveInteger(value.windowMs) && value.verificationRequired === true
90
+ && (value.provenance === "stated" || value.provenance === "installed_explicit_policy")
91
+ && text(value.issuer) && text(value.authorizingSessionEvent) && canonicalTime(value.authorizedAt)
92
+ && positiveInteger(value.version) && value.version <= version && (value.expiresAt === undefined || canonicalTime(value.expiresAt));
93
+ }
94
+ function policyRecord(value) {
95
+ if (!record(value) || !exactKeys(value, ["schemaVersion", "version", "desiredStates", "routineActionGrants", "updatedAt"])
96
+ || value.schemaVersion !== 1 || typeof value.version !== "number" || !Number.isSafeInteger(value.version) || value.version < 0
97
+ || !record(value.desiredStates) || !record(value.routineActionGrants)
98
+ || !(value.updatedAt === null ? value.version === 0 : canonicalTime(value.updatedAt)))
99
+ return false;
100
+ const version = value.version;
101
+ return Object.entries(value.desiredStates).every(([key, entry]) => text(key) && desiredEntry(entry, version))
102
+ && Object.entries(value.routineActionGrants).every(([key, entry]) => text(key) && grantEntry(entry, version));
103
+ }
104
+ function policyImage(bytes) {
105
+ if (Buffer.byteLength(bytes) > MAX_POLICY_BYTES)
106
+ throw new Error("steward policy audit image exceeds its bound");
107
+ if (bytes === "")
108
+ return structuredClone(EMPTY_POLICY);
109
+ let value;
110
+ try {
111
+ value = JSON.parse(bytes);
112
+ }
113
+ catch (cause) {
114
+ throw new Error("steward policy audit image is invalid", { cause });
115
+ }
116
+ if (!policyRecord(value))
117
+ throw new Error("steward policy audit image is invalid");
118
+ return value;
119
+ }
120
+ function mutationFingerprint(kind, key, result) {
121
+ const generated = ["version", "issuer", "authorizedAt", "authorizingSessionEvent"];
122
+ const input = Object.fromEntries(Object.entries(result).filter(([field]) => !generated.includes(field)).sort(([left], [right]) => left.localeCompare(right)));
123
+ return sha256(JSON.stringify([kind, key, input]));
124
+ }
125
+ function operationIdentity(issuer, event, requestId, kind, key) {
126
+ return JSON.stringify([issuer, event, requestId, kind, key]);
127
+ }
128
+ function auditRow(value) {
129
+ if (!record(value) || !exactKeys(value, ["schemaVersion", "transactionId", "precedingBytesSha256", "mutationKind", "key", "mutationFingerprint", "affectedKeyResult", "affectedKeyResultSha256", "issuer", "authorizingSessionEvent", "authorization", "preimage", "preimageVersion", "preimageSha256", "postimage", "postimageVersion", "postimageSha256", "at"])
130
+ || value.schemaVersion !== 2 || (value.mutationKind !== "set_desired_state" && value.mutationKind !== "grant_routine_action")
131
+ || !text(value.key) || !text(value.issuer) || !text(value.authorizingSessionEvent) || !ownerAuthorization(value.authorization)
132
+ || typeof value.preimage !== "string" || typeof value.postimage !== "string" || !canonicalTime(value.at))
133
+ return false;
134
+ const before = policyImage(value.preimage);
135
+ const after = policyImage(value.postimage);
136
+ const result = value.mutationKind === "set_desired_state" ? after.desiredStates[value.key] : after.routineActionGrants[value.key];
137
+ if (!result || value.preimageVersion !== before.version || value.postimageVersion !== after.version
138
+ || after.version !== before.version + 1 || result.version !== after.version || after.updatedAt !== value.at
139
+ || value.preimageSha256 !== sha256(value.preimage) || value.postimageSha256 !== sha256(value.postimage)
140
+ || value.postimage !== JSON.stringify(after, null, 2) || !(0, node_util_1.isDeepStrictEqual)(value.affectedKeyResult, result)
141
+ || value.affectedKeyResultSha256 !== sha256(JSON.stringify(result))
142
+ || value.mutationFingerprint !== mutationFingerprint(value.mutationKind, value.key, result)
143
+ || value.transactionId !== sha256(JSON.stringify([operationIdentity(value.issuer, value.authorizingSessionEvent, value.authorization.requestId, value.mutationKind, value.key), value.mutationFingerprint])))
144
+ return false;
145
+ if ("action" in result && (result.issuer !== value.issuer || result.authorizingSessionEvent !== value.authorizingSessionEvent || result.authorizedAt !== value.at))
146
+ return false;
147
+ const expected = {
148
+ ...before, version: after.version, updatedAt: value.at,
149
+ desiredStates: value.mutationKind === "set_desired_state" ? { ...before.desiredStates, [value.key]: result } : before.desiredStates,
150
+ routineActionGrants: value.mutationKind === "grant_routine_action" ? { ...before.routineActionGrants, [value.key]: result } : before.routineActionGrants,
151
+ };
152
+ return (0, node_util_1.isDeepStrictEqual)(after, expected);
153
+ }
49
154
  function policyDir(agentRoot) {
50
155
  return path.join(agentRoot, "state", "policy");
51
156
  }
@@ -62,6 +167,15 @@ function ensureDirectory(agentRoot) {
62
167
  fs.mkdirSync(policyDir(agentRoot), { recursive: true, mode: 0o700 });
63
168
  fs.chmodSync(policyDir(agentRoot), 0o700);
64
169
  }
170
+ function syncParentDirectory(filePath) {
171
+ const directory = fs.openSync(path.dirname(filePath), fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
172
+ try {
173
+ fs.fsyncSync(directory);
174
+ }
175
+ finally {
176
+ fs.closeSync(directory);
177
+ }
178
+ }
65
179
  function appendReceipt(filePath, value) {
66
180
  const creating = !fs.existsSync(filePath);
67
181
  const fd = fs.openSync(filePath, "a", 0o600);
@@ -73,18 +187,11 @@ function appendReceipt(filePath, value) {
73
187
  fs.closeSync(fd);
74
188
  }
75
189
  fs.chmodSync(filePath, 0o600);
76
- if (creating) {
77
- const directory = fs.openSync(path.dirname(filePath), fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
78
- try {
79
- fs.fsyncSync(directory);
80
- }
81
- finally {
82
- fs.closeSync(directory);
83
- }
84
- }
190
+ if (creating)
191
+ syncParentDirectory(filePath);
85
192
  }
86
193
  function requireText(value, label) {
87
- const result = value.trim();
194
+ const result = typeof value === "string" ? value.trim() : "";
88
195
  if (!result)
89
196
  throw new Error(`${label} must be nonempty`);
90
197
  return result;
@@ -93,67 +200,198 @@ function optionalExpiry(value, now) {
93
200
  if (!value)
94
201
  return undefined;
95
202
  const epoch = Date.parse(value);
96
- if (!Number.isFinite(epoch) || new Date(epoch).toISOString() !== value || epoch <= Date.parse(now))
203
+ if (!Number.isFinite(epoch) || new Date(epoch).toISOString() !== value || (now !== undefined && epoch <= Date.parse(now)))
97
204
  throw new Error("policy expiry must be a future canonical timestamp");
98
205
  return value;
99
206
  }
100
207
  function validateStewardPolicy(value) {
101
- if (!value || typeof value !== "object" || Array.isArray(value))
102
- throw new Error("steward policy is invalid");
103
- const candidate = value;
104
- if (candidate.schemaVersion !== 1 || !Number.isInteger(candidate.version) || candidate.version < 0 || typeof candidate.desiredStates !== "object" || !candidate.desiredStates || typeof candidate.routineActionGrants !== "object" || !candidate.routineActionGrants) {
208
+ if (!policyRecord(value))
105
209
  throw new Error("steward policy is invalid");
210
+ return value;
211
+ }
212
+ function readAuditBytes(agentRoot) {
213
+ try {
214
+ const bytes = fs.readFileSync(auditPath(agentRoot));
215
+ const decoded = bytes.toString("utf8");
216
+ if (!Buffer.from(decoded, "utf8").equals(bytes))
217
+ throw new Error("steward policy audit encoding is invalid");
218
+ return decoded;
219
+ }
220
+ catch (error) {
221
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
222
+ return "";
223
+ throw error;
106
224
  }
107
- return candidate;
108
225
  }
109
- function readStewardPolicy(agentRoot) {
226
+ function readPolicyTransaction(filePath, lease) {
227
+ const exists = fs.existsSync(filePath);
228
+ if (exists && fs.statSync(filePath).size > MAX_POLICY_BYTES)
229
+ throw new Error("steward policy exceeds its bound");
230
+ const snapshot = (0, session_transaction_1.readSessionTransaction)(filePath, lease);
231
+ const raw = exists ? fs.readFileSync(filePath) : Buffer.alloc(0);
232
+ if (!Buffer.from(snapshot.bytes, "utf8").equals(raw))
233
+ throw new Error("steward policy encoding or bytes changed");
234
+ return { ...snapshot, exists };
235
+ }
236
+ function readAuditedPolicy(agentRoot, lease) {
110
237
  const filePath = policyPath(agentRoot);
111
- if (!fs.existsSync(filePath))
112
- return structuredClone(EMPTY_POLICY);
113
- return validateStewardPolicy(JSON.parse(fs.readFileSync(filePath, "utf8")));
238
+ let snapshot = readPolicyTransaction(filePath, lease);
239
+ let policy = !snapshot.exists && snapshot.bytes === "" ? structuredClone(EMPTY_POLICY) : validateStewardPolicy(snapshot.value);
240
+ const auditBytes = readAuditBytes(agentRoot);
241
+ const rows = [];
242
+ const seen = new Set();
243
+ const preceding = (0, node_crypto_1.createHash)("sha256");
244
+ let legacyVersion = 0;
245
+ if (auditBytes && !auditBytes.endsWith("\n"))
246
+ throw new Error("steward policy audit has a partial final row");
247
+ for (const line of auditBytes ? auditBytes.slice(0, -1).split("\n") : []) {
248
+ if (Buffer.byteLength(line) > MAX_AUDIT_ROW_BYTES)
249
+ throw new Error("steward policy audit row exceeds its bound");
250
+ let row;
251
+ try {
252
+ row = JSON.parse(line);
253
+ }
254
+ catch (cause) {
255
+ throw new Error("steward policy audit row is invalid", { cause });
256
+ }
257
+ if (record(row) && row.schemaVersion === 1) {
258
+ if (rows.length || !positiveInteger(row.policyVersion) || row.policyVersion <= legacyVersion)
259
+ throw new Error("steward policy audit legacy prefix is invalid");
260
+ legacyVersion = row.policyVersion;
261
+ }
262
+ else {
263
+ if (!auditRow(row) || row.precedingBytesSha256 !== preceding.copy().digest("hex"))
264
+ throw new Error("steward policy audit row is invalid");
265
+ const prior = rows.at(-1);
266
+ if (prior ? row.preimage !== prior.postimage || row.preimageVersion !== prior.postimageVersion : row.preimageVersion !== legacyVersion)
267
+ throw new Error("steward policy audit chain is invalid");
268
+ const identity = operationIdentity(row.issuer, row.authorizingSessionEvent, row.authorization.requestId, row.mutationKind, row.key);
269
+ if (seen.has(identity))
270
+ throw new Error("steward policy audit contains a duplicate transaction");
271
+ seen.add(identity);
272
+ rows.push(row);
273
+ }
274
+ preceding.update(`${line}\n`, "utf8");
275
+ }
276
+ const last = rows.at(-1);
277
+ if (!last) {
278
+ if (policy.version !== legacyVersion)
279
+ throw new Error("steward policy audit does not match the policy head");
280
+ }
281
+ else if (snapshot.bytes !== last.postimage) {
282
+ if (snapshot.bytes !== last.preimage)
283
+ throw new Error("steward policy audit does not match the policy head");
284
+ const fd = fs.openSync(auditPath(agentRoot), "r");
285
+ try {
286
+ fs.fsyncSync(fd);
287
+ }
288
+ finally {
289
+ fs.closeSync(fd);
290
+ }
291
+ syncParentDirectory(auditPath(agentRoot));
292
+ (0, session_transaction_1.writeSessionTransaction)(filePath, policyImage(last.postimage), { lease, expectedRevision: snapshot.revision });
293
+ snapshot = readPolicyTransaction(filePath, lease);
294
+ if (snapshot.bytes !== last.postimage)
295
+ throw new Error("steward policy audit recovery readback differs");
296
+ policy = validateStewardPolicy(snapshot.value);
297
+ (0, runtime_1.emitNervesEvent)({ component: "heart", event: "heart.steward_policy_recovered", message: "recovered an authorized steward policy write", meta: { version: policy.version } });
298
+ }
299
+ return { ...snapshot, policy, rows, auditBytes };
300
+ }
301
+ function readPolicyState(agentRoot) {
302
+ if (!fs.existsSync(policyPath(agentRoot)) && !fs.existsSync(auditPath(agentRoot))) {
303
+ return { policy: structuredClone(EMPTY_POLICY), rows: [] };
304
+ }
305
+ return (0, session_transaction_1.withImmediateSessionTurnLease)(policyPath(agentRoot), (lease) => readAuditedPolicy(agentRoot, lease));
306
+ }
307
+ function readStewardPolicy(agentRoot) {
308
+ return readPolicyState(agentRoot).policy;
114
309
  }
115
310
  function updateStewardPolicy(agentRoot, input) {
116
311
  if (input.actor.trustLevel !== "family")
117
312
  throw new Error("steward policy mutation requires family authority");
118
313
  const authorizingSessionEvent = requireText(input.actor.sessionEventId, "authorizing session event");
119
314
  const issuer = requireText(input.actor.friendId, "issuer");
315
+ const authorization = input.actor.authorization;
316
+ if (!ownerAuthorization(authorization))
317
+ throw new Error("steward policy mutation requires current owner authorization");
318
+ if (input.mutation.kind !== "set_desired_state" && input.mutation.kind !== "grant_routine_action")
319
+ throw new Error("steward policy mutation kind is invalid");
120
320
  return (0, session_transaction_1.withImmediateSessionTurnLease)(policyPath(agentRoot), (lease) => {
121
- const snapshot = (0, session_transaction_1.readSessionTransaction)(policyPath(agentRoot), lease);
122
- const current = snapshot.value === null ? structuredClone(EMPTY_POLICY) : validateStewardPolicy(snapshot.value);
123
- if (current.version !== input.expectedVersion)
124
- throw new Error(`steward policy version changed: expected ${input.expectedVersion}, got ${current.version}`);
321
+ const snapshot = readAuditedPolicy(agentRoot, lease);
322
+ const current = snapshot.policy;
125
323
  const now = input.now ?? new Date().toISOString();
126
- const expiresAt = optionalExpiry(input.mutation.expiresAt, now);
324
+ if (!canonicalTime(now))
325
+ throw new Error("policy update time must be canonical");
326
+ const expiresAt = optionalExpiry(input.mutation.expiresAt);
127
327
  const version = current.version + 1;
328
+ if (!positiveInteger(version))
329
+ throw new Error("steward policy version is exhausted");
128
330
  const next = { ...current, version, desiredStates: { ...current.desiredStates }, routineActionGrants: { ...current.routineActionGrants }, updatedAt: now };
331
+ const key = requireText(input.mutation.key, input.mutation.kind === "set_desired_state" ? "desired state key" : "routine action key");
332
+ let affectedKeyResult;
129
333
  if (input.mutation.kind === "set_desired_state") {
130
- next.desiredStates[requireText(input.mutation.key, "desired state key")] = {
334
+ if (input.mutation.provenance !== "stated" && input.mutation.provenance !== "observed" && input.mutation.provenance !== "default")
335
+ throw new Error("desired state provenance is invalid");
336
+ affectedKeyResult = {
131
337
  value: requireText(input.mutation.value, "desired state value"),
132
338
  provenance: input.mutation.provenance,
133
339
  version,
134
340
  source: requireText(input.mutation.source, "desired state source"),
135
341
  ...(expiresAt ? { expiresAt } : {}),
136
342
  };
343
+ next.desiredStates = { ...current.desiredStates, [key]: affectedKeyResult };
137
344
  }
138
345
  else {
139
346
  if (input.mutation.provenance !== "stated" && input.mutation.provenance !== "installed_explicit_policy")
140
347
  throw new Error("routine action grants require explicit authority");
141
- if (!Number.isInteger(input.mutation.maxCount) || input.mutation.maxCount < 1 || !Number.isFinite(input.mutation.windowMs) || input.mutation.windowMs <= 0)
348
+ if (!positiveInteger(input.mutation.maxCount) || !positiveInteger(input.mutation.windowMs))
142
349
  throw new Error("routine action grant bounds are invalid");
143
- if (!input.mutation.verificationRequired)
350
+ if (input.mutation.verificationRequired !== true)
144
351
  throw new Error("routine action grants require post-action verification");
145
- const targets = [...new Set(input.mutation.targets.map((value) => requireText(value, "routine action target")))];
352
+ if (!Array.isArray(input.mutation.targets) || !Array.isArray(input.mutation.exclusions))
353
+ throw new Error("routine action targets and exclusions must be arrays");
354
+ const targets = [...new Set(input.mutation.targets.map((value) => requireText(value, "routine action target")))].sort();
146
355
  if (targets.length === 0)
147
356
  throw new Error("routine action grant requires a target");
148
- next.routineActionGrants[requireText(input.mutation.key, "routine action key")] = {
357
+ affectedKeyResult = {
149
358
  action: requireText(input.mutation.action, "routine action"), targets, maxCount: input.mutation.maxCount, windowMs: input.mutation.windowMs,
150
- verificationRequired: input.mutation.verificationRequired, exclusions: [...new Set(input.mutation.exclusions)], provenance: input.mutation.provenance,
359
+ verificationRequired: input.mutation.verificationRequired, exclusions: [...new Set(input.mutation.exclusions.map((value) => requireText(value, "routine action exclusion")))].sort(), provenance: input.mutation.provenance,
151
360
  issuer, authorizedAt: now, authorizingSessionEvent, version, ...(expiresAt ? { expiresAt } : {}),
152
361
  };
362
+ next.routineActionGrants = { ...current.routineActionGrants, [key]: affectedKeyResult };
363
+ }
364
+ const fingerprint = mutationFingerprint(input.mutation.kind, key, affectedKeyResult);
365
+ const identity = operationIdentity(issuer, authorizingSessionEvent, authorization.requestId, input.mutation.kind, key);
366
+ const previousIndex = snapshot.rows.findIndex((row) => operationIdentity(row.issuer, row.authorizingSessionEvent, row.authorization.requestId, row.mutationKind, row.key) === identity);
367
+ if (previousIndex >= 0) {
368
+ const previous = snapshot.rows[previousIndex];
369
+ const currentResult = previous.mutationKind === "set_desired_state" ? current.desiredStates[key] : current.routineActionGrants[key];
370
+ if (previous.mutationFingerprint !== fingerprint || snapshot.rows.slice(previousIndex + 1).some((row) => row.mutationKind === previous.mutationKind && row.key === key)
371
+ || !(0, node_util_1.isDeepStrictEqual)(currentResult, previous.affectedKeyResult))
372
+ throw new Error("steward policy transaction replay changed");
373
+ return current;
153
374
  }
375
+ if (current.version !== input.expectedVersion)
376
+ throw new Error(`steward policy version changed: expected ${input.expectedVersion}, got ${current.version}`);
377
+ optionalExpiry(expiresAt, now);
378
+ const postimage = JSON.stringify(next, null, 2);
379
+ if (Buffer.byteLength(postimage) > MAX_POLICY_BYTES)
380
+ throw new Error("steward policy exceeds its bound");
381
+ const row = {
382
+ schemaVersion: 2, transactionId: sha256(JSON.stringify([identity, fingerprint])), precedingBytesSha256: sha256(snapshot.auditBytes),
383
+ mutationKind: input.mutation.kind, key, mutationFingerprint: fingerprint, affectedKeyResult, affectedKeyResultSha256: sha256(JSON.stringify(affectedKeyResult)),
384
+ issuer, authorizingSessionEvent, authorization: { ...authorization },
385
+ preimage: snapshot.bytes, preimageVersion: current.version, preimageSha256: snapshot.revision,
386
+ postimage, postimageVersion: version, postimageSha256: sha256(postimage), at: now,
387
+ };
388
+ if (Buffer.byteLength(JSON.stringify(row)) > MAX_AUDIT_ROW_BYTES)
389
+ throw new Error("steward policy audit row exceeds its bound");
154
390
  ensureDirectory(agentRoot);
391
+ appendReceipt(auditPath(agentRoot), row);
155
392
  (0, session_transaction_1.writeSessionTransaction)(policyPath(agentRoot), next, { lease, expectedRevision: snapshot.revision });
156
- appendReceipt(auditPath(agentRoot), { schemaVersion: 1, policyVersion: version, issuer, authorizingSessionEvent, mutationKind: input.mutation.kind, at: now });
393
+ if (readPolicyTransaction(policyPath(agentRoot), lease).bytes !== postimage)
394
+ throw new Error("steward policy publication readback differs");
157
395
  (0, runtime_1.emitNervesEvent)({ component: "heart", event: "heart.steward_policy_updated", message: "updated steward policy", meta: { version, mutationKind: input.mutation.kind, issuer } });
158
396
  return next;
159
397
  });
@@ -170,58 +408,106 @@ function readRoutineActionReceipts(agentRoot) {
170
408
  latest.set(receipt.id, receipt);
171
409
  return [...latest.values()];
172
410
  }
173
- function expectedOff(policy, target, now) {
174
- const desired = policy.desiredStates[`container:${target}`];
175
- if (!desired || (desired.expiresAt && Date.parse(desired.expiresAt) <= Date.parse(now)))
411
+ function currentRequester(value, target) {
412
+ if (!record(value) || !text(value.friendId))
176
413
  return false;
177
- return /^(?:off|disabled|paused|intentionally_off|intentionally_paused)$/u.test(desired.value.trim().toLowerCase());
414
+ if (value.kind === "owner" || value.kind === "household_request") {
415
+ return exactKeys(value, ["kind", "friendId", "profileId", "requestId", "sessionEventId", "origin"])
416
+ && value.profileId === (value.kind === "owner" ? "sanctuary-owner" : "sanctuary-household")
417
+ && text(value.requestId) && text(value.sessionEventId) && record(value.origin)
418
+ && exactKeys(value.origin, ["friendId", "channel", "key"])
419
+ && value.origin.friendId === value.friendId && value.origin.channel === "telegram" && text(value.origin.key);
420
+ }
421
+ return value.kind === "owner_event" && exactKeys(value, ["kind", "friendId", "profileId", "event", "target"])
422
+ && value.profileId === "sanctuary-event" && record(value.target) && exactKeys(value.target, ["id", "name"])
423
+ && text(value.target.id) && value.target.name === target && record(value.event)
424
+ && exactKeys(value.event, ["schemaVersion", "recordPath", "agent", "source", "eventId", "generation", "observationRevision", "claimOwner"])
425
+ && value.event.schemaVersion === 1 && value.event.source === "sanctuary-health"
426
+ && value.event.eventId === `container:${value.target.id}:availability`
427
+ && text(value.event.recordPath) && text(value.event.agent) && positiveInteger(value.event.generation)
428
+ && text(value.event.observationRevision) && text(value.event.claimOwner);
429
+ }
430
+ function appliedEntry(rows, kind, key, entry) {
431
+ return rows.some((row) => row.mutationKind === kind && row.key === key && row.postimageVersion === entry.version && (0, node_util_1.isDeepStrictEqual)(row.affectedKeyResult, entry));
178
432
  }
179
433
  const UNRESOLVED_ACTION_STATES = new Set(["reserved", "attempting", "effect_acknowledged", "recovery_pending", "indeterminate"]);
180
- function inspectRoutineActionGrantSnapshot(policy, receipts, input) {
434
+ function inspectRoutineActionGrantSnapshot(policy, rows, receipts, input) {
435
+ const now = input.now === undefined ? new Date().toISOString() : input.now;
436
+ if (!canonicalTime(now))
437
+ return { allowed: false, reason: "routine action time must be canonical" };
438
+ if (!text(input.key) || !text(input.action) || !text(input.target))
439
+ return { allowed: false, reason: "routine action requires an exact key, action, and target" };
181
440
  if (input.expectedPolicyVersion !== undefined && policy.version !== input.expectedPolicyVersion)
182
441
  return { allowed: false, reason: "routine action policy version changed" };
442
+ if (!currentRequester(input.requester, input.target) || !positiveInteger(input.authorizationVersion))
443
+ return { allowed: false, reason: "routine action requires a current versioned requester" };
444
+ const desiredKey = `container:${input.target}`;
445
+ const desired = policy.desiredStates[desiredKey];
446
+ if (input.expectedDesiredStateVersion !== undefined && desired?.version !== input.expectedDesiredStateVersion)
447
+ return { allowed: false, reason: "routine action desired state version changed" };
448
+ const activeDesired = desired && (desired.expiresAt === undefined || Date.parse(desired.expiresAt) > Date.parse(now));
449
+ const desiredValue = desired?.value.toLowerCase();
450
+ if (activeDesired && /^(?:off|disabled|paused|intentionally_off|intentionally_paused)$/u.test(desiredValue))
451
+ return { allowed: false, reason: "container is expected off" };
183
452
  const grant = policy.routineActionGrants[input.key];
453
+ if (input.expectedGrantVersion !== undefined && grant?.version !== input.expectedGrantVersion)
454
+ return { allowed: false, reason: "routine action grant version changed" };
455
+ const fallback = input.requester.kind === "owner" ? { approvalFallback: true } : {};
184
456
  if (!grant)
185
- return { allowed: false, reason: "routine action grant is missing" };
457
+ return { allowed: false, reason: "routine action grant is missing", ...fallback };
458
+ if (grant.provenance !== "stated")
459
+ return { allowed: false, reason: "routine action grant must be owner-stated" };
460
+ if (!appliedEntry(rows, "grant_routine_action", input.key, grant))
461
+ return { allowed: false, reason: "routine action grant has no applied owner authorization" };
186
462
  if (grant.action !== input.action)
187
463
  return { allowed: false, reason: "routine action does not match the grant" };
188
464
  if (!grant.targets.includes(input.target) || grant.exclusions.includes(input.target))
189
465
  return { allowed: false, reason: "routine action target is not authorized" };
190
- const now = input.now ?? new Date().toISOString();
191
466
  if (grant.expiresAt && Date.parse(grant.expiresAt) <= Date.parse(now))
192
- return { allowed: false, reason: "routine action grant expired" };
193
- if (expectedOff(policy, input.target, now))
194
- return { allowed: false, reason: "container is expected off" };
467
+ return { allowed: false, reason: "routine action grant expired", ...fallback };
468
+ if (!activeDesired || desired.provenance !== "stated" || !appliedEntry(rows, "set_desired_state", desiredKey, desired))
469
+ return { allowed: false, reason: "container has no active applied owner-stated desired state" };
470
+ if (!["on", "always_on", "expected_on"].includes(desiredValue) && !(desiredValue === "on_demand" && input.requester.kind !== "owner_event"))
471
+ return { allowed: false, reason: "container desired state does not authorize this request" };
195
472
  if (receipts.some((receipt) => receipt.action === input.action && receipt.target === input.target && UNRESOLVED_ACTION_STATES.has(receipt.state))) {
196
473
  return { allowed: false, reason: "routine action has an unresolved receipt for this target" };
197
474
  }
198
- return { allowed: true, policyVersion: policy.version, grantVersion: grant.version, key: input.key, action: grant.action, target: input.target };
475
+ const windowStart = Date.parse(now) - grant.windowMs;
476
+ if (receipts.filter((receipt) => receipt.key === input.key && Date.parse(receipt.reservedAt) > windowStart).length >= grant.maxCount)
477
+ return { allowed: false, reason: "routine action rate limit reached" };
478
+ return { allowed: true, policyVersion: policy.version, desiredStateVersion: desired.version, grantVersion: grant.version, key: input.key, action: grant.action, target: input.target, requester: input.requester, authorizationVersion: input.authorizationVersion };
199
479
  }
200
480
  function inspectRoutineActionGrant(agentRoot, input) {
201
481
  try {
202
- const policy = readStewardPolicy(agentRoot);
203
- return inspectRoutineActionGrantSnapshot(policy, readRoutineActionReceipts(agentRoot), input);
482
+ const { policy, rows } = readPolicyState(agentRoot);
483
+ return inspectRoutineActionGrantSnapshot(policy, rows, readRoutineActionReceipts(agentRoot), input);
204
484
  }
205
485
  catch (error) {
206
486
  return { allowed: false, reason: error instanceof Error ? error.message : "routine action policy is unavailable" };
207
487
  }
208
488
  }
209
489
  function consumeRoutineActionGrant(agentRoot, input) {
490
+ if (!text(input.authorizationReceiptId))
491
+ throw new Error("routine action requires a current authorization receipt");
492
+ const authorizationReceiptId = input.authorizationReceiptId;
210
493
  return (0, session_transaction_1.withImmediateSessionTurnLease)(policyPath(agentRoot), (lease) => {
211
- const snapshot = (0, session_transaction_1.readSessionTransaction)(policyPath(agentRoot), lease);
212
- const policy = snapshot.value === null ? structuredClone(EMPTY_POLICY) : validateStewardPolicy(snapshot.value);
494
+ const snapshot = readAuditedPolicy(agentRoot, lease);
495
+ const policy = snapshot.policy;
213
496
  const grant = policy.routineActionGrants[input.key];
214
- const now = input.now ?? new Date().toISOString();
215
- const action = input.action ?? grant?.action ?? "";
497
+ const now = input.now === undefined ? new Date().toISOString() : input.now;
498
+ const action = input.action === undefined ? grant?.action ?? "" : input.action;
216
499
  const receipts = readRoutineActionReceipts(agentRoot);
217
- const decision = inspectRoutineActionGrantSnapshot(policy, receipts, { key: input.key, action, target: input.target, expectedPolicyVersion: input.expectedPolicyVersion, now });
500
+ const decision = inspectRoutineActionGrantSnapshot(policy, snapshot.rows, receipts, { ...input, action, now });
218
501
  if (!decision.allowed)
219
502
  throw new Error(decision.reason);
503
+ const resolvedTarget = input.resolvedTarget === undefined
504
+ ? decision.requester.kind === "owner_event" ? decision.requester.target : { id: "unresolved", name: input.target }
505
+ : input.resolvedTarget;
506
+ if (!record(resolvedTarget) || !text(resolvedTarget.id) || resolvedTarget.name !== input.target)
507
+ throw new Error("routine action resolved target is invalid");
508
+ if (decision.requester.kind === "owner_event" && resolvedTarget.id !== decision.requester.target.id)
509
+ throw new Error("routine action event target binding changed");
220
510
  ensureDirectory(agentRoot);
221
- const windowStart = Date.parse(now) - grant.windowMs;
222
- const used = receipts.filter((receipt) => receipt.key === input.key && Date.parse(receipt.reservedAt) > windowStart).length;
223
- if (used >= grant.maxCount)
224
- throw new Error("routine action rate limit reached");
225
511
  const id = `action-${(0, node_crypto_1.randomUUID)()}`;
226
512
  const receipt = {
227
513
  schemaVersion: 2,
@@ -232,15 +518,17 @@ function consumeRoutineActionGrant(agentRoot, input) {
232
518
  target: input.target,
233
519
  policyVersion: policy.version,
234
520
  grantVersion: grant.version,
521
+ desiredStateVersion: decision.desiredStateVersion,
522
+ requester: structuredClone(decision.requester),
235
523
  reservedAt: now,
236
524
  updatedAt: now,
237
- authorizationReceiptId: input.authorizationReceiptId ?? `policy-${grant.issuer}-${grant.authorizingSessionEvent}`,
238
- authorizationVersion: input.authorizationVersion ?? grant.version,
525
+ authorizationReceiptId,
526
+ authorizationVersion: decision.authorizationVersion,
239
527
  attemptId: input.attemptId ?? `attempt-${(0, node_crypto_1.randomUUID)()}`,
240
528
  attempt: 1,
241
529
  expectedBeforeState: input.expectedBeforeState ?? null,
242
- resolvedTarget: input.resolvedTarget ?? { id: "unresolved", name: input.target },
243
- effect: input.effect ?? { operation: action, targetId: input.resolvedTarget?.id ?? "unresolved" },
530
+ resolvedTarget: { id: resolvedTarget.id, name: resolvedTarget.name },
531
+ effect: input.effect ?? { operation: action, targetId: resolvedTarget.id },
244
532
  effectReceipt: null,
245
533
  verifiedAfterState: null,
246
534
  recoveryState: { state: "not_needed", compensation: "none" },
@@ -250,6 +538,34 @@ function consumeRoutineActionGrant(agentRoot, input) {
250
538
  return receipt;
251
539
  });
252
540
  }
541
+ function withStewardPolicyLease(agentRoot, operation) {
542
+ return (0, session_transaction_1.withSessionTurnLease)(policyPath(agentRoot), operation);
543
+ }
544
+ async function withRoutineActionAttempt(agentRoot, reservation, validate, attempt) {
545
+ await withStewardPolicyLease(agentRoot, async (lease) => {
546
+ await validate();
547
+ const snapshot = readAuditedPolicy(agentRoot, lease);
548
+ const receipts = readRoutineActionReceipts(agentRoot);
549
+ const current = receipts.find((receipt) => receipt.id === reservation.id);
550
+ if (!current || current.state !== "reserved" || !(0, node_util_1.isDeepStrictEqual)(current, reservation))
551
+ throw new Error("routine action reservation changed");
552
+ if (!reservation.requester || reservation.desiredStateVersion === undefined)
553
+ throw new Error("routine action reservation has no current requester binding");
554
+ const now = new Date().toISOString();
555
+ const decision = inspectRoutineActionGrantSnapshot(snapshot.policy, snapshot.rows, receipts.filter((receipt) => receipt.id !== reservation.id), {
556
+ key: reservation.key, action: reservation.action, target: reservation.target, requester: reservation.requester,
557
+ authorizationVersion: reservation.authorizationVersion, expectedPolicyVersion: reservation.policyVersion,
558
+ expectedDesiredStateVersion: reservation.desiredStateVersion, expectedGrantVersion: reservation.grantVersion, now,
559
+ });
560
+ if (!decision.allowed)
561
+ throw new Error(decision.reason);
562
+ const reservedAt = Date.parse(reservation.reservedAt);
563
+ const windowStart = Date.parse(now) - snapshot.policy.routineActionGrants[reservation.key].windowMs;
564
+ if (!(reservedAt > windowStart && reservedAt <= Date.parse(now)))
565
+ throw new Error("routine action reservation is outside its rate window");
566
+ await attempt();
567
+ });
568
+ }
253
569
  function transitionRoutineActionReceipt(agentRoot, input) {
254
570
  return (0, session_transaction_1.withImmediateSessionTurnLease)(policyPath(agentRoot), () => {
255
571
  const current = readRoutineActionReceipts(agentRoot).find((receipt) => receipt.id === input.id);
@@ -283,7 +283,14 @@ async function executeApprovalDecision(options) {
283
283
  if (decided.state !== "claimed")
284
284
  return decided;
285
285
  await options.hooks?.afterClaim?.();
286
- if (options.currentSessionRevision !== decided.suspendedSessionRevision) {
286
+ let currentSessionRevision;
287
+ try {
288
+ currentSessionRevision = typeof options.currentSessionRevision === "function" ? options.currentSessionRevision() : options.currentSessionRevision;
289
+ }
290
+ catch {
291
+ return terminalizeClaimed(options.approvalStore, decided, "drifted", "current session revision is unavailable");
292
+ }
293
+ if (currentSessionRevision !== decided.suspendedSessionRevision) {
287
294
  return terminalizeClaimed(options.approvalStore, decided, "session_head_changed", "suspended session revision changed");
288
295
  }
289
296
  const checkpoint = options.checkpointStore.read(decided.approvalId);