feat(acl): shared records are search-only and always labelled as someone else's
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 28s
CI & Build / integration (push) Successful in 35s
CI & Build / Python tests (push) Failing after 41s
CI & Build / Build & push image (push) Has been skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 28s
CI & Build / integration (push) Successful in 35s
CI & Build / Python tests (push) Failing after 41s
CI & Build / Build & push image (push) Has been skipped
Narrows theb7d6fc7widening per the operator's call, and fixes a regression it introduced. Decision recorded as note 2094. Two scopes now, deliberately different: readable_notes_clause — everything the ACL permits, including records reached only via a direct/group note share. For EXPLICIT acts: a search the caller typed, a fetch by id. browsable_notes_clause — the caller's own records plus anything in a project they can reach. For PASSIVE surfaces: browse lists, facet counts, the process->skill manifest. The split is a trust boundary. Anything appearing unasked — in your own list, your own counts, or as a skill installed on your machine — reads as material you endorsed. A one-off someone shared with you hasn't earned that standing, so it waits until you go looking. This also dissolves the shared-Process problem by construction rather than by special case: the manifest is a passive surface, so a directly-shared Process is never installed as an auto-surfacing skill. Regression fix (#2093):b7d6fc7widened the list queries but left the fetch path owner-only, so on the MCP path a record could be listed and then not opened — get_snippet raised not-found, get_process couldn't resolve, and the manifest emitted stubs whose get_process call would fail. snippets.get_snippet and notes.resolve_process now resolve the read scope. delete_snippet gained an explicit can_write_note guard, since being able to SEE a shared snippet must not imply being able to bin it. Provenance, so nothing arrives looking like the operator's own work: - access.describe_provenance / label_shared_items add shared/owner/permission; labelling costs no query when everything is the caller's own. - MCP: get_snippet, list_snippets, get_process and list_processes carry it, and get_process now says outright NOT to follow a shared process verbatim — its follow-as-written contract was the sharpest instance of the problem. - The skill stub for a shared Process names its author and asks for a go-ahead, instead of describing it as "the operator's saved Scribe process". - Policy stated once in the MCP _INSTRUCTIONS and the reusing-code skill: a shared record is that person's suggestion, weigh it, attribute it, ask before adopting it. - UI: shared snippets show "by <owner>" in the list and a notice above the code in the detail view, reusing SharedWithMeView's vocabulary. Plugin 0.1.15 -> 0.1.16 (skill text changed). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLwAaV4DQEmVyn496HnEvt
This commit is contained in:
@@ -412,15 +412,23 @@ async def convert_task_to_note(user_id: int, note_id: int) -> Note:
|
||||
async def resolve_process(user_id: int, name_or_id) -> tuple[Note | None, list[dict]]:
|
||||
"""Resolve a stored process by id or name.
|
||||
|
||||
Owner-scoped, note_type='process', non-trashed. Precedence: numeric id →
|
||||
exact case-insensitive title → substring. Returns (note, other_candidates);
|
||||
on a substring tie with no exact hit, `note` is the most-recently-updated
|
||||
match and `other_candidates` lists the rest as [{id, title}] so the caller
|
||||
can disambiguate. Returns (None, []) when nothing matches.
|
||||
note_type='process', non-trashed, scoped to what this user may READ — owned
|
||||
plus shared (rule #78). Naming one is an explicit act, so a Process shared
|
||||
with the caller resolves here even though it is deliberately absent from the
|
||||
passive process list and the skill manifest (decision note 2094); without
|
||||
this, a Process a search surfaced could not then be run — see #2093.
|
||||
|
||||
Precedence: numeric id → exact case-insensitive title → substring. Returns
|
||||
(note, other_candidates); on a substring tie with no exact hit, `note` is the
|
||||
most-recently-updated match and `other_candidates` lists the rest as
|
||||
[{id, title}] so the caller can disambiguate. Returns (None, []) when nothing
|
||||
matches.
|
||||
"""
|
||||
from scribe.services.access import readable_notes_clause
|
||||
visible = await readable_notes_clause(user_id)
|
||||
async with async_session() as session:
|
||||
base = select(Note).where(
|
||||
Note.user_id == user_id,
|
||||
visible,
|
||||
Note.note_type == "process",
|
||||
Note.deleted_at.is_(None),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user