Implement the designated-target write guard with an allow/refuse matrix #196
Labels
No labels
bug
chore
documentation
enhancement
feature
ready
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
david/pi-extensions-and-skills#196
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implement the designated-target write guard: a session-scoped allow-list plus an authorisation tool, so the extension writes only to files it created in a nominated project, or to a file the user explicitly named. Every write path checks it before sending anything.
Background
Depends on: #195
Frictionless multi-screen authoring and "the agent scribbled over my real designs" are in tension. The design resolves it structurally rather than by per-write confirmation: bulk screen creation inside a nominated scope stays smooth, while a write to anything outside that scope is refused and reported with an actionable message.
The allow-list has exactly two provenances:
The guard is a pure decision function plus a small session store, so the allow/refuse matrix is unit-testable without HTTP, and every write path calls the same one function.
Documentation Required
A separate process downloads these into the listed folders before this issue is implemented. Check the folders for the actual reference material before starting.
docs/reference/penpot-api/<PENPOT_URL>/api/main/doc/openapi.json— confirmget-file's project/team fields (needed to map a file id to its project) and the permission-related fields returned for a team.docs/reference/pi-coding-agent/descriptionauthoring (this tool's description carries the safety contract), and how to return a refusal as a normal tool result rather than an exception.docs/reference/typebox/file_idorproject_id" in a parameter schema; use a union/optional pair with an explicit runtime check if the schema cannot express it.Implementation Details
extensions/penpot/src/guard.ts— pure, unit-testedState (session-scoped, module-level):
Functions:
authorizeFile(fileId, note?)/authorizeProject(projectId, note?)— record a designation.registerCreatedFile(fileId, projectId)— called by any tool that creates a file.resolveFileProject(fileId)— look up a file's project (viaget-file, then team/project fields), used only when a file is not increatedFilesand a project nomination might cover it.isWriteAllowed(fileId, deps)→{ allowed: true, provenance: "created" | "named" | "nominated-project" } | { allowed: false, reason, hint }.allowedwhen the file is increatedFiles, or inallowedFileIds, or resolves to a project innominatedProjectIds.reasondistinguishes: file unknown to the allow-list; project not nominated; resolution failed (could not determine the file's project) — the last must fail closed, never default to allowed.hintis the actionable next step: name the file withpenpot_authorize_target, or nominate its project.clearAuthorizations()— used by tests and by session reset.extensions/penpot/src/tools/target.ts—penpot_authorize_targetParameters: exactly one of
file_id(string) orproject_id(string), plusnote(string, optional). Validating "exactly one" is a runtime check with a clear error. The tool description must state plainly: only call this for a file or project the user has explicitly named; it is the write allow-list.Returns the current allow-list contents (so the agent can see the effective scope) in text and in
details.Wiring
penpot_commit(and every later write tool) callsisWriteAllowedbefore building the request. A refusal returns a normal tool result explaining what was refused, why, and the exact authorisation call that would permit it — nothing is written and the changeset stays staged.penpot_commitby replacing the permissive placeholder left by the previous step.penpot_authorize_targetinsrc/index.ts.Acceptance Criteria
createdFilesis allowed, and the provenance is reported ascreated.named.nominatedProjectIdsis allowed, provenancenominated-project.penpot_authorize_target.penpot_commitagainst a non-designated file is refused, reports the refusal, and leaves the staged changeset intact.penpot_authorize_targetrequires exactly one offile_id/project_idand errors clearly when given neither or both.Test Plan
Live validation (requires
PENPOT_URL/PENPOT_TOKEN, scratch project):penpot_commit→ expect a refusal naming the authorisation tool, and confirm by re-reading that the file'srevndid not change.penpot_authorize_targetwith thatfile_id, then commit again → expect success and the assets present.penpot_authorize_targetwith the scratch project'sproject_id, then commit to a second file in that project → expect success; commit to a file in a different project → expect refusal.penpot_authorize_targetwith both parameters and with neither → expect clear errors.pi-loop opened and merged a pull request for this issue: #229