Files
terminalX/CLAUDE.md
kid 5dbe937222 feat: M4b publickey 认证(SE 优先/软件回退,sign_callback)
- TXTransport: SSHSigner 协议 + Authentication.publicKeyCallback;SSHSession 用
  @convention(c) sshSignCallback(abstract 桥 SignerBox + malloc 出参)接 libssh2_userauth_publickey
- app SigningKeyProvider: SE 优先(kSecAttrTokenIDSecureEnclave),未签名/无 SE 回退软件 P-256
  (导出落文件持久化),同一 SSHSigner 路径;SecKeyCreateSignature(ecdsa)→ECDSAConv.derToSSHSignature
- 公钥 blob=ecdsa-sha2-nistp256||nistp256||X963;-txPubkeyAuth 日志打 authorized_keys 行
- 验证(192.168.9.199):不给密码,公钥加 authorized_keys 后免密登录成功(whoami=kid);
  模拟器 SE 生成 hw=true 可达,真机 SE 硬件签名+keychain 持久化待补

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-24 18:17:30 +08:00

89 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CLAUDE.md — terminalX
原生苹果生态终端工具,解决 iPad/iPhone 远程运维 macOS/Linux + vibe coding 痛点。
平台优先级 **iPadOS > iOS > macOS(Apple Silicon)**。四大核心:**libghostty** 引擎 · **mosh** · **Tailscale(tsnet 用户态)** · **tmux control-mode hook**
iOS/iPadOS 只做远程(沙盒禁 fork/execmacOS 支持本地 PTY。
> **新会话必读**:先读 `docs/HANDOFF.md`(项目全貌 + 里程碑状态 + 如何继续)。本文件只讲操作规范。
## ⚠️ 铁律:绝不臆想工具结果
调用任何工具后,在**真实结果返回前**绝不预测/编造/续写其输出(不写假想的 `BUILD SUCCEEDED`、日志、`ls`、截图内容)。一次调用→停→读真实返回→再推理。想说预期只在调用**之前**用将来时。发现自己编造要**硬停**重新核实。(曾因此误诊,教训见 git 历史 / HANDOFF。
## 环境约束(关键,决定一切构建方式)
- 构建机走**带认证的 HTTPS 代理**(疑似国内网)。规律:`github.com` release blob 可经 `curl` 下;**`go.dev`/`ghcr.io`(brew bottle) 被代理 abort****SwiftPM 不支持带认证代理** → 所有 SPM 依赖必须 **vendor 本地化**(见 `vendor/`。Go 模块走 `goproxy.cn`
- **Go 1.26.5 在 `~/.local/go`**(不在默认 PATH经 aliyun 镜像 curl 装);**gomobile/gobind 在 `~/go/bin`**。`go env` 已固化 `GOPROXY=https://goproxy.cn GOSUMDB=off`
- 工具链Xcode 26.6 / Swift 6.3 / xcodegen 2.46 / zig 0.15.2 / cmake 4.4 / ninja 1.13。
## 构建 & 验证命令
```sh
# 纯逻辑单测(无需 Xcode/模拟器)
swift test --package-path packages/TXCore # 35 tests
# 重建 SSH 库 xcframeworklibssh2+mbedTLS改 vendor/libssh2|mbedtls 后)
bash vendor/build/build-cssh.sh all # 产出 artifacts/CSSHCore.xcframework (library-only!)
# 重建 mosh 库 xcframeworkmosh C++ + protobuf-lite改 vendor/mosh|protobuf 或 build/mosh/* 后)
bash vendor/build/build-mosh.sh all # 产出 artifacts/MoshCore.xcframework (library-only!)
# 注:首次需 host protoc 生成 .pb.cc已 checkin无需重跑
# cmake -S vendor/protobuf -B vendor/protobuf/build-host -G Ninja -Dprotobuf_BUILD_TESTS=OFF && \
# cmake --build vendor/protobuf/build-host --target protoc && \
# vendor/protobuf/build-host/protoc --proto_path=vendor/mosh/src/protobufs --cpp_out=vendor/mosh/src/protobufs *.proto
# 重建 tsnet 桥(改 vendor/tsnet-bridge/tsnetbridge/bridge.go 后)
export GOROOT=$HOME/.local/go; export PATH=$GOROOT/bin:$HOME/go/bin:$PATH
cd vendor/tsnet-bridge && gomobile bind -target=ios,iossimulator \
-o ../../artifacts/TsnetBridge.xcframework ./tsnetbridge
# 构建 app改 project.yml 后先 xcodegen generate
cd apps/TerminalX && xcodegen generate --spec project.yml
xcodebuild -project TerminalX.xcodeproj -scheme TerminalX \
-destination 'platform=iOS Simulator,id=<UDID>' \
-clonedSourcePackagesDirPath /tmp/tx-spm build
```
## 无头验证(本环境无 GUI用 simctl 截图取证)
iPad Pro 13(M5) 模拟器 UDID`2EA9055E-4557-4B20-82AF-56A68AC1D454`(变了用 `xcrun simctl list devices`)。
```sh
UDID=2EA9055E-4557-4B20-82AF-56A68AC1D454
APP=$(find ~/Library/Developer/Xcode/DerivedData/TerminalX-*/Build/Products/Debug-iphonesimulator -maxdepth 1 -name TerminalX.app | head -1)
xcrun simctl boot $UDID; xcrun simctl install $UDID "$APP"
xcrun simctl launch $UDID ai.athom.terminalx --args <见下>
sleep N; xcrun simctl io $UDID screenshot /tmp/x.png # 然后用 Read 工具看 /tmp/x.png
```
**启动参数NSUserDefaults 参数域,自动连接,供无头验证)**
- 直连 SSH`-txHost H -txPort 22 -txUser U -txPass PW -txAutoCommand "echo hi"`
- tsnet SSH额外加 `-txTsnetKey <authkey>`-txHost 填 tailnet IP
- 纯 tsnet 自检(加入+对端枚举):只给 `-txTsnetKey <authkey>`(不给 -txHost
- **mosh 直连(LAN)**`-txHost H -txPort 22 -txUser U -txPass PW -txMosh 1`(登录后跑 mosh-server→解析 MOSH CONNECT→mosh_main 直连 UDP
- **mosh over tsnet**:额外加 `-txTsnetKey <authkey>`-txHost 填 tailnet IPrelay=loopback↔tsnet UDP
- 自定义 mosh-server 命令:`-txMoshServerCmd "…"`(默认 `mosh-server new -s -c 256 -l LANG=en_US.UTF-8 -l LC_ALL=en_US.UTF-8`
- **M4 publickey 认证**`-txPubkeyAuth 1`(不给 -txPassSigningKeyProvider 造 P-256 key日志打 authorized_keys 行,加到服务器后重连免密登录)
- **M4 host key mismatch 测试**`-txHostKeyPinOverrideBase64 <b64>`(播种假 pin → 真 host key 不符 → 弹告警)
**M3 挂起→恢复无头验证**:退后台 `simctl openurl $UDID https://apple.com`;冻结 `kill -STOP $(pgrep -x TerminalX)`;解冻 `kill -CONT`;前台 `simctl openurl $UDID "terminalx://resume"`URL scheme 已注册)。日志 `log stream | grep -E 'MOSHDBG|TXM3'``wake pulse``TXM3 recovered` 时间差判 <3s。**模拟器看门狗会杀掉被 STOP 冻结过久(>~30s)的 app且模拟器不复现 socket defunct/WG 过期,深挂起需真机锁屏补测。**
## 仓库结构
- `packages/TXCore` — 零依赖纯逻辑tmux 解析器/layout/`SessionMachine`(会话状态机+退避重连)/`TmuxControlSequence`(DCS 检测)。
- `packages/TXTransport``SSHSession`(libssh2, 支持外部 fd)/`MoshSession`(mosh_main 桥)/`Transport` 协议C shim: `CSSH`+`CSSHCore`(libssh2)、`CMosh`+`MoshCore`(mosh)。
- `apps/TerminalX/iOS``SSHTerminalModel`(连接编排+tsnet+tmux 路由+mosh 编排)/`ContentView`/`TmuxController`(tmux 网关)/`TsnetProbe`(M1 自检)。
- `vendor/` — 所有非 Swift 依赖本地化:`libghostty-spm`(GhosttyKit)/`MSDisplayLink`/`libssh2`/`mbedtls`/`tsnet-bridge`(Go)/`mosh`(blinksh/ios 分支)/`protobuf`(3.21.12)`build/mosh/`(手写 config.h+CMakeLists)。
- `artifacts/` — 预编译 xcframework**有意提交**,非 gitignore`CSSHCore`/`TsnetBridge`/`MoshCore`
## 约定 & 坑
- **CSSHCore/MoshCore 必须 library-only**(无 Headers/modulemapSwift module 由包内 `CSSH`/`CMosh` C target 提供。否则与 GhosttyKit 撞 `include/module.modulemap`
- **mosh 构建**:绕过 autotools 用 CMake 直编 `src/*.cc`(手写 `build/mosh/config.h`crypto 用 CommonCrypto(`USE_APPLE_COMMON_CRYPTO_AES=1`);用 `terminaldisplayinit_ios.cc`(无 terminfo);已 patch 3 处 vendored 源(`::bind` 消歧义 / curses include 包 `#if !IOS_CONTROLLER` / 去 iOS 禁用 `system()`)protobuf 只需 lite runtime`.pb.cc` 已 checkin。
- **protoc 装不了不是问题**:用源码 CMake 编一个 host protoc 生成 `.pb.cc` 即可(一次性,产物 checkin
- **一进程一个 Go runtime、只一次 gomobile bind**(全在 `tsnetbridge` 包)。
- **Swift `\r\n` 是单个 Character扩展字形簇**:判行尾用 `Character.isNewline`,不能 `c != "\r"``MoshConnectScanner` 曾踩)。
- **Swift6 `SendNonSendable` pass 可能崩溃**`pthread_create`+`Unmanaged` 上下文+C 入口的组合):改用 `Foundation.Thread`+线程内 `pthread_self()` 规避(见 `MoshSession`)。
- GhosttyKit 的 `InMemoryTerminalSession.receive` 在 surface 未 attach 时**丢弃**写入 → 用 `OutputGate` 缓冲到 `surfaceSize != nil` 再 flush。
- tmux `-CC attach` **不推 %window-add** → 必须主动 `list-windows` 枚举。
- **tmux 多 pane 尺寸走"路线 b"**app 按全屏格子发 `refresh-client -C WxH` 让 tmux 布局pane surface 尺寸严格取 layout rect%output 按 tmux pane 宽高排版,二者不一致必换行/清屏错乱pane surface 的 resize 回调**只断言不反向驱动 tmux**防反馈环。diff 用 full layout、渲染用 visible layoutzoom 免费SwiftUI identity 只用 paneID布局变不重建 surface。新 pane 在 `capture-pane` %end 前丢弃其 %outputtmux 单线程串行保证快照不缺不重)。
- **tmux 网关字节必须单消费者保序**:多个 `Task{@MainActor}` 到主线程顺序无保证,多 pane 高吞吐会打碎 % 协议 → 用 `TmuxByteChannel`(AsyncStream) 单消费者。
- SourceKit "No such module" 是索引噪声,**以 `swift build`/`xcodebuild` 为准**。
- 密钥/密码**绝不写进仓库**;测试主机凭据由用户即时提供。
- 提交/推送 git 只在用户明确要求时(当前已 `git init`,尚无 commit
- 与用户始终用**简体中文**交流。
</content>