Skip to content

[finding] A package's whole declared capability is dropped on a name collision with no diagnostic — the same doubly-optional logger as #17516, one axis over #18023

Description

@claude

Blocked-by: #17516
Unlock-action: re-check PR #18022

A package's entire declared capability is dropped on a capability-name collision, and the drop is invisible to the author who caused it.

packages/plugins/plugin-security/src/bootstrap-declared-capabilities.ts, the else arm at the skippedForeign branch (anchor by content — out.skippedForeign += 1), on origin/main 32a321430:

} else {
  out.skippedForeign += 1;
  logger?.warn?.('[security] capability name owned by another package — skipped', {
    name: cap.name, declaredBy: packageId, ownedBy: existing.package_id,
  });
}

Why this is a finding

This is the same shape as the permission-set defect on the sibling card #17516, one axis over, and it was found while delivering that one. logger?.warn?. is optionally chained twice, so a caller that passes no logger produces no output at all — the declared capability vanishes with only an internal counter incremented.

⛔ The skip itself is correct under ADR-0086 D4 (a package never writes into a foreign record) and this card does not propose changing it. The defect is that the refusal never reaches the author.

Measured, with a lit control

Driven through bootstrapDeclaredCapabilities with a foreign-owned sys_capability row and no logger passed, spying on all five console channels:

skippedForeign                = 1     (the declared capability was dropped)
author-visible console lines  = 0     (log, info, warn, error, debug — all five)

LIT CONTROL, same harness and same measurement on the permission-set axis after its repair landed: skippedForeign = 1, author-visible console lines = 1. So the zero above is a reading, not an artefact of the probe.

The repo's own existing pins on this branch (bootstrap-seed-round-trips.test.ts) all pass a logger, so none of them exercises the no-logger path.

Why it is a separate card rather than a rider

The permission-set fix was fenced to its own three lines. The capability axis needs its own diagnostic record type, its own key on the capability seeder's outcome, and its own pins — a second design rather than a mechanical copy — so riding it in would have widened that card's verification surface.

The shared machinery is already exported from the same package and should be reused rather than re-derived: permissionSetNameIsForeign, permissionSetNameCollisionDiagnostic and formatPermissionSetNameCollisionDiagnostic in packages/plugins/plugin-security/src/permission-set-name-collision.ts. The owner-comparison predicate is axis-independent; only the wording and the record are not.

Dedup

Searched before filing, one targeted query, with the channel proven live by the subject itself returning first. Nearest hits, none of them this: #11096 (batching of the same seeder, closed), #4967 (a refused capability declaration suppressing the back-compat derivation, closed). No open card covers this.

⛔ Ungraded — triage sets type and priority. Filed unassigned.


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions