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
+23
View File
@@ -733,6 +733,8 @@ export type AuthUser = {
/** When the account's avatar was last set (null = none) — versions its URL. */
avatarAt?: string | null;
createdAt?: string;
/** An admin gave this password: the user must choose their own before anything else. */
mustChangePassword?: boolean;
};
export type AuthState = { mode: "local" } | { mode: "server"; user: AuthUser | null };
/** GET /api/me/prefs — the signed-in account's own UI preferences (localStorage keys). */
@@ -1007,6 +1009,9 @@ export const api = {
body: file,
headers: { "Content-Type": "application/octet-stream" },
}),
/** POST /api/me/avatar/url — the server downloads the picture from an https link. */
setAvatarFromUrl: (url: string) =>
request<MyAccount>("/me/avatar/url", { method: "POST", body: JSON.stringify({ url }) }),
/** DELETE /api/me/avatar — back to the initials. */
removeAvatar: () => request<MyAccount>("/me/avatar", { method: "DELETE" }),
},
@@ -3051,6 +3056,14 @@ export const api = {
aiTools: {
/** GET /api/ai-tools - the catalog, what is installed, enabled, and the default. */
list: () => request<AiToolsView>("/ai-tools"),
/** GET /api/ai-tools/omp-sharing - the host's Oh My Pi login shared with members (S3). */
ompSharing: () => request<OmpSharingState>("/ai-tools/omp-sharing"),
/** PUT /api/ai-tools/omp-sharing - an admin's switch. */
setOmpSharing: (enabled: boolean) =>
request<OmpSharingState>("/ai-tools/omp-sharing", {
method: "PUT",
body: JSON.stringify({ enabled }),
}),
/** POST /api/ai-tools/refresh - re-read the login shell's PATH and re-detect. */
refresh: () => request<AiToolsView>("/ai-tools/refresh", { method: "POST" }),
/** PUT /api/ai-tools/:id - enable/disable a tool, make it the default, or set its default model ("" clears). */
@@ -4307,6 +4320,16 @@ export interface ArtifactCandidate {
pinned: boolean;
}
/** `GET /api/ai-tools/omp-sharing` — never a token. */
export interface OmpSharingState {
/** Server mode with per-user Linux accounts (S3): the only place it applies. */
available: boolean;
enabled: boolean;
installed: boolean;
broker: { running: boolean; error?: string | null };
gateway: { running: boolean; port?: number; error?: string | null };
}
export interface ArtifactSettings {
serverMode: boolean;
canShare: boolean;
+29
View File
@@ -0,0 +1,29 @@
/**
* @file markdownView — Settings → Appearance → "Open Markdown as": a viewer
* preference in `localStorage` (`agenthub.markdown.view`, synced to the
* account through `lib/userPrefs.ts`). Preview (the default) opens a `.md`
* file rendered; Raw opens its source. Read each time a viewer opens a file
* (the Files/Changes panel, the config file viewer, a chat artifact); the
* viewer's own Preview/Raw toggle still flips that one file.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
export const MARKDOWN_VIEW_KEY = "agenthub.markdown.view";
/** True when a Markdown file opens rendered (the default). */
export function readMarkdownPreview(): boolean {
try {
return window.localStorage.getItem(MARKDOWN_VIEW_KEY) !== "raw";
} catch {
return true;
}
}
export function writeMarkdownPreview(on: boolean) {
try {
if (on) window.localStorage.removeItem(MARKDOWN_VIEW_KEY);
else window.localStorage.setItem(MARKDOWN_VIEW_KEY, "raw");
} catch {
/* storage blocked — the default */
}
}
+26
View File
@@ -0,0 +1,26 @@
/**
* @file passwordRules.ts
* @description The rules a password an account picks for ITSELF must pass —
* a copy of `passwordProblems` in server/lib/auth.js, so the checklist can
* tick each one as it is typed. The server's copy is the one that decides
* (EWEAKPASSWORD); a password an admin gives someone else has no rules.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
export const PASSWORD_MIN_LENGTH = 8;
export const PASSWORD_RULES = ["length", "lower", "upper", "digit", "symbol", "username"] as const;
export type PasswordRule = (typeof PASSWORD_RULES)[number];
/** The rules `password` fails (empty = it passes every one). */
export function passwordProblems(password: string, username?: string): PasswordRule[] {
const out: PasswordRule[] = [];
if ([...password].length < PASSWORD_MIN_LENGTH) out.push("length");
if (!/\p{Ll}/u.test(password)) out.push("lower");
if (!/\p{Lu}/u.test(password)) out.push("upper");
if (!/\p{N}/u.test(password)) out.push("digit");
if (!/[^\p{L}\p{N}]/u.test(password)) out.push("symbol");
const name = (username || "").toLowerCase();
if (name && password.toLowerCase().includes(name)) out.push("username");
return out;
}
+1
View File
@@ -43,6 +43,7 @@ export const PREF_KEYS = [
"agenthub.codex.bypass",
"agenthub.tour",
"agenthub.workspace.allTabs",
"agenthub.markdown.view",
] as const;
const SYNCED = new Set<string>(PREF_KEYS);
const PUSH_DELAY_MS = 400;