@inetafrica/open-claudia 3.5.74-usage.2 → 3.5.75
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 +17 -9
- package/Dockerfile +0 -2
- package/bin/cli.js +41 -0
- package/bin/owner-setup.js +56 -0
- package/bot.js +42 -0
- package/channels/kazee/call/camera.js +2 -1
- package/channels/kazee/call/session.js +5 -6
- package/channels/spaces/adapter.js +25 -1
- package/channels/spaces/client.js +6 -2
- package/channels/telegram/adapter.js +1 -2
- package/channels/web/adapter.js +27 -11
- package/core/access.js +25 -0
- package/core/app-pass.js +145 -0
- package/core/bot-profile.js +228 -0
- package/core/config.js +7 -0
- package/core/connected-apps.js +19 -4
- package/core/handlers.js +3 -1
- package/core/identity.js +14 -0
- package/core/lessons.js +1 -2
- package/core/loopback.js +21 -0
- package/core/owner-setup.js +216 -0
- package/core/pack-review.js +10 -77
- package/core/recall/index.js +2 -3
- package/core/recall/warm-walker.js +1 -31
- package/core/runner.js +1 -4
- package/core/system-prompt.js +20 -0
- package/core/usage-log.js +0 -1
- package/core/web-otp.js +17 -8
- package/core/web-sessions.js +11 -2
- package/docker-entrypoint.sh +0 -10
- package/package.json +2 -2
- package/test-provider-language.js +4 -3
- package/web.js +19 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 3.5.
|
|
3
|
+
## 3.5.75
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
3.5.74 plus personal bots act for their owner through a pass (from the Maia build), and the one-tap switch-on card. Nothing changes for a bot until Central has `bot:me` (an older Central answers 404 and the bot stays as it was). Autonomous bots are unchanged.
|
|
6
6
|
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
- **
|
|
7
|
+
- **Type check** (`core/bot-profile.js`): on start, and every 15 minutes, the bot asks Central `GET /api/bots/me` with its own key for its kind, its one owner and the owner's consent (ticked apps, end date, `renewDue`). The answer is kept in `bot-profile.json`, so a restart while Central is down still knows.
|
|
8
|
+
- **One owner**: a personal bot's owner is the person in Central. Her id replaces `KAZEE_OWNER_USER_ID`, from config load and on every refresh. `access.isChatOwner` counts no other owner (no people records, `auth.json` or chat ids marked owner), and the web dashboard and the voice bridge reach her rather than being owners themselves.
|
|
9
|
+
- **One pass helper** (`core/app-pass.js`): every app call of a personal bot trades its key for a 10-minute pass for that app (`POST /api/bots/pass`), reused until a minute before it ends, one request per app at a time; the bot's own key is never sent to an app. Kazee Chat keeps the bot's own key. The owner reads each app's own name (Spaces, CRM, Tickets, Agent Space, Infrastructure) for Central's exact app names.
|
|
10
|
+
- **Spaces**: the conversational channel stays idle for a personal bot (no socket of its own); anything still sent through it, and the connected-apps plane watching the apps the owner ticked, goes out with her pass.
|
|
11
|
+
- **One-tap card** (SEC-28, `core/owner-setup.js`): when an app refuses a pass because she has not switched the bot on there, and a week before her consent ends, the owner gets a Kazee Chat card with one button that opens Central at the form (Switch me on / Renew); at most once a day per app and reason. `open-claudia owner-setup [--apps a,b] [--renew]` sends it on request; `open-claudia pass <app>` prints a fresh pass, never the key.
|
|
12
|
+
- **Instructions**: a personal bot gets its own block in the core instructions (who it works for, approvals, money and deletes need her tap, send the one-tap card).
|
|
13
13
|
|
|
14
|
-
Tests:
|
|
14
|
+
Tests: `test-bot-acts-for-owner.js` (in the suite).
|
|
15
|
+
|
|
16
|
+
## 3.5.74
|
|
17
|
+
|
|
18
|
+
3.5.73 plus the Rust command helper, released on Sumeet's card of 8 Nov ("Yes, Maya Cloud first, then one bot at a time") after 3 clean days on his Mac (about 62,000 commands, no crashes).
|
|
19
|
+
|
|
20
|
+
- **`open-claudia` is now maia-rs** (`rust/`, built in a static musl stage of the image): cards, decisions, tasks, wakeups, crons, watches, send-file/photo/voice, say/tts/transcribe and tool runs (read-only, and the owner's flagged writes) run without starting Node. Under 1 MB and a few milliseconds per call instead of about 50 MB and a tenth of a second. Anything else is handed to `bin/cli.js` unchanged.
|
|
21
|
+
- **Kill switch per bot**: `OC_RUST_CLI=0` in the bot's environment hands every command to the Node CLI, as before.
|
|
22
|
+
- **Fixes from the Mac trial**: `--quiet` kept on wakeups, crons and watches; exits 0 when its reader has gone (a closed pipe used to abort with exit 134).
|
|
15
23
|
|
|
16
24
|
## 3.5.73
|
|
17
25
|
|
package/Dockerfile
CHANGED
|
@@ -102,7 +102,5 @@ EXPOSE 8080
|
|
|
102
102
|
# Switch to non-root user
|
|
103
103
|
USER 1001
|
|
104
104
|
|
|
105
|
-
# OC_NODE_FLAGS in a bot's env: docker-entrypoint.sh puts those flags on this
|
|
106
|
-
# node command only. Unset, the command below runs as is.
|
|
107
105
|
ENTRYPOINT ["docker-entrypoint.sh"]
|
|
108
106
|
CMD ["node", "bin/cli.js", "web"]
|
package/bin/cli.js
CHANGED
|
@@ -218,6 +218,41 @@ switch (command) {
|
|
|
218
218
|
break;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
case "pass": {
|
|
222
|
+
// A fresh pass for one app (core/app-pass.js), for a tool that calls an
|
|
223
|
+
// app for a personal bot's owner: e.g. `open-claudia pass kazee-spaces-backend`.
|
|
224
|
+
// Never prints the bot's own key: an autonomous bot has no pass to give.
|
|
225
|
+
const applicationName = args[1];
|
|
226
|
+
if (!applicationName) {
|
|
227
|
+
console.error("Usage: open-claudia pass <Central application name, e.g. kazee-spaces-backend>");
|
|
228
|
+
process.exit(2);
|
|
229
|
+
}
|
|
230
|
+
(async () => {
|
|
231
|
+
const botProfile = require(path.join(botDir, "core/bot-profile"));
|
|
232
|
+
const appPass = require(path.join(botDir, "core/app-pass"));
|
|
233
|
+
await botProfile.refresh();
|
|
234
|
+
if (!botProfile.isPersonal()) {
|
|
235
|
+
console.error("Only a personal bot uses passes; this bot calls apps with its own key.");
|
|
236
|
+
process.exit(2);
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
process.stdout.write(await appPass.tokenFor(applicationName));
|
|
240
|
+
process.stdout.write("\n");
|
|
241
|
+
process.exit(0);
|
|
242
|
+
} catch (e) {
|
|
243
|
+
console.error(e instanceof appPass.PassRefused ? e.ownerMessage : e.message);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
})();
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
case "owner-setup": {
|
|
251
|
+
// The one-tap card for a personal bot's owner (core/owner-setup.js).
|
|
252
|
+
require("./owner-setup").run(args.slice(1)).then((code) => process.exit(code));
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
|
|
221
256
|
case "health": {
|
|
222
257
|
console.log("Running health checks...\n");
|
|
223
258
|
const verbose = args.includes("-v") || args.includes("--verbose");
|
|
@@ -679,6 +714,12 @@ Background work (only inside an active bot-spawned task):
|
|
|
679
714
|
Cluster control (self-management of this bot's own deployment via the broker):
|
|
680
715
|
open-claudia cluster status|logs|restart|start|stop|scale <0|1>|sync
|
|
681
716
|
|
|
717
|
+
Personal bots (acting for their owner):
|
|
718
|
+
open-claudia pass <app> A fresh 10-minute pass to act as your owner at one app
|
|
719
|
+
(e.g. kazee-spaces-backend); never your own key
|
|
720
|
+
open-claudia owner-setup [--apps a,b] [--renew] Send your owner a one-tap card in Kazee Chat to switch
|
|
721
|
+
you on (default: apps not switched on yet) or renew
|
|
722
|
+
|
|
682
723
|
Multi-user / cross-channel:
|
|
683
724
|
open-claudia people list|show|add|note|link Roster of known team members
|
|
684
725
|
open-claudia intros list|approve|reject Pending introductions from unknown chats
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// open-claudia owner-setup — send a personal bot's owner the one-tap card that opens
|
|
2
|
+
// Central at the form to switch the bot on (or renew it). See
|
|
3
|
+
// core/owner-setup.js. The running bot sends it to the owner's direct Kazee chat.
|
|
4
|
+
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const HELP = `
|
|
8
|
+
Send your owner a one-tap card to switch you on, or to renew.
|
|
9
|
+
|
|
10
|
+
open-claudia owner-setup the apps you can work in that your owner hasn't switched on yet
|
|
11
|
+
open-claudia owner-setup --apps Tickets just these apps (Central names or Tickets, CRM, Spaces...)
|
|
12
|
+
open-claudia owner-setup --renew renew before your owner's permission ends
|
|
13
|
+
|
|
14
|
+
Personal bots only. You can never switch yourself on: only your owner can confirm.
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
function takeFlag(args, name) {
|
|
18
|
+
const idx = args.indexOf(`--${name}`);
|
|
19
|
+
if (idx < 0) return null;
|
|
20
|
+
const v = args[idx + 1];
|
|
21
|
+
args.splice(idx, 2);
|
|
22
|
+
return v;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Resolves the exit code. deps (tests): refresh, post, log, error.
|
|
26
|
+
async function run(args, deps = {}) {
|
|
27
|
+
const log = deps.log || console.log;
|
|
28
|
+
const error = deps.error || console.error;
|
|
29
|
+
args = [...args];
|
|
30
|
+
if (args[0] === "--help" || args[0] === "help") { log(HELP); return 0; }
|
|
31
|
+
const botProfile = require(path.join(__dirname, "..", "core", "bot-profile"));
|
|
32
|
+
const ownerSetup = require(path.join(__dirname, "..", "core", "owner-setup"));
|
|
33
|
+
const renew = args.includes("--renew");
|
|
34
|
+
const apps = String(takeFlag(args, "apps") || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
35
|
+
try {
|
|
36
|
+
await (deps.refresh || botProfile.refresh)();
|
|
37
|
+
} catch (_) { /* the last answer on disk */ }
|
|
38
|
+
const planned = ownerSetup.plan({ apps, renew });
|
|
39
|
+
if (planned.refused) { error(planned.refused); return 2; }
|
|
40
|
+
if (planned.nothing) { log(planned.nothing); return 0; }
|
|
41
|
+
try {
|
|
42
|
+
const post = deps.post || require("./loopback-client").postJson;
|
|
43
|
+
const res = await post("owner-setup", { apps, renew });
|
|
44
|
+
if (res.nothing) { log(res.nothing); return 0; }
|
|
45
|
+
const who = botProfile.ownerFirstName();
|
|
46
|
+
if (res.sent === "already-sent") log(`${who} already got this card in the last day; not sent again.`);
|
|
47
|
+
else if (res.sent === "card") log(`Sent ${who} a card in Kazee Chat: "${res.title}".`);
|
|
48
|
+
else log(`The card could not be sent, so ${who} got the text with the link instead.`);
|
|
49
|
+
return 0;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
error(`owner-setup: ${e.message}`);
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { run, HELP };
|
package/bot.js
CHANGED
|
@@ -264,6 +264,42 @@ setInterval(checkForUpdates, 5 * 60 * 1000);
|
|
|
264
264
|
|
|
265
265
|
// ── Boot ────────────────────────────────────────────────────────────
|
|
266
266
|
|
|
267
|
+
// Tell a personal bot's owner in her Kazee DM that an app refused a pass: a
|
|
268
|
+
// one-tap card to switch the bot on there, else the text (core/owner-setup.js).
|
|
269
|
+
// Best effort: no Kazee adapter or DM means no message.
|
|
270
|
+
async function tellPersonalOwner(text, err) {
|
|
271
|
+
const ownerSetup = require("./core/owner-setup");
|
|
272
|
+
const kazee = adapters.find((a) => a.type === "kazee");
|
|
273
|
+
if (kazee) await ownerSetup.tellOwner(ownerSetup.forRefusal(err, text), { adapter: kazee });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// A week before the consent ends: the renew card, once a day.
|
|
277
|
+
async function maybeSendRenewNotice() {
|
|
278
|
+
const kazee = adapters.find((a) => a.type === "kazee");
|
|
279
|
+
if (kazee) await require("./core/owner-setup").maybeSendRenew({ adapter: kazee });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function startBotProfile() {
|
|
283
|
+
const botProfile = require("./core/bot-profile");
|
|
284
|
+
const applyOwner = () => {
|
|
285
|
+
const owner = botProfile.applyOwner(config);
|
|
286
|
+
if (!owner) return;
|
|
287
|
+
for (const a of adapters) {
|
|
288
|
+
if (a.type === "kazee" || a.type === "spaces") a.ownerUserId = owner;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
await botProfile.refresh();
|
|
292
|
+
applyOwner();
|
|
293
|
+
console.log(botProfile.isPersonal()
|
|
294
|
+
? `Bot profile: personal, owner ${botProfile.ownerName() || botProfile.ownerUserId()}; apps ticked: ${botProfile.consentApplications().join(", ") || "none"}`
|
|
295
|
+
: `Bot profile: ${botProfile.get()?.bot?.botKind || "unknown (Central has no bot:me)"}`);
|
|
296
|
+
require("./core/app-pass").setOwnerNotifier(tellPersonalOwner);
|
|
297
|
+
botProfile.startRefreshing(applyOwner);
|
|
298
|
+
const renewTimer = setInterval(() => { maybeSendRenewNotice(); }, 60 * 60 * 1000);
|
|
299
|
+
if (renewTimer.unref) renewTimer.unref();
|
|
300
|
+
setTimeout(() => { maybeSendRenewNotice(); }, 60 * 1000).unref?.();
|
|
301
|
+
}
|
|
302
|
+
|
|
267
303
|
// Minimal boot: adapters + slash commands only. No scheduler/crons, no dream,
|
|
268
304
|
// no connected-apps, no loopback, no web UI, no onboarding — nothing that can
|
|
269
305
|
// run an agent turn or write memory. The alert tells the owner exactly why and
|
|
@@ -332,6 +368,12 @@ async function safeModeBoot() {
|
|
|
332
368
|
}
|
|
333
369
|
} catch (e) {}
|
|
334
370
|
|
|
371
|
+
// Personal or autonomous, and who the one owner is: asked of Central before
|
|
372
|
+
// the adapters start (core/bot-profile.js). A personal bot's app calls go
|
|
373
|
+
// through passes for that owner (core/app-pass.js).
|
|
374
|
+
try { await startBotProfile(); }
|
|
375
|
+
catch (e) { console.error("bot profile init failed:", e.message); }
|
|
376
|
+
|
|
335
377
|
initScheduler(adapters);
|
|
336
378
|
try { require("./core/dream").initDream(adapters); }
|
|
337
379
|
catch (e) { console.error("dream init failed:", e.message); }
|
|
@@ -11,6 +11,7 @@ const fs = require("fs");
|
|
|
11
11
|
const os = require("os");
|
|
12
12
|
const path = require("path");
|
|
13
13
|
const { spawn } = require("child_process");
|
|
14
|
+
const { dePacketizeRtpPackets } = require("werift");
|
|
14
15
|
|
|
15
16
|
const STILL_EVERY_S = 2;
|
|
16
17
|
const STILL_SIZE = 768;
|
|
@@ -151,7 +152,7 @@ class CameraStills {
|
|
|
151
152
|
if ((packets[index].header.sequenceNumber - packets[index - 1].header.sequenceNumber + 65536) % 65536 !== 1) return this._lost();
|
|
152
153
|
}
|
|
153
154
|
let frame;
|
|
154
|
-
try { frame =
|
|
155
|
+
try { frame = dePacketizeRtpPackets("VP8", packets); } catch (_) { return this._lost(); }
|
|
155
156
|
if (!frame?.data?.length) return;
|
|
156
157
|
const size = frame.isKeyframe ? vp8KeyframeSize(frame.data) : null;
|
|
157
158
|
if (this._needKeyframe) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
const werift = () => require("werift");
|
|
3
|
+
const { MediaStreamTrack, RTCPeerConnection, RTCRtpHeaderExtensionParameters, RtpHeader, RtpPacket } = require("werift");
|
|
5
4
|
const { JanusClient } = require("./janus-client");
|
|
6
5
|
const { projectAssignedIce, supportedIceServers } = require("./access");
|
|
7
6
|
const { AdaptiveTurnDetector, OpusAudioCodec, WEBRTC_SAMPLES, downsample48To16, upsampleTo48, rmsDbfs } = require("./audio");
|
|
@@ -228,7 +227,7 @@ class KazeeCallSession {
|
|
|
228
227
|
constructor({ roomId, chatId, callerId, botUserId, janusUrl, janusToken, janusTokenExpiresAt, roomToken, roomTokenExpiresAt, managedRoom = false, refreshRoomAccess, iceServers = [], iceCredentialsExpiresAt, onTranscript, onEnded, onMediaReady,
|
|
229
228
|
maxCallMs, idleMs, checkInMs, checkInGraceMs, dropMs, dropGiveUpMs, humDb, finalWaitMs, synthesize = synthesizePcm, synthesizeStream = null, asCaller = false, greeting = null,
|
|
230
229
|
turnDecision = detectTurn, acknowledgement = selectAcknowledgement, onSpeechStart, onSpeechNoise, onEarlyWords, onUtteranceDropped,
|
|
231
|
-
JanusClientImpl = JanusClient, PeerConnectionImpl =
|
|
230
|
+
JanusClientImpl = JanusClient, PeerConnectionImpl = RTCPeerConnection, SpeechStreamImpl = SpeechStream } = {}) {
|
|
232
231
|
this.roomId = Number(roomId);
|
|
233
232
|
this.chatId = String(chatId);
|
|
234
233
|
this.callerId = String(callerId);
|
|
@@ -358,7 +357,7 @@ class KazeeCallSession {
|
|
|
358
357
|
// A feed's connection also takes the caller's camera; with the
|
|
359
358
|
// orientation extension a phone held upright gives an upright still.
|
|
360
359
|
const pc = new this.PeerConnectionImpl(camera
|
|
361
|
-
? { iceServers: this.iceServers, headerExtensions: { audio: [], video: [new
|
|
360
|
+
? { iceServers: this.iceServers, headerExtensions: { audio: [], video: [new RTCRtpHeaderExtensionParameters({ id: 13, uri: VIDEO_ORIENTATION_URI })] } }
|
|
362
361
|
: { iceServers: this.iceServers });
|
|
363
362
|
if (Number.isFinite(expiresAt)) {
|
|
364
363
|
this._transportIceExpiry.set(pc, expiresAt);
|
|
@@ -516,7 +515,7 @@ class KazeeCallSession {
|
|
|
516
515
|
this._maybeGreet();
|
|
517
516
|
}
|
|
518
517
|
});
|
|
519
|
-
const track = new
|
|
518
|
+
const track = new MediaStreamTrack({ kind: "audio" });
|
|
520
519
|
const sender = pc.addTrack(track);
|
|
521
520
|
this.publisher = { pc, track, sender, pending: [], remoteSet: false };
|
|
522
521
|
const offer = await pc.createOffer();
|
|
@@ -1047,7 +1046,7 @@ class KazeeCallSession {
|
|
|
1047
1046
|
if (frame.length < WEBRTC_SAMPLES * 2) frame = Buffer.concat([frame, Buffer.alloc(WEBRTC_SAMPLES * 2 - frame.length)]);
|
|
1048
1047
|
const opus = this.codec.encode(frame);
|
|
1049
1048
|
const payloadType = this.publisher?.sender?.codec?.payloadType || 111;
|
|
1050
|
-
this.publisher?.track?.writeRtp(new
|
|
1049
|
+
this.publisher?.track?.writeRtp(new RtpPacket(new RtpHeader({
|
|
1051
1050
|
payloadType, sequenceNumber: this.sequence++ & 0xffff, timestamp: this.timestamp >>> 0, ssrc: this.ssrc,
|
|
1052
1051
|
}), opus));
|
|
1053
1052
|
this.timestamp = (this.timestamp + WEBRTC_SAMPLES) >>> 0;
|
|
@@ -21,6 +21,15 @@ const { SpacesClient, contentText } = require("./client");
|
|
|
21
21
|
const { createSpacesSocket } = require("./socket");
|
|
22
22
|
const spacesState = require("./state");
|
|
23
23
|
|
|
24
|
+
// Central's name for the Spaces back end, the app a personal bot's pass is for.
|
|
25
|
+
const SPACES_APPLICATION = "kazee-spaces-backend";
|
|
26
|
+
|
|
27
|
+
function spacesBearer(token) {
|
|
28
|
+
let personal = false;
|
|
29
|
+
try { personal = require("../../core/bot-profile").isPersonal(); } catch (_) { /* no profile: as before */ }
|
|
30
|
+
return personal ? require("../../core/app-pass").tokenFor(SPACES_APPLICATION) : token;
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
// Notification `type`/`category` values that warrant a full agent turn. The
|
|
25
34
|
// Spaces server emits PLURAL categories (mentions, comments, reactions,
|
|
26
35
|
// task_updated), so the engage set must match the plural forms — matching only
|
|
@@ -54,7 +63,11 @@ class SpacesAdapter {
|
|
|
54
63
|
// the only thing calling poll(); it can be disabled, so the adapter drives
|
|
55
64
|
// its own timer and never depends on it.
|
|
56
65
|
this.pollMs = Number(pollMs) > 0 ? Number(pollMs) : 60000;
|
|
57
|
-
|
|
66
|
+
// A personal bot never shows Spaces its own key: anything that still goes
|
|
67
|
+
// through this channel while it is idle (a reply, an approval mirror, a
|
|
68
|
+
// task title) carries a pass for its owner (core/app-pass.js). Other bots
|
|
69
|
+
// keep the key, as before.
|
|
70
|
+
this.client = new SpacesClient({ baseUrl: this.url, token: () => spacesBearer(token) });
|
|
58
71
|
this._listeners = { message: new Set(), action: new Set() };
|
|
59
72
|
this._socket = null;
|
|
60
73
|
this._seen = new Set(); // notification-id dedup across socket + poll
|
|
@@ -79,6 +92,17 @@ class SpacesAdapter {
|
|
|
79
92
|
}
|
|
80
93
|
|
|
81
94
|
async start() {
|
|
95
|
+
// A personal bot acts in Spaces only as its owner, through a pass
|
|
96
|
+
// (core/app-pass.js), so it has no Spaces identity of its own to watch or
|
|
97
|
+
// reply as. It watches its owner's Spaces inbox through the connected-apps
|
|
98
|
+
// plane instead, and the conversational channel stays idle.
|
|
99
|
+
try {
|
|
100
|
+
if (require("../../core/bot-profile").isPersonal()) {
|
|
101
|
+
this.idleReason = "personal-bot";
|
|
102
|
+
console.log("Spaces channel idle: a personal bot acts in Spaces only as its owner, through a pass");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
} catch (_) { /* no profile: behave as before */ }
|
|
82
106
|
this._socket = createSpacesSocket({
|
|
83
107
|
url: this.url,
|
|
84
108
|
token: this.token,
|
|
@@ -222,13 +222,17 @@ function buildCommentContent(text, mentions = []) {
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
class SpacesClient {
|
|
225
|
+
// `token` is the bearer, or a function that returns it (sync or async): a
|
|
226
|
+
// personal bot passes core/app-pass's provider, so every call goes out with
|
|
227
|
+
// a fresh pass for its owner and never with the bot's own key.
|
|
225
228
|
constructor({ baseUrl = "https://api.spaces.kazee.africa", token }) {
|
|
226
229
|
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
227
230
|
this.token = token;
|
|
228
231
|
}
|
|
229
232
|
|
|
230
|
-
|
|
231
|
-
|
|
233
|
+
async _bearer() { return typeof this.token === "function" ? this.token() : this.token; }
|
|
234
|
+
async _get(urlPath, timeoutMs) { return request(this.baseUrl, await this._bearer(), "GET", urlPath, null, timeoutMs); }
|
|
235
|
+
async _post(urlPath, body, timeoutMs) { return request(this.baseUrl, await this._bearer(), "POST", urlPath, body, timeoutMs); }
|
|
232
236
|
|
|
233
237
|
async listTasks() {
|
|
234
238
|
const res = await this._get("/api/tasks");
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const fs = require("fs");
|
|
7
7
|
const path = require("path");
|
|
8
8
|
const https = require("https");
|
|
9
|
+
const TelegramBot = require("node-telegram-bot-api");
|
|
9
10
|
const { TEMP_DIR, FILES_DIR, CONFIG_DIR } = require("../../core/config");
|
|
10
11
|
const { safeJoin } = require("../../core/safe-filename");
|
|
11
12
|
const { canonicalForChannel } = require("../../core/identity");
|
|
@@ -26,8 +27,6 @@ class TelegramAdapter {
|
|
|
26
27
|
// one bot) sidesteps the stale-socket trap that drove the old restart storm.
|
|
27
28
|
// We still own the agent so a heal can destroy any lingering socket.
|
|
28
29
|
this._agent = new https.Agent({ keepAlive: false });
|
|
29
|
-
// Loaded here so a bot with no Telegram token never holds this library.
|
|
30
|
-
const TelegramBot = require("node-telegram-bot-api");
|
|
31
30
|
this.bot = new TelegramBot(token, {
|
|
32
31
|
polling: { autoStart: false, params: { timeout: 30 } },
|
|
33
32
|
// Socket-idle timeout on every request (getUpdates included). With
|
package/channels/web/adapter.js
CHANGED
|
@@ -23,6 +23,20 @@ const HISTORY_LIMIT = 200;
|
|
|
23
23
|
const MEDIA_TTL_MS = 60 * 60 * 1000;
|
|
24
24
|
|
|
25
25
|
const WEB_OWNER_ID = "web-owner";
|
|
26
|
+
// A personal bot's dashboard session that names nobody (password login,
|
|
27
|
+
// Agent Space's link): not its owner.
|
|
28
|
+
const WEB_GUEST_ID = "web-guest";
|
|
29
|
+
|
|
30
|
+
// Who a dashboard message is from. An autonomous bot's dashboard is its
|
|
31
|
+
// owner's channel. A personal bot has one owner, Central's: the session
|
|
32
|
+
// names the person it was opened for (her /dashboard link), else a guest,
|
|
33
|
+
// and access.js decides whether that is the owner.
|
|
34
|
+
function webSpeaker(sessionUserId) {
|
|
35
|
+
let personal = false;
|
|
36
|
+
try { personal = require("../../core/bot-profile").isPersonal(); } catch (e) {}
|
|
37
|
+
if (!personal) return WEB_OWNER_ID;
|
|
38
|
+
return sessionUserId ? String(sessionUserId) : WEB_GUEST_ID;
|
|
39
|
+
}
|
|
26
40
|
|
|
27
41
|
class WebAdapter {
|
|
28
42
|
constructor({ id = "web" } = {}) {
|
|
@@ -64,38 +78,40 @@ class WebAdapter {
|
|
|
64
78
|
|
|
65
79
|
// ── inbound (called by web.js /api/chat routes) ─────────────────
|
|
66
80
|
|
|
67
|
-
postText(text) {
|
|
81
|
+
postText(text, { userId } = {}) {
|
|
68
82
|
const body = String(text || "");
|
|
69
83
|
if (!body.trim()) return null;
|
|
84
|
+
const speaker = webSpeaker(userId);
|
|
70
85
|
const messageId = this._mkId("t");
|
|
71
86
|
// Echo into history/stream so every connected tab sees the user's turn.
|
|
72
87
|
this._pushFrame({ kind: "message", role: "user", messageId, text: body, ts: Date.now() });
|
|
73
88
|
this._emit("message", {
|
|
74
89
|
adapter: this,
|
|
75
|
-
channelId:
|
|
76
|
-
canonicalUserId: canonicalForChannel("web",
|
|
77
|
-
userId:
|
|
90
|
+
channelId: speaker,
|
|
91
|
+
canonicalUserId: canonicalForChannel("web", speaker),
|
|
92
|
+
userId: speaker,
|
|
78
93
|
type: body.trim().startsWith("/") ? "command" : "text",
|
|
79
94
|
text: body,
|
|
80
95
|
messageId,
|
|
81
|
-
from: { id:
|
|
96
|
+
from: { id: speaker, name: speaker === WEB_OWNER_ID ? "Owner" : "", username: "" },
|
|
82
97
|
raw: { source: "web" },
|
|
83
98
|
});
|
|
84
99
|
return messageId;
|
|
85
100
|
}
|
|
86
101
|
|
|
87
|
-
postAction({ id, payload, messageId } = {}) {
|
|
102
|
+
postAction({ id, payload, messageId } = {}, { userId } = {}) {
|
|
88
103
|
const p = String(payload != null ? payload : (id != null ? id : ""));
|
|
89
104
|
if (!p) return null;
|
|
105
|
+
const speaker = webSpeaker(userId);
|
|
90
106
|
const callbackId = this._mkId("cb");
|
|
91
107
|
this._emit("action", {
|
|
92
108
|
adapter: this,
|
|
93
|
-
channelId:
|
|
94
|
-
canonicalUserId: canonicalForChannel("web",
|
|
95
|
-
userId:
|
|
109
|
+
channelId: speaker,
|
|
110
|
+
canonicalUserId: canonicalForChannel("web", speaker),
|
|
111
|
+
userId: speaker,
|
|
96
112
|
type: "action",
|
|
97
113
|
messageId,
|
|
98
|
-
from: { id:
|
|
114
|
+
from: { id: speaker, name: speaker === WEB_OWNER_ID ? "Owner" : "", username: "" },
|
|
99
115
|
action: {
|
|
100
116
|
buttonId: String(id != null ? id : p),
|
|
101
117
|
payload: p,
|
|
@@ -260,4 +276,4 @@ class WebAdapter {
|
|
|
260
276
|
}
|
|
261
277
|
}
|
|
262
278
|
|
|
263
|
-
module.exports = { WebAdapter, WEB_OWNER_ID };
|
|
279
|
+
module.exports = { WebAdapter, WEB_OWNER_ID, WEB_GUEST_ID };
|
package/core/access.js
CHANGED
|
@@ -14,6 +14,14 @@ const { atomicWriteFileSync, readJsonWithFallback, withFileLock } = require("./f
|
|
|
14
14
|
// owner is identified by the Kazee user id. Resolve via env at call time so
|
|
15
15
|
// /channel add can update the value without a process restart.
|
|
16
16
|
function transportOwnerUserId(transport) {
|
|
17
|
+
// A personal bot's dashboard and voice bridge are ways to reach its one
|
|
18
|
+
// owner (Central's), not owners of their own: they count only when they
|
|
19
|
+
// name her.
|
|
20
|
+
const personalOwner = personalBotOwner();
|
|
21
|
+
if (personalOwner !== null && (transport === "web" || transport === "voice")) {
|
|
22
|
+
if (transport === "web") return personalOwner;
|
|
23
|
+
return String(config.VOICE_OWNER_USER_ID || "") === personalOwner ? personalOwner : "";
|
|
24
|
+
}
|
|
17
25
|
if (transport === "kazee") return config.KAZEE_OWNER_USER_ID || "";
|
|
18
26
|
// Spaces actors are central/Kazee user ids, so the owner carries the same id
|
|
19
27
|
// there. Without this the owner's own task comments authorize as a stranger
|
|
@@ -29,6 +37,16 @@ function transportOwnerUserId(transport) {
|
|
|
29
37
|
return "";
|
|
30
38
|
}
|
|
31
39
|
|
|
40
|
+
// Central's owner of a personal bot ("" when not yet known), or null for an
|
|
41
|
+
// autonomous bot.
|
|
42
|
+
function personalBotOwner() {
|
|
43
|
+
try {
|
|
44
|
+
const botProfile = require("./bot-profile");
|
|
45
|
+
if (botProfile.isPersonal()) return botProfile.ownerUserId();
|
|
46
|
+
} catch (e) {}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
32
50
|
function matchesTransportOwner() {
|
|
33
51
|
const transport = currentTransport();
|
|
34
52
|
const userId = currentUserId();
|
|
@@ -109,6 +127,13 @@ function isChatAuthorized(chatId, envelope) {
|
|
|
109
127
|
// member of that group to owner.
|
|
110
128
|
function isChatOwner(chatId) {
|
|
111
129
|
if (matchesTransportOwner()) return true;
|
|
130
|
+
// A personal bot has exactly one owner, the person in Central
|
|
131
|
+
// (core/bot-profile.js, applied to KAZEE_OWNER_USER_ID). The second-owner
|
|
132
|
+
// setup — people records, auth.json or chat ids marked owner — does not
|
|
133
|
+
// apply: admins look after the bot through Agent Space and Central.
|
|
134
|
+
try {
|
|
135
|
+
if (require("./bot-profile").isPersonal()) return false;
|
|
136
|
+
} catch (e) {}
|
|
112
137
|
const inGroup = currentIsGroup();
|
|
113
138
|
const speakerId = String(currentUserId() || "");
|
|
114
139
|
// In a group only the speaker counts; in a DM the passed chat id is the
|
package/core/app-pass.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// One pass helper for every app call (plan v2 "personal bots act for their
|
|
2
|
+
// owner", 25 Oct).
|
|
3
|
+
//
|
|
4
|
+
// A personal bot never hands its own key (KAZEE_BOT_TOKEN) to an app. For each
|
|
5
|
+
// app it trades the key for a 10-minute pass at Central (POST /api/bots/pass),
|
|
6
|
+
// reuses it until just before it runs out, then gets a new one. The pass acts
|
|
7
|
+
// as its owner: the app sees "Cynthia, via smart-cynthia" with the AI mark.
|
|
8
|
+
// An autonomous bot, or one whose Central has no bot:me yet, keeps using its
|
|
9
|
+
// own key, unchanged.
|
|
10
|
+
//
|
|
11
|
+
// A refused pass throws PassRefused with a plain message telling the owner how
|
|
12
|
+
// to switch it on; the owner hears it at most once a day per app and reason
|
|
13
|
+
// (bot.js sends it as a one-tap card, core/owner-setup.js).
|
|
14
|
+
//
|
|
15
|
+
// Parent process only, like the key itself (a CONFINED secret never passed to
|
|
16
|
+
// a provider or tool subprocess).
|
|
17
|
+
|
|
18
|
+
const botProfile = require("./bot-profile");
|
|
19
|
+
|
|
20
|
+
// Get a new pass this long before the old one runs out.
|
|
21
|
+
const RENEW_BEFORE_MS = 60 * 1000;
|
|
22
|
+
const TELL_OWNER_EVERY_MS = 24 * 60 * 60 * 1000;
|
|
23
|
+
|
|
24
|
+
// Central's Application.name -> what the owner calls it.
|
|
25
|
+
const APP_LABELS = {
|
|
26
|
+
"kazee-spaces-backend": "Spaces",
|
|
27
|
+
"kazee-crm": "CRM",
|
|
28
|
+
crm: "CRM",
|
|
29
|
+
Ticket_Central: "Tickets",
|
|
30
|
+
"agent-space-backend": "Agent Space",
|
|
31
|
+
"kazee-infrastructure": "Infrastructure",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const passes = new Map(); // applicationName -> { token, expiresAt }
|
|
35
|
+
const inflight = new Map(); // applicationName -> Promise<string>
|
|
36
|
+
const told = new Map(); // `${app}:${reason}` -> last told at
|
|
37
|
+
let ownerNotifier = null;
|
|
38
|
+
|
|
39
|
+
class PassRefused extends Error {
|
|
40
|
+
constructor(applicationName, reason, ownerMessage, status) {
|
|
41
|
+
super(`Central refused a pass for ${applicationName}: ${reason}`);
|
|
42
|
+
this.applicationName = applicationName;
|
|
43
|
+
this.reason = reason;
|
|
44
|
+
this.ownerMessage = ownerMessage;
|
|
45
|
+
this.status = status;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function appLabel(applicationName) {
|
|
50
|
+
return APP_LABELS[applicationName] || applicationName;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// What the owner reads when Central refuses a pass.
|
|
54
|
+
function ownerHelp(reason, applicationName) {
|
|
55
|
+
const first = botProfile.ownerFirstName();
|
|
56
|
+
const app = appLabel(applicationName);
|
|
57
|
+
const where = botProfile.MY_ASSISTANTS;
|
|
58
|
+
switch (reason) {
|
|
59
|
+
case "consent-off":
|
|
60
|
+
return `${first}, my permission to act for you is switched off, so I can't work in ${app} for you. To switch it back on: ${where}, tap Let it act for me and tick ${app}.`;
|
|
61
|
+
case "consent-ended":
|
|
62
|
+
return `${first}, your permission for me to act for you has ended, so I can't work in ${app}. Renew it with one tap in ${where}.`;
|
|
63
|
+
case "app-not-ticked":
|
|
64
|
+
return `${first}, you haven't let me act for you in ${app}. To allow it: ${where}, and tick ${app}.`;
|
|
65
|
+
case "owner-no-access":
|
|
66
|
+
return `${first}, your own account can't use ${app} right now, so I can't either. Ask your admin for access.`;
|
|
67
|
+
case "owner-inactive":
|
|
68
|
+
return `I can't act in ${app}: Central says my owner's account is not active.`;
|
|
69
|
+
case "bot-disabled":
|
|
70
|
+
return `I can't act in ${app}: an admin has switched me off in Central.`;
|
|
71
|
+
case "consent-none":
|
|
72
|
+
default:
|
|
73
|
+
return `${first}, I can't work in ${app} for you yet. To let me act for you: open ${where}, tap Let it act for me and tick ${app}.`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function setOwnerNotifier(fn) {
|
|
78
|
+
ownerNotifier = typeof fn === "function" ? fn : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function tellOwnerOnce(err, now) {
|
|
82
|
+
if (!ownerNotifier) return;
|
|
83
|
+
const key = `${err.applicationName}:${err.reason}`;
|
|
84
|
+
const last = told.get(key) || 0;
|
|
85
|
+
if (now - last < TELL_OWNER_EVERY_MS) return;
|
|
86
|
+
told.set(key, now);
|
|
87
|
+
try { await ownerNotifier(err.ownerMessage, err); }
|
|
88
|
+
catch (e) { console.error("app-pass: could not tell the owner:", e.message); }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function fetchPass(applicationName, request, now) {
|
|
92
|
+
const token = botProfile.botToken();
|
|
93
|
+
if (!token) throw new Error("KAZEE_BOT_TOKEN unset");
|
|
94
|
+
const res = await request("POST", "/api/bots/pass", token, { applicationName });
|
|
95
|
+
const body = (res && res.body) || {};
|
|
96
|
+
if (res && (res.status === 200 || res.status === 201) && typeof body.pass === "string" && body.pass.startsWith("kzp_")) {
|
|
97
|
+
const expiresAt = Date.parse(body.expiresAt);
|
|
98
|
+
passes.set(applicationName, { token: body.pass, expiresAt: Number.isFinite(expiresAt) ? expiresAt : now() + 9 * 60 * 1000 });
|
|
99
|
+
return body.pass;
|
|
100
|
+
}
|
|
101
|
+
const reason = body.reason || (res && res.status === 401 ? "bot-key-invalid" : `http-${res && res.status}`);
|
|
102
|
+
const err = new PassRefused(applicationName, reason, ownerHelp(reason, applicationName), res && res.status);
|
|
103
|
+
await tellOwnerOnce(err, now());
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The token to present at `applicationName`: a pass for a personal bot, the
|
|
108
|
+
// bot's own key otherwise.
|
|
109
|
+
async function tokenFor(applicationName, { request = botProfile.centralRequest, now = Date.now } = {}) {
|
|
110
|
+
if (!botProfile.isPersonal()) return botProfile.botToken();
|
|
111
|
+
const held = passes.get(applicationName);
|
|
112
|
+
if (held && held.expiresAt - now() > RENEW_BEFORE_MS) return held.token;
|
|
113
|
+
if (inflight.has(applicationName)) return inflight.get(applicationName);
|
|
114
|
+
const p = fetchPass(applicationName, request, now).finally(() => inflight.delete(applicationName));
|
|
115
|
+
inflight.set(applicationName, p);
|
|
116
|
+
return p;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// A token getter for clients that take one (channels/spaces/client.js).
|
|
120
|
+
function provider(applicationName, opts) {
|
|
121
|
+
return () => tokenFor(applicationName, opts);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Drop a held pass (an app answered 401 to it).
|
|
125
|
+
function invalidate(applicationName) {
|
|
126
|
+
passes.delete(applicationName);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function reset() {
|
|
130
|
+
passes.clear();
|
|
131
|
+
inflight.clear();
|
|
132
|
+
told.clear();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
tokenFor,
|
|
137
|
+
provider,
|
|
138
|
+
invalidate,
|
|
139
|
+
reset,
|
|
140
|
+
setOwnerNotifier,
|
|
141
|
+
ownerHelp,
|
|
142
|
+
appLabel,
|
|
143
|
+
PassRefused,
|
|
144
|
+
RENEW_BEFORE_MS,
|
|
145
|
+
};
|