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:
@@ -25,6 +25,7 @@ from scribe.services import knowledge as knowledge_svc
|
||||
from scribe.services import notes as notes_svc
|
||||
from scribe.services import projects as projects_svc
|
||||
from scribe.services import rulebooks as rulebooks_svc
|
||||
from scribe.services.access import label_shared_items
|
||||
from scribe.services.embeddings import semantic_search_notes
|
||||
from scribe.services.retrieval_telemetry import record_retrieval
|
||||
from scribe.services.settings import get_setting
|
||||
@@ -74,13 +75,22 @@ async def build_process_manifest(user_id: int) -> dict:
|
||||
(note_type='process'). Instance-agnostic: derived from whatever Processes the
|
||||
calling install owns, no operator-specific coupling.
|
||||
|
||||
Returns {"processes": [{id, name, slug, description}], "total": int}.
|
||||
Slugs are unique within the result (a collision gets an -<id> suffix).
|
||||
SCOPE: this is the most consequential passive surface Scribe has — every
|
||||
entry becomes a skill file on the operator's machine that auto-surfaces and
|
||||
is followed as written. It therefore uses the BROWSE scope (via the
|
||||
no-query knowledge list): a Process shared directly with the operator is
|
||||
never installed here, only one they own or reach through a shared project
|
||||
(decision note 2094). Project-shared entries are labelled with their owner so
|
||||
the stub can't pass off someone else's procedure as the operator's own.
|
||||
|
||||
Returns {"processes": [{id, name, slug, description, shared?, owner?}],
|
||||
"total": int}. Slugs are unique within the result (collision gets -<id>).
|
||||
"""
|
||||
items, _ = await knowledge_svc.query_knowledge(
|
||||
user_id=user_id, note_type="process", tags=[], sort="modified",
|
||||
q=None, limit=100, offset=0,
|
||||
)
|
||||
items = await label_shared_items(user_id, items)
|
||||
procs: list[dict] = []
|
||||
seen: set[str] = set()
|
||||
for it in items:
|
||||
@@ -95,16 +105,32 @@ async def build_process_manifest(user_id: int) -> dict:
|
||||
preview = " ".join((it.get("snippet") or "").split())
|
||||
if len(preview) > _PROC_PREVIEW_CHARS:
|
||||
preview = preview[:_PROC_PREVIEW_CHARS].rstrip() + "…"
|
||||
description = (
|
||||
f'Run the operator\'s saved Scribe process "{title}".'
|
||||
+ (f" {preview}" if preview else "")
|
||||
+ f' Use when {title}-type work is requested, or when asked to run'
|
||||
f' the "{title}" process.'
|
||||
)
|
||||
procs.append({
|
||||
if it.get("shared"):
|
||||
owner = it.get("owner") or "another user"
|
||||
description = (
|
||||
f'A shared Scribe process "{title}", authored by {owner} — NOT the'
|
||||
f" operator's own."
|
||||
+ (f" {preview}" if preview else "")
|
||||
+ f' Use when {title}-type work is requested, or when asked to run'
|
||||
f' the "{title}" process — but summarise it and get the operator\'s'
|
||||
f" go-ahead before following it, since it reflects {owner}'s"
|
||||
f" judgement rather than theirs."
|
||||
)
|
||||
else:
|
||||
description = (
|
||||
f'Run the operator\'s saved Scribe process "{title}".'
|
||||
+ (f" {preview}" if preview else "")
|
||||
+ f' Use when {title}-type work is requested, or when asked to run'
|
||||
f' the "{title}" process.'
|
||||
)
|
||||
entry = {
|
||||
"id": it["id"], "name": title, "slug": slug,
|
||||
"description": description,
|
||||
})
|
||||
}
|
||||
if it.get("shared"):
|
||||
entry["shared"] = True
|
||||
entry["owner"] = it.get("owner")
|
||||
procs.append(entry)
|
||||
return {"processes": procs, "total": len(procs)}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user