agentleFS
Adonis0123Adonis0123/adonis-skills/skills/local-web-surface/SKILL.md

local-web-surface

Skill4 starsChanged 5 days agoMITView on GitHub

Build or extend a persistent macOS local web surface when the request includes a stable *.localhost URL, an existing loopback daemon/gateway, login startup, or a thin .app launcher that opens or wakes a persistent local HTTP surface. Once triggered, it can add file-backed CRUD or local/public content projections without changing public publishing. Covers domain ownership, exact Host routing, explicit access/threat boundaries, safe mutations, launchd lifecycle, TDD, curl checks, and proportional browser QA. Do not use for public or remote URL launchers, public hosting, production deployment, ordinary frontend/CRUD work on an existing dev server, merely opening or interacting with an existing URL, website audits, or native apps with no local HTTP surface.

The file

Files
SKILL.md

Local Web Surface

把一个“本地页面”交付成可长期使用的产品入口:固定 *.localhost URL、登录后自动可用、可选 .app 启动器、真实运行验收。

此 Skill 编排已有代码与系统能力,不规定前端框架。默认优先复用现有 loopback daemon;只有证据证明生命周期、权限或故障域必须隔离时,才新增监听进程。

适用性门槛

请求至少要包含一个持久 surface 信号:稳定 *.localhost 身份、loopback daemon/gateway、登录自启、共享 listener,或为持久本地 HTTP surface 提供唤醒/打开能力的 macOS .app launcher。只包装 https://... 公网或内网页面的 .app 不属于本 Skill。普通 dev server 上的页面/CRUD 实现不使用本 Skill;只需打开、操作或截图现有 URL 时交给 Browser/agent-browser。站点扫描与修复应交给当前环境中已安装且匹配的审计流程;不要硬路由到未验证存在的 Skill。

先定义可验证结果

开始前写出以下合同:

Local Web Surface Contract
- Surface: <name>
- Canonical URL: http://<name>.localhost:<port>/
- Data owner: <repo/module>
- Transport owner: <existing daemon or new service>
- Read boundary: <what may be displayed>
- Publish boundary: <none or what may leave the machine>
- Write boundary: <none or exact mutations>
- Access/threat boundary: <trusted local user/processes, or exact authentication/authorization>
- Launcher: <none or App name>
- Done signals: <evidence selected by Change surfaces; Host/handler-only = focused tests + diff scope + curl 200/421/404>

若用户已授权自主完成,可从代码和本机状态解析这些值,不重复询问。若选择会改变数据真相源、公开范围、访问/认证边界或占用未知服务端口,先停在一个决策问题上。

第一性原理

先从约束推导方案:

  1. 页面必须有稳定地址,用户不应记随机端口或手动启动命令。
  2. 数据必须只有一个真相源;UI、CLI 与 Agent 不得各写一份副本。
  3. 当 Write boundary 非 none 时,写入必须有一个受控端口;浏览器和 Agent 不直接拼 YAML/JSON。
  4. 本地服务只监听 loopback,不能因为“只在自己电脑上”就省略 Host、Origin、输入和路径检查;loopback、Host 与 Origin 都不是调用者身份认证。
  5. 多个页面共享同一生命周期时,一个 listener + 精确 Host 分发比多个常驻 daemon 更实用。
  6. .app 只负责唤醒/打开,不承载业务和第二份服务生命周期。

.localhost 及其子域由客户端作为 loopback 特殊域处理;不要为常规 *.localhost 别名修改 /etc/hosts。实现前仍应以当前系统上的 DNS/curl 结果验证,而不是只凭规范推断。

DDD 与依赖方向

把职责放回拥有它的 bounded context:

边界拥有什么不拥有什么
Domain/Data owner数据模型、校验、所需 repository、内容过滤、HTML/DTO 产物端口、LaunchAgent、全局 Host 表
Local application页面 handler、应用路由、用例编排daemon 生命周期、其他 surface 的内部实现
Loopback gatewayTCP listener、精确 Host 分发、body 上限、响应桥接surface 业务规则、数据文件格式
Launcher登录自启、.app、打开 canonical URL第二个 server、数据写入、业务状态

依赖方向固定为:gateway 依赖一个窄的 value contract;data owner 不反向依赖 gateway。跨仓库集成时,传递普通 request/response 值或加载一个稳定 handler 入口,不把 domain 类和 storage 细节复制进 gateway。

高内聚体现在:一个数据变更的校验、并发和持久化都留在 repository;低耦合体现在:新增 surface 只增加 Host 映射、handler 入口和 launcher spec,不修改其他 surface 的业务分支。

Change surfaces(变更面)

先按实际 diff 选择最小验收面,不因 Skill 覆盖完整产品生命周期就默认执行所有检查:

变更面典型改动最小必需证据
Host/handler-onlyexact Host 映射、只读 handler、跨 surface 路由focused gateway/handler tests、diff scope、curl 的 canonical 200/未知 Host 421/跨 surface 404
Lifecycle/launcherlistener 启动方式、LaunchAgent、installer、.app opener加做 plist、tracked PID、单 listener、重复安装和 launcher 参数验收
UI页面结构、交互、响应式加做合同相关的真实浏览器、Console、桌面与移动 viewport
Write/datamutation、repository、文件持久化加做 schema、ETag、并发、atomic write、临时 fixture 与失败路径
Public projectionlocal/public catalog 或 sync加做临时 public sync 与 private/local-only 排除

Host/handler-only 仅在现有 listener 与生命周期不变、没有 UI/写入/发布改动时成立;不要为它重装 LaunchAgent、重复查 PID、跑双 viewport 或执行全仓验证。每轮改动后重新看 diff;一旦跨入其他行,就只补对应行的证据。仓库自身明确要求的检查仍然优先。

前置检查

编辑前完成:

  1. 读取每个涉及仓库的 AGENTS.md 和架构/测试文档。
  2. 分别记录 git status --short;保留现有 dirty changes,禁止重置或顺手整理。
  3. 先分类 Change surfaces。只有涉及 lifecycle/launcher 时,才用 lsof -nP -iTCP:<port> -sTCP:LISTEN、launchctl print、进程命令和 launcher 代码确认真实生命周期。
  4. 只有依赖或 runner 相关改动时,才从 package.json、lockfile 与本机二进制确认 Node、runner、依赖版本。
  5. 只查当前变更面会触及的现有 server、handler、CLI、.app、LaunchAgent、数据文件、writer 或发布过滤;优先扩展现有模块。
  6. 对版本敏感 API 查官方文档或项目锁定版本的文档,并用运行结果验证。

设计门禁

在写代码前形成最小 surface 表:

SurfaceExact HostOwnerReadsWrites
existingexisting.localhost:<port>existing repoexisting dataexisting API
newnew.localhost:<port>data repoexplicit allowlistexplicit commands or none

必须明确:

  • gateway 使用 exact Host allowlist;未知、畸形或错误端口返回 421,不能回落到默认管理页。
  • 每个 Host 只看见合同列出的路由;不能访问其他 surface 的页面或 API。
  • 当 Read boundary 涉及公开/私有内容时,必须把“本地可见”和“允许发布”写成两个独立投影;意图不明确或页面可能被他人访问时,本地 reader 默认 fail closed,只展示显式公开内容。
  • private / local-only 只描述“不进入 public projection”,不承诺保密。loopback 服务会替调用方读取文件;同机其他账户或进程可能直接发 HTTP。Access/threat boundary 若不信任它们,必须加入真实认证/授权,或停止暴露该内容。
  • 仅当 Write boundary 非 none 时,写请求才至少检查 canonical Origin、Content-Type: application/json、应用标记 header、body 上限和 If-Match。Origin 与应用 header 主要约束浏览器写入,不是客户端身份。
  • 页面、CLI 与 Agent 等多个入口读写同一份数据时,统一路由到一个唯一 service/repository writer;禁止绕过它直接编辑。只有无法消除多个 OS 进程直接写入时,才增加跨进程锁。

内容型 Reader 的双投影合同(条件)

只在 surface 读取带发布状态的文章或文档时启用本节。不要把 public 同时当作本地读取权限和发布资格,除非用户明确要求两者完全相同。

当用户确认这是个人 loopback workspace、Access/threat boundary 允许当前本机调用者,并要求本地查看公开与私密成文时:

  • Local catalog 只扫描合同指定的文章根,例如 posts/**/*.md;接纳显式布尔 public: true 与 public: false。
  • Public projection 或 sync 仍只接纳 public === true。不要为满足本地预览去放宽发布脚本。
  • 缺失或非布尔 public 的记录既不展示也不发布,并只报告相对路径和元数据错误,不返回正文。
  • 研究素材、事实底座和助理数据(例如 raw/、notes/、digests/)不属于文章 catalog;只有独立 surface 合同明确纳入时才读取。
  • DTO 投影稳定 visibility: public | private、目录分类和规范化 tags。页面提供 all/public/private、category、tag 与搜索的组合筛选。
  • 私密卡片与详情必须明确标记 local-only,并说明 public sync 会排除它,防止“看得见”被误解为“会发布”。

当页面不是个人 loopback workspace、用户没有确认可显示私密内容,或访问边界不能保护敏感内容时,继续 fail closed,仅展示显式 public: true。若 public: false 代表真正机密而非发布状态,必须先加认证/授权,不能把 local-only 标签当作保护。

TDD 实施顺序

行为变更按 RED → GREEN → REFACTOR:

  1. Domain tests:覆盖合同中的数据规则;只读 surface 不虚构 mutation 或状态机。
  2. Mutation/concurrency tests(条件):仅当 Write boundary 非 none 时覆盖强 ETag、缺 If-Match、stale revision、同进程串行、revision 重检和临时文件清理。只有不可消除的多个 OS 进程会直接写同一文件时,才增加跨进程 lock、崩溃恢复、活 owner 检查和 replacement-lock 测试。
  3. Catalog tests(条件):仅当读取发布型内容时分别覆盖 local catalog 与 public projection、稳定 ID、排序、分类/tags/visibility、frontmatter 错误不泄露正文、Markdown XSS、图片 traversal/symlink escape。
  4. Handler tests:页面 shell、合同列出的用例、跨 surface 404、安全 guard、稳定错误码。
  5. Gateway tests:Host allowlist、bridge failure closed,以及合同需要的 body limit。
  6. Lifecycle/launcher tests(条件):只有对应变更面存在时才覆盖 launcher specs、plist、tracked PID 与重复安装生命周期。
  7. Runtime checks:只有适用于该合同的上述测试通过后才安装/重启真实服务。

不要先写整套实现再补快照测试。每个发现的运行时 bug 先补能失败的回归测试,再改实现。

文件型 CRUD 的最低安全线(条件)

只在 Write boundary 非 none 且存储为 YAML/JSON 单文件时启用本节。repository 至少做到:

  • 在系统边界严格校验根结构、允许字段、值域和真实日期。
  • 用内容 hash 生成 strong ETag;所有 mutation 要求 If-Match。
  • 唯一 writer 内的写操作串行,并在替换前重新检查 revision。优先让 CLI、页面与 Agent 调用这个 writer,避免制造多进程写拓扑。
  • 只有证据证明多个 OS 进程必须直接写同一文件时,才使用 crash-recoverable 独占 lock。优先复用成熟的 atomic directory + heartbeat/stale lease 或项目已验证的 advisory-lock,并让所有 writer 使用完全一致的 lock path、stale/update/retry 参数和 compromised 处理。
  • 若不得不自建 owner lock,记录随机 token、PID、进程启动身份和时间;活 lease 或身份匹配的 live owner 绝不接管。stale recovery 必须串行,并让所有 writer 在接管窗口前后检查带 owner identity 或 lease/heartbeat 的 recovery guard,防止两个 reaper 删除刚创建的新 lock。
  • 在目标文件同目录写唯一 temp file,flush/sync,重新检查 revision,再 atomic rename。
  • finally 只清理自己 token 对应的 temp/lock;删除前重新核对 owner,绝不删除已被新 owner 替换的 lock。
  • 多进程 writer 场景的回归测试必须覆盖:持锁子进程异常退出后 writer 可恢复;live owner 不被接管;旧 owner 清理时不能删除新 owner 的 lock。唯一 writer 场景不虚构跨进程 lock 测试。
  • mutation 返回新 revision;冲突返回 412,缺前置条件返回 428。
  • 自动化 CRUD 验收使用临时 fixture。不要为了测试 UI 改写用户真实数据,除非用户明确授权且恢复策略可靠。

页面实现

优先交付无构建依赖的响应式页面,除非现有应用已有框架:

  • 用户内容进入 DOM 时使用 textContent;只有经过受控 sanitizer/安全 Markdown renderer 的 HTML 才可 innerHTML。
  • 表单和按钮有可访问名称、键盘路径和 aria-live 状态。
  • mutation 失败保留用户输入;412 明确提示 reload/retry。
  • 若合同是 Todo CRUD,最少具备 create、list/read、rename/update、complete/reopen、delete 与确认。
  • 若合同是内容 Reader,最少具备合同要求的文章列表、搜索、目录分类、标签、可见性筛选、deep link、正文与安全图片;只有合同允许私密内容时才显示 private 状态。
  • 同时验收桌面和约 390x844 的移动 viewport。

Gateway 与 launchd

gateway 只监听 127.0.0.1(或明确验证过的 loopback),并把传入 header 缩到 handler 实际需要的集合。loopback/Host 隔离不等于认证;按 Access/threat boundary 对敏感读取和写入增加真实授权。body 超限时排空请求,不要销毁底层 socket;否则客户端可能收不到稳定 413。

macOS 登录服务遵守:

  • 一个 ~/Library/LaunchAgents/<label>.plist,使用 RunAtLoad + KeepAlive。
  • LaunchAgent 必须拥有最终长期 Node listener。优先让 ProgramArguments 直接执行 node --import tsx <absolute-cli.ts> web;只做环境准备且最后 exec node ... 的 shell 也可保持同一 PID。不要让 LaunchAgent 只跟踪会再 spawn 子进程的 tsx CLI wrapper,否则 bootout 可能留下孤儿 listener。
  • WorkingDirectory、logs 与 PATH 使用绝对值;plutil -lint 必须通过。
  • 多个 .app 是薄 opener:exec <cli> open-web <surface>。每个 app 有独立 bundle ID/name,但不启动第二份 daemon。
  • installer 必须可重复执行。至少连续安装两次并确认仍只有一个 listener、LaunchAgent state = running、error log 无 EADDRINUSE。

真实验收

先按 Change surfaces 选择证据。Host/handler-only 的完整默认验收只有三项:focused gateway/handler tests;git diff --check 与范围核对;临时或既有安全 listener 上用 curl --noproxy '*' 证明 canonical Host 200、未知 Host 421、跨 surface 路由 404。这类改动默认不要求 full tests/typecheck/build、LaunchAgent 重装、PID/lsof、浏览器或 public sync。

diff 扩大后,按新增变更面补充以下检查,不机械执行无关项:

  1. 所有变更都跑涉及仓库的 focused tests;只有仓库规则要求或 diff 涉及共享类型、构建、依赖时才加 full tests、typecheck/build。单仓库任务不得虚构第二个仓库。
  2. git diff --check,确认没有范围外文件、secret、临时 lock 或 fixture。
  3. Lifecycle/launcher:plutil -lint 涉及的 plist;检查每个变更的 .app/Contents/MacOS/launcher 的 surface 参数。
  4. Lifecycle/launcher:launchctl print gui/<uid>/<label> 显示 state = running 且有唯一 tracked PID。
  5. Lifecycle/launcher:lsof 证明目标端口只有一个 loopback listener,且 tracked PID 就是 listener PID,而非短命 wrapper。
  6. curl --noproxy '*':每个 canonical Host 返回正确页面/API;未知 Host 421;跨 surface 路由不泄露;合同包含 body guard 时再证明超大 body 稳定 413。
  7. UI:用真实浏览器打开变更的 URL,按合同检查 Console、快照与功能;只有合同包含搜索/deep link 或 CRUD 时才验收它们;同时检查桌面与移动 viewport。
  8. Public projection:仅对发布型改动跑临时 target sync,证明 public:false 与合同列出的其他本地内容没有进入公共产物;若本地 reader 允许私密内容,再用 API 和浏览器分别证明它们确实进入本地列表、详情、搜索/筛选与 deep link。

若 UI CRUD 已由临时 repository/handler 集成测试覆盖,真实浏览器验收可以只读;不要用“为了验收”作为改动用户数据的理由。

失败恢复

  • EADDRINUSE:先解析 tracked PID、listener PID、父子/进程组和完整命令,并搜索应用是否主动 spawn/fork/daemonize;只终止已验证属于该服务的进程。修 installer 后重复安装验证,不只手工 kill 一次。
  • dynamic handler 缺失或响应越界:gateway 返回稳定 503,不暴露本地绝对路径和 stack。
  • lock timeout:返回可重试错误;按选定协议用 heartbeat/stale lease,或 token、PID 与进程启动身份判断 live/stale。只有 crash-recovery 协议能证明 owner 已失效时才回收;无法证明就保留 lock 并报告,不按未经 heartbeat 约束的文件年龄猜删。
  • 浏览器能开但 mutation 失败:检查真实 Origin、Host、ETag、header 与 socket 行为,不用关闭安全 guard 取巧。
  • local/public catalog 有错误:报告相对路径和元数据问题,不返回文章正文;先核对当前 surface 的 Read boundary 与 Publish boundary,不用删掉私密投影来掩盖发布边界缺失。

完成报告

只在真实信号齐全时声明完成:

Local Web Surface Result
- URLs: <canonical URLs>
- Launchers: <none | apps + LaunchAgent when that change surface exists>
- Data ownership: <source of truth + only writer>
- Security boundaries: <Host/Origin/access-auth/local visibility/publication/path controls>
- Verification: <exact evidence selected by Change surfaces>
- User data touched during QA: no | <explicit details>
- UNVERIFIED: none | <blocked check and reason>

未经用户授权,不 stage、commit、push、deploy 或改写真实业务数据。

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.