hongjr03
12a2f3117f
Revert "fix: preserve run provider capability"
...
This reverts commit 63c86322de .
2026-07-11 15:06:48 +08:00
hongjr03
2ee84d9543
Revert "fix: carry provider capability in dedicated header"
...
This reverts commit 3087132083 .
2026-07-11 15:06:47 +08:00
hongjr03
3087132083
fix: carry provider capability in dedicated header
2026-07-11 15:02:51 +08:00
hongjr03
63c86322de
fix: preserve run provider capability
2026-07-11 15:00:34 +08:00
hongjr03
d36b00bbec
feat: make agent roles and skills dynamic
2026-07-11 12:55:05 +08:00
hongjr03
17c0536958
fix: enable only curated agent skills
2026-07-11 12:25:52 +08:00
hongjr03
96e120e02c
feat: add curated curriculum agent skills
2026-07-11 12:22:01 +08:00
hongjr03
7fcb57013e
fix: restore bounded agent sandbox execution
2026-07-11 02:34:56 +08:00
hongjr03
035c264179
fix: keep agent sandbox sockets on short paths
2026-07-11 02:22:19 +08:00
hongjr03
848f913597
feat: secure organization provider credentials
2026-07-10 22:04:43 +08:00
hongjr03
f07f280b8f
fix: prove Linux Agent sandbox boundary
2026-07-10 17:59:56 +08:00
hongjr03
f4968d6657
fix: confine Agent and MCP data access
2026-07-10 16:38:08 +08:00
hongjr03
1094ebd651
fix: let agents download Feishu message resources
2026-07-10 12:17:07 +08:00
hongjr03
5d315fff21
fix: enforce role tool whitelist
2026-07-09 20:48:03 +08:00
hongjr03
716101eed0
feat: add agent cost reporting
2026-07-09 20:29:56 +08:00
hongjr03
d01ea0e1cb
feat: interrupt running agent via card button with confirm dialog
...
Add a ⛔ 中断 button to live streaming cards; Feishu's native confirm
dialog is the confirmation step, so no extra card round-trip is needed.
- builder.ts: render interrupt action (danger button + confirm) while the
run is live; new `interrupted` flag renders a 已中断 footer instead of
完成/失败 on the complete card.
- streaming-card.ts: finish(text, { interrupted }) threads the flag into
the final patch; overflow cards suppress the button.
- runner.ts: RunRequest gains abortController, passed to the SDK query;
abort surfaces as RunStatus "interrupted" (no error) in the catch.
- trigger.ts: activeRuns registry maps runId → AbortController; onCardAction
resolves the interrupt button, authorizes agent.cancel, aborts the run.
Interrupted runs persist as CANCELED (spec RunState.canceled, terminal →
lock released per ADR-0002).
- authorizer.ts: agent.cancel now consults PermissionSettings.agentCancel
(MANAGE_ONLY/DISABLED) — previously only agentTrigger was honored, but
spec/PermissionGrant deliberately splits these knobs ("谁能触发" ≠
"谁能取消"). Default role remains MANAGE (Capability.normalCancel).
Tests: runner abort unit test, trigger interrupt + denied integration
tests, three agent.cancel authorization tests (manage allowed, edit denied,
DISABLED policy denies even manage). 26 files / 175 tests pass.
2026-07-09 18:59:31 +08:00
hongjr03
d6724e5a83
feat: streaming agent card with tool-use trace, reasoning panel, and tool results
...
Replace text-only PatchableTextStream with a unified StreamingAgentCard that
renders the full agent run lifecycle in a single Feishu interactive card:
- Tool-use panel: collapsible, shows each tool call with status (running/
success/error), input summary, and result/error in code blocks
- Reasoning panel: collapsible, streams thinking text inline then collapses
on completion
- Answer text: rendered via native Feishu markdown component (no post-row
round-trip)
Runner now captures previously-discarded SDK events:
- thinking_delta → reasoning stream
- tool_use id + input on content_block_start / assistant message
- tool_result from user messages (was entirely unhandled)
New files:
- card/trace-store.ts: per-run in-memory tool-use trace with secret redaction
- card/builder.ts: Feishu card JSON builder (tool panel + reasoning + text)
- card/streaming-card.ts: StreamingAgentCard controller (400ms throttled)
client.ts: add sendCard/patchCard raw JSON primitives
2026-07-09 17:18:40 +08:00
hongjr03
3f486232a8
feat(hub): 抽出运行时配置
2026-07-08 15:25:49 +08:00
hongjr03
ead5cf04d6
fix(hub): 稳定 Feishu trigger 集成测试
2026-07-08 15:13:50 +08:00
hongjr03
ecbc2c8666
fix(hub): 对齐 ADR-0018 agent 执行面边界
...
runner.ts: 启用 SDK 内置沙箱(bubblewrap/seatbelt),写入限制在 workspace,
denyRead 敏感路径,failIfUnavailable 硬拒非沙箱运行。bypassPermissions 保留
(headless 无交互),沙箱是硬边界而非权限提示层。
install_service.sh: 默认 service user 从 root 改为 cph-hub;env 模板修正
OPENROUTER_API_KEY → ANTHROPIC_AUTH_TOKEN/ANTHROPIC_BASE_URL/ANTHROPIC_API_KEY
(与 server.ts 实际读取一致);补 __BASE_DIR__ sed 替换。
cph-hub.service: AssertPathExists=/usr/bin/bwrap 强制沙箱依赖;NoNewPrivileges。
不加 ProtectSystem/ProtectHome(会破坏 cph render-cache 与 bwrap user-namespace)。
trigger.ts: 权限闸门去掉 if (senderOpenId !== '') 短路——缺 open_id 一律
fail-closed 拒绝,不再静默跳过;role gate 同步去掉冗余守卫(已由上游保证)。
顺手把 JSON.parse(msg.content) 的 inline cast 换成 Zod schema parse
(FileMessageContentSchema / TextMessageContentSchema)。
ADR-0018: 状态改为 Accepted+implemented,机制段写定 SDK 沙箱,网络决定为开放,
Open Questions 更新。
2026-07-08 13:11:14 +08:00
hongjr03
c73b41e1da
fix(hub): resume Claude SDK sessions
2026-07-08 11:48:20 +08:00
hongjr03
012c8a7d89
fix(hub): default to OpenRouter Sonnet model
2026-07-08 11:48:16 +08:00
hongjr03
250f918346
chore: delete dead code
2026-07-08 02:37:44 +08:00
hongjr03
4bd5b03bb1
refactor(hub): Vercel AI SDK → Claude Code SDK
...
用 @anthropic-ai/claude-agent-sdk 的 query() 替换 Vercel AI SDK 的
streamText。SDK 自带 Read/Write/Bash/Glob/Grep 工具——read_file/
write_file/list_files/cph_check/cph_build 全部不需要了,Bash 跑
cph 直接。
UX 同时改成 workbuddy 风格:
- 收到 @bot → 表情回复(👍 )确认收到,不发'已开始处理'
- agent 回复作为文本流式发送(text-as-card patch,markdown 渲染)
- 超长自动续发新消息,不截断
- 完成/出错不再额外发状态消息
- 去掉卡片(model选择器/取消按钮)
ADR-0017 更新:放弃 provider-agnostic,Claude Code SDK 是 Anthropic
专有。换来:compaction、工具审批、partial message 流式、成熟 agent
loop。
tsc rc=0。旧文件(workspace.ts/cph.ts/provider.ts/tools.ts)暂留,
下个 commit 清理。
2026-07-08 01:36:59 +08:00
hongjr03
e7924f78d5
feat(hub): 结构化运行历史(A-3) + lock 心跳(A-5)
...
A-3 结构化历史:
- 新增 AgentMessage 表(每条消息的 queryable 投影,transcript 文件仍是 agent 读回记忆)
- 新增 AgentFileChange 表(run 期间文件变更 before/after hash + operation)
- runner.ts: generateText 后 persistAgentMessages 落库 result.responseMessages
- workspace.ts writeFileTool: 写前算 beforeHash(SHA-256),写后算 afterHash,通过 ctx.onFileChange sink 记录
- runner.ts flushFileChanges: 收集 sink 的变更批量写入 AgentFileChange
- ToolContext 加 onFileChange sink 字段
- trigger.ts 接线 runId/sessionId/prisma 到 RunRequest
A-5 lock 心跳:
- ProjectAgentLock 加 heartbeatAt 列
- runner.ts onStepFinish 回调:每步 model step 后更新 heartbeatAt(吞错,lock 可能被 force-release)
60 tests 全过(tsc 干净)。
2026-07-07 22:20:30 +08:00
hongjr03
a2c8fa8eaf
refactor(hub): 手搓 agent loop 换成 Vercel AI SDK
...
将 provider/runner/tools 三件手搓轮子替换为 AI SDK v7 的 generateText + tool:
- 删 openrouter-provider.ts(翻译层消失)、provider.ts 自定义消息类型
- runner.ts 循环体改为 generateText({stopWhen: stepCountIs}),保留 RunRequest/RunResult 公共签名
- tools.ts ToolRegistry 改为 ToolFactory + build(ctx, names?) 按 role 白名单构建 tools record
- workspace/cph/feishu 工具改写为 tool() 定义,execute 闭包捕获 per-run ToolContext
- transcript.ts 改为 ModelMessage[] JSONL(0.0.0 cutover,无迁移)
- server.ts/trigger.ts 接 modelFactory + toolWhitelist
- 测试:helpers 写 MockLanguageModel implements LanguageModelV4;5 个测试改写 + 新增 runner.test.ts
- 移除未使用的 openai 依赖
- ADR-0017 Consequences 同步:loop 委托给 AI SDK,provider seam 是 LanguageModel
tsc 干净,54 tests 全过。
2026-07-07 20:07:02 +08:00
hongjr03
afaf5bee09
feat(hub): RoleEntry 完整 bundle + per-run tool 白名单 + per-role 触发权限
...
RoleEntry 扩成 (model, systemPrompt, tools) bundle,id 兼任 slash command 名。
ToolRegistry.subset() 支持按白名单构造 per-run 视图,模型永远看不到 role 外的工具。
trigger 解析 /<role> 命令,查 RoleEntry 组装 systemPrompt + 子集 registry 传给 runner。
新增 RoleTriggerGrant 表 + canTriggerRole gate,与 ADR-0004 canTriggerAgent 串联:
先问'能不能触发 agent',再问'能触发哪个 role'。未配置 role 放行(back-compat)。
- models.ts: RoleEntry 加 systemPrompt + tools 字段
- tools.ts: ToolRegistry.subset(names) 返回共享 handler 的子集视图
- trigger.ts: extractRole 解析 slash; 传 systemPrompt + runTools; catch 链容错 P2025
- runner.ts: RunRequest.systemPrompt 改 string | undefined (exactOptionalPropertyTypes)
- schema.prisma + migration: RoleTriggerGrant(projectId, roleId, principal, revokedAt)
- permission.ts: canTriggerRole gate (有 grant 记录即白名单模式,含 revoked)
- server.ts: draft/review 两个 role 加 systemPrompt + tools 白名单
- 测试: role-permission.test.ts (5) + trigger.test.ts (+3), 53 全绿
2026-07-07 18:47:51 +08:00
hongjr03
0fe6cabc68
feat(hub): session transcript 连续性 + slash commands (/new /resume /reset)
...
修复每次 @bot 都是失忆状态的问题。
- transcript.ts: JSONL 文件存 session 对话消息(workspace/.cph/sessions/
<id>.jsonl),append-only;和 Claude Code 一致;ADR-0003 说的'不存全
历史'是群聊历史,session 对话是 Hub 自己产生的,不冲突
- runner.ts: RunRequest 加 transcriptPath;run 开始时读历史拼进初始
messages(system prompt 只在首条);run 结束时 append 本轮新消息
(loadedCount 区分历史 vs 新增,不重复写);best-effort(写失败不丢
run 结果)
- trigger.ts: slash commands 在 lock 检查之前(/new /resume /reset
不创建 run 不需要锁);transcriptPath 传给 runner
/new = 归档当前 session,下次 @bot 自动建新的
/resume = 恢复最近归档的 session
/reset = 同 /new
未知 /cmd 透传给 agent 当普通 prompt
- unit: transcript.test.ts 读写回环/append 累积/损坏行恢复/路径(6)
- integration: trigger.test.ts 加 /new /resume /reset /unknown(4)
修了 slash command 被 lock 挡住的问题(提到 lock 之前)。
vitest run 45/45 通过;tsc rc=0。
2026-07-07 17:50:05 +08:00
hongjr03
ce767e9cab
feat(hub): 真实飞书读取 + 状态卡片 + 权限 gate + 部署脚本
...
FeishuRead(ADR-0003): feishuContextTool 从 stub 换成真实 lark
im.v1.message.get(单条 trigger/reply/status_card)+ list(thread 回复);
ADR-0003 chat 授权不变式不变(handler 已 gate)。
StatusCard: sendCard + buildRunStatusCard——完成/出错发 interactive card
(status 文案 + model 选择器 + 取消按钮带 runId),替代纯文本。
Permission(ADR-0004, project-wise): triggerAgent 查 PermissionGrant 对
project 的 edit+ 能力(manage⊇edit 单调);sender open_id 当 principal
(子类型学 OPEN,务实选择,permission.ts 标注);per-artifact OPEN;
settings 组合规则 OPEN。无权限回 '无权限触发'。
Deploy: cph-hub.service systemd unit(ExecStartPre 跑 prisma migrate)+
install_service.sh(idempotent,seed env)+ deploy_platform.sh
(rsync + npm ci + build + restart + healthz)。
client.ts 抽 createLarkClient/startFeishuListenerWithClient,tools 与
ws 共用同一 client。
tsc rc=0;prisma validate 通过;deploy 脚本 bash -n 通过。
2026-07-06 23:44:07 +08:00
hongjr03
a4d52e1850
feat(hub): workspace 文件工具 + cph 子进程工具(hub↔courseware 耦合点)
...
agent 的窄工具面补全:
- workspace.ts: read_file/write_file/list_files,路径全 confinement 到
ctx.workspaceDir(ADR-0007 目录树);.. 与绝对路径逃逸返 error JSON 不中断 run
- cph.ts: cph_check/cph_build 子进程,cwd=workspace;ADR-0016 版本契约由 cph
自身在 load 阶段校验(cphVersionMismatch),Hub 透传诊断不重复校验;
CPH_BIN 可配,缺二进制返 exitCode 127 不抛
- server.ts: 注册全部 5 个工具
修 dynamic import(rule: ts-no-dynamic-import)——mkdir 改顶部静态 import;
三个 handler 的 confine 包进 try(操作性错误返 JSON 而非冒泡中断 run)。
tsc rc=0;smoke 通过(逃逸拒绝/读写回环/列举/cph ENOENT)。
2026-07-06 23:19:28 +08:00
hongjr03
313e890b6c
feat(hub): 飞书 @bot 触发链路 + ProjectAgentLock 不变式(ADR-0001/0002)
...
群消息事件 → 创建 AgentRun → 获锁 → 调 runAgent → 释放锁 + 状态回写:
- feishu/client.ts: lark WSClient 长连接接收 im.message.receive_v1;
sendText 经 client.im.v1 发送(SDK 运行时动态 API,类型弱化务实收口)
- feishu/trigger.ts: @bot 识别(chat 绑定解析 ADR-0001)→ 创建 run+
session(provider-bound ADR-0017)→ acquireLock(ADR-0002,竞态回退 busy)→
runAgent(fire-and-forget)→ finally releaseLock;非项目群/无 @bot/非文本
全部忽略
- lock.ts: ADR-0002 WellFormed 落代码——currentLockRunId 读锁时校验
持锁 run 非终止,终止 run 持锁即 release-path bug(抛而非静默)
- models.ts: resolve 提到 ModelRegistry 接口(之前漏在实现上)
- server.ts: Fastify + lark ws 启动,/api/healthz
prompt 提取 smoke 通过(@bot+prompt→prompt;纯@bot→null;无mention→null;
非文本→null)。修了 mention 正则 bug(/@_\w+_/ 的回溯错误)。tsc rc=0。
2026-07-06 23:11:35 +08:00
hongjr03
a08c33205b
refactor(hub): role 改为数据条目,不再是写死的闭集枚举
...
ADR-0017 说 role-based routing 是 product/admin config,不是 spec invariant。
之前 RunRole = "draft"|"review"|"triage" 闭集违反这条——role 钉死在代码里,
教师/admin 无法自定义。改为:
- RoleEntry(id+label+defaultModel)是数据,registry 持有 role 集合
- roleId 退化为 string,新 role 是配置改动不是代码改动
- runner 不再带 role 字段(model 已由 caller resolve 好传入,role 透传无意义)
- 未知 role 优雅降级到首个启用 model,不抛
tsc --noEmit rc=0;role-as-data + ADR-0003 不变式 smoke 通过。
2026-07-06 22:41:39 +08:00
hongjr03
3bca137b48
feat(hub): provider-agnostic agent seam 骨架(ADR-0001/0003/0017)
...
hub/ TS 新部件,平级于 spec/。本仓从零 TS 重写(非迁移旧服务),栈:
Fastify+Prisma 待补;本提交落 provider-agnostic agent runtime 骨架:
- provider.ts: AgentProvider seam,OpenAI-compatible messages+tools+tool_calls
形状(语言通解,非 OpenAI 承诺);@Claude 仅为触发品牌(ADR-0017)
- openrouter-provider.ts: openai SDK 指向 OpenRouter 的具体适配器,仅做
Message↔SDK 翻译;agent loop 不 import SDK,换 provider 不改 loop
- runner.ts: 自有 agent loop(dispatch tools/循环到 stop/budget),per-run
model(ADR-0017 role-based routing,run 带其 model)
- tools.ts: 窄工具面 + ADR-0003 McpReadRequest.Authorized 落代码:
feishu_read_context 拒绝非本项目绑定 chat 的读取(实测错 chat 抛
UnauthorizedChatRead,对 chat 放行)
- models.ts: ModelRegistry seam(role→default 映射策略 OPEN)
- server.ts: 接线入口(stub Feishu read,无活凭证即可编译)
tsc --noEmit rc=0;ADR-0003 不变式 smoke 通过。
2026-07-06 22:38:04 +08:00