myco:mcp-response-redaction-governance
Skill979 starsChanged 19 days agoMITView on GitHub
Use this skill whenever you're adding, changing, or reviewing sensitive-field disclosure behavior on an MCP tool or API response — even if the user just asks to "expose this field" or "let callers opt into raw data." Covers: configuring per-surface redaction via redact_sensitive_fields and its env-var hierarchy (should_redact_sensitive_fields in unifi-core), the two real, separately-owned policy mechanisms in unifi-core (PolicyGateChecker for mutation gates, the redaction resolver for response disclosure) and how to extend either for a new domain, the two distinct enforcement points (per-tool/per-route redaction application vs. the StrictKwargFastMCP.call_tool write-back marker guard), and migrating legacy include_sensitive-style caller flags to server-owned policy. Trigger this any time a change would let a request argument control whether secrets/tokens/sensitive fields are disclosed in a response — that decision belongs to server/operator policy, never to the caller.
We split this skill into the rules it gives an agent
14 rules. About 0 are common, 6 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈6
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 38
Do not assume a single unified "PolicyEngine" class exists.
- Distinctive: line 60
Keep this convention when adding new redaction toggles.
- Very specific: line 71
Default: true (redact) if nothing is set — safe-by-default
- Distinctive: line 92
When adding a new surface, add its per-surface env var to the same hierarchy rather than hardcoding a local redaction flag, and decide …
- Very specific: line 125
Put the resolver/checker itself in packages/unifi-core so every product package shares one implementation.
- Very specific: line 127
If a shared per-surface convenience wrapper is useful — as should_redact_response_sensitive_fields in …
- Very specific: line 150
Every read tool/route resolves the boolean early and passes it explicitly into redact_sensitive_fields(payload, redact_sensitive=...) …
- Very specific: line 189
When reviewing a redaction-related change, check both mechanisms independently: does every new read path call redact_sensitive_fields(), …
- Very specific: line 197
If you find an existing tool/API parameter that lets a caller request raw/unredacted data directly (e.g.
- Distinctive: line 201
Remove the caller-controlled parameter from the public tool schema / API request shape entirely — do not just gate it, remove it.
- Distinctive: line 214
If the surface still needs to reject a caller attempting to pass the old parameter (rather than silently ignoring it), return an explicit …
- Distinctive: line 235
Safe-by-default: if no policy signal is present anywhere in the hierarchy, the system must default to redacting.
- Distinctive: line 241
Test both independently when changing either.
- Very specific: line 242
Don't invent a third policy shape: before adding any new server-side policy check, decide whether it's an allow/deny gate …
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
MCP Response Redaction Governance
This domain covers how sensitive-field disclosure (secrets, tokens, raw
credentials) is controlled across MCP tools and API responses. The
governing principle: request intent is not authorization. A caller
argument like include_sensitive=true expresses what the caller wants,
but only server/operator policy decides whether to honor it. Never let a
per-call argument directly gate secret disclosure.
Prerequisites
- Know which surface you're touching: an MCP product server
(
network/access/protect) or the REST API (api). Each surface has its own env-var prefix and resolves the redaction decision at a different point in the request lifecycle (see Procedure A). - Do not assume a single unified "PolicyEngine" class exists. Check
packages/unifi-core/src/unifi_core/policy.pyandpackages/unifi-core/src/unifi_core/policy_gate.pydirectly — there are two separate mechanisms, not one (see Procedure B). - Locate the correct enforcement point for what you're changing: read-path redaction and the mutation write-back guard are two different checks in two different files (see Procedure C). Don't assume one covers the other.
Procedure A: Configuring redaction policy per surface
Sensitive-field disclosure is controlled by a boolean, named for the enforced behavior rather than a data format:
policy:
response:
redact_sensitive_fields: true # default; true = redact secretsNaming rationale: redact_sensitive_fields: true|false is clearer than
alternatives like sensitive_fields: redacted|raw, because the operator
is deciding whether the server redacts, not choosing an output format.
Keep this convention when adding new redaction toggles.
The boolean is resolved by should_redact_sensitive_fields(server_prefix,
config, env) in packages/unifi-core/src/unifi_core/policy.py, in this
order (highest specificity wins):
UNIFI_{SURFACE}_REDACT_SENSITIVE_FIELDS— per-surface env override (e.g.UNIFI_NETWORK_REDACT_SENSITIVE_FIELDS,UNIFI_API_REDACT_SENSITIVE_FIELDS)UNIFI_REDACT_SENSITIVE_FIELDS— global env fallbackpolicy.response.redact_sensitive_fieldsfrom the surface's own config- Default:
true(redact) if nothing is set — safe-by-default
Resolution timing differs by surface — check this before assuming a live env-var change takes effect:
- MCP surfaces: each app defines a local
should_redact_sensitive_fields() -> boolbinding function in its own runtime module — e.g.apps/network/src/unifi_network_mcp/runtime.py,apps/access/src/unifi_access_mcp/runtime.py,apps/protect/src/unifi_protect_mcp/runtime.py— that calls the shared resolver on every invocation. Because MCP tool functions call this binding at the top of each call, an env-var change takes effect on the next tool call with no restart. - API surface:
apps/api/src/unifi_api/config.pycallsshould_redact_sensitive_fields("api", config=container)once, insideload_config(), and caches the result onApiConfig.policy.response .redact_sensitive_fields. Routes then read that cached attribute per request. ChangingUNIFI_API_REDACT_SENSITIVE_FIELDSorUNIFI_REDACT_SENSITIVE_FIELDSfor the API surface requires a process restart to take effect — it is not re-resolved per request.
When adding a new surface, add its per-surface env var to the same hierarchy rather than hardcoding a local redaction flag, and decide deliberately whether the new surface resolves per-call (like MCP) or at config-load time (like the API) — don't silently inherit whichever pattern is closest by copy-paste.
Procedure B: Extending policy for a new domain — pick the right existing shape
There is no single unified policy engine class. Two separate
mechanisms live in unifi-core, and each new policy question should be
modeled as one more instance of whichever shape fits, not a third bespoke
mechanism:
- Gate checks (
PolicyGateCheckerinpackages/unifi-core/src/unifi_core/policy_gate.py) answer "is this category/action combination allowed?" via a 3-level env-var hierarchy:UNIFI_POLICY_<SERVER>_<CATEGORY>_<ACTION>→UNIFI_POLICY_<SERVER>_<ACTION>→UNIFI_POLICY_<ACTION>→ allowed by default. Use this shape for new allow/deny gates on categorized actions (mutation authorization is the existing example via.check(category, action)). - Resolvers (
should_redact_sensitive_fieldsinpackages/unifi-core/src/unifi_core/policy.py) answer "what is the value of this policy setting for this surface?" via the per-surface → global → config → default hierarchy described in Procedure A. Use this shape for a typed value (not just allow/deny) that varies by surface.
To add a new domain:
- Decide whether the question is a categorized allow/deny gate or a surface-scoped value resolver, and follow the matching precedent file above rather than writing a new env-var precedence scheme from scratch.
- Put the resolver/checker itself in
packages/unifi-coreso every product package shares one implementation. - If a shared per-surface convenience wrapper is useful — as
should_redact_response_sensitive_fieldsinpackages/unifi-mcp-shared/src/unifi_mcp_shared/response_policy.pydoes for redaction — add it inpackages/unifi-mcp-shared, and bind it per surface with a thinshould_redact_sensitive_fields() -> boolfunction inside that surface's own runtime module (followingapps/network/src/unifi_network_mcp/runtime.pyas the template) rather than having every tool call the generic resolver with a hardcoded surface string.
Gotcha: design discussions for this domain have proposed unifying
gate-checks and resolvers under one PolicyEngine/PolicyContext
interface. That interface does not exist in the codebase today — verify
with grep -r PolicyEngine before writing code or docs that assume it
does, and don't reintroduce it without also migrating both existing
mechanisms onto it in the same change.
Procedure C: Enforcing redaction and the write-back guard — two different checks
These are separate mechanisms enforced in separate places. Don't assume fixing one covers the other.
1. Read-path redaction (per-tool / per-route, not centralized):
Every read tool/route resolves the boolean early and passes it explicitly
into redact_sensitive_fields(payload, redact_sensitive=...) (defined in
packages/unifi-core/src/unifi_core/redaction.py) when shaping its
response:
# MCP tool (e.g. apps/network/src/unifi_network_mcp/tools/system.py)
redact_sensitive = should_redact_sensitive_fields()
return redact_sensitive_fields({...}, redact_sensitive=redact_sensitive)
# API route (e.g. apps/api/src/unifi_api/routes/actions.py)
redact_sensitive = request.app.state.config.policy.response.redact_sensitive_fields
shaped = redact_sensitive_fields(result.payload, redact_sensitive=redact_sensitive)This call is not applied automatically by a middleware or dispatcher
— every new read tool, route, serializer, or GraphQL resolver that
returns a payload with potentially sensitive fields must call it
explicitly. When adding a new read surface, grep sibling tools/routes in
the same file for redact_sensitive_fields( to find the local pattern to
follow, and confirm the new code path actually calls it (a missing call
is the most likely regression, not a wrong env var).
2. Write-back marker guard (centralized in MCP transport):
StrictKwargFastMCP.call_tool, implemented in
packages/unifi-mcp-shared/src/unifi_mcp_shared/strict_dispatch.py,
inspects incoming tool arguments via redaction_marker_paths() (defined
in packages/unifi-core/src/unifi_core/redaction.py) and rejects any
argument that still carries the redaction marker value, so a caller can't
round-trip a previously-redacted response field back as if it were a real
update. This is centralized — but it only covers the "marker submitted
as new data" case, not the read-redaction decision above.
The API surface has an analogous but separate guard: it rejects any
include_sensitive key present in the request args outright — see
apps/api/src/unifi_api/services/actions.py and
apps/api/src/unifi_api/routes/actions.py — rather than doing
marker-pattern detection.
When reviewing a redaction-related change, check both mechanisms
independently: does every new read path call redact_sensitive_fields(),
and (for MCP) is the new tool's manifest/schema covered by the marker
write-back check so a redacted field can't be replayed as a mutation
input?
Procedure D: Migrating away from include_sensitive-style caller flags
If you find an existing tool/API parameter that lets a caller request
raw/unredacted data directly (e.g. include_sensitive=true), migrate it
to server-owned policy:
- Remove the caller-controlled parameter from the public tool schema / API request shape entirely — do not just gate it, remove it. Leaving it present but "gated" still collapses request-intent and authorization into one flag.
- Replace any internal use of the old flag name with redaction-oriented
naming:
should_redact_sensitive_fields(),should_redact_response_sensitive_fields(),redact_sensitive=True|Falseat the actual redaction boundary inredact_sensitive_fields(). Do not carry the oldinclude_sensitivename forward as an internal helper pattern. - Compute any internal "include sensitive" boolean, if still needed
downstream, as
not redact_sensitive_fieldsderived purely from server policy — never from a caller-supplied value. - If the surface still needs to reject a caller attempting to pass the
old parameter (rather than silently ignoring it), return an explicit
error naming the correct replacement env var/config key — see the
unsupported-argument error constant defined in
apps/api/src/unifi_api/services/actions.py— instead of silently dropping the argument. - Coordinate the change across every package that touches the response path (the shared policy resolver, each MCP product server, and the REST API) — a partial migration reintroduces the caller-controlled disclosure gap on the surfaces left behind.
Cross-Cutting Gotchas
- Two mechanisms, not one: gate checks (
PolicyGateChecker) and resolvers (should_redact_sensitive_fields) are independent code paths inunifi-coretoday. A fix or extension to one does not automatically apply to the other — check both when auditing policy coverage. - Resolution timing varies by surface: MCP resolves per call; the API surface resolves once at config-load time and caches it (see Procedure A). Don't assume a live env-var override works identically on every surface — verify against the surface's actual resolution point.
- Safe-by-default: if no policy signal is present anywhere in the hierarchy, the system must default to redacting. Never default to disclosure.
- Read-redaction vs. write-back guard are different checks: fixing a
bug in the read-path
redact_sensitive_fields()call site does not touch theStrictKwargFastMCP.call_toolmarker guard, and vice versa. Test both independently when changing either. - Don't invent a third policy shape: before adding any new
server-side policy check, decide whether it's an allow/deny gate
(
PolicyGateCheckershape) or a surface-scoped value (resolver shape), and extend the matching existing mechanism inpackages/unifi-core. - Secret-vocabulary boundary vs. logging hygiene: CodeQL's
clear-text-logging-sensitive-data alert rule, applied to Network
client/device managers and tools, typically flags MAC-address logger
arguments, not leaked passwords or tokens.
unifi_core.redactionintentionally targets secret key material and leaves inventory identifiers (MAC addresses, serials) readable — that's a separate boundary from this domain's secret-field redaction pattern. Don't add inventory identifiers to the redaction vocabulary to silence these alerts; fix the logging call site instead. - Exception text is a disclosure surface too: error-classification /
support-bundle code must never serialize a raw exception's text or
status property back into a response — a hostile exception subclass can
carry sensitive data via its message or attributes
(
test_error_classification_never_serializes_exception_textguards this). Treat exception serialization as another redaction-adjacent boundary alongside tool/route payloads, not something the read-pathredact_sensitive_fields()call alone covers.
Historical context
This governance model was established while closing a reported weakness
where a per-call caller-supplied flag could influence sensitive
disclosure directly. The fix removed the caller-controlled parameter from
public surfaces in favor of the redact_sensitive_fields config/env
model in Procedure A, and added the write-back marker guard in Procedure
C. Design discussion during that work proposed unifying mutation-gate and
response-disclosure policy under one PolicyEngine/PolicyContext
interface; that unification was the intended end-state but has not
shipped — the codebase still has the two separate mechanisms described in
Procedure B. Treat any reference to a single "PolicyEngine" as
aspirational until it is actually merged, and verify against the
unifi-core policy modules directly.
Other versions1 project uses a version of this.
Word for word · 1
- sirkirby/unifi-mcp★ 978100% same
Edited · 0
None found.
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository16 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- docs/llms.txtllms.txt
- add-integration-api-toolSkill
- myco:api-endpoint-serializer-authoringSkill
- community-pr-batch-reviewSkill
- myco:community-pr-reviewSkill
- myco:extend-unifi-apiSkill
- myco:live-smoke-testingSkill
- myco:monorepo-release-pipelineSkill
- myco:runtime-bootstrap-and-test-isolationSkill
- myco:skill-reference-and-manifest-drift-maintenanceSkill
- test-pruningSkill
- myco:unifi-client-data-correctnessSkill
- code-reviewSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

