@inetafrica/open-claudia 2.1.0 → 2.1.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/bin/auth.js +62 -0
- package/bin/cli.js +9 -2
- package/core/access.js +32 -0
- package/core/audit.js +19 -2
- package/core/handlers.js +181 -3
- package/core/loopback.js +43 -4
- package/core/system-prompt.js +13 -1
- package/package.json +1 -1
package/bin/auth.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// open-claudia auth <subcommand>
|
|
2
|
+
// Owner-only view of the raw auth.json store and direct deauth. Use
|
|
3
|
+
// `open-claudia people` for richer roster operations. This is the
|
|
4
|
+
// low-level escape hatch for legacy entries that don't have a person
|
|
5
|
+
// record yet.
|
|
6
|
+
|
|
7
|
+
const { postJson } = require("./loopback-client");
|
|
8
|
+
|
|
9
|
+
const HELP = `
|
|
10
|
+
Raw authorization store.
|
|
11
|
+
|
|
12
|
+
open-claudia auth list
|
|
13
|
+
open-claudia auth revoke <chatId>
|
|
14
|
+
|
|
15
|
+
Owner-only. Revoking also unlinks the chat from any matching person.
|
|
16
|
+
Owner entries are protected — refuse to self-deauth.
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
async function runList() {
|
|
20
|
+
try {
|
|
21
|
+
const res = await postJson("auth-list", {});
|
|
22
|
+
const authorized = res.authorized || [];
|
|
23
|
+
const pending = res.pending || [];
|
|
24
|
+
if (authorized.length === 0 && pending.length === 0) { console.log("No authorized or pending chats."); process.exit(0); }
|
|
25
|
+
if (authorized.length) {
|
|
26
|
+
console.log("Authorized:");
|
|
27
|
+
for (const a of authorized) {
|
|
28
|
+
const owner = a.isOwner ? " (owner)" : "";
|
|
29
|
+
const person = a.personName ? ` → ${a.personName}` : " → (no person record)";
|
|
30
|
+
console.log(` ${a.chatId}${owner} ${a.name || a.username || ""}${person}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (pending.length) {
|
|
34
|
+
console.log("\nPending:");
|
|
35
|
+
for (const p of pending) console.log(` ${p.chatId} ${p.name || p.username || ""} (requested ${p.requestedAt})`);
|
|
36
|
+
}
|
|
37
|
+
process.exit(0);
|
|
38
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function runRevoke(args) {
|
|
42
|
+
const chatId = args[0];
|
|
43
|
+
if (!chatId) { console.error("Usage: auth revoke <chatId>"); process.exit(2); }
|
|
44
|
+
try {
|
|
45
|
+
const res = await postJson("auth-revoke", { chatId });
|
|
46
|
+
console.log(`Revoked ${chatId}${res.personId ? ` (was linked to person ${res.personId})` : ""}.`);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function run(args) {
|
|
52
|
+
const sub = args[0];
|
|
53
|
+
const rest = args.slice(1);
|
|
54
|
+
if (!sub || sub === "--help" || sub === "help") { console.log(HELP); process.exit(sub ? 0 : 2); }
|
|
55
|
+
switch (sub) {
|
|
56
|
+
case "list": case "ls": return runList();
|
|
57
|
+
case "revoke": case "rm": return runRevoke(rest);
|
|
58
|
+
default: console.error(`Unknown auth subcommand: ${sub}`); console.log(HELP); process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { run, HELP };
|
package/bin/cli.js
CHANGED
|
@@ -188,11 +188,17 @@ switch (command) {
|
|
|
188
188
|
break;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
case "auth":
|
|
192
|
-
|
|
191
|
+
case "auth": {
|
|
192
|
+
const sub = args[1];
|
|
193
|
+
if (["list", "ls", "revoke", "rm"].includes(sub)) {
|
|
194
|
+
require("./auth").run(args.slice(1));
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
// Legacy: open-claudia auth (no subcommand) → setup.js auth mode
|
|
193
198
|
process.argv = [process.argv[0], process.argv[1], "--auth"];
|
|
194
199
|
require(path.join(botDir, "setup.js"));
|
|
195
200
|
break;
|
|
201
|
+
}
|
|
196
202
|
|
|
197
203
|
case "stop": {
|
|
198
204
|
const pids = findBotProcesses();
|
|
@@ -318,6 +324,7 @@ Background work (only inside an active bot-spawned task):
|
|
|
318
324
|
Multi-user / cross-channel:
|
|
319
325
|
open-claudia people list|show|add|note|link Roster of known team members
|
|
320
326
|
open-claudia intros list|approve|reject Pending introductions from unknown chats
|
|
327
|
+
open-claudia auth list|revoke <chatId> Raw authorization view + direct deauth
|
|
321
328
|
open-claudia send-to --person "<name>" "<msg>" Relay a message to another team member
|
|
322
329
|
open-claudia recent --person "<name>" [--limit] Read recent activity from another chat
|
|
323
330
|
|
package/core/access.js
CHANGED
|
@@ -134,6 +134,36 @@ function recordPendingAuthRequest({ chatId, name, username }) {
|
|
|
134
134
|
return { status: "queued" };
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// Remove a chatId from the legacy auth.json. Owners are never removed
|
|
138
|
+
// this way to avoid self-locking; the caller has to handle owner
|
|
139
|
+
// demotion explicitly. Returns { ok, reason }.
|
|
140
|
+
function revokeChat(chatId) {
|
|
141
|
+
const id = String(chatId || "");
|
|
142
|
+
if (!id) return { ok: false, reason: "missing_id" };
|
|
143
|
+
const auth = loadAuth();
|
|
144
|
+
const before = auth.authorized.length;
|
|
145
|
+
auth.authorized = auth.authorized.filter((a) => {
|
|
146
|
+
if (String(a.chatId) !== id) return true;
|
|
147
|
+
if (a.isOwner === true) return true;
|
|
148
|
+
return false;
|
|
149
|
+
});
|
|
150
|
+
auth.pending = auth.pending.filter((p) => String(p.chatId) !== id);
|
|
151
|
+
if (auth.authorized.length === before) {
|
|
152
|
+
if (auth.pending.length === 0) return { ok: false, reason: "not_found" };
|
|
153
|
+
}
|
|
154
|
+
saveAuth(auth);
|
|
155
|
+
updateAuthorizedChatEnv(auth);
|
|
156
|
+
return { ok: true };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function listAuthorizedRaw() {
|
|
160
|
+
const auth = loadAuth();
|
|
161
|
+
return {
|
|
162
|
+
authorized: auth.authorized.slice(),
|
|
163
|
+
pending: auth.pending.slice(),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
137
167
|
function hasOwner() {
|
|
138
168
|
if (CHAT_ID) return true;
|
|
139
169
|
try {
|
|
@@ -173,4 +203,6 @@ module.exports = {
|
|
|
173
203
|
updateAuthorizedChatEnv,
|
|
174
204
|
hasOwner,
|
|
175
205
|
bootstrapOwner,
|
|
206
|
+
revokeChat,
|
|
207
|
+
listAuthorizedRaw,
|
|
176
208
|
};
|
package/core/audit.js
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
// Append-only audit log for security-sensitive events: people CRUD,
|
|
2
2
|
// intro decisions, cross-channel sends. One JSON record per line.
|
|
3
|
+
// Auto-rotates when the file exceeds MAX_BYTES — the current file is
|
|
4
|
+
// renamed to <name>.1 (overwriting any previous archive) and a fresh
|
|
5
|
+
// file starts.
|
|
3
6
|
|
|
4
7
|
const fs = require("fs");
|
|
5
8
|
const { AUDIT_FILE } = require("./config");
|
|
6
9
|
|
|
10
|
+
const MAX_BYTES = 2 * 1024 * 1024; // 2 MB; rotate at this size
|
|
11
|
+
|
|
12
|
+
function rotateIfNeeded() {
|
|
13
|
+
try {
|
|
14
|
+
const stat = fs.statSync(AUDIT_FILE);
|
|
15
|
+
if (stat.size > MAX_BYTES) {
|
|
16
|
+
try { fs.renameSync(AUDIT_FILE, `${AUDIT_FILE}.1`); } catch (e) {}
|
|
17
|
+
}
|
|
18
|
+
} catch (e) {} // file may not exist yet
|
|
19
|
+
}
|
|
20
|
+
|
|
7
21
|
function log(kind, payload) {
|
|
8
22
|
const entry = { at: new Date().toISOString(), kind, ...payload };
|
|
9
23
|
const line = JSON.stringify(entry) + "\n";
|
|
10
|
-
try {
|
|
24
|
+
try {
|
|
25
|
+
rotateIfNeeded();
|
|
26
|
+
fs.appendFileSync(AUDIT_FILE, line);
|
|
27
|
+
} catch (e) {}
|
|
11
28
|
return entry;
|
|
12
29
|
}
|
|
13
30
|
|
|
@@ -20,4 +37,4 @@ function tail(n = 50) {
|
|
|
20
37
|
} catch (e) { return []; }
|
|
21
38
|
}
|
|
22
39
|
|
|
23
|
-
module.exports = { log, tail };
|
|
40
|
+
module.exports = { log, tail, MAX_BYTES };
|
package/core/handlers.js
CHANGED
|
@@ -121,6 +121,7 @@ register({
|
|
|
121
121
|
"Session: /session /sessions /projects /continue /status /stop /end",
|
|
122
122
|
"Settings: /model /effort /budget /plan /compact /worktree /mode",
|
|
123
123
|
"Identity: /whoami /link",
|
|
124
|
+
"Team: /people /intros /auth (owner)",
|
|
124
125
|
"Automation: /cron /vault /soul",
|
|
125
126
|
"Claude auth: /auth_status /login /setup_token /use_oauth_token /clear_oauth_token",
|
|
126
127
|
"Codex auth: /codex_auth_status /codex_login /codex_setup_token",
|
|
@@ -133,9 +134,41 @@ register({
|
|
|
133
134
|
});
|
|
134
135
|
|
|
135
136
|
register({
|
|
136
|
-
name: "auth", description: "Request access
|
|
137
|
+
name: "auth", description: "Request access (or list/revoke as owner)", args: "[list | revoke <chatId>]",
|
|
137
138
|
authRequired: false,
|
|
138
|
-
handler: async (env) => {
|
|
139
|
+
handler: async (env, { tail }) => {
|
|
140
|
+
if (tail) {
|
|
141
|
+
if (!authorized(env)) return;
|
|
142
|
+
if (!ownerEnv(env)) return send("Owner only — auth list/revoke are restricted.");
|
|
143
|
+
if (tail === "list") {
|
|
144
|
+
const raw = accessStore.listAuthorizedRaw();
|
|
145
|
+
const lines = ["Authorized chats:"];
|
|
146
|
+
for (const a of raw.authorized) {
|
|
147
|
+
const person = peopleStore.findByHandle("telegram", a.chatId) || peopleStore.findByHandle("kazee", a.chatId);
|
|
148
|
+
lines.push(` ${a.chatId}${a.isOwner ? " (owner)" : ""} ${a.name || a.username || ""}${person ? " → " + person.name : " → (no person)"}`);
|
|
149
|
+
}
|
|
150
|
+
if (raw.authorized.length === 0) lines.push(" (none)");
|
|
151
|
+
if (raw.pending.length > 0) {
|
|
152
|
+
lines.push("", "Pending:");
|
|
153
|
+
for (const p of raw.pending) lines.push(` ${p.chatId} ${p.name || p.username || ""}`);
|
|
154
|
+
}
|
|
155
|
+
return send(lines.join("\n"));
|
|
156
|
+
}
|
|
157
|
+
const revokeMatch = tail.match(/^revoke\s+(\S+)$/i);
|
|
158
|
+
if (revokeMatch) {
|
|
159
|
+
const chatId = revokeMatch[1];
|
|
160
|
+
const person = peopleStore.findByHandle("telegram", chatId) || peopleStore.findByHandle("kazee", chatId);
|
|
161
|
+
if (person && person.isOwner) return send("Refusing to revoke owner chat.");
|
|
162
|
+
if (person) {
|
|
163
|
+
const detected = (person.handles || []).find((h) => String(h.channelId) === String(chatId));
|
|
164
|
+
if (detected) peopleStore.unlinkHandle(person.id, { adapter: detected.adapter, channelId: detected.channelId });
|
|
165
|
+
}
|
|
166
|
+
const result = accessStore.revokeChat(chatId);
|
|
167
|
+
if (!result.ok) return send(`Revoke failed: ${result.reason}`);
|
|
168
|
+
return send(`Revoked ${chatId}${person ? ` (was ${person.name})` : ""}.`);
|
|
169
|
+
}
|
|
170
|
+
return send("Usage: /auth | /auth list | /auth revoke <chatId>");
|
|
171
|
+
}
|
|
139
172
|
if (authorized(env)) {
|
|
140
173
|
send("You're already authorized.");
|
|
141
174
|
return;
|
|
@@ -1001,4 +1034,149 @@ register({
|
|
|
1001
1034
|
},
|
|
1002
1035
|
});
|
|
1003
1036
|
|
|
1004
|
-
|
|
1037
|
+
// ── Team (people / intros / auth) ─────────────────────────────────
|
|
1038
|
+
|
|
1039
|
+
const peopleStore = require("./people");
|
|
1040
|
+
const introsStore = require("./intros");
|
|
1041
|
+
const introFlow = require("./intro-flow");
|
|
1042
|
+
const accessStore = require("./access");
|
|
1043
|
+
|
|
1044
|
+
function formatPersonShort(p) {
|
|
1045
|
+
const handles = (p.handles || []).map((h) => `${h.adapter}:${h.channelId}`).join(", ") || "(no handles)";
|
|
1046
|
+
return `${p.name}${p.isOwner ? " (owner)" : ""} — ${handles}`;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
register({
|
|
1050
|
+
name: "people", description: "Manage team members", args: "[show|note|link|unlink|set-primary|add|remove ...]",
|
|
1051
|
+
handler: async (env, { tail }) => {
|
|
1052
|
+
if (!authorized(env)) return;
|
|
1053
|
+
if (!tail || tail === "list") {
|
|
1054
|
+
const all = peopleStore.list();
|
|
1055
|
+
if (all.length === 0) return send("No people yet.");
|
|
1056
|
+
const lines = ["Team:"];
|
|
1057
|
+
for (const p of all) lines.push(` ${formatPersonShort(p)}`);
|
|
1058
|
+
lines.push("", "More: /people show <name>, /people note <name> <text>");
|
|
1059
|
+
return send(lines.join("\n"));
|
|
1060
|
+
}
|
|
1061
|
+
const showMatch = tail.match(/^show\s+(.+)$/i);
|
|
1062
|
+
if (showMatch) {
|
|
1063
|
+
const ref = showMatch[1].trim();
|
|
1064
|
+
const person = peopleStore.findById(ref) || peopleStore.findByName(ref);
|
|
1065
|
+
if (!person) return send(`Not found: ${ref}`);
|
|
1066
|
+
const handles = (person.handles || []).map((h) => ` ${h.adapter}:${h.channelId}${person.primaryChannel && person.primaryChannel.adapter === h.adapter && String(person.primaryChannel.channelId) === String(h.channelId) ? " (primary)" : ""}`).join("\n");
|
|
1067
|
+
const notes = (person.notes || []).slice(-5).map((n) => ` - [${(n.at || "").slice(0, 10)}] ${n.text}`).join("\n");
|
|
1068
|
+
return send([
|
|
1069
|
+
`${person.name}${person.isOwner ? " (owner)" : ""} id=${person.id}`,
|
|
1070
|
+
person.bio ? `Bio: ${person.bio}` : null,
|
|
1071
|
+
"Handles:",
|
|
1072
|
+
handles || " (none)",
|
|
1073
|
+
notes ? "Notes:" : null,
|
|
1074
|
+
notes || null,
|
|
1075
|
+
].filter((x) => x !== null && x !== "").join("\n"));
|
|
1076
|
+
}
|
|
1077
|
+
const noteMatch = tail.match(/^note\s+(\S+)\s+(.+)$/i);
|
|
1078
|
+
if (noteMatch) {
|
|
1079
|
+
const person = peopleStore.findById(noteMatch[1]) || peopleStore.findByName(noteMatch[1]);
|
|
1080
|
+
if (!person) return send(`Not found: ${noteMatch[1]}`);
|
|
1081
|
+
const entry = peopleStore.note(person.id, noteMatch[2], { by: env.canonicalUserId || null });
|
|
1082
|
+
return send(`Noted on ${person.name}: ${entry.text}`);
|
|
1083
|
+
}
|
|
1084
|
+
if (!ownerEnv(env)) return send("Owner only — add/link/unlink/set-primary/remove.");
|
|
1085
|
+
const addMatch = tail.match(/^add\s+(.+)$/i);
|
|
1086
|
+
if (addMatch) {
|
|
1087
|
+
try {
|
|
1088
|
+
const p = peopleStore.add({ name: addMatch[1].trim() });
|
|
1089
|
+
return send(`Added ${p.name} (${p.id}). Link a channel with /people link ${p.name} <adapter> <channelId>.`);
|
|
1090
|
+
} catch (e) { return send(`Failed: ${e.message}`); }
|
|
1091
|
+
}
|
|
1092
|
+
const linkMatch = tail.match(/^link\s+(\S+)\s+(\S+)\s+(\S+)$/i);
|
|
1093
|
+
if (linkMatch) {
|
|
1094
|
+
const person = peopleStore.findById(linkMatch[1]) || peopleStore.findByName(linkMatch[1]);
|
|
1095
|
+
if (!person) return send(`Not found: ${linkMatch[1]}`);
|
|
1096
|
+
try {
|
|
1097
|
+
peopleStore.linkHandle(person.id, { adapter: linkMatch[2], channelId: linkMatch[3], approvedBy: env.canonicalUserId });
|
|
1098
|
+
return send(`Linked ${linkMatch[2]}:${linkMatch[3]} → ${person.name}`);
|
|
1099
|
+
} catch (e) { return send(`Failed: ${e.message}`); }
|
|
1100
|
+
}
|
|
1101
|
+
const unlinkMatch = tail.match(/^unlink\s+(\S+)\s+(\S+)\s+(\S+)$/i);
|
|
1102
|
+
if (unlinkMatch) {
|
|
1103
|
+
const person = peopleStore.findById(unlinkMatch[1]) || peopleStore.findByName(unlinkMatch[1]);
|
|
1104
|
+
if (!person) return send(`Not found: ${unlinkMatch[1]}`);
|
|
1105
|
+
const updated = peopleStore.unlinkHandle(person.id, { adapter: unlinkMatch[2], channelId: unlinkMatch[3] });
|
|
1106
|
+
if (!updated) return send("Handle not found on that person.");
|
|
1107
|
+
if (!person.isOwner) accessStore.revokeChat(unlinkMatch[3]);
|
|
1108
|
+
return send(`Unlinked ${unlinkMatch[2]}:${unlinkMatch[3]} from ${person.name}.`);
|
|
1109
|
+
}
|
|
1110
|
+
const setPrimaryMatch = tail.match(/^set-primary\s+(\S+)\s+(\S+)\s+(\S+)$/i);
|
|
1111
|
+
if (setPrimaryMatch) {
|
|
1112
|
+
const person = peopleStore.findById(setPrimaryMatch[1]) || peopleStore.findByName(setPrimaryMatch[1]);
|
|
1113
|
+
if (!person) return send(`Not found: ${setPrimaryMatch[1]}`);
|
|
1114
|
+
try {
|
|
1115
|
+
peopleStore.setPrimary(person.id, { adapter: setPrimaryMatch[2], channelId: setPrimaryMatch[3] });
|
|
1116
|
+
return send(`Primary for ${person.name} → ${setPrimaryMatch[2]}:${setPrimaryMatch[3]}`);
|
|
1117
|
+
} catch (e) { return send(`Failed: ${e.message}`); }
|
|
1118
|
+
}
|
|
1119
|
+
const removeMatch = tail.match(/^remove\s+(.+)$/i);
|
|
1120
|
+
if (removeMatch) {
|
|
1121
|
+
const person = peopleStore.findById(removeMatch[1]) || peopleStore.findByName(removeMatch[1]);
|
|
1122
|
+
if (!person) return send(`Not found: ${removeMatch[1]}`);
|
|
1123
|
+
if (person.isOwner) return send("Refusing to remove the owner.");
|
|
1124
|
+
const handles = (person.handles || []).slice();
|
|
1125
|
+
peopleStore.remove(person.id);
|
|
1126
|
+
let revokedCount = 0;
|
|
1127
|
+
for (const h of handles) { const r = accessStore.revokeChat(h.channelId); if (r.ok) revokedCount += 1; }
|
|
1128
|
+
return send(`Removed ${person.name}. Deauthed ${revokedCount} chat${revokedCount === 1 ? "" : "s"}.`);
|
|
1129
|
+
}
|
|
1130
|
+
send("Usage: /people | /people show <name> | /people note <name> <text> | /people add|link|unlink|set-primary|remove ... (owner)");
|
|
1131
|
+
},
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
register({
|
|
1135
|
+
name: "intros", description: "Pending intros from unknown chats", args: "[approve|reject <id>]", ownerOnly: true,
|
|
1136
|
+
handler: async (env, { tail }) => {
|
|
1137
|
+
if (!authorized(env)) return;
|
|
1138
|
+
if (!ownerEnv(env)) return send("Owner only.");
|
|
1139
|
+
introsStore.sweep();
|
|
1140
|
+
if (!tail || tail === "list") {
|
|
1141
|
+
const pending = introsStore.listPending();
|
|
1142
|
+
if (pending.length === 0) return send("No pending intros.");
|
|
1143
|
+
const lines = ["Pending intros:"];
|
|
1144
|
+
const buttons = [];
|
|
1145
|
+
for (const i of pending) {
|
|
1146
|
+
const claim = i.claim || {};
|
|
1147
|
+
let claimLine = "no claim yet";
|
|
1148
|
+
if (claim.kind === "existing") {
|
|
1149
|
+
const p = peopleStore.findById(claim.personId);
|
|
1150
|
+
claimLine = `claims to be ${p ? p.name : claim.personId}`;
|
|
1151
|
+
} else if (claim.kind === "new") {
|
|
1152
|
+
claimLine = `new: ${claim.name}${claim.bio ? " — " + claim.bio : ""}`;
|
|
1153
|
+
}
|
|
1154
|
+
lines.push(` ${i.id} ${i.adapter}:${i.channelId} ${claimLine}`);
|
|
1155
|
+
buttons.push([
|
|
1156
|
+
{ text: `Approve ${i.id.slice(-6)}`, callback_data: `intro:approve:${i.id}` },
|
|
1157
|
+
{ text: `Reject ${i.id.slice(-6)}`, callback_data: `intro:reject:${i.id}` },
|
|
1158
|
+
]);
|
|
1159
|
+
}
|
|
1160
|
+
return send(lines.join("\n"), buttons.length ? { keyboard: { inline_keyboard: buttons } } : undefined);
|
|
1161
|
+
}
|
|
1162
|
+
const approveMatch = tail.match(/^approve\s+(\S+)$/i);
|
|
1163
|
+
if (approveMatch) {
|
|
1164
|
+
const intro = introsStore.byId(approveMatch[1]);
|
|
1165
|
+
if (!intro) return send(`No intro: ${approveMatch[1]}`);
|
|
1166
|
+
try {
|
|
1167
|
+
const result = await introFlow.applyApproval(intro, env.from?.name || env.canonicalUserId || "owner");
|
|
1168
|
+
await introFlow.notifyIntroUser(intro, `You're in. Welcome${result.person ? ", " + result.person.name : ""}.`);
|
|
1169
|
+
return send(`Approved. Person: ${result.person?.name || "?"}`);
|
|
1170
|
+
} catch (e) { return send(`Failed: ${e.message}`); }
|
|
1171
|
+
}
|
|
1172
|
+
const rejectMatch = tail.match(/^reject\s+(\S+)(?:\s+(.+))?$/i);
|
|
1173
|
+
if (rejectMatch) {
|
|
1174
|
+
const intro = introsStore.byId(rejectMatch[1]);
|
|
1175
|
+
if (!intro) return send(`No intro: ${rejectMatch[1]}`);
|
|
1176
|
+
introsStore.reject(intro.id, env.from?.name || env.canonicalUserId || "owner", rejectMatch[2] || null);
|
|
1177
|
+
await introFlow.notifyIntroUser(intro, "Your access request was denied.");
|
|
1178
|
+
return send(`Rejected ${intro.id}.`);
|
|
1179
|
+
}
|
|
1180
|
+
send("Usage: /intros | /intros approve <id> | /intros reject <id> [reason]");
|
|
1181
|
+
},
|
|
1182
|
+
});
|
package/core/loopback.js
CHANGED
|
@@ -23,6 +23,7 @@ const intros = require("./intros");
|
|
|
23
23
|
const introFlow = require("./intro-flow");
|
|
24
24
|
const relay = require("./relay");
|
|
25
25
|
const audit = require("./audit");
|
|
26
|
+
const access = require("./access");
|
|
26
27
|
const fs2 = require("fs");
|
|
27
28
|
const path2 = require("path");
|
|
28
29
|
const { TRANSCRIPTS_DIR } = require("./config");
|
|
@@ -77,6 +78,7 @@ const JSON_KINDS = new Set([
|
|
|
77
78
|
"people-remove", "people-set-primary",
|
|
78
79
|
"intros-list", "intros-approve", "intros-reject",
|
|
79
80
|
"relay-send", "recent-fetch", "audit-tail",
|
|
81
|
+
"auth-list", "auth-revoke",
|
|
80
82
|
]);
|
|
81
83
|
|
|
82
84
|
function callerIsOwner(payload) {
|
|
@@ -305,8 +307,12 @@ async function handleJson(req, res, url, kind) {
|
|
|
305
307
|
if (!person) return reply(res, 404, { error: "not found" });
|
|
306
308
|
const updated = peopleStore.unlinkHandle(person.id, { adapter: payload.linkAdapter, channelId: payload.linkChannelId });
|
|
307
309
|
if (!updated) return reply(res, 404, { error: "handle not found on person" });
|
|
308
|
-
|
|
309
|
-
|
|
310
|
+
let revoked = null;
|
|
311
|
+
if (!person.isOwner) {
|
|
312
|
+
revoked = access.revokeChat(payload.linkChannelId);
|
|
313
|
+
}
|
|
314
|
+
audit.log("people.unlink", { by: payload.canonicalUserId, personId: person.id, adapter: payload.linkAdapter, channelId: payload.linkChannelId, authRevoked: revoked });
|
|
315
|
+
return reply(res, 200, { ok: true, person: updated, authRevoked: !!(revoked && revoked.ok) });
|
|
310
316
|
}
|
|
311
317
|
|
|
312
318
|
if (kind === "people-set-primary") {
|
|
@@ -329,9 +335,42 @@ async function handleJson(req, res, url, kind) {
|
|
|
329
335
|
const person = peopleStore.findById(ref) || peopleStore.findByName(ref);
|
|
330
336
|
if (!person) return reply(res, 404, { error: "not found" });
|
|
331
337
|
if (person.isOwner) return reply(res, 400, { error: "refusing to delete owner record" });
|
|
338
|
+
const handles = (person.handles || []).slice();
|
|
332
339
|
peopleStore.remove(person.id);
|
|
333
|
-
|
|
334
|
-
|
|
340
|
+
let revokedCount = 0;
|
|
341
|
+
for (const h of handles) {
|
|
342
|
+
const r = access.revokeChat(h.channelId);
|
|
343
|
+
if (r && r.ok) revokedCount += 1;
|
|
344
|
+
}
|
|
345
|
+
audit.log("people.remove", { by: payload.canonicalUserId, personId: person.id, name: person.name, revokedCount, handles });
|
|
346
|
+
return reply(res, 200, { ok: true, removed: person, revokedCount });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (kind === "auth-list") {
|
|
350
|
+
if (!callerIsOwner(payload)) return reply(res, 403, { error: "owner only" });
|
|
351
|
+
const raw = access.listAuthorizedRaw();
|
|
352
|
+
const enriched = raw.authorized.map((a) => {
|
|
353
|
+
const person = peopleStore.findByHandle("telegram", a.chatId) || peopleStore.findByHandle("kazee", a.chatId);
|
|
354
|
+
return { ...a, personId: person?.id || null, personName: person?.name || null };
|
|
355
|
+
});
|
|
356
|
+
return reply(res, 200, { ok: true, authorized: enriched, pending: raw.pending });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (kind === "auth-revoke") {
|
|
360
|
+
if (!callerIsOwner(payload)) return reply(res, 403, { error: "owner only" });
|
|
361
|
+
const chatId = payload.chatId;
|
|
362
|
+
if (!chatId) return reply(res, 400, { error: "missing chatId" });
|
|
363
|
+
const person = peopleStore.findByHandle("telegram", chatId) || peopleStore.findByHandle("kazee", chatId);
|
|
364
|
+
if (person && person.isOwner) return reply(res, 400, { error: "refusing to revoke owner chat" });
|
|
365
|
+
if (person) {
|
|
366
|
+
const detected = (person.handles || []).find((h) => String(h.channelId) === String(chatId));
|
|
367
|
+
if (detected) {
|
|
368
|
+
peopleStore.unlinkHandle(person.id, { adapter: detected.adapter, channelId: detected.channelId });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const result = access.revokeChat(chatId);
|
|
372
|
+
audit.log("auth.revoke", { by: payload.canonicalUserId, chatId, personId: person?.id || null, result });
|
|
373
|
+
return reply(res, result.ok ? 200 : 404, { ...result, personId: person?.id || null });
|
|
335
374
|
}
|
|
336
375
|
|
|
337
376
|
if (kind === "intros-list") {
|
package/core/system-prompt.js
CHANGED
|
@@ -11,6 +11,7 @@ const { vault } = require("./vault-store");
|
|
|
11
11
|
const { transcriptPointerNote } = require("./transcripts");
|
|
12
12
|
const tasksStore = require("./tasks");
|
|
13
13
|
const people = require("./people");
|
|
14
|
+
const commandsRegistry = require("./commands");
|
|
14
15
|
|
|
15
16
|
function loadSoul() {
|
|
16
17
|
try { return fs.readFileSync(SOUL_FILE, "utf-8"); }
|
|
@@ -63,6 +64,15 @@ function buildSystemPrompt() {
|
|
|
63
64
|
}
|
|
64
65
|
} catch (e) {}
|
|
65
66
|
|
|
67
|
+
let slashCommandsBlock = "";
|
|
68
|
+
try {
|
|
69
|
+
const cmds = commandsRegistry.publicCommands();
|
|
70
|
+
if (cmds.length > 0) {
|
|
71
|
+
const lines = cmds.map((c) => `- /${c.name}${c.args ? " " + c.args : ""}${c.description ? " — " + c.description : ""}`);
|
|
72
|
+
slashCommandsBlock = `\n## Slash commands available in chat\nThe user can type these directly. When a request maps to a slash command, prefer guiding them to use it rather than running open-claudia CLIs on their behalf. Owner-only commands are marked in their descriptions.\n\n${lines.join("\n")}\n`;
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {}
|
|
75
|
+
|
|
66
76
|
return `
|
|
67
77
|
${soul}
|
|
68
78
|
|
|
@@ -82,7 +92,7 @@ ${soul}
|
|
|
82
92
|
- Received user files directory: ${FILES_DIR}
|
|
83
93
|
|
|
84
94
|
${transcriptPointerNote(state)}
|
|
85
|
-
${currentSpeakerBlock}${teamBlock}${pendingTasksBlock}
|
|
95
|
+
${currentSpeakerBlock}${teamBlock}${pendingTasksBlock}${slashCommandsBlock}
|
|
86
96
|
## Delivery
|
|
87
97
|
Reply normally in your final answer. To send a file, image, or voice clip back to the current chat, run the bot CLI from inside this task — channel context is already in the env:
|
|
88
98
|
- \`open-claudia send-file <path> [caption]\` — any document/binary
|
|
@@ -101,6 +111,8 @@ Wake-ups and crons (real schedulers, not hallucinations — use them instead of
|
|
|
101
111
|
|
|
102
112
|
Persistent todo list with plans + subtasks (per channel; survives compaction and restart).
|
|
103
113
|
|
|
114
|
+
Note: any \`<system-reminder>\` you see about "task tools" / TaskCreate / TaskUpdate is from the underlying Claude Code harness — that's a different, ephemeral todo system. Open Claudia work belongs in \`open-claudia task\` (the persistent, channel-scoped one). Don't double-track.
|
|
115
|
+
|
|
104
116
|
Use a plan when work is likely to outlive a single turn — i.e. it may hit a compaction, a restart, or a scheduled wakeup before completing, or its progress is something the user will want to see between turns. The plan is a parent task whose children are the steps. As you work, mark each subtask in_progress when you begin and completed when done — this is how a resumed turn sees where you left off. If pending tasks already exist when you start a turn they will be shown under "## Pending tasks" above; check them first.
|
|
105
117
|
|
|
106
118
|
Skip the plan for work that visibly completes within the current turn (e.g. running a few CLI commands in sequence and replying). A plan there is overhead the user has to read past.
|
package/package.json
CHANGED