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:
@@ -37,6 +37,10 @@ export interface Snippet {
|
||||
updated_at: string;
|
||||
snippet: SnippetFields;
|
||||
systems?: { id: number; name: string }[];
|
||||
/** Set when another user owns this record; `owner` is their username and
|
||||
* `permission` your access level on it. */
|
||||
shared?: boolean;
|
||||
owner?: string | null;
|
||||
}
|
||||
|
||||
/** Lightweight list item from the knowledge preview feed. Note: the `snippet`
|
||||
@@ -50,6 +54,10 @@ export interface SnippetListItem {
|
||||
snippet: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
/** Present only when the record belongs to someone else — a suggestion from
|
||||
* them, not one of your own. Absent means it's yours. */
|
||||
shared?: boolean;
|
||||
owner?: string | null;
|
||||
}
|
||||
|
||||
/** Create/update payload — discrete fields the backend serializes into the
|
||||
|
||||
@@ -83,6 +83,12 @@ async function confirmDelete() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="snippet.shared" class="shared-notice">
|
||||
Shared by <strong>{{ snippet.owner ?? "another user" }}</strong> — their
|
||||
suggestion, not one of your own records. Worth weighing on its merits
|
||||
before you build on it.
|
||||
</p>
|
||||
|
||||
<p v-if="snippet.snippet.when_to_use" class="when-to-use">
|
||||
{{ snippet.snippet.when_to_use }}
|
||||
</p>
|
||||
@@ -215,6 +221,19 @@ async function confirmDelete() {
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
/* Shown only for a record another user owns. Deliberately above the code: the
|
||||
provenance has to be read before the implementation is trusted. */
|
||||
.shared-notice {
|
||||
margin: 0.75rem 0 0;
|
||||
padding: 0.6rem 0.85rem;
|
||||
border-left: 3px solid var(--color-text-muted);
|
||||
border-radius: 6px;
|
||||
background: var(--color-bg-secondary);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
|
||||
@@ -181,6 +181,9 @@ function languageOf(tags: string[]): string {
|
||||
</p>
|
||||
<div class="card-footer">
|
||||
<span class="meta-date">Updated {{ new Date(s.updated_at).toLocaleDateString() }}</span>
|
||||
<span v-if="s.shared" class="shared-tag" :title="`Shared by ${s.owner ?? 'another user'} — a suggestion, not your own record`">
|
||||
by {{ s.owner ?? "another user" }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -417,11 +420,25 @@ function languageOf(tags: string[]): string {
|
||||
|
||||
.card-footer {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.meta-date {
|
||||
font-size: 0.73rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
/* Marks a record someone else owns. Present only on shared rows, so an
|
||||
unmarked card is unambiguously the operator's own. */
|
||||
.shared-tag {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
background: color-mix(in srgb, var(--color-text-muted) 15%, transparent);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Header + select-mode */
|
||||
.header-actions {
|
||||
|
||||
Reference in New Issue
Block a user