Skip to content

Personal edition: sync from internal + decouple infra + harden distribution endpoints#323

Merged
floodsung merged 219 commits into
mainfrom
personal-edition-preview
Jun 15, 2026
Merged

Personal edition: sync from internal + decouple infra + harden distribution endpoints#323
floodsung merged 219 commits into
mainfrom
personal-edition-preview

Conversation

@floodsung

Copy link
Copy Markdown
Contributor

Brings GitHub `main` up to date with the internal source of truth and turns this repo into the self-hostable personal edition.

What this does

  • Syncs ~214 internal commits since 2026-05-25 (GitHub main was at `0d083ac`). The two GitHub-only commits (fix(claude): auto-approve ExitPlanMode via canUseTool #312 ExitPlanMode auto-approve, fix(bridge): show full agent activity content in Feishu card #314 full agent activity content) are already present here via their internal equivalents; `main` is merged in as an ancestor so this is a clean fast-forward.
  • Decouples from internal infra: removes Feilian/SSO + internal-host coupling. The personal edition runs locally with a single API token — no SSO, no corporate VPN.
  • Fixes hardcoded internal defaults that would break external installs: `METABOT_CORE_URL` → `http://localhost:9200\`; data/chat dirs `/vepfs/...` → `~/.metabot-core/...`; installers clone from GitHub.
  • Security hardening: `/cli/` and `/install/` install endpoints (previously anonymous behind a corporate VPN) are now token-gated by default, opt back in with `METABOT_PUBLIC_DISTRIBUTION=1`. A path-normalization bypass (`/cli//install.sh`) was found in review and fixed with defense-in-depth + regression tests.
  • Docs: README/getting-started/configuration rewritten for the personal edition; added a self-host/auth section.

Verification

  • root + packages/server + web-ui typecheck clean
  • 594 bridge + 359 server tests pass
  • Reviewed by a multi-agent workflow (4 dimensions + adversarial verify); all confirmed findings fixed.

🤖 Generated with Claude Code

floodsung and others added 30 commits May 18, 2026 06:35
First MR on the internal GitLab mirror. Updates CLAUDE.md to reflect the
new workflow (GitLab REST API instead of gh pr), adds Repo / Remotes
docs, and ships an internal .gitlab-ci.yml + .gitleaks.toml so the rest
of the project lifecycle stays on internal infra.

See MR !1 for details.
feat(core-integration): Phase 4 — delete embedded memory + skill-hub, retarget to metabot-core

See merge request xvirobotics/metabot!2
feat(cli): unified `metabot` dispatcher (bridge + metabot-core feature CLI)

See merge request xvirobotics/metabot!4
feat(install): metabot-core era — drop embedded MetaMemory + dead skills

See merge request xvirobotics/metabot!5
fix(install): symlink-safe cp guard for lark-cli mirror loops

See merge request xvirobotics/metabot!6
feat(agent-bus): drop per-bot talkSecret; visibility is the permission

See merge request xvirobotics/metabot!7
chore: monorepo merge — subtree + workspaces + boundary triple-line

See merge request xvirobotics/metabot!8
docs: monorepo sweep — CLAUDE.md, READMEs, architecture, CHANGELOG

See merge request xvirobotics/metabot!9
refactor(install+cli): in-tree core CLI resolution + selective workspace install

See merge request xvirobotics/metabot!10
fix(memory-cli): default create/mkdir into caller namespace, add --path

See merge request xvirobotics/metabot!11
refactor(cli): fold mb into metabot — single CLI binary

See merge request xvirobotics/metabot!12
chore(cli): remove dead bin/mm + bot-skills surface

See merge request xvirobotics/metabot!13
fix(web-ui): memory tab 404s on folder navigation

See merge request xvirobotics/metabot!14
MR !14 fixed multi-segment path lookups but missed single-segment
top-level folders (/shared, /t5t). On the browser cookie path,
oauth2-proxy/Caddy collapses the `//` route-prefix boundary and strips
the path's leading slash; decodeMemoryIdOrPath only re-added it when the
slice contained an interior slash, so a bare `shared` was treated as a
UUID id and findFolderById missed → 404.

Disambiguate by UUID shape instead: anything not UUID-shaped is a path
that lost its leading slash. Preserves genuine id lookups.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(web-ui): memory tab 404s on top-level folder navigation

See merge request xvirobotics/metabot!15
Flood Sung and others added 29 commits June 10, 2026 08:43
…sure, card dedup

- message-bridge: TTL sweep + executor-removed cleanup for chatId-keyed
  maps (recentQuestionCard, exitPlanCardsShown); destroy() clears all
  timers/buffers; new async destroyAsync() awaits executor shutdown
- executor-registry: crashed executors are parked and resurrected by
  resuming the same Claude session (500ms..30s backoff, 3 attempts);
  intentional release/LRU eviction never triggers respawn
- index: graceful shutdown awaits bridge teardown (15s cap) before exit
- peer-manager: forwardTask() only targets known/verified peers;
  optional METABOT_ALLOWED_PEER_CIDRS constraint; rejected forwards logged
- http-server: /api/health returns minimal liveness only; rich payload
  moved to authenticated /api/status
- feishu: dedup card-builder/card-builder-v2 shared constants+helpers
  into card-builder-utils

Tests: 453 passed (46 files), incl. new crash-resurrection (7) and
SSRF validation (4) tests. tsc -p tsconfig.bridge.json clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hardened /api/health handler returns only minimal liveness info by
design, but the auth middleware ran before it and rejected
unauthenticated probes with 401. Verified live on a trunks-only test
bridge: /api/health 200 without auth, /api/status 401 without / 200
with the API secret.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- New request-rate-limiter: per-IP sliding window (300 req/min global,
  10 failed auths -> 60s lockout), LRU-capped + swept bookkeeping,
  METABOT_RATE_LIMIT_MAX / _AUTH_FAILS / _DISABLED env knobs,
  GET /api/health exempt, 429 with Retry-After
- API secret and WS token comparisons now use sha256 +
  crypto.timingSafeEqual (no timing/length side channels)
- /api/status adds memory rss/heapUsed, executor pool total/active,
  rate-limiter tracked IPs

Live-tested on trunks test bridge: lockout after 10 fails, health
exempt during lockout, 60s self-heal, /api/talk E2E green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
99 new tests: /status /model /memory /sync command handling (43),
agent-team-store CRUD/lifecycle/cascade (37), one-time task
scheduling/persistence/retry (19). Scheduler tests isolate
SESSION_STORE_DIR to a temp dir so they never race a live bridge
writing ~/.metabot/scheduled-tasks.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/api/health is now documented as unauthenticated minimal liveness;
rich diagnostics documented under authenticated /api/status; new
peer-forwarding CIDR allowlist env var added to .env.example and
the env reference (en/zh).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
require() inside vi.mock factory → async factory with dynamic import;
let → const for the never-reassigned removed arrays. Introduced by the
review-batch-1 merge (pipeline 342 on main failed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix: lint errors breaking main CI (executor-registry-crash.test.ts)

See merge request xvirobotics/metabot!96
VPN clients with smart split-tunneling (飞连) can capture *.feishu.cn
routes into a tunnel that is down while still claiming connected. Live
ws sockets held by an old process keep working, but the next
metabot restart cannot reconnect — so the failure surfaces right after
an upgrade and looks like the upgrade broke message delivery.

METABOT_LOCAL_ADDRESS=<ip> pins the source address of every Feishu
socket so the OS source-based-routes them out the interface owning that
IP, bypassing the tunnel:

- REST: set httpsAgent on the lark SDK's shared defaultHttpInstance,
  which every lark.Client here uses. Mutating it (instead of passing a
  custom httpInstance) keeps the SDK's interceptors intact — a custom
  axios instance must re-implement the response-unwrap interceptor or
  the SDK's internal tenant-token fetch silently breaks.
- wss: pass the same agent to lark.WSClient, whose agent option goes
  straight to the WebSocket constructor.

No undici global dispatcher: nothing here calls Feishu via native
fetch, and a global dispatcher would re-route peer/metabot-core
traffic out of scope. Unset env → nothing constructed, zero behavior
change.

Reported-by: shizhanxu

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat: METABOT_LOCAL_ADDRESS — bind Feishu sockets to a source IP (bypass 飞连 smart routing)

See merge request xvirobotics/metabot!95
…, durationMs, exec, atomic token)

Six confirmed defects from the 2026-06-15 repo review whose original fix
session was lost before commit:

1. T5T cross-project authz bypass: appendWipItem/appendTopFive adopted
   prev.project from a guessable id; now assert prev.project === input.project.
2. doc-sync update branch lacked try/catch; one failed update aborted the
   whole sync. Wrap and record the error per-document.
3. rate-limiter invoked fn()/pendingFn() un-awaited -> unhandled rejection.
4. message-bridge returned lastState.durationMs (never set) -> 'Duration: NaN s';
   use the locally computed durationMs in both return paths.
5. file-routes used execSync string interpolation for soffice -> execFileSync.
6. wechat-client token write was non-atomic -> tmp file + rename.

Typecheck clean (root + packages/server); 349 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix: review batch-2 hardening (t5t authz, sync, rate-limiter, durationMs, exec, atomic token)

See merge request xvirobotics/metabot!97
…s) + harden distribution endpoints

