Skip to content

Commit 89e7d18

Browse files
ThinkOffAppclaude
andcommitted
v0.8.1 — per-agent chat author attribution for confirmations
@Petrus chose option 2 from the 'why are confirmations all from @claudemb?' question: per-agent API key map + from_handle override on POST /intent. Changes: - createIntent({fromHandle}) — new optional originator handle param, passed through composeAnnouncers to the announcer. - makeGroupmindAnnouncer({apiKey, apiKeys}) — apiKeys map of '@handle' → API key. When fromHandle matches a key in the map, the chat post is authored as that agent rather than the daemon owner. Falls back to default apiKey on no match. - POST /intent body — accepts from_handle field; forwarding daemons (claudemm mini, Codex mini) include it so chat posts show the originating agent. - Daemon config: mcp.confirmations.api_keys = {'@CodexMB':'...', ...} to enable cross-agent attribution. When mini's daemon becomes the central registry (next step), the daemon owner just configures api_keys with each forwarding agent's GroupMind key and the chat headers become accurate. Tests: 76/76 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b3d0883 commit 89e7d18

3 files changed

Lines changed: 23 additions & 5 deletions

File tree

bin/iak-mcp-daemon.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ const serverAnnouncerMap = {};
4646
if (cc.room && apiKey) {
4747
serverAnnouncerMap.groupmind = makeGroupmindAnnouncer({
4848
apiKey, room: cc.room, callbackBase: cc.callback_base || `http://127.0.0.1:${cc.port || 8788}`,
49+
// Per-agent author attribution: configure
50+
// `mcp.confirmations.api_keys` as { "@CodexMB": "xfb_...", ... }
51+
// and forwarding daemons that include `from_handle` in POST /intent
52+
// bodies will have their announcement authored by that agent.
53+
apiKeys: cc.api_keys || {},
4954
});
5055
}
5156
if (cc.codewatch_gate_url) {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ide-agent-kit",
3-
"version": "0.7.5",
3+
"version": "0.8.1",
44
"description": "Built for OpenClaw workflows \u2014 ACP session orchestration, room-triggered automation, comment polling, Discord + Moltbook + GitHub connectors, receipts, exec approvals",
55
"type": "module",
66
"bin": {

src/confirmations.mjs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ export async function createIntent({
101101
timeoutSec = 600,
102102
announce = async () => {},
103103
receiptsPath,
104+
fromHandle, // optional originator handle (e.g. "@CodexMB") for per-agent
105+
// chat-author attribution; passed through to announcers.
104106
}) {
105107
const id = randomUUID().slice(0, 8);
106108
const intent = {
@@ -120,7 +122,7 @@ export async function createIntent({
120122
});
121123
// Side effects — never let an announce failure block the intent itself.
122124
try {
123-
await announce({ id, prompt, session, channels });
125+
await announce({ id, prompt, session, channels, fromHandle });
124126
} catch (e) {
125127
postReceipt(receiptsPath, {
126128
kind: 'intent.announce_failed', id, error: e.message,
@@ -228,6 +230,10 @@ export function startConfirmationsServer({
228230
channels: Array.isArray(payload.channels) ? payload.channels : (announce ? ['groupmind'] : []),
229231
announce: announce || (async () => {}),
230232
receiptsPath,
233+
// Forwarding daemons (claudemm mini, Codex mini) include
234+
// `from_handle` so the GroupMind announcer authors the chat
235+
// post as the originating agent rather than the daemon owner.
236+
fromHandle: typeof payload.from_handle === 'string' ? payload.from_handle : undefined,
231237
});
232238
res.writeHead(201, { 'Content-Type': 'application/json' });
233239
res.end(JSON.stringify({ ok: true, id }));
@@ -384,9 +390,16 @@ function renderIntentsHtml() {
384390
// Post the intent prompt to a GroupMind room with quick-reply text the user
385391
// can copy / type, and a curl example for the watch-gate. Idempotent (same
386392
// id is harmless).
387-
export function makeGroupmindAnnouncer({ apiKey, room, callbackBase }) {
388-
return async ({ id, prompt, session }) => {
393+
export function makeGroupmindAnnouncer({ apiKey, room, callbackBase, apiKeys }) {
394+
// apiKeys: optional map of agent handle (e.g. "@claudemm") → API key.
395+
// When the intent payload includes `fromHandle`, the announcer uses
396+
// the matching key from this map so the chat post is authored by the
397+
// ORIGINATING agent rather than always by the daemon owner.
398+
// Falls back to the default `apiKey` when no match is found.
399+
return async ({ id, prompt, session, fromHandle }) => {
389400
if (!apiKey || !room) return;
401+
// Per-agent key override.
402+
const effectiveKey = (fromHandle && apiKeys && apiKeys[fromHandle]) || apiKey;
390403
const uiLink = callbackBase ? `${callbackBase}/` : null;
391404
const body =
392405
`[Confirmation needed] **${prompt}**\n` +
@@ -409,7 +422,7 @@ export function makeGroupmindAnnouncer({ apiKey, room, callbackBase }) {
409422
return new Promise((resolve, reject) => {
410423
const r = req.request(
411424
'https://groupmind.one/api/v1/messages',
412-
{ method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey } },
425+
{ method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': effectiveKey } },
413426
(res) => {
414427
// drain + resolve regardless; the message-id isn't useful here.
415428
res.resume();

0 commit comments

Comments
 (0)