Skip to content

feat(storage): joined filesystem, in-store authorization, task-file identity, and rocketlib.getTask - #1686

Merged
Rod-Christensen merged 9 commits into
developfrom
feat/deploy-1
Jul 30, 2026
Merged

feat(storage): joined filesystem, in-store authorization, task-file identity, and rocketlib.getTask#1686
Rod-Christensen merged 9 commits into
developfrom
feat/deploy-1

Conversation

@Rod-Christensen

@Rod-Christensen Rod-Christensen commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Storage/identity foundation for team-scoped deployments: deployments will belong to teams, so storage, permissions, and subprocess identity all had to stop assuming "everything belongs to the calling user". Deployment records and team run logs build on this next.

What & why

In-store authorization — file_store.py is the security boundary. Storage I/O happens from many places (rrext_store, rrext_log, fetch, the run-log writer, deployments). Checking permissions in command handlers meant every new caller could forget one. Now every path of every operation authorizes inside the store, at the single point where paths resolve, against the RequestContext bound at construction. Command handlers do zero storage permission checks.

RequestContext identity (copied verbatim from feat/alb, with MERGE NOTEs) so that merge becomes a dedupe instead of a rework. The internal()/engine() factories make the entire trusted surface greppable.

Store singleton + RR_STORE_URL (legacy STORE_URL hard-fails — a silent fallback would never get migrated). Providers restructured into packages; write locks and open handles moved to the shared Store so per-connection FileStore instances still exclude each other on one physical path.

Joined filesystem, grammar v3. @ is a virtual root listing User/, Team/, Org/ so a file browser walks user, team, and org storage as one tree with ordinary path joining — while plain paths stay "my files" for nodes and SDK code. Bare segments are display names resolved only through the caller's own session membership (a foreign org's names are inexpressible by construction) and round-trip byte-exact. =<id> is the only cross-boundary spelling (sys.admin support access). Unknown / foreign / permission-less / ambiguous references all deny with one identical message (no existence leak). =-prefixed names are uncreatable at scope tops so id references can never be shadowed. Case-exact, alias-free; normalization runs before parsing (the \@\Team\... bypass family is fuzz-tested).

System-owned trees. .logs and .deployments are engine-written and served by their domain APIs (rrext_log today, rrext_deploy next). Through the file API they are invisible in listings and denied on access for every session identity except sys.admin. Domain APIs act through the internal identity and keep their own task.monitor/task.control gates.

Three live cross-team permission holes closed: on_execute accepted an arbitrary teamId and merged that team's secrets into the task env before any membership check (secret exfiltration); on_restart restarted token-addressed tasks with only a defaultTeam check; the cprofile proxy could drive another team's engine subprocess. All three now resolve permissions against the addressed team/task first, with uniform denials.

Task-file identity + storage anchor. Subprocess identity rides the 0600 task file (identity block) — never the environment (ROCKETRIDE_CLIENT_ID is gone). The new storage block gives nodes chroot semantics: dev runs anchor at the owner's tree (unchanged behavior), deploy runs at teams/<teamId>/files/tasks/<projectId> — node-level paths are identical in both modes, a deployed task needs no user, and concurrent deployments cannot fight over working storage. The engine context rejects every @ spelling and system tree regardless of anchor.

rocketlib.getTask() C++ binding. The engine already parses the task file, so Python re-reading argv would duplicate parsing and couple to the launch contract. ITask::execute() publishes the task JSON around the begin/end window (before beginTask, because endpoint and node-global construction happens inside it) and clears it on every exit path. The store layer resolves identity + anchor itself (Store.engine_file_store()), so tool_filesystem carries zero jobConfig plumbing.

Also: explorer-ui roots its listing at @ to present the joined tree.

Testing

  • 1572 server tests green (suite grew ~90 tests: authorization matrix, grammar fuzz, system-tree policy, anchor chroot, cprofile proxy).
  • Engine rebuilt on both dists; rocketlib.getTask verified live (returns None when idle).

Pairs with the saas feat/deploy-1 PR (alembic migration foundation + submodule pointer). Merge this one first, then re-stamp the saas pointer.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Task-aware filesystem access now uses per-task storage anchors, including virtual scope mounts for browsing.
    • Added an engine API to retrieve the currently running task’s trusted metadata.
  • Bug Fixes
    • Strengthened authorization across task and filesystem operations, including cross-team control/proxy profiling and log/fetch/store flows.
    • Blocked destructive actions at scope roots; tightened system-tree and path handling.
    • Improved file-handle ownership/cleanup and shared lock coordination; Explorer refresh and drag/upload now follow the correct storage root.
  • Documentation
    • Updated filesystem tool docs and service descriptions to match anchor-based paths and default whitelist behavior.
  • Tests
    • Added/updated coverage for task-aware authorization, scope grammar, and engine integration.

@github-actions github-actions Bot added docs Documentation module:server C++ engine and server components module:nodes Python pipeline nodes module:ai AI/ML modules labels Jul 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor
🤖 Internal: Discord sync marker

Auto-managed by the Discord notification workflow. Stores the linked Discord message ID and forum thread ID. Do not edit or delete.

@Rod-Christensen

Copy link
Copy Markdown
Collaborator Author

Paired saas PR: rocketride-ai/rocketride-saas#395 (alembic Phase 0 + submodule pointer). Merge this PR first, then re-stamp the saas pointer to the squash-merged develop commit.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR replaces environment-based filesystem identity with engine-published task metadata, adds context-bound storage authorization and shared handle management, applies team-scoped task permissions, updates filesystem tooling and Explorer integration, and expands related tests.

Changes

Task identity and storage authorization

