feat(registry): add team publishing and team-private visibility - #1640
Conversation
Teamspaces become a publishing target. Authorized members publish agents and all five component types into a team namespace, and can keep them visible only to that team. Visibility is public or team. team_id is the sole ownership and privacy axis: organization scoping is not used for registry visibility. Migration 018_team_publishing adds is_private to agents, which had no privacy concept, and team_id to agents, the five component listings, and component sources. It performs no backfill, because the shared helpers already resolve a legacy private row with no teamspace as creator-only. Authorization is enforced by shared helpers rather than per-route logic: apply_visibility_filter and apply_registry_scope for queries, resolve_visible_listing and check_listing_visibility_async for detail and install paths, and resolve_publish_target for publish authorization. Team owners and team reviewers clear review for team-visibility publishing only. Team roles are self-service, so publishing PUBLIC from a team namespace still enters the global review queue, and turning a team-private listing public returns it to that queue unless the actor is a global reviewer. Agent composition is enforced at validate, draft save, create, version, publish, and install: a public agent cannot contain a private component, and a team-private agent cannot contain another team's private component. CLI gains --team and --visibility on publish and --team and --namespace on browse. The web builder scopes component pickers to the agent's target. Co-Authored-By: Hari Srinivasan <harisrini21@gmail.com>
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThe pull request adds team-scoped publishing and public or team visibility across server APIs, CLI commands, web registry pages, review workflows, migrations, tests, and documentation. It replaces organization-based listing access with team membership and caller-aware visibility checks. ChangesTeam publishing and visibility
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
🧹 Nitpick comments (22)
web/src/components/registry/agent-edit-form.tsx (1)
64-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove
AgentDetailto the shared type module.
web/src/pages/registry/agents/detail.tsxdefines the same API shape. Keep one exportedAgentDetailtype in@/lib/typesand import it in both files. This prevents team and visibility fields from drifting between the detail page and edit form.As per coding guidelines, “Centralize frontend types in
src/lib/types.ts; do not define inline API response types.”🤖 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 `@web/src/components/registry/agent-edit-form.tsx` around lines 64 - 65, Move the exported AgentDetail type from the registry detail page into the shared `@/lib/types` module, then import and reuse it in both detail.tsx and the agent edit form. Remove the duplicate local API type definitions while preserving the existing team_id and visibility fields.Source: Coding guidelines
web/src/pages/registry/agents/detail.tsx (1)
168-170: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove the agent API response type to the shared type module.
AgentDetailnow duplicatesteam_id,visibility, andis_privatefromRegistryItem. Export an agent detail type fromweb/src/lib/types/registry.tsand import it here. This prevents response-contract drift between registry screens.🤖 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 `@web/src/pages/registry/agents/detail.tsx` around lines 168 - 170, Move the AgentDetail API response type from the detail page into the shared registry types module, exporting it alongside RegistryItem with the existing team_id, visibility, and is_private fields. Update the detail page to import and use the shared agent detail type, removing the duplicated local definition.Source: Coding guidelines
tests/test_dashboard_leaderboard.py (1)
156-160: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePair each download query with its table by name, not by position.
zip(_LISTING_TABLES, _download_queries(db), strict=True)assumescomponent_leaderboardemits the five download queries in exactly the_LISTING_TABLESorder. If the route reorders the component types, the assertion fails with a confusing message about the wrong table. Select the statement whose SQL contains the table name instead.♻️ Proposed pairing by table name
- for table, stmt in zip(_LISTING_TABLES, _download_queries(db), strict=True): - sql = _sql(stmt) + queries = [_sql(stmt) for stmt in _download_queries(db)] + assert len(queries) == len(_LISTING_TABLES) + for table in _LISTING_TABLES: + sql = next(q for q in queries if table in q) assert "agents.is_private = false" in sqlAlso applies to: 170-174
🤖 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 `@tests/test_dashboard_leaderboard.py` around lines 156 - 160, Update the download-query assertions around _LISTING_TABLES and _download_queries(db) to pair each table with the statement whose SQL contains that table name, rather than relying on positional zip order. Apply the same table-name lookup to the related assertions around the second download-query block, while preserving the existing privacy checks.tests/test_draft_workflow.py (1)
362-367: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the redundant
dbrebinding.
_sequenced_db(...)buildsdb, thenapp, db, _ = _app_with(user=user, db=db)rebinds the same object. The second assignment reads as if_app_withproduced a new mock. Drop the rebinding to keep one owner of the sequenced db. The same pattern repeats at lines 391, 431, 448, 466 and 487.♻️ Proposed change
- app, db, _ = _app_with(user=user, db=db) + app, _, _ = _app_with(user=user, db=db)🤖 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 `@tests/test_draft_workflow.py` around lines 362 - 367, Remove the redundant db rebinding in the affected tests: keep the _sequenced_db result assigned to db, then unpack only app and the unused return value from _app_with while passing db as its argument. Apply this consistently to the repeated cases identified in the comment, preserving the existing mock object.tests/test_registry_types.py (2)
751-791: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winParametrize the route cases instead of looping inside each test.
The four tests iterate over
_list_endpoint_cases()in aforloop. When one route fails, pytest stops at that route and the failure message does not name it, and the remaining routes are not checked. Move the cases into@pytest.mark.parametrizeso each route reports as a separate test id.♻️ Proposed parametrization
+_LIST_CASES = [pytest.param(*case, id=case[1]) for case in _list_endpoint_cases()] + + `@pytest.mark.asyncio` -async def test_component_lists_hide_team_private_listings_from_anonymous_callers(): - for list_items, table, filters, _expected in _list_endpoint_cases(): - sql = _inline_sql(await _run_list(list_items, filters, current_user=None)) - assert f"{table}.is_private = false" in sql - assert f"{table}.is_private = true" not in sql - assert "team_memberships" not in sql +@pytest.mark.parametrize(("list_items", "table", "filters", "_expected"), _LIST_CASES) +async def test_component_lists_hide_team_private_listings_from_anonymous_callers( + list_items, table, filters, _expected +): + sql = _inline_sql(await _run_list(list_items, filters, current_user=None)) + assert f"{table}.is_private = false" in sql + assert f"{table}.is_private = true" not in sql + assert "team_memberships" not in sql
_list_endpoint_cases()imports route modules at call time, so move the imports to module scope or wrap the case list in a fixture if collection-time import order matters.🤖 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 `@tests/test_registry_types.py` around lines 751 - 791, Parametrize all four tests with the route cases from _list_endpoint_cases() so each route executes as an independent pytest case with a descriptive id. Remove the internal for loops and accept the unpacked case values as test arguments. Because parametrization is evaluated during collection, move _list_endpoint_cases() route imports to module scope or provide the cases through a fixture if collection-time import order requires it.
741-748: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTwo helpers named
_membership_predicateassert different SQL. Both build the correlatedEXISTSonteam_membershipsas a raw SQL substring, but the bodies diverge: the copy intests/test_registry_types.pyincludes theis_private = true AND team_id IS NOT NULLprefix, and the copy intests/test_dashboard_leaderboard.pyincludes only the bareEXISTS. A reader who trusts the shared name assumes both assert the same guarantee, and the dashboard copy accepts SQL that gates membership without checkingis_private.
tests/test_registry_types.py#L741-L748: move this helper into a shared test module and keep the full predicate, including theis_privateandteam_idprefix.tests/test_dashboard_leaderboard.py#L44-L50: delete the local copy and import the shared helper, or rename it to state that it asserts only theEXISTSfragment.🤖 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 `@tests/test_registry_types.py` around lines 741 - 748, Move the full `_membership_predicate` helper from tests/test_registry_types.py:741-748 into a shared test module, preserving the is_private and team_id conditions, and update callers there to use it. Remove the duplicate helper from tests/test_dashboard_leaderboard.py:44-50 and import the shared implementation; do not retain a same-named helper that only asserts the EXISTS fragment.tests/test_component_update_visibility.py (1)
73-152: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftOne listing mock is copied into two test files. Both files build the same MagicMock listing with the same ~80 type-specific attributes and the same explicit
is_private/team_id/visibilitytriple. When a model gains a field, one copy is updated and the other keeps returning a truthy MagicMock attribute, which is the exact failure mode both files added comments to prevent.
tests/test_component_update_visibility.py#L73-L152: replace the local_listing_mockwith an import of the shared builder.tests/test_listing_detail_access.py#L62-L81: move this_listing_mockinto a shared test helper module or aconftest.pyfixture that both files use, keeping theis_private,team_id, andvisibilityparameters.🤖 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 `@tests/test_component_update_visibility.py` around lines 73 - 152, The duplicated listing mocks must use one shared builder. In tests/test_listing_detail_access.py lines 62-81, move the local _listing_mock into a shared test helper or conftest.py while preserving the is_private, team_id, and visibility parameters; in tests/test_component_update_visibility.py lines 73-152, remove the local _listing_mock and import/use that shared builder. Keep both tests’ existing call behavior unchanged.tests/test_team_publishing.py (1)
73-115: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winCover non-member rejection in
resolve_publish_target. The implementation already raises HTTP 403 for a non-member with a suppliedteam_id; add a regression test for this branch.🤖 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 `@tests/test_team_publishing.py` around lines 73 - 115, Extend TestResolvePublishTargetAutoApprove with an async regression test that supplies a team_id for a user who has no team membership, invokes resolve_publish_target, and asserts the call raises HTTP 403. Reuse the existing _mock_db, _user, and team_id setup helpers and preserve the current matrix tests.tests/test_insights_access.py (1)
396-410: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe static guard only inspects async handlers.
The loop skips
ast.FunctionDef, so a synchronous route handler added later passes this check without any gate. Include both node types to close that gap.♻️ Proposed change
unguarded = [] for node in ast.walk(tree): - if not isinstance(node, ast.AsyncFunctionDef) or node.name.startswith("_"): + if not isinstance(node, ast.AsyncFunctionDef | ast.FunctionDef) or node.name.startswith("_"): continue🤖 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 `@tests/test_insights_access.py` around lines 396 - 410, Update the AST traversal in the authorization-coverage check around the unguarded handler collection to inspect both ast.AsyncFunctionDef and ast.FunctionDef nodes. Preserve the existing private-name and exempt-handler filtering, call detection, and assertion behavior for all route handlers.observal-server/tests/test_registry_namespace.py (1)
337-337: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueStrengthen the
owner_org_idpin.The entity is constructed with
owner_org_id=Noneat Line 337, so the assertion at Line 366 also passes iftransfer_ownershipnever reads or writes the field. To pin the "no organization inheritance on transfer" rule, start from a non-null value and assert the transfer does not replace it withtarget.org_id.♻️ Proposed test change
- owner_org_id=None, + owner_org_id=original_org_id,- assert entity.owner_org_id is None + assert entity.owner_org_id == original_org_id + assert entity.owner_org_id != target.org_idDefine
original_org_id = uuid.uuid4()before building_TransferEntity.Also applies to: 366-366
🤖 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 `@observal-server/tests/test_registry_namespace.py` at line 337, Strengthen the ownership-transfer test by defining a non-null original_org_id with uuid.uuid4() before constructing _TransferEntity, passing it as owner_org_id, and retaining the assertion that transfer preserves this value rather than replacing it with target.org_id.tests/test_team_visibility_migrations.py (1)
70-74: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRelax the exact-set assertion on migration operations.
op_calls == {...}fails whenever the migration stops using one of the four allowed operations, even though that change is safe. The property under test is "no operation outside the allowlist". Assert containment instead.♻️ Proposed change
- assert op_calls == {f"op.{name}" for name in ALLOWED_SCHEMA_OPS} + assert op_calls <= {f"op.{name}" for name in ALLOWED_SCHEMA_OPS} assert not any(call.startswith("sa.update") for call in calls) assert "op.execute" not in op_calls🤖 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 `@tests/test_team_visibility_migrations.py` around lines 70 - 74, Update the migration operation assertion in the test around _called_op_methods and COMPONENT_SOURCE_MIGRATION so it verifies op_calls is contained within the operations generated from ALLOWED_SCHEMA_OPS, rather than requiring an exact set match. Preserve the existing assertions rejecting sa.update calls and op.execute.tests/test_insights_agent_lookup.py (1)
14-19: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDuplicate
_resultSQLAlchemy result mock in two insights test modules. Both modules define the same helper because no shared test helper exists for the insights suite.
tests/test_insights_agent_lookup.py#L14-L19: remove the local_resultdefinition and import it from a sharedconftest.pyhelper.tests/test_insights_access.py#L32-L37: move this definition into the sharedconftest.pyhelper so both modules use one implementation.🤖 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 `@tests/test_insights_agent_lookup.py` around lines 14 - 19, Centralize the duplicated _result SQLAlchemy mock helper in the shared tests/conftest.py, moving the implementation from tests/test_insights_access.py lines 32-37. Remove the local definition from tests/test_insights_agent_lookup.py lines 14-19 and import/use the shared _result helper there; update both modules as needed so they resolve the same implementation.tests/test_sec009_component_source_ownership.py (1)
65-72: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRestore the previous dependency overrides instead of clearing them.
_apiinstalls overrides on the sharedmain.appand then callsapp.dependency_overrides.clear().clear()also removes overrides that other test modules or session-scoped fixtures installed on the same application object. Snapshot the mapping and restore it, so this helper only undoes its own changes.♻️ Proposed change
- app.dependency_overrides[get_db] = _db - if user is not None: - app.dependency_overrides[get_current_user] = lambda: user - try: - async with _make_client() as client: - yield client, sessions - finally: - app.dependency_overrides.clear() + previous = dict(app.dependency_overrides) + app.dependency_overrides[get_db] = _db + if user is not None: + app.dependency_overrides[get_current_user] = lambda: user + try: + async with _make_client() as client: + yield client, sessions + finally: + app.dependency_overrides.clear() + app.dependency_overrides.update(previous)🤖 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 `@tests/test_sec009_component_source_ownership.py` around lines 65 - 72, Update the _api helper’s dependency-override cleanup: snapshot app.dependency_overrides before installing get_db and get_current_user overrides, then restore that snapshot in finally instead of calling clear(). Preserve all pre-existing overrides while removing only the changes made by this helper.observal-server/api/routes/agent_versions.py (2)
109-114: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueConsider forwarding
prefer_user_idfor consistent name resolution.Other routes call the base loader with
prefer_user_id=current_user.id. This wrapper omits it. For a name-basedagent_id, the version routes can therefore resolve to a different agent than the CRUD routes resolve for the same caller and the same name. Visibility is still enforced, so this is a consistency gap rather than an access issue.♻️ Proposed change
async def _load_agent(db: AsyncSession, agent_id: str, current_user: User | None = None) -> Agent | None: """Thin wrapper that delegates to the route-level _load_agent.""" optic.trace("agent_id={}", agent_id) from api.routes.agent import _load_agent as _base_load - return await _base_load(db, agent_id, current_user=current_user) + return await _base_load( + db, + agent_id, + prefer_user_id=current_user.id if current_user else None, + current_user=current_user, + )🤖 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 `@observal-server/api/routes/agent_versions.py` around lines 109 - 114, Update the wrapper _load_agent to pass prefer_user_id=current_user.id to the delegated _base_load when current_user is available, while preserving None handling for anonymous callers and the existing visibility behavior.
45-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDelete the unused
auditcoroutine and its test patches.No production code calls
api.routes.agent_versions.audit; the test patches target an unused attribute. Remove both the dead function and those patches.🤖 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 `@observal-server/api/routes/agent_versions.py` around lines 45 - 49, Remove the unused audit coroutine from api.routes.agent_versions and delete the corresponding test patches that target agent_versions.audit. Ensure no references to this obsolete attribute remain.Source: Coding guidelines
observal-server/api/routes/agent/install.py (1)
120-217: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueExtract the repeated scope-and-visibility wrapper.
The same
apply_publish_scope(apply_visibility_filter(...))wrapper is repeated for five listing models. A small local helper removes the duplication and keeps the two filters in a fixed order at every call site.♻️ Proposed helper
def _scoped(stmt, model, current_user, target_team_id): return apply_publish_scope( apply_visibility_filter(stmt, model, current_user), model, target_team_id )- mcp_stmt = apply_publish_scope( - apply_visibility_filter( - select(McpListing).where(McpListing.id.in_(mcp_comp_ids)), McpListing, current_user - ), - McpListing, - component_target_team_id, - ) + mcp_stmt = _scoped( + select(McpListing).where(McpListing.id.in_(mcp_comp_ids)), + McpListing, + current_user, + component_target_team_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 `@observal-server/api/routes/agent/install.py` around lines 120 - 217, Introduce a local _scoped helper near the preload logic that applies apply_visibility_filter before apply_publish_scope with the supplied statement, model, current_user, and target team. Replace each repeated nested wrapper for the MCP, skill, hook, prompt, and sandbox listing queries with _scoped, preserving their existing filters, options, and execution behavior.observal-server/api/routes/dashboard.py (1)
334-339: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoffConsider a caller-scoped cache key instead of no caching.
Removing the shared cache is correct, because a single cache entry would serve team-private rows to non-members. The cost is that
component_leaderboardnow runs five grouped join queries plus up to five backfill queries on every request, with no caching. A cache key that includes the caller's visibility scope (for example anonymous, or the caller's team id set) restores caching without cross-tenant leakage.🤖 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 `@observal-server/api/routes/dashboard.py` around lines 334 - 339, Update component_leaderboard to use caller-scoped caching rather than disabling caching entirely. Build the cache key from the requested window and limit plus the caller’s visibility scope, distinguishing anonymous users and the caller’s team ID set, so private leaderboard rows cannot be shared across tenants. Preserve the existing query and backfill behavior on cache misses.observal-server/api/routes/mcp.py (2)
404-422: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
_reject_visibility_editsis duplicated across five route modules. The function body and the docstring are identical in all five files. Only the item type in the error detail differs. A future change to the visibility policy must be applied five times, and any missed copy weakens the guard on that route. Extract one shared helper that takes the item type, and import it in each route module.Suggested shared signature in
observal-server/api/routes/_component_archive.pyor a new_component_visibility.py:def reject_visibility_edits(listing, req, item_type: str) -> None: """Refuse teamspace or visibility changes sent to a draft update route.""" if req.team_id is not None and req.team_id != listing.team_id: raise HTTPException( status_code=400, detail="team_id cannot be changed here. A listing stays in the teamspace it was created under.", ) if req.visibility is not None and req.visibility != listing.visibility: raise HTTPException( status_code=400, detail=( "visibility cannot be changed here. " f"Use PATCH /api/v1/registry/{item_type}/{listing.id}/visibility." ), )
observal-server/api/routes/mcp.py#L404-L422: delete the local helper and callreject_visibility_edits(listing, req, "mcp")at line 440.observal-server/api/routes/prompt.py#L269-L287: delete the local helper and callreject_visibility_edits(listing, req, "prompt")at line 305.observal-server/api/routes/hook.py#L310-L328: delete the local helper and callreject_visibility_edits(listing, req, "hook")at line 346.observal-server/api/routes/sandbox.py#L249-L267: delete the local helper and callreject_visibility_edits(listing, req, "sandbox")at line 285.observal-server/api/routes/skill.py#L412-L430: delete the local helper and callreject_visibility_edits(listing, req, "skill")at line 448.Delete each local copy in the same change. As per coding guidelines, "Use a hard rewrite policy: do not add deprecation wrappers; update callers in the same change and delete dead code immediately."
🤖 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 `@observal-server/api/routes/mcp.py` around lines 404 - 422, Extract the duplicated _reject_visibility_edits logic into one shared reject_visibility_edits(listing, req, item_type: str) helper, preserving both validation behaviors and using item_type in the visibility endpoint detail. In observal-server/api/routes/mcp.py:404-422, prompt.py:269-287, hook.py:310-328, sandbox.py:249-267, and skill.py:412-430, delete each local helper and update their draft routes to call the shared helper with "mcp", "prompt", "hook", "sandbox", and "skill" respectively; do not add wrappers or retain dead copies.Source: Coding guidelines
308-317: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSimplify the fallback guard.
Line 311 already proves
listingis falsy before the reassignment. Line 313 then re-testsnot listinginside a combined condition.observal-server/api/routes/hook.py(lines 215-223) andobserval-server/api/routes/skill.py(lines 300-308) use a clearer two-step form for the same logic. Align this route with that form.♻️ Proposed refactor
if not listing: listing = await resolve_visible_listing(McpListing, listing_id, db, current_user) - if not listing or ( + if not listing: + raise HTTPException(status_code=404, detail="Listing not found or not approved") + if ( listing.status != ListingStatus.archived and get_effective_component_permission(listing, current_user) != "owner" ): raise HTTPException(status_code=404, detail="Listing not found or not approved")🤖 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 `@observal-server/api/routes/mcp.py` around lines 308 - 317, In the fallback logic around resolve_visible_listing, remove the redundant not listing check after reassignment, since the outer if already establishes the first lookup was falsy. Align the guard with the two-step pattern used in the hook and skill routes: re-resolve the listing, then separately raise 404 when the fallback result is absent or fails the archived/owner permission condition.observal-server/api/routes/skill.py (1)
227-233: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoffReconsider including
skill_md_contentin the keyword search.
skill_md_contentholds the full SKILL.md document. Line 240 also runs aCOUNT(*)over the same predicate. Every search request therefore scans full document text twice, and no index covers this column. Consider a dedicated full-text index or a materialized search column for the large fields.🤖 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 `@observal-server/api/routes/skill.py` around lines 227 - 233, Update the skill search query to avoid applying the keyword predicate directly to SkillVersion.skill_md_content, while preserving searches across the smaller indexed fields. If full-document search is required, route it through an appropriate dedicated full-text index or materialized search column and ensure the matching COUNT(*) predicate uses the same optimized search path.observal-server/api/deps.py (1)
468-477: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the duplicated local
or_imports.The module already imports
or_at module scope; Line 379 uses it. The localfrom sqlalchemy import or_statements insideapply_publish_scopeandapply_visibility_filteradd no value.♻️ Proposed cleanup
def apply_publish_scope(stmt, model, target_team_id): """Limit components to what an agent published for one target can contain.""" - from sqlalchemy import or_ - if not hasattr(model, "is_private") or not hasattr(model, "team_id"):Public listings are visible to everyone. Team-private listings require a membership row for the requesting user, while legacy private user listings remain visible only to their creator. """ - from sqlalchemy import or_ - from models.team import TeamMembershipAlso applies to: 508-508
🤖 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 `@observal-server/api/deps.py` around lines 468 - 477, Remove the redundant local sqlalchemy.or_ imports from apply_publish_scope and apply_visibility_filter, and rely on the existing module-level or_ import while preserving both functions’ query behavior.observal-server/services/agent_resolver.py (1)
15-15: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider moving the scope helpers out of the api layer.
services/agent_resolver.pynow imports fromapi.depsat module scope. That inverts the usual dependency direction and creates a cycle risk, becauseapi/routes/agent/helpers.pyimports bothapi.depsand this service.apply_visibility_filterandapply_publish_scopeare pure query builders over models. A neutral module such asservices/visibility.pywould let both layers import them safely.🤖 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 `@observal-server/services/agent_resolver.py` at line 15, Move apply_publish_scope and apply_visibility_filter from api.deps into a neutral services visibility module, then update agent_resolver and api/routes/agent/helpers.py to import them from that module. Remove the api.deps dependency from services/agent_resolver.py while preserving both helpers’ query-building behavior and signatures.
🤖 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 `@docs/cli/registry.md`:
- Around line 35-49: Update the Teamspace visibility documentation to cover the
published skill visibility change endpoint, including PATCH
/api/v1/registry/skill/{listing_id}/visibility and its visibility values public
or team; otherwise remove the claim that team owners and reviewers can change
visibility after publication.
In `@observal_cli/cmd_agent.py`:
- Around line 1021-1023: Update the agent publish flow around add_publish_target
and the update operation near the visibility-only PATCH so --team is not
silently ignored: either pass the normalized team_id through a dedicated update
operation, or explicitly reject --team with --update. Normalize and validate
visibility before issuing the PATCH request, preserving the existing non-update
publish behavior.
In `@observal-server/alembic/versions/018_team_publishing.py`:
- Around line 80-103: Extend _assert_no_team_owned_rows to also detect agents
rows where is_private is true and team_id is null, alongside the existing
team-owned row checks. Include those personal-private agent counts in the owned
details and prevent downgrade before agents.is_private is dropped, while
preserving the current handling and error message for all guarded rows.
In `@observal-server/api/deps.py`:
- Around line 526-531: Update the creator_column selection in the authenticated
user filtering logic to avoid boolean evaluation of SQLAlchemy attributes:
choose model.created_by only when getattr(model, "submitted_by", None) returns
None, otherwise retain submitted_by. Preserve the existing own condition and
current_user.id comparison behavior.
In `@observal-server/api/routes/feedback.py`:
- Around line 47-54: Reduce query fan-out in `_visible_listing_by_id` by
resolving the listing type from the stored `Feedback.listing_type` for the given
`listing_id`, then invoke `_visible_listing` only for that model instead of
iterating through every `LISTING_MODELS` entry. Preserve the existing visibility
checks and 404 behavior when no matching feedback or listing exists.
In `@observal-server/api/routes/insights.py`:
- Around line 421-424: In export_report_html, call _authorize_report_agent
immediately after confirming the report exists and before checking
report.status. Keep the existing 400 response for incomplete reports, ensuring
unauthorized callers cannot learn the report’s completion state.
In `@observal-server/api/routes/preview.py`:
- Around line 122-151: Update the component-resolution flow around
`_visible_map` and the four
`McpListing`/`SkillListing`/`HookListing`/`PromptListing` lookups to enforce
approval authorization in addition to privacy visibility. Public components with
pending or rejected status must resolve only for their submitter, co-authors,
admins, or reviewers; otherwise require approved status. Preserve the existing
404 behavior using the `requested` and `resolved` sets so unauthorized
components are not exposed.
In `@observal-server/api/routes/registry.py`:
- Around line 198-217: Update the agent visibility validation branch in the
registry route to validate component references from every approved
AgentVersion, not only listing.components from the latest version. Iterate
through the approved versions, apply validate_component_ids with the existing
visibility and target-team parameters, and raise the same 409 conflict when any
version has errors.
In `@tests/test_draft_workflow.py`:
- Around line 757-771: Add an explicit assertion in
test_global_reviewer_auto_approves_without_membership that the team membership
lookup mock is not called during submission, while preserving the existing
approval and reviewed_by assertions.
In `@web/src/components/registry/submit-component-dialog.tsx`:
- Around line 177-180: Update the dialog’s reset() function to call
setTeamId("") and setVisibility("public") so closing after a team-only
submission restores the default publication target. Also synchronize these state
values from the current editItem when the dialog opens, ensuring reused
edit-dialog instances reflect the active item.
In `@web/src/pages/registry/agents/builder.tsx`:
- Around line 100-101: Update the local draft save payload and
restoreLocalDraft() to persist and restore the team publication state using the
existing teamId and visibility symbols, storing them as team_id and visibility
alongside the other draft fields. Ensure restored values repopulate both state
variables so team-only drafts retain their intended visibility after refresh.
- Around line 258-262: Update the dependency list of the effect that validates
the publication payload containing allComponents, teamId, and visibility to
include teamId and visibility alongside selectedComponents, ensuring validation
reruns whenever the publication target changes.
In `@web/src/pages/registry/components/index.tsx`:
- Around line 220-224: Preserve team filters for discoverable teams by using a
stable route/API-supported team value instead of relying only on
membership-aware useTeams(). Update web/src/pages/registry/components/index.tsx
lines 220-224 and web/src/pages/registry/agents/index.tsx lines 426-460 to
resolve and apply that value, and update web/src/pages/registry/teamspaces.tsx
lines 100-102 to pass the same stable filter value through the navigation path.
In `@web/src/pages/registry/teamspaces.tsx`:
- Around line 100-102: Update the “Browse registry” control in the teamspaces
page to render the Link through Button using the existing asChild pattern,
rather than nesting a Button inside the Link; preserve the current destination,
search parameter, variant, and size.
---
Nitpick comments:
In `@observal-server/api/deps.py`:
- Around line 468-477: Remove the redundant local sqlalchemy.or_ imports from
apply_publish_scope and apply_visibility_filter, and rely on the existing
module-level or_ import while preserving both functions’ query behavior.
In `@observal-server/api/routes/agent_versions.py`:
- Around line 109-114: Update the wrapper _load_agent to pass
prefer_user_id=current_user.id to the delegated _base_load when current_user is
available, while preserving None handling for anonymous callers and the existing
visibility behavior.
- Around line 45-49: Remove the unused audit coroutine from
api.routes.agent_versions and delete the corresponding test patches that target
agent_versions.audit. Ensure no references to this obsolete attribute remain.
In `@observal-server/api/routes/agent/install.py`:
- Around line 120-217: Introduce a local _scoped helper near the preload logic
that applies apply_visibility_filter before apply_publish_scope with the
supplied statement, model, current_user, and target team. Replace each repeated
nested wrapper for the MCP, skill, hook, prompt, and sandbox listing queries
with _scoped, preserving their existing filters, options, and execution
behavior.
In `@observal-server/api/routes/dashboard.py`:
- Around line 334-339: Update component_leaderboard to use caller-scoped caching
rather than disabling caching entirely. Build the cache key from the requested
window and limit plus the caller’s visibility scope, distinguishing anonymous
users and the caller’s team ID set, so private leaderboard rows cannot be shared
across tenants. Preserve the existing query and backfill behavior on cache
misses.
In `@observal-server/api/routes/mcp.py`:
- Around line 404-422: Extract the duplicated _reject_visibility_edits logic
into one shared reject_visibility_edits(listing, req, item_type: str) helper,
preserving both validation behaviors and using item_type in the visibility
endpoint detail. In observal-server/api/routes/mcp.py:404-422,
prompt.py:269-287, hook.py:310-328, sandbox.py:249-267, and skill.py:412-430,
delete each local helper and update their draft routes to call the shared helper
with "mcp", "prompt", "hook", "sandbox", and "skill" respectively; do not add
wrappers or retain dead copies.
- Around line 308-317: In the fallback logic around resolve_visible_listing,
remove the redundant not listing check after reassignment, since the outer if
already establishes the first lookup was falsy. Align the guard with the
two-step pattern used in the hook and skill routes: re-resolve the listing, then
separately raise 404 when the fallback result is absent or fails the
archived/owner permission condition.
In `@observal-server/api/routes/skill.py`:
- Around line 227-233: Update the skill search query to avoid applying the
keyword predicate directly to SkillVersion.skill_md_content, while preserving
searches across the smaller indexed fields. If full-document search is required,
route it through an appropriate dedicated full-text index or materialized search
column and ensure the matching COUNT(*) predicate uses the same optimized search
path.
In `@observal-server/services/agent_resolver.py`:
- Line 15: Move apply_publish_scope and apply_visibility_filter from api.deps
into a neutral services visibility module, then update agent_resolver and
api/routes/agent/helpers.py to import them from that module. Remove the api.deps
dependency from services/agent_resolver.py while preserving both helpers’
query-building behavior and signatures.
In `@observal-server/tests/test_registry_namespace.py`:
- Line 337: Strengthen the ownership-transfer test by defining a non-null
original_org_id with uuid.uuid4() before constructing _TransferEntity, passing
it as owner_org_id, and retaining the assertion that transfer preserves this
value rather than replacing it with target.org_id.
In `@tests/test_component_update_visibility.py`:
- Around line 73-152: The duplicated listing mocks must use one shared builder.
In tests/test_listing_detail_access.py lines 62-81, move the local _listing_mock
into a shared test helper or conftest.py while preserving the is_private,
team_id, and visibility parameters; in tests/test_component_update_visibility.py
lines 73-152, remove the local _listing_mock and import/use that shared builder.
Keep both tests’ existing call behavior unchanged.
In `@tests/test_dashboard_leaderboard.py`:
- Around line 156-160: Update the download-query assertions around
_LISTING_TABLES and _download_queries(db) to pair each table with the statement
whose SQL contains that table name, rather than relying on positional zip order.
Apply the same table-name lookup to the related assertions around the second
download-query block, while preserving the existing privacy checks.
In `@tests/test_draft_workflow.py`:
- Around line 362-367: Remove the redundant db rebinding in the affected tests:
keep the _sequenced_db result assigned to db, then unpack only app and the
unused return value from _app_with while passing db as its argument. Apply this
consistently to the repeated cases identified in the comment, preserving the
existing mock object.
In `@tests/test_insights_access.py`:
- Around line 396-410: Update the AST traversal in the authorization-coverage
check around the unguarded handler collection to inspect both
ast.AsyncFunctionDef and ast.FunctionDef nodes. Preserve the existing
private-name and exempt-handler filtering, call detection, and assertion
behavior for all route handlers.
In `@tests/test_insights_agent_lookup.py`:
- Around line 14-19: Centralize the duplicated _result SQLAlchemy mock helper in
the shared tests/conftest.py, moving the implementation from
tests/test_insights_access.py lines 32-37. Remove the local definition from
tests/test_insights_agent_lookup.py lines 14-19 and import/use the shared
_result helper there; update both modules as needed so they resolve the same
implementation.
In `@tests/test_registry_types.py`:
- Around line 751-791: Parametrize all four tests with the route cases from
_list_endpoint_cases() so each route executes as an independent pytest case with
a descriptive id. Remove the internal for loops and accept the unpacked case
values as test arguments. Because parametrization is evaluated during
collection, move _list_endpoint_cases() route imports to module scope or provide
the cases through a fixture if collection-time import order requires it.
- Around line 741-748: Move the full `_membership_predicate` helper from
tests/test_registry_types.py:741-748 into a shared test module, preserving the
is_private and team_id conditions, and update callers there to use it. Remove
the duplicate helper from tests/test_dashboard_leaderboard.py:44-50 and import
the shared implementation; do not retain a same-named helper that only asserts
the EXISTS fragment.
In `@tests/test_sec009_component_source_ownership.py`:
- Around line 65-72: Update the _api helper’s dependency-override cleanup:
snapshot app.dependency_overrides before installing get_db and get_current_user
overrides, then restore that snapshot in finally instead of calling clear().
Preserve all pre-existing overrides while removing only the changes made by this
helper.
In `@tests/test_team_publishing.py`:
- Around line 73-115: Extend TestResolvePublishTargetAutoApprove with an async
regression test that supplies a team_id for a user who has no team membership,
invokes resolve_publish_target, and asserts the call raises HTTP 403. Reuse the
existing _mock_db, _user, and team_id setup helpers and preserve the current
matrix tests.
In `@tests/test_team_visibility_migrations.py`:
- Around line 70-74: Update the migration operation assertion in the test around
_called_op_methods and COMPONENT_SOURCE_MIGRATION so it verifies op_calls is
contained within the operations generated from ALLOWED_SCHEMA_OPS, rather than
requiring an exact set match. Preserve the existing assertions rejecting
sa.update calls and op.execute.
In `@web/src/components/registry/agent-edit-form.tsx`:
- Around line 64-65: Move the exported AgentDetail type from the registry detail
page into the shared `@/lib/types` module, then import and reuse it in both
detail.tsx and the agent edit form. Remove the duplicate local API type
definitions while preserving the existing team_id and visibility fields.
In `@web/src/pages/registry/agents/detail.tsx`:
- Around line 168-170: Move the AgentDetail API response type from the detail
page into the shared registry types module, exporting it alongside RegistryItem
with the existing team_id, visibility, and is_private fields. Update the detail
page to import and use the shared agent detail type, removing the duplicated
local definition.
🪄 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: CHILL
Plan: Pro Plus
Run ID: 38e08426-8335-4ed0-9130-80d867270864
⛔ Files ignored due to path filters (2)
observal-server/uv.lockis excluded by!**/*.lockuv.lockis excluded by!**/*.lock
📒 Files selected for processing (97)
docs/cli/agent.mddocs/cli/registry.mdobserval-server/alembic/versions/018_team_publishing.pyobserval-server/api/deps.pyobserval-server/api/routes/_component_archive.pyobserval-server/api/routes/admin/org.pyobserval-server/api/routes/agent/crud.pyobserval-server/api/routes/agent/draft.pyobserval-server/api/routes/agent/helpers.pyobserval-server/api/routes/agent/install.pyobserval-server/api/routes/agent_versions.pyobserval-server/api/routes/bulk.pyobserval-server/api/routes/co_authors.pyobserval-server/api/routes/component_source.pyobserval-server/api/routes/component_versions.pyobserval-server/api/routes/dashboard.pyobserval-server/api/routes/exec_dashboard.pyobserval-server/api/routes/feedback.pyobserval-server/api/routes/hook.pyobserval-server/api/routes/insights.pyobserval-server/api/routes/mcp.pyobserval-server/api/routes/preview.pyobserval-server/api/routes/prompt.pyobserval-server/api/routes/registry.pyobserval-server/api/routes/sandbox.pyobserval-server/api/routes/skill.pyobserval-server/models/agent.pyobserval-server/models/component_source.pyobserval-server/models/hook.pyobserval-server/models/mcp.pyobserval-server/models/prompt.pyobserval-server/models/sandbox.pyobserval-server/models/skill.pyobserval-server/pyproject.tomlobserval-server/schemas/agent.pyobserval-server/schemas/component_source.pyobserval-server/schemas/constants.pyobserval-server/schemas/hook.pyobserval-server/schemas/mcp.pyobserval-server/schemas/prompt.pyobserval-server/schemas/sandbox.pyobserval-server/schemas/skill.pyobserval-server/services/agent_builder.pyobserval-server/services/agent_resolver.pyobserval-server/services/insights/self_learn.pyobserval-server/services/ownership.pyobserval-server/services/teamspace.pyobserval-server/tests/test_agent_pull.pyobserval-server/tests/test_component_versions_api.pyobserval-server/tests/test_registry_namespace.pyobserval-server/tests/test_teamspace.pyobserval_cli/client.pyobserval_cli/cmd_agent.pyobserval_cli/cmd_hook.pyobserval_cli/cmd_mcp.pyobserval_cli/cmd_migrate.pyobserval_cli/cmd_prompt.pyobserval_cli/cmd_sandbox.pyobserval_cli/cmd_skill.pyobserval_cli/skills/observal-agents/SKILL.mdobserval_cli/skills/observal-registry/SKILL.mdobserval_cli/skills/observal/SKILL.mdpackages/pi-extension/package.jsonpyproject.tomltests/test_agent_composition.pytests/test_co_authors.pytests/test_component_update_visibility.pytests/test_dashboard_leaderboard.pytests/test_draft_workflow.pytests/test_insights_access.pytests/test_insights_agent_lookup.pytests/test_listing_detail_access.pytests/test_listing_version_flush.pytests/test_preview_config.pytests/test_pull_and_agent_cli.pytests/test_registry_types.pytests/test_registry_visibility.pytests/test_sec009_component_source_ownership.pytests/test_sec009_org_scoping.pytests/test_team_publishing.pytests/test_team_visibility_migrations.pyweb/package.jsonweb/src/components/registry/agent-edit-form.tsxweb/src/components/registry/component-picker.tsxweb/src/components/registry/submit-component-dialog.tsxweb/src/hooks/use-api.tsweb/src/hooks/use-registry-api.tsweb/src/lib/api.tsweb/src/lib/types/registry.tsweb/src/pages/registry/agents/builder.tsxweb/src/pages/registry/agents/detail.tsxweb/src/pages/registry/agents/index.tsxweb/src/pages/registry/components/detail.tsxweb/src/pages/registry/components/index.tsxweb/src/pages/registry/teamspaces.tsxweb/src/routes/_authed/agents/index.tsxweb/src/routes/_authed/components/index.tsx
💤 Files with no reviewable changes (5)
- observal-server/api/routes/bulk.py
- observal-server/api/routes/admin/org.py
- observal-server/services/insights/self_learn.py
- observal-server/services/ownership.py
- observal-server/api/routes/exec_dashboard.py
| creator_column = getattr(model, "submitted_by", None) or getattr(model, "created_by", None) | ||
| if hasattr(model, "team_id"): | ||
| own = (model.is_private == True) & model.team_id.is_(None) # noqa: E712 | ||
| else: | ||
| own = model.is_private == True # noqa: E712 | ||
| own = own & (creator_column == current_user.id) if creator_column is not None else False |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check truthiness of a SQLAlchemy InstrumentedAttribute and list creator columns per registry model.
set -euo pipefail
python - <<'PY'
import sqlalchemy
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
print("sqlalchemy", sqlalchemy.__version__)
class Base(DeclarativeBase):
pass
class T(Base):
__tablename__ = "t"
id: Mapped[int] = mapped_column(primary_key=True)
submitted_by: Mapped[int]
try:
print("bool(T.submitted_by) =", bool(T.submitted_by))
except Exception as exc:
print("raised:", type(exc).__name__, exc)
PY
rg -nP --type=py -g '**/models/*.py' '\b(submitted_by|created_by)\s*:\s*Mapped'Repository: Observal/Observal
Length of output: 278
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- target function ---'
sed -n '480,590p' observal-server/api/deps.py
printf '%s\n' '--- creator-field declarations ---'
rg -n --type=py '\b(submitted_by|created_by)\b' observal-server | head -200
printf '%s\n' '--- dependency declarations ---'
rg -n --glob 'requirements*.txt' --glob 'pyproject.toml' --glob 'uv.lock' --glob 'poetry.lock' 'SQLAlchemy|sqlalchemy' .
printf '%s\n' '--- usages of the containing dependency ---'
rg -n --type=py 'get_current_user|current_user|creator_column|submitted_by' observal-server/api observal-server | head -250Repository: Observal/Observal
Length of output: 50373
🌐 Web query:
SQLAlchemy 2.0 InstrumentedAttribute bool() TypeError Boolean value of this clause is not defined
💡 Result:
The TypeError: Boolean value of this clause is not defined error in SQLAlchemy occurs when a SQL expression object (such as an InstrumentedAttribute or a binary expression) is evaluated in a Python boolean context (e.g., in an if statement, or with bool) [1][2][3]. Because SQLAlchemy objects are designed to construct SQL strings rather than represent truthy or falsy Python values, the library explicitly raises this error to prevent ambiguous behavior [1][3]. Common Scenarios and Solutions: 1. Testing for Existence of a Filter or Column: If you are trying to check if a filter or attribute exists in an if statement, do not check the object itself [1]. Instead, check if the variable is not None [1]. Incorrect: if my_filter: Correct: if my_filter is not None: [1] 2. Hybrid Properties: When using @hybrid_property, you must define both a Python-side implementation and a SQL-side expression [2][3]. The Python-side implementation runs in standard Python and should return a regular Python value (like True, False, or None), while the expression-side handles the SQL translation [2][3]. The error often happens when the expression-side logic is unintentionally triggered in a Python context where it cannot be evaluated [2][3]. 3. Mismatched Usage in Mapped Annotations: In some versions of SQLAlchemy 2.0 (specifically versions prior to 2.0.51), inappropriate use of objects inside Mapped annotations could trigger this error [4][5]. Ensure you are using the correct type annotations for your columns [4][5]. If you encounter this, verify you are on a recent version of SQLAlchemy, as these errors have been improved for clarity [4][5]. In summary, the error is a safeguard against misuse [1]. Whenever you see it, look for where a SQL-construct (like a Column, QueryableAttribute, or BinaryExpression) is being evaluated as if it were a simple Python boolean [1][2].
Citations:
- 1: https://stackoverflow.com/questions/78616567/sqlalchemy-checking-if-a-filter-has-been-set-causes-typeerror-boolean-value-o
- 2: https://stackoverflow.com/questions/55690796/sqlalchemy-typeerror-boolean-value-of-this-clause-is-not-defined
- 3: https://stackoverflow.com/questions/54924977/sqlalchemy-boolean-expression-for-hybrid-property
- 4: https://www.sqlalchemy.org/changelog/CHANGES_2_0_51
- 5: https://github.com/sqlalchemy/sqlalchemy/blob/0138954e/doc/build/changelog/changelog_20.rst
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- apply_visibility_filter callers ---'
rg -n -B3 -A8 --type=py 'apply_visibility_filter\(' observal-server
printf '%s\n' '--- model class declarations and registry models ---'
rg -n --type=py '^(class |from models\.)|__tablename__' observal-server/models/{agent,hook,mcp,prompt,sandbox,skill,component_bundle,submission}.py
printf '%s\n' '--- exact SQLAlchemy pins ---'
awk '
/name = "sqlalchemy"/ {show=1; n=0}
show {print; n++}
show && n >= 8 {show=0}
' ob ser val-server/uv.lock 2>/dev/null || true
awk '
/name = "sqlalchemy"/ {show=1; n=0}
show {print; n++}
show && n >= 8 {show=0}
' uv.lockRepository: Observal/Observal
Length of output: 27478
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import ast
from pathlib import Path
root = Path("observal-server/models")
fields = {"submitted_by", "created_by", "is_private", "team_id"}
for path in sorted(root.glob("*.py")):
tree = ast.parse(path.read_text(), filename=str(path))
for node in tree.body:
if not isinstance(node, ast.ClassDef):
continue
names = set()
for item in node.body:
if isinstance(item, (ast.AnnAssign, ast.Assign)):
targets = item.targets if isinstance(item, ast.Assign) else [item.target]
for target in targets:
if isinstance(target, ast.Name):
names.add(target.id)
present = sorted(fields & names)
if {"is_private"} <= names:
creator = "submitted_by" if "submitted_by" in names else "created_by" if "created_by" in names else "<none>"
print(f"{path}:{node.lineno} {node.name}: creator={creator}, fields={','.join(present)}")
PYRepository: Observal/Observal
Length of output: 804
Select creator_column without truthiness evaluation
For authenticated non-privileged users, registry models with submitted_by raise TypeError: Boolean value of this clause is not defined because or evaluates the SQLAlchemy InstrumentedAttribute. Select the fallback only when the first attribute is None.
🛠️ Proposed fix
- creator_column = getattr(model, "submitted_by", None) or getattr(model, "created_by", None)
+ creator_column = getattr(model, "submitted_by", None)
+ if creator_column is None:
+ creator_column = getattr(model, "created_by", None)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| creator_column = getattr(model, "submitted_by", None) or getattr(model, "created_by", None) | |
| if hasattr(model, "team_id"): | |
| own = (model.is_private == True) & model.team_id.is_(None) # noqa: E712 | |
| else: | |
| own = model.is_private == True # noqa: E712 | |
| own = own & (creator_column == current_user.id) if creator_column is not None else False | |
| creator_column = getattr(model, "submitted_by", None) | |
| if creator_column is None: | |
| creator_column = getattr(model, "created_by", None) | |
| if hasattr(model, "team_id"): | |
| own = (model.is_private == True) & model.team_id.is_(None) # noqa: E712 | |
| else: | |
| own = model.is_private == True # noqa: E712 | |
| own = own & (creator_column == current_user.id) if creator_column is not None else False |
🤖 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 `@observal-server/api/deps.py` around lines 526 - 531, Update the
creator_column selection in the authenticated user filtering logic to avoid
boolean evaluation of SQLAlchemy attributes: choose model.created_by only when
getattr(model, "submitted_by", None) returns None, otherwise retain
submitted_by. Preserve the existing own condition and current_user.id comparison
behavior.
…m global reviewers Team-private items were reviewable only by a global reviewer who was not in the team, so private content was routed to outsiders and the team was blocked until one acted. The reviewer team role granted no review capability at all. Global reviewers no longer see team-private listings. Admins and super_admins still do. can_see_private_listings in api/deps.py names that rule, and is kept separate from may_view_unapproved, which answers a status question and keeps its reviewer arm so the public queue still works. The review queue is now capability scoped. Team owners and team reviewers review their own teamspace's private items. Global reviewers keep the public catalog, including team-to-public flips. A team owner cannot approve a public item in their own namespace, which is the escalation team_role_self_publishes closes at publish time. review_scope and can_review in services/teamspace.py answer once, so the queue and the actions cannot drift apart. Publishing into a teamspace you do not belong to is now an admin capability. A global reviewer cannot read another team's private listings, so allowing one to publish there would create a listing its own author can no longer see. Web gains a teamspace detail route with Agents, Components, Members, and a Review tab shown only to team owners and reviewers, since the admin review page stays gated to global roles. Flipping a listing to public now asks for confirmation and says it leaves the catalog until a reviewer approves it. Co-Authored-By: Hari Srinivasan <harisrini21@gmail.com>
58db12e to
93630aa
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (6)
web/src/pages/registry/teamspace-detail.tsx (2)
627-636: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueLeaving a teamspace runs without confirmation.
The Leave button calls
leaveTeam.mutatedirectly and then navigates away. A team owner can also lose access this way. Confirm the action first, as the Delete button does.🤖 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 `@web/src/pages/registry/teamspace-detail.tsx` around lines 627 - 636, Update the Leave button handler in the teamspace detail component to show the same confirmation flow used by the Delete button before invoking leaveTeam.mutate. Only proceed with leaving and navigating to /teamspaces after confirmation, including for team owners.
344-356: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMember removal runs without confirmation.
A single click on the trash button removes the member. The action is destructive and has no undo. The same page already confirms teamspace deletion with
AlertDialogat lines 698-716. Add the same confirmation step beforeremoveMember.mutate.🤖 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 `@web/src/pages/registry/teamspace-detail.tsx` around lines 344 - 356, Update the member removal button’s onClick flow in the canManageMembers block to show an AlertDialog confirmation before invoking removeMember.mutate(member.id). Reuse the page’s existing teamspace deletion AlertDialog pattern and only perform the mutation after the user confirms.web/src/pages/registry/agents/detail.tsx (1)
721-746: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the shared visibility-change flow. Both detail pages carry byte-identical copies of
readReturnedToReviewandapplyVisibility, plus the same confirmation dialog copy. The shared root cause is a copied flow rather than one extracted module. A future change to the review-queue messaging or the response contract must be applied twice, and the copies will diverge.
web/src/pages/registry/agents/detail.tsx#L721-L746: replace the localapplyVisibilityand thereadReturnedToReviewhelper at lines 122-129 with the shared implementation, passing the display name andtype: "agents".web/src/pages/registry/components/detail.tsx#L170-L195: replace the localapplyVisibilityand thereadReturnedToReviewhelper at lines 63-70 with the same shared implementation, passing the display name and the currenttype.A shared hook, for example
useVisibilityChange({ type, id, displayName })returning{ applyVisibility, isPending }, keeps the dialog markup in each page while centralizing the response parsing and the toast text.🤖 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 `@web/src/pages/registry/agents/detail.tsx` around lines 721 - 746, Extract the duplicated readReturnedToReview and applyVisibility flow into a shared useVisibilityChange implementation returning applyVisibility and isPending; preserve the existing response parsing and toast behavior. Update web/src/pages/registry/agents/detail.tsx lines 721-746 to use the shared implementation with type "agents" and the display name, and update web/src/pages/registry/components/detail.tsx lines 170-195 with the shared implementation using the current type and display name; keep the confirmation dialog markup in each page.web/src/routes/_authed/teamspaces.$handle.tsx (1)
15-22: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
COMPONENT_TYPESis duplicated and can drift.
web/src/pages/registry/teamspace-detail.tsxdeclares its ownCOMPONENT_TYPESlist at lines 70-76 for the component tabs. If a new registry type is added to that list only,validateSearchdrops thetypesearch parameter and the selected tab is not preserved in the URL. The failure is silent. Export one list and derive the validation set from it.🤖 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 `@web/src/routes/_authed/teamspaces`.$handle.tsx around lines 15 - 22, Remove the duplicated COMPONENT_TYPES declaration from the route and reuse the exported component-type list from teamspace-detail.tsx in validateSearch. Ensure the validation set is derived from that shared list so newly supported registry types preserve the type search parameter consistently.web/src/pages/registry/components/detail.tsx (1)
137-138: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the redundant
visibilitycast.
RegistryItemdeclaresvisibilityandis_privateinweb/src/lib/types/registry.ts. The fallback already produces a string accepted byPickerSelect.🤖 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 `@web/src/pages/registry/components/detail.tsx` around lines 137 - 138, Remove the unnecessary string cast from the currentVisibility assignment in the detail component. Use item?.visibility directly with the existing nullish fallback based on item?.is_private, preserving the string value passed to PickerSelect.Source: Coding guidelines
web/src/routes/_authed/teamspaces.tsx (1)
14-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse a file-based index route for
/teamspaces.Make
teamspaces.tsxrender only<Outlet />, and moveTeamspacesPagetoteamspaces.index.tsx. This uses TanStack Router’s route matching instead of manuallocation.pathnamematching. A configuredbasepathis stripped fromuseLocation().pathname.🤖 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 `@web/src/routes/_authed/teamspaces.tsx` around lines 14 - 23, Update TeamspacesRoute in teamspaces.tsx to render only Outlet, remove the manual useLocation/pathname matching, and move the TeamspacesPage rendering into a new teamspaces.index.tsx file-based index route so /teamspaces is handled by TanStack Router.Source: Coding guidelines
🤖 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 `@observal-server/api/routes/review.py`:
- Around line 93-105: Apply a consistent non-disclosure policy across the review
write routes: in _authorize_item, avoid revealing visibility details by
returning 404 for team-private items or using a visibility-neutral message; at
observal-server/api/routes/review.py:882-894, remove listing.name and report
only that the bundle contains an out-of-scope member; at
observal-server/api/routes/review.py:1054-1056, replace skill.name with the
caller-supplied sid in the refusal message.
- Around line 93-105: Update _authorize_item to avoid disclosing an unauthorized
entity’s visibility: use the same non-revealing 404 behavior as get_review for
out-of-scope private items, or replace both visibility-specific 403 details with
one message that does not identify whether the item is private or public. Keep
authorization success and in-scope denial behavior unchanged.
In `@web/src/hooks/use-teams-api.ts`:
- Around line 39-46: Update the return object in useTeamsApi to treat an active
background refetch without a team as loading by setting isLoading from
query.isLoading or query.isFetching && !team, and require !query.isFetching
alongside the existing notFound conditions.
---
Nitpick comments:
In `@web/src/pages/registry/agents/detail.tsx`:
- Around line 721-746: Extract the duplicated readReturnedToReview and
applyVisibility flow into a shared useVisibilityChange implementation returning
applyVisibility and isPending; preserve the existing response parsing and toast
behavior. Update web/src/pages/registry/agents/detail.tsx lines 721-746 to use
the shared implementation with type "agents" and the display name, and update
web/src/pages/registry/components/detail.tsx lines 170-195 with the shared
implementation using the current type and display name; keep the confirmation
dialog markup in each page.
In `@web/src/pages/registry/components/detail.tsx`:
- Around line 137-138: Remove the unnecessary string cast from the
currentVisibility assignment in the detail component. Use item?.visibility
directly with the existing nullish fallback based on item?.is_private,
preserving the string value passed to PickerSelect.
In `@web/src/pages/registry/teamspace-detail.tsx`:
- Around line 627-636: Update the Leave button handler in the teamspace detail
component to show the same confirmation flow used by the Delete button before
invoking leaveTeam.mutate. Only proceed with leaving and navigating to
/teamspaces after confirmation, including for team owners.
- Around line 344-356: Update the member removal button’s onClick flow in the
canManageMembers block to show an AlertDialog confirmation before invoking
removeMember.mutate(member.id). Reuse the page’s existing teamspace deletion
AlertDialog pattern and only perform the mutation after the user confirms.
In `@web/src/routes/_authed/teamspaces`.$handle.tsx:
- Around line 15-22: Remove the duplicated COMPONENT_TYPES declaration from the
route and reuse the exported component-type list from teamspace-detail.tsx in
validateSearch. Ensure the validation set is derived from that shared list so
newly supported registry types preserve the type search parameter consistently.
In `@web/src/routes/_authed/teamspaces.tsx`:
- Around line 14-23: Update TeamspacesRoute in teamspaces.tsx to render only
Outlet, remove the manual useLocation/pathname matching, and move the
TeamspacesPage rendering into a new teamspaces.index.tsx file-based index route
so /teamspaces is handled by TanStack Router.
🪄 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: CHILL
Plan: Pro Plus
Run ID: 8b03225d-177e-40d6-ad4e-89dc85c643a8
📒 Files selected for processing (23)
observal-server/api/deps.pyobserval-server/api/routes/registry.pyobserval-server/api/routes/review.pyobserval-server/services/teamspace.pytests/test_dashboard_leaderboard.pytests/test_insights_access.pytests/test_listing_detail_access.pytests/test_registry_types.pytests/test_registry_visibility.pytests/test_sec009_org_scoping.pytests/test_team_publishing.pytests/test_team_review.pytests/test_team_visibility_migrations.pyweb/src/hooks/use-review-api.tsweb/src/hooks/use-teams-api.tsweb/src/lib/api.tsweb/src/pages/registry/agents/detail.tsxweb/src/pages/registry/components/detail.tsxweb/src/pages/registry/teamspace-detail.tsxweb/src/pages/registry/teamspaces.tsxweb/src/routeTree.gen.tsweb/src/routes/_authed/teamspaces.$handle.tsxweb/src/routes/_authed/teamspaces.tsx
🚧 Files skipped from review as they are similar to previous changes (7)
- tests/test_team_visibility_migrations.py
- web/src/lib/api.ts
- tests/test_dashboard_leaderboard.py
- tests/test_team_publishing.py
- tests/test_insights_access.py
- tests/test_registry_visibility.py
- tests/test_sec009_org_scoping.py
| def _authorize_item(entity, scope: ReviewScope) -> None: | ||
| """Authorize one item from its own visibility and teamspace.""" | ||
| if can_review(entity, scope): | ||
| return | ||
| if getattr(entity, "is_private", False): | ||
| raise HTTPException( | ||
| status_code=403, | ||
| detail="Team-private items are reviewed by their teamspace's owners and reviewers", | ||
| ) | ||
| raise HTTPException( | ||
| status_code=403, | ||
| detail="Public items are reviewed by global reviewers, not by teamspace roles", | ||
| ) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Review write routes describe out-of-scope items in the 403 detail. The read routes hide an out-of-scope item: get_review returns 404 at line 545 and get_related_skills returns 404 at line 969, both to avoid confirming that a team-private item exists. The three write paths do the opposite and return a 403 that describes the item, so a caller who supplies an id learns the item's visibility class or its name. Apply one disclosure rule across all review routes.
observal-server/api/routes/review.py#L93-L105: return 404 for the team-private branch, or use one message that does not reveal the item's visibility.observal-server/api/routes/review.py#L882-L894: removelisting.namefrom the bundle refusal message and report only that the bundle contains an out-of-scope member.observal-server/api/routes/review.py#L1054-L1056: replaceskill.namewith the caller-suppliedsidin the refusal message.
📍 Affects 1 file
observal-server/api/routes/review.py#L93-L105(this comment)observal-server/api/routes/review.py#L882-L894observal-server/api/routes/review.py#L1054-L1056
🤖 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 `@observal-server/api/routes/review.py` around lines 93 - 105, Apply a
consistent non-disclosure policy across the review write routes: in
_authorize_item, avoid revealing visibility details by returning 404 for
team-private items or using a visibility-neutral message; at
observal-server/api/routes/review.py:882-894, remove listing.name and report
only that the bundle contains an out-of-scope member; at
observal-server/api/routes/review.py:1054-1056, replace skill.name with the
caller-supplied sid in the refusal message.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
_authorize_item discloses the visibility of items the caller cannot review.
get_review returns 404 for an out-of-scope item at line 545, with a comment stating that a 403 would confirm a team-private item exists. _authorize_item returns 403 and, through two distinct messages, also tells the caller whether the item is team-private or public. The approve and reject routes therefore disclose what the detail route hides.
Align the two surfaces. Either return 404 here for the private case, or use a single message that does not reveal the item's visibility.
🛡️ Proposed direction
def _authorize_item(entity, scope: ReviewScope) -> None:
"""Authorize one item from its own visibility and teamspace."""
if can_review(entity, scope):
return
if getattr(entity, "is_private", False):
- raise HTTPException(
- status_code=403,
- detail="Team-private items are reviewed by their teamspace's owners and reviewers",
- )
+ # Mirror get_review: never confirm that a team-private item exists.
+ raise HTTPException(status_code=404, detail="Listing not found")
raise HTTPException(
status_code=403,
detail="Public items are reviewed by global reviewers, not by teamspace roles",
)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def _authorize_item(entity, scope: ReviewScope) -> None: | |
| """Authorize one item from its own visibility and teamspace.""" | |
| if can_review(entity, scope): | |
| return | |
| if getattr(entity, "is_private", False): | |
| raise HTTPException( | |
| status_code=403, | |
| detail="Team-private items are reviewed by their teamspace's owners and reviewers", | |
| ) | |
| raise HTTPException( | |
| status_code=403, | |
| detail="Public items are reviewed by global reviewers, not by teamspace roles", | |
| ) | |
| def _authorize_item(entity, scope: ReviewScope) -> None: | |
| """Authorize one item from its own visibility and teamspace.""" | |
| if can_review(entity, scope): | |
| return | |
| if getattr(entity, "is_private", False): | |
| # Mirror get_review: never confirm that a team-private item exists. | |
| raise HTTPException(status_code=404, detail="Listing not found") | |
| raise HTTPException( | |
| status_code=403, | |
| detail="Public items are reviewed by global reviewers, not by teamspace roles", | |
| ) |
🤖 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 `@observal-server/api/routes/review.py` around lines 93 - 105, Update
_authorize_item to avoid disclosing an unauthorized entity’s visibility: use the
same non-revealing 404 behavior as get_review for out-of-scope private items, or
replace both visibility-specific 403 details with one message that does not
identify whether the item is private or public. Keep authorization success and
in-scope denial behavior unchanged.
| return { | ||
| team, | ||
| isLoading: query.isLoading, | ||
| isError: query.isError, | ||
| error: query.error, | ||
| refetch: query.refetch, | ||
| notFound: !query.isLoading && !query.isError && !team, | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
TanStack Query v5 isLoading vs isPending vs isFetching difference
💡 Result:
In TanStack Query v5, isPending, isLoading, and isFetching are distinct flags that provide information about the current state of your query [1][2][3]. Understanding their differences is crucial for correctly handling data loading and background synchronization [1][4]. 1. isPending This boolean indicates that the query has no data and is currently in the 'pending' status [1][2]. It is the recommended flag for determining if you need to show an initial loading state [1][4]. When a query is in this state, it means the query attempt has not finished yet and no cached data is available [2][3]. 2. isLoading In v5, isLoading is a derived boolean defined as isPending && isFetching [2][3][5]. It is true only when the query is in the initial 'pending' state and is actively executing the query function (fetching) [2][3]. This replaces the deprecated isInitialLoading flag [2][5]. Because it relies on the fetch status, it can be false even if the query has no data (for example, if the query is 'paused' due to a lack of network connection) [6][4]. 3. isFetching This is a broader flag that indicates whether the query function is currently executing [1][2]. It is true whenever a fetch is in-flight, which includes both the initial 'pending' fetch and any background refetches (e.g., when a component re-mounts or when data is being synchronized in the background while data already exists) [2][3]. Summary Comparison - status vs. fetchStatus: The status (pending, error, success) tells you whether data is available [1][4]. The fetchStatus (fetching, paused, idle) tells you whether the query function is running [1][4]. - Recommendation: For most use cases, checking isPending is sufficient to handle the initial loading state [1][4]. isFetching is primarily used when you want to show a background indicator (e.g., a "Refreshing..." spinner) while the user is already viewing the existing data [7][4]. Migration Note: In v4, the flag previously called isLoading was renamed to isPending in v5, and the new v5 isLoading is equivalent to the old v4 isInitialLoading [5][8].
Citations:
- 1: https://tanstack.com/query/v5/docs/framework/react/guides/queries
- 2: https://tanstack.com/query/v5/docs/framework/react/reference/useQuery
- 3: https://tanstack.com/query/latest/docs/framework/react/reference/useQuery
- 4: https://tanstack.com/query/latest/docs/framework/react/guides/queries
- 5: https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5
- 6: Why do the docs recommend `isPending` instead of `isLoading`? TanStack/query#6297
- 7: https://tanstack.com/query/v5/docs/framework/react/guides/background-fetching-indicators
- 8: https://github.com/TanStack/query/blob/feb1efd8/docs/framework/react/guides/migrating-to-v5.md
🏁 Script executed:
# Inspect the hook, its consumers, and the installed TanStack Query version without running repository code.
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(use-teams-api\.ts|package\.json|.*team.*\.(ts|tsx))$' | head -80
printf '%s\n' '--- hook ---'
cat -n web/src/hooks/use-teams-api.ts | sed -n '1,120p'
printf '%s\n' '--- query API usage ---'
rg -n -C 3 'useTeam|notFound|useAllTeams|invalidateQueries|isFetching' web/src --glob '*.ts' --glob '*.tsx' | head -240
printf '%s\n' '--- TanStack Query declarations ---'
rg -n -C 2 '"`@tanstack/react-query`"|"`@tanstack/query`"' --glob 'package.json' --glob '*lock*' --glob 'pnpm-workspace.yaml' --glob 'yarn.lock'Repository: Observal/Observal
Length of output: 21824
🏁 Script executed:
printf '%s\n' '--- package versions ---'
cat -n web/package.json | sed -n '1,180p'
printf '%s\n' '--- detail route and page references ---'
cat -n web/src/routes/_authed/teamspaces.\$handle.tsx | sed -n '1,220p'
rg -n -C 8 'useTeamByHandle|No teamspace|notFound|isLoading' web/src/pages web/src/routes --glob '*.ts' --glob '*.tsx'
printf '%s\n' '--- all teams query and creation call sites ---'
rg -n -C 6 'useAllTeams|useCreateTeam|queryKey:\s*\["teams"|listAll' web/src/hooks web/src/pages web/src/routes --glob '*.ts' --glob '*.tsx' | head -240Repository: Observal/Observal
Length of output: 50374
🏁 Script executed:
printf '%s\n' '--- teamspace detail state handling ---'
cat -n web/src/pages/registry/teamspace-detail.tsx | sed -n '505,575p'
printf '%s\n' '--- creation success/navigation flow ---'
rg -n -C 10 'mutate|onSuccess|useCreateTeam|Teamspace created|teamspaces/\$|teamspaces/' web/src/pages/registry/teamspaces.tsx web/src/pages/registry/teamspace-detail.tsx web/src --glob '*.ts' --glob '*.tsx' | head -220
printf '%s\n' '--- exact relevant dependency lock entries ---'
rg -n -A 8 -B 3 '`@tanstack/react-query`@|/@tanstack/react-query@|version: 5\.' web/pnpm-lock.yaml pnpm-lock.yaml 2>/dev/null | head -100Repository: Observal/Observal
Length of output: 23199
🏁 Script executed:
python3 - <<'PY'
from itertools import product
def current(query_is_loading, query_is_fetching, query_is_error, team):
is_loading = query_is_loading
not_found = not query_is_loading and not query_is_error and not team
page_not_found = not_found or not team
return is_loading, not_found, page_not_found
def proposed(query_is_loading, query_is_fetching, query_is_error, team):
is_loading = query_is_loading or (query_is_fetching and not team)
not_found = (
not query_is_loading
and not query_is_fetching
and not query_is_error
and not team
)
page_not_found = not_found or not team
return is_loading, not_found, page_not_found
case = (False, True, False, False) # cached list, background refetch, handle absent
print("background-refetch case:")
print("current :", current(*case))
print("proposed:", proposed(*case))
print("\nrelevant state table (isLoading, notFound, page_not_found):")
for is_loading, is_fetching, is_error, team in product([False, True], repeat=4):
if (is_fetching, team) in ((True, False), (False, False)) and not is_error:
print(
{
"query_isLoading": is_loading,
"query_isFetching": is_fetching,
"query_isError": is_error,
"team": team,
"current": current(is_loading, is_fetching, is_error, team),
"proposed": proposed(is_loading, is_fetching, is_error, team),
}
)
PYRepository: Observal/Observal
Length of output: 929
🏁 Script executed:
python3 - <<'PY'
def render_state(query_is_loading, query_is_fetching, query_is_error, team):
is_loading = query_is_loading or (query_is_fetching and not team)
not_found = (
not query_is_loading
and not query_is_fetching
and not query_is_error
and not team
)
if is_loading:
return "skeleton"
if query_is_error:
return "error"
if not_found or not team:
return "not-found"
return "detail"
cases = {
"initial fetch without cached team": (True, True, False, False),
"background refetch; handle absent from cached list": (False, True, False, False),
"idle successful query; handle absent": (False, False, False, False),
"background refetch; cached handle present": (False, True, False, True),
}
for label, state in cases.items():
print(f"{label}: {render_state(*state)}")
PYRepository: Observal/Observal
Length of output: 358
Gate both loading and not-found states on background refetches.
React Query v5 sets isLoading to false when cached data exists during a background refetch. If the new handle is absent from cached data, the detail page can render “No teamspace named X” before the refetch completes. Set isLoading to query.isLoading || (query.isFetching && !team) and require !query.isFetching in notFound.
🤖 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 `@web/src/hooks/use-teams-api.ts` around lines 39 - 46, Update the return
object in useTeamsApi to treat an active background refetch without a team as
loading by setting isLoading from query.isLoading or query.isFetching && !team,
and require !query.isFetching alongside the existing notFound conditions.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
tests/test_component_update_visibility.py (1)
43-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winWire
db.executeexplicitly in_mock_db.
dbis a bareAsyncMock, soawait db.execute(...)returns aMagicMock, andresult.scalar_one_or_none()returns a truthy sentinel. If the update route runs any query, the success-path tests inTestUnchangedUpdatesSucceedpass on that sentinel instead of on real behavior. The sibling helpers document this hazard and wire the result explicitly:_db_with_membershipintests/test_draft_workflow.pyand_mock_dbintests/test_listing_detail_access.py.Set
db.executeto anAsyncMockwith an explicit result, or to aside_effectsequence that raises on an unexpected query.♻️ Proposed helper change
def _mock_db(): db = AsyncMock() db.add = MagicMock() db.commit = AsyncMock() db.flush = AsyncMock() db.refresh = AsyncMock() db.delete = AsyncMock() + # A bare AsyncMock hands back a truthy MagicMock for every query result. + # Fail loudly instead: these routes are expected to run no query here. + db.execute = AsyncMock(side_effect=AssertionError("unexpected query in the update route")) return db🤖 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 `@tests/test_component_update_visibility.py` around lines 43 - 50, Update the _mock_db helper to explicitly configure db.execute with a realistic result or a side-effect sequence that raises on unexpected queries, ensuring update-route tests validate actual query behavior instead of truthy AsyncMock sentinels.tests/test_registry_visibility.py (1)
561-571: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winAssert the review metadata on the never-approved path too.
_seed_team_private_componentsetsreviewed_byandreviewed_aton every seeded version, including thedraft,pending, andrejectedcases.test_team_owner_making_a_listing_public_sends_it_back_to_reviewasserts that both fields are cleared, but this test does not. A route that clears review metadata only on the approved branch, or that leaves stale team-level approval metadata on a never-approved listing, still passes here.💚 Proposed assertion
assert result["returned_to_review"] is False assert result["status"] == status.value + + async with sessions() as session: + listing = await session.get(McpListing, seed.listing_id) + version = await session.get(McpVersion, listing.latest_version_id) + assert version.status == status🤖 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 `@tests/test_registry_visibility.py` around lines 561 - 571, Extend test_never_approved_listing_is_not_moved_when_it_becomes_public to assert that the listing’s review metadata is cleared after _patch_visibility: verify reviewed_by and reviewed_at are both absent or null, while preserving the existing returned_to_review and status assertions for draft, pending, and rejected cases.tests/test_team_publishing.py (1)
34-40: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSet
org_idexplicitly on the mock user.
_userbuilds aMagicMock(spec=User)withoutorg_id. The spec allows the attribute, so any read returns a truthyMagicMock. Ifresolve_publish_targetor a submit route branches on org scoping, the tests take the org branch by accident. The sibling helpers in this PR set the field explicitly:tests/test_component_update_visibility.pyline 39,tests/test_listing_detail_access.pyline 37, andtests/test_preview_config.pyline 30.♻️ Proposed helper change
def _user(role=UserRole.user, **kw): u = MagicMock(spec=User) u.id = kw.get("id", uuid.uuid4()) u.role = role u.username = kw.get("username", "testuser") u.email = kw.get("email", "test@example.com") + # A bare spec'd attribute is truthy, which would put every caller in an org. + u.org_id = kw.get("org_id") return u🤖 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 `@tests/test_team_publishing.py` around lines 34 - 40, Update the _user test helper to explicitly assign org_id on the User mock, using a kwarg override with an appropriate default consistent with the sibling helpers, so org-scoping logic does not observe an implicit truthy MagicMock.tests/test_agent_composition.py (1)
1218-1232: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winDocument or align visibility-change status codes.
Agent routes return
422; component draft routes return400for this rejection. Record the intended API distinction or use one status code across both route families.🤖 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 `@tests/test_agent_composition.py` around lines 1218 - 1232, Align the visibility-change rejection status code between the agent route tested by test_visibility_change_is_rejected and the corresponding component draft routes, choosing either the existing 422/400 convention consistently or documenting the intentional distinction in the relevant API contract.
🤖 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 `@observal-server/models/sandbox.py`:
- Around line 36-38: Update the team deletion flow in delete_team to handle
listings belonging to the deleted team instead of leaving private listings with
team_id=NULL and visibility "team": block deletion or archive/reassign those
listings, preserving valid access semantics. Add regression coverage for the
team-deletion transition and ensure visibility and membership checks remain
consistent.
---
Nitpick comments:
In `@tests/test_agent_composition.py`:
- Around line 1218-1232: Align the visibility-change rejection status code
between the agent route tested by test_visibility_change_is_rejected and the
corresponding component draft routes, choosing either the existing 422/400
convention consistently or documenting the intentional distinction in the
relevant API contract.
In `@tests/test_component_update_visibility.py`:
- Around line 43-50: Update the _mock_db helper to explicitly configure
db.execute with a realistic result or a side-effect sequence that raises on
unexpected queries, ensuring update-route tests validate actual query behavior
instead of truthy AsyncMock sentinels.
In `@tests/test_registry_visibility.py`:
- Around line 561-571: Extend
test_never_approved_listing_is_not_moved_when_it_becomes_public to assert that
the listing’s review metadata is cleared after _patch_visibility: verify
reviewed_by and reviewed_at are both absent or null, while preserving the
existing returned_to_review and status assertions for draft, pending, and
rejected cases.
In `@tests/test_team_publishing.py`:
- Around line 34-40: Update the _user test helper to explicitly assign org_id on
the User mock, using a kwarg override with an appropriate default consistent
with the sibling helpers, so org-scoping logic does not observe an implicit
truthy MagicMock.
🪄 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: CHILL
Plan: Pro Plus
Run ID: 6812b322-a3c6-4c1d-9ff5-5a0bdc0600a0
📒 Files selected for processing (101)
docs/cli/agent.mddocs/cli/registry.mdobserval-server/alembic/versions/018_team_publishing.pyobserval-server/api/deps.pyobserval-server/api/routes/_component_archive.pyobserval-server/api/routes/admin/org.pyobserval-server/api/routes/agent/crud.pyobserval-server/api/routes/agent/draft.pyobserval-server/api/routes/agent/helpers.pyobserval-server/api/routes/agent/install.pyobserval-server/api/routes/agent_versions.pyobserval-server/api/routes/bulk.pyobserval-server/api/routes/co_authors.pyobserval-server/api/routes/component_source.pyobserval-server/api/routes/component_versions.pyobserval-server/api/routes/dashboard.pyobserval-server/api/routes/exec_dashboard.pyobserval-server/api/routes/feedback.pyobserval-server/api/routes/hook.pyobserval-server/api/routes/insights.pyobserval-server/api/routes/mcp.pyobserval-server/api/routes/preview.pyobserval-server/api/routes/prompt.pyobserval-server/api/routes/registry.pyobserval-server/api/routes/review.pyobserval-server/api/routes/sandbox.pyobserval-server/api/routes/skill.pyobserval-server/models/agent.pyobserval-server/models/component_source.pyobserval-server/models/hook.pyobserval-server/models/mcp.pyobserval-server/models/prompt.pyobserval-server/models/sandbox.pyobserval-server/models/skill.pyobserval-server/schemas/agent.pyobserval-server/schemas/component_source.pyobserval-server/schemas/constants.pyobserval-server/schemas/hook.pyobserval-server/schemas/mcp.pyobserval-server/schemas/prompt.pyobserval-server/schemas/sandbox.pyobserval-server/schemas/skill.pyobserval-server/services/agent_builder.pyobserval-server/services/agent_resolver.pyobserval-server/services/insights/self_learn.pyobserval-server/services/ownership.pyobserval-server/services/teamspace.pyobserval-server/tests/test_agent_pull.pyobserval-server/tests/test_component_versions_api.pyobserval-server/tests/test_registry_namespace.pyobserval-server/tests/test_teamspace.pyobserval_cli/client.pyobserval_cli/cmd_agent.pyobserval_cli/cmd_hook.pyobserval_cli/cmd_mcp.pyobserval_cli/cmd_migrate.pyobserval_cli/cmd_prompt.pyobserval_cli/cmd_sandbox.pyobserval_cli/cmd_skill.pyobserval_cli/skills/observal-agents/SKILL.mdobserval_cli/skills/observal-registry/SKILL.mdobserval_cli/skills/observal/SKILL.mdtests/test_agent_composition.pytests/test_co_authors.pytests/test_component_update_visibility.pytests/test_dashboard_leaderboard.pytests/test_draft_workflow.pytests/test_insights_access.pytests/test_insights_agent_lookup.pytests/test_listing_detail_access.pytests/test_listing_version_flush.pytests/test_preview_config.pytests/test_pull_and_agent_cli.pytests/test_registry_types.pytests/test_registry_visibility.pytests/test_sec009_component_source_ownership.pytests/test_sec009_org_scoping.pytests/test_team_publishing.pytests/test_team_review.pytests/test_team_visibility_migrations.pyweb/src/components/registry/agent-edit-form.tsxweb/src/components/registry/component-picker.tsxweb/src/components/registry/submit-component-dialog.tsxweb/src/hooks/use-api.tsweb/src/hooks/use-registry-api.tsweb/src/hooks/use-review-api.tsweb/src/hooks/use-teams-api.tsweb/src/lib/api.tsweb/src/lib/types/registry.tsweb/src/pages/registry/agents/builder.tsxweb/src/pages/registry/agents/detail.tsxweb/src/pages/registry/agents/index.tsxweb/src/pages/registry/components/detail.tsxweb/src/pages/registry/components/index.tsxweb/src/pages/registry/teamspace-detail.tsxweb/src/pages/registry/teamspaces.tsxweb/src/routeTree.gen.tsweb/src/routes/_authed/agents/index.tsxweb/src/routes/_authed/components/index.tsxweb/src/routes/_authed/teamspaces.$handle.tsxweb/src/routes/_authed/teamspaces.tsx
💤 Files with no reviewable changes (5)
- observal-server/services/insights/self_learn.py
- observal-server/api/routes/bulk.py
- observal-server/api/routes/admin/org.py
- observal-server/services/ownership.py
- observal-server/api/routes/exec_dashboard.py
🚧 Files skipped from review as they are similar to previous changes (86)
- observal-server/tests/test_registry_namespace.py
- observal-server/schemas/constants.py
- web/src/routes/_authed/components/index.tsx
- web/src/routes/_authed/teamspaces.tsx
- observal-server/services/agent_builder.py
- web/src/routes/_authed/teamspaces.$handle.tsx
- observal_cli/skills/observal-registry/SKILL.md
- tests/test_co_authors.py
- docs/cli/agent.md
- observal_cli/cmd_skill.py
- docs/cli/registry.md
- observal-server/api/routes/co_authors.py
- web/src/components/registry/component-picker.tsx
- web/src/pages/registry/teamspace-detail.tsx
- tests/test_listing_version_flush.py
- observal-server/models/hook.py
- observal_cli/skills/observal/SKILL.md
- observal-server/api/routes/insights.py
- observal-server/api/routes/component_source.py
- observal-server/schemas/agent.py
- observal_cli/client.py
- web/src/pages/registry/agents/index.tsx
- web/src/pages/registry/components/detail.tsx
- observal-server/models/skill.py
- web/src/lib/types/registry.ts
- observal_cli/cmd_migrate.py
- web/src/lib/api.ts
- observal-server/api/routes/agent/helpers.py
- observal-server/schemas/prompt.py
- observal-server/api/routes/component_versions.py
- observal-server/schemas/skill.py
- tests/test_insights_agent_lookup.py
- web/src/pages/registry/components/index.tsx
- observal-server/tests/test_agent_pull.py
- observal_cli/skills/observal-agents/SKILL.md
- tests/test_dashboard_leaderboard.py
- web/src/hooks/use-teams-api.ts
- observal-server/models/component_source.py
- web/src/hooks/use-registry-api.ts
- web/src/routes/_authed/agents/index.tsx
- observal_cli/cmd_agent.py
- observal-server/api/routes/feedback.py
- web/src/hooks/use-review-api.ts
- observal-server/services/agent_resolver.py
- observal-server/api/routes/registry.py
- observal-server/alembic/versions/018_team_publishing.py
- tests/test_pull_and_agent_cli.py
- observal-server/models/agent.py
- tests/test_sec009_org_scoping.py
- tests/test_insights_access.py
- observal-server/api/routes/agent_versions.py
- observal_cli/cmd_sandbox.py
- observal-server/models/mcp.py
- observal-server/tests/test_teamspace.py
- observal_cli/cmd_prompt.py
- observal-server/schemas/mcp.py
- observal-server/models/prompt.py
- observal-server/api/routes/dashboard.py
- web/src/components/registry/agent-edit-form.tsx
- observal-server/api/routes/prompt.py
- observal-server/api/routes/sandbox.py
- observal-server/api/routes/preview.py
- observal-server/api/routes/agent/install.py
- observal-server/schemas/hook.py
- observal-server/api/routes/hook.py
- observal_cli/cmd_hook.py
- observal_cli/cmd_mcp.py
- web/src/hooks/use-api.ts
- web/src/pages/registry/agents/builder.tsx
- web/src/pages/registry/agents/detail.tsx
- observal-server/api/routes/mcp.py
- observal-server/schemas/sandbox.py
- web/src/components/registry/submit-component-dialog.tsx
- observal-server/api/routes/_component_archive.py
- observal-server/api/routes/skill.py
- web/src/routeTree.gen.ts
- observal-server/api/routes/agent/draft.py
- observal-server/tests/test_component_versions_api.py
- tests/test_team_visibility_migrations.py
- web/src/pages/registry/teamspaces.tsx
- observal-server/schemas/component_source.py
- tests/test_sec009_component_source_ownership.py
- observal-server/api/routes/agent/crud.py
- observal-server/services/teamspace.py
- observal-server/api/routes/review.py
- observal-server/api/deps.py
The team_id foreign keys are ON DELETE SET NULL, so deleting a teamspace did not delete its listings, it stripped their teamspace and left them behind with is_private=True and team_id=NULL. Reproduced against a live stack: the delete returned 204, the submitter kept access through the personal-private clause, and every other member went from 200 to 404 with no warning. The listing still reported visibility "team" while belonging to no team, and nothing could reattach it because setting team visibility requires an existing teamspace. Deleting a team also frees its handle, so a later registrant could claim the namespace those listings still carry. Public listings therefore block the delete too: changing visibility does not clear team_id, so only transferring or deleting a listing actually detaches it, and the error says exactly that. Co-Authored-By: Hari Srinivasan <harisrini21@gmail.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
tests/test_team_review.py (1)
596-664: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDeletion-guard tests match the current implementation but don't cover the soft-delete gap.
TestTeamDeletionGuardcorrectly validates the six-model count loop and the 409/204 branching against the current_team_owned_listing_countsanddelete_teamimplementation. These tests mockdb.scalargenerically, so they cannot surface the missingdeleted_atfilter on theAgentcount flagged inobserval-server/api/routes/teams.py(Lines 205-232): a soft-deleted-only agent would still count as "owned" and block deletion in production, but this mock-based test can't distinguish that from a real filtered query. Once that fix lands, add a test that seedsAgent.deleted_atas set and asserts the query still returns 0 (or asserts the actualWHEREclause via a real query against the in-memory DB used earlier in this file, rather than a plainAsyncMock).🤖 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 `@tests/test_team_review.py` around lines 596 - 664, Add coverage in TestTeamDeletionGuard for a team containing only a soft-deleted Agent: seed Agent.deleted_at as set and verify _team_owned_listing_counts or the delete_team flow treats the count as zero, preferably using the file’s in-memory database or by inspecting the generated query’s WHERE clause rather than generic db.scalar mocking. Keep the existing deletion-guard tests unchanged.tests/e2e/teamspace-review.spec.ts (2)
140-160: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe confirmation dialog's "confirm" path is untested.
This test only exercises the Cancel branch: it opens the dialog, verifies its copy, then cancels and asserts
is_privatestaystrue. There is no assertion for the accept path, where confirming should flipis_privatetofalseand route the listing into public review. Since the dialog exists specifically to warn about that state transition, add a companion test (or extend this one) that clicks the confirm button and asserts the resultingis_private/review state.🤖 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 `@tests/e2e/teamspace-review.spec.ts` around lines 140 - 160, Extend the teamspace visibility test around the alertdialog to exercise the confirm path, using the dialog’s confirm/accept button rather than the existing cancel control. After confirmation, fetch the seeded listing and assert is_private becomes false and the listing enters the expected public-review state, while preserving the existing dialog-copy and cancellation coverage.
113-160: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSeeded skill listing is never cleaned up.
The "visibility confirmation" test creates a new skill via
POST /api/v1/skills/submiton every run (e2e-vis-${Date.now()}) but never deletes it afterward. Repeated CI runs against the same environment will accumulate orphaned team-private skill listings tied toTEAM_HANDLE. Add anafterEach/finallycleanup call (or a delete API call) once the assertions complete.🤖 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 `@tests/e2e/teamspace-review.spec.ts` around lines 113 - 160, The visibility confirmation test leaves the skill created by the POST request in the environment. Track seeded.id and add cleanup after the test, using the authenticated owner token to delete the seeded listing via the appropriate skills delete API, ensuring cleanup runs 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 `@observal-server/api/routes/teams.py`:
- Around line 205-232: Update _team_owned_listing_counts so Agent counts include
only active rows by adding a deleted_at IS NULL condition to the Agent query,
while preserving the existing team_id filtering and counting behavior for all
other listing models.
- Around line 241-266: Update delete_team’s team-loading path to acquire a
row-level SELECT FOR UPDATE lock on the team via _load_team, keeping that lock
held through _team_owned_listing_counts, db.delete, and transaction commit.
Ensure concurrent team-scoped listing creation cannot insert between the
ownership check and deletion, while preserving the existing conflict response.
---
Nitpick comments:
In `@tests/e2e/teamspace-review.spec.ts`:
- Around line 140-160: Extend the teamspace visibility test around the
alertdialog to exercise the confirm path, using the dialog’s confirm/accept
button rather than the existing cancel control. After confirmation, fetch the
seeded listing and assert is_private becomes false and the listing enters the
expected public-review state, while preserving the existing dialog-copy and
cancellation coverage.
- Around line 113-160: The visibility confirmation test leaves the skill created
by the POST request in the environment. Track seeded.id and add cleanup after
the test, using the authenticated owner token to delete the seeded listing via
the appropriate skills delete API, ensuring cleanup runs even when assertions
fail.
In `@tests/test_team_review.py`:
- Around line 596-664: Add coverage in TestTeamDeletionGuard for a team
containing only a soft-deleted Agent: seed Agent.deleted_at as set and verify
_team_owned_listing_counts or the delete_team flow treats the count as zero,
preferably using the file’s in-memory database or by inspecting the generated
query’s WHERE clause rather than generic db.scalar mocking. Keep the existing
deletion-guard tests unchanged.
🪄 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: CHILL
Plan: Pro Plus
Run ID: 7f9d5bde-186a-42cc-809f-71c28dcf523d
📒 Files selected for processing (3)
observal-server/api/routes/teams.pytests/e2e/teamspace-review.spec.tstests/test_team_review.py
| team = await _require_owner_or_admin(db, team_id, current_user) | ||
|
|
||
| # Refuse while the teamspace still owns listings. ON DELETE SET NULL would | ||
| # otherwise leave every one of them with is_private=True and team_id=NULL: | ||
| # the membership check can no longer match anybody, so each listing silently | ||
| # collapses to creator-only access while still reporting visibility "team", | ||
| # and no member other than its original submitter can reach it again. The | ||
| # handle also becomes free to claim, which would hand those listings' | ||
| # namespace to whoever registers it next. Make the owner deal with the | ||
| # listings first rather than destroying access as a side effect. | ||
| owned = await _team_owned_listing_counts(db, team.id) | ||
| if owned: | ||
| # Public listings block too. Changing visibility does not clear team_id, and | ||
| # deleting the team frees the handle for anyone to claim, which would hand a | ||
| # new owner the namespace those listings still carry. Only transferring or | ||
| # deleting them actually detaches them, so the message says exactly that. | ||
| detail = ", ".join(f"{count} {label}" for label, count in sorted(owned.items())) | ||
| raise HTTPException( | ||
| status_code=409, | ||
| detail=( | ||
| f"Teamspace '{team.handle}' still owns {detail}. Transfer or delete those listings before " | ||
| "deleting the teamspace, otherwise their members lose access and the namespace is " | ||
| "left claimable by someone else." | ||
| ), | ||
| ) | ||
|
|
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- candidate files ---'
fd -i 'teams.py|.*(team|listing).*\.py$' . | head -80
printf '%s\n' '--- relevant symbols and foreign keys ---'
rg -n -C 3 '_team_owned_listing_counts|_load_team|delete_team|team_id|ondelete' observal-server --glob '*.py' | head -300Repository: Observal/Observal
Length of output: 20180
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- teams route ---'
sed -n '1,330p' observal-server/api/routes/teams.py
printf '%s\n' '--- team model and listing model declarations ---'
cat -n observal-server/models/team.py
rg -n -C 5 'ForeignKey\("teams\.id"|class .*Listing|team_id' observal-server/models --glob '*.py' | head -500
printf '%s\n' '--- listing creation/update paths ---'
rg -n -C 5 'team_id|publish|create.*listing|Listing\(' observal-server/api/routes --glob '*.py' | head -500Repository: Observal/Observal
Length of output: 50373
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- team publish resolution ---'
ast-grep outline observal-server/services/teamspace.py
sed -n '1,300p' observal-server/services/teamspace.py
printf '%s\n' '--- all direct team-scoped listing writes ---'
rg -n -C 8 'team_id\s*=|team_id:' observal-server/api/routes observal-server/services --glob '*.py' \
| rg -n 'team_id|listing|Listing|resolve_publish_target|db\.(add|flush|commit)' | head -400
printf '%s\n' '--- migration foreign keys and delete constraints ---'
rg -n -C 4 'team_id|teams|SET NULL|ForeignKeyConstraint' observal-server/alembic/versions/017_teams.py observal-server/alembic/versions/018_team_publishing.py
printf '%s\n' '--- relevant tests ---'
rg -n -C 8 'delete.*team|team.*delete|owned_listing|team_id.*team|SET NULL|orphan' observal-server/tests tests --glob '*.py' | head -400Repository: Observal/Observal
Length of output: 50373
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import ast
import re
route = Path("observal-server/api/routes/teams.py").read_text()
service = Path("observal-server/services/teamspace.py").read_text()
tree = ast.parse(route)
functions = {n.name: n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))}
load = ast.get_source_segment(route, functions["_load_team"])
delete = ast.get_source_segment(route, functions["delete_team"])
counts = ast.get_source_segment(route, functions["_team_owned_listing_counts"])
print("load_team_uses_for_update:", "with_for_update" in load or "for_update" in load)
print("delete_order:", [
"require_owner_or_admin" if "_require_owner_or_admin" in delete else None,
"owned_counts" if "_team_owned_listing_counts" in delete else None,
"db.delete" if "db.delete" in delete else None,
"db.commit" if "db.commit" in delete else None,
])
print("counted_models:", re.findall(r"from models\.(\w+) import (\w+)", counts))
print("delete_fk_policy_mentions:", "ON DELETE SET NULL" in counts)
service_tree = ast.parse(service)
resolve = next(
n for n in service_tree.body
if isinstance(n, ast.AsyncFunctionDef) and n.name == "resolve_publish_target"
)
resolve_source = ast.get_source_segment(service, resolve)
print("publish_target_loads_team_without_lock:", "db.get(Team, team_id)" in resolve_source)
print("publish_target_writes_team_id:", "team_id=target.team_id" in route or "team_id=target.team_id" in service)
print("\nteam_id foreign-key declarations:")
for path in sorted(Path("observal-server/models").glob("*.py")):
text = path.read_text()
if 'ForeignKey("teams.id' in text:
for line_no, line in enumerate(text.splitlines(), 1):
if 'ForeignKey("teams.id' in line:
print(f"{path}:{line_no}:{line.strip()}")
PY
printf '%s\n' '--- exact migration constraints for team_id ---'
rg -n -B 2 -A 3 'ForeignKeyConstraint\(\["team_id"\].*teams\.id|teams\.id.*ondelete="SET NULL"|team_id.*teams\.id' \
observal-server/alembic/versions --glob '*.py'Repository: Observal/Observal
Length of output: 2847
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- team publishing migration ---'
cat -n observal-server/alembic/versions/018_team_publishing.py
printf '%s\n' '--- publish transaction tails ---'
for f in observal-server/api/routes/{mcp,skill,hook,prompt,sandbox,component_source}.py observal-server/api/routes/agent/{draft,crud}.py; do
if [ -f "$f" ]; then
printf '\n--- %s ---\n' "$f"
rg -n -A 18 -B 6 'resolve_publish_target|team_id=target\.team_id|db\.commit\(\)' "$f" | head -120
fi
done
printf '%s\n' '--- component source schema and access behavior ---'
sed -n '1,150p' observal-server/api/routes/component_source.pyRepository: Observal/Observal
Length of output: 44292
🌐 Web query:
PostgreSQL official documentation SELECT FOR UPDATE conflicts with FOR KEY SHARE foreign key insert
💡 Result:
In PostgreSQL, the SELECT FOR UPDATE command conflicts with the FOR KEY SHARE lock implicitly acquired by foreign key operations, which leads to blocking [1][2]. When you perform an INSERT into a table with a foreign key constraint, PostgreSQL automatically acquires a FOR KEY SHARE lock on the referenced row in the parent table to ensure the referenced data remains valid [3][2]. The FOR UPDATE lock is an exclusive lock that prevents other transactions from modifying, deleting, or locking the same row [1][2]. Because FOR UPDATE is incompatible with FOR KEY SHARE, a transaction holding a FOR UPDATE lock on a parent row will block any concurrent transaction from inserting child rows that reference that parent row [1][2]. To avoid this contention, you can use FOR NO KEY UPDATE if you do not need to modify the columns that are part of a unique index or primary key [3][4]. FOR NO KEY UPDATE is a weaker lock that is compatible with FOR KEY SHARE, allowing child row inserts to proceed concurrently [1][2]. Summary of Locking Compatibility: - FOR UPDATE: Conflicts with FOR KEY SHARE (blocks inserts) [1][5]. - FOR NO KEY UPDATE: Compatible with FOR KEY SHARE (does not block inserts) [1][2].
Citations:
- 1: https://www.postgresql.org/docs/current/explicit-locking.html
- 2: https://vladmihalcea.com/postgresql-for-no-key-update/
- 3: https://www.cybertec-postgresql.com/en/select-for-update-considered-harmful-postgresql/
- 4: https://dba.stackexchange.com/questions/188169/select-for-update-locking-other-tables-in-postgresql
- 5: https://www.postgresql.org/docs/17/explicit-locking.html
Serialize team deletion with team-scoped listing creation.
delete_team counts listings before db.delete(team), but _load_team does not lock the team row. A concurrent publish can insert a row after the count and before the delete commit. ON DELETE SET NULL then detaches that listing and recreates the orphaned-access scenario. Load the team with SELECT ... FOR UPDATE in the deletion transaction and hold the lock through the counts and commit. This blocks concurrent foreign-key inserts or makes them fail cleanly.
🤖 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 `@observal-server/api/routes/teams.py` around lines 241 - 266, Update
delete_team’s team-loading path to acquire a row-level SELECT FOR UPDATE lock on
the team via _load_team, keeping that lock held through
_team_owned_listing_counts, db.delete, and transaction commit. Ensure concurrent
team-scoped listing creation cannot insert between the ownership check and
deletion, while preserving the existing conflict response.
…owns
?team_id= reused the publish-scope predicate, so it returned every public
listing in the registry plus the team's private ones. Teamspace pages and the
CLI --team flag therefore showed other namespaces' listings as if the team
owned them: GET /skills?team_id=<acme> returned 80 rows, 5 of them belonging to
f3hooli, f3umbrella, f3initech, f3globex and ccs.
The two questions now have separate parameters:
team_id ownership. What this teamspace published, nothing
else. Used by the teamspace page, the registry list
filter, and the CLI --team flag.
composable_for_team_id publish scope. Public listings plus this teamspace's
private ones, the set an agent published for that
target may contain. Used only by the agent builder's
component picker.
Caller visibility is still applied first either way, so a non-member passing a
teamspace id sees only that team's public rows.
CLI help and the bundled skill docs described the old widening behaviour and
now describe ownership.
Co-Authored-By: Hari Srinivasan <harisrini21@gmail.com>
…visibility Six issues from review, all on paths the read-path probes never exercised. resolve_listing now enforces visibility on every lookup, not just on ambiguous bare names. Archive, unarchive, draft edit and the editing lock all resolve through it and authorize with get_effective_component_permission, which answers "owner" for the original submitter forever, so a user removed from a teamspace could still mutate a listing they could no longer read. Version endpoints no longer serve unapproved versions to callers who cannot review or own them. A version carries the real payload, and a listing goes public the moment its visibility changes while its versions return to the queue, so without this an ordinary caller read content no reviewer had accepted. review_publication_to_public now returns EVERY approved version to review, not only the latest, because installs can pin any approved version. The visibility endpoint's agent branch validated only the latest version's components. It now covers every installable version, matching the rule the component branch already applied. Deleting a teamspace was a dead end: emptying it required a transfer, and transfer refused every team-owned listing. Transfer now detaches, for a team owner or admin only, clearing team_id, dropping team visibility, and returning the listing to review because leaving a teamspace makes it public. The delete guard also counts component sources and ignores soft-deleted agents. The migration downgrade guard now also catches personal-private agents, which lose is_private on downgrade and return public. Preview gates on approval status as well as privacy, so a public but unapproved component is no longer previewable by anyone. The builder's local draft keeps its teamspace and visibility, so restoring one no longer silently turns a team-private agent into a personal public one, and validation reruns when the target changes. The teamspace review tab now opens the same detail sheet the global queue uses, so a reviewer inspects the prompt, command or script before approving instead of deciding from summary text. Visibility controls render only for callers the server accepts. The feedback summary resolves a listing's type from its feedback rows instead of probing all six listing tables on an anonymous endpoint hit on every registry detail render. observal agent publish --update now refuses --team instead of ignoring it, and applies visibility before the update so a refused publish cannot leave a half-applied edit. Co-Authored-By: Hari Srinivasan <harisrini21@gmail.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
observal-server/alembic/versions/018_team_publishing.py (1)
103-110: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDistinguish personal-private agents in the downgrade error.
When
private_agentsis non-zero, Line 104 adds a non-team-owned entry toowned. The error still says “team-owned listings” and instructs the operator to reassign rows because teamspace ownership is lost. A personal-private agent has no teamspace to reassign. Use remediation text that distinguishes team-owned rows from personal-private agents.Proposed wording fix
- "Cannot downgrade 018_team_publishing: team-owned listings still exist " + "Cannot downgrade 018_team_publishing: protected registry rows still exist " f"({detail}). Reassign or delete them before rolling back, otherwise " - "their teamspace ownership is lost and a later re-upgrade cannot restore it." + "their ownership or privacy state is lost and a later re-upgrade cannot restore it."🤖 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 `@observal-server/alembic/versions/018_team_publishing.py` around lines 103 - 110, Update the downgrade error construction around the `owned` summary and `private_agents` handling to distinguish team-owned listings from personal-private agents. Keep the existing teamspace reassignment guidance for team-owned rows, but provide separate remediation for personal-private agents that does not suggest reassigning them to a teamspace.observal-server/api/routes/co_authors.py (1)
196-207: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReorder the identity conflict check before the team-detach and review side effects.
The code mutates
entity.team_idandentity.is_private, and callsreview_publication_to_public, before checkingidentity_exists. Ifidentity_existsreturnsTrue, the function raisesHTTPException(409)after these side effects already ran.
AsyncSessionautoflushes pending changes before a query by default, so the in-memoryteam_id/is_privatechanges are likely sent to the database as part of theidentity_existsSELECT, before the conflict is known.review_publication_to_publicmay also produce side effects, such as a review-queue entry, that a later exception cannot undo. Move theidentity_existscheck before the team-detach and review-publication block, so a name conflict does not leave the entity partially detached from its team.🐛 Proposed fix to reorder the conflict check
- was_private = bool(getattr(entity, "is_private", False)) - if team_id is not None: - entity.team_id = None - entity.is_private = False - await review_publication_to_public(entity, current_user, db, was_private=was_private) - - model = ENTITY_MODELS[entity_type] - if await identity_exists(db, model, target_user.username, entity.slug, exclude_id=entity.id): - raise HTTPException( - status_code=409, - detail=f"{target_user.username}/{entity.slug} already exists", - ) + model = ENTITY_MODELS[entity_type] + if await identity_exists(db, model, target_user.username, entity.slug, exclude_id=entity.id): + raise HTTPException( + status_code=409, + detail=f"{target_user.username}/{entity.slug} already exists", + ) + + was_private = bool(getattr(entity, "is_private", False)) + if team_id is not None: + entity.team_id = None + entity.is_private = False + await review_publication_to_public(entity, current_user, db, was_private=was_private)🤖 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 `@observal-server/api/routes/co_authors.py` around lines 196 - 207, Move the `identity_exists` conflict check using `ENTITY_MODELS` before the `team_id`/`is_private` mutations and the `review_publication_to_public` call. Preserve the existing 409 `HTTPException` response, and only execute the team-detach and review-publication block after the identity is confirmed available.
🧹 Nitpick comments (1)
observal-server/api/routes/component_versions.py (1)
98-100: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the repeated version-visibility filter into a helper.
_list_versions,_get_version, and_review_versioneach buildversion_filterswith the same two lines: append the listing filter, then appendversion_model.status == ListingStatus.approvedunlessmay_view_unapprovedis true. Extract this into a shared helper to avoid drift if the unapproved-visibility rule changes.♻️ Proposed helper extraction
def _version_visibility_filters(listing, version_model, current_user, *extra): filters = list(extra) if not may_view_unapproved(get_effective_component_permission(listing, current_user), current_user): filters.append(version_model.status == ListingStatus.approved) return filtersThen in each function:
- version_filters = [version_model.listing_id == listing.id] - if not may_view_unapproved(get_effective_component_permission(listing, current_user), current_user): - version_filters.append(version_model.status == ListingStatus.approved) + version_filters = _version_visibility_filters(listing, version_model, current_user, version_model.listing_id == listing.id)Also applies to: 138-140, 282-284
🤖 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 `@observal-server/api/routes/component_versions.py` around lines 98 - 100, Extract the shared version-visibility construction into a helper such as _version_visibility_filters, accepting the listing, version model, current user, and optional extra filters; ensure it includes listing.id matching and conditionally appends approved status based on may_view_unapproved. Update _list_versions, _get_version, and _review_version to use this helper while preserving any function-specific filters.
🤖 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 `@observal-server/alembic/versions/018_team_publishing.py`:
- Around line 103-110: Update the downgrade error construction around the
`owned` summary and `private_agents` handling to distinguish team-owned listings
from personal-private agents. Keep the existing teamspace reassignment guidance
for team-owned rows, but provide separate remediation for personal-private
agents that does not suggest reassigning them to a teamspace.
In `@observal-server/api/routes/co_authors.py`:
- Around line 196-207: Move the `identity_exists` conflict check using
`ENTITY_MODELS` before the `team_id`/`is_private` mutations and the
`review_publication_to_public` call. Preserve the existing 409 `HTTPException`
response, and only execute the team-detach and review-publication block after
the identity is confirmed available.
---
Nitpick comments:
In `@observal-server/api/routes/component_versions.py`:
- Around line 98-100: Extract the shared version-visibility construction into a
helper such as _version_visibility_filters, accepting the listing, version
model, current user, and optional extra filters; ensure it includes listing.id
matching and conditionally appends approved status based on may_view_unapproved.
Update _list_versions, _get_version, and _review_version to use this helper
while preserving any function-specific filters.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 9db7ae1d-aca1-4dc9-842e-88a5e8b7608e
📒 Files selected for processing (33)
observal-server/alembic/versions/018_team_publishing.pyobserval-server/api/deps.pyobserval-server/api/routes/agent/crud.pyobserval-server/api/routes/agent/draft.pyobserval-server/api/routes/co_authors.pyobserval-server/api/routes/component_versions.pyobserval-server/api/routes/feedback.pyobserval-server/api/routes/hook.pyobserval-server/api/routes/mcp.pyobserval-server/api/routes/preview.pyobserval-server/api/routes/prompt.pyobserval-server/api/routes/registry.pyobserval-server/api/routes/sandbox.pyobserval-server/api/routes/skill.pyobserval-server/api/routes/teams.pyobserval-server/services/teamspace.pyobserval_cli/cmd_agent.pyobserval_cli/cmd_hook.pyobserval_cli/cmd_mcp.pyobserval_cli/cmd_prompt.pyobserval_cli/cmd_sandbox.pyobserval_cli/cmd_skill.pyobserval_cli/skills/observal-agents/SKILL.mdobserval_cli/skills/observal-registry/SKILL.mdtests/test_co_authors.pytests/test_registry_types.pytests/test_sec009_org_scoping.pytests/test_team_review.pyweb/src/components/registry/component-picker.tsxweb/src/pages/registry/agents/builder.tsxweb/src/pages/registry/agents/detail.tsxweb/src/pages/registry/components/detail.tsxweb/src/pages/registry/teamspace-detail.tsx
🚧 Files skipped from review as they are similar to previous changes (27)
- observal-server/api/routes/teams.py
- web/src/components/registry/component-picker.tsx
- observal_cli/cmd_skill.py
- observal_cli/cmd_prompt.py
- observal_cli/cmd_hook.py
- web/src/pages/registry/teamspace-detail.tsx
- observal_cli/skills/observal-agents/SKILL.md
- observal_cli/skills/observal-registry/SKILL.md
- web/src/pages/registry/components/detail.tsx
- observal-server/api/routes/preview.py
- web/src/pages/registry/agents/detail.tsx
- observal_cli/cmd_sandbox.py
- observal-server/api/routes/registry.py
- observal-server/api/routes/feedback.py
- tests/test_team_review.py
- tests/test_registry_types.py
- observal_cli/cmd_mcp.py
- observal_cli/cmd_agent.py
- observal-server/api/routes/mcp.py
- observal-server/api/routes/agent/draft.py
- observal-server/api/routes/skill.py
- observal-server/services/teamspace.py
- web/src/pages/registry/agents/builder.tsx
- observal-server/api/deps.py
- tests/test_sec009_org_scoping.py
- observal-server/api/routes/prompt.py
- observal-server/api/routes/hook.py
Every observal team command called server_supports("teamspaces") first, which
fetched /api/v1/config/version and compared min(cli_version, server_version)
against a hardcoded minimum in features.py.
That minimum could only ever be wrong. Both versions come from this repository,
so the negotiated version is just this package's version and the gate reduces to
comparing a number against itself. It shipped naming 1.11.0 while teamspaces were
already in the 1.10.7 tree, so the check failed on every server that fully
supported the feature:
$ observal team list
Teamspaces are not supported by this server.
Nobody downgrades a server behind a CLI to a version predating teamspaces, and a
server that genuinely lacks the endpoints answers for itself. The gate, its eight
call sites, and the now-unreferenced feature entry are removed, and
web/src/lib/features.ts is regenerated through scripts/sync_features.py.
Co-Authored-By: Hari Srinivasan <harisrini21@gmail.com>
5e0c22e to
6705441
Compare
…eable Agent content bypassed publication review. _load_agent applied the approved-status condition only on the name path, so a UUID or prefix returned an agent whose version was pending, which is the state a team-private agent enters the moment it is made public. The version list, diff and generated-config routes returned every version regardless of status. Both now gate on status unless the caller owns the agent or can review it, matching what component versions already did. Teamspace deletion is now enforced by the database. The team_id foreign keys move from ON DELETE SET NULL to RESTRICT in migration 019, so the check and the delete are one statement and a publish landing mid-delete can no longer be orphaned. The application count is demoted to a pre-check that exists only to name what is in the way, and it counts soft-deleted agents again: they are restorable and still carry the namespace, so freeing the handle while one exists lets a new team inherit it. Transfer was authorized in the wrong order. _get_entity_for_transfer refused anyone who was not the original submitter before the teamspace rule was consulted, so a team owner could not move a member's listing and a teamspace could never be emptied. Authority now follows the listing: a team-owned listing answers to its teamspace owners and admins, a personal one to its owner. Transferring a team listing to yourself is allowed, since that is how a teamspace is emptied. Co-author management resolved entities with a raw db.get, bypassing visibility entirely, so a removed member kept write access to an item they could no longer read. It resolves through resolve_listing now. The /my routes selected purely on the author column for the same reason and are visibility-scoped too. Migration 018's downgrade guard excluded component_sources on the claim that they follow their listing. They have no listing relationship, only their own team_id and is_public, so dropping team_id strands them the same way. They are guarded. Review write routes answer 404 rather than 403 for a team-private item outside the caller's scope, so a refusal no longer confirms the item exists and is private. Public refusals still say why, since a public item is discoverable anyway. observal agent publish now refuses --visibility with --update. They are two server calls with no atomic form, so ordering them only chooses which half is left applied when one fails. The component submit dialog resets its teamspace and visibility on close, so the next submission cannot inherit the previous target. Co-Authored-By: Hari Srinivasan <harisrini21@gmail.com>
2abd105 to
bba894a
Compare
Purpose / Description
Feature 3 of the registry publish loop: team publishing. Feature 2 gave teamspaces an identity and members; this makes them a publishing target. Authorized members can publish agents and all five component types into a team namespace, and can keep them visible only to that team. Agents gain a private concept for the first time.
Fixes
Approach
018_team_publishingaddsis_privatetoagentsandteam_idtoagents, the five component listings, andcomponent_sources.team_idis the sole ownership and privacy axis; organization scoping is not used for registry visibility. There is no backfill: the shared helpers already resolve a legacy private row with no teamspace as creator-only, so a bulk publish would disclose data for no benefit. The downgrade refuses to run while any listing still belongs to a teamspace, because droppingteam_idloses the only record of team ownership.apply_visibility_filterandapply_registry_scopefor queries,resolve_visible_listingandcheck_listing_visibility_asyncfor detail and install paths,resolve_publish_targetfor publish authorization. A hidden listing returns 404, never 403, so it is not distinguishable from one that does not exist.--teamand--visibilityon publish,--teamand--namespaceon browse, and composition validation inagent build. Bundled skills updated to match.How Has This Been Tested?
Automated:
tests/: 4,980 passed, 20 skipped, zero failures.observal_cli/tests/: 78 passed.observal-server/tests/: 407 passed, with 19 pre-existing failures intest_jwt.pyandtest_multi_tenancy.pyfrom an uninitialized KeyManager in this environment, unrelated to this branch. Notemake testruns onlytests/, so the other two directories need running directly, andobserval-server/tests/needsPYTHONPATH=packages/observal-shared.team_idcolumns appear and disappear together, and the downgrade guard fires while team-owned rows exist.Manual, against a local stack with 22 users, 5 teamspaces (one user deliberately in two), 50 components across all five types, and 10 agents:
To reproduce the stack:
make rebuild-fast, thenuv tool install --editable . --force.Learning (optional, can help others)
Adversarially probing the running stack found defects the unit suite did not, all fixed here:
POST /agents/preview-configresolved component ids with no visibility filter and returned the rendered config, disclosing team-private MCP commands, hook handlers, prompt templates, and SKILL.md to any authenticated user. The code comment said the builder UI had already scoped the selection, which is client-side trust.PATCH /registry/{type}/{id}/visibilitynever changed status, so a team role could self-approve a team-private listing and flip it public, reaching the global catalog without review._require_agent_edit_accessininsights.pyhad been weakened to a condition that can never fire, exposing every public agent's insight reports.post_update=Trueonlatest_version, whichAgentalready had. Any unit of work touching a listing field and a version field raisedCircularDependencyError, which made resubmitting a pending or rejected component name a 500.Two stale-mock lessons: patching a route-local binding stops working the moment a route calls a shared helper instead, and an unset attribute on a
MagicMockis truthy, so an unsetis_privatesilently made every listing look private and kept assertions green for the wrong reason.Checklist
Please, go through these checks before submitting the PR.
This branch does change the web registry and agent builder, so the box stays unchecked. Screenshots will be added by the author rather than generated.
AI Assistance
Was generative AI tooling used to co-author this PR?
Summary by CodeRabbit