@cyberred/cli
v0.1.1
Published
CyberRedd command line client; every command is derived from the operation registry
Downloads
74
Maintainers
Readme
@cyberred/cli
CyberRedd 命令行客户端。命令树从 operation registry 派生(src/command-tree.ts):
registry 里每一条已发布(active / deprecated)的 operation 长出一条命令,与两个 MCP 入口
的工具面是同一个集合;planned 的不长。今天是七条:research run/get/cancel、pack get、
community search/get、rules get。派生规则(机械,没有按 operation 的特例):
- 命令词 = tool 名按
.拆开;REST 路径占位符 → 位置参数; - inputSchema 的其余顶层属性 → 选项(snake_case → kebab-case):
boolean→--flag/--flag=false(缺席、true、显式 false 是三个值);integer→ 整数选项(取值范围交给冻结 schema 判);string→ 字符串选项;对象 / 数组 / 多态 →--<name>-file <path|->读 JSON; 带const的属性自动注入;required缺席是用法错(退 2)。 - 本地校验用 registry 的
input_schema_ref反查出的契约名走assertValid; 哪个入参进 path / query / body 用@cyberred/sdk/mcp的routeToolArguments(),与 MCP 同一份。 - 人读渲染是唯一按 operation 特化的地方(
src/renderers.ts,可选,缺席用通用渲染)。
⚠️ 今天哪几条连不上真服务
仓库里唯一在建的 HTTP transport(apps/api)只暴露 pack.get。其余六条都按
冻结 wire 契约实现完毕,端到端用例对着本地 HTTP 桩真起进程、真发请求跑通;
但"契约上做完了"不等于"线上可用"。
用法
cyberred --help # 命令清单从 registry 生成,以它为准
cyberred research run --workspace-id <uuid> --tier <fast|standard|deep> \
--max-credits <n> --task-file <path|-> [--dry-run] [--async]
cyberred research get <id>
cyberred research cancel <id>
cyberred pack get <id> [--revision <n>]
cyberred community search --query <value> [--language <value>] [--limit <n>] [--cursor <value>]
cyberred community get <id>
cyberred rules get <id> [--verify | --verify=false]凭据
cyberred login(Device Code):打印验证码与 Console/device链接并轮询;拿到的 access + refresh 只存 OS keychain(macOS 钥匙串 / Linux Secret Service,经@cyberred/credential-broker;Windows 明确报不支持, 不退回写文件)。registry 命令在没有显式 API key 时用它,到期前自动轮换。cyberred logout先向授权服务器撤销 再删本地(撤销失败不删;--local-only只删本地);cyberred whoami只显示非敏感信息。--for mcp给本地 MCP 单独授权一次(独立 client、独立 keychain 条目),CLI 自己不读那一份。 需要--issuer/FORUMLAYER_OAUTH_ISSUER;--resource/FORUMLAYER_OAUTH_RESOURCE缺省等于 base URL; 给了--console-origin/FORUMLAYER_CONSOLE_ORIGIN就强制验证链接与它同源。- 显式 API key 两个来源:
FORUMLAYER_TOKEN环境变量,或--token-file <path>。只供服务端到服务端的 REST 集成(CI / 后端 job),显式给出时优先于 keychain。 (必须是常规文件、属当前用户、权限0600;O_NOFOLLOW | O_NONBLOCK打开后对 fd 做fstat——检查的和读的是同一个 inode,符号链接直接拒,FIFO 不会把进程挂死)。 两者同时给出即拒绝,不猜。形状不是 API key 的直接拒(见下)。 - (历史,已被 Device Code 登录取代)没有
auth login,不写任何凭据文件,没有默认凭据路径。 架构口径是"凭据只进 keychain",而 keychain 后端属于尚未落地的认证控制面;在这里发明一个落盘凭据存储 等于替 owner 裁定。 - token 只活在
Secret里,toString/toJSON/util.inspect三个入口都返回[redacted];它绝不进入 SDKbuildRequest()的 headers,Authorization在fetch前一刻才注入。所有 stdout / stderr 还会再过一层字面量擦洗。 - 擦洗层除原文外还擦 JSON 转义形态(
--json是先stringify再擦洗的,一个含"的 token 只擦原文会漏)与百分号编码形态。 - ⚠️ 不宣称穷尽:base64、分段回显、Unicode 规范化仍然穿得过去;擦洗也挡不住进程外
观察(core dump、
--inspect、同用户进程读内存)。第一层(结构上不含)才是主防线。 - 凭据必须是控制面签发的 API key 形状(
flk_v1_<32 位小写 hex>_<64 位小写 hex>)。判据是@cyberred/contracts的isApiKeyPresentation(),它现读签发响应契约api_key_issued.v1的secret.pattern—— 与控制面签发时过的是同一个字符串。形状不对(截断、大小写、误粘 Console 会话串fls_v1_…、别家 token)⇒ 退 3、零请求,报错不回显任何片段。定长形状同时是擦洗层的前提 (以前靠「最短 16 字符」这条自定下限,已删除)。 ⚠️ 这把 CLI 的凭据限定为 API key;设备授权(Device Code)落地时要显式加第二种形状,不是把判据放宽。
重试
判据的唯一来源是 contracts 的 ERROR_POLICY,经 SDK 的
decideRetryForWireError() 查询。本包不看 HTTP status、不维护错误码清单,只提供
调度(指数退避 + 抖动 + --max-attempts)。
retryable与safeToRetry是两件事,两者都真才自动重试。- 有副作用的 operation(registry 的
requires_idempotency_key)带Idempotency-Key(UUIDv7,ARCHITECTURE §10.3.3),同一次命令调用的所有重试复用同一个键。 - transport reject(DNS / 连接重置 / 超时)一律不自动重试:没有
ErrorCode就没有 判据,而且"没收到响应"证明不了服务端没执行。
跨进程重放:不保证,且必须手动带键
本包不做 invocation journal(ARCHITECTURE §10.3.3 允许不做,但要求明写)。因此:
不保证跨进程重试幂等。 进程退出后键就没了;要安全重放,必须用
--idempotency-key显式带上同一个键并发同一份请求体。换一个键 = 一次全新操作, 可能重复计点。
失败输出(人读与 --json 都有)会把这次用的键交出来。
--json 的 replay.safe_to_rerun 在 dispatch = outcome_unknown 时一律是 false,
read-only 命令也不例外:side_effect_class: read_only 说的是业务数据不变,不是"重放
没有可观察副作用"(重放照样触发实时核验、计量、配额与上游采集),而今天没有 operation
级的重放契约。拒绝理由取自 SDK 的 assessReplayAfterUnknownOutcome(),出现在
replay.refusal_reason 里。
退出码与 dispatch
退出码回答"哪一类结局",dispatch 回答**"这次有没有可能已经在服务端执行过"** ——
后者才是 agent 决定要不要重跑的依据。
| 码 | 含义 | dispatch |
| --- | --- | --- |
| 0 | 成功 | response_received |
| 2 | 用法错误 | not_attempted |
| 3 | 本地错误(凭据 / base URL / 本地 schema / 请求被 SDK 拒) | not_attempted |
| 4 | 服务端 error,ERROR_POLICY 判定不可重试 | response_received |
| 5 | 可重试但放弃(次数用尽,或不带幂等键因而不敢自动重试) | response_received |
| 6 | 结局未知:请求可能已执行(transport reject / 响应过不了契约) | outcome_unknown |
| 130 | 中断 | 视是否已发出 |
迁移记:请求构造收敛到 SDK 之后的可观察变化
请求的 method / path 模板 / 逐参数编码 / 查询串 / 幂等键要求现在全部来自 SDK 的
planOperationRequest(),本包不再自己填模板、不再自己 encodeURIComponent。随之而来的
可观察变化:
- 含
/的合法 id(pack_ref.v1.id是任意 1–128 字符)现在发得出去,斜杠被编码在 段内(/v1/packs/ns%2Fpack-1)。⚠️ 这只保证 CLI 发出的原始 request-target 保持模板 结构;网关 / CDN / 路由在解码或归一化之后怎么看是另一回事,本仓在建的apps/api今天仍然拒绝含/的 pack id。 - 点段之类的值改由规划期拒绝(
PATH_PARAMETER_INVALID,SDK 对路径参数的限制严于它对 整条 path 的限制),拒绝点比以前更早:此时 SDK 从未签发过路径,所以--json的request.method与request.path都是null,而不是一条从未存在过的字符串。 - 规划失败在解析 base URL 与凭据之前处理,于是它不会被一条无关的本地错误盖住。
- transport 失败种类改用 SDK
replay.ts的闭合词表:原来的aborted现在叫canceled。 --verbose的行格式变了:尝试行只带 method(路径是不透明句柄,取不到字符串),真实 路径在请求构造成功之后单独打一行请求:GET /v1/...。- 包导出
callShape改名为planCall,返回SdkResult而不再抛异常。
输出面
- stdout 只承载结果,stderr 承载全部诊断。
--json | jq不会被诊断行污染。 --json的顶层是 CLI 自己的信封(schema_version: forumlayer_cli_result.v0, 未冻结),canonical 三段体status/envelope/result原样嵌在response字段里。不把 CLI 事实塞进三段体顶层——response.v2的additionalProperties是 false,那样做会造出一个冒充它的对象。没有可信响应时response是null, 不拼一个假的出来。--verbose只打 header 名、不打 header 值,一个字节的 body 都不打。
Ctrl-C
D34 明确未裁定 Ctrl-C 是否传播成服务端 cancel。因此本 CLI 不自动发 cancel,
只中断本地请求,并在 stderr 说明"服务端 Run 可能仍在跑仍在计点",给出
cyberred research cancel <run-id> 的原文。
信任分区(D27)
跑在用户设备上,属不可信区。不内嵌 PDP、不加载策略快照、不预留额度、不铸也不读
permit receipt、不施加 obligations、不建结果缓存、不从别处拼 pack locator。
workspace 依赖只有 contracts 与 sdk(tools/dep-boundaries 的 client 区强制)。
MCP 工具面从 @cyberred/sdk/mcp 拿;协议接线包 @cyberred/mcp-protocol 与 @modelcontextprotocol/sdk
都不在本包的依赖闭包里(test/dependency-closure.test.ts 钉住;依赖边界 lint 对 cli → mcp-protocol 报违规)。
--max-credits 是请求契约的必填字段,不是许可 —— 服务端会再执行一次上限检查。
已知没覆盖的形状
- 没有
--wait/ 轮询:--async只交出run_id,不代跑research get。 - 授权命令是顶层的
login/logout/whoami(不是 ARCHITECTURE §10.1 草案里的auth命令族);没有auth explain,也没有login --api-key(静态 key 不进本地存储)。 - 没有 invocation journal(见上),也没有并发 single-flight。
- 没有
research resume/research list/pack revisions|diff|export(D34 明确不在范围)。 - 没有代理(
HTTPS_PROXY)支持,也没有自定义 CA。 http只对字面量回环 IP(127.0.0.1/[::1])放行,localhost不算 —— 它的解析结果由 hosts / DNS 决定,按名字放行就等于把"明文不出机器"交给一份可被 改写的映射。要按名字放行,得由 transport 校验实际连上的对端地址,本包做不到。- 对着真服务端只有
pack get跑过,且只在测试里:apps/api/test/cli-mcp-console-key.e2e.pg.test.ts用真控制面签 key、进程内起apps/api、spawn 本包的dist/bin.js(认证通过 / 撤销即拒 / scope 不足三条)。 其余命令只跑过本地 HTTP 桩;没有对着任何部署跑过。 - 桩不覆盖:分块 / gzip 响应、HTTP/2、TLS 错误、重定向、超大响应体(大小上限只有 单元层面的实现,没有端到端用例)。