Layer / File(s) Summary
Task identity publication and storage anchors
packages/server/engine-lib/..., packages/ai/src/ai/modules/task/task_engine.py, nodes/src/nodes/tool_filesystem/*
The engine publishes task JSON through rocketlib.getTask(). Task identity and storage roots are embedded in task configuration, and filesystem initialization uses the engine file store.
Context-bound FileStore and Store lifecycle
packages/ai/src/ai/account/*
RequestContext carries caller identity and connection state. Store becomes process-wide, creates identity-bound stores, shares handle registries, and FileStore authorizes scoped paths and protects scope roots.
Task command and filesystem integration
packages/ai/src/ai/modules/task/*, apps/explorer-ui/src/ExplorerSidebar.tsx
Task operations use team- or task-scoped permissions, filesystem handlers use context-owned handles and virtual mounts, fetch and logs use the new store APIs, and Explorer refreshes from @.
Authorization and storage regression coverage
packages/ai/tests/*
Tests cover scope parsing, identity restrictions, shared locks, handle cleanup, team permissions, task storage roots, command authorization, and updated FileStore construction.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TaskEngine
  participant EngineBinding
  participant FilesystemNode
  participant Store
  participant FileStore
  TaskEngine->>EngineBinding: publish TASK FILE JSON
  EngineBinding->>FilesystemNode: expose rocketlib.getTask()
  FilesystemNode->>Store: request engine_file_store()
  Store->>FileStore: create anchored context-bound store
  FileStore->>FileStore: authorize and resolve path
Loading

Possibly related PRs

Suggested labels: module:ai, module:nodes, module:server, module:ui

Suggested reviewers: jmaionchi, stepmikhaylov

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 74.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main storage, authorization, task identity, and getTask changes in the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/deploy-1

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/ai/tests/ai/account/test_store.py (1)

563-586: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use monkeypatch for RR_STORE_URL instead of mutating os.environ directly.

test_create_with_default_url pops the var and never restores it, and test_create_with_env_var only cleans up on the success path — a failed assertion at Lines 581-583 leaves RR_STORE_URL set for every later test in the session (Store.create() reads it). monkeypatch.delenv/setenv restores automatically.

♻️ Proposed fix
-    def test_create_with_default_url(self):
+    def test_create_with_default_url(self, monkeypatch):
         """Test creating store with default URL."""
         # Clear environment
-        os.environ.pop('RR_STORE_URL', None)
+        monkeypatch.delenv('RR_STORE_URL', raising=False)
@@
-    def test_create_with_env_var(self, tmp_path):
-        """Test creating store from STORE_URL environment variable."""
-        os.environ['RR_STORE_URL'] = f'filesystem://{tmp_path}'
+    def test_create_with_env_var(self, tmp_path, monkeypatch):
+        """Test creating store from RR_STORE_URL environment variable."""
+        monkeypatch.setenv('RR_STORE_URL', f'filesystem://{tmp_path}')
@@
         assert store._store._root_path == tmp_path
-
-        # Cleanup
-        os.environ.pop('RR_STORE_URL', None)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/tests/ai/account/test_store.py` around lines 563 - 586, Update
test_create_with_default_url and test_create_with_env_var to accept pytest’s
monkeypatch fixture and use monkeypatch.delenv/setenv for RR_STORE_URL instead
of directly mutating os.environ. Remove the manual cleanup so the environment is
restored automatically even when assertions fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/explorer-ui/src/ExplorerSidebar.tsx`:
- Line 117: Update handleMove and handleUpload so an empty targetDir or
dragsTargetDir uses the “@” mount before appending file.name, matching the root
prefix returned by listRecursive(client, '@'). Preserve existing path behavior
for non-root directories and ensure root writes target “@/…” rather than the
bare filename.

In `@nodes/src/nodes/tool_filesystem/IGlobal.py`:
- Around line 27-29: The task-storage documentation still claims all operations
use a universal user-root path. Update
nodes/src/nodes/tool_filesystem/IGlobal.py lines 27-29 to document the
engine-provided task storage anchor and both development and deployed roots;
update nodes/src/nodes/tool_filesystem/README.md lines 8-17 to make the
whitelist and storage-location sections anchor-relative; and update the
filesystem.pathWhitelist source description in
nodes/src/nodes/tool_filesystem/services.json line 12 so generated documentation
reflects anchor-relative paths, without editing the generated README block.

In `@packages/ai/src/ai/account/file_store.py`:
- Around line 18-23: Update the usage examples in the module and the class
docstring around Store.file_store to call it with ctx first and the client
identifier via client_id. Remove the obsolete connection_id argument from the
write and read examples, while preserving the existing file paths and
operations.
- Around line 840-844: Update the scope-root guard in the rename flow to reject
empty or normalized-root old_path and new_path regardless of whether _resolve
returns kind 'own'. Preserve valid non-root renames while preventing rename('')
from enumerating, copying, and deleting the entire account.
- Around line 929-941: Update _get_handle to fail closed when the caller’s
conn_id is empty: reject the handle before returning it, rather than
conditionally skipping the ownership check. Preserve the existing rejection for
mismatched non-empty connection IDs and allow access only when both the caller
and handle have the same valid connection ID.

In `@packages/ai/src/ai/account/store.py`:
- Around line 691-724: Extract the per-handle teardown currently duplicated in
Store.close_all_handles and FileStore._force_close_handle/_release_handle into a
shared Store helper, preserving the existing close/commit, error logging,
closed-state, registry removal, and write-lock release sequence. Update both
Store.close_all_handles and FileStore’s force-close path to delegate to that
helper, keeping behavior consistent across callers.
- Around line 676-687: Update the client_id resolution around is_session so
non-internal authenticated sessions are handled as sessions even when
AccountInfo.userId is empty. Reject such sessions before accepting any explicit
client_id, and only allow a client_id that matches the authenticated userId;
otherwise derive it from that identity. Preserve the existing explicit-client
requirement for internal/engine contexts.

In `@packages/ai/src/ai/modules/task/commands/cmd_debug.py`:
- Around line 228-231: The org_id fallback in the task handlers must not default
to an empty string for authorized callers without caller-side team membership.
In packages/ai/src/ai/modules/task/commands/cmd_debug.py lines 228-231, update
the launch resolution to obtain the target team’s real organization or reject
the request, and correct the comment to cover all applicable bypasses; apply the
same resolution in packages/ai/src/ai/modules/task/commands/cmd_task.py lines
144-151 so on_execute and on_launch consistently anchor tasks to the target
organization.

In `@packages/ai/src/ai/modules/task/task_engine.py`:
- Around line 477-483: Update _storage_root to validate the derived storage
anchor before returning it, using the existing validate_storage_root helper for
both deploy anchors containing team_id and non-deploy anchors containing
client_id. Ensure invalid project_id or team_id values, including path traversal
or disallowed leading characters, raise a clear launch-time error while
preserving the current deploy team_id requirement and valid anchor formats.

In `@packages/ai/src/ai/modules/task/task_server.py`:
- Around line 662-672: Remove the outer sys.admin permission check in the task
authorization block and always call resolve_task_permissions when account_info
is present and require is set. Preserve the existing no-permission and
missing-required-permission errors, relying on resolve_task_permissions to
provide the admin/internal bypass.

In `@packages/ai/tests/ai/account/test_file_store.py`:
- Around line 639-648: Remove the first, unused unowned FileStore construction
in test_handle_cap_not_enforced_for_unowned_context, including its
unauthenticated RequestContext argument. Keep the later internal context setup
with an empty conn_id and the resulting FileStore construction as the sole test
fixture.

In `@packages/ai/tests/ai/modules/task/commands/test_cmd_task.py`:
- Around line 129-139: Update
test_on_execute_checks_permission_on_the_target_team to patch or mock
account.get_merged_env, matching the sibling permission test, so execution stops
before the real secret-merge backend lookup while preserving the assertion that
team-target is checked with task.control.

In `@packages/ai/tests/ai/modules/task/test_log_stream.py`:
- Around line 154-161: The inline FileStore construction is duplicated across
the run-log tests instead of using one shared helper. In
packages/ai/tests/ai/modules/task/test_run_log.py#L420-L427, define the helper
beside open_writer, ensuring each call creates its own Store wrapper, and
replace the listed constructions there; in
packages/ai/tests/ai/modules/task/test_log_stream.py#L154-L161 and its four
sibling call sites, use that helper; in
packages/ai/tests/ai/modules/task/test_run_log_reader.py#L93-L115 and its three
sibling call sites, import and use the helper.

In `@packages/server/engine-lib/engLib/task/headers/task.hpp`:
- Around line 121-130: Update the public API contract for task::currentTask() to
explicitly state that it is main-task-thread-only and unsafe for concurrent
worker-thread or Python-binding access, unless implementing a thread-safe
publication mechanism for g_currentTask. Ensure the documentation and
implementation consistently enforce the chosen access model, including
rocketlib.getTask() callers.

---

Outside diff comments:
In `@packages/ai/tests/ai/account/test_store.py`:
- Around line 563-586: Update test_create_with_default_url and
test_create_with_env_var to accept pytest’s monkeypatch fixture and use
monkeypatch.delenv/setenv for RR_STORE_URL instead of directly mutating
os.environ. Remove the manual cleanup so the environment is restored
automatically even when assertions fail.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 60da8b6b-cb15-4182-aed0-c549d2844850

📥 Commits

Reviewing files that changed from the base of the PR and between f773adf and bfb3866.

📒 Files selected for processing (39)
  • apps/explorer-ui/src/ExplorerSidebar.tsx
  • nodes/src/nodes/tool_filesystem/IGlobal.py
  • nodes/src/nodes/tool_filesystem/README.md
  • nodes/src/nodes/tool_filesystem/services.json
  • packages/ai/src/ai/account/__init__.py
  • packages/ai/src/ai/account/file_store.py
  • packages/ai/src/ai/account/models.py
  • packages/ai/src/ai/account/store.py
  • packages/ai/src/ai/account/store_providers/filesystem/__init__.py
  • packages/ai/src/ai/account/store_providers/filesystem/filesystem.py
  • packages/ai/src/ai/account/store_providers/memory/__init__.py
  • packages/ai/src/ai/account/store_providers/memory/memory.py
  • packages/ai/src/ai/modules/task/commands/cmd_cprofile.py
  • packages/ai/src/ai/modules/task/commands/cmd_debug.py
  • packages/ai/src/ai/modules/task/commands/cmd_log.py
  • packages/ai/src/ai/modules/task/commands/cmd_store.py
  • packages/ai/src/ai/modules/task/commands/cmd_task.py
  • packages/ai/src/ai/modules/task/fetch.py
  • packages/ai/src/ai/modules/task/task_conn.py
  • packages/ai/src/ai/modules/task/task_engine.py
  • packages/ai/src/ai/modules/task/task_server.py
  • packages/ai/tests/ai/account/test_file_store.py
  • packages/ai/tests/ai/account/test_store.py
  • packages/ai/tests/ai/account/test_store_auth.py
  • packages/ai/tests/ai/modules/task/commands/test_cmd_cprofile.py
  • packages/ai/tests/ai/modules/task/commands/test_cmd_debug.py
  • packages/ai/tests/ai/modules/task/commands/test_cmd_task.py
  • packages/ai/tests/ai/modules/task/test_log_stream.py
  • packages/ai/tests/ai/modules/task/test_run_log.py
  • packages/ai/tests/ai/modules/task/test_run_log_reader.py
  • packages/ai/tests/ai/modules/task/test_task_conn.py
  • packages/ai/tests/ai/modules/task/test_task_engine.py
  • packages/ai/tests/ai/modules/task/test_task_server.py
  • packages/server/engine-lib/engLib/store/python/bindings.cpp
  • packages/server/engine-lib/engLib/task/core/task.cpp
  • packages/server/engine-lib/engLib/task/headers/task.hpp
  • packages/server/engine-lib/rocketlib-python/lib/rocketlib/__init__.py
  • packages/server/engine-lib/rocketlib-python/lib/rocketlib/__init__.pyi
  • packages/server/engine-lib/rocketlib-python/lib/rocketlib/engine.py

Comment thread apps/explorer-ui/src/ExplorerSidebar.tsx
Comment thread nodes/src/nodes/tool_filesystem/IGlobal.py Outdated
Comment thread packages/ai/src/ai/account/file_store.py Outdated
Comment thread packages/ai/src/ai/account/file_store.py
Comment thread packages/ai/src/ai/account/file_store.py
Comment thread packages/ai/src/ai/modules/task/task_server.py Outdated
Comment thread packages/ai/tests/ai/account/test_file_store.py
Comment thread packages/ai/tests/ai/modules/task/commands/test_cmd_task.py Outdated
Comment thread packages/ai/tests/ai/modules/task/test_log_stream.py
Comment thread packages/server/engine-lib/engLib/task/headers/task.hpp
Rod-Christensen added a commit that referenced this pull request Jul 29, 2026
… universal root guards, real org resolution

Addresses the 14 CodeRabbit review threads:

Security/correctness:
- file_store: _get_handle requires an EXACT conn_id match. The handle
  registry became Store-wide in this PR, so the lenient empty-conn_id
  check would have let any default RequestContext reach another
  connection's live handle. Fail closed.
- file_store: the rename/delete root guards no longer exempt the caller's
  own namespace — rename('') resolved to the account root and turned the
  copy+delete loop into a whole-account move. delete/rename/rmdir now
  reject every root uniformly.
- store: a session-shaped ctx (account present, not internal/engine) with
  an empty userId can no longer anchor a FileStore via explicit client_id;
  that combination (e.g. a task-scoped AccountInfo built from an empty
  control.userId) would unlock an arbitrary user's namespace. Account-LESS
  contexts stay constructible because resolve_scope already denies their
  every operation with 'Not authenticated'.
- task_conn/cmd_task/cmd_debug: new resolve_org_for_team() — members
  resolve via their own membership; callers that pass the team check
  without membership (sys.admin, internal) resolve the target team's REAL
  org through the account backend, and an unresolvable team is denied with
  the uniform message. Previously such callers registered the task with
  orgId='' travelling in the task file as trusted identity.
- task_engine: _storage_root() validates through validate_storage_root at
  launch, so a malformed client-supplied project_id fails with a clear
  launch-time error instead of tool_filesystem disabling itself mid-run
  inside the subprocess.

Minor:
- explorer-ui: handleMove/handleUpload carry the '@' mount through an
  empty targetDir — the tree is rooted at '@', so bare names silently
  targeted the plain user store.
- tool_filesystem: IGlobal docstring, README handwritten sections, and the
  services.json pathWhitelist description describe the task storage anchor
  (dev = user tree, deploy = team task subtree) instead of the universal
  user-root claim. The generated README block regenerates on develop
  (nodes:docs-generate is branch-gated by design).
- file_store: stale usage examples fixed (Store.file_store(ctx), no
  connection_id params); handle-cap comment updated to string conn_ids.
- task.hpp/task.cpp: currentTask()'s main-task-thread-only access model is
  stated on the public API — publish, reads, and clear are strictly
  sequential on the single main task thread, so the value is
  unsynchronized by design.
- store/file_store: per-handle force-close teardown deduped into
  Store._teardown_handle(); FileStore._force_close_handle and
  close_all_handles delegate so the sequences cannot drift.
- task_server: dropped the outer sys.admin short-circuit duplicated inside
  resolve_task_permissions.

Tests: real resolve_org_for_team bound into the cmd_task/cmd_debug stubs,
get_merged_env patched where execution reaches the secret merge, dead
unowned FileStore construction removed, 16 inline FileStore(Store(istore),
...) constructions replaced with a shared make_file_store() helper, and
the sys.admin bypass test now asserts the resolver-side grant.

1572 passed / 122 skipped; ruff clean; explorer-ui rebuilt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ai/src/ai/modules/task/task_conn.py`:
- Around line 437-475: Update the membership fast path in resolve_org_for_team
so it validates the organization ID before returning it. When the matching team
has a missing or empty org.get('id', ''), raise the same PermissionError used by
the fallback path; return the ID only when non-empty, preserving the existing
backend resolution behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 62f74d1c-be8f-4b2f-985d-2a1c6eb3db8e

📥 Commits

Reviewing files that changed from the base of the PR and between bfb3866 and 44cd058.

📒 Files selected for processing (20)
  • apps/explorer-ui/src/ExplorerSidebar.tsx
  • nodes/src/nodes/tool_filesystem/IGlobal.py
  • nodes/src/nodes/tool_filesystem/README.md
  • nodes/src/nodes/tool_filesystem/services.json
  • packages/ai/src/ai/account/file_store.py
  • packages/ai/src/ai/account/store.py
  • packages/ai/src/ai/modules/task/commands/cmd_debug.py
  • packages/ai/src/ai/modules/task/commands/cmd_task.py
  • packages/ai/src/ai/modules/task/task_conn.py
  • packages/ai/src/ai/modules/task/task_engine.py
  • packages/ai/src/ai/modules/task/task_server.py
  • packages/ai/tests/ai/account/test_file_store.py
  • packages/ai/tests/ai/modules/task/commands/test_cmd_debug.py
  • packages/ai/tests/ai/modules/task/commands/test_cmd_task.py
  • packages/ai/tests/ai/modules/task/test_log_stream.py
  • packages/ai/tests/ai/modules/task/test_run_log.py
  • packages/ai/tests/ai/modules/task/test_run_log_reader.py
  • packages/ai/tests/ai/modules/task/test_task_server.py
  • packages/server/engine-lib/engLib/task/core/task.cpp
  • packages/server/engine-lib/engLib/task/headers/task.hpp
💤 Files with no reviewable changes (1)
  • packages/ai/tests/ai/account/test_file_store.py

Comment thread packages/ai/src/ai/modules/task/task_conn.py
Rod-Christensen and others added 3 commits July 28, 2026 21:28
…dentity, and rocketlib.getTask

Foundation for team-scoped deployments: deployments will belong to teams,
so storage, permissions, and subprocess identity all had to stop assuming
"everything belongs to the calling user". This lands that groundwork;
deployment records and team run logs build on it next.

WHY each piece exists:

* In-store authorization — file_store.py IS the security boundary.
  Storage I/O happens from many places (rrext_store, rrext_log, fetch,
  the run-log writer, deployments). Checking permissions in command
  handlers meant every new caller could forget one — the "forgot to
  check" bug class. Now every path of every operation authorizes INSIDE
  the store, at the single point where paths resolve, against the
  RequestContext bound at construction. Command handlers do zero storage
  permission checks.

* RequestContext identity (copied verbatim from feat/alb, with MERGE
  NOTEs). feat/alb generalizes per-request identity for every handler;
  adopting the identical shape now makes that merge a dedupe instead of
  a rework. The internal()/engine() factories make the entire trusted
  surface greppable: every privileged caller is a
  RequestContext.internal(...) call site.

* Store singleton + RR_STORE_URL (STORE_URL hard-fails). A server has
  exactly one store, so Store.instance()/Store.file_store(ctx) replaces
  the multi-step create()/get_file_store dance that invited divergent
  configuration. The legacy env name fails loudly instead of falling
  back, because a silent fallback would never get migrated. Providers
  became packages (store_providers/filesystem/, memory/). Write locks
  and open handles moved to the shared Store so per-connection FileStore
  instances still exclude each other on one physical path.

* Joined filesystem, grammar v3: '@' is a virtual root listing User/,
  Team/, Org/ so a file browser can walk user, team, and org storage as
  ONE tree with ordinary path joining — while plain paths stay "my
  files" for nodes and SDK code (simple mode). Bare segments are display
  NAMES that resolve ONLY through the caller's own session membership
  (a foreign org's names are inexpressible by construction) and
  round-trip byte-exact: the path you browse to is the path you open.
  =<id> is the only cross-boundary spelling (sys.admin support access).
  Unknown, foreign, permission-less, and ambiguous references all deny
  with ONE identical message so nothing leaks existence. Names starting
  with '=' are uncreatable at scope tops so id references can never be
  shadowed. Case-exact and alias-free: one location, one path string.
  Normalization runs BEFORE parsing, killing the \@\Team\... bypass
  family (fuzz-tested).

* System-owned trees: .logs and .deployments are engine-written and
  served by their domain APIs (rrext_log today, rrext_deploy next).
  Raw file access could corrupt them or expose data the user must not
  see, so through the file API they are invisible in listings and denied
  on access for every session identity except sys.admin (who may do
  anything). Domain APIs act through the internal identity and keep
  their own task.monitor/task.control gates at the command layer.

* Three live cross-team permission holes closed: on_execute accepted an
  arbitrary teamId and merged that team's secrets into the task env
  BEFORE any membership check (secret exfiltration); on_restart
  restarted token-addressed tasks with only a defaultTeam check; the
  cprofile proxy could drive another team's engine subprocess. All three
  now resolve permissions against the ADDRESSED team/task first
  (verify_team_permission / get_task), with uniform denials.

* Task-file identity + storage anchor: subprocess identity rides the
  0600 task file ('identity' block) — never the environment, whose
  ROCKETRIDE_* namespace is caller-influenced by design; the
  ROCKETRIDE_CLIENT_ID env var is gone. The new 'storage' block gives
  nodes chroot semantics: dev runs anchor at the owner's whole tree
  (unchanged behavior), deploy runs at teams/<teamId>/files/tasks/
  <projectId>, so node-level paths are IDENTICAL in both modes, a
  deployed task needs no user, and concurrent deployments cannot fight
  over working storage. The engine context rejects every '@' spelling
  and system tree regardless of anchor — LLM-generated paths cannot
  leave the sandbox.

* rocketlib.getTask() C++ binding: the engine already parses the task
  file, so python re-reading argv would duplicate parsing and couple to
  the launch contract. ITask::execute() publishes the task JSON around
  the begin/end window — before beginTask, because endpoint (and node
  global) construction happens inside it — and clears it on every exit
  path. The store layer resolves identity+anchor itself
  (Store.engine_file_store()), so tool_filesystem carries zero jobConfig
  plumbing and future consumers get task data for free. Single-threaded
  by contract (main task thread only), hence no lock.

* explorer-ui roots its listing at '@' to present the joined tree.

1572 server tests green (suite grew ~90 tests: authorization matrix,
grammar fuzz, system-tree policy, anchor chroot, cprofile proxy).
Engine rebuilt on both dists; rocketlib.getTask verified live.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… universal root guards, real org resolution

Addresses the 14 CodeRabbit review threads:

Security/correctness:
- file_store: _get_handle requires an EXACT conn_id match. The handle
  registry became Store-wide in this PR, so the lenient empty-conn_id
  check would have let any default RequestContext reach another
  connection's live handle. Fail closed.
- file_store: the rename/delete root guards no longer exempt the caller's
  own namespace — rename('') resolved to the account root and turned the
  copy+delete loop into a whole-account move. delete/rename/rmdir now
  reject every root uniformly.
- store: a session-shaped ctx (account present, not internal/engine) with
  an empty userId can no longer anchor a FileStore via explicit client_id;
  that combination (e.g. a task-scoped AccountInfo built from an empty
  control.userId) would unlock an arbitrary user's namespace. Account-LESS
  contexts stay constructible because resolve_scope already denies their
  every operation with 'Not authenticated'.
- task_conn/cmd_task/cmd_debug: new resolve_org_for_team() — members
  resolve via their own membership; callers that pass the team check
  without membership (sys.admin, internal) resolve the target team's REAL
  org through the account backend, and an unresolvable team is denied with
  the uniform message. Previously such callers registered the task with
  orgId='' travelling in the task file as trusted identity.
- task_engine: _storage_root() validates through validate_storage_root at
  launch, so a malformed client-supplied project_id fails with a clear
  launch-time error instead of tool_filesystem disabling itself mid-run
  inside the subprocess.

Minor:
- explorer-ui: handleMove/handleUpload carry the '@' mount through an
  empty targetDir — the tree is rooted at '@', so bare names silently
  targeted the plain user store.
- tool_filesystem: IGlobal docstring, README handwritten sections, and the
  services.json pathWhitelist description describe the task storage anchor
  (dev = user tree, deploy = team task subtree) instead of the universal
  user-root claim. The generated README block regenerates on develop
  (nodes:docs-generate is branch-gated by design).
- file_store: stale usage examples fixed (Store.file_store(ctx), no
  connection_id params); handle-cap comment updated to string conn_ids.
- task.hpp/task.cpp: currentTask()'s main-task-thread-only access model is
  stated on the public API — publish, reads, and clear are strictly
  sequential on the single main task thread, so the value is
  unsynchronized by design.
- store/file_store: per-handle force-close teardown deduped into
  Store._teardown_handle(); FileStore._force_close_handle and
  close_all_handles delegate so the sequences cannot drift.
- task_server: dropped the outer sys.admin short-circuit duplicated inside
  resolve_task_permissions.

Tests: real resolve_org_for_team bound into the cmd_task/cmd_debug stubs,
get_merged_env patched where execution reaches the secret merge, dead
unowned FileStore construction removed, 16 inline FileStore(Store(istore),
...) constructions replaced with a shared make_file_store() helper, and
the sys.admin bypass test now asserts the resolver-side grant.

1572 passed / 122 skipped; ruff clean; explorer-ui rebuilt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s.json description

resolve_org_for_team returned org.get('id', '') straight from the
membership loop, so a membership record whose org carries no id skipped
the empty-org guard the function exists to enforce. Both paths now feed
one guard: membership resolves the id (falling through to the account
backend when it is empty), and an unresolvable org denies with the
uniform no-permissions message.

tool_filesystem/services.json keeps develop's (#1651) description text
verbatim as the conflict resolution for the rebase onto develop; the
sink code's storage wording will be reconciled with the task-anchor
model when this PR merges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/ai/tests/ai/account/test_store.py (1)

1336-1393: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add a regression test for cross-connection handle rejection.

The strict _get_handle ownership check (file_store.py Lines 937-943) is the security-critical change in this area — the registry is now Store-wide, so a handle leak crosses identities, not just instances. Nothing here or in test_store_auth.py asserts that a handle opened by one conn_id is rejected for another.

💚 Suggested test
`@pytest.mark.asyncio`
async def test_handle_not_usable_from_another_connection(self, temp_dir):
    """A handle is bound to the conn_id that opened it."""
    store = Store.create(url=f'filesystem://{temp_dir}')
    fs_a = store._file_store(RequestContext.internal('conn-a'), client_id='test-user')
    fs_b = store._file_store(RequestContext.internal('conn-b'), client_id='test-user')

    handle_id = await fs_a.open_write('owned.bin')
    with pytest.raises(StorageError, match='another connection'):
        await fs_b.write_chunk(handle_id, b'intruder')
    await fs_a.close_write(handle_id)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/tests/ai/account/test_store.py` around lines 1336 - 1393, Add a
regression test alongside the handle tests that creates two file-store instances
with distinct connection IDs, opens a write handle through the first, and
verifies the second cannot use it via write_chunk, raising StorageError with the
expected cross-connection message. Close the handle through the owning instance
afterward.
packages/ai/src/ai/modules/task/task_engine.py (1)

1254-1283: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the token substitution — the same six lines run twice.

The {token}/{public_auth}/{project_id}/{source} replacement block is duplicated verbatim in the str and dict branches, so a future token has to be added in two places. Also note the getattr guards exist only for test stubs — project_id/source are assigned unconditionally in __init__ (Lines 242-243), so a real Task always carries them.

♻️ Proposed refactor
+    def _substitute_note_tokens(self, text: str) -> str:
+        """Replace the note placeholders with this task's identity values."""
+        text = text.replace('{token}', self.token)
+        text = text.replace('{public_auth}', self.public_auth)
+        # getattr: test stubs may not carry the identity attributes.
+        for name in ('project_id', 'source'):
+            value = getattr(self, name, None)
+            if value is not None:
+                text = text.replace('{' + name + '}', str(value))
+        return text

Then both branches become note = self._substitute_note_tokens(note) / value = self._substitute_note_tokens(value).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/ai/modules/task/task_engine.py` around lines 1254 - 1283,
Extract the duplicated token-replacement logic from the note-processing loop
into a helper method on the task class, such as _substitute_note_tokens,
covering token, public_auth, project_id, and source substitutions. Use this
helper for string notes and every string value in dict notes, while preserving
non-string values and the existing note-appending behavior; use the task’s
initialized project_id and source attributes rather than repeating getattr
guards.
apps/explorer-ui/src/ExplorerSidebar.tsx (1)

114-122: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Rooting at @ turns refresh into a full multi-scope recursive crawl.

listRecursive (Line 361) awaits one fsListDir per directory, sequentially. Previously that walked only the caller's own tree; from @ it now descends @/User, every team under @/Team, and @/Org — and refresh() runs on mount and after every rename/delete/move/upload. For a user in several teams with deep trees this is a large serial request cascade before the sidebar paints.

Consider lazy expansion (list a directory when its node is expanded) rather than eager full-depth recursion from the virtual root.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/explorer-ui/src/ExplorerSidebar.tsx` around lines 114 - 122, Replace the
eager full-tree crawl in refresh with lazy directory loading: initialize the
sidebar from the virtual root without recursively traversing all descendants,
and invoke listRecursive or equivalent directory listing only when a node is
expanded. Update the relevant expansion handler and refresh flow while
preserving entry updates after rename, delete, move, and upload operations.
nodes/src/nodes/tool_filesystem/services.json (1)

12-12: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Node description still documents the removed ROCKETRIDE_CLIENT_ID env var.

Line 118 was updated to the new task storage anchor, but this description still claims operations are scoped to users/<client_id>/files/ "resolved from the ROCKETRIDE_CLIENT_ID env var injected by the task engine" — the exact mechanism this PR replaces with task-file identity, and wrong for deploy runs that anchor under the team subtree. This string renders in the node UI.

📝 Proposed wording
-	"description": ["File system node. As an agent tool it reads, writes, deletes, lists, and creates directories within the account-scoped RocketRide file store (the same store exposed via the client SDK's fs_* methods); as a pipeline sink it persists incoming lane data (documents, text, tables, and streamed image/audio/video) to the same store.", "All operations are scoped to users/<client_id>/files/, resolved from the ROCKETRIDE_CLIENT_ID env var injected by the task engine.", "Per-operation toggles (allowRead, allowWrite, allowDelete, allowList, allowMkdir, allowStat) gate which methods the agent can call."],
+	"description": ["File system node. As an agent tool it reads, writes, deletes, lists, and creates directories within the account-scoped RocketRide file store (the same store exposed via the client SDK's fs_* methods); as a pipeline sink it persists incoming lane data (documents, text, tables, and streamed image/audio/video) to the same store.", "All operations are scoped to the task's storage anchor published in the task file: the owning user's file tree for development runs, the task's team subtree for deployed runs.", "Per-operation toggles (allowRead, allowWrite, allowDelete, allowList, allowMkdir, allowStat) gate which methods the agent can call."],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nodes/src/nodes/tool_filesystem/services.json` at line 12, Update the
description metadata for the filesystem node to remove the obsolete
ROCKETRIDE_CLIENT_ID reference and users/<client_id>/files/ scope. Describe the
current task-file identity/storage anchor instead, including team-subtree
behavior where applicable, while preserving the existing operation and toggle
descriptions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ai/src/ai/account/file_store.py`:
- Around line 785-790: Update the root guard in the rmdir implementation around
_resolve to reject every scope root with an unconditional not rest check,
matching the guards used by delete and rename; remove the kind != 'own'
exception while preserving the existing empty-path validation.
- Around line 166-172: The scope-entry validation in the shown parsing function
only rejects names whose first component starts with “=”. Extend the
first-component check to also reject “@”, preserving the existing ValueError
behavior and message style so scoped entries such as “@weird” remain
unavailable.
- Around line 307-314: Update the own-namespace branch in the resolver around
kind == 'own' and kind == 'user' with ref is None so non-admin users are granted
access to users/{client_id}/files without checking account_info.defaultTeam or
resolve_task_permissions. Preserve the existing admin behavior and path
resolution.

In `@packages/ai/src/ai/account/models.py`:
- Around line 169-177: Update the `account_info` attribute documentation in the
request context model to state that `None` is valid for both pre-auth commands
handled by `on_auth` and unauthenticated engine subprocess contexts created by
`RequestContext.engine()`. Keep the remaining `conn_id` and `source`
documentation unchanged.

In `@packages/ai/src/ai/account/store.py`:
- Around line 450-494: Make Store.instance() thread-safe using double-checked
locking so concurrent callers initialize exactly one Store and all callers
receive the same instance. Add a class-level synchronization primitive, guard
creation after rechecking cls._instance inside the lock, and preserve the
existing lazy Store.create() behavior and shared lock registries.

In `@packages/ai/src/ai/modules/task/commands/cmd_store.py`:
- Around line 329-353: In _store_fs_list_dir, normalize the requested path by
collapsing empty segments, then pass that same normalized value to
_get_file_store().list_dir so resolution and _is_scope_root filtering stay
aligned; update packages/ai/tests/ai/modules/task/commands/test_cmd_task.py
lines 597-611 to include bypassing spellings such as '`@//User`' in the scope-root
cases and verify system trees remain hidden for non-sys.admin callers.

In `@packages/ai/src/ai/modules/task/task_engine.py`:
- Around line 477-491: Update Task construction/startup around start_task(),
_build_task(), and _storage_root() so launches with the default empty client_id
do not produce users//files and fail unexpectedly: either reject the missing
client_id immediately with a clear user-facing message or preserve the existing
anonymous-run behavior by handling the empty value without calling
validate_storage_root on an invalid path.

In `@packages/ai/tests/ai/account/test_store_auth.py`:
- Around line 503-531: Add coverage to TestScopeRootGuards for the caller’s own
root, exercising delete and both rename directions with the spellings '', '.',
and '/'. Assert each operation raises StorageError matching 'root', so the tests
pin the universal root guard rather than only scoped and joined mount roots.

In `@packages/ai/tests/ai/account/test_store.py`:
- Around line 563-568: Update test_create_with_default_url to use the pytest
monkeypatch fixture and remove both RR_STORE_URL and legacy STORE_URL via
monkeypatch.delenv, allowing missing variables. Keep the existing Store.create()
default-URL assertion unchanged and rely on monkeypatch for automatic
environment cleanup.

In `@packages/ai/tests/ai/modules/task/commands/test_cmd_task.py`:
- Around line 597-611: Extend
test_listing_hides_system_trees_from_ordinary_sessions to include the malformed
scope-root spelling '`@/User`' with an extra slash, ensuring it still hides .logs
and .deployments while exposing docs. Preserve the existing path matrix and
assertions so the normalization behavior is pinned.
- Around line 27-35: Update the Store.file_store monkeypatch in
_patch_store_file_store to accept the production signature, including the
optional root parameter after client_id, while continuing to return the mocked
fs for all arguments.

In `@packages/ai/tests/ai/modules/task/test_task_engine.py`:
- Around line 217-226: Add a mirror test beside
test_build_task_deploy_without_team_refuses covering a dev run with an empty
client_id. Configure the task and filesystem/executable stubs consistently, call
Task._build_task, and assert it raises ValueError matching client_id.

---

Outside diff comments:
In `@apps/explorer-ui/src/ExplorerSidebar.tsx`:
- Around line 114-122: Replace the eager full-tree crawl in refresh with lazy
directory loading: initialize the sidebar from the virtual root without
recursively traversing all descendants, and invoke listRecursive or equivalent
directory listing only when a node is expanded. Update the relevant expansion
handler and refresh flow while preserving entry updates after rename, delete,
move, and upload operations.

In `@nodes/src/nodes/tool_filesystem/services.json`:
- Line 12: Update the description metadata for the filesystem node to remove the
obsolete ROCKETRIDE_CLIENT_ID reference and users/<client_id>/files/ scope.
Describe the current task-file identity/storage anchor instead, including
team-subtree behavior where applicable, while preserving the existing operation
and toggle descriptions.

In `@packages/ai/src/ai/modules/task/task_engine.py`:
- Around line 1254-1283: Extract the duplicated token-replacement logic from the
note-processing loop into a helper method on the task class, such as
_substitute_note_tokens, covering token, public_auth, project_id, and source
substitutions. Use this helper for string notes and every string value in dict
notes, while preserving non-string values and the existing note-appending
behavior; use the task’s initialized project_id and source attributes rather
than repeating getattr guards.

In `@packages/ai/tests/ai/account/test_store.py`:
- Around line 1336-1393: Add a regression test alongside the handle tests that
creates two file-store instances with distinct connection IDs, opens a write
handle through the first, and verifies the second cannot use it via write_chunk,
raising StorageError with the expected cross-connection message. Close the
handle through the owning instance afterward.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2f0eb75c-f72f-443a-91e5-0ad89f424599

📥 Commits

Reviewing files that changed from the base of the PR and between 44cd058 and 33e576a.

📒 Files selected for processing (39)
  • apps/explorer-ui/src/ExplorerSidebar.tsx
  • nodes/src/nodes/tool_filesystem/IGlobal.py
  • nodes/src/nodes/tool_filesystem/README.md
  • nodes/src/nodes/tool_filesystem/services.json
  • packages/ai/src/ai/account/__init__.py
  • packages/ai/src/ai/account/file_store.py
  • packages/ai/src/ai/account/models.py
  • packages/ai/src/ai/account/store.py
  • packages/ai/src/ai/account/store_providers/filesystem/__init__.py
  • packages/ai/src/ai/account/store_providers/filesystem/filesystem.py
  • packages/ai/src/ai/account/store_providers/memory/__init__.py
  • packages/ai/src/ai/account/store_providers/memory/memory.py
  • packages/ai/src/ai/modules/task/commands/cmd_cprofile.py
  • packages/ai/src/ai/modules/task/commands/cmd_debug.py
  • packages/ai/src/ai/modules/task/commands/cmd_log.py
  • packages/ai/src/ai/modules/task/commands/cmd_store.py
  • packages/ai/src/ai/modules/task/commands/cmd_task.py
  • packages/ai/src/ai/modules/task/fetch.py
  • packages/ai/src/ai/modules/task/task_conn.py
  • packages/ai/src/ai/modules/task/task_engine.py
  • packages/ai/src/ai/modules/task/task_server.py
  • packages/ai/tests/ai/account/test_file_store.py
  • packages/ai/tests/ai/account/test_store.py
  • packages/ai/tests/ai/account/test_store_auth.py
  • packages/ai/tests/ai/modules/task/commands/test_cmd_cprofile.py
  • packages/ai/tests/ai/modules/task/commands/test_cmd_debug.py
  • packages/ai/tests/ai/modules/task/commands/test_cmd_task.py
  • packages/ai/tests/ai/modules/task/test_log_stream.py
  • packages/ai/tests/ai/modules/task/test_run_log.py
  • packages/ai/tests/ai/modules/task/test_run_log_reader.py
  • packages/ai/tests/ai/modules/task/test_task_conn.py
  • packages/ai/tests/ai/modules/task/test_task_engine.py
  • packages/ai/tests/ai/modules/task/test_task_server.py
  • packages/server/engine-lib/engLib/store/python/bindings.cpp
  • packages/server/engine-lib/engLib/task/core/task.cpp
  • packages/server/engine-lib/engLib/task/headers/task.hpp
  • packages/server/engine-lib/rocketlib-python/lib/rocketlib/__init__.py
  • packages/server/engine-lib/rocketlib-python/lib/rocketlib/__init__.pyi
  • packages/server/engine-lib/rocketlib-python/lib/rocketlib/engine.py

Comment thread packages/ai/src/ai/account/file_store.py Outdated
Comment thread packages/ai/src/ai/account/file_store.py Outdated
Comment thread packages/ai/src/ai/account/file_store.py Outdated
Comment thread packages/ai/src/ai/account/models.py
Comment thread packages/ai/src/ai/account/store.py
Comment thread packages/ai/tests/ai/account/test_store_auth.py
Comment thread packages/ai/tests/ai/account/test_store.py Outdated
Comment thread packages/ai/tests/ai/modules/task/commands/test_cmd_task.py
Comment thread packages/ai/tests/ai/modules/task/commands/test_cmd_task.py
Comment thread packages/ai/tests/ai/modules/task/test_task_engine.py
…ation, sigil reservation, membership-based own-tree access

- file_store: normalize_path extracted to module level as THE wire-path
  normalization; FileStore._validate_path delegates to it. cmd_store's
  system-tree filter now normalizes with this same routine AND resolves the
  listing from the same normalized path — deciding on the raw spelling while
  resolving the normalized one let '@//User'-family spellings reach the user
  root with .logs/.deployments unfiltered.
- file_store: enforce the documented '@' reservation at scope tops alongside
  '=' — a scoped rest could smuggle in an '@'-leading name inside the
  grammar room the docs already reserved.
- file_store: the caller's OWN tree no longer hinges on the defaultTeam
  pointer (an unset or stale defaultTeam denied a user their own storage);
  the file-storage permission grants when ANY membership carries it.
- file_store: rmdir root guard made universal (plain `not rest`) to match
  delete/rename — the kind-conditional form was exactly the guard a future
  normalization change could quietly defeat.
- store: double-checked locking on the Store singleton — two threads racing
  instance() could each build a Store with its own _shared_handles /
  _shared_write_locks registry, silently defeating the cross-instance write
  exclusion this PR introduces (engine-subprocess node init is the realistic
  race).
- task_engine: anonymous dev runs (client_id='' — OSS/standalone) carry NO
  storage anchor instead of failing the launch: identity.userId rides empty
  too, so engine_file_store() yields None and the storage tools disable
  themselves — the same degradable posture as the run-log writer; the shared
  'users//files' prefix is never composed.
- models: RequestContext.account_info docs now name engine-subprocess
  contexts as the second legitimate None case.
- tests: pin the universal own-root guard (delete/rename of ''), the
  normalization-bypass listing matrix ('@//User', '@/./User', '\@\User',
  '/'), the dev-run no-anchor posture, STORE_URL/RR_STORE_URL isolation via
  monkeypatch.delenv, and mirror Store.file_store's real signature
  (root=None) in the cmd_task stub.

1636 server tests green (./builder ai:run-pytest --verbose).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@kwit75 kwit75 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Read this one as an authorization boundary rather than a storage refactor, since that's the part that's expensive to get wrong later. The enforcement design is genuinely tight — but I found one thing I'd want resolved before it ships, and I was able to size it against prod.

What I went looking for and didn't find

No traversal escape. parse_scope runs on an already-normalized path, _split_ref rejects ./.., validate_storage_root re-validates the anchor even though _build_task produced it (right call — the subprocess hands it back, so it's untrusted on the return leg), and the filesystem provider still does resolve() + relative_to(root) as a backstop. @/Team/<name> can't reach a foreign org because the only dictionary consulted is the caller's own membership list, and unknown/foreign/ambiguous all collapse to one _DENIED string so there's no existence oracle. Reserving = at the top of every scope so id references can't be shadowed by a real file is a nice touch I wouldn't have thought of.

The engine-context restriction is the part I like most: ctx.source == 'engine' rejects every @ spelling and every system tree, so an LLM-authored path is confined to the task anchor by construction rather than by filtering. And checking _system_tree(rest) before scope resolution keeps that denial uniform and early.

The one thing: this inherits the singular-organization assumption, and prod already violates it

AccountInfo.organization is a single Optional[OrgInfo]models.py:56 says so explicitly ("single org per user"). Both authorization dictionaries in the new code derive from it: _team_ids() and _resolve_team_name() walk organization['teams'], and resolve_scope's @/Org branch treats org['id'] as "my org, implicitly".

Prod has 9 users in two organizations. All 9 are members of a second org beyond the one holding their default_team_id, and 6 of them have actual team memberships in both. I checked directly:

orgs per user:  1 -> 288 users
                2 ->   9 users
users with team memberships in >1 org        6
multi-org users whose default_team resolves  9  (always to a team in one of their orgs)
multi-org users also in a different org      9

Two consequences, and the second is the one that worries me:

Unconditionally: for those 9, the teams in their other org are inexpressible. _resolve_team_name won't find the name, and @/Org/=<other-org-id> is denied unless the caller is sys.admin. The docstring frames inexpressibility as the security property — which it is for a genuinely foreign org, but for these users it denies a team they are really a member of.

Conditionally, and worse: the ordinary-session branch for plain paths calls resolve_task_permissions(account_info, account_info.defaultTeam), which walks organization['teams'] and returns [] when the team isn't there — and resolve_scope turns [] into PermissionError("Permission 'task.store' denied"). So if the session materialises the org that doesn't contain the user's default_team_id, they lose every plain path, not just team scopes. That's total storage failure for that user, on the most ordinary code path. Which of the two orgs gets materialised is saas-side, so I can't tell you from here whether it currently trips — only that all 9 users have the ingredients.

Why I'd rather flag it now than let it land: this is the same root cause as saas #373/#374, the dashboard blanket-deny we fixed in June — one AccountInfo.organization where the user has several. That fix lives in the saas layer; this is the engine re-deriving the same assumption for storage. I recognise the # identical lines exist on feat/alb; dedupe on merge note, so the duplication is already on your radar — my point is narrower: the dedupe target itself is the shape that was wrong.

And the failure will be painful to diagnose, precisely because of a decision that is otherwise correct. The uniform _DENIED means a legitimately-denied member and a blocked cross-tenant attempt produce identical output. A user reporting "I can't open my team's files" will look exactly like an attack being correctly stopped. If the multi-org case isn't fixed in this PR, I'd at least want the denial to log a distinguishable internal reason code — visible in logs, never in the response.

On tests: test_store_auth.py:78 builds a single organization={'id': 'org-1', ...}, so the fixture can't express a two-org caller at all. That's why the matrix looks complete and still misses this. A fixture with a second org, and a case asserting that a team in the second org resolves, would pin it either way.

Smaller notes, none blocking

sys.admin =id crossing is the right call, but it's the one path with no membership check@/Org/=<any-id> and @/Team/=<any-id> resolve mechanically for a sys.admin session. That is deliberate ("the platform support capability") and I agree with it; I'd just want those resolutions audited, since it's the only place in the function where a boundary is crossed rather than enforced. We already have AccountService.audit() (saas#366) — this seems like exactly its use case.

internal returns before the system-tree check. Reading the order, an internal identity gets users|teams|orgs/<id>/files without passing _system_tree. That's consistent with the docstring ("system trees included") so I read it as intended — flagging only to confirm it's intent and not fall-through, because it's the one branch where the early uniform denial doesn't apply.

resolve_task_permissions returns [] while resolve_team_permissions raises, for the same "no membership" condition. Both are correct in isolation and the docstrings call out the difference, but two functions one line apart with opposite failure modes is the kind of thing that gets picked wrong under time pressure. Worth a name that carries the difference.


Nothing here is a traversal or cross-tenant hole — I looked for that first and the boundary holds. The multi-org item is a fail-closed correctness bug, which is the right direction to fail, but it has 9 real users behind it today. Happy to take the fixture-plus-second-org test myself if that's useful, and I can pull the exact user ids for you privately if you want to confirm the behaviour against a real session before deciding.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/ai/tests/ai/account/test_store.py (1)

579-590: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Use monkeypatch for the RR_STORE_URL override.

If Store.create() or an assertion fails, Line 590 never runs and leaks storage configuration into later tests.

Proposed fix
-    def test_create_with_env_var(self, tmp_path):
+    def test_create_with_env_var(self, tmp_path, monkeypatch):
         """Test creating store from STORE_URL environment variable."""
-        os.environ['RR_STORE_URL'] = f'filesystem://{tmp_path}'
+        monkeypatch.setenv('RR_STORE_URL', f'filesystem://{tmp_path}')
 
         store = Store.create()
 
         assert isinstance(store, Store)
         assert isinstance(store._store, FilesystemStore)
         assert store._store._root_path == tmp_path
-
-        # Cleanup
-        os.environ.pop('RR_STORE_URL', None)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/tests/ai/account/test_store.py` around lines 579 - 590, Update
test_create_with_env_var to use pytest’s monkeypatch fixture for the
RR_STORE_URL override instead of directly modifying os.environ. Set the variable
through monkeypatch so cleanup occurs automatically even when Store.create or an
assertion fails, and remove the manual os.environ cleanup.
packages/ai/src/ai/modules/task/commands/cmd_store.py (2)

407-414: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

_store_fs_stat doesn't share _store_fs_list_dir's normalization fix.

path = (args.get('path') or '').strip('/') only strips slashes, and the fallthrough call still uses the original args.get('path') rather than path. Bypass spellings such as '@//User', '@/./User', '\@\User' won't match the virtual-mount tuple and instead fall through to a real .stat() call — not a security bypass (the store's own resolve_scope still authorizes correctly), but it's the same normalization-consistency gap that _store_fs_list_dir already fixed elsewhere in this file, so the "virtual" quick-path is inconsistently applied.

♻️ Proposed fix
-        path = (args.get('path') or '').strip('/')
+        path = normalize_path(args.get('path', '') or '')
         if path in ('@', '`@/User`', '`@/Team`', '`@/Org`'):
             return self.build_response(request, body={'exists': True, 'type': 'dir', 'virtual': True})
 
-        result = await self._get_file_store().stat(args.get('path'))
+        result = await self._get_file_store().stat(path)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/ai/modules/task/commands/cmd_store.py` around lines 407 -
414, Update _store_fs_stat to use the same path normalization as
_store_fs_list_dir, including separator and dot-segment handling, before
checking virtual mount roots. Pass the normalized path to the subsequent
filesystem-stat fallthrough instead of the original args.get('path') value,
while preserving the existing virtual response for @, `@/User`, `@/Team`, and `@/Org`.

48-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale class docstring — no longer matches _get_file_store's identity resolution.

Says access "Requires self._server.store... self._account_info.userId for user-scoped file access", but _get_file_store (lines 125-136) now goes through Store.file_store(self.request_context()), with scoping/identity resolved entirely inside the identity-bound FileStore.

📝 Proposed fix
-    Provides ``on_rrext_store`` which dispatches to ``fs_*`` subcommands.
-    Requires ``self._server.store`` for the Store instance and
-    ``self._account_info.userId`` for user-scoped file access.
+    Provides ``on_rrext_store`` which dispatches to ``fs_*`` subcommands.
+    Every subcommand routes through ``Store.file_store(self.request_context())``,
+    which resolves and authorizes identity/scoping internally.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/ai/modules/task/commands/cmd_store.py` around lines 48 - 55,
Update the StoreCommands class docstring to remove the outdated requirements for
self._server.store and self._account_info.userId, and describe that user-scoped
access is resolved through Store.file_store using the identity-bound FileStore
obtained from request_context().
packages/ai/src/ai/modules/task/task_engine.py (1)

456-475: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Retain or reject userId for deploy tasks to keep the deploy contract consistent

Deploy runs currently require only team_id, but the published task identity still writes identity.userId as client_id. Since Store.engine_file_store() returns None whenever identity.userId is empty, a deploy task launched with an empty client/session id gets a valid team storage anchor with engine file-store disabled; deployment records also write under users/<client_id>/deployments/.... Require a client identity for deploy launches, or use a deploy-only internal identity and decouple engine file-store availability from identity.userId.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/ai/modules/task/task_engine.py` around lines 456 - 475,
Update the deploy launch path and the identity/storage setup around
_storage_root and engine_file_store so deploy tasks never publish an empty
identity.userId while still claiming a valid team storage anchor. Either require
a non-empty client_id for deploy launches, or assign a documented deploy-only
internal identity and adjust Store.engine_file_store and deployment record paths
to use it consistently; preserve the existing team-scoped storage behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/ai/src/ai/modules/task/commands/cmd_store.py`:
- Around line 407-414: Update _store_fs_stat to use the same path normalization
as _store_fs_list_dir, including separator and dot-segment handling, before
checking virtual mount roots. Pass the normalized path to the subsequent
filesystem-stat fallthrough instead of the original args.get('path') value,
while preserving the existing virtual response for @, `@/User`, `@/Team`, and `@/Org`.
- Around line 48-55: Update the StoreCommands class docstring to remove the
outdated requirements for self._server.store and self._account_info.userId, and
describe that user-scoped access is resolved through Store.file_store using the
identity-bound FileStore obtained from request_context().

In `@packages/ai/src/ai/modules/task/task_engine.py`:
- Around line 456-475: Update the deploy launch path and the identity/storage
setup around _storage_root and engine_file_store so deploy tasks never publish
an empty identity.userId while still claiming a valid team storage anchor.
Either require a non-empty client_id for deploy launches, or assign a documented
deploy-only internal identity and adjust Store.engine_file_store and deployment
record paths to use it consistently; preserve the existing team-scoped storage
behavior.

In `@packages/ai/tests/ai/account/test_store.py`:
- Around line 579-590: Update test_create_with_env_var to use pytest’s
monkeypatch fixture for the RR_STORE_URL override instead of directly modifying
os.environ. Set the variable through monkeypatch so cleanup occurs automatically
even when Store.create or an assertion fails, and remove the manual os.environ
cleanup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: feeb99fe-856f-448b-aa24-7b7c01c3b5ca

📥 Commits

Reviewing files that changed from the base of the PR and between 33e576a and 3ff44eb.

📒 Files selected for processing (9)
  • packages/ai/src/ai/account/file_store.py
  • packages/ai/src/ai/account/models.py
  • packages/ai/src/ai/account/store.py
  • packages/ai/src/ai/modules/task/commands/cmd_store.py
  • packages/ai/src/ai/modules/task/task_engine.py
  • packages/ai/tests/ai/account/test_store.py
  • packages/ai/tests/ai/account/test_store_auth.py
  • packages/ai/tests/ai/modules/task/commands/test_cmd_task.py
  • packages/ai/tests/ai/modules/task/test_task_engine.py

…ocument resolver contract split (PR #1686)

Addresses the three non-blocking notes from the authorization review:

- file_store: sys.admin '=id' resolutions are the ONE place resolve_scope
  crosses the storage boundary instead of enforcing it (the platform support
  capability). Each crossing — foreign team/org/user id — now emits a
  server-side audit trace (actor, conn, target) via rocketlib.debug. Member
  '=id' resolutions cross nothing and are NOT audited. Trace-only: the wire
  response still cannot distinguish a crossing from an ordinary resolve, so
  the uniform-denial / no-oracle posture is preserved.
- file_store: comment the internal-identity branch as deliberately ordered
  BEFORE the system-tree gate (intent, not fall-through) — the run-log writer
  and domain APIs write .logs/.deployments through that identity;
  test_internal_identity_writes_logs is now also the ordering pin.
- models: banner both resolve_task_permissions (RETURNS []) and
  resolve_team_permissions (RAISES) with their opposite no-membership
  contracts. The rename to carry the difference in the name (get_* vs
  require_*) is deferred to the feat/alb dedupe, where both branches touch
  these functions — renaming here would fight that dedupe.
- tests: TestSysAdminCrossingAudit pins that foreign-id crossings audit and
  member resolutions don't (monkeypatches the hook, since the sink is native
  rocketlib.debug).

757 account+task tests green (./builder ai:run-pytest path).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Rod-Christensen

Copy link
Copy Markdown
Collaborator Author

@kwit75 — thank you, this was exactly the review this PR needed. Reading it as an authorization boundary rather than a storage refactor is the right lens, and the confirmations (no traversal escape, no existence oracle, engine confinement by construction, = reservation) match the intent.

The multi-org finding — resolved by policy, not code. One org per user is the product invariant: AccountInfo.organization singular is deliberate, and the engine encoding it is intended, not a re-derived assumption. The 9 prod users with a second-org membership are invariant-violating data, not a supported configuration — the engine fail-closing on them is the correct direction. So the actionable work is saas-side and tracked separately: reconcile those 9 (we'll take you up on the offer to pull the exact user ids) and enforce one-org-per-user at membership creation so the invariant holds by construction. We'll also update the OrgInfo docstring to state it's an enforced invariant rather than an assumption, so this doesn't get re-filed. Your saas #373/#374 link is apt — the difference is that here the single-org shape is the intended contract, not an accident.

The three smaller notes — all addressed in 10a399b:

  1. sys.admin =id crossings are now audited. Every foreign team/org/user =id resolution — the only place resolve_scope crosses the boundary rather than enforcing it — emits a server-side trace (actor, conn, target). Member =id resolutions cross nothing and are deliberately not audited. Trace-only, so the uniform-denial / no-oracle posture on the wire is untouched. (AccountService.audit() lives saas-side and isn't reachable from resolve_scope, so this uses the engine's trace channel; wiring the richer saas audit is a follow-up.)
  2. The internal-before-system-tree ordering is now pinned as intent — comment on the branch plus test_internal_identity_writes_logs doubling as the ordering pin. Confirmed: internal bypassing the system-tree gate is intent, not fall-through — the run-log writer and rrext_deploy write .logs/.deployments through exactly that identity.
  3. The resolver failure-mode split is documented at both signatures (resolve_task_permissions RETURNS [], resolve_team_permissions RAISES). The rename to carry the difference in the name (get_* vs require_*) is deferred to the feat/alb dedupe — both branches touch these functions there, so the rename lands once against a reconciled definition instead of fighting the dedupe.

Appreciate the offer on the two-org fixture and the user ids — the ids would help us confirm which org materializes for the 3 users without second-org team memberships before the saas cleanup.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/ai/src/ai/account/models.py (1)

177-179: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the engine source value.

RequestContext.engine() sets source='engine' on Line 226, but this contract only documents local and orchestrator. It also describes local as OSS-only although RequestContext.internal() uses it.

Proposed fix
-        source:       ``'local'`` for OSS standalone connections or
-                      ``'orchestrator'`` for commands forwarded by the
-                      Orchestrator via an internal pod connection.
+        source:       ``'local'`` for local connections and trusted internal
+                      callers, ``'orchestrator'`` for commands forwarded via
+                      an internal pod connection, or ``'engine'`` for engine
+                      subprocess tool nodes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/ai/account/models.py` around lines 177 - 179, Update the
source-value documentation near RequestContext.engine() and
RequestContext.internal() to include the 'engine' value and accurately describe
when 'local' is used, including internal connections beyond OSS standalone
connections. Keep the documented values aligned with the source assignments in
these methods.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/ai/src/ai/account/models.py`:
- Around line 177-179: Update the source-value documentation near
RequestContext.engine() and RequestContext.internal() to include the 'engine'
value and accurately describe when 'local' is used, including internal
connections beyond OSS standalone connections. Keep the documented values
aligned with the source assignments in these methods.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 046e4a98-0887-47f1-bfb4-a8cbcbe044e5

📥 Commits

Reviewing files that changed from the base of the PR and between 3ff44eb and 10a399b.

📒 Files selected for processing (3)
  • packages/ai/src/ai/account/file_store.py
  • packages/ai/src/ai/account/models.py
  • packages/ai/tests/ai/account/test_store_auth.py

@Rod-Christensen
Rod-Christensen enabled auto-merge (squash) July 30, 2026 05:08

@asclearuc asclearuc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @Rod-Christensen — the architecture here is the right call. Moving authorization into file_store.py at the single point where paths resolve makes "every caller is checked" structural instead of a convention, the internal()/engine() factories make the trusted surface greppable, and the three cross-team holes closed on the way (on_execute's unchecked teamId feeding the secret merge, on_restart's defaultTeam-only check, the cprofile proxy) are real. test_store_auth.py is a genuinely strong matrix — the normalization-bypass fuzzing and the sys.admin crossing audit are exactly the tests I would have asked for.

Requesting changes on one point, plus three smaller ones:

  1. Signed download URLs 500 for every team- and org-scoped path. get_url() signs the wire path into the JWT; /task/fetch re-resolves it under RequestContext.internal('fetch'), which accepts =<id> references only. I ran the resolver from this branch to confirm — table and detail inline on fetch.py. Filesystem backend only, which is the live ALB dev config (RR_STORE_URL unset, per the comment rocketride-ai/rocketride-saas#395 renames), and explorer-ui now roots its tree at @, so team paths are the normal spelling the UI holds.
  2. rocketlib.getTask() returns the whole task file, which includes the resolved pipeline with substituted secrets, where Store.engine_file_store() needs two keys. Parity with existing jobConfig access rather than a regression, but cheap to narrow while the API is new. Inline on bindings.cpp.
  3. Store.reset() has zero call sites — the singleton is never reset between tests, so the first Store.instance() pins the backend and its shared registries for the whole session. Inline on store.py, together with two STORE_URL docstrings the rename missed.
  4. Two consistency nits, inline on file_store.py and cmd_store.py.

Coordination note: rocketride-ai/rocketride-saas#395's submodule pointer (33e576a3) is not an ancestor of this branch's head (10a399b5) — the review-fix commit landed after that pointer was stamped, so that PR's CI is not currently exercising the code under review here. Worth re-stamping before either merges.

Non-blocking: nothing in the repo covers get_url or /task/fetch, which is why finding 1 got past a 613-line auth suite. One round-trip test — issue a URL for a team path as an entitled session, drive handle_fetch with the token, assert 200 — would have caught it and would guard the fix.

Comment thread packages/ai/src/ai/modules/task/fetch.py Outdated
Comment thread packages/server/engine-lib/engLib/store/python/bindings.cpp
Comment thread packages/ai/src/ai/account/store.py
Comment thread packages/ai/src/ai/account/file_store.py Outdated
Comment thread packages/ai/src/ai/modules/task/commands/cmd_store.py Outdated
Comment thread packages/ai/src/ai/account/file_store.py
Rod-Christensen and others added 3 commits July 30, 2026 09:53
…aths, live Store.reset, single-parse resolver

- get_url now signs the RESOLVED physical store path instead of the wire
  spelling. The fetch handler re-resolved the claim under an internal
  identity, which has no name dictionary and no org context — so signed
  URLs for '@/Team/<name>' and '@/Org' paths died with an unhandled
  PermissionError (HTTP 500). Authorization already ran at issuance under
  the session identity; the signed JWT is the capability, so the handler
  now serves the claim verbatim through backend._get_full_path (which
  guards traversal) with no scope re-resolution at all.
  FileStore.resolve() existed solely for that handler re-resolution and
  lost its only caller — removed rather than left as dead public surface.
  New TestSignedFetchUrls pins the reviewer's failure matrix (plain,
  @/User, @/Team by name, @/Team by id, @/Org), the resolve-free serve
  path, and that issuance still gates (@/Org without org.admin cannot
  mint a URL).

- Store.reset() is now actually wired: an autouse fixture in the ai tests
  conftest drops the process singleton around EVERY test, so no test can
  pin the RR_STORE_URL (plus _shared_handles/_shared_write_locks) that
  happened to be live when the first singleton-reaching test ran —
  singleton state was order-dependent and stateful before this.
  Also corrected the two stale STORE_URL docstrings in store.py to
  RR_STORE_URL (the legacy name hard-fails at create()).

- resolve_scope now returns (kind, scope_root, rest): it already had the
  kind in hand from its own parse_scope call, so _resolve no longer
  re-parses every path just to recover it — one parse per operation, and
  no "cannot fail" reasoning left for readers to re-verify.

- _store_fs_stat's virtual-mount check now runs normalize_path — the same
  ONE-normalization rule _store_fs_list_dir documents and follows —
  instead of .strip('/'), so mount detection and store resolution can
  never disagree about a spelling, and traversal raises instead of being
  stripped.

Full ai suite: 1646 passed, 122 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The root package.json pinned EXACT @types/react 18.2.79 / react-dom 18.2.25
while shared-ui had moved to ~18.3.29, so pnpm materialized TWO peer-variant
instances of every @types/react-peer package (react-markdown and friends) —
duplicated store copies and forked type-checking between apps. One types
version ends that: root, shell-ui, chat-ui, and dropper-ui now declare the
same ~18.3.29 / ~18.3.7 ranges shared-ui already uses. Types-only change;
chat-ui and dropper-ui type-check clean on the new majors, and the
pre-existing shell-ui contract-check delta is unchanged (verified against a
stashed baseline).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4b12d76 was meant to carry these pins but landed only the lockfile
reshuffle (a stash-pop failure dropped the package.json edits before the
commit — message and content disagreed). This commit lands the actual
alignment: root, shell-ui, chat-ui, dropper-ui at ~18.3.29/~18.3.7;
vscode's floor raised to the resolved ^18.3.31; shared-ui's tilde floor
follows to ~18.3.31. One @types/react instance in the lock (zero 18.2
refs), so @types/react-peer packages (react-markdown et al.) no longer
fork into duplicate peer-variant copies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the module:vscode VS Code extension label Jul 30, 2026
@github-actions github-actions Bot added the module:ui Chat UI and Dropper UI label Jul 30, 2026
… PyPI (#1725)

The -gpu build of 1.20.1 is absent from the index (not yanked; the release
key itself is gone), so every dependency resolve that misses uv's cache dies
with "no version of onnxruntime-gpu==1.20.1 ... requirements are
unsatisfiable". The CPU build of the same version survives, which is what
let the split sit broken unnoticed.

Move all five consumers to 1.22.0 together. It has wheels for -gpu on
win/linux and for the CPU build on macOS-arm, which 1.20.1 no longer does.
Note that the obvious one-patch bump is a trap: 1.20.2 exists only for -gpu,
so pinning it would break macOS.

Also correct the comment that justified the pin. It claimed rtmlib needs
exactly this version; rtmlib declares onnxruntime unpinned, as does gliner,
and faster-whisper asks for <2,>=1.14. The pin exists so the ~200 MB runtime
installs once across consumers — the number is ours to choose, provided all
five files agree.

Verified with the four consuming nodes, opted in via ROCKETRIDE_INCLUDE_SKIP
(real installs and model downloads, not just resolution):
pose_estimation (rtmlib), anonymize, ner (gliner), audio_transcribe
(faster-whisper) — all pass.

Fixes #1723
@Rod-Christensen
Rod-Christensen disabled auto-merge July 30, 2026 18:12
@Rod-Christensen
Rod-Christensen dismissed asclearuc’s stale review July 30, 2026 18:53

All requests applied

@asclearuc asclearuc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Round 4. Four of the five went in cleanly, and the round-3 fixes are better than what I asked for — resolve_scope returning (kind, scope_root, rest) reads well, dropping FileStore.resolve() once its only caller went away was the right instinct rather than leaving dead public surface, the autouse Store.reset() fixture is exactly the wiring that was missing, and TestSignedFetchUrls pins the failure matrix from my last comment including the issuance gate. Worth noting the fetch fix also repaired a second latent bug nobody had reported: a deploy-run engine context signs an anchored path (teams/<id>/files/tasks/<project>/x), and the old handler would have re-resolved that under users/<client_id>/files/ and served the wrong file or 404'd.

One MUST fix, and one carry-over:

  1. The claim changed meaning but did not change shape, so pre-deploy tokens are reinterpreted as physical paths. The old payload signed the wire path, the new one signs the resolved path, and neither carries a version marker — so handle_fetch cannot tell them apart and serves an old token's wire path straight through _get_full_path. I ran it against the real FilesystemStore: an attacker-minted pre-deploy token reads another user's file, a foreign team's file, and system-tree content. Bounded to the token TTL (≤1h) after the new code goes live, and the fix is the 'v': 2 claim from my last comment. Detail inline on fetch.py.
  2. rocketlib.getTask() still returns the whole task file — including the resolved pipeline with substituted secrets. Not mentioned in dde31f47, so flagging it as still open rather than assuming it was declined. Inline on bindings.cpp (unchanged from round 3); if the call is that you would rather keep the binding general and narrow it when a second consumer appears, say so and I will drop it.

Resolved and verified, no action needed:

  • get_url / /task/fetch 500 — fixed at the root by signing the resolved path and removing re-resolution entirely, rather than by patching the internal identity. TestSignedFetchUrls covers plain, @/User, @/Team by name, @/Team by id, and @/Org.
  • Store.reset() dead code — now an autouse fixture in packages/ai/tests/conftest.py dropping the singleton around every test; both stale STORE_URL docstrings corrected.
  • Double parse in _resolveresolve_scope now returns the kind it already had.
  • fs_stat normalization — now normalize_path, and it passes the normalized path down to stat() as well, which is more than I asked for.

Housekeeping: the file count went 39 → 52 because develop merges brought @types/react and the onnxruntime pin along. I checked the content — those 13 files are byte-identical between origin/develop and this branch, so it is a merge-base artifact in GitHub's view, not real divergence. Nothing to do; a rebase before merge would clear the noise. The saas pointer is also now correct: rocketride-ai/rocketride-saas#395 points at 4e7fecb4, which is this branch's head.

Comment thread packages/ai/src/ai/modules/task/fetch.py
Comment thread packages/server/engine-lib/engLib/store/python/bindings.cpp
@asclearuc
asclearuc self-requested a review July 30, 2026 19:03
@Rod-Christensen

Copy link
Copy Markdown
Collaborator Author
  1. since this is the first Rev without any existing wire code, this is correct
  2. this is also correct. Making a distinction in this api is not any more secure since the tokens actually do need to be obtained and are available in the file itself client cares to read it and on the IEndpoint itself

@Rod-Christensen
Rod-Christensen dismissed asclearuc’s stale review July 30, 2026 20:09

Addressed in comments above

@Rod-Christensen
Rod-Christensen merged commit baea4ab into develop Jul 30, 2026
28 checks passed
@Rod-Christensen
Rod-Christensen deleted the feat/deploy-1 branch July 30, 2026 20:09
dylan-savage added a commit that referenced this pull request Jul 30, 2026
…-nodes

Adapts to #1686's task-file identity model: the task engine no longer sets
ROCKETRIDE_CLIENT_ID in node subprocesses (identity rides the 0600 task
file; the ROCKETRIDE_* env namespace is caller-influenced by design), so

- _build_subprocess_env keeps the credential scrubs + DSN injection but no
  longer sets the identity env var (env test updated to assert absence)
- the vector store's connection subKey now keys on the DSN's tenant
  database (the actual isolation unit) instead of env identity
- rocketride_db docs updated: current_client_id remains only for the
  account fallback used by server-process callers and tests

Local: 106 passed (45 translator + 35 seam + 26 integration in
RR_REQUIRE_DB_TESTS mode).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation module:ai AI/ML modules module:nodes Python pipeline nodes module:server C++ engine and server components module:ui Chat UI and Dropper UI module:vscode VS Code extension

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants