plugin-reach — 触及:一次工具调用能碰本机的什么
包名: @aalis/plugin-reach
源码: packages/plugin-reach/src/index.ts
契约: api-reach(服务描述符 reach、凭据与沙箱策略的类型、触及的纯函数)
概述
按一次工具调用签发触及凭据。凭据回答「这一次调用能读写哪些文件、命令在哪执行、能不能联网」:工具从调用上下文取得凭据后转交给存储与沙箱,路径类目标要先经 require 判过。工具、存储与沙箱还没有接入凭据,装上本插件不改变现有工具的行为。只有主体是 owner 的调用(人亲手的一轮、这次调用的参与者都是 owner;续做与子任务的类别随起它的那一轮,主体按这次调用的参与者,批过的按批的人)有本地效果,不看会话的受众:owner 在群里亲手发起的一轮同样有,主目录是默认工作区;别人亲手发起的、自主回合与没有回合的调用一律没有。
另管两样东西:本会话临时区、子进程环境。
插件声明
definePlugin({
name: '@aalis/plugin-reach',
displayName: '触及',
subsystem: 'authority',
provides: [reach],
uses: {
provide,
events,
lifecycle,
logger,
paths: hostPaths,
sessionManager,
authority: optional(authority),
process: optional(processService),
},
apply: run,
});authority缺席时谁都不是 owner,凭据一律没有本地效果。process缺席时取不到 login shell 的环境,子进程用守护进程的环境。
签发凭据
forCall(call) 的输入与执行守卫同一口径(会话、平台、发言者、授权身份、入站回合),按下面几项交给 api-reach 的 resolveReach:
| 输入 | 取法 |
|---|---|
| 主体 | 权限服务的 principalOf(call):类别为人亲手、这次调用的参与者都是 owner 的为 owner,不论在哪个会话;续做与子任务的类别沿 fromTurn 随起它的那一轮(见下文「子任务」),经审批放行的调用参与者是批的人;其余(自主回合、有别人参与的、没有回合的调用)为 other,没有本地效果 |
| 档位、越界策略 | 固定为工作区档、越界要问 |
| 主目录 | 项目工作区(根会话带绑定)是绑定目录;其余会话(WebUI、IM 私聊、群、内部会话等)一律是默认工作区目录 HostPaths.workspace(<数据目录>/workspace)。不跟 plugin-storage-local 的 roots 配置走:用户在 roots 里改了 workspace 根的路径,文件工具的 workspace:/ 跟着变,主目录照旧是 HostPaths.workspace |
| 绑定 | 根会话的附加可写目录与本会话树建的 worktree |
| 仓库的 git 目录 | 从绑定目录往上找 .git(见下文) |
| 本会话临时区 | tmp/sessions/<会话 id 的 UTF-8 十六进制>/ |
| 三张清单 | 激活时算好(见下文「清单」) |
主体、绑定与仓库的 git 目录在签发时取好。
宿主通知开的回合(后台任务结束后的续做)的主体同样按 principalOf 判(见上):owner 那一轮的续做有本地效果,在群里也一样;群成员那一轮的没有。
子任务
子任务与续做同一条规则:子任务插件在入站回合里派出的任务消息与追问带着派它的那一轮(fromTurn),子任务那一轮的类别随它、参与者是它交来的,权限服务的 principalOf 再按这次调用的参与者判(经审批放行的调用是批的人),不按父会话里别的哪一轮(比如最近一次签发凭据的那一轮)。每一轮按它自己的任务消息算:同一个子会话,定时任务那一轮派的回合没有本地效果,owner 那一轮追问的回合随 owner。子任务里转后台的调用的续做同样沿 fromTurn 回到派子任务的那一轮。派它的是定时任务等自主回合,或任务消息不是在入站回合里发出的(如编排的工具节点)时,主体是 other;owner 与别人一起派的,子任务里的调用主体是 other,经 owner 批过的那一次随 owner。
仓库的 git 目录
项目会话的绑定目录在 git 仓库里时,仓库的 git 目录(共用的那个)在工作区档可写,落点(钩子、配置)除外;绑定在 worktree 上时,它在 worktree 之外,要另算进来。从绑定目录往上找 .git:目录形式的就是它,链接到目录的按它指向的目录算;文件形式的(worktree、子模块)按里面的 gitdir 与 commondir 找到共用的那个,读不到时按不在仓库里。找到的目录没有 HEAD 与 objects 时不算,免得一个 .git 把任意目录指成可写;不论算不算,找到 .git 就不再往上找外层仓库。仓库的其他 worktree(包括正在跑生产的那一份)不在可写范围里。
回给模型的话
require 判不行时回一句话:要做什么、为什么不行、能不能申请。
- 宿主控制面与拒读清单:写明不可申请,不要尝试绕过;
- 越界策略为直接拒绝的:写明原因与越界策略;
- 要问 owner 的:写明原因,并说明这里没有可用的审批;
- 没有本地效果的:写明这一轮不只有 owner 的话。
清单
三张清单(宿主控制面、持久化与自动执行落点、拒读清单)在激活时按数据目录布局、操作系统用户的主目录与平台生成一次,工具边界与沙箱用同一份,规则见 api-reach。
- 比较一律用真实路径:安装集与用户主目录先取真实路径,安装集经符号链接给出时按它指向的目录。
- 链接出去的根:清单里以具体路径为根的规则(如
~/.zshrc、~/.gitconfig、~/.ssh、数据目录里的主配置与secrets.yaml),路径存在且路径上有符号链接的(常见于链接到 dotfiles 仓库),按真实路径再加一份同样的规则,例外随根挪过去:读写它们的真实路径同样拒读、判宿主控制面或落点。取真实路径在激活时做一次,之后链接改指向、新建的链接不跟踪,要重启插件;取不到真实路径的(没有权限、链接成环)记 warn,那一条只按写法拦。目录里单个链接出去的文件(如~/.ssh/config链接到别处、~/.ssh本身不是链接)不在内。 - 安装集的形态:安装集下有
packages/目录的是源码形态,其余是 npm 形态(create-aalis 生成的项目,数据目录在安装集里)。npm 形态下另把守护进程的入口文件(启动脚本process.argv[1]的真实路径,取不到不加)与安装集的package.json(插件发现读它的依赖)列进宿主控制面;源码形态下它们是在改的源码,不列。 - 守护进程加载的代码:安装集的依赖解析到安装集以外的目录(经链接装进来的插件与库)也归宿主控制面。取法是看安装集
node_modules下每一项(含@scope/下的)的真实路径,落在安装集以外的整个目录都算;源码形态下packages/里链接到安装集以外的包,与安装集里的包一样只算它的dist与node_modules。断掉的链接、不是目录的@scope跳过。 - 覆盖不到的:依赖没装进安装集、守护进程按 Node 的规则从更上层目录的
node_modules加载的包。
本会话临时区
tmp/sessions/<会话 id 的 UTF-8 十六进制>/,权限 0700。十六进制只有小写,大小写不敏感的文件系统(macOS 默认、Windows)上两个会话也不会落到同一个目录。
- 有本地效果的调用签发凭据时建好;没有本地效果的不建。
- 删除会话时在后台删,不拖住删会话;删不掉记 warn。同一个 id 的会话删了又建(如终端的
cli-default)时,签发凭据等前一份删完再建。 - 启动时与每天清扫一次,在后台跑,不拖住激活:7 天没有改动(目录与它下面任何一项的修改时间都早于 7 天前)、会话也不在运行的删掉;会话表里查不到的同样按 7 天算,不立即删(会话表读不出时表是空的,不能当作会话都删了)。删之前再查一次会话在不在运行;遍历时被别处删掉的这一轮不动。名字按十六进制解码再编码回去对不上的(不是某个会话 id 的编码)不动。逐个处理:一个看不了或删不掉的记 warn(写明目录与原因),不挡别的。
- 插件停下时等在跑的清扫与删除结束。
插件自己的临时目录(浏览器截图、代码执行的脚本目录)不在这里,清扫不碰。
子进程环境
凭据的 processEnv(mode) 是子进程环境的唯一来源,工具只转交。
- 来源:在主目录下用 owner 的 login shell(
SHELL,没有时取操作系统账户登记的)取完整环境(交互式登录 shell,-i -l -c,限时 10 秒),缓存在内存、不落盘;同一个主目录取到一次就一直用,同时来的调用共用一次取。默认工作区目录由 runtime 初始化时建,主目录不在时在用户主目录下取。取不到(没有 shell、超时、失败、没有进程服务)时用守护进程的环境并记一条告警,1 分钟内沿用,之后的调用再取一次。 - 宿主执行(
host):完整的 login shell 环境。 - 沙箱内(
sandbox):- 同样是完整的登录环境,不按名字或值猜凭据:沙箱里的命令能看到你登录环境里的所有变量,令牌、密码、带密码的网址都在内。能进沙箱的只有主体是 owner 的一轮,与密钥的其余处理同一个理由:密钥不靠在出口过滤,靠权限隔离。HOME 保持 owner 的。
- 只去掉两样:
AALIS_*是实例自己的命名空间,不交给沙箱里的命令;SSH_AUTH_SOCK是 ssh-agent 的 socket,ssh-agent 不对沙箱开放。 - 注入
npm_config_userconfig:本会话临时区里的npmrc,是用户主目录.npmrc去掉 npm 定义的认证键(令牌、用户名与密码、客户端证书与私钥,文件或内联)的副本,镜像源与作用域注册表照留(~/.npmrc在拒读清单里,沙箱里的 npm 读这份副本);没有.npmrc时是空文件。临时区在沙箱里可写,副本先以随机名独占创建、再改名盖过去,沙箱里的命令把npmrc换成符号链接也改写不到别处;临时区本身不是目录(被换成了链接)时拒绝。npm 的环境变量不分大小写,同名的旧值先去掉。 - 注入
npm_config_nodedir:这份环境的 PATH 上第一个node所在的安装,带头文件(include/node/node.h)时才给。 - 注入
PUPPETEER_CACHE_DIR(本会话临时区下的puppeteer)与npm_config_side_effects_cache=false(沙箱里构建出的产物不进 pnpm 的共享缓存)。 - 经
GIT_CONFIG_COUNT与GIT_CONFIG_KEY_<n>/GIT_CONFIG_VALUE_<n>注入,接在登录环境原有的那组后面编号(原有的与GIT_CONFIG_PARAMETERS照留):github.com、gitlab.com的 SSH 地址拉取时改走 HTTPS(insteadOf,沙箱里没有 SSH 凭据),推送仍是 SSH(pushInsteadOf),要宿主执行。
没有本地效果的凭据调 processEnv 抛错。