shadok-ai 0.17.8 → 0.17.9

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/dist/server.js CHANGED
@@ -42,7 +42,7 @@ import { computePace, paceBlock, WINDOW_SEC } from "./pace.js";
42
42
  import { getUsage } from "./usage.js";
43
43
  import { loadChannels, loadGroups, saveGroups, normalizeGroups, ensureMainGroup, upsertChannel, removeChannel, mergeClientChannels, channelWriteAllowed, profilePatch, loadTgGroup, saveTgGroup, isHomeChannel, homeAdoptionTarget, resolveSessionTarget, resolveResumeId, resumeTarget, } from "./channels.js";
44
44
  import { deliverWithRetry } from "./reload.js";
45
- import { startTelegram, renameTelegramTopic, closeTelegramTopic, probeToken, isStalePreface, announceLoggedOut, resetLoggedOutNotice, attachmentPrompt, pasteFileName, MEDIA_DIR, } from "./telegram.js";
45
+ import { startTelegram, renameTelegramTopic, closeTelegramTopic, probeToken, isStalePreface, announceLoggedOut, resetLoggedOutNotice, attachmentPrompt, pasteFileName, mediaDir, } from "./telegram.js";
46
46
  import { transcribe, audioClipExtension } from "./whisper.js";
47
47
  import { cancelPairingCode, issuePairingCode, pairingStatus } from "./tg-pairing.js";
48
48
  import { migrateTgBindings } from "./channels.js";
@@ -1377,8 +1377,9 @@ app.post("/paste", express.raw({ type: () => true, limit: PASTE_LIMIT }), (req,
1377
1377
  const name = decodeURIComponent(String(req.headers["x-filename"] ?? ""));
1378
1378
  const kind = contentType.trim().toLowerCase().startsWith("image/") ? "image" : "file";
1379
1379
  try {
1380
- fs.mkdirSync(MEDIA_DIR, { recursive: true });
1381
- const file = path.join(MEDIA_DIR, pasteFileName(randomUUID(), name, contentType));
1380
+ const dir = mediaDir();
1381
+ fs.mkdirSync(dir, { recursive: true });
1382
+ const file = path.join(dir, pasteFileName(randomUUID(), name, contentType));
1382
1383
  fs.writeFileSync(file, body);
1383
1384
  // The very wording Telegram already uses, so an agent sees one format.
1384
1385
  res.json({ path: file, line: attachmentPrompt([{ path: file, kind }]) });
@@ -2246,6 +2247,51 @@ function channelStateHome(chan) {
2246
2247
  }
2247
2248
  /** Task 12: the one shared pot every group HOME links as `~/shared`. */
2248
2249
  const SHARED_POT_PATH = (process.env.SHADOK_SHARED_POT_PATH ?? "").trim() || DEFAULT_SHARED_POT_PATH;
2250
+ // Multi-tenant: Telegram/paste attachments must be readable by a privilege-
2251
+ // dropped agent, but the default `~/.shadok-ai/media` is `/root/.shadok-ai/media`
2252
+ // — and `/root` (mode 700) is un-traversable by the `main`/`sk-*` drop users, so
2253
+ // an agent handed `[Attached image: /root/.shadok-ai/media/…]` cannot open it
2254
+ // (invariant 54's family). Relocate media under the shared pot (which every drop
2255
+ // user is in `shadok-shared` to traverse), carry existing attachments over so
2256
+ // already-sent images keep resolving, and point `mediaDir()` there via the env.
2257
+ // Best-effort: a failure just leaves media at `~/.shadok-ai/media`, the old
2258
+ // (broken-for-agents) behaviour, never a boot failure. Mono-tenant is untouched.
2259
+ if (DEPLOYMENT_MODE === "multi-tenant") {
2260
+ try {
2261
+ const shared = path.join(SHARED_POT_PATH, "media");
2262
+ fs.mkdirSync(shared, { recursive: true });
2263
+ // Own it to the pot's group (`shadok-shared`) with setgid, so files the
2264
+ // server writes inherit that group and the drop users can read them.
2265
+ try {
2266
+ const potGid = fs.statSync(SHARED_POT_PATH).gid;
2267
+ fs.chownSync(shared, 0, potGid);
2268
+ fs.chmodSync(shared, 0o2775);
2269
+ }
2270
+ catch {
2271
+ /* pot not provisioned yet (pre-migration) — the next boot fixes it */
2272
+ }
2273
+ const legacy = path.join(os.homedir(), ".shadok-ai", "media");
2274
+ if (legacy !== shared && fs.existsSync(legacy)) {
2275
+ for (const name of fs.readdirSync(legacy)) {
2276
+ const dst = path.join(shared, name);
2277
+ if (!fs.existsSync(dst)) {
2278
+ try {
2279
+ fs.copyFileSync(path.join(legacy, name), dst);
2280
+ }
2281
+ catch {
2282
+ /* skip an unreadable/odd entry */
2283
+ }
2284
+ }
2285
+ }
2286
+ }
2287
+ process.env.SHADOK_MEDIA_DIR = shared;
2288
+ console.log(`media: using ${shared} (reachable by dropped agents)`);
2289
+ }
2290
+ catch (e) {
2291
+ console.warn("media: could not set up the shared media dir, keeping ~/.shadok-ai/media: " +
2292
+ (e instanceof Error ? e.message : String(e)));
2293
+ }
2294
+ }
2249
2295
  /** Run a privileged account command. Root runs it directly; otherwise go through
2250
2296
  * NON-interactive sudo (`-n`) so a MISSING right fails fast with a non-zero exit
2251
2297
  * instead of prompting — the T3 false positive (sudoers allows `true` but not
@@ -4289,8 +4335,9 @@ app.get("/download", (req, res) => {
4289
4335
  // same `nosniff` + `sandbox` CSP as /download neutralises an SVG opened directly.
4290
4336
  app.get("/media/:name", (req, res) => {
4291
4337
  const base = path.basename(String(req.params.name ?? ""));
4292
- const file = path.join(MEDIA_DIR, base);
4293
- if (!file.startsWith(path.join(MEDIA_DIR, path.sep)) && path.dirname(file) !== MEDIA_DIR) {
4338
+ const dir = mediaDir();
4339
+ const file = path.join(dir, base);
4340
+ if (!file.startsWith(path.join(dir, path.sep)) && path.dirname(file) !== dir) {
4294
4341
  return res.status(400).type("text").send("bad name");
4295
4342
  }
4296
4343
  let stat;