@ouro.bot/cli 0.1.0-alpha.870 → 0.1.0-alpha.871
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 +43 -8
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/steward-policy.js +7 -0
- package/dist/repertoire/tools-steward-policy.js +6 -0
- 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.871",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Restating an already-recorded steward desired state is now a no-op (no version bump, audit row or source rewrite) and the tool says unchanged; the replay gate's no-telegram check fails only on owner notices attributable to the replay run."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
4
10
|
{
|
|
5
11
|
"version": "0.1.0-alpha.870",
|
|
6
12
|
"changes": [
|
|
@@ -86,14 +86,40 @@ export function isStalledQueueItem(item, nowMs = Date.now()) {
|
|
|
86
86
|
return size > 0 && left > 0 && noEta && Number.isFinite(addedAt) && nowMs - addedAt >= STALL_MIN_AGE_HOURS * 3_600_000
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/** Telegram effect records written during [start, end] that
|
|
90
|
-
export function
|
|
89
|
+
/** Telegram effect records written during [start, end] that are owner notices, before attribution. */
|
|
90
|
+
export function ownerNoticeEffects(effects, start, end) {
|
|
91
91
|
return effects.filter((effect) => {
|
|
92
92
|
const at = Date.parse(effect.createdAt)
|
|
93
93
|
return Number.isFinite(at) && at >= start && at <= end && String(effect.idempotencyKey ?? "").startsWith("owner-notice:")
|
|
94
94
|
})
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
const AWAIT_NOTICE_KEY = /^owner-notice:await:(.+):(resolved|expired|asked_owner):(\d{4}-\d{2}-\d{2}T[^:]*:[^:]*:[^:]*Z)$/
|
|
98
|
+
const unquote = (value) => String(value ?? "").replace(/^["']|["']$/g, "")
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Splits the in-window owner notices into leaks (attributable to the replay run, or not attributable at all, which fails
|
|
102
|
+
* closed) and info (an await notice whose await was filed by a friend that is not a replay peer: the Butler's real owner
|
|
103
|
+
* work, which correctly reached Ari during the window). awaits: entries { name, createdAt, filedFor } from awaiting/ and
|
|
104
|
+
* awaiting/.done/. replayFriends: the replay peers' friend ids.
|
|
105
|
+
*/
|
|
106
|
+
export function classifyOwnerNotices(effects, start, end, { replayFriends = [], awaits = [] } = {}) {
|
|
107
|
+
const leaks = []
|
|
108
|
+
const info = []
|
|
109
|
+
for (const effect of ownerNoticeEffects(effects, start, end)) {
|
|
110
|
+
const key = String(effect.idempotencyKey)
|
|
111
|
+
if (key.startsWith("owner-notice:delegated:")) { leaks.push({ key, reason: "delegated command notice (replay peers are the only delegating senders)" }); continue }
|
|
112
|
+
const match = AWAIT_NOTICE_KEY.exec(key)
|
|
113
|
+
if (!match) { leaks.push({ key, reason: "unattributable owner notice" }); continue }
|
|
114
|
+
const [, name, , createdAt] = match
|
|
115
|
+
const found = awaits.find((entry) => String(entry.name).replace(/\.md$/, "") === name && unquote(entry.createdAt) === createdAt)
|
|
116
|
+
if (!found || !found.filedFor) { leaks.push({ key, reason: "await not found or has no filing friend" }); continue }
|
|
117
|
+
if (replayFriends.includes(found.filedFor)) leaks.push({ key, reason: `await filed by replay friend ${found.filedFor}` })
|
|
118
|
+
else info.push({ key, reason: `await filed by non-replay friend ${found.filedFor}` })
|
|
119
|
+
}
|
|
120
|
+
return { leaks, info }
|
|
121
|
+
}
|
|
122
|
+
|
|
97
123
|
// ---- the cases ---------------------------------------------------------------------------------------------------
|
|
98
124
|
// Each case: id, words, sender, delegated, optional applicable(before) -> reason|null, optional poll, readback(ctx) -> checks.
|
|
99
125
|
// ctx: { trace, reply, error, before, after, sink (lines inside this case's window), friends: { principal, stranger } }
|
|
@@ -278,10 +304,11 @@ export async function runSuite(host, { cases = CASES.map((entry) => entry.id), p
|
|
|
278
304
|
}
|
|
279
305
|
const runEnd = host.now()
|
|
280
306
|
const finalObservation = await host.observe()
|
|
281
|
-
const leaks =
|
|
307
|
+
const { leaks, info } = classifyOwnerNotices(finalObservation.effects, runStart, runEnd, { replayFriends: [host.friends.principal, host.friends.stranger], awaits: [...(finalObservation.awaiting ?? []), ...(finalObservation.done ?? [])] })
|
|
282
308
|
const readable = finalObservation.effectsReadable === true
|
|
283
309
|
const clean = readable && leaks.length === 0
|
|
284
|
-
const
|
|
310
|
+
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"
|
|
311
|
+
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)] }
|
|
285
312
|
const all = [...results, telegram]
|
|
286
313
|
const failed = all.filter((entry) => entry.status === "fail").map((entry) => entry.id)
|
|
287
314
|
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 } }
|
|
@@ -354,9 +381,17 @@ export function selfTest() {
|
|
|
354
381
|
const stalled = { trackedDownloadStatus: "warning" }
|
|
355
382
|
if (CASES.find((entry) => entry.id === "stall-kept").applicable(emptyObservation({ queue: [{ id: 1 }] })) === null) problems.push("stall-kept ran with no stalled item")
|
|
356
383
|
if (CASES.find((entry) => entry.id === "stall-kept").applicable(emptyObservation({ queue: [stalled] })) !== null) problems.push("stall-kept skipped with a stalled item")
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
384
|
+
const at = new Date(500).toISOString()
|
|
385
|
+
const created = "2026-10-08T03:03:00.876Z"
|
|
386
|
+
const ctx = { replayFriends: ["p", "s"], awaits: [{ name: "replay-wait.md", createdAt: created, filedFor: "p" }, { name: "chef_show_s2_landed.md", createdAt: created, filedFor: "real-friend" }] }
|
|
387
|
+
const classify = (key) => classifyOwnerNotices([{ idempotencyKey: key, createdAt: at }, { idempotencyKey: "other", createdAt: at }], 0, 1000, ctx)
|
|
388
|
+
if (classify("owner-notice:delegated:x").leaks.length !== 1) problems.push("no-telegram missed a replay-delegated owner notice")
|
|
389
|
+
if (classify(`owner-notice:await:replay-wait:asked_owner:${created}`).leaks.length !== 1) problems.push("no-telegram missed an await notice filed by a replay friend")
|
|
390
|
+
const real = classify(`owner-notice:await:chef_show_s2_landed:asked_owner:${created}`)
|
|
391
|
+
if (real.leaks.length !== 0 || real.info.length !== 1) problems.push("no-telegram did not pass a real owner await notice with an info detail")
|
|
392
|
+
if (classify("owner-notice:mystery").leaks.length !== 1) problems.push("no-telegram did not fail closed on an unknown owner-notice key")
|
|
393
|
+
if (classify(`owner-notice:await:missing:asked_owner:${created}`).leaks.length !== 1) problems.push("no-telegram did not fail closed on an await it could not find")
|
|
394
|
+
if (classifyOwnerNotices([{ idempotencyKey: "owner-notice:x", createdAt: new Date(5000).toISOString() }], 0, 1000, ctx).leaks.length !== 0) problems.push("no-telegram counted an out-of-window record")
|
|
360
395
|
return problems
|
|
361
396
|
}
|
|
362
397
|
|
|
@@ -378,7 +413,7 @@ export function makeHost({ bundle = DEFAULT_BUNDLE, cardUrl, log = console.error
|
|
|
378
413
|
const list = (dir) => (existsSync(dir) ? readdirSync(dir) : [])
|
|
379
414
|
const awaitEntries = (dir) => list(dir).filter((name) => name.endsWith(".md")).map((name) => {
|
|
380
415
|
const text = readFileSync(path.join(dir, name), "utf8")
|
|
381
|
-
return { name, status: /^status:\s*(\S+)/m.exec(text)?.[1] ?? "pending", createdAt: /^created_at:\s*(\S+)/m.exec(text)?.[1] ?? "", reason: /^cancel_reason:\s*(.+)$/m.exec(text)?.[1]?.trim() ?? "" }
|
|
416
|
+
return { name, status: /^status:\s*(\S+)/m.exec(text)?.[1] ?? "pending", createdAt: /^created_at:\s*(\S+)/m.exec(text)?.[1] ?? "", filedFor: unquote(/^filed_for_friend_id:\s*(\S+)/m.exec(text)?.[1] ?? ""), reason: /^cancel_reason:\s*(.+)$/m.exec(text)?.[1]?.trim() ?? "" }
|
|
382
417
|
})
|
|
383
418
|
return {
|
|
384
419
|
friends: { principal: provisioned.principal.friendId, stranger: provisioned.stranger.friendId },
|
|
@@ -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.871</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -381,6 +381,13 @@ function updateStewardPolicy(agentRoot, input) {
|
|
|
381
381
|
}
|
|
382
382
|
if (current.version !== input.expectedVersion)
|
|
383
383
|
throw new Error(`steward policy version changed: expected ${input.expectedVersion}, got ${current.version}`);
|
|
384
|
+
if (input.mutation.kind === "set_desired_state") {
|
|
385
|
+
const existing = current.desiredStates[key];
|
|
386
|
+
const sameValue = existing !== undefined && existing.provenance === input.mutation.provenance && existing.value === affectedKeyResult.value && existing.expiresAt === expiresAt;
|
|
387
|
+
// Restating what is already recorded is a no-op: no version bump, no audit row, no provenance rewrite.
|
|
388
|
+
if (sameValue)
|
|
389
|
+
return current;
|
|
390
|
+
}
|
|
384
391
|
optionalExpiry(expiresAt, now);
|
|
385
392
|
const postimage = JSON.stringify(next, null, 2);
|
|
386
393
|
if (Buffer.byteLength(postimage) > MAX_POLICY_BYTES)
|
|
@@ -117,6 +117,7 @@ exports.stewardPolicyToolDefinition = {
|
|
|
117
117
|
|| ctx.currentSession?.key !== sessionKey || !sessionMatches() || ctx.currentExternalEvent) {
|
|
118
118
|
throw new Error("steward policy owner authorization or session changed before mutation");
|
|
119
119
|
}
|
|
120
|
+
const versionBefore = (0, steward_policy_1.readStewardPolicy)(agentRoot).version;
|
|
120
121
|
const result = (0, steward_policy_1.updateStewardPolicy)(agentRoot, {
|
|
121
122
|
expectedVersion: expectedVersion ?? (0, steward_policy_1.readStewardPolicy)(agentRoot).version,
|
|
122
123
|
actor: {
|
|
@@ -127,6 +128,11 @@ exports.stewardPolicyToolDefinition = {
|
|
|
127
128
|
mutation,
|
|
128
129
|
});
|
|
129
130
|
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.steward_policy_tool_call", message: "updated steward policy", meta: { action: args.action } });
|
|
131
|
+
if (mutation.kind === "set_desired_state" && result.version === versionBefore) {
|
|
132
|
+
// A same-value restatement writes nothing; the entry is necessarily present.
|
|
133
|
+
const existing = result.desiredStates[mutation.key];
|
|
134
|
+
return JSON.stringify({ ...result, unchanged: true, note: `already recorded as ${existing.value} (source: ${existing.source})` });
|
|
135
|
+
}
|
|
130
136
|
return JSON.stringify(result);
|
|
131
137
|
});
|
|
132
138
|
},
|
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.871",
|
|
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.871",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|