Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions test/real-agent-demo.mjs → scripts/real-agent-demo.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
// Real MCP-driven request_confirmation demo.
// NOT a test: it posts a REAL confirmation card to the room and waits up to
// 10 min. It lived in test/, where a bare `node --test` picked it up and sent
// petrus two live "deploy to production?" cards on 7 Oct 2026. Run by hand.
//
// Acts as a Claude Code agent: spawns the IAK MCP server, calls
// request_confirmation as it would in production, blocks until the user
Expand Down
9 changes: 9 additions & 0 deletions src/confirmations.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2210,6 +2210,15 @@ export function startChatReplyPoller({ apiKey, room, intervalMs = 5000, log, own
// to owners is the fix, silence never is.
if (!ownerSet.has(sender)) {
emit(`${text} from ${m.from}: sender is not the owner — ignoring`);
// Same multi-poller rule as the unknown-intent branch below: only
// the poller that HOLDS the intent may tell anyone it failed. On
// 7 Oct 2026 petrus approved 2806f59b; the Mini's poller (which
// holds it, and lists him as owner) settled it, while this
// MacBook's poller (no owners configured, never held the id)
// answered "NOT recorded" in the room. Refusing is right; the
// public failure notice from a poller that does not own the
// intent is a false alarm.
if (!getIntent(match[2])) continue;
// Answer only senders who plausibly ARE the owner (`petrus`,
// `petrus-boox`, a future `petrus-watch`). claudeMB's review caught
// that replying to everything amplifies the very misbehaviour this
Expand Down
34 changes: 34 additions & 0 deletions test/confirmations.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1052,3 +1052,37 @@ test('the HTML queue renders the server wording, and never calls an unknown outc
server.close();
}
});

test('chat-reply poller: a non-owner /approve for an intent held elsewhere stays silent', async () => {
// 7 Oct 2026: a second poller with no owners and no such intent posted
// "NOT recorded" over an approval the owning poller had already settled.
_resetForTests();
const batches = [
{ messages: [] },
{ messages: [
{ id: 'x1', from: 'petrus', body: '/approve 2806f59b', isHuman: true },
] },
];
let call = 0;
const posts = [];
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url, opts) => {
if (opts && opts.method === 'POST') { posts.push(JSON.parse(opts.body)); return { ok: true, json: async () => ({}) }; }
const batch = batches[Math.min(call++, batches.length - 1)];
return { ok: true, json: async () => batch };
};
const lines = [];
const handle = startChatReplyPoller({
apiKey: 'k', room: 'r', intervalMs: 10,
owners: [],
log: (m) => lines.push(m),
});
try {
await new Promise((r) => setTimeout(r, 120));
} finally {
clearInterval(handle);
globalThis.fetch = originalFetch;
}
assert.equal(posts.length, 0, 'no room reply for an intent this poller does not hold');
assert.match(lines.join('\n'), /not the owner/, 'the refusal is still logged');
});
Loading