@inetafrica/open-claudia 2.14.4 → 2.14.6
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.md +10 -0
- package/core/actions.js +3 -19
- package/core/channel-wizard.js +32 -0
- package/core/handlers.js +2 -17
- package/core/identity.js +20 -0
- package/core/people.js +25 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.14.6
|
|
4
|
+
- **`/channel add kazee` started from the inline "Add Kazee" button now completes — owner-capture confirm lands on the operator's channel, not on Kazee.** The wizard's owner-confirm prompt (*"Is this you — the owner?"*) is meant to go back to the channel the operator ran `/channel` from, via `channel-wizard.sayToOrigin`, which needs `session.origin` — captured from the envelope passed to `start()`. The typed `/channel add kazee` path (`handlers.js`) passed the envelope; the button path (`actions.js` `chn:add:kazee`) did **not**, so `origin` was null and the prompt fell back to ambient `io.send` scope — which, while processing the operator's inbound Kazee trigger message, is the Kazee room. The Yes/No/Skip buttons then rendered on Kazee, where a tap can never resolve the session: it's keyed by the operator's canonical id, but a Kazee tap resolves to the owner's *unlinked* Kazee user id (the very thing owner-capture is establishing) → `handleAction` finds no session → *"That setup step expired."* `actions.js` now passes the envelope to `wizard.start`, exactly like the typed path.
|
|
5
|
+
- **`/channel remove` and the inline "Remove <id>" button now share one teardown, so they can't drift.** v2.14.5 made the typed `/channel remove` a complete teardown (adapter + `.env` + `people.unlinkAdapterHandles` + `identity.removeTransportIdentities`), but the button (`actions.js` `chn:rm:`) still ran the pre-v2.14.5 version that cleared only `.env` — so removing via the button left exactly the handle/identity residue v2.14.5 was built to clear. The full teardown is now a single `channel-wizard.removeChannel(id)` called by both entry points; callers only guard `telegram` and format `r.summary` / `r.error`. Net −10 lines.
|
|
6
|
+
- **Agent-space / OpenClaw:** no new deps, no new env, no schema change; Docker image builds identically. Both fixes close divergences between the button and typed entry points of the owner-driven `/channel` flow — inert for deployments that never add/remove a channel. Full `npm test` green.
|
|
7
|
+
|
|
8
|
+
## v2.14.5
|
|
9
|
+
- **`/channel remove <id>` is now a complete teardown — a re-add starts from a genuinely clean slate.** Removal used to clear only the `.env` channel config (`CHANNELS` + the `KAZEE_*` keys), but the wizard's v2.14.4 owner-capture also links the owner's Kazee handle onto their people record (`people.linkHandle`) and mirrors an identity mapping into `identities.json` (`setIdentityMapping`). Those were left behind, so a subsequent `/channel add kazee` stacked on stale handle + identity state. `/channel remove` now also drops every handle for that transport across all people records (new `people.unlinkAdapterHandles`) and removes its identity mappings — channel→canonical links and preferred-channel pointers (new `identity.removeTransportIdentities`) — and clears the previously-orphaned `KAZEE_BOT_USER_ID`.
|
|
10
|
+
- **The cleanup goes through the in-memory stores, so disk and the running process stay consistent.** `identities.json` is loaded once at boot into a shared in-memory object (`identity.js`), and `saveIdentities()` re-serializes that whole object on every write. Because `router.js` calls `autoLinkOwnerChannel` → `setIdentityMapping` → `saveIdentities` on each owner message, any entry lingering in memory is rewritten to disk continuously — so an out-of-band file edit can't stick, and a partial teardown leaves ghosts that reappear. Removing through the in-memory maps (then persisting) means the teardown holds. The `/channel remove` reply now reports what it cleared (e.g. `Removed channel: kazee — cleared 1 handle + identity links`).
|
|
11
|
+
- **Agent-space / OpenClaw:** no new deps, no new env, no schema change; Docker image builds identically. Scoped entirely to the owner-driven `/channel remove` path — inert for deployments that never remove a channel. Full `npm test` green.
|
|
12
|
+
|
|
3
13
|
## v2.14.4
|
|
4
14
|
- **The owner no longer gets asked to double-approve their own Kazee reply — split-brain owner resolution is fixed.** On Kazee a person is identified by `userId` while the room is `channelId` (the chat-document); the two differ. The auth layer already recognised the owner correctly (via `access.matchesTransportOwner` → `currentUserId()`), but the independent relationship guardrail re-derived ownership from `channelId` — which never equals the configured `KAZEE_OWNER_USER_ID` — and so classed the owner as *external*, escalating the owner's own approval a second time. `relationship.speakerFor` now takes an optional third `userId` and keys identity/owner classification off `speakerId = userId || channelId` (the person), while still replying/escalating to `channelId` (the room). The raw per-user id is threaded to every subprocess egress guard via a new `OC_CHANNEL_USER_ID` env var + loopback query/body param (`bin/cli.js`, `bin/entity.js`, `bin/tool.js`, `bin/loopback-client.js`, `core/io.js`, `core/loopback.js`, `core/system-prompt.js`). **On Telegram `userId === channelId` in DMs, so this is a strict no-op** for existing Telegram deployments.
|
|
5
15
|
- **`/channel add kazee` now links the owner's Kazee handle at capture time.** Owner recognition previously lived only in the env/adapter (`KAZEE_OWNER_USER_ID`); the owner's people record had no Kazee handle, so the guardrail (which resolves through canonical identity) re-derived the operator as external. `finalizeOwner` now calls `people.linkHandle(owner.id, { adapter: "kazee", channelId: ownerUserId, … })` so the wizard's captured owner is recognised the same way the rest of the bot resolves identity — belt-and-suspenders with the `speakerFor` fix above.
|
package/core/actions.js
CHANGED
|
@@ -488,28 +488,12 @@ async function handleAction(envelope) {
|
|
|
488
488
|
if (d.startsWith("chn:")) {
|
|
489
489
|
if (!isChatOwner(envelope.channelId)) { await send("Owner only."); return; }
|
|
490
490
|
const wizard = require("./channel-wizard");
|
|
491
|
-
|
|
492
|
-
if (d === "chn:add:kazee") { await wizard.start(envelope.canonicalUserId, "kazee"); return; }
|
|
491
|
+
if (d === "chn:add:kazee") { await wizard.start(envelope.canonicalUserId, "kazee", envelope); return; }
|
|
493
492
|
if (d.startsWith("chn:rm:")) {
|
|
494
493
|
const id = d.slice("chn:rm:".length);
|
|
495
494
|
if (id === "telegram") { await send("Refusing to remove the Telegram adapter."); return; }
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
const { config, saveEnvKey } = require("./config");
|
|
499
|
-
const channels = (config.CHANNELS || "telegram").split(",").map((s) => s.trim()).filter(Boolean);
|
|
500
|
-
const kept = channels.filter((c) => c !== id && !c.startsWith(id + ":") && c !== id.split(":")[0]);
|
|
501
|
-
const next = kept.join(",") || "telegram";
|
|
502
|
-
saveEnvKey("CHANNELS", next);
|
|
503
|
-
config.CHANNELS = next;
|
|
504
|
-
if (id === "kazee") {
|
|
505
|
-
saveEnvKey("KAZEE_URL", "");
|
|
506
|
-
saveEnvKey("KAZEE_BOT_TOKEN", "");
|
|
507
|
-
saveEnvKey("KAZEE_OWNER_USER_ID", "");
|
|
508
|
-
config.KAZEE_URL = "";
|
|
509
|
-
config.KAZEE_BOT_TOKEN = "";
|
|
510
|
-
config.KAZEE_OWNER_USER_ID = "";
|
|
511
|
-
}
|
|
512
|
-
await send(`Removed channel: ${id}`);
|
|
495
|
+
const r = await wizard.removeChannel(id);
|
|
496
|
+
await send(r.ok ? r.summary : r.error);
|
|
513
497
|
return;
|
|
514
498
|
}
|
|
515
499
|
}
|
package/core/channel-wizard.js
CHANGED
|
@@ -325,6 +325,37 @@ async function finalizeOwner(canonicalUserId, session, ownerUserId) {
|
|
|
325
325
|
);
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
// Full teardown for a live channel, shared by the /channel remove command and
|
|
329
|
+
// the inline "Remove <id>" button so the two entry points can't drift. Removes
|
|
330
|
+
// the adapter, strips its keys from .env (so the change survives restart), and
|
|
331
|
+
// drops its handles + identity mappings through the in-memory stores — a stale
|
|
332
|
+
// handle/link otherwise survives in memory and gets rewritten on the next add,
|
|
333
|
+
// which is what made a re-add feel un-clean. Callers guard "telegram" (their
|
|
334
|
+
// own connection) and format r.summary / r.error for their channel.
|
|
335
|
+
async function removeChannel(id) {
|
|
336
|
+
const result = await registry.removeAdapter(id);
|
|
337
|
+
if (!result.ok) return { ok: false, error: result.error };
|
|
338
|
+
const transport = id.split(":")[0];
|
|
339
|
+
const channels = (config.CHANNELS || "telegram").split(",").map((s) => s.trim()).filter(Boolean);
|
|
340
|
+
const kept = channels.filter((c) => c !== id && !c.startsWith(id + ":") && c !== transport);
|
|
341
|
+
saveEnvKey("CHANNELS", kept.join(",") || "telegram");
|
|
342
|
+
config.CHANNELS = kept.join(",") || "telegram";
|
|
343
|
+
if (transport === "kazee") {
|
|
344
|
+
for (const k of ["KAZEE_URL", "KAZEE_BOT_TOKEN", "KAZEE_OWNER_USER_ID", "KAZEE_BOT_USER_ID"]) {
|
|
345
|
+
saveEnvKey(k, "");
|
|
346
|
+
config[k] = "";
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
let unlinked = 0;
|
|
350
|
+
try { unlinked = people.unlinkAdapterHandles(transport).length; } catch (e) {}
|
|
351
|
+
let idsCleared = false;
|
|
352
|
+
try { idsCleared = require("./identity").removeTransportIdentities(transport); } catch (e) {}
|
|
353
|
+
const extra = (unlinked || idsCleared)
|
|
354
|
+
? ` — cleared ${unlinked} handle${unlinked === 1 ? "" : "s"} + identity links`
|
|
355
|
+
: "";
|
|
356
|
+
return { ok: true, id, transport, unlinked, idsCleared, summary: `Removed channel: ${id}${extra}` };
|
|
357
|
+
}
|
|
358
|
+
|
|
328
359
|
module.exports = {
|
|
329
360
|
isAwaiting,
|
|
330
361
|
getSession,
|
|
@@ -334,5 +365,6 @@ module.exports = {
|
|
|
334
365
|
handleText,
|
|
335
366
|
handleAction,
|
|
336
367
|
tryCaptureOwner,
|
|
368
|
+
removeChannel,
|
|
337
369
|
validateKazee,
|
|
338
370
|
};
|
package/core/handlers.js
CHANGED
|
@@ -1672,23 +1672,8 @@ register({
|
|
|
1672
1672
|
if (removeMatch) {
|
|
1673
1673
|
const id = removeMatch[1];
|
|
1674
1674
|
if (id === "telegram") return send("Refusing to remove the Telegram adapter from a /channel command — that's your own connection.");
|
|
1675
|
-
const
|
|
1676
|
-
|
|
1677
|
-
// Strip from .env so the change survives restart.
|
|
1678
|
-
const channels = (config.CHANNELS || "telegram").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1679
|
-
const kept = channels.filter((c) => c !== id && !c.startsWith(id + ":") && c !== id.split(":")[0]);
|
|
1680
|
-
const next = kept.join(",") || "telegram";
|
|
1681
|
-
saveEnvKey("CHANNELS", next);
|
|
1682
|
-
config.CHANNELS = next;
|
|
1683
|
-
if (id === "kazee") {
|
|
1684
|
-
saveEnvKey("KAZEE_URL", "");
|
|
1685
|
-
saveEnvKey("KAZEE_BOT_TOKEN", "");
|
|
1686
|
-
saveEnvKey("KAZEE_OWNER_USER_ID", "");
|
|
1687
|
-
config.KAZEE_URL = "";
|
|
1688
|
-
config.KAZEE_BOT_TOKEN = "";
|
|
1689
|
-
config.KAZEE_OWNER_USER_ID = "";
|
|
1690
|
-
}
|
|
1691
|
-
return send(`Removed channel: ${id}`);
|
|
1675
|
+
const r = await wizard.removeChannel(id);
|
|
1676
|
+
return send(r.ok ? r.summary : r.error);
|
|
1692
1677
|
}
|
|
1693
1678
|
|
|
1694
1679
|
send("Usage: /channel | /channel add kazee | /channel remove <id>");
|
package/core/identity.js
CHANGED
|
@@ -137,6 +137,25 @@ function setIdentityMapping(transport, channelId, canonicalUserId) {
|
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
// Remove every identity mapping for a transport — used when a channel is torn
|
|
141
|
+
// down (/channel remove) so no stale channel→canonical link or preferred-channel
|
|
142
|
+
// pointer lingers to be rewritten to disk on the next identity write. Mutates
|
|
143
|
+
// the in-memory map the whole process shares, THEN persists, so memory and disk
|
|
144
|
+
// stay in sync (a disk-only edit would be resurrected by the cached object).
|
|
145
|
+
function removeTransportIdentities(transport) {
|
|
146
|
+
const t = String(transport || "").toLowerCase();
|
|
147
|
+
if (!t) return false;
|
|
148
|
+
let changed = false;
|
|
149
|
+
for (const key of Object.keys(identities.channels)) {
|
|
150
|
+
if (key.slice(0, t.length + 1) === t + ":") { delete identities.channels[key]; changed = true; }
|
|
151
|
+
}
|
|
152
|
+
for (const [userId, pref] of Object.entries(identities.preferred)) {
|
|
153
|
+
if (pref && String(pref.transport || "").toLowerCase() === t) { delete identities.preferred[userId]; changed = true; }
|
|
154
|
+
}
|
|
155
|
+
if (changed) saveIdentities();
|
|
156
|
+
return changed;
|
|
157
|
+
}
|
|
158
|
+
|
|
140
159
|
module.exports = {
|
|
141
160
|
identities,
|
|
142
161
|
normalizeCanonicalUserId,
|
|
@@ -149,5 +168,6 @@ module.exports = {
|
|
|
149
168
|
canonicalForTelegram,
|
|
150
169
|
canonicalForStoredUserKey,
|
|
151
170
|
setIdentityMapping,
|
|
171
|
+
removeTransportIdentities,
|
|
152
172
|
saveIdentities,
|
|
153
173
|
};
|
package/core/people.js
CHANGED
|
@@ -159,6 +159,30 @@ function unlinkHandle(personId, { adapter, channelId }) {
|
|
|
159
159
|
return p;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// Remove every handle for a given adapter across ALL people records — used when
|
|
163
|
+
// a channel is torn down (/channel remove) so no stranded handle survives to be
|
|
164
|
+
// re-linked or re-mirrored on a later add. primaryChannel falls back to the next
|
|
165
|
+
// remaining handle. Returns the ids of the people that were changed.
|
|
166
|
+
function unlinkAdapterHandles(adapter) {
|
|
167
|
+
const a = String(adapter || "").toLowerCase();
|
|
168
|
+
if (!a) return [];
|
|
169
|
+
const all = load();
|
|
170
|
+
const affected = [];
|
|
171
|
+
for (const p of all) {
|
|
172
|
+
const before = (p.handles || []).length;
|
|
173
|
+
if (!before) continue;
|
|
174
|
+
p.handles = p.handles.filter((h) => h.adapter !== a);
|
|
175
|
+
if (p.handles.length === before) continue;
|
|
176
|
+
if (p.primaryChannel && p.primaryChannel.adapter === a) {
|
|
177
|
+
p.primaryChannel = p.handles[0] ? { adapter: p.handles[0].adapter, channelId: p.handles[0].channelId } : null;
|
|
178
|
+
}
|
|
179
|
+
p.updatedAt = nowIso();
|
|
180
|
+
affected.push(p.id);
|
|
181
|
+
}
|
|
182
|
+
if (affected.length) save(all);
|
|
183
|
+
return affected;
|
|
184
|
+
}
|
|
185
|
+
|
|
162
186
|
function setPrimary(personId, { adapter, channelId }) {
|
|
163
187
|
const all = load();
|
|
164
188
|
const p = all.find((x) => x.id === personId);
|
|
@@ -264,7 +288,7 @@ module.exports = {
|
|
|
264
288
|
load, save, list, roster,
|
|
265
289
|
add, update, remove,
|
|
266
290
|
findById, findByName, findByCanonicalUserId, findByHandle,
|
|
267
|
-
linkHandle, unlinkHandle, setPrimary,
|
|
291
|
+
linkHandle, unlinkHandle, unlinkAdapterHandles, setPrimary,
|
|
268
292
|
note, removeNote,
|
|
269
293
|
setEntitySlug, resolveEntitySlug,
|
|
270
294
|
owners, hasOwnerRecord, seedOwnerFromLegacy,
|
package/package.json
CHANGED