Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
8.0 KiB
status, system, requirements, created, owners
| status | system | requirements | created | owners | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| current | plugins |
|
2026-09-07 |
|
Plugin Capability Approval and Audit System Design
Purpose and boundaries
This design describes the generic plugin capability-approval substrate in
apps/backend/internal/plugins: the host-minted installation identity, the
workspace-scoped approval ledger, the append-only event trail, the
fail-closed authorization decision, uninstall tombstones, the legacy v1
fence, and the audit receipt primitives. Higher-level surfaces (approval UI,
coordinator flows, host receipt persistence) consume this substrate without
being part of it.
Requirement mapping
| Requirement | Design section |
|---|---|
REQ-PLUGINS-CAPABILITY-APPROVAL-001 |
Identity and approval ledger |
REQ-PLUGINS-CAPABILITY-APPROVAL-002 |
Append-only event trail |
REQ-PLUGINS-CAPABILITY-APPROVAL-003 |
Fail-closed authorization |
REQ-PLUGINS-CAPABILITY-APPROVAL-004 |
Tombstones and reinstall identity |
REQ-PLUGINS-CAPABILITY-APPROVAL-005 |
Legacy v1 fence |
REQ-PLUGINS-CAPABILITY-APPROVAL-006 |
Audit receipts |
Identity and approval ledger
store.Record.InstallationID (apps/backend/internal/plugins/store/store.go)
is the host-minted opaque principal for an installed plugin release. It is
minted at install time and persisted with the record; it is never derived from
the plugin id, package digest, workspace, or install path, so a reinstall
necessarily produces a different identity.
CapabilityApproval (apps/backend/internal/plugins/approval_store.go) is the
current approval row: (installation_id, workspace_id) is the ledger key, and
at most one current row exists per pair. A grant replaces the previous
current row for its pair. The row stores the exact manifest_digest the
approval was granted against, the canonically sorted and deduplicated
capability_ids, the active/revoked state, the human actor, the immutable
human policy version, and the audit revision. HumanPolicyVersionImmutable
records that all current approvals are granted under the one unversioned
human deny policy.
Approval writes are admitted only at the exact next revision. A write naming
any other revision fails with ErrApprovalRevisionConflict and leaves stored
state unchanged (revokeIfRevision in approval_store.go). Reused audit ids
are idempotent only for byte-identical inputs; a different input on the same
audit id fails without changing stored state.
CanonicalCapabilityList (apps/backend/internal/plugins/approval.go) is the
single canonicalizer for capability lists: trim, bounded length, no NUL,
deduplicate, sort. It rejects human-reserved capabilities and any id that is
not an exact host.v2 capability. ManifestCapabilityIDs derives the exact
host.v2.read:<resource> / host.v2.write:<resource> capability set from an
installed manifest, and ManifestCapabilityDigest derives its canonical
digest. Service.validateApprovalManifest rejects a grant whose manifest
digest does not match the installed manifest or that names capabilities the
manifest does not declare: the approval can never be broader than the
manifest.
Ledger state persists as approvals.json under the plugins directory through
the approvalLedger type (load/migrate semantics in approval_store.go).
Append-only event trail
CapabilityApprovalEvent records every approval change after commit:
installation, workspace, before/after revisions and digests, actor, reason, the
CapabilityApprovalEventType (grant, narrow, revoke,
upgrade_review), and the observed timestamp. Events are appended to the
ledger; grants, narrows, revokes, and upgrade reviews never rewrite or remove
earlier events. The API surface (apps/backend/internal/plugins/approval_api.go)
exposes list/get so future audit consumers can replay authority history.
Fail-closed authorization
Service.authorizePluginCapability (approval_service.go) is the single
decision path for a capability request. It evaluates in a fixed order and
returns a typed ApprovalDecision with a stable ApprovalDenyReason:
- Structural validation (
malformedAuthorizationRequestReason): bounded identifiers and digests, NUL-free, and an exact capability form. A malformed request denies asmalformed_request(orunsupported_capabilityfor a non-host.v2form) before any lookup. - Human-reserved capability check denies as
human_reserved. - Ledger lookup. A missing row denies as
missing_capability_approval; an unavailable ledger denies asunavailable_capability. A foreign workspace intentionally reuses the missing-approval reason: a more specific cross-workspace reason would disclose that the installation exists in another workspace. - Revoked state or a non-nil tombstone denies as
capability_revoked. - A requested revision different from the current row denies as
stale_capability_revision. - Manifest intersection (
manifestIntersectionDenyReason): the installed manifest must still declare the capability under the digest the approval was granted against, otherwiseunavailable_capability. - The capability must appear in the approved list, otherwise
undeclared_capability. Otherwise the decision is allowed and carries the audit identity.
Every denial is side-effect free: no Host operation runs inside the approval
layer. The deny vocabulary is typed (ApprovalDenyReason constants in
approval.go) so callers can match reasons machine-readably.
Tombstones and reinstall identity
Service.approvalTombstoneInstallation writes a tombstone for an
installation identity at uninstall time. A tombstoned identity denies all
later approvals and requests (ErrApprovalInstallationTombstoned,
capability_revoked), permanently. Because the reinstall mints a new
installation_id, no prior approval can attach to the new installation:
authority cannot be silently inherited across an uninstall/reinstall cycle.
Legacy v1 fence
isExactHostV2Capability gates both grant admission and authorization. A
capability id that is not an exact host.v2 capability is rejected from
approval lists (CanonicalCapabilityList) and denied at authorization time as
unsupported_capability. Legacy v1 manifest declarations keep their original
runtime behavior but cannot be granted or exercised through this substrate.
Audit receipts
ApprovalReceipt (approval.go) is the bounded, deterministic metadata a
future host adapter can persist on read or write receipts without carrying
authority: installation, workspace, revision, capability id, request and
method digests, audit id, result, and observed time. CanonicalApprovalDigest
builds stable digest identities from trimmed, NUL-joined inputs. Receipt
fields pass through the same safe-bounds helpers as the decision inputs, so a
hostile identifier is truncated or redacted rather than propagated: the
receipt is always bounded and cannot be coerced into authority.
Persistence and compatibility
The approval ledger file carries approvals, events, tombstones, and idempotency
inputs with forward-compatible JSON decoding (missing maps initialize empty).
Optional fields on store.Record (installation id) decode on older records
without migration; a missing installation identity simply has no approvals.
The substrate admits only HumanPolicyVersionImmutable today; the stored
per-row policy version is the stable extension point for a future policy
revision.
Observability
Decisions are pure typed results; the audit id and receipt give future adapters the exact metadata to persist. Events themselves are the observable authority history; there is no separate metric emission in this substrate.