Prepares this public GitHub tree as a self-hostable personal edition,
synced from internal main. Changes:

Defaults (were hardcoded to internal hosts/paths — broke external installs):
- METABOT_CORE_URL defaults -> http://localhost:9200 across cli-core, cli,
  server, memory-client, bin/metabot, web-ui, and CLI help text.
- chat-store + server data dir defaults /vepfs/users/floodsung/... ->
  ~/.metabot-core/{chat-files,data}.
- installers (install.sh/.ps1, bootstrap.sh, install-cli.sh) clone from GitHub,
  default to localhost.

Security:
- /cli/* and /install/* distribution endpoints were anonymous on the assumption
  of a corporate VPN front door. Personal edition has no such fallback, so they
  are now token-gated by DEFAULT; set METABOT_PUBLIC_DISTRIBUTION=1 to opt back
  into anonymous serving. Tests updated (default-deny + opt-in + token paths).

Isolation:
- Removed internal-only docs (docs/internal/**, docs/metabot-core/**),
  Feilian/oauth2-proxy/systemd deploy units, internal .claude/plans, and the
  internal ROADMAP.
- Generalized deploy/install.sh + metabot-core.service to a generic self-host
  flow; reframed all Feilian/SSO references as an OPTIONAL proxy.
- Rewrote public docs (README, getting-started, configuration, SKILL) for a
  single local API token, no SSO/VPN.

Verified: root + packages/server + web-ui typecheck clean; 594 bridge + 355
server tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… roadmap TODOs

- Add '自托管 & 鉴权 / Self-Hosted & Auth' section (zh+en): local-first,
  single local API token, no SSO/VPN, distribution endpoints token-gated by
  default with METABOT_PUBLIC_DISTRIBUTION opt-in.
- Remove stale roadmap items: async agent protocol (already ships as the
  agent bus / Agent-to-Agent) and multi-tenant mode (counter to the personal
  edition's positioning).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n decoupling

From multi-agent review of personal-edition-preview:

SECURITY (high): the /cli/* + /install/* token gate used exact-path matching,
so slash variants (/cli//install.sh, /cli/install.sh/) skipped the gate but
were still served by the UI-host static layer (path.join collapses the slashes).
Fix: (1) normalize slashes before the gate match so variants are gated, and
(2) defense-in-depth — tryServeStatic now refuses to serve the cli/ and install/
subtrees entirely. Added regression tests (default host + UI host, both endpoints).

DOCS/ISOLATION:
- install.sh + .env.example onboarding no longer tells personal-edition users to
  'sign in via SSO' — token is auto-generated at ~/.metabot-core/token.
- Removed internal host leak teamclaude.xvirobotics.com from executor.ts comment.
- server README no longer references deploy files deleted in this branch
  (caddy/snippet.caddyfile, oauth2-proxy/*, cert-renew); TLS/SSO framed as BYO.
- Documented METABOT_PUBLIC_DISTRIBUTION in env-vars doc, server README, .env.example.
- Dropped stale 'absorbed docs/metabot-core/' README note + removed empty dir;
  fixed dangling docs/internal/web-ui-arch.md pointer in server.ts.

Verified: root+server+web-ui typecheck clean; 594 bridge + 359 server tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GitHub-only commits #312 (ExitPlanMode auto-approve) and #314 (full agent
activity content) are already present in this branch via their internal
equivalents (exit-plan-mode.ts is byte-identical; #314 = internal 8316195).
GitLab is the source of truth, so this branch's tree is kept verbatim (-s ours);
this merge only makes GitHub main an ancestor so the cutover is a clean
fast-forward.
The synced packages/cli + metamemory + skill-hub test suites import the
compiled entry of @xvirobotics/cli-core (exports -> dist/index.js). CI ran
`tsc --noEmit` (no emit) then `npm test`, so those imports failed to resolve
('Failed to resolve entry for package @xvirobotics/cli-core'). Add a build
step for the three thin libraries before tests. Pre-existing gap surfaced by
the workspace tests this sync brings to GitHub; not a content regression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@floodsung floodsung merged commit 664fbac into main Jun 15, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant