openscienceScience
§ 00 · Source-level Observatory

openscience 科研 Agent 工作台

OpenScience 不是单一聊天壳,而是把本地 server、SolidJS workspace、模型路由、科研数据库连接器和插件系统接成一个科研循环:读论文、写代码、跑实验、沉淀结果。

★ 1,411
GitHub Stars
~86K
LOC
500+
Files
active
Last commit
Browser Workspace ↓ HTTP + SSE Local Hono Server ↓ tools / models / science DBs Research Agent Runtime
§ 00 · OVERVIEW

它解决什么,凭什么值得拆

这个仓库值得拆,是因为它把“科研 Agent”从口号落到本地应用骨架:CLI 入口在 backend/cli/src/index.ts:55 建出完整命令树,默认 web 命令在 backend/cli/src/cli/cmd/web.ts:60 接管裸 `openscience`,server 再服务浏览器工作台。它的关键不是模型调用,而是把模型、工具、权限、配置和 UI 都放在同一个可审计闭环里。

源码里最有价值的设计是“本地优先但可扩展”:Host/Origin guard 在 backend/cli/src/server/host-guard.ts:19 限定 loopback host,又在 backend/cli/src/server/host-guard.ts:54 单独判断 origin;配置系统从远程 well-known、global、project、`.openscience` 和 synced config 分层合并,见 backend/cli/src/config/config.ts:75 到 backend/cli/src/config/config.ts:207。这让它既像桌面软件,又保留 agent 工具链的可组合性。 入口 middleware 还会设置运行时标记 backend/cli/src/index.ts:83。 credential 注入发生在命令执行前 backend/cli/src/index.ts:101。 compute keys 也在同一层应用 backend/cli/src/index.ts:105。 命令注册覆盖 serve/web/session/project backend/cli/src/index.ts:121。 错误格式化集中在 fatal catch backend/cli/src/index.ts:189。

▸核心数据

架构模式
Local-first monorepo:CLI 启动本地 Hono server,浏览器 workspace 通过 HTTP/SSE 访问 session、工具和配置。
核心抽象
Agent registry、Provider routing、ConnectorRegistry、Plugin hooks、Workspace renderer registry。
性能/精度数据
仓库未给 benchmark;可验证指标是 1,411 stars、Bun monorepo、数百个 TypeScript/TSX 文件和多数据库连接器。
下游依赖
科学研究者、ML/biology/physics agent、插件作者、使用 BYOK 或 Atlas managed models 的本地工作流。
本文导航 — 6 个章节依次:概述 → 架构图 → 核心模块 → 关键流程 → 测试与验证 → 风险。子页面在每个 H3 末尾以 深度阅读 → 入口展开。
§ 01 · ARCHITECTURE

本地科研工作台的三层结构

从源码看,OpenScience 的边界先由 CLI 入口建立,再由 web 命令启动 server 和浏览器,最后由 server guard 把访问面约束在本机。backend/cli/src/index.ts:55 的 yargs 命令树、backend/cli/src/cli/cmd/web.ts:147 的 server listen、backend/cli/src/server/host-guard.ts:46 的 host 检查形成第一条主链。

▸系统拓扑

openscience CLI → yargs command registry → web command / network options → local Hono server on loopback → SolidJS workspace → agent runtime / providers / science connectors / plugins

▸模块清单

模块不是按“页面”拆,而是按运行时责任拆。backend/cli/src/index.ts:112 到 backend/cli/src/index.ts:143 把 CLI 子命令注册成产品入口;backend/cli/src/agent/agent.ts:77 建立内置 agent;backend/cli/src/provider/provider.ts:57 装配 provider SDK;backend/cli/src/science/connectors/index.ts:30 注册科学数据源。

CLI 与启动
backend/cli/src/index.ts:55 到 backend/cli/src/index.ts:159 是命令入口;web 命令负责启动本地工作台。
配置与凭证
backend/cli/src/config/config.ts:75 分层加载 config,backend/cli/src/index.ts:98 注入 settings credentials。
Agent registry
backend/cli/src/agent/agent.ts:77 定义 research/biology/physics/ml/plan 等模式,并合并权限规则。
Provider routing
backend/cli/src/provider/provider.ts:57 维护 SDK provider 表,backend/cli/src/provider/provider.ts:143 处理 managed wallet 路由。
Science connectors
backend/cli/src/science/connectors/types.ts:76 统一 search/fetch 契约,backend/cli/src/science/connectors/index.ts:30 批量注册。
Plugin hooks
tooling/plugin/src/index.ts:148 暴露 Hooks,插件可改 chat 参数、工具调用和权限询问。

▸关键数据结构

src/backend/cli/src/science/connectors/types.ts · L76-92TypeScript
76/** The uniform contract every scientific data source implements. */
77export interface Connector {
78  /** Unique, stable, lowercase id used to route (e.g. "uniprot", "rcsb-pdb"). */
79  id: string
80  /** Display name (e.g. "UniProt", "RCSB PDB"). */
81  name: string
82  /** Primary domain for grouping in the catalog. */
83  domain: ConnectorDomain
84  /** One-line description shown in `science_list_dbs`. */
85  description: string
86  /** Homepage / docs URL. */
87  homepage?: string
88  /** Search the source; returns normalized hits. */
89  search(query: string, opts?: SearchOptions): Promise<ConnectorHit[]>
90  /** Fetch a single record by id. Return shape is source-specific. */
91  fetch(id: string, opts?: FetchOptions): Promise<unknown>
92}
深度阅读 · CLI 到本地 server 的启动链路
§ 02 · CORE MODULES

Agent、模型与配置如何装配

▸Agent registry 把科研角色变成权限化模式

`Agent.Info` 先定义统一 schema,再在实例状态里合并默认权限、用户权限和内置角色。比如 research 允许 question 和 plan_enter,证据在 backend/cli/src/agent/agent.ts:77、backend/cli/src/agent/agent.ts:85、backend/cli/src/agent/agent.ts:88;plan 模式则显式 deny edit,只允许写计划文件,见 backend/cli/src/agent/agent.ts:164、backend/cli/src/agent/agent.ts:176、backend/cli/src/agent/agent.ts:179。这说明它不是把 prompt 当角色,而是把角色和权限边界一起声明。 biology 继承相同 permission merge 模式 backend/cli/src/agent/agent.ts:97。 ml agent 把训练和评估作为描述核心 backend/cli/src/agent/agent.ts:131。 task subagent 禁止 todo 工具避免污染主上下文 backend/cli/src/agent/agent.ts:188。 explore subagent 把读搜索类工具打开 backend/cli/src/agent/agent.ts:203。 literature-review subagent 只开放检索和读取 backend/cli/src/agent/agent.ts:230。

