@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 +6 -0
- package/core/identity.js +18 -0
- package/core/relationship.js +26 -7
- package/package.json +1 -1
- package/test-relationship.js +24 -0
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,
|
package/core/relationship.js
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
32
|
-
slug: null, mandate: "", style: "", entityName:
|
|
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
package/test-relationship.js
CHANGED
|
@@ -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");
|