import type { AgentResult, RunnerErrorEnvelope, SafetyTemplates, TaskInput } from '@cutie-crypto/connector-core'; import type { HermesAdapterConfig, PlatformAdapter, PlatformId, PlatformSetupResult } from './base'; declare const CUTIE_PROFILE_NAME = "cutie"; /** 整体重写 .env 文件(rewriteDotEnv 删除字段,writeDotEnv 只追加/更新) */ export declare function rewriteDotEnv(filePath: string, kept: Record): void; /** 执行命令,失败时 try/catch + log.warn(幂等辅助) */ declare function tryExec(cmd: string): void; /** 移除 crontab 中匹配 pattern 的行 */ declare function removeCrontabEntry(pattern: string): void; export declare class HermesPlatform implements PlatformAdapter { readonly id: PlatformId; private readonly home; private readonly mainConfigFile; private readonly cutieDir; private readonly cutieEnvFile; private readonly cutieConfigFile; private readonly workbenchDir; private readonly workbenchEnvFile; private readonly workbenchConfigFile; /** 由 ConnectorConnection wrapper 在 start 之前注入。窄字段类型(约束 2): * callAgent / augmentHeartbeat 用 gateway_url / gateway_secret / agent_model / hermes_profile */ private config; attachConfig(config: HermesAdapterConfig): void; callAgent(input: TaskInput): Promise; /** * profile 名合法字符集:字母数字下划线短横线。拒绝一切 `.` / `/` / 空白等—— * 下游 profileDir() 直接 `path.join(this.home, 'profiles', profileName)`, * 不做这层校验的话 `hermes:..` 会拼出 `~/.hermes/profiles/..` = `~/.hermes`, * `readProfileGatewayCredentials` 就会去读主 profile 的 `~/.hermes/.env`, * 违反"绝不读主 profile key"的隔离原则(2026-07-03 Codex review P1)。 */ private static readonly VALID_PROFILE_NAME_RE; /** 从 `hermes:` 形态的 agent_route 里解析出 profile 名;非法/缺失返回 null。 */ private extractHermesRouteProfile; /** * 按路由到的 profile 名动态解析实际要打的 gateway_url/secret。 * * - routedProfile 为 null(无 agent_route 或非 hermes:* 前缀)或等于 connector * 启动时已连接的 profile(this.config.hermes_profile,缺省视为 cutie):直接用 * this.config 的静态 gateway_url/secret,不读盘(现状行为不变,含 cutie profile)。 * - routedProfile 是另一个 profile(如 workbench):读取 * ~/.hermes/profiles//.env 的 API_SERVER_PORT/API_SERVER_KEY 现算 gateway。 * 文件缺失或字段不全时 warn + fallback 到已连接的 gateway(不阻断任务)。 * 绝不读主 profile(~/.hermes/.env)的 key,隔离原则不变。 */ private resolveGatewayForProfile; /** 读取 ~/.hermes/profiles//.env 的 API_SERVER_PORT/KEY;缺失/非法返回 null。 */ private readProfileGatewayCredentials; selfUpgrade(targetVersion: string): Promise; augmentHeartbeat(envelope: Record): Record; probeAgentHealth(): Promise; getCapabilities(): string[]; /** * W2.1 route precedence: * 1. input.agent_route(server 计算好的 `hermes:` 路由名 → 取 profile 部分) * 2. input.model(旧 payload.agent_model 透传) * * 防御:非 hermes:* 前缀显然不属于本平台,记 warn 并 fallback。 */ private routeForHermes; getDataDir(): string; isInstalled(): boolean; getDefaultGatewayPort(): number; getDefaultAgentModel(): string; readGatewaySecret(): string | null; getCutieWorkspaceDir(): string; private profileDir; private profileEnvFile; private profileConfigFile; private profileExists; private cutieProfileExists; private createProfile; private createCutieProfile; private static readonly MESSAGING_ENV_PATTERNS; private sanitizeEnvFile; private sanitizeClonedEnv; /** * clone→exists 判定→sanitize 的完整幂等链路,cutie/workbench 共用。 * * 返回 `isNew`(本次调用是否真的新建了 profile,而不是复用已存在的)—— * 2026-07-03 Codex review P2-1:`hermes profile create --clone` 会把 * 主 profile 的 `API_SERVER_PORT`(通常是 KOL 主 profile 正在用的端口,如 * 8642)原样克隆进新 profile 的 .env。resetApiServerFields 的 B3 幂等逻辑 * ("读到既有端口就复用")如果读到的是这个刚克隆继承来的值,会把它误判成 * "已配置过、直接复用",导致新建的 workbench/cutie profile 的 gateway 端口 * 撞主 profile 的端口。调用方必须在 `isNew === true` 时强制走端口扫描分配, * 不能信任 .env 里刚克隆继承的 API_SERVER_PORT。 */ private ensureProfile; addCutieAgent(_agentModel: string): boolean; applySafetyTemplates(templates: SafetyTemplates): void; /** * 把 HARDENED SOUL.md/AGENTS.md 写入指定 profile 目录。抽出供 applySafetyTemplates * (cutie,公开接口方法)和 workbench profile setup(内部直接调用,不重复触发上面 * 的 canary_token 一次性 warn)共用。 */ private writeHardenedWorkspace; enableResponsesEndpoint(): boolean; private enableResponsesEndpointInternal; /** * 强制重置 API_SERVER_ENABLED/KEY/PORT + 按传入白名单重写 platform_toolsets。 * cutie/workbench 共用;差异只在 dir/envFile/configFile/portScanStart/toolsets 参数。 * * @param opts.forceNewPort true 时无条件跳过"读既有端口复用",直接走扫描分配。 * 2026-07-03 Codex review P2-1:`hermes profile create --clone` 会把主 * profile 的 API_SERVER_PORT 原样克隆进新 profile 的 .env——如果这次调用是 * 紧跟着刚创建的全新 profile(ensureProfile 返回 isNew=true),.env 里读到 * 的"既有端口"其实是继承自主 profile 的值,不是 connector 自己配过的,必须 * 无视它、强制扫描分配新端口,否则 workbench/cutie gateway 会撞主 profile * 的端口。已配置过的 profile(isNew=false,B3 幂等场景)继续复用,不受影响。 */ private resetApiServerFields; /** 读取 profile .env 里已配置的 API_SERVER_PORT;不存在/非法返回 null。 */ private readExistingPort; private readExistingCutiePort; /** cutie/workbench 的 toolsets 白名单都必须覆盖这份已知 platform 全集。 */ private static readonly ALL_TOOLSET_PLATFORMS; /** 全平台 [](cutie profile 用:AI 分身面向粉丝必须锁死所有工具)。 */ private static emptyToolsets; private configureToolsetsEmpty; /** * 定位 config.yaml 里某个顶层 key 的起止行号(顶层 key 本身 → 下一个顶层 * key,或 EOF)。`keyLinePattern` 必须同时兼容"块形态"(`key:` 单独一行, * 子项缩进跟在后面)和"已经整块覆盖过的单行形态"(如 `key: {}`)——否则 * 重跑幂等检测会把 `key: {}` 误判成"没有这个 key",在文件末尾重复追加一份 * 造成 YAML 里出现两个同名顶层 key。platform_toolsets/mcp_servers 共用。 */ private static locateTopLevelBlock; /** * 把 config.yaml 里某个顶层 key 对应的块整体替换为 `newBlockLines`(找不到就 * 追加到文件末尾),幂等(内容相同则跳过写盘),原子写(中断不会留下截断的 * YAML)。platform_toolsets(cutie/workbench 工具白名单)和 mcp_servers * (clone 继承的 MCP key 清空,2026-07-03 DoClaw 生产实测)共用这套机制。 */ private rewriteTopLevelYamlBlock; /** * 强制把 platform_toolsets 整块重写为传入的白名单(未出现在 toolsets 里的 * platform 视为 [])。不做"部分合并"——clone 来的主 profile 可能有 * `cli: [hermes-cli]` 等非空值,必须整块覆盖才能保证白名单语义(IMPL §7.1 对 * cutie 的要求:所有 platform 强制关工具;workbench 只开 api_server: ['web'])。 */ private writeToolsetsBlock; /** * 强制把 mcp_servers 整块清空为 `{}`。 * * 2026-07-03 DoClaw 生产实测发现的安全缺陷:`hermes profile create * --clone` 会把主 profile config.yaml 的 `mcp_servers.cutie`(含 KOL 的 MCP * key,46 个工具含 cutie_create_signal 等写工具)原样克隆进新 profile。Hermes * 的 MCP 工具**不受 platform_toolsets 白名单约束**——DoClaw 上 workbench 的 * api_server 白名单只有 `web`,但 Agent 实测自报有全套 cutie_* 工具并真实调用 * 了 mcp_cutie_read_resource。workbench 设计的安全边界是"抓网页内容的 prompt * injection 最坏只是草稿被带偏(只读)",一旦带上 MCP 写工具,注入就能真的 * 发信号。cutie profile(AI 分身)此前没暴露这个问题纯属时序运气:存量 KOL 的 * cutie profile clone 于主 profile 配 MCP(6-30)之前;新 KOL 若主 profile * 已配 MCP,cutie profile 同样会泄漏。 * * 与 platform_toolsets 同样的哲学:整块强制覆盖(不做部分合并/白名单过滤), * 每次 setup 都强制刷新(幂等:已经是 `{}` 就跳过写盘)。 */ private sanitizeMcpServers; private allocateFreePort; private isPortInUse; waitForGatewayReady(opts: { port: number; secret: string; timeoutMs: number; profileName?: string; }): Promise; configureSessionMaintenance(): boolean; getRestartCommand(): string; coerceAgentModel(serverModel: string | undefined, configModel: string): string; derivePersonalitySync(): { main_workspace: string; cutie_workspace: string; } | null; private detectActiveProfile; runPlatformSetup(ctx: { agentsMd?: string; soulMd?: string; agentModel: string; }): Promise; /** * workbench profile 完整 setup:clone→sanitize→端口/key 重置→toolsets 白名单 * (仅 api_server: ['web'])→gateway unit 安装→启动→waitForGatewayReady。 * 与 cutie profile 同构,端口从 cutie 实际生效端口 + 1 开始扫描分配,避免撞 * cutie 的端口;已配置过的 workbench profile 端口幂等复用(同 B3 教训); * 刚 clone 出的全新 workbench profile 强制走扫描分配,不信任克隆继承的端口 * (P2-1,见 resetApiServerFields 的 forceNewPort 注释)。 */ private setupWorkbenchProfile; /** * 启动 hermes-gateway-。cutie/workbench 共用(原 startCutieGateway * 泛化,profileName 参数化,行为对 cutie 完全不变,startCutieGateway 仍保留原名 * 作为薄封装,测试直接调用它的用例不受影响)。 * * 优先用 `hermes -p gateway run --replace`(nohup detach),因为: * - `--replace` 会 SIGTERM 已有同 profile gateway 再启动,端口冲突安全 * - 不依赖 systemd unit 是否正确安装(setup 场景下 unit 可能刚装、还没 daemon-reload) * - 作为 npx 子进程运行时不触发 Hermes approval(execSync 是直接 fork,不走 terminal tool) * * systemd restart 降为 fallback:unit 已存在且 enable 时 systemd 接管生命周期更好, * 但 setup 首次跑时 unit 可能不可靠。 */ private startGateway; private startCutieGateway; /** systemctl --user is-active — 返回 stdout 严格等于 'active' 才算活跃 */ private isSystemdServiceActive; } export { CUTIE_PROFILE_NAME, tryExec, removeCrontabEntry };