@inetafrica/open-claudia 2.13.0 → 2.13.1

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.13.1
4
+ - **Guardrail no longer mistakes the owner for an external speaker on `/link`'d or env-unified channels (Phase 3 fix).** v2.13.0's relationship classifier resolved ownership through `people.findByHandle` alone — the handles physically listed on the owner's person record — while the queue/state/run-lock resolve it through `identity.canonicalForChannel`, which also honours explicit `/link`s (`identities.json`) and the configured-owner env path. A channel the owner had unified by `/link` or env-config (e.g. a second Kazee handle) was therefore *owner* to the run-lock (shared queue → "Queued.") but *external* to the guardrail (default-deny → the stranger reply), so the bot could greet its own owner as a limited-access outsider. The classifier now resolves ownership through **canonical identity — the same source of truth the rest of the bot uses**: a channel is the owner's when it resolves to the owner canonical **or** its person record is `isOwner`. New `identity.channelIsOwner()`; `relationship.speakerFor` consults it before default-deny. Every guard consumer (external-mode system-prompt block, `io`/`loopback` egress gates, runner turn-guard, CLI guards) funnels through this one function, so the fix lands everywhere at once.
5
+ - **R1 preserved — a note still can never confer owner.** `channelIsOwner` is driven only by the canonical map, which is written **exclusively by owner/operator actions** (`/link`, `.env`), never by a note; and the owner stays anchored to a real owner record via `ownerCanonical → people.isOwner`. The default-deny posture for genuinely external or unmapped channels is unchanged.
6
+ - **Tests.** `test-relationship.js` gains a regression reproducing the exact production bug — an owner Kazee channel `/link`'d to the owner canonical but **absent from the people record** now resolves to `owner` (unguarded), while an unmapped channel on the same transport stays `external` (guarded), proving the fix does not widen access. Full `npm test` green.
7
+ - **Agent-space / OpenClaw:** no new deps, no new env, no schema change; Docker image builds identically. Purely a correctness fix to owner recognition — inert for deployments that never hit the split-brain.
8
+
3
9
  ## v2.13.0
4
10
  - **The independent relationship guardrail is now LIVE — an owned bot is finally safe to point at external people (Phase 3).** Phases 0–2 unified the bot's identity across channels and taught it WHO it's talking to; this phase adds the missing half: hard limits on what it may do or say when that person is **not** the owner. Everything is a strict **no-op for the owner and for no-context callers** (CLI/cron/tests) — the owner reply/tool/file paths stay **byte-for-byte unchanged** (risk R9/R14), so this is inert on every existing single-owner deployment until an external channel is actually `/auth`'d. Classification lives in `core/relationship.js` and is **cheap, synchronous, and never derived from message text**: the owner is owner **only** via `people.isOwner`; a non-owner whose note claims relationship `owner` is coerced to `external` (**R1 — a note can never confer owner powers**); every unclassified or unmapped authed non-owner defaults to `external` (**default-deny**).
5
11
  - **Owner-authored mandate, set from chat (3.1).** `entity persona <name> --relationship|--role|--style|--knows|--mandate` lets the owner declare, per person, the ONLY things the bot is cleared to do or discuss with them. The verb is **owner-gated** (an external speaker can never edit their own mandate — self-escalation is refused) and each flag replaces its section wholesale.
package/core/identity.js CHANGED
@@ -69,6 +69,23 @@ function ownerCanonical() {
69
69
  return "";
70
70
  }
71
71
 
72
+ // True when a channel resolves to the owner's canonical identity — the shared
73
+ // source of truth the queue/state/run-lock already uses. The relationship
74
+ // guardrail leans on this so the owner is recognised on EVERY channel they've
75
+ // unified: a people-record handle, an explicit /link (identities.json), or the
76
+ // configured-owner env path. Resolving ownership through the people-record
77
+ // handle list alone is too narrow — a channel unified by /link or env-config is
78
+ // never mirrored onto the record, so the owner would be falsely classed
79
+ // external there. The canonical map is only ever written by owner/operator
80
+ // actions, so this can never let a note confer owner (R1 preserved — a note
81
+ // touches neither identities.json nor .env).
82
+ function channelIsOwner(transport, channelId) {
83
+ const canonical = normalizeCanonicalUserId(canonicalForChannel(transport, channelId));
84
+ if (!canonical) return false;
85
+ const owner = normalizeCanonicalUserId(ownerCanonical());
86
+ return !!owner && canonical === owner;
87
+ }
88
+
72
89
  function canonicalForChannel(transport, channelId) {
73
90
  const key = channelKey(transport, channelId);
74
91
  const explicit = normalizeCanonicalUserId(identities.channels[key]);
@@ -127,6 +144,7 @@ module.exports = {
127
144
  defaultCanonicalForChannel,
128
145
  isConfiguredOwnerChannel,
129
146
  ownerCanonical,
147
+ channelIsOwner,
130
148
  canonicalForChannel,
131
149
  canonicalForTelegram,
132
150
  canonicalForStoredUserKey,
@@ -18,18 +18,37 @@ function speakerFor(adapterType, channelId) {
18
18
  if (!adapterType || !channelId) return null;
19
19
  const people = require("./people");
20
20
  const person = people.findByHandle(adapterType, channelId);
21
- if (!person) {
22
- // Authed but not mapped to a person record treat as external.
21
+
22
+ // Ownership is resolved through canonical identity the SAME source of
23
+ // truth the queue/state/run-lock uses — so the owner is recognised on every
24
+ // channel they've unified (a people handle, an explicit /link, or the
25
+ // configured-owner env path), not just channels physically mirrored onto
26
+ // the people record. Consulting only findByHandle is too narrow: a channel
27
+ // unified by /link or env-config is never written back to the record, so the
28
+ // owner would be falsely classed external there. channelIsOwner is driven
29
+ // only by owner/operator-written identity links (identities.json / .env), so
30
+ // a note can never trip it (R1 preserved — the owner stays anchored to a
31
+ // real owner record via ownerCanonical → people.isOwner).
32
+ let ownerByCanonical = false;
33
+ try { ownerByCanonical = require("./identity").channelIsOwner(adapterType, channelId); } catch (e) {}
34
+
35
+ if (ownerByCanonical || (person && person.isOwner)) {
36
+ let ownerPerson = person && person.isOwner ? person : null;
37
+ if (!ownerPerson) { try { ownerPerson = (people.owners() || [])[0] || null; } catch (e) {} }
38
+ const name = ownerPerson ? ownerPerson.name : (person ? person.name : "");
23
39
  return {
24
- person: null, name: "", isOwner: false, relationship: "external",
25
- slug: null, mandate: "", style: "", entityName: "",
40
+ person: ownerPerson || person || null, name, isOwner: true, relationship: "owner",
41
+ slug: null, mandate: "", style: "", entityName: name,
26
42
  adapterType: String(adapterType), channelId: String(channelId),
27
43
  };
28
44
  }
29
- if (person.isOwner) {
45
+
46
+ if (!person) {
47
+ // Authed but not mapped to a person record and not an owner-canonical
48
+ // channel → treat as external (default-deny).
30
49
  return {
31
- person, name: person.name, isOwner: true, relationship: "owner",
32
- slug: null, mandate: "", style: "", entityName: person.name,
50
+ person: null, name: "", isOwner: false, relationship: "external",
51
+ slug: null, mandate: "", style: "", entityName: "",
33
52
  adapterType: String(adapterType), channelId: String(channelId),
34
53
  };
35
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.13.0",
3
+ "version": "2.13.1",
4
4
  "description": "Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram or Kazee Chat",
5
5
  "main": "bot.js",
6
6
  "bin": {
@@ -119,4 +119,28 @@ assert.ok(target, "ownerTarget resolves an owner handle");
119
119
  assert.strictEqual(target.adapter, "telegram", "ownerTarget adapter");
120
120
  assert.strictEqual(target.channelId, CH_OWNER_ALT, "ownerTarget picks the primary channel, not the first");
121
121
 
122
+ // ── split-brain fix: an owner channel unified via /link (identities.json) but
123
+ // NEVER mirrored onto the people record must STILL resolve to the owner. This
124
+ // is the production bug — a Kazee handle /link'd to the owner's canonical id was
125
+ // invisible to people.findByHandle, so the guardrail falsely treated the owner
126
+ // as an external speaker. Ownership now flows through canonical identity. ──
127
+ const identity = require("./core/identity");
128
+ const ownerCanon = identity.ownerCanonical();
129
+ assert.ok(ownerCanon, "owner has a canonical id");
130
+ identity.setIdentityMapping("kazee", "owner-linked-kazee", ownerCanon);
131
+ assert.strictEqual(people.findByHandle("kazee", "owner-linked-kazee"), null,
132
+ "precondition: the /link'd kazee channel is absent from the people record");
133
+ const linkedSp = relationship.speakerFor("kazee", "owner-linked-kazee");
134
+ assert.strictEqual(linkedSp.isOwner, true, "owner-canonical channel → owner even without a people handle");
135
+ assert.strictEqual(linkedSp.relationship, "owner", "owner-canonical channel relationship");
136
+ assert.strictEqual(linkedSp.mandate, "", "owner never carries a mandate");
137
+ assert.strictEqual(relationship.guardActive(linkedSp), false, "owner-canonical channel is never guarded");
138
+
139
+ // ── and the fix must NOT widen: an unmapped channel on the same transport that
140
+ // does not resolve to the owner canonical stays external + guarded. ──
141
+ const strayKazee = relationship.speakerFor("kazee", "some-stranger-999");
142
+ assert.strictEqual(strayKazee.isOwner, false, "unmapped kazee channel is not owner");
143
+ assert.strictEqual(strayKazee.relationship, "external", "unmapped kazee channel → external");
144
+ assert.strictEqual(relationship.guardActive(strayKazee), true, "unmapped kazee channel stays guarded");
145
+
122
146
  console.log("relationship OK");