Skip to content
Open
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 bin/iak-mcp-daemon.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ if (!apiKey) {
} else {
startChatReplyPoller({
apiKey, room, intervalMs: 5000,
// Owner identities that may settle intents. A person is not one handle:
// petrus posts from a web session, a tablet and (soon) a watch.
owners: cc.owners,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Forward aliases from the in-process MCP server

This forwards mcp.confirmations.owners only when running the standalone daemon. When no daemon is detected, src/mcp-server.mjs starts the same poller in-process without passing confirmCfg.owners, so configured aliases silently fall back to ['petrus'] and a reply from @petrus-boox remains rejected in that supported deployment. Pass the configured owners through that startup path as well.

Useful? React with 👍 / 👎.

log: (msg) => console.log(`[iak-mcp-daemon] ${msg}`),
});
console.log(`[iak-mcp-daemon] chat-reply poller watching room "${room}" every 5s`);
Expand Down
18 changes: 16 additions & 2 deletions src/confirmations.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1065,11 +1065,20 @@ export function composeAnnouncers(map) {
//
// Logs go to stderr only (stdout is the MCP stdio protocol channel — writing
// there would corrupt it). Returns the interval handle so callers can stop it.
export function startChatReplyPoller({ apiKey, room, intervalMs = 5000, log }) {
// `owners` is an EXPLICIT list of handles allowed to settle intents. It is a
// list of exact names, deliberately not a prefix match: an agent can register
// any handle it likes, so `petrus-*` would let a fleet agent call itself
// "petrus-helper" and inherit approval authority — which is the precise attack
// this guard was written to stop.
export function startChatReplyPoller({ apiKey, room, intervalMs = 5000, log, owners = ['petrus'] }) {
if (!apiKey || !room) {
process.stderr.write('[iak-mcp] chat-reply poller: missing apiKey or room — disabled\n');
return null;
}
const ownerSet = new Set(
(Array.isArray(owners) && owners.length ? owners : ['petrus'])
.map((o) => String(o).replace(/^@/, '').toLowerCase())
);
const emit = log || ((msg) => process.stderr.write(`[iak-mcp] ${msg}\n`));
const seen = new Set();
let primed = false;
Expand All @@ -1092,8 +1101,13 @@ export function startChatReplyPoller({ apiKey, room, intervalMs = 5000, log }) {
// which this poller happily executed — any agent could approve any
// gated command. Agent senders carry a handle ("@ether", "hermes");
// the owner posts as plain "petrus" (CodeWatch button taps included).
// 2026-08-03: this used to compare against the literal "petrus", so a
// decision from petrus's own tablet ("@petrus-boox") was dropped in
// silence — he tapped Approve on camera and nothing happened. Owner
// identities are now configured, because a person is not one handle:
// they are a laptop, a tablet and a watch.
const sender = String(m.from || '').replace(/^@/, '').toLowerCase();
if (sender !== 'petrus' && m.isHuman !== true) {
if (!ownerSet.has(sender) && m.isHuman !== true) {
emit(`${text} from ${m.from}: sender is not the owner — ignoring`);
continue;
}
Expand Down
Loading