@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 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 all = [...results, telegram]
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,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.873",
2
+ "runtimeVersion": "0.1.0-alpha.874",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-09-03T00:00:00.000Z"
5
5
  }
@@ -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.873</Repository>
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 === "set_desired_state" ? after.desiredStates[value.key] : after.routineActionGrants[value.key];
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 === "set_desired_state" ? { ...before.desiredStates, [value.key]: result } : before.desiredStates,
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 === "set_desired_state" ? "desired state key" : "routine action key");
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 === "set_desired_state") {
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 === "set_desired_state" ? current.desiredStates[key] : current.routineActionGrants[key];
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, kind, key, entry) {
446
- return rows.some((row) => row.mutationKind === kind && row.key === key && row.postimageVersion === entry.version && (0, node_util_1.isDeepStrictEqual)(row.affectedKeyResult, entry));
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");
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.873",
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.873",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.873",
3
+ "version": "0.1.0-alpha.874",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },