Skip to content

refactor: use filesystem scope architecture - #397

Merged
ytallo merged 10 commits into
mainfrom
feat/filesystem-scope-architecture
Jul 3, 2026
Merged

ytallo merged 10 commits into
mainfrom
feat/filesystem-scope-architecture

Conversation

@ytallo

@ytallo ytallo commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This is the review target for the filesystem access architecture.

  • Replace the folder/workspace grant API with a trusted filesystem-scope contract across shell, harness, approval-gate, and console.
  • Have harness stamp fs_scope { root, grants } and expose harness::filesystem::* control-plane functions.
  • Rename approval-gate and console surfaces to filesystem access, including access_duration and filesystem_access_request records.
  • Deep-link the filesystem access dialog directly to shell/fs/host_roots in the configuration UI.
  • Remove legacy migration/support paths for the replaced folder-access contract.

PR Organization

Validation

Local verification:

  • cargo test --manifest-path shell/Cargo.toml
  • cargo test --manifest-path harness/Cargo.toml
  • cargo test --manifest-path approval-gate/Cargo.toml
  • pnpm --dir console/web typecheck
  • pnpm --dir console/web test
  • cargo clippy --manifest-path shell/Cargo.toml --all-targets --all-features -- -D warnings
  • cargo test --manifest-path shell/Cargo.toml --test code_golden_errors
  • cargo test --manifest-path shell/Cargo.toml --lib from_json
  • cargo test --manifest-path shell/Cargo.toml --lib removed_host_root
  • cargo test --manifest-path shell/Cargo.toml --lib seed_default_matches_shipped_config_yaml
  • npm run --silent build from shell/tests/e2e/workers/harness
  • git diff --check

Latest follow-ups pushed:

  • 4e7866ec fixes shell clippy on config helpers.
  • 94c7ffcf normalizes filesystem access error goldens across platforms.
  • 7b61f764 updates the shell E2E configs/scripts to use fs.host_roots, quotes the false executable in YAML, isolates generated configuration state, and removes stale public host_root terminology.
  • a7656bdc re-owns E2E sandbox::fs::* mocks per mock case so the wire-shape tests stay isolated while real iii-sandbox remains available for exec sandbox coverage.

@vercel

vercel Bot commented Jul 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview, Comment Jul 3, 2026 4:45pm
workers-tech-spec Ready Ready Preview, Comment Jul 3, 2026 4:45pm

Request Review

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 31 skipped (no docs/).

Layer Result
structure ✓
vale ✓
ai ✓
render ✓

Four for four. Nicely done.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@ytallo, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ebdbab04-f627-432b-a789-4f7def08f843

📥 Commits

Reviewing files that changed from the base of the PR and between a77a478 and 4355574.

