chore: v2.8.15 — Oh My Pi login sharing, omp --allow-home, Markdown Preview default, first-login password

- omp-share.js: an admin's switch (S3) runs omp's own auth broker + gateway as
  the host on loopback; a member's omp pane reaches it as a scoped client keyed
  by its pane token (clients file = live omp panes only), runs on
  --model agenthub/<model>, never holds a credential.
- omp catalog: --allow-home, so a workspace at the account's home keeps its
  sessions there (they were recorded under a temp dir and dropped).
- Settings → Appearance → Open Markdown as: Preview (default) or Raw, for
  Files/Changes, the config viewer and a chat's file panel.
- First-login password change and password rules, avatar URLs, Artifacts
  "Mine" includes installed shared skills/plugins, a member's own hook pages.
This commit is contained in:
2026-10-22 10:04:30 +07:00
parent 8a6dc6a29e
commit 750d74a2af
66 changed files with 3021 additions and 696 deletions
+30 -9
View File
@@ -39,7 +39,8 @@
* Share, unshare, install and uninstall are written to `audit_log`.
* - Editing a skill's name, description or instructions (`editSkill`) writes
* a NEW snapshot and swaps the row to it; the old snapshot is dropped, and
* copies already installed stay as they were (Reinstall picks it up).
* copies already installed stay as they were (uninstall and install again
* to take the new one).
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
@@ -149,7 +150,8 @@ function view(row, req, owners = null) {
}
/**
* The rows this request may see. `scope`: `mine` (own rows; every row in
* The rows this request may see. `scope`: `mine` (own library rows plus the
* shared skills/plugins of others the caller has installed; every row in
* local mode), `hub` (shared rows, server mode), anything else = all visible.
* Each row also carries `installed` (`installedCheck`).
*/
@@ -158,8 +160,12 @@ function list(req, { scope = "all", kind, q, laneId, sessionId, limit = 200 } =
const where = [v.where];
const params = [...v.params];
if (scope === "mine" && req?.user) {
where.push("a.owner_id = ? AND a.in_library = 1");
params.push(req.user.id);
// Their own library, plus what others shared that they installed (kept
// below only when `installed` — a disk read, so it cannot be SQL).
where.push(
"((a.owner_id = ? AND a.in_library = 1) OR (a.shared = 1 AND a.owner_id IS NOT ? AND a.kind IN ('skill', 'plugin')))"
);
params.push(req.user.id, req.user.id);
} else if (scope === "hub") {
where.push("a.shared = 1");
}
@@ -193,7 +199,8 @@ function list(req, { scope = "all", kind, q, laneId, sessionId, limit = 200 } =
).all(...params);
const owners = new Map();
const installed = installedCheck(req);
return rows.map((r) => ({ ...view(r, req, owners), installed: installed(r) }));
const out = rows.map((r) => ({ ...view(r, req, owners), installed: installed(r) }));
return scope === "mine" && req?.user ? out.filter((a) => a.mine || a.installed) : out;
}
// ── Small helpers ─────────────────────────────────────────────────────────
@@ -284,12 +291,25 @@ function addPage(req, { url, title, description, laneId, sessionId }) {
);
}
/**
* Who a hook's page belongs to when no workspace owns its session (a member's
* `claude` started from their own terminal): the S3 member the hook's
* connection came from (`hook-auth.js` `hookPeer`, by uid — never the body).
* The host's account is shared by every admin, so it names nobody.
*/
function peerOwner(peer) {
if (peer?.kind !== "member" || !peer.name) return null;
const row = sql("SELECT id FROM users WHERE username = ? AND disabled = 0").get(peer.name);
return row ? row.id : null;
}
/**
* The `Artifact` tool's PostToolUse hook: a publish answers with the page's
* claude.ai URL somewhere in its response. Recorded once per (owner, url),
* owned by the session's workspace owner. Returns the row or null.
* owned by the session's workspace owner, else by the member who sent the
* hook (`peerOwner`). Returns the row or null.
*/
function captureFromHook(sessionId, data) {
function captureFromHook(sessionId, data, peer = null) {
const input = data?.tool_input || {};
if (input.action && input.action !== "publish") return null;
const text = JSON.stringify(data?.tool_response ?? "");
@@ -297,7 +317,8 @@ function captureFromHook(sessionId, data) {
if (!m) return null;
const url = m[0].replace(/[\\"]+$/, "");
if (!PAGE_URL_RE.test(url)) return null;
const ownerId = sessionOwner(sessionId);
const laneId = sessionLane(sessionId);
const ownerId = laneId != null ? sessionOwner(sessionId) : peerOwner(peer);
const same = sql("SELECT * FROM artifacts WHERE kind = 'page' AND url = ? AND owner_id IS ?").get(
url,
ownerId
@@ -309,7 +330,7 @@ function captureFromHook(sessionId, data) {
title: cleanTitle(input.title, file ? path.basename(file) : "Artifact"),
description: cleanDescription(input.description),
owner_id: ownerId,
lane_id: sessionLane(sessionId),
lane_id: laneId,
session_id: String(sessionId),
source: file ? file.slice(0, 1000) : null,
url,