@ouro.bot/cli 0.1.0-alpha.873 → 0.1.0-alpha.874
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.json +6 -0
- package/deploy/unraid/sanctuary-replay-gate.mjs +31 -1
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/a2a/replay-harness.js +26 -0
- package/dist/heart/steward-policy.js +37 -14
- package/dist/repertoire/tools-steward-policy.js +8 -3
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.874",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Replay identities (the replay gate's test peers) can no longer write the real owner steward policy, even by a no-op restatement; a new steward_policy_manage correct_provenance action fixes a wrong attribution with an audited, value-preserving write; and the replay gate gains an owner-policy-untouched case that hashes steward.json and its audit file before the first case and after the last"
|
|
8
|
+
]
|
|
9
|
+
},
|
|
4
10
|
{
|
|
5
11
|
"version": "0.1.0-alpha.873",
|
|
6
12
|
"changes": [
|
|
@@ -245,6 +245,18 @@ export function newAwaits(before, after) {
|
|
|
245
245
|
return [...after.awaiting.filter((entry) => !known.has(key(entry))).map((entry) => view(entry, false)), ...after.done.filter((entry) => !known.has(key(entry))).map((entry) => view(entry, true))]
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Cross-cutting proof that the replay run never wrote the real owner policy: the sha256 of the steward.json bytes and of
|
|
250
|
+
* the audit file, taken before the first case and after the last, must be identical. No case may change them, whatever
|
|
251
|
+
* the Butler decided inside its turn (a refused policy write is fine; a written one is not).
|
|
252
|
+
*/
|
|
253
|
+
export function ownerPolicyUntouched(before, after) {
|
|
254
|
+
return [
|
|
255
|
+
check("the real steward.json bytes are unchanged", before.stewardSha === after.stewardSha, `${before.stewardSha?.slice(0, 12) ?? "absent"} -> ${after.stewardSha?.slice(0, 12) ?? "absent"}`),
|
|
256
|
+
check("the policy audit file bytes are unchanged", before.auditSha === after.auditSha, `${before.auditSha?.slice(0, 12) ?? "absent"} -> ${after.auditSha?.slice(0, 12) ?? "absent"}`),
|
|
257
|
+
]
|
|
258
|
+
}
|
|
259
|
+
|
|
248
260
|
// ---- orchestration (all I/O is behind `host`) --------------------------------------------------------------------
|
|
249
261
|
|
|
250
262
|
export async function runCase(host, testCase, { plant } = {}) {
|
|
@@ -289,6 +301,7 @@ export async function runSuite(host, { cases = CASES.map((entry) => entry.id), p
|
|
|
289
301
|
const selected = CASES.filter((entry) => cases.includes(entry.id))
|
|
290
302
|
await host.waitReady?.()
|
|
291
303
|
const runStart = host.now()
|
|
304
|
+
const baseline = await host.observe()
|
|
292
305
|
const results = []
|
|
293
306
|
host.openWindow(windowMinutes)
|
|
294
307
|
try {
|
|
@@ -310,7 +323,9 @@ export async function runSuite(host, { cases = CASES.map((entry) => entry.id), p
|
|
|
310
323
|
const clean = readable && leaks.length === 0
|
|
311
324
|
const detail = readable ? [...leaks.map((leak) => `${leak.key} (${leak.reason})`), ...info.map((entry) => `info: ${entry.key} (${entry.reason})`)].join(", ") : "state/telegram/effects is not readable, so the absence of a leak cannot be shown"
|
|
312
325
|
const telegram = { id: "no-telegram", status: clean ? "pass" : "fail", checks: [check("no owner-notice Telegram effect attributable to the replay run was recorded", clean, detail)] }
|
|
313
|
-
const
|
|
326
|
+
const policyChecks = ownerPolicyUntouched(baseline, finalObservation)
|
|
327
|
+
const policy = { id: "owner-policy-untouched", status: policyChecks.every((entry) => entry.ok) ? "pass" : "fail", checks: policyChecks }
|
|
328
|
+
const all = [...results, policy, telegram]
|
|
314
329
|
const failed = all.filter((entry) => entry.status === "fail").map((entry) => entry.id)
|
|
315
330
|
return { results: all, summary: { ok: failed.length === 0, passed: all.filter((entry) => entry.status === "pass").length, skipped: all.filter((entry) => entry.status === "skipped").length, failed } }
|
|
316
331
|
}
|
|
@@ -379,6 +394,11 @@ export function selfTest() {
|
|
|
379
394
|
if (pass !== (kind === "pass")) problems.push(`${testCase.id}: the ${kind} fixture ${pass ? "passed" : "failed"} (${checks.filter((entry) => !entry.ok).map((entry) => entry.name).join("; ")})`)
|
|
380
395
|
}
|
|
381
396
|
}
|
|
397
|
+
const same = ownerPolicyUntouched(emptyObservation({ auditSha: "x" }), emptyObservation({ auditSha: "x" }))
|
|
398
|
+
if (!same.every((entry) => entry.ok)) problems.push("owner-policy-untouched failed on identical policy bytes")
|
|
399
|
+
for (const [name, changed] of [["steward.json", { stewardSha: "bbb" }], ["audit file", { auditSha: "y" }]]) {
|
|
400
|
+
if (ownerPolicyUntouched(emptyObservation({ auditSha: "x" }), emptyObservation({ auditSha: "x", ...changed })).every((entry) => entry.ok)) problems.push(`owner-policy-untouched passed with different ${name} bytes`)
|
|
401
|
+
}
|
|
382
402
|
const stalled = { trackedDownloadStatus: "warning" }
|
|
383
403
|
if (CASES.find((entry) => entry.id === "stall-kept").applicable(emptyObservation({ queue: [{ id: 1 }] })) === null) problems.push("stall-kept ran with no stalled item")
|
|
384
404
|
if (CASES.find((entry) => entry.id === "stall-kept").applicable(emptyObservation({ queue: [stalled] })) !== null) problems.push("stall-kept skipped with a stalled item")
|
|
@@ -471,6 +491,7 @@ export function makeHost({ bundle = DEFAULT_BUNDLE, cardUrl, log = console.error
|
|
|
471
491
|
},
|
|
472
492
|
async observe() {
|
|
473
493
|
const policy = path.join(state, "policy", "steward.json")
|
|
494
|
+
const audit = path.join(state, "policy", "policy-audit.ndjson")
|
|
474
495
|
const ledger = path.join(bundle, "books", "ledger.ndjson")
|
|
475
496
|
let queue = null
|
|
476
497
|
try {
|
|
@@ -487,6 +508,7 @@ export function makeHost({ bundle = DEFAULT_BUNDLE, cardUrl, log = console.error
|
|
|
487
508
|
})
|
|
488
509
|
return {
|
|
489
510
|
stewardSha: existsSync(policy) ? sha256(readFileSync(policy)) : null,
|
|
511
|
+
auditSha: existsSync(audit) ? sha256(readFileSync(audit)) : null,
|
|
490
512
|
ledgerLines: readLines(ledger).length,
|
|
491
513
|
queue,
|
|
492
514
|
containers,
|
|
@@ -536,6 +558,7 @@ export function provision({ bundle = DEFAULT_BUNDLE, cardUrl, log = console.log,
|
|
|
536
558
|
const sink = path.join(replayDir, "notices.ndjson")
|
|
537
559
|
writeFileSync(sink, "", { flag: "a", mode: 0o600 }); chownSync(sink, owner.uid, owner.gid)
|
|
538
560
|
mkdirSync(clientDir, { recursive: true, mode: 0o700 }); chownSync(clientDir, owner.uid, owner.gid)
|
|
561
|
+
const replayIdentities = {}
|
|
539
562
|
const previous = existsSync(path.join(clientDir, "provision.json")) ? JSON.parse(readFileSync(path.join(clientDir, "provision.json"), "utf8")) : {}
|
|
540
563
|
const resolvedCard = cardUrl ?? previous.cardUrl ?? discover()
|
|
541
564
|
const out = { cardUrl: resolvedCard }
|
|
@@ -555,8 +578,15 @@ export function provision({ bundle = DEFAULT_BUNDLE, cardUrl, log = console.log,
|
|
|
555
578
|
if (grant && record.delegationGrant?.scope !== "principal_commands") grantPrincipalCommands(file)
|
|
556
579
|
if (!grant && record.delegationGrant) throw new Error(`${name} must not hold a delegation grant`)
|
|
557
580
|
out[who] = { friendId, did, containerIdentityFile, hostIdentity }
|
|
581
|
+
replayIdentities[friendId] = { name, who }
|
|
558
582
|
log(`${name}: friend ${friendId} (${trust}${grant ? ", principal_commands" : ", no grant"})`)
|
|
559
583
|
}
|
|
584
|
+
// Permanent marker, root-owned and never cleared with the window: the Butler refuses every owner-policy write from these friends (src/a2a/replay-harness.ts isReplayIdentity).
|
|
585
|
+
const registry = path.join(replayDir, "identities.json")
|
|
586
|
+
let known = {}
|
|
587
|
+
try { known = JSON.parse(readFileSync(registry, "utf8")).friends ?? {} } catch { /* first provision */ }
|
|
588
|
+
writeAtomic(registry, JSON.stringify({ friends: { ...known, ...replayIdentities } }, null, 2), 0o644)
|
|
589
|
+
chownSync(registry, rootUid, rootGid)
|
|
560
590
|
writeAtomic(path.join(clientDir, "provision.json"), JSON.stringify(out, null, 2), 0o644)
|
|
561
591
|
log(`provisioned; card ${resolvedCard}`)
|
|
562
592
|
return out
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
2
|
<Container version="2">
|
|
3
3
|
<Name>ouro-butler</Name>
|
|
4
|
-
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.874</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -36,6 +36,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.REPLAY_WINDOW_MAX_MS = void 0;
|
|
37
37
|
exports.replayWindowPath = replayWindowPath;
|
|
38
38
|
exports.replaySinkPath = replaySinkPath;
|
|
39
|
+
exports.replayIdentitiesPath = replayIdentitiesPath;
|
|
40
|
+
exports.isReplayIdentity = isReplayIdentity;
|
|
39
41
|
exports.isReplayWindowOpen = isReplayWindowOpen;
|
|
40
42
|
exports.appendReplayNotice = appendReplayNotice;
|
|
41
43
|
exports.replayNoticeRecorded = replayNoticeRecorded;
|
|
@@ -62,6 +64,30 @@ function replayWindowPath(agentRoot) {
|
|
|
62
64
|
function replaySinkPath(agentRoot) {
|
|
63
65
|
return path.join(replayDir(agentRoot), "notices.ndjson");
|
|
64
66
|
}
|
|
67
|
+
/** Permanent registry of replay identities, written by the host's provisioning and never cleared. */
|
|
68
|
+
function replayIdentitiesPath(agentRoot) {
|
|
69
|
+
return path.join(replayDir(agentRoot), "identities.json");
|
|
70
|
+
}
|
|
71
|
+
function listsFriend(file, friendId) {
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// A missing file lists nobody; a file that exists but cannot be read or parsed fails closed.
|
|
78
|
+
return error.code !== "ENOENT";
|
|
79
|
+
}
|
|
80
|
+
const friends = parsed?.friends;
|
|
81
|
+
return !!friends && typeof friends === "object" && Object.hasOwn(friends, friendId);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* True when `friendId` is a replay identity: listed in the permanent registry, or holding any window entry (open or
|
|
85
|
+
* expired). Unlike the window check this needs no trusted-owner proof, because the marker only ever restricts: forging
|
|
86
|
+
* an entry can block a friend from writing owner policy, never grant anything. Owner policy writes refuse these identities.
|
|
87
|
+
*/
|
|
88
|
+
function isReplayIdentity(agentRoot, friendId) {
|
|
89
|
+
return listsFriend(replayIdentitiesPath(agentRoot), friendId) || listsFriend(replayWindowPath(agentRoot), friendId);
|
|
90
|
+
}
|
|
65
91
|
/** A root-controlled directory: a real directory (no symlink), owned by the trusted uid, writable by neither group nor other. */
|
|
66
92
|
function isTrustedDirectory(target, trustedUid) {
|
|
67
93
|
try {
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.readStewardPolicy = readStewardPolicy;
|
|
37
|
+
exports.assertNotReplayIdentity = assertNotReplayIdentity;
|
|
37
38
|
exports.updateStewardPolicy = updateStewardPolicy;
|
|
38
39
|
exports.readRoutineActionReceipts = readRoutineActionReceipts;
|
|
39
40
|
exports.inspectRoutineActionGrant = inspectRoutineActionGrant;
|
|
@@ -47,6 +48,7 @@ const path = __importStar(require("node:path"));
|
|
|
47
48
|
const node_crypto_1 = require("node:crypto");
|
|
48
49
|
const node_util_1 = require("node:util");
|
|
49
50
|
const runtime_1 = require("../nerves/runtime");
|
|
51
|
+
const replay_harness_1 = require("../a2a/replay-harness");
|
|
50
52
|
const session_transaction_1 = require("../mind/session-transaction");
|
|
51
53
|
const EMPTY_POLICY = { schemaVersion: 1, version: 0, desiredStates: {}, routineActionGrants: {}, updatedAt: null };
|
|
52
54
|
const MAX_POLICY_BYTES = 1024 * 1024;
|
|
@@ -82,8 +84,8 @@ function delegation(value) {
|
|
|
82
84
|
&& text(value.delegateFriendId) && text(value.delegateDid) && text(value.noticeId);
|
|
83
85
|
}
|
|
84
86
|
function desiredEntry(value, version) {
|
|
85
|
-
return record(value) && exactKeys(value, ["value", "provenance", "version", "source"], ["expiresAt"])
|
|
86
|
-
&& text(value.value) && text(value.source) && positiveInteger(value.version) && value.version <= version
|
|
87
|
+
return record(value) && exactKeys(value, ["value", "provenance", "version", "source"], ["correction", "expiresAt"])
|
|
88
|
+
&& text(value.value) && text(value.source) && (value.correction === undefined || text(value.correction)) && positiveInteger(value.version) && value.version <= version
|
|
87
89
|
&& (value.provenance === "stated" || value.provenance === "observed" || value.provenance === "default")
|
|
88
90
|
&& (value.expiresAt === undefined || canonicalTime(value.expiresAt));
|
|
89
91
|
}
|
|
@@ -131,14 +133,14 @@ function operationIdentity(issuer, event, requestId, kind, key) {
|
|
|
131
133
|
}
|
|
132
134
|
function auditRow(value) {
|
|
133
135
|
if (!record(value) || !exactKeys(value, ["schemaVersion", "transactionId", "precedingBytesSha256", "mutationKind", "key", "mutationFingerprint", "affectedKeyResult", "affectedKeyResultSha256", "issuer", "authorizingSessionEvent", "authorization", "preimage", "preimageVersion", "preimageSha256", "postimage", "postimageVersion", "postimageSha256", "at"], ["delegatedVia"])
|
|
134
|
-
|| value.schemaVersion !== 2 || (value.mutationKind !== "set_desired_state" && value.mutationKind !== "grant_routine_action")
|
|
136
|
+
|| value.schemaVersion !== 2 || (value.mutationKind !== "set_desired_state" && value.mutationKind !== "grant_routine_action" && value.mutationKind !== "correct_provenance")
|
|
135
137
|
|| !text(value.key) || !text(value.issuer) || !text(value.authorizingSessionEvent) || !ownerAuthorization(value.authorization)
|
|
136
138
|
|| (value.delegatedVia !== undefined && !delegation(value.delegatedVia))
|
|
137
139
|
|| typeof value.preimage !== "string" || typeof value.postimage !== "string" || !canonicalTime(value.at))
|
|
138
140
|
return false;
|
|
139
141
|
const before = policyImage(value.preimage);
|
|
140
142
|
const after = policyImage(value.postimage);
|
|
141
|
-
const result = value.mutationKind === "
|
|
143
|
+
const result = value.mutationKind === "grant_routine_action" ? after.routineActionGrants[value.key] : after.desiredStates[value.key];
|
|
142
144
|
if (!result || value.preimageVersion !== before.version || value.postimageVersion !== after.version
|
|
143
145
|
|| after.version !== before.version + 1 || result.version !== after.version || after.updatedAt !== value.at
|
|
144
146
|
|| value.preimageSha256 !== sha256(value.preimage) || value.postimageSha256 !== sha256(value.postimage)
|
|
@@ -149,9 +151,17 @@ function auditRow(value) {
|
|
|
149
151
|
return false;
|
|
150
152
|
if ("action" in result && (result.issuer !== value.issuer || result.authorizingSessionEvent !== value.authorizingSessionEvent || result.authorizedAt !== value.at))
|
|
151
153
|
return false;
|
|
154
|
+
if (value.mutationKind === "correct_provenance") {
|
|
155
|
+
// A correction changes the source and records its note on one existing entry, and nothing else about it.
|
|
156
|
+
// A missing prior entry spreads to nothing, so the comparison fails without a separate branch.
|
|
157
|
+
const prior = before.desiredStates[value.key];
|
|
158
|
+
const corrected = result;
|
|
159
|
+
if (!(0, node_util_1.isDeepStrictEqual)(result, { ...prior, source: corrected.source, correction: corrected.correction, version: after.version }))
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
152
162
|
const expected = {
|
|
153
163
|
...before, version: after.version, updatedAt: value.at,
|
|
154
|
-
desiredStates: value.mutationKind
|
|
164
|
+
desiredStates: value.mutationKind !== "grant_routine_action" ? { ...before.desiredStates, [value.key]: result } : before.desiredStates,
|
|
155
165
|
routineActionGrants: value.mutationKind === "grant_routine_action" ? { ...before.routineActionGrants, [value.key]: result } : before.routineActionGrants,
|
|
156
166
|
};
|
|
157
167
|
return (0, node_util_1.isDeepStrictEqual)(after, expected);
|
|
@@ -312,7 +322,13 @@ function readPolicyState(agentRoot) {
|
|
|
312
322
|
function readStewardPolicy(agentRoot) {
|
|
313
323
|
return readPolicyState(agentRoot).policy;
|
|
314
324
|
}
|
|
325
|
+
/** Replay identities (the gate's test peers) can never write owner policy: the marker comes from the friend ids, never from model text. */
|
|
326
|
+
function assertNotReplayIdentity(agentRoot, friendIds) {
|
|
327
|
+
if (friendIds.some((friendId) => friendId !== undefined && (0, replay_harness_1.isReplayIdentity)(agentRoot, friendId)))
|
|
328
|
+
throw new Error("replay identities cannot write owner policy");
|
|
329
|
+
}
|
|
315
330
|
function updateStewardPolicy(agentRoot, input) {
|
|
331
|
+
assertNotReplayIdentity(agentRoot, [input.actor.friendId, input.actor.delegatedVia?.delegateFriendId]);
|
|
316
332
|
if (input.actor.trustLevel !== "family")
|
|
317
333
|
throw new Error("steward policy mutation requires family authority");
|
|
318
334
|
const authorizingSessionEvent = requireText(input.actor.sessionEventId, "authorizing session event");
|
|
@@ -322,7 +338,7 @@ function updateStewardPolicy(agentRoot, input) {
|
|
|
322
338
|
throw new Error("steward policy mutation requires current owner authorization");
|
|
323
339
|
if (input.actor.delegatedVia !== undefined && !delegation(input.actor.delegatedVia))
|
|
324
340
|
throw new Error("steward policy delegation is invalid");
|
|
325
|
-
if (input.mutation.kind !== "set_desired_state" && input.mutation.kind !== "grant_routine_action")
|
|
341
|
+
if (input.mutation.kind !== "set_desired_state" && input.mutation.kind !== "grant_routine_action" && input.mutation.kind !== "correct_provenance")
|
|
326
342
|
throw new Error("steward policy mutation kind is invalid");
|
|
327
343
|
return (0, session_transaction_1.withImmediateSessionTurnLease)(policyPath(agentRoot), (lease) => {
|
|
328
344
|
const snapshot = readAuditedPolicy(agentRoot, lease);
|
|
@@ -330,14 +346,21 @@ function updateStewardPolicy(agentRoot, input) {
|
|
|
330
346
|
const now = input.now ?? new Date().toISOString();
|
|
331
347
|
if (!canonicalTime(now))
|
|
332
348
|
throw new Error("policy update time must be canonical");
|
|
333
|
-
const expiresAt = optionalExpiry(input.mutation.expiresAt);
|
|
349
|
+
const expiresAt = input.mutation.kind === "correct_provenance" ? undefined : optionalExpiry(input.mutation.expiresAt);
|
|
334
350
|
const version = current.version + 1;
|
|
335
351
|
if (!positiveInteger(version))
|
|
336
352
|
throw new Error("steward policy version is exhausted");
|
|
337
353
|
const next = { ...current, version, desiredStates: { ...current.desiredStates }, routineActionGrants: { ...current.routineActionGrants }, updatedAt: now };
|
|
338
|
-
const key = requireText(input.mutation.key, input.mutation.kind === "
|
|
354
|
+
const key = requireText(input.mutation.key, input.mutation.kind === "grant_routine_action" ? "routine action key" : "desired state key");
|
|
339
355
|
let affectedKeyResult;
|
|
340
|
-
if (input.mutation.kind === "
|
|
356
|
+
if (input.mutation.kind === "correct_provenance") {
|
|
357
|
+
const existing = current.desiredStates[key];
|
|
358
|
+
if (!existing)
|
|
359
|
+
throw new Error("provenance correction requires an existing desired state key");
|
|
360
|
+
affectedKeyResult = { ...existing, source: requireText(input.mutation.source, "corrected source"), correction: requireText(input.mutation.correction, "correction"), version };
|
|
361
|
+
next.desiredStates = { ...current.desiredStates, [key]: affectedKeyResult };
|
|
362
|
+
}
|
|
363
|
+
else if (input.mutation.kind === "set_desired_state") {
|
|
341
364
|
if (input.mutation.provenance !== "stated" && input.mutation.provenance !== "observed" && input.mutation.provenance !== "default")
|
|
342
365
|
throw new Error("desired state provenance is invalid");
|
|
343
366
|
affectedKeyResult = {
|
|
@@ -373,7 +396,7 @@ function updateStewardPolicy(agentRoot, input) {
|
|
|
373
396
|
const previousIndex = snapshot.rows.findIndex((row) => operationIdentity(row.issuer, row.authorizingSessionEvent, row.authorization.requestId, row.mutationKind, row.key) === identity);
|
|
374
397
|
if (previousIndex >= 0) {
|
|
375
398
|
const previous = snapshot.rows[previousIndex];
|
|
376
|
-
const currentResult = previous.mutationKind === "
|
|
399
|
+
const currentResult = previous.mutationKind === "grant_routine_action" ? current.routineActionGrants[key] : current.desiredStates[key];
|
|
377
400
|
if (previous.mutationFingerprint !== fingerprint || snapshot.rows.slice(previousIndex + 1).some((row) => row.mutationKind === previous.mutationKind && row.key === key)
|
|
378
401
|
|| !(0, node_util_1.isDeepStrictEqual)(currentResult, previous.affectedKeyResult))
|
|
379
402
|
throw new Error("steward policy transaction replay changed");
|
|
@@ -442,8 +465,8 @@ function currentRequester(value, target) {
|
|
|
442
465
|
&& text(value.event.recordPath) && text(value.event.agent) && positiveInteger(value.event.generation)
|
|
443
466
|
&& text(value.event.observationRevision) && text(value.event.claimOwner);
|
|
444
467
|
}
|
|
445
|
-
function appliedEntry(rows,
|
|
446
|
-
return rows.some((row) => row.mutationKind
|
|
468
|
+
function appliedEntry(rows, kinds, key, entry) {
|
|
469
|
+
return rows.some((row) => kinds.includes(row.mutationKind) && row.key === key && row.postimageVersion === entry.version && (0, node_util_1.isDeepStrictEqual)(row.affectedKeyResult, entry));
|
|
447
470
|
}
|
|
448
471
|
const UNRESOLVED_ACTION_STATES = new Set(["reserved", "attempting", "effect_acknowledged", "recovery_pending", "indeterminate"]);
|
|
449
472
|
function inspectRoutineActionGrantSnapshot(policy, rows, receipts, input) {
|
|
@@ -472,7 +495,7 @@ function inspectRoutineActionGrantSnapshot(policy, rows, receipts, input) {
|
|
|
472
495
|
return { allowed: false, reason: "routine action grant is missing", ...fallback };
|
|
473
496
|
if (grant.provenance !== "stated")
|
|
474
497
|
return { allowed: false, reason: "routine action grant must be owner-stated" };
|
|
475
|
-
if (!appliedEntry(rows, "grant_routine_action", input.key, grant))
|
|
498
|
+
if (!appliedEntry(rows, ["grant_routine_action"], input.key, grant))
|
|
476
499
|
return { allowed: false, reason: "routine action grant has no applied owner authorization" };
|
|
477
500
|
if (grant.action !== input.action)
|
|
478
501
|
return { allowed: false, reason: "routine action does not match the grant" };
|
|
@@ -480,7 +503,7 @@ function inspectRoutineActionGrantSnapshot(policy, rows, receipts, input) {
|
|
|
480
503
|
return { allowed: false, reason: "routine action target is not authorized" };
|
|
481
504
|
if (grant.expiresAt && Date.parse(grant.expiresAt) <= Date.parse(now))
|
|
482
505
|
return { allowed: false, reason: "routine action grant expired", ...fallback };
|
|
483
|
-
if (!activeDesired || desired.provenance !== "stated" || !appliedEntry(rows, "set_desired_state", desiredKey, desired))
|
|
506
|
+
if (!activeDesired || desired.provenance !== "stated" || !appliedEntry(rows, ["set_desired_state", "correct_provenance"], desiredKey, desired))
|
|
484
507
|
return { allowed: false, reason: "container has no active applied owner-stated desired state" };
|
|
485
508
|
if (!["on", "always_on", "expected_on"].includes(desiredValue) && !(desiredValue === "on_demand" && input.requester.kind !== "owner_event"))
|
|
486
509
|
return { allowed: false, reason: "container desired state does not authorize this request" };
|
|
@@ -16,16 +16,17 @@ exports.stewardPolicyToolDefinition = {
|
|
|
16
16
|
type: "function",
|
|
17
17
|
function: {
|
|
18
18
|
name: "steward_policy_manage",
|
|
19
|
-
description: "Read or update the household steward's typed desired-state and routine-action policy. Updates require the current authenticated owner Telegram request and fresh authorization. expectedVersion is optional; omit it to apply against the current policy version, or supply a version from read for explicit stale-write detection.",
|
|
19
|
+
description: "Read or update the household steward's typed desired-state and routine-action policy. Updates require the current authenticated owner Telegram request and fresh authorization. Replay identities can never update it. correct_provenance (key, source, correction) is only for fixing a wrong attribution on an existing desired state: it replaces the recorded source and records the correction note, never changes the value, and leaves an audit row; do not use it to change a value. expectedVersion is optional; omit it to apply against the current policy version, or supply a version from read for explicit stale-write detection.",
|
|
20
20
|
parameters: {
|
|
21
21
|
type: "object",
|
|
22
22
|
properties: {
|
|
23
|
-
action: { type: "string", enum: ["read", "set_desired_state", "grant_routine_action"] },
|
|
23
|
+
action: { type: "string", enum: ["read", "set_desired_state", "grant_routine_action", "correct_provenance"] },
|
|
24
24
|
expectedVersion: { type: "integer", minimum: 0, description: "Optional version returned by read. When omitted, the runtime uses the current policy version." },
|
|
25
25
|
key: { type: "string" },
|
|
26
26
|
value: { type: "string" },
|
|
27
27
|
provenance: { type: "string", enum: ["stated", "observed", "default", "installed_explicit_policy"] },
|
|
28
|
-
source: { type: "string" },
|
|
28
|
+
source: { type: "string", description: "Who or what stated this. For correct_provenance, the corrected attribution that replaces the recorded source." },
|
|
29
|
+
correction: { type: "string", description: "correct_provenance only: required note naming what is being corrected and why." },
|
|
29
30
|
routineAction: { type: "string" },
|
|
30
31
|
targetsJson: { type: "string", description: "JSON array of exact target names" },
|
|
31
32
|
maxCount: { type: "integer", minimum: 1 },
|
|
@@ -54,6 +55,7 @@ exports.stewardPolicyToolDefinition = {
|
|
|
54
55
|
throw new Error("steward policy mutation requires authenticated relationship authority");
|
|
55
56
|
const { friendId, trustLevel, sessionEventId } = actor;
|
|
56
57
|
const { profileId, requestId } = relationship;
|
|
58
|
+
(0, steward_policy_1.assertNotReplayIdentity)(ctx.agentRoot, [friendId, ctx.delegatedCommand?.delegateFriendId]);
|
|
57
59
|
const sessionKey = ctx.currentSession?.key;
|
|
58
60
|
const delegated = ctx.delegatedCommand;
|
|
59
61
|
// Two ways to hold owner authority: the owner's own Telegram turn, or a delegated
|
|
@@ -82,6 +84,9 @@ exports.stewardPolicyToolDefinition = {
|
|
|
82
84
|
throw new Error("desired state provenance is invalid");
|
|
83
85
|
mutation = { kind: "set_desired_state", key: args.key ?? "", value: args.value ?? "", provenance: args.provenance, source: sourceFor(args.source), ...(args.expiresAt ? { expiresAt: args.expiresAt } : {}) };
|
|
84
86
|
}
|
|
87
|
+
else if (args.action === "correct_provenance") {
|
|
88
|
+
mutation = { kind: "correct_provenance", key: args.key ?? "", source: sourceFor(args.source), correction: args.correction ?? "" };
|
|
89
|
+
}
|
|
85
90
|
else if (args.action === "grant_routine_action") {
|
|
86
91
|
if (args.provenance !== "stated" && args.provenance !== "installed_explicit_policy")
|
|
87
92
|
throw new Error("routine action provenance is invalid");
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.874",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.874",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|