Skip to content

fix(runtime): keep UI writes out of disposable checkout#1149

Merged
zts212653 merged 4 commits into
mainfrom
fix/1141-runtime-write-protection
Jul 14, 2026
Merged

fix(runtime): keep UI writes out of disposable checkout#1149
zts212653 merged 4 commits into
mainfrom
fix/1141-runtime-write-protection

Conversation

@zts212653

@zts212653 zts212653 commented Jul 14, 2026

Copy link
Copy Markdown
Owner

What

Establish one persistent project-path boundary for settings, governance, Skill, MCP/mount, thread, proposal, and invocation flows:

  • redirect runtime-root paths and descendants to the durable workspace root;
  • reject Clowder AI/runtime/workspace roots and descendants for portable governance bootstrap;
  • lazily migrate legacy thread/proposal paths while stale runtime descendants fail closed;
  • preserve public-main route behavior while porting the invariant;
  • add regression coverage across mutation and read-back paths.

Why

Page-driven mutations could treat the disposable runtime checkout, including packages/api, as a persistent project. Governance bootstrap then created tracked files there, and a legacy thread path let later agent work add more tracked changes. The next normal daemon start correctly failed closed on the dirty runtime worktree.

Issue Closure

Original Requirements

Plan / ADR

  • Plan: investigation and TDD checkpoints in the linked Cat Cafe thread
  • ADR: none; this is a route-level safety invariant, not a new ownership topology
  • Architecture cell: none
  • Map delta: none
  • Why: one shared resolver replaces duplicated route assumptions; no Store, Queue, Router, Adapter, Dispatcher, or Binding is introduced

Tips Contribution

  • Added or updated a capability tip
  • Existing tip still covers this change
  • tips_exempt: runtime safety bugfix; no new user action or discoverable capability

Tradeoff

The patch does not move, delete, stash, or overwrite existing dirty files because their keep/discard intent is unknown. It prevents new runtime writes and migrates stored path references. Missing external paths unrelated to runtime retain their legacy behavior.

Test Evidence

  • packages/api public test suite — 16,586 tests; 16,558 pass; 0 fail; 28 skip
  • pnpm check — exit 0
  • pnpm lint — exit 0; baseline warnings only
  • pnpm -r run build — exit 0
  • git diff --check — exit 0
  • Targeted path and migration suites green
  • Root Artifact Guard: empty
  • Exact reviewed HEAD: a1617d792f251794dc453601315ea4a92ddc7441

Open Questions

  • Does every persistent write and read-back path preserve the runtime-to-workspace invariant?
  • Are symlink aliases and deleted runtime descendants fail-closed without breaking unrelated external paths?
  • Does the manual public port preserve public-main behavior without importing an old sync snapshot?

本地 Review: [x] sonnet independently reviewed and approved exact HEAD
云端 Review: [ ] trigger after PR creation

Why: settings, governance, skill, thread, and invocation flows could persist the runtime worktree or its descendants as project roots. That let page-driven mutations dirty the disposable checkout and made a normal restart fail closed. Centralize the persistent-path boundary, migrate legacy runtime paths to the workspace, and reject governance self-bootstrap descendants.
@zts212653

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@zts212653

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@zts212653

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a1617d792f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +54 to +55
const runtimeRoot = await validateProjectPathDetailed(runtimeRootRaw, options);
if (!runtimeRoot.ok) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid blocking projects on runtime-root allowlisting

When legacy allowlist mode is enabled (PROJECT_ALLOWED_ROOTS) and CAT_CAFE_RUNTIME_ROOT points outside that allowlist, an otherwise valid external project passes the target validation but then fails here with runtime_root_invalid before the code can determine that the target is not under the runtime root. That makes routes migrated from validateProjectPath to resolvePersistentProjectPath reject allowed external projects unless the internal disposable runtime directory is also added to the user's project allowlist; the runtime root should be canonicalized independently, or only make the request fail after the target is known to be inside it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8abbd2b. The disposable runtime checkout is now canonicalized with realpath plus directory validation independently of PROJECT_ALLOWED_ROOTS; user-selected targets and the persistent workspace still pass normal project policy. The regression reproduced runtime_root_invalid for an allowed external project and now passes.

Why: cloud review found that stored game sentinels could be mistaken for runtime descendants, while legacy allowlists could reject valid external projects solely because the internal runtime checkout was not allowlisted. Preserve known sentinels and canonicalize the internal runtime root independently from user project policy.

[小太阳·砚砚/GPT-5.6 Sol🐾]
@zts212653

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8abbd2bbad

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/api/src/routes/threads.ts Outdated
Comment on lines +461 to +463
let threads = migratedProjectPath
? await threadStore.listByProject(userId, migratedProjectPath)
: await threadStore.list(userId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don't drop runtime-owned threads during project filtering

When a user filters GET /api/threads?projectPath=<runtime path>, this remaps the requested path to the persistent workspace before calling listByProject, but both in-memory and Redis thread stores compare t.projectPath === projectPath exactly. Existing threads that still have the old runtime-root projectPath are therefore excluded before migrateRuntimeProjectPath() can update them, so they disappear from project-filtered views instead of being migrated. Fetching all threads first or querying both the migrated and original path would preserve those legacy threads.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8c858f2. Project filtering now queries both the legacy requested coordinate and the migrated persistent coordinate, deduplicates by thread id, then migrates returned threads. The regression starts with a legacy runtime-owned thread and makes the filtered request first.

export const skillsWriteRoutes: FastifyPluginAsync<SkillsWriteRouteOptions> = async (app, opts) => {
const CAT_CAFE_SKILLS_SRC = opts.skillsSourceDir ?? resolveSkillsSourceDir();
const skillsRepoRoot = dirname(CAT_CAFE_SKILLS_SRC);
const globalProjectRoot = await redirectRuntimeProjectPath(opts.mainProjectRoot ?? skillsRepoRoot);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass the remapped root into skill mount rule reads

This computes the persistent global root for skill writes, but the sync handlers below still call readMountRules(projectRoot, opts.mainProjectRoot ?? resolveMainRepoPath()), which points back at the runtime checkout when CAT_CAFE_RUNTIME_ROOT is active. Since the mount-rules route now writes defaultMountRules under the persistent root, /api/skills/sync and /api/skills/sync-skill can miss operator-configured default mount rules and reconcile using built-in/runtime defaults instead. Use globalProjectRoot as the main root for these reads.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8c858f2. Both sync and sync-skill now pass globalProjectRoot to readMountRules. A shared regression stores custom defaultMountRules only in the persistent workspace and verifies both routes mount through that persistent configuration.

Why: Cloud review exposed four remaining coordinate mismatches: allowlist policy ran before runtime remapping, internal governance roots were treated as user projects, filtered legacy threads disappeared before migration, and skill sync read defaults from the disposable checkout. Canonicalize internal roots separately, query both legacy and persistent thread coordinates, and use the persistent global root for mount rules.\n\nTests: API build plus project-path, threads-endpoint, and skills-route regressions.\n\n[小太阳·砚砚/GPT-5.6 Sol🐾]
@zts212653

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8c858f2cd6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +163 to +164
const catCafeRoot = opts?.catCafeRoot ?? findMonorepoRoot(process.cwd());
if (validated === catCafeRoot) {
reply.status(400);
return { error: 'Cannot setup governance for Clowder AI itself' };
const validatedResult = await validateExternalProjectPathDetailed(projectPath, catCafeRoot);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Redirect setup bootstrap root before writing registry

In the default production registration, projectSetupRoute is registered without opts.catCafeRoot, and runtime-worktree starts run with process.cwd() inside CAT_CAFE_RUNTIME_ROOT. This leaves catCafeRoot pointing at the disposable checkout even though the target project is now validated through the persistent resolver; the same value is later passed to GovernanceBootstrapService, whose registry writes .cat-cafe/governance-registry.json under catCafeRoot. A UI /api/projects/setup call for any external project can therefore still dirty the runtime worktree and break the next daemon start; resolve/redirect this root to CAT_CAFE_WORKSPACE_ROOT before validation/bootstrap.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 02f7fce. projectSetupRoute now redirects the default or explicit Clowder AI root through the persistent boundary at plugin startup, and validation plus GovernanceBootstrapService share that resolved workspace coordinate. The regression runs default registration from a temporary runtime cwd and proves governance-registry.json is written only under the persistent workspace.

Why: Current-head cloud review found two remaining default-root paths that still pointed at the disposable runtime checkout. Resolve the Skills main-rule root and project-setup registry root through the persistent boundary at plugin startup so reads and governance bootstrap share the workspace coordinate.\n\nTests: API build plus Skills GET/default-mount and Projects Setup registry-location regressions.\n\n[小太阳·砚砚/GPT-5.6 Sol🐾]
@zts212653

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 02f7fce680

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@zts212653 zts212653 merged commit fae0877 into main Jul 14, 2026
5 checks passed
@zts212653 zts212653 deleted the fix/1141-runtime-write-protection branch July 14, 2026 07:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: 纯代码方式运行项目后(没有在后台执行命令,均在页面操作),停止服务再启动失败

1 participant