Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions tinyagentos/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,8 +398,11 @@ async def _probe_backend(backend: dict) -> dict:
from tinyagentos.projects.canvas.store import ProjectCanvasStore as ProjectCanvasStoreImpl
from tinyagentos.projects.canvas.snapshotter import CanvasSnapshotter
from tinyagentos.projects.invite_store import ProjectInviteStore
from tinyagentos.projects.lists_store import ProjectListsStore, ProjectListEntriesStore
project_store = ProjectStore(data_dir / "projects.db")
project_invite_store = ProjectInviteStore(data_dir / "project_invites.db")
project_lists_store = ProjectListsStore(data_dir / "project_lists.db")
project_list_entries_store = ProjectListEntriesStore(data_dir / "project_list_entries.db")
Comment on lines +401 to +405

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Missing projectlistsstore class 🐞 Bug ≡ Correctness

tinyagentos/app.py imports and instantiates ProjectListsStore, but
tinyagentos/projects/lists_store.py defines only ProjectListEntriesStore, causing ImportError during
backend startup/probe. This prevents the FastAPI app from initializing stores and serving requests.
Agent Prompt
## Issue description
`ProjectListsStore` is imported/instantiated during app setup but is not defined in `tinyagentos/projects/lists_store.py`, which will raise `ImportError` and crash startup.

## Issue Context
`tinyagentos/app.py` now wires both `ProjectListsStore` and `ProjectListEntriesStore` into `app.state` and calls `.init()` during lifespan.

## Fix Focus Areas
- tinyagentos/projects/lists_store.py[1-182]
- tinyagentos/app.py[401-405]
- tinyagentos/projects/__init__.py[1-16]

## Suggested fix
- Either implement a `ProjectListsStore` class (with its own `SCHEMA` for lists metadata and the API expected by routes), or stop importing/instantiating/exporting it until it exists.
- Ensure the store(s) you keep are initialized in lifespan and exposed via `app.state` consistently with how other stores are wired.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

project_event_broker = ProjectEventBroker()
from tinyagentos.desktop_control import DesktopCommandBroker
desktop_command_broker = DesktopCommandBroker()
Expand Down Expand Up @@ -575,6 +578,8 @@ async def lifespan(app: FastAPI):
await project_task_store.init()
await project_element_store.init()
await routine_store.init()
await project_lists_store.init()
await project_list_entries_store.init()
await project_canvas_store.init()
await decision_store.init()
await execution_policy_store.init()
Expand Down Expand Up @@ -1606,6 +1611,8 @@ async def dispatch(self, request, call_next):
app.state.coding_session_store = coding_session_store
app.state.beads_bridge = None
app.state.canvas_snapshotter = None
app.state.project_lists_store = project_lists_store
app.state.project_list_entries_store = project_list_entries_store
projects_root.mkdir(parents=True, exist_ok=True)
app.state.projects_root = projects_root
app.state.chat_hub = chat_hub
Expand Down
16 changes: 16 additions & 0 deletions tinyagentos/projects/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from tinyagentos.projects.element_store import ProjectElementStore
from tinyagentos.projects.invite_store import ProjectInviteStore
from tinyagentos.projects.lists_store import ProjectListsStore, ProjectListEntriesStore
from tinyagentos.projects.project_store import ProjectStore
from tinyagentos.projects.routines_store import ProjectRoutinesStore
from tinyagentos.projects.task_store import ProjectTaskStore

__all__ = [
"ProjectElementStore",
"ProjectInviteStore",
"ProjectListsStore",
"ProjectListEntriesStore",
"ProjectStore",
"ProjectRoutinesStore",
"ProjectTaskStore",
]
182 changes: 182 additions & 0 deletions tinyagentos/projects/lists_store.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
from __future__ import annotations

import time

from tinyagentos.base_store import BaseStore
from tinyagentos.projects.ids import new_id

class ProjectListEntriesStore(BaseStore):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Implement or stop importing ProjectListsStore. The new module defines ProjectListEntriesStore only, but both package export and app startup import ProjectListsStore; after the syntax error is fixed, this raises ImportError and prevents application startup.

  • tinyagentos/projects/lists_store.py#L8-L8: add the ProjectListsStore implementation expected by this PR.
  • tinyagentos/projects/__init__.py#L3-L3: export only symbols actually provided by tinyagentos.projects.lists_store.
  • tinyagentos/app.py#L401-L401: construct only implemented stores, or use the newly added ProjectListsStore.
📍 Affects 3 files
  • tinyagentos/projects/lists_store.py#L8-L8 (this comment)
  • tinyagentos/projects/__init__.py#L3-L3
  • tinyagentos/app.py#L401-L401
🤖 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 `@tinyagentos/projects/lists_store.py` at line 8, Resolve the missing
ProjectListsStore contract: in tinyagentos/projects/lists_store.py:8-8,
implement ProjectListsStore alongside ProjectListEntriesStore or remove its
usage; in tinyagentos/projects/__init__.py:3-3, export only symbols actually
defined by lists_store; and in tinyagentos/app.py:401-401, construct only
implemented stores or use the implemented ProjectListsStore.

SCHEMA = """
CREATE TABLE IF NOT EXISTS project_list_entries (
id TEXT PRIMARY KEY,
list_id TEXT NOT NULL,
project_id TEXT NOT NULL,
text TEXT NOT NULL,
original_text TEXT NOT NULL,
category TEXT,
status TEXT NOT NULL DEFAULT 'new',
done INTEGER NOT NULL DEFAULT 0,
author_kind TEXT NOT NULL,
author_id TEXT NOT NULL,
edited_by TEXT,
position INTEGER NOT NULL,
created_at REAL NOT NULL,
updated_at REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_entries_list ON project_list_entries(list_id);
CREATE INDEX IF NOT EXISTS idx_entries_project ON project_list_entries(project_id);
CREATE INDEX IF NOT EXISTS idx_entries_list_project ON project_list_entries(list_id, project_id);
CREATE INDEX IF NOT EXISTS idx_entries_status ON project_list_entries(project_id, status);
"""
MIGRATIONS = [] # No migrations needed per spec

async def add_entry(
self,
list_id: str,
project_id: str,
text: str,
original_text: str,
author_kind: str,
author_id: str,
category: str | None = None,
position: int | None = None,
) -> dict:
entry_id = new_id("lle")
now = time.time()

# Store immutability: original_text cannot be changed after creation
# It is stored verbatim as the first written text
await self._db.execute(
"INSERT INTO project_list_entries (
id, list_id, project_id, text, original_text, category, status,
done, author_kind, author_id, position, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
Comment on lines +49 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ruff check tinyagentos/projects/lists_store.py

Repository: jaylfc/taOS

Length of output: 20372


Fix add_entry() so it can be parsed.

The current line 50 multi-line string uses a normal double-quoted string, which leaves the ruff/Python parser with multiple invalid-string and cascade parsing errors instead of a concise “missing closing quote” for just this SQL statement. Wrap the SQL in a triple-quoted string, or keep it on one line and pass the bind tuple as the second argument to execute().

🧰 Tools
🪛 OpenGrep (1.25.0)

[ERROR] 49-59: SQL query built via f-string passed to execute()/executemany(). Use parameterized queries with placeholders instead.

(coderabbit.sql-injection.python-fstring-execute)

🪛 Ruff (0.15.21)

[warning] 50-50: missing closing quote in string literal

(invalid-syntax)


[warning] 51-51: Expected ,, found name

(invalid-syntax)


[warning] 53-53: Simple statements must be separated by newlines or semicolons

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: missing closing quote in string literal

(invalid-syntax)

🤖 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 `@tinyagentos/projects/lists_store.py` around lines 49 - 53, Update add_entry()
so the multiline SQL passed to self._db.execute uses a properly closed
triple-quoted string, preserving the existing INSERT statement and bind
parameters while eliminating parser errors.

Source: Linters/SAST tools

(
entry_id, list_id, project_id, text, original_text,
category, "new", 0, author_kind, author_id,
position if position is not None else 0, now, now,
Comment on lines +42 to +57

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Append entries at the next list position.

When position is omitted, every entry is stored at position 0; after any reorder, new entries can appear before existing entries. Use the existing _get_next_position() helper instead.

Proposed fix
     ) -> dict:
         entry_id = new_id("lle")
         now = time.time()
+        if position is None:
+            position = await self._get_next_position(project_id, list_id)
         
         # Store immutability: original_text cannot be changed after creation
@@
-                position if position is not None else 0, now, now,
+                position, now, now,
📝 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.

Suggested change
position: int | None = None,
) -> dict:
entry_id = new_id("lle")
now = time.time()
# Store immutability: original_text cannot be changed after creation
# It is stored verbatim as the first written text
await self._db.execute(
"INSERT INTO project_list_entries (
id, list_id, project_id, text, original_text, category, status,
done, author_kind, author_id, position, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(
entry_id, list_id, project_id, text, original_text,
category, "new", 0, author_kind, author_id,
position if position is not None else 0, now, now,
position: int | None = None,
) -> dict:
entry_id = new_id("lle")
now = time.time()
if position is None:
position = await self._get_next_position(project_id, list_id)
# Store immutability: original_text cannot be changed after creation
# It is stored verbatim as the first written text
await self._db.execute(
"INSERT INTO project_list_entries (
id, list_id, project_id, text, original_text, category, status,
done, author_kind, author_id, position, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(
entry_id, list_id, project_id, text, original_text,
category, "new", 0, author_kind, author_id,
position, now, now,
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 50-50: missing closing quote in string literal

(invalid-syntax)


[warning] 51-51: Expected ,, found name

(invalid-syntax)


[warning] 53-53: Simple statements must be separated by newlines or semicolons

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: Expected an expression or a ')'

(invalid-syntax)


[warning] 53-53: Got unexpected token ?

(invalid-syntax)


[warning] 53-53: missing closing quote in string literal

(invalid-syntax)


[warning] 54-54: Unexpected indentation

(invalid-syntax)

🤖 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 `@tinyagentos/projects/lists_store.py` around lines 42 - 57, When creating an
entry in the method containing the INSERT, replace the omitted-position fallback
of 0 with the result of the existing _get_next_position() helper. Preserve
explicitly supplied positions while ensuring new entries append after the
current list contents.

),
)
await self._db.commit()
return await self.get_entry(entry_id)

async def get_entry(self, entry_id: str) -> dict | None:
async with self._db.execute(
"SELECT * FROM project_list_entries WHERE id = ?", (entry_id,)
) as cur:
row = await cur.fetchone()
if row is None:
return None
keys = [d[0] for d in cur.description]
return dict(zip(keys, row))

async def list_entries(
self,
project_id: str,
list_id: str | None = None,
status: str | None = None,
category: str | None = None,
) -> list[dict]:
where_clause = "project_id = ?"
params = [project_id]

if list_id is not None:
where_clause += " AND list_id = ?"
params.append(list_id)

if status is not None:
where_clause += " AND status = ?"
params.append(status)

if category is not None:
where_clause += " AND category = ?"
params.append(category)

async with self._db.execute(
f"SELECT * FROM project_list_entries WHERE {where_clause} ORDER BY position ASC, created_at ASC",
params,
) as cur:
rows = await cur.fetchall()
keys = [d[0] for d in cur.description]
return [dict(zip(keys, r)) for r in rows]

async def update_entry(
self,
entry_id: str,
text: str | None = None,
category: str | None = None,
status: str | None = None,
done: int | None = None,
position: int | None = None,
edited_by: str | None = None,
) -> dict | None:
# Enforce immutability: original_text cannot be changed
# Only text, category, status, done, position can be updated
sets = []
params = []

if text is not None:
sets.append("text = ?")
params.append(text)

if category is not None:
sets.append("category = ?")
params.append(category)

if status is not None:
sets.append("status = ?")
params.append(status)

if done is not None:
sets.append("done = ?")
params.append(done)

if position is not None:
sets.append("position = ?")
params.append(position)

if edited_by is not None:
sets.append("edited_by = ?")
params.append(edited_by)

if not sets:
return await self.get_entry(entry_id)

sets.append("updated_at = ?")
params.append(time.time())
params.append(entry_id)

# Before updating, verify that original_text cannot be modified
existing = await self.get_entry(entry_id)
if existing is None:
return None

await self._db.execute(
f"UPDATE project_list_entries SET {', '.join(sets)} WHERE id = ?",
params,
)
await self._db.commit()
return await self.get_entry(entry_id)

async def delete_entry(self, entry_id: str) -> bool:
cursor = await self._db.execute(
"DELETE FROM project_list_entries WHERE id = ?", (entry_id,)
)
await self._db.commit()
return cursor.rowcount == 1

async def reorder_entries(self, project_id: str, entries: list[dict]) -> None:
for entry in entries:
await self._db.execute(
"UPDATE project_list_entries SET position = ?, updated_at = ? WHERE id = ? AND project_id = ?",
(entry["position"], time.time(), entry["id"], project_id),
)
await self._db.commit()

async def _get_next_position(self, project_id: str, list_id: str | None = None) -> int:
async with self._db.execute(
"SELECT MAX(position) + 1 FROM project_list_entries WHERE project_id = ? AND list_id = ?",
(project_id, list_id or ""),
) as cur:
Comment on lines +176 to +180

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. Next position computed incorrectly 🐞 Bug ≡ Correctness

ProjectListEntriesStore._get_next_position converts list_id=None into an empty string, but list_id
is stored as a real non-empty identifier, so the query will not match existing rows and will return
0. Any caller using this helper with list_id omitted will repeatedly assign position 0 and break
ordering semantics.
Agent Prompt
## Issue description
`_get_next_position()` substitutes `None` list_id with `""`, which won’t match normal stored `list_id` values, causing the computed next position to reset to 0.

## Issue Context
Schema enforces `list_id TEXT NOT NULL`, and `add_entry()` inserts the provided `list_id` verbatim.

## Fix Focus Areas
- tinyagentos/projects/lists_store.py[10-24]
- tinyagentos/projects/lists_store.py[176-182]

## Suggested fix
- Make `list_id` a required parameter for `_get_next_position(project_id, list_id)` and remove the `or ""` fallback; or
- If `None` is intended to mean “across all lists in project”, change the SQL to omit the `list_id = ?` predicate when `list_id is None`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

row = await cur.fetchone()
return (row[0] if row[0] is not None else 0)
13 changes: 12 additions & 1 deletion tinyagentos/routes/agent_auth_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@
# scope keeps an existing approval meaning exactly what it meant when it was
# given, and makes authoring an explicit per-agent opt-in.
"project_tasks_create",
# Lists access: idea capture with agent triage (statuses, checkboxes, agent tidy)
# Least-privilege read + write on the agent's OWN project only, bound by the
# token's project_id claim.
"lists_read",
"lists_write",
Comment on lines +65 to +69

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. _allowed_scopes missing lists scopes 📜 Skill insight ≡ Correctness

lists_read and lists_write were added to VALID_SCOPES but not to _ALLOWED_SCOPES in
agent_registry.py, so the two scope sets are no longer synchronized. This likely breaks the
equality check/test and can prevent minting/granting the new scopes consistently.
Agent Prompt
## Issue description
The PR adds new scopes (`lists_read`, `lists_write`) to the consent-flow `VALID_SCOPES`, but `tinyagentos/routes/agent_registry.py`'s `_ALLOWED_SCOPES` is not updated, violating the requirement that these sets stay synchronized.

## Issue Context
A test asserts these scope sets are equal. If they drift, registry minting/validation paths may reject scopes that the consent flow considers valid.

## Fix Focus Areas
- tinyagentos/routes/agent_registry.py[87-100]
- tinyagentos/routes/agent_auth_requests.py[43-81]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

# Canvas access: read and write on a specific project's canvas. Like
# project_tasks, a project_id is required so the token is bound to the
# operator-validated project rather than whatever the unauthenticated agent
Expand Down Expand Up @@ -194,7 +199,13 @@ async def _retire_scope_request_notification(request: Request, request_id: str)
# project_tasks_create globally. One definition, referenced everywhere.
_CANVAS_SCOPES = {"canvas_read", "canvas_write"}
_FILES_SCOPES = {"files_read", "files_write"}
_PROJECT_SCOPES = {"project_tasks", "project_tasks_create"} | _CANVAS_SCOPES | _FILES_SCOPES
_LISTS_SCOPES = {"lists_read", "lists_write"}
_PROJECT_SCOPES = {
"project_tasks",
"project_tasks_create",
"lists_read",
"lists_write",
} | _CANVAS_SCOPES | _FILES_SCOPES | _LISTS_SCOPES
Comment on lines +202 to +208

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Scope-set test will fail 🐞 Bug ☼ Reliability

_PROJECT_SCOPES now includes lists_read/lists_write, but tests/test_agent_scope_requests.py asserts
an exact set without those entries, so CI will fail. This blocks verification of the PR until the
expected scope set is updated.
Agent Prompt
## Issue description
The PR expands `_PROJECT_SCOPES` to include `lists_read` and `lists_write`, but the unit test still asserts the previous exact set contents.

## Issue Context
`tests/test_agent_scope_requests.py::test_project_scope_set_is_a_single_definition` compares `_PROJECT_SCOPES` to a hard-coded set.

## Fix Focus Areas
- tinyagentos/routes/agent_auth_requests.py[200-209]
- tests/test_agent_scope_requests.py[554-580]

## Suggested fix
- Update the expected set in the test to include `lists_read` and `lists_write`.
- Keep the alias identity assertions (`is`) intact so drift is still caught.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



def _get_approve_lock(request: Request, request_id: str) -> asyncio.Lock:
Expand Down
Loading