-
-
Notifications
You must be signed in to change notification settings - Fork 41
[lib-audit] Q2-3 projects router nits (status codes, mixin, slug regex, mode Literal, existence oracle, broker stop-gaps) #2964
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| ### Fixed | ||
|
|
||
| - Projects router: Changed `require_owner_or_admin` to `_get_owned_project` for 6 routes to provide consistent 404 behavior for non-owners | ||
| - Projects router: Updated `delete_element` mode parameter to use `Literal["strict", "untag"]` for type safety | ||
| - Projects router: Consolidated `_SLUG_RE` regex definition from 3 locations to 1 in `element_store.py` | ||
| - Projects router: Added `_TaskRequestModelMixin` to `CreateChecklistItemIn` model | ||
| - Projects router: Fixed `project_events` stream to include `id` field in emitted events | ||
| - Projects events: Added `maxsize` parameter to prevent unbounded queue growth | ||
| - Projects events: Clean up empty subscriber keys to prevent memory leaks | ||
| - Element store: Updated import to use centralized `_SLUG_RE` from `element_store.py` | ||
| - Fixed imports in projects.py: removed unused `re` import, added `Literal` and `_SLUG_RE` imports |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| ### Added | ||
|
|
||
| - Added `assemble_bundle.sh` release-build smoke test verifying Sparkle.framework is bundled on success and missing-framework fails non-zero | ||
| - Added domain audit test ensuring no `taos.app` feed or download references remain under `mac/` | ||
|
|
||
| S2-23: Mac updater is a no-op: Sparkle never fetched; feed host is not the project domain |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -135,3 +135,46 @@ PEM | |||||
| run grep -q 'dependencies: \["Sparkle"\]' "$pkg_swift" | ||||||
| [ "$status" -eq 0 ] | ||||||
| } | ||||||
|
|
||||||
| @test "assemble_bundle.sh bundles Sparkle.framework in a successful release build" { | ||||||
| local fake_root="$BATS_TEST_TMPDIR/repo" | ||||||
| mkdir -p "$fake_root/mac/build" "$fake_root/mac/appcast" \ | ||||||
| "$fake_root/mac/launcher/Sources/taOSLauncher/Resources" | ||||||
| cp "$REPO_ROOT/mac/build/assemble_bundle.sh" "$fake_root/mac/build/assemble_bundle.sh" | ||||||
| cp "$REPO_ROOT/mac/launcher/Sources/taOSLauncher/Resources/Info.plist.in" \ | ||||||
| "$fake_root/mac/launcher/Sources/taOSLauncher/Resources/Info.plist.in" | ||||||
| for path in tinyagentos static data app-catalog pyproject.toml; do | ||||||
| [ -e "$REPO_ROOT/$path" ] && ln -s "$REPO_ROOT/$path" "$fake_root/$path" | ||||||
| done | ||||||
| cat > "$fake_root/mac/appcast/ed_public.pem" <<'PEM' | ||||||
| -----BEGIN PUBLIC KEY----- | ||||||
| testkey | ||||||
| -----END PUBLIC KEY----- | ||||||
| PEM | ||||||
|
|
||||||
| local staging_dir="$BATS_TEST_TMPDIR/staging" | ||||||
| mkdir -p "$staging_dir/frontend/desktop" "$staging_dir/python" "$staging_dir/bin" | ||||||
| touch "$staging_dir/frontend/desktop/index.html" | ||||||
| touch "$staging_dir/bin/container" | ||||||
| mkdir -p "$staging_dir/Sparkle.framework/Versions/A" | ||||||
| touch "$staging_dir/Sparkle.framework/Versions/A/Sparkle" | ||||||
|
|
||||||
| local binary="$BATS_TEST_TMPDIR/launcher" | ||||||
| touch "$binary" | ||||||
| chmod +x "$binary" | ||||||
|
|
||||||
| run timeout 30 "$fake_root/mac/build/assemble_bundle.sh" \ | ||||||
| --release \ | ||||||
| --version "1.2.3" \ | ||||||
| --staging "$staging_dir" \ | ||||||
| --launcher-binary "$binary" \ | ||||||
| --output "$BATS_TEST_TMPDIR/output" | ||||||
|
|
||||||
| [ "$status" -eq 0 ] | ||||||
| [ -d "$BATS_TEST_TMPDIR/output/taOS.app/Contents/Frameworks/Sparkle.framework" ] | ||||||
| } | ||||||
|
|
||||||
| @test "no taos.app feed or download domain references under mac/" { | ||||||
| run grep -rE "(https?://taos\.app|taos\.app/(appcast|releases))" "$REPO_ROOT/mac" | ||||||
| [ "$status" -ne 0 ] | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Fail when
Proposed fix- [ "$status" -ne 0 ]
+ [ "$status" -eq 1 ]📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,22 +27,26 @@ def __init__(self, replay_size: int = 32) -> None: | |
| self._lock = asyncio.Lock() | ||
|
|
||
| async def subscribe(self, project_id: str) -> asyncio.Queue[ProjectEvent]: | ||
| queue: asyncio.Queue[ProjectEvent] = asyncio.Queue() | ||
| queue: asyncio.Queue[ProjectEvent] = asyncio.Queue(maxsize=self._replay_size) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🤖 get_repo_knowledge executed:
Length of output: 5023 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- events.py ---'
cat -n tinyagentos/projects/events.py | sed -n '1,130p'
printf '%s\n' '--- replay_size references ---'
rg -n -C 3 'replay_size|ProjectEventBroker|asyncio\.Queue' tinyagentos changelog.d CHANGELOG.md 2>/dev/null | head -240
printf '%s\n' '--- changed files ---'
git diff --stat
git status --shortRepository: jaylfc/taOS Length of output: 19304 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- relevant project event routes and tests ---'
rg -n -C 4 'subscribe\(|unsubscribe\(|publish\(|ProjectEventBroker|replay' tinyagentos --glob '*.py' | head -320
printf '%s\n' '--- constructor and changelog context ---'
rg -n -C 5 'class ProjectEventBroker|def __init__|replay_size' tinyagentos/projects/events.py
printf '%s\n' '--- available changelog fragment ---'
find changelog.d -maxdepth 1 -type f -print | sort | tail -20Repository: jaylfc/taOS Length of output: 26022 Use a separate positive queue limit when
🤖 Prompt for AI Agents |
||
| async with self._lock: | ||
| self._queues.setdefault(project_id, []).append(queue) | ||
| for ev in self._replay.get(project_id, ()): | ||
| queue.put_nowait(ev) | ||
| await queue.put(ev) | ||
| return queue | ||
|
|
||
| async def unsubscribe(self, project_id: str, queue: asyncio.Queue[ProjectEvent]) -> None: | ||
| async with self._lock: | ||
| qs = self._queues.get(project_id, []) | ||
| if queue in qs: | ||
| qs.remove(queue) | ||
| if not qs: | ||
| self._queues.pop(project_id, None) | ||
| self._replay.pop(project_id, None) | ||
|
|
||
| async def publish(self, project_id: str, event: ProjectEvent) -> None: | ||
| async with self._lock: | ||
| buf = self._replay.setdefault(project_id, deque(maxlen=self._replay_size)) | ||
| buf.append(event) | ||
| # Only iterate over a copy in case unsubscribe is called during iteration | ||
| for q in list(self._queues.get(project_id, [])): | ||
| q.put_nowait(event) | ||
| await q.put(event) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🤖 get_repo_knowledge executed:
Length of output: 5023 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- events.py ---'
cat -n tinyagentos/projects/events.py
printf '%s\n' '--- direct references ---'
rg -n -C 3 'EventBroker|subscribe\(|unsubscribe\(|publish\(' tinyagentos --glob '*.py'
printf '%s\n' '--- changelog files ---'
git ls-files 'changelog.d/*' | tail -20Repository: jaylfc/taOS Length of output: 40942 Move subscriber delivery outside
🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,6 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| import re | ||
| import time as _time | ||
| import uuid | ||
|
|
||
|
|
@@ -11,13 +10,15 @@ | |
| from fastapi import APIRouter, Depends, HTTPException, Request | ||
| from fastapi.responses import JSONResponse, StreamingResponse | ||
| from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator | ||
| from typing import Literal | ||
|
|
||
| from tinyagentos.agent_token_auth import ( | ||
| PROJECT_SCOPE_MISMATCH_DETAIL, | ||
| check_agent_project_grants, | ||
| check_agent_scope_for_project, | ||
| ) | ||
| from tinyagentos.auth_context import CurrentUser, current_user, require_owner_or_admin | ||
| from tinyagentos.auth_context import CurrentUser, current_user | ||
| from tinyagentos.projects.element_store import _SLUG_RE | ||
| from tinyagentos.projects.folders import ( | ||
| ensure_element_folder, | ||
| ensure_project_layout, | ||
|
|
@@ -28,8 +29,6 @@ | |
| logger = logging.getLogger(__name__) | ||
| router = APIRouter() | ||
|
|
||
| _SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,62}$") | ||
|
|
||
| # The documented task status enum surfaced by the kanban read endpoints. The | ||
| # store itself is more permissive internally (it also tracks ``cancelled`` and | ||
| # ``quarantined``), but the READ/aggregate API surface only advertises these | ||
|
|
@@ -206,11 +205,11 @@ async def update_project( | |
| request: Request, | ||
| user: CurrentUser = Depends(current_user), | ||
| ): | ||
| store = request.app.state.project_store | ||
| p = await store.get_project(project_id) | ||
| if p is None: | ||
| return JSONResponse({"error": "not found"}, status_code=404) | ||
| require_owner_or_admin(user, p["user_id"]) | ||
| pstore = request.app.state.project_store | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Bind When 🤖 Prompt for AI Agents |
||
| project_or_err = await _get_owned_project(pstore, project_id, user) | ||
| if isinstance(project_or_err, JSONResponse): | ||
| return project_or_err | ||
| p = project_or_err | ||
| try: | ||
| await store.update_project( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CRITICAL: Reply with |
||
| project_id, | ||
|
|
@@ -243,11 +242,11 @@ async def archive_project( | |
| request: Request, | ||
| user: CurrentUser = Depends(current_user), | ||
| ): | ||
| store = request.app.state.project_store | ||
| p = await store.get_project(project_id) | ||
| if p is None: | ||
| return JSONResponse({"error": "not found"}, status_code=404) | ||
| require_owner_or_admin(user, p["user_id"]) | ||
| pstore = request.app.state.project_store | ||
| project_or_err = await _get_owned_project(pstore, project_id, user) | ||
| if isinstance(project_or_err, JSONResponse): | ||
| return project_or_err | ||
| p = project_or_err | ||
| await store.set_status(project_id, "archived") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CRITICAL: Reply with |
||
| p = await store.get_project(project_id) | ||
| await store.log_activity(project_id, user.user_id, "project.archived", {}) | ||
|
|
@@ -260,11 +259,11 @@ async def delete_project( | |
| request: Request, | ||
| user: CurrentUser = Depends(current_user), | ||
| ): | ||
| store = request.app.state.project_store | ||
| project = await store.get_project(project_id) | ||
| if project is None: | ||
| return JSONResponse({"error": "not found"}, status_code=404) | ||
| require_owner_or_admin(user, project["user_id"]) | ||
| pstore = request.app.state.project_store | ||
| project_or_err = await _get_owned_project(pstore, project_id, user) | ||
| if isinstance(project_or_err, JSONResponse): | ||
| return project_or_err | ||
| project = project_or_err | ||
|
|
||
| await store.set_status(project_id, "deleted") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CRITICAL: Reply with |
||
| await store.log_activity(project_id, user.user_id, "project.deleted", {}) | ||
|
|
@@ -307,11 +306,11 @@ async def add_member( | |
| request: Request, | ||
| user: CurrentUser = Depends(current_user), | ||
| ): | ||
| store = request.app.state.project_store | ||
| project = await store.get_project(project_id) | ||
| if project is None: | ||
| return JSONResponse({"error": "project not found"}, status_code=404) | ||
| require_owner_or_admin(user, project["user_id"]) | ||
| pstore = request.app.state.project_store | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CRITICAL: The refactor replaced Reply with |
||
| project_or_err = await _get_owned_project(pstore, project_id, user) | ||
| if isinstance(project_or_err, JSONResponse): | ||
| return project_or_err | ||
| project = project_or_err | ||
|
|
||
| if payload.mode == "native": | ||
| if not payload.agent_id: | ||
|
|
@@ -397,11 +396,11 @@ async def set_project_lead( | |
| Session-only (owner or admin, same gate as the members routes). A member id | ||
| not in the project returns 404. | ||
| """ | ||
| store = request.app.state.project_store | ||
| p = await store.get_project(project_id) | ||
| if p is None: | ||
| return JSONResponse({"error": "not found"}, status_code=404) | ||
| require_owner_or_admin(user, p["user_id"]) | ||
| pstore = request.app.state.project_store | ||
| project_or_err = await _get_owned_project(pstore, project_id, user) | ||
| if isinstance(project_or_err, JSONResponse): | ||
| return project_or_err | ||
| p = project_or_err | ||
| try: | ||
| await store.set_lead(project_id, body.member_id) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CRITICAL: Reply with |
||
| except KeyError as e: | ||
|
|
@@ -431,11 +430,11 @@ async def remove_member( | |
| request: Request, | ||
| user: CurrentUser = Depends(current_user), | ||
| ): | ||
| store = request.app.state.project_store | ||
| project = await store.get_project(project_id) | ||
| if project is None: | ||
| return JSONResponse({"error": "not found"}, status_code=404) | ||
| require_owner_or_admin(user, project["user_id"]) | ||
| pstore = request.app.state.project_store | ||
| project_or_err = await _get_owned_project(pstore, project_id, user) | ||
| if isinstance(project_or_err, JSONResponse): | ||
| return project_or_err | ||
| project = project_or_err | ||
| await store.remove_member(project_id, member_id) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CRITICAL: Reply with |
||
| await store.log_activity(project_id, user.user_id, "member.removed", {"member_id": member_id}) | ||
| members = await store.list_members(project_id) | ||
|
|
@@ -1436,7 +1435,7 @@ class AddCommentIn(_TaskRequestModelMixin, BaseModel): | |
| replies_to_comment_id: str | None = None | ||
|
|
||
|
|
||
| class CreateChecklistItemIn(BaseModel): | ||
| class CreateChecklistItemIn(_TaskRequestModelMixin, BaseModel): | ||
| text: str | ||
|
|
||
|
|
||
|
|
@@ -1850,7 +1849,7 @@ async def delete_element( | |
| project_id: str, | ||
| element_id: str, | ||
| request: Request, | ||
| mode: str = "strict", | ||
| mode: Literal["strict", "untag"] = "strict", | ||
| user: CurrentUser = Depends(current_user), | ||
| ): | ||
| pstore = request.app.state.project_store | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Check that the framework payload is bundled.
The directory check passes if the bundle creates an empty
Sparkle.framework. Check the staged executable path so this test verifies thatassemble_bundle.shcopied the framework payload.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents