@inetafrica/open-claudia 2.14.2 → 2.14.4
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 +12 -0
- package/bin/cli.js +1 -0
- package/bin/entity.js +1 -1
- package/bin/loopback-client.js +2 -0
- package/bin/tool.js +1 -1
- package/core/channel-wizard.js +52 -4
- package/core/enforcer.js +0 -0
- package/core/io.js +2 -2
- package/core/loopback.js +8 -3
- package/core/relationship.js +13 -5
- package/core/runner.js +22 -4
- package/core/system-prompt.js +5 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.14.4
|
|
4
|
+
- **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
|
+
- **`/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.
|
|
6
|
+
- **Debug/internal chatter is owner-only.** Recall banners (`/recall`), tool/skill traces (`/tooltrace`), and usage/cost alerts are now suppressed for a guarded (external) speaker regardless of the toggle state — internal mechanics of how the assistant works must never leak to a non-owner. Resolved once per turn (`isCurrentSpeakerGuarded`) and applied at all three emitters (`core/runner.js`).
|
|
7
|
+
- **Escalation & approval prompts render properly on Telegram.** The external-guardrail Approve/Deny/Always-allow message (`core/enforcer.js`) and the destructive-tool approval prompt (`core/loopback.js`) build HTML (`<b>`/`<pre>`, all dynamic content `esc()`-escaped) but were sent without a parse mode, so the tags showed literally. They now set `parseMode: "HTML"` **only for the Telegram adapter** (other adapters ignore it).
|
|
8
|
+
- **Friendlier stale-button handling.** A wizard callback pressed after the session is gone (bot restart / timeout) previously did nothing — a dead button reads as a hang. It now replies *"That setup step expired — I'm no longer listening. Run /channel add kazee to start again."*
|
|
9
|
+
- **Agent-space / OpenClaw:** no new deps, no new required env (`OC_CHANNEL_USER_ID` is bot-internal, injected per-turn), no schema change; Docker image builds identically. Correctness fix to owner recognition on Kazee + polish to the onboarding wizard — inert for Telegram-only deployments (userId === channelId). Full `npm test` green.
|
|
10
|
+
|
|
11
|
+
## v2.14.3
|
|
12
|
+
- **`/channel add kazee` owner-confirm no longer depends on the inline button round-tripping.** After the wizard captures a candidate owner message it asks *"Is this you — the owner?"* with Yes/No/Skip buttons on the operator's channel — but if that button callback never reaches the bot (e.g. a channel that drops `message:action` server-side), the operator was stuck: the confirm step (`core/channel-wizard.js` `handleText`) only understood "skip" or a pasted 24-hex id, so typing "yes" did nothing. It now accepts **typed "yes"/"no"** (plus y/n/yep/yeah/nope) as a first-class answer — "yes" finalizes the captured candidate as owner, "no" clears it and keeps listening — and any other text nudges with the three options. The confirm prompt now reads *"Tap a button below, or just reply "yes" / "no"."*
|
|
13
|
+
- **Agent-space / OpenClaw:** no new deps, no new env, no schema change; Docker image builds identically. Purely a robustness fix to the Kazee onboarding wizard's owner-confirm step — inert for deployments that never run `/channel add kazee`.
|
|
14
|
+
|
|
3
15
|
## v2.14.2
|
|
4
16
|
- **Token validation now hits the right service — the previous probe could never succeed.** v2.14.0/.1 validated a Kazee bot token by `GET {host}/api/me`, but chat-central has **no `/me` route** (confirmed against `src/config/routes.ts`) — every attempt 404'd. chat-central doesn't resolve identity itself; it delegates on every request to `@libraries/inet-central-auth`, which POSTs the opaque `kzb_…` token to **Central's `POST /api/principal/verify`**. `core/kazee-probe.js` now calls that endpoint: one request both **validates the token and returns the bot's own user id** (`principal.id`, with `principal.kind === "Bot"` enforced) plus its display name. Verified live end-to-end (real token → `ok` + botUserId + "Maya Bot"; bad token → clean 401).
|
|
5
17
|
- **Central is hard-coded too (env-overridable).** New `CENTRAL_DEFAULT_URL = "https://central.inet.africa"` mirrors chat-central's own default; override with `CENTRAL_BASE_URL` for non-prod. The wizard now says "Verifying the bot token…" and greets the bot by name ("Connected as Maya Bot ✅"); `setup.js` prints "Verified as <name>."
|
package/bin/cli.js
CHANGED
|
@@ -60,6 +60,7 @@ function sendViaLoopback(kind, restArgs) {
|
|
|
60
60
|
const fileName = path.basename(filePath);
|
|
61
61
|
const stat = fs.statSync(filePath);
|
|
62
62
|
const params = new URLSearchParams({ channelId, adapter: adapterId, fileName });
|
|
63
|
+
if (process.env.OC_CHANNEL_USER_ID) params.set("userId", process.env.OC_CHANNEL_USER_ID);
|
|
63
64
|
if (caption) params.set("caption", caption);
|
|
64
65
|
|
|
65
66
|
const req = http.request({
|
package/bin/entity.js
CHANGED
|
@@ -71,7 +71,7 @@ function run(args) {
|
|
|
71
71
|
// owner person (R1) — a note can never confer owner powers.
|
|
72
72
|
try {
|
|
73
73
|
const relationship = require("../core/relationship");
|
|
74
|
-
const sp = relationship.speakerFor(process.env.OC_CHANNEL_ADAPTER, process.env.OC_CHANNEL_ID);
|
|
74
|
+
const sp = relationship.speakerFor(process.env.OC_CHANNEL_ADAPTER, process.env.OC_CHANNEL_ID, process.env.OC_CHANNEL_USER_ID);
|
|
75
75
|
if (relationship.guardActive(sp)) {
|
|
76
76
|
console.error("⛔ Only the owner can set a persona/mandate.");
|
|
77
77
|
process.exitCode = 3; return;
|
package/bin/loopback-client.js
CHANGED
|
@@ -36,6 +36,7 @@ function context() {
|
|
|
36
36
|
sendToken,
|
|
37
37
|
channelId: process.env.OC_CHANNEL_ID,
|
|
38
38
|
adapterId: process.env.OC_CHANNEL_ADAPTER,
|
|
39
|
+
userId: process.env.OC_CHANNEL_USER_ID || null,
|
|
39
40
|
canonicalUserId: process.env.OC_CANONICAL_USER_ID || null,
|
|
40
41
|
sessionId: process.env.OC_LAST_SESSION_ID || null,
|
|
41
42
|
sessionKey: process.env.OC_LAST_SESSION_KEY || null,
|
|
@@ -57,6 +58,7 @@ function postJson(kind, body) {
|
|
|
57
58
|
const payload = JSON.stringify({
|
|
58
59
|
channelId: ctx.channelId,
|
|
59
60
|
adapter: ctx.adapterId,
|
|
61
|
+
userId: ctx.userId,
|
|
60
62
|
canonicalUserId: ctx.canonicalUserId,
|
|
61
63
|
sessionId: ctx.sessionId,
|
|
62
64
|
sessionKey: ctx.sessionKey,
|
package/bin/tool.js
CHANGED
|
@@ -388,7 +388,7 @@ async function run(args) {
|
|
|
388
388
|
let speaker = null;
|
|
389
389
|
try {
|
|
390
390
|
const relationship = require("../core/relationship");
|
|
391
|
-
const sp = relationship.speakerFor(process.env.OC_CHANNEL_ADAPTER, process.env.OC_CHANNEL_ID);
|
|
391
|
+
const sp = relationship.speakerFor(process.env.OC_CHANNEL_ADAPTER, process.env.OC_CHANNEL_ID, process.env.OC_CHANNEL_USER_ID);
|
|
392
392
|
if (relationship.guardActive(sp)) speaker = sp; // else owner/no-context → ungated
|
|
393
393
|
} catch (e) { speaker = null; }
|
|
394
394
|
if (speaker) {
|
package/core/channel-wizard.js
CHANGED
|
@@ -20,6 +20,7 @@ const { send, deleteMessage } = require("./io");
|
|
|
20
20
|
const { publicCommands } = require("./commands");
|
|
21
21
|
const registry = require("./adapter-registry");
|
|
22
22
|
const { validateKazee } = require("./kazee-probe");
|
|
23
|
+
const people = require("./people");
|
|
23
24
|
|
|
24
25
|
const sessions = new Map(); // canonicalUserId -> session
|
|
25
26
|
|
|
@@ -122,8 +123,9 @@ async function handleText(envelope) {
|
|
|
122
123
|
return;
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
// Text fallback for step 3 — the operator can
|
|
126
|
-
// "skip" on their own channel instead of
|
|
126
|
+
// Text fallback for step 3 — the operator can type "yes"/"no" to answer the
|
|
127
|
+
// owner-confirm prompt, or a user id / "skip", on their own channel instead of
|
|
128
|
+
// relying on the inline buttons (which may not round-trip on every channel).
|
|
127
129
|
if (session.step === "awaiting-owner-message" || session.step === "confirming-owner") {
|
|
128
130
|
if (text.toLowerCase() === "skip") {
|
|
129
131
|
await finalizeOwner(envelope.canonicalUserId, session, "");
|
|
@@ -133,6 +135,30 @@ async function handleText(envelope) {
|
|
|
133
135
|
await finalizeOwner(envelope.canonicalUserId, session, text);
|
|
134
136
|
return;
|
|
135
137
|
}
|
|
138
|
+
if (session.step === "confirming-owner") {
|
|
139
|
+
const t = text.toLowerCase();
|
|
140
|
+
if (t === "yes" || t === "y" || t === "yep" || t === "yeah") {
|
|
141
|
+
const cand = session.data.candidate;
|
|
142
|
+
if (cand?.userId) {
|
|
143
|
+
await finalizeOwner(envelope.canonicalUserId, session, cand.userId);
|
|
144
|
+
} else {
|
|
145
|
+
session.step = "awaiting-owner-message";
|
|
146
|
+
await send("Lost that candidate — send another message from Kazee.", { keyboard: cancelKeyboard });
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (t === "no" || t === "n" || t === "nope") {
|
|
151
|
+
session.data.candidate = null;
|
|
152
|
+
session.step = "awaiting-owner-message";
|
|
153
|
+
await send("OK — still listening. Send a message from the owner's Kazee account.", { keyboard: cancelKeyboard });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
await send(
|
|
157
|
+
`Reply "yes" to confirm ${session.data.candidate?.name || "that account"} as the owner, "no" to keep listening, or "skip".`,
|
|
158
|
+
{ keyboard: cancelKeyboard },
|
|
159
|
+
);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
136
162
|
if (session.step === "awaiting-owner-message") {
|
|
137
163
|
await send(
|
|
138
164
|
"Still listening. Send a message from your Kazee account, paste a user id, or send \"skip\".",
|
|
@@ -162,7 +188,7 @@ async function tryCaptureOwner(envelope) {
|
|
|
162
188
|
const who = name ? `${name} (${userId})` : userId;
|
|
163
189
|
await sayToOrigin(
|
|
164
190
|
session,
|
|
165
|
-
`Got a Kazee message from ${who}.\n\nIs this you — the owner
|
|
191
|
+
`Got a Kazee message from ${who}.\n\nIs this you — the owner? Tap a button below, or just reply "yes" / "no".`,
|
|
166
192
|
{ keyboard: { inline_keyboard: [
|
|
167
193
|
[{ text: "Yes, that's me", callback_data: "chw:owner-yes" }],
|
|
168
194
|
[{ text: "No, keep listening", callback_data: "chw:owner-no" }],
|
|
@@ -183,7 +209,12 @@ async function handleAction(envelope) {
|
|
|
183
209
|
await cancel(envelope.canonicalUserId);
|
|
184
210
|
return true;
|
|
185
211
|
}
|
|
186
|
-
if (!session)
|
|
212
|
+
if (!session) {
|
|
213
|
+
// Session gone (bot restart or timeout). Don't silently swallow — a dead
|
|
214
|
+
// button that does nothing reads as a hang. Tell the operator to re-run.
|
|
215
|
+
await send("That setup step expired — I'm no longer listening. Run /channel add kazee to start again.");
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
187
218
|
|
|
188
219
|
if (payload === "chw:owner-yes") {
|
|
189
220
|
const cand = session.data.candidate;
|
|
@@ -268,6 +299,23 @@ async function finalizeOwner(canonicalUserId, session, ownerUserId) {
|
|
|
268
299
|
config.KAZEE_OWNER_USER_ID = ownerUserId;
|
|
269
300
|
const adapter = registry.findAdapter("kazee");
|
|
270
301
|
if (adapter) adapter.ownerUserId = ownerUserId;
|
|
302
|
+
// Link the owner's Kazee handle onto their people record so relationship
|
|
303
|
+
// resolution (speakerFor → channelIsOwner) recognizes their Kazee messages
|
|
304
|
+
// as the owner. Without this, ownership is only known to the env/adapter and
|
|
305
|
+
// the guardrail re-derives the operator as external → spurious re-approval.
|
|
306
|
+
if (ownerUserId) {
|
|
307
|
+
try {
|
|
308
|
+
const owner = people.owners()[0];
|
|
309
|
+
if (owner) {
|
|
310
|
+
people.linkHandle(owner.id, {
|
|
311
|
+
adapter: "kazee",
|
|
312
|
+
channelId: ownerUserId,
|
|
313
|
+
displayName: session.data?.candidate?.name || null,
|
|
314
|
+
approvedBy: "channel-wizard",
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
} catch (e) { console.error("channel-wizard linkHandle:", e.message); }
|
|
318
|
+
}
|
|
271
319
|
sessions.delete(canonicalUserId);
|
|
272
320
|
await sayToOrigin(
|
|
273
321
|
session,
|
package/core/enforcer.js
CHANGED
|
Binary file
|
package/core/io.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// here so that the right adapter is invoked for the in-flight message
|
|
3
3
|
// without each handler having to know about transports.
|
|
4
4
|
|
|
5
|
-
const { currentAdapter, currentChannelId } = require("./context");
|
|
5
|
+
const { currentAdapter, currentChannelId, currentUserId } = require("./context");
|
|
6
6
|
|
|
7
7
|
async function send(text, opts = {}) {
|
|
8
8
|
const adapter = currentAdapter();
|
|
@@ -53,7 +53,7 @@ async function sendFile(filePath, caption) {
|
|
|
53
53
|
// (external) speaker. Owner / no-context → guardActive is false → no-op.
|
|
54
54
|
try {
|
|
55
55
|
const relationship = require("./relationship");
|
|
56
|
-
if (relationship.guardActive(relationship.speakerFor(adapter.type, channelId))) {
|
|
56
|
+
if (relationship.guardActive(relationship.speakerFor(adapter.type, channelId, currentUserId()))) {
|
|
57
57
|
console.error("sendFile blocked: external speaker — file egress requires owner approval");
|
|
58
58
|
return false;
|
|
59
59
|
}
|
package/core/loopback.js
CHANGED
|
@@ -106,6 +106,7 @@ function readBodyAsString(req, max = 64 * 1024) {
|
|
|
106
106
|
|
|
107
107
|
async function handleSend(req, res, url, kind) {
|
|
108
108
|
const channelId = url.searchParams.get("channelId");
|
|
109
|
+
const userId = url.searchParams.get("userId") || "";
|
|
109
110
|
const adapterId = url.searchParams.get("adapter");
|
|
110
111
|
const caption = url.searchParams.get("caption") || "";
|
|
111
112
|
const fileName = url.searchParams.get("fileName") || `file-${Date.now()}.bin`;
|
|
@@ -119,7 +120,7 @@ async function handleSend(req, res, url, kind) {
|
|
|
119
120
|
// unaffected — guardActive is false there, so this is a strict no-op for them.
|
|
120
121
|
try {
|
|
121
122
|
const relationship = require("./relationship");
|
|
122
|
-
const speaker = relationship.speakerFor(adapter.type, channelId);
|
|
123
|
+
const speaker = relationship.speakerFor(adapter.type, channelId, userId);
|
|
123
124
|
if (relationship.guardActive(speaker)) {
|
|
124
125
|
return reply(res, 403, { error: "file egress to an external contact is blocked by the guardrail — an unvetted file cannot be sent to a non-owner; ask the owner to send it" });
|
|
125
126
|
}
|
|
@@ -152,6 +153,7 @@ async function handleJson(req, res, url, kind) {
|
|
|
152
153
|
catch (e) { return reply(res, 400, { error: "invalid JSON body" }); }
|
|
153
154
|
}
|
|
154
155
|
const channelId = String(payload.channelId || url.searchParams.get("channelId") || "");
|
|
156
|
+
const userId = String(payload.userId || url.searchParams.get("userId") || "");
|
|
155
157
|
const adapterId = String(payload.adapter || url.searchParams.get("adapter") || "");
|
|
156
158
|
if (!channelId || !adapterId) return reply(res, 400, { error: "missing channelId/adapter" });
|
|
157
159
|
const adapter = registry.findAdapter(adapterId);
|
|
@@ -485,7 +487,7 @@ async function handleJson(req, res, url, kind) {
|
|
|
485
487
|
// non-owner, refuse: never hand an external the approve button (R8/R10).
|
|
486
488
|
try {
|
|
487
489
|
const relationship = require("./relationship");
|
|
488
|
-
const speaker = relationship.speakerFor(adapter.type, channelId);
|
|
490
|
+
const speaker = relationship.speakerFor(adapter.type, channelId, userId);
|
|
489
491
|
if (relationship.guardActive(speaker)) {
|
|
490
492
|
return reply(res, 403, { error: "approval requests are owner-only; external mutating runs escalate through the guardrail" });
|
|
491
493
|
}
|
|
@@ -507,7 +509,10 @@ async function handleJson(req, res, url, kind) {
|
|
|
507
509
|
{ text: "✅ Approve", callback_data: `apr:${rec.id}:ok` },
|
|
508
510
|
{ text: "⛔ Deny", callback_data: `apr:${rec.id}:no` },
|
|
509
511
|
]] };
|
|
510
|
-
|
|
512
|
+
// <pre> needs Telegram's HTML parse mode to render; other adapters ignore it.
|
|
513
|
+
const sendOpts = { keyboard };
|
|
514
|
+
if (adapter.type === "telegram") sendOpts.parseMode = "HTML";
|
|
515
|
+
const ok = await adapter.send(channelId, text, sendOpts);
|
|
511
516
|
if (!ok) { approvals.decide(rec.id, "denied", "send-failed"); return reply(res, 500, { error: "could not deliver approval prompt" }); }
|
|
512
517
|
audit.log("tool.approval.requested", { id: rec.id, tool: rec.tool, verb: rec.verb, channelId });
|
|
513
518
|
return reply(res, 200, { ok: true, id: rec.id });
|
package/core/relationship.js
CHANGED
|
@@ -13,11 +13,19 @@
|
|
|
13
13
|
// ungated. This is what keeps the owner reply/tool/file paths byte-for-byte
|
|
14
14
|
// unchanged when the guardrail is dormant (R9/R14).
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// `userId` (optional) is the PERSON's id, distinct from `channelId` (the
|
|
17
|
+
// reply target / room) on transports that separate them — Kazee sets
|
|
18
|
+
// userId = the sender and channelId = the chat-document. Identity (owner
|
|
19
|
+
// classification + person lookup) must key off the person, never the room,
|
|
20
|
+
// or the owner is falsely classed external on Kazee (channelId ≠ the
|
|
21
|
+
// configured KAZEE_OWNER_USER_ID). On Telegram userId === channelId, so
|
|
22
|
+
// passing it is a strict no-op. channelId is still what we reply/escalate to.
|
|
23
|
+
function speakerFor(adapterType, channelId, userId) {
|
|
17
24
|
try {
|
|
18
25
|
if (!adapterType || !channelId) return null;
|
|
26
|
+
const speakerId = String(userId || channelId);
|
|
19
27
|
const people = require("./people");
|
|
20
|
-
const person = people.findByHandle(adapterType,
|
|
28
|
+
const person = people.findByHandle(adapterType, speakerId);
|
|
21
29
|
|
|
22
30
|
// Ownership is resolved through canonical identity — the SAME source of
|
|
23
31
|
// truth the queue/state/run-lock uses — so the owner is recognised on every
|
|
@@ -30,7 +38,7 @@ function speakerFor(adapterType, channelId) {
|
|
|
30
38
|
// a note can never trip it (R1 preserved — the owner stays anchored to a
|
|
31
39
|
// real owner record via ownerCanonical → people.isOwner).
|
|
32
40
|
let ownerByCanonical = false;
|
|
33
|
-
try { ownerByCanonical = require("./identity").channelIsOwner(adapterType,
|
|
41
|
+
try { ownerByCanonical = require("./identity").channelIsOwner(adapterType, speakerId); } catch (e) {}
|
|
34
42
|
|
|
35
43
|
if (ownerByCanonical || (person && person.isOwner)) {
|
|
36
44
|
let ownerPerson = person && person.isOwner ? person : null;
|
|
@@ -80,11 +88,11 @@ function speakerFor(adapterType, channelId) {
|
|
|
80
88
|
|
|
81
89
|
function currentSpeaker() {
|
|
82
90
|
try {
|
|
83
|
-
const { currentAdapter, currentChannelId } = require("./context");
|
|
91
|
+
const { currentAdapter, currentChannelId, currentUserId } = require("./context");
|
|
84
92
|
const adapter = currentAdapter();
|
|
85
93
|
const channelId = currentChannelId();
|
|
86
94
|
if (!adapter || !channelId) return null; // no context → ungated (owner-local)
|
|
87
|
-
return speakerFor(adapter.type, channelId);
|
|
95
|
+
return speakerFor(adapter.type, channelId, currentUserId());
|
|
88
96
|
} catch (e) {
|
|
89
97
|
return null;
|
|
90
98
|
}
|
package/core/runner.js
CHANGED
|
@@ -12,7 +12,7 @@ const {
|
|
|
12
12
|
CONFIG_DIR, WORKSPACE, config,
|
|
13
13
|
} = require("./config");
|
|
14
14
|
const { currentState, saveState, recordSession, userOwnsClaudeSession, resetSessionUsage } = require("./state");
|
|
15
|
-
const { chatContext, currentChannelId, currentAdapter } = require("./context");
|
|
15
|
+
const { chatContext, currentChannelId, currentAdapter, currentUserId } = require("./context");
|
|
16
16
|
const { splitLeadingOriginGroup } = require("./queue-drain");
|
|
17
17
|
const { buildSystemPrompt, promptWithDynamicContext } = require("./system-prompt");
|
|
18
18
|
const { redactSensitive } = require("./redact");
|
|
@@ -169,7 +169,11 @@ function logTurnUsage(state, usage, costUsd, announce, opts = {}) {
|
|
|
169
169
|
const result = evaluateUsageAlert(previousRecords, record, policy, lastAlertAt);
|
|
170
170
|
if (result.alert) {
|
|
171
171
|
state.usageAlertLastAt = Date.now();
|
|
172
|
-
|
|
172
|
+
// Owner-only: version/model/context internals must never surface to an
|
|
173
|
+
// external speaker, regardless of the usage-alert policy.
|
|
174
|
+
let usageOwnerOnly = true;
|
|
175
|
+
try { usageOwnerOnly = !require("./relationship").isCurrentSpeakerGuarded(); } catch (e) {}
|
|
176
|
+
if (announce && usageOwnerOnly) {
|
|
173
177
|
const baseline = result.trend?.baselineAvgContextTokens || 0;
|
|
174
178
|
const rate = result.trend?.latestRate || 0;
|
|
175
179
|
const contextLabel = usageScope === "turn_delta" ? "Context this turn" : "Session context total";
|
|
@@ -197,6 +201,11 @@ function chatEnvOverlay() {
|
|
|
197
201
|
const overlay = {};
|
|
198
202
|
if (adapter && adapter.id) overlay.OC_CHANNEL_ADAPTER = adapter.id;
|
|
199
203
|
if (channelId) overlay.OC_CHANNEL_ID = String(channelId);
|
|
204
|
+
// Raw per-user id (distinct from channelId on Kazee) so subprocess egress
|
|
205
|
+
// guards classify the speaker by person, not room — matching in-process
|
|
206
|
+
// relationship.speakerFor. On Telegram this equals channelId.
|
|
207
|
+
const uid = currentUserId();
|
|
208
|
+
if (uid) overlay.OC_CHANNEL_USER_ID = String(uid);
|
|
200
209
|
if (lb) {
|
|
201
210
|
overlay.OC_SEND_URL = lb.url;
|
|
202
211
|
overlay.OC_SEND_TOKEN = lb.token;
|
|
@@ -780,6 +789,11 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
780
789
|
const channelId = currentChannelId();
|
|
781
790
|
const adapter = currentAdapter();
|
|
782
791
|
const { settings } = state;
|
|
792
|
+
// Owner-vs-external, resolved once per turn. All internal-activity chatter
|
|
793
|
+
// (recall banners, tool/skill traces, usage alerts) is owner-only so nothing
|
|
794
|
+
// about how the assistant works leaks to an external speaker.
|
|
795
|
+
let externalSpeaker = false;
|
|
796
|
+
try { externalSpeaker = require("./relationship").isCurrentSpeakerGuarded(); } catch (e) {}
|
|
783
797
|
cwd = resolveRunCwd(cwd);
|
|
784
798
|
|
|
785
799
|
if (state.runningProcess) {
|
|
@@ -920,6 +934,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
920
934
|
// is invoked or its SKILL.md is written/patched. Deduped per turn.
|
|
921
935
|
const skillNotified = new Set();
|
|
922
936
|
const notifySkill = (key, text) => {
|
|
937
|
+
if (externalSpeaker) return; // never leak internal activity to a non-owner
|
|
923
938
|
if (skillNotified.has(key)) return;
|
|
924
939
|
skillNotified.add(key);
|
|
925
940
|
chatContext.run(store, () => send(text).catch(() => {}));
|
|
@@ -1085,14 +1100,17 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1085
1100
|
turnRecallTier = String(r.tier || "");
|
|
1086
1101
|
const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1087
1102
|
const fmt = (arr, icon) => arr.map((x) => (x.why ? `${icon} <b>${esc(x.name)}</b> — ${esc(x.why)}` : `${icon} <b>${esc(x.name)}</b>`));
|
|
1103
|
+
// Debug traces are owner-only: recall/tool/engine internals must never
|
|
1104
|
+
// leak to an external speaker even when a debug toggle is left on.
|
|
1105
|
+
const traceOwnerOnly = !externalSpeaker;
|
|
1088
1106
|
// 🧠 packs/entities recall — gated by /recall.
|
|
1089
|
-
if (settings.showRecall) {
|
|
1107
|
+
if (traceOwnerOnly && settings.showRecall) {
|
|
1090
1108
|
const lines = [...fmt(r.packs || [], "📦"), ...fmt(r.entities || [], "👤"), ...fmt(r.episodes || [], "📓")];
|
|
1091
1109
|
if (lines.length) send(`🧠 <b>Recall this turn</b> (${esc(r.engine)})\n${lines.join("\n")}`, telegramHtmlOpts()).catch(() => {});
|
|
1092
1110
|
else if (r.gated) send(`🧠 <b>Recall</b> (${esc(r.engine)}): skipped by pre-gate — trivial turn.`, telegramHtmlOpts()).catch(() => {});
|
|
1093
1111
|
}
|
|
1094
1112
|
// 🔧 tools surfaced this turn (first-class discoverer nodes) — gated by /tooltrace.
|
|
1095
|
-
if (settings.showToolTrace) {
|
|
1113
|
+
if (traceOwnerOnly && settings.showToolTrace) {
|
|
1096
1114
|
const toolLines = fmt(r.tools || [], "🔧");
|
|
1097
1115
|
if (toolLines.length) send(`🔧 <b>Tools surfaced this turn</b>\n${toolLines.join("\n")}`, telegramHtmlOpts()).catch(() => {});
|
|
1098
1116
|
}
|
package/core/system-prompt.js
CHANGED
|
@@ -7,7 +7,7 @@ const path = require("path");
|
|
|
7
7
|
const { SOUL_FILE, CRONS_FILE, VAULT_FILE, FILES_DIR, BOT_DIR, WHISPER_CLI, FFMPEG, config } = require("./config");
|
|
8
8
|
const CONFIG_DIR = require("../config-dir");
|
|
9
9
|
const { currentState } = require("./state");
|
|
10
|
-
const { currentAdapter, currentChannelId } = require("./context");
|
|
10
|
+
const { currentAdapter, currentChannelId, currentUserId } = require("./context");
|
|
11
11
|
const { vault } = require("./vault-store");
|
|
12
12
|
const { transcriptPointerNote } = require("./transcripts");
|
|
13
13
|
const tasksStore = require("./tasks");
|
|
@@ -146,7 +146,10 @@ function buildSystemPrompt() {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
if (adapter && channelId) {
|
|
149
|
-
|
|
149
|
+
// Identify the speaker by their person id (userId), not the room —
|
|
150
|
+
// on Kazee channelId is the chat-document, so a channelId lookup would
|
|
151
|
+
// never match the owner's handle. On Telegram the two are equal.
|
|
152
|
+
const speaker = people.findByHandle(adapter.type, String(currentUserId() || channelId));
|
|
150
153
|
if (speaker) {
|
|
151
154
|
const recentNotes = (speaker.notes || []).slice(-3).map((n) => `- [${n.at?.slice(0, 10) || ""}] ${n.text}`).join("\n");
|
|
152
155
|
currentSpeakerBlock = `\n## Speaker\nYou are talking to ${speaker.name}${speaker.isOwner ? " (the owner)" : ""} on ${adapter.type}.${speaker.bio ? "\nBio: " + speaker.bio : ""}${recentNotes ? "\nRecent notes:\n" + recentNotes : ""}\n`;
|
package/package.json
CHANGED