src/backend/cli/src/agent/agent.ts · L77-95TypeScript
77    const result: Record<string, Info> = {
78      // --- Research modes (top) ---
79      research: {
80        name: "research",
81        description:
82          "Scientific research agent — literature review, data analysis, GPU compute, and synthesis across 241 skills.",
83        options: {},
84        color: "#06b6d4",
85        permission: PermissionNext.merge(
86          defaults,
87          PermissionNext.fromConfig({
88            question: "allow",
89            plan_enter: "allow",
90          }),
91          user,
92        ),
93        mode: "primary",
94        native: true,
95      },

▸Provider routing 同时处理 BYOK、managed wallet 和本地模型

Provider 层先列出 SDK 工厂,见 backend/cli/src/provider/provider.ts:57 到 backend/cli/src/provider/provider.ts:80;随后用 backend/cli/src/provider/provider.ts:82 识别 Atlas key,用 backend/cli/src/provider/provider.ts:153 判断本地 baseURL,用 backend/cli/src/provider/provider.ts:176 防止 BYOK key 被误送到 Atlas proxy。这个细节比“支持很多模型”更重要,因为科研工作台最怕凭证和账单路径混淆。 OpenAI loader 可选择 responses API backend/cli/src/provider/provider.ts:241。 Copilot loader 对 GPT-5 以后模型走 responses backend/cli/src/provider/provider.ts:251。 本地 URL 判定保留 localhost 模型 backend/cli/src/provider/provider.ts:157。 synsci 无 key 时只保留免费模型 backend/cli/src/provider/provider.ts:229。 Atlas session key 只在 managed proxy 路径注入 backend/cli/src/provider/provider.ts:106。

src/backend/cli/src/provider/provider.ts · L82-108TypeScript
82  function isAtlasApiKey(key: unknown): key is string {
83    return typeof key === "string" && key.startsWith("thk_")
84  }
85
86  function isAtlasProxyBaseURL(baseURL: unknown): baseURL is string {
87    return typeof baseURL === "string" && baseURL.includes("/api/llm/proxy/")
88  }
89
90  // Explicit apiKey for a provider routed through the Atlas managed proxy: force
91  // the session thk_ token. These SDKs read `<PROVIDER>_API_KEY` straight from
92  // env, so a shell `export OPENAI_API_KEY=sk-...` would otherwise shadow the
93  // managed token and 401 the proxy ("thk_* token not found"). Returns {} unless
94  // both managed spend is on AND the baseURL is the Atlas proxy — genuine BYOK
95  // (no proxy URL) is untouched and hits the provider directly.
96  async function managedProxyKey(providerID: string, baseURL: unknown): Promise<{ apiKey?: string }> {
97    const managed =
98      isAtlasProxyBaseURL(baseURL) && (await Config.get().catch(() => undefined))?.billing?.llm === "managed"
99    if (!managed) return {}
100    // Managed wallet routes through OpenRouter ONLY: never attach the wallet's
101    // thk_ token to a first-party proxy (anthropic / openai / google). Those
102    // providers are also dropped from availability (see isProviderAllowed), so
103    // this is belt-and-suspenders — a managed token can only ever reach the
104    // sanctioned OpenRouter (or hosted synsci) route.
105    if (!managedProviderAllowed(providerID)) return {}
106    const session = await OpenScience.getSession().catch(() => null)
107    return session?.api_key ? { apiKey: session.api_key } : {}
108  }
深度阅读 · 科学数据库连接器与统一 registry
§ 03 · KEY FLOW

从 openscience 命令到浏览器会话

启动流程不是简单 `open browser`。backend/cli/src/cli/cmd/web.ts:79 先解析网络选项,backend/cli/src/cli/cmd/web.ts:87 处理首次 onboarding,backend/cli/src/cli/cmd/web.ts:100 判断 Atlas 登录,backend/cli/src/cli/cmd/web.ts:121 给 managed config sync 设置 6 秒预算,最后 backend/cli/src/cli/cmd/web.ts:147 绑定 server。这个顺序避免浏览器首屏读到过期模型配置。 macOS Full Disk Access 探针在启动前运行 backend/cli/src/cli/cmd/web.ts:17。 项目参数会先切换 cwd backend/cli/src/cli/cmd/web.ts:71。 未配置模型时只提示不阻断 backend/cli/src/cli/cmd/web.ts:105。 超时后继续后台同步 backend/cli/src/cli/cmd/web.ts:127。 退出时 server.stop 有 watchdog backend/cli/src/cli/cmd/web.ts:173。

▸首启路径:配置先于 server 绑定

web handler → resolveNetworkOptions → onboarding / model setup → OpenScience.syncServices with 6s budget → Server.listen → openUrl(localhost)

▸请求防护:Host 与 Origin 双闸

Browser request → Host header must be localhost / 127.0.0.1 / ::1 → Origin must be loopback, tauri, trusted subdomain, or whitelist → Sec-Fetch-Site catches cross-site no-origin cases
深度阅读 · 插件 hooks 与前端 artifact renderer
§ 04 · TESTS & VALIDATION

测试与验证边界

这个仓库的质量边界更多体现在运行时防护和配置容错里。比如 remote well-known config 失败不能让 CLI 崩掉,源码在 backend/cli/src/config/config.ts:85、backend/cli/src/config/config.ts:100;server guard 则把 DNS rebinding 和 cross-origin websocket 放在注释里解释,见 backend/cli/src/server/host-guard.ts:5、backend/cli/src/server/host-guard.ts:11。

▸测试覆盖

仓库包含前端序列化和布局滚动测试,但这次源码分析更关注可直接验证的防护代码。下面的 guard 函数很短,却承担了本地 server 的暴露面控制:backend/cli/src/server/host-guard.ts:46 解析 host,backend/cli/src/server/host-guard.ts:54 校验 origin,backend/cli/src/server/host-guard.ts:70 兜底 Sec-Fetch-Site。

tests/backend/cli/src/server/host-guard.ts · L46-77TypeScript
46export function isAllowedHost(hostHeader: string | undefined): boolean {
47  if (!hostHeader) return false
48  const host = hostHeader.startsWith("[")
49    ? hostHeader.slice(0, hostHeader.indexOf("]") + 1) // keep "[::1]" bracket form
50    : hostHeader.split(":")[0]
51  return ALLOWED_HOSTS.has(host)
52}
53
54export function isAllowedOrigin(origin: string, extraWhitelist: string[] = []): boolean {
55  if (origin.startsWith("http://localhost:") || origin === "http://localhost") return true
56  if (origin.startsWith("http://127.0.0.1:") || origin === "http://127.0.0.1") return true
57  if (origin === "tauri://localhost" || origin === "http://tauri.localhost") return true
58  // Trusted hosted-UI subdomains (default syntheticsciences.ai, extended via
59  // OPENSCIENCE_CORS_DOMAINS). See matchesAllowedSubdomain.
60  if (matchesAllowedSubdomain(origin)) return true
61  return extraWhitelist.includes(origin)
62}
63
64// Decide whether a NETWORK request is cross-origin and must be rejected. An
65// Origin header (always present on cross-origin fetch and WebSocket upgrades)
66// is checked against the allow-list; when it is absent (same-origin requests
67// and same-/cross-site GET navigations omit it) we fall back to Sec-Fetch-Site
68// and reject only explicit cross-site contexts. Non-browser clients send
69// neither header and are allowed (the loopback bind already gates them).
70export function isCrossOrigin(
71  origin: string | undefined,
72  secFetchSite: string | undefined,
73  extraWhitelist: string[] = [],
74): boolean {
75  if (origin !== undefined) return !isAllowedOrigin(origin, extraWhitelist)
76  return secFetchSite === "cross-site"
77}

▸Provider 路由的 BYOK 保护

这一段补足一个容易被 README 忽略的安全细节:当用户自己的 key 和 Atlas managed proxy 同时存在时,backend/cli/src/provider/provider.ts:176 会计算 effective key,backend/cli/src/provider/provider.ts:181 排除 managed key,backend/cli/src/provider/provider.ts:184 发现 BYOK 误走 Atlas proxy 就改回 public endpoint。这个逻辑不是功能炫技,而是在保护科研工作台里最敏感的凭证路径。

src/backend/cli/src/provider/provider.ts · L176-190TypeScript
176  function pinByokToPublicEndpoint(provider: Info, options: Record<string, any>, publicURL: string) {
177    const effectiveKey =
178      (typeof options["apiKey"] === "string" ? (options["apiKey"] as string) : undefined) ??
179      provider.key ??
180      provider.env.map((key) => Env.get(key)).find((value): value is string => !!value)
181    // Managed (thk_*) keys must keep their Atlas proxy routing.
182    if (isAtlasApiKey(effectiveKey)) return
183    if (!isByokKey(effectiveKey)) return
184    if (isAtlasProxyBaseURL(options["baseURL"])) {
185      log.warn("refusing to route BYOK key through Atlas proxy — pinning to public endpoint", {
186        provider: provider.id,
187      })
188      options["baseURL"] = publicURL
189    }
190  }

▸配置目录也会安装插件依赖

另一个必须看源码才会注意到的点在配置加载:backend/cli/src/config/config.ts:178 检查配置目录是否已有 node_modules,backend/cli/src/config/config.ts:179 触发 installDependencies,backend/cli/src/config/config.ts:272 会安装 `@synsci/plugin`,backend/cli/src/config/config.ts:281 还会执行 `bun install`。所以扩展性和供应链信任是绑在一起的。

src/backend/cli/src/config/config.ts · L261-281TypeScript
261  export async function installDependencies(dir: string) {
262    const pkg = path.join(dir, "package.json")
263
264    if (!(await Bun.file(pkg).exists())) {
265      await Bun.write(pkg, "{}")
266    }
267
268    const gitignore = path.join(dir, ".gitignore")
269    const hasGitIgnore = await Bun.file(gitignore).exists()
270    if (!hasGitIgnore) await Bun.write(gitignore, ["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"))
271
272    await BunProc.run(
273      ["add", "@synsci/plugin@" + (Installation.isLocal() ? "latest" : Installation.VERSION), "--exact"],
274      {
275        cwd: dir,
276      },
277    ).catch(() => {})
278
279    // Install any additional dependencies defined in the package.json
280    // This allows local plugins and custom tools to use external packages
281    await BunProc.run(["install"], { cwd: dir }).catch(() => {})

▸已知边界 / 失败模式

  • YouTube 式远端服务不可控时,OpenScience 的 remote well-known config 已经用 backend/cli/src/config/config.ts:88 到 backend/cli/src/config/config.ts:105 做了降级;但每个具体 science connector 的失败策略还需要逐个审。
  • Agent 权限默认很宽:defaults 里 backend/cli/src/agent/agent.ts:55 设置 `* allow`,再叠加用户规则;部署给高风险科研环境时应把项目策略收紧。
  • README 明确说 agent 不是 sandbox;源码里 Host/Origin guard 保护的是本地 server 暴露面,不等于 shell/code execution 隔离。
§ 05 · HIGHLIGHTS

值得借鉴的工程亮点

配置加载很务实
backend/cli/src/config/config.ts:78 到 backend/cli/src/config/config.ts:131 按 remote、global、custom、project、inline 分层,且远程失败只 warning。
模型路由避免账单串线
backend/cli/src/provider/provider.ts:176 会把 BYOK key 从 Atlas proxy 拉回 public endpoint,保护用户凭证。
科研数据源被标准化
backend/cli/src/science/connectors/types.ts:76 用 search/fetch 契约统一 UniProt/PDB/arXiv 等连接器。
插件表面足够细
tooling/plugin/src/index.ts:171、tooling/plugin/src/index.ts:184、tooling/plugin/src/index.ts:218 分别允许改 chat 参数、工具参数和 compaction。
§ 06 · RISKS & GAPS

技术亮点之外,别忽略这些

源码显示 OpenScience 有清楚的本地边界和扩展点,但它也把很多能力集中在一个本地 agent runtime 内。backend/cli/src/index.ts:98 会把凭证注入进程,backend/cli/src/config/config.ts:272 会为配置目录安装 plugin 依赖,backend/cli/src/agent/agent.ts:55 默认权限偏开放;这些都适合个人工作台,但企业环境需要额外策略。

不是执行 sandbox
README 已提示 agent 不隔离;backend/cli/src/server/host-guard.ts:19 保护网络入口,不保护子进程副作用。
插件依赖安装面
backend/cli/src/config/config.ts:261 到 backend/cli/src/config/config.ts:281 会创建 package 并 bun install,本地插件目录需要可信来源。
Provider 配置复杂
backend/cli/src/provider/provider.ts:90 到 backend/cli/src/provider/provider.ts:125 的 managed proxy 逻辑很细,测试缺口会直接影响账单和凭证路径。
连接器可靠性不均
统一 registry 在 backend/cli/src/science/connectors/index.ts:30 注册多个数据库,但每个外部 API 的限流和失败语义仍需单独观察。
诚实记录 — 这部分列的是我在源码里看到、但作者没有明确文档化的限制。读者请按需取舍。