⛔ Files ignored due to path filters (1)
  • approval-gate/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (122)
  • approval-gate/README.md
  • approval-gate/src/config.rs
  • approval-gate/src/configuration.rs
  • approval-gate/src/events.rs
  • approval-gate/src/filesystem_access.rs
  • approval-gate/src/filesystem_access_state.rs
  • approval-gate/src/functions/filesystem_access_watch.rs
  • approval-gate/src/functions/gate.rs
  • approval-gate/src/functions/get_pending.rs
  • approval-gate/src/functions/list_pending.rs
  • approval-gate/src/functions/mod.rs
  • approval-gate/src/functions/on_session_deleted.rs
  • approval-gate/src/functions/on_turn_completed.rs
  • approval-gate/src/functions/remove_always_allow.rs
  • approval-gate/src/functions/resolve.rs
  • approval-gate/src/harness.rs
  • approval-gate/src/lib.rs
  • approval-gate/src/main.rs
  • approval-gate/src/pending.rs
  • approval-gate/src/settings.rs
  • approval-gate/src/shell_config.rs
  • approval-gate/src/types.rs
  • approval-gate/tests/golden/schemas/approval.filesystem-access-watch.json
  • approval-gate/tests/golden/schemas/approval.gate.json
  • approval-gate/tests/golden/schemas/approval.get-pending.json
  • approval-gate/tests/golden/schemas/approval.list-pending.json
  • approval-gate/tests/golden/schemas/approval.pending-created.json
  • approval-gate/tests/golden/schemas/approval.resolve.json
  • approval-gate/tests/schemas.rs
  • console/web/src/components/chat/ChatView.tsx
  • console/web/src/components/chat/Composer.tsx
  • console/web/src/components/chat/DirectoryPicker.tsx
  • console/web/src/components/chat/FunctionCallGroup.tsx
  • console/web/src/components/chat/FunctionCallMessage.tsx
  • console/web/src/components/chat/Message.tsx
  • console/web/src/components/chat/MessageList.tsx
  • console/web/src/components/chat/engine/parsers.ts
  • console/web/src/components/permissions/FilesystemAccessDialog.tsx
  • console/web/src/components/permissions/FilesystemAccessPrompt.tsx
  • console/web/src/hooks/use-conversations.ts
  • console/web/src/hooks/use-filesystem-grants.ts
  • console/web/src/lib/backend/filesystem-grants.ts
  • console/web/src/lib/backend/real-metadata.test.ts
  • console/web/src/lib/backend/real.ts
  • console/web/src/lib/backend/shell-roots.ts
  • console/web/src/lib/backend/translate.test.ts
  • console/web/src/lib/backend/translate.ts
  • console/web/src/lib/backend/types.ts
  • console/web/src/lib/sessions/entry-mapper.test.ts
  • console/web/src/lib/sessions/entry-mapper.ts
  • console/web/src/pages/Configuration/tabs/ConsoleSettingsTab.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/WorkerEditor.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/ArrayField.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/DictionaryField.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/EnumField.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/FieldShell.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/NullableField.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/ObjectSection.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/OneOfField.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/StringField.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/TemplatableField.tsx
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/path.test.ts
  • console/web/src/pages/Configuration/tabs/WorkersTab/schema-form/path.ts
  • console/web/src/types/chat.ts
  • console/web/src/types/iii-agent-event.ts
  • harness/src/clients/engine.rs
  • harness/src/deferred.rs
  • harness/src/filesystem_grants.rs
  • harness/src/filesystem_scope.rs
  • harness/src/functions/filesystem.rs
  • harness/src/functions/function_resolve.rs
  • harness/src/functions/function_trigger.rs
  • harness/src/functions/mod.rs
  • harness/src/functions/on_session_deleted.rs
  • harness/src/functions/send.rs
  • harness/src/hooks/runner.rs
  • harness/src/lib.rs
  • harness/src/state.rs
  • harness/src/subagent.rs
  • harness/src/trigger.rs
  • harness/src/turn_loop.rs
  • harness/src/types/turn.rs
  • harness/src/workspace_inject.rs
  • harness/tests/golden/schemas/harness.function.resolve.json
  • shell/CHANGELOG.md
  • shell/src/code/error.rs
  • shell/src/code/functions/create_file.rs
  • shell/src/code/functions/delete_file.rs
  • shell/src/code/functions/list_folder.rs
  • shell/src/code/functions/mod.rs
  • shell/src/code/functions/move_file.rs
  • shell/src/code/functions/read_file.rs
  • shell/src/code/functions/search.rs
  • shell/src/code/functions/tree.rs
  • shell/src/code/functions/update_file.rs
  • shell/src/code/mod.rs
  • shell/src/code/path.rs
  • shell/src/code/state.rs
  • shell/src/configuration.rs
  • shell/src/exec/host.rs
  • shell/src/exec/mod.rs
  • shell/src/exec/policy.rs
  • shell/src/filesystem_access.rs
  • shell/src/fs/host.rs
  • shell/src/fs/mod.rs
  • shell/src/fs/sandbox.rs
  • shell/src/functions/exec.rs
  • shell/src/functions/exec_bg.rs
  • shell/src/functions/types.rs
  • shell/src/lib.rs
  • shell/src/main.rs
  • shell/src/scode.rs
  • shell/tests/code_golden_errors.rs
  • shell/tests/code_path_jail.rs
  • shell/tests/code_unified_protection.rs
  • shell/tests/code_update_ops.rs
  • shell/tests/e2e/run-tests.sh
  • shell/tests/e2e/workers/harness/src/cases-fs-sandbox.ts
  • shell/tests/features/coder/path_security.feature
  • shell/tests/golden/errors.json
  • shell/tests/host_fs_branches.rs
  • shell/tests/sandbox_dispatch.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/filesystem-scope-architecture

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

ytallo added 10 commits July 3, 2026 13:36
When the agent hits a folder outside the session's allowed roots, the
approval-gate now parks the call as a folder_access pending record. The
chat renders a dedicated prompt card for it — allow once / allow this
session / always allow (confirmed, writes shell fs.host_roots) / deny —
in place of the standard approval row.

- FolderAccessPrompt: inline prompt with scope buttons and destructive
  confirm for permanent grants; remounts per grant request so a
  re-parked call never inherits stale submit state
- FolderAccessDialog: per-session management surface (workspace,
  session grants with revoke, permanent roots read-only with deep-link
  to the shell configuration editor)
- use-folder-grants: harness::workspace::grants/revoke adapter with
  optimistic updates and response-ordering guards; hidden when the
  harness predates workspace grants
- translate/entry-mapper/ChatView carry the folderAccess payload
  through dedupe and snapshot re-derivation; approval::resolve gains
  grant_scope only for folder_access resolutions (old gates unaffected)
- discoverability copy in DirectoryPicker and console settings
…ution

A new approval::grant-watch hook on harness::hook::post-trigger
(shell::*/coder::*, fail_open) watches dispatch failures for the shell's
jail-scope grant_hint tail (S215/S220/C215/C218) and converts the raw
rejection into a held folder_access pending approval, so the console can
ask "allow access to <dir>?" instead of surfacing a jail error to the
model. The ladder fails toward delivery at every rung: workspace dirs
never prompt (stamp holes log loudly), already-granted and user-denied
dirs are suppressed, and a per-call re-ask cap (grant_reask_limit,
default 3) prevents ping-pong.

approval::resolve gains grant_scope for folder_access allows:
- once: releases the held call with one-shot extra_roots
- session: harness::workspace::grant first (falls back to extra_roots
  if the RPC fails), then release
- always: session grant plus best-effort persistence of the dir into
  the shell configuration's fs.host_roots
- deny: records the dir in per-session denied memory before delivering

New modules: grant.rs (pure hint parsing + workspace guard),
grant_state.rs (denied memory, attempt caps), shell_config.rs
(fs.host_roots append), functions/grant_watch.rs (orchestration).
Session/turn purge handlers clean up the new state; record/request
fields are wire-compatible with older consoles and stored records.
@ytallo
ytallo force-pushed the feat/filesystem-scope-architecture branch from a7656bd to 4355574 Compare July 3, 2026 16:45
@ytallo
ytallo merged commit ec368c4 into main Jul 3, 2026
44 checks passed
andersonleal added a commit that referenced this pull request Jul 3, 2026
Main's fs-scope refactor (#397) introduced the bare DispatchError struct;
the react-bridge reconcile pass logs it with %e, which needs Display —
an error type should carry one anyway.
andersonleal added a commit that referenced this pull request Jul 3, 2026
…-in, wire hardening, and spawn console view (#401)

* feat(harness): reactive trigger bridge (harness::react) with join fan-in and lifecycle hardening

Ports the engine's trigger/notify primitives into a harness-native reactive
sub-agent bridge and hardens the full registration/fire/teardown lifecycle
against gaps found in live testing.

- harness::react: sub-agent spec fires on engine triggers (turn events,
  state, cron, stream); join fan-in with an expect array, fire-once
  accumulator, and rearm for standing watchers.
- Interceptor pass-through (subscribe.rs): agent-issued
  engine::register_trigger calls get owner + subscription id stamped
  server-side into the react metadata, closing several trust gaps in the
  raw registration path.
- Idempotent registration (dedup by canonical request key) and a durable
  owner sweep on session::deleted, replacing two pipelines that could
  double-register the same reaction.
- Startup reconcile: GC react bindings whose owner session is gone and
  notify bindings unknown to the local registry; never GC on doubt.
- Loop breakers: self-edge drop, reactive-depth cap, per-subscription
  fire-rate limit.
- Join results deliver into the registering (owner) session by default
  instead of a detached, unread child session; parent nesting falls back
  from the event's session through the owner stamp to resolve_root.
- Registration advisories for turn-event filters naming a nonexistent
  session, and for a join key wired to the same event source as a
  sibling key.
- Policy aid: narrowed sub-agents are told their allowed/denied function
  surface directly in the system prompt instead of discovering it via a
  denied functions::list call.
- Heal dangling function_calls left by interrupted/compacted turns
  before the next generate step.
- Docs: tech spec, skill, and all prompt variants updated for the
  react/join doctrine.

* feat(console): dedicated chat view for harness::spawn

Replace the raw-JSON fallback card with an instrument-panel view:
policy chips (model/mode/turns/thinking/output/allow/deny), the task
rendered as markdown, and the child's result as markdown, highlighted
JSON, or the direct-call child ids. Guard errors and failed children
route through the existing SandboxErrorView; the approval gate gets a
policy-first preview. Session ids link to the child conversation via
the sidebar's select when the console knows the session.

Includes Zod parsers for the spawn wire schema (excerpt-tolerant),
fixtures for all six card states, a gated-spawn playground scenario,
and parser tests locking envelope unwrapping and error-before-success
dispatch.

* fix(harness): seed react-bridge TurnRecord fields in subagent test

Upstream #388 added a test TurnRecord literal that predates this branch's
display_parent_session_id / spawned_by_subscription_id / reactive_depth
fields; the rebase merged clean but test compilation broke.

* feat(harness): prompt doctrine — name every spawned child session

Every harness::spawn must pass session_id: a short readable job slug plus
a few random characters (fetch-headlines-b4k9), replacing the opaque
engine-minted UUIDs in the console tree. Never the parent session id as a
prefix; the random suffix carries the run-uniqueness guarantee instead
(a reused id silently resumes the old session). Scoped to direct spawn
calls only — in a react trigger's metadata a fixed session_id funnels
every firing into one session and re-aims join delivery. Fan-in doctrine
updated to the same naming across all five prompt variants.

* fix(providers): keep displaced tool results adjacent to their call

A notification or steering user entry injected while a call window is open
(a parked harness::spawn holds one open for minutes) lands between
function_call and function_result in the durable transcript. Every wire
mapper only repaired MISSING results (orphan placeholder) — a DISPLACED
result survived to the wire as assistant(tool_use) / user(text) /
user(tool_result), which Anthropic 400s ('tool_use ids were found without
tool_result blocks immediately after') and OpenAI/xAI/Responses reject as
a user row between tool_calls and its tool rows. The durable transcript
replays the shape on every retry, permanently wedging the turn.

Fix: shared llm_router::types::messages::reorder_displaced_results runs
first in all four providers' to_wire_messages — each FunctionResult moves
directly after the assistant that emitted its call, order preserved,
orphan results untouched. Wedged sessions self-heal: the transcript
itself was never illegal, only the wire projection.

Repro test written first and failed with the exact live shape; regression
tests in all four providers plus unit tests on the shared helper.

* fix(harness): rotate mid-generation user arrivals past the interrupted reply

A user entry appended while a step is generating (or assembling — the
compaction/hook window) lands before that step's assistant entry in the
durable log. The steering check then re-generates, but the assembled
context ENDS with a call-less assistant message — a prefill request newer
Anthropic models reject ('This model does not support assistant message
prefill. The conversation must end with a user message.'), wedging the
turn on every retry. Older models silently accepted prefill, hiding this
path.

Fix: rotate_mid_generation_users presents arrivals after the previous
step's watermark AFTER the reply they interrupted — semantically exact,
the model answered without seeing them. Two invariants hardened by
adversarial review:
- the new watermark is assigned only after router.chat returns; the
  pre-generate put_turn persists the OLD one, so a redelivered step keeps
  its rotation window instead of re-issuing the rejected shape forever
- rotation runs on the FINAL assembled values, never on the candidate:
  compaction persists tail_start_entry_id as a log-order cursor indexed
  from the candidate, and rotating first would silently drop the rotated
  message from every future window

Also: has_user_after_watermark now loads include_custom=true, matching
the list the watermark comes from (a watermark landing on a custom entry
silently disabled the steering check).

* style: cargo fmt (harness, provider-openai, provider-xai)

* fix(harness): Display for DispatchError + fmt (rebase fallout)

Main's fs-scope refactor (#397) introduced the bare DispatchError struct;
the react-bridge reconcile pass logs it with %e, which needs Display —
an error type should carry one anyway.

* docs(harness): revert harness.md spec changes

Restore tech-specs/2026-06-agentic/harness.md to main's version — the
react-bridge spec additions come out of this PR.

* fix(harness): address CodeRabbit review on #401

- react: include the join (id, key) in the fallback fire-gate hash —
  state-based join predecessors share the whole downstream spec except
  their key, so a wide join shared one 10-fires/min budget and tripped
  the breaker spuriously
- react: retry the join accumulator delete (3 attempts) — a failed
  delete left fire=1 behind, permanently wedging a rearmed join's
  fire-once guard; persistent failure on a rearmed join now logs at
  error level with the recovery path
- spawn: strip spawned_by_subscription_id / reactive_depth on the
  model-reachable dispatch path — react-internal bookkeeping a model
  could spoof to defeat the self-edge breaker and depth cap
- skills: align SKILL.md fan-in naming with the prompt doctrine (slug +
  random suffix, never the originating session id as a prefix)
- tests: multi-owner displaced-result reorder case; round-trip the
  react-bridge TurnRecord fields with real values

This branch was successfully deployed

2 active deployments
Preview – workers-tech-spec — 43555746 Deployed Jul 3, 2026 by vercel[bot]
Preview – workers — 43555746 Deployed Jul 3, 2026 by vercel[bot]
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.

1 participant