Self-audit (2026-06-02)
A read-through of README.adoc, lib/, examples/, and test/ against the
five concerns supplied by the estate-level auditor. Findings are ordered by
priority (worst gap first); each item has an associated audit branch
(audit/<slug>) carrying a small DRAFT PR.
Priority 1 — OUTBOUND (egress) mode is entirely absent
The repository is exclusively an inbound governance layer. Every call path
in lib/http_capability_gateway/{gateway,proxy}.ex assumes the gateway is a
reverse proxy in front of a single backend. There is no mode in which an
estate application can route an outbound HTTP/HTTPS call (e.g. to
api.anthropic.com, api.openai.com, the LLM provider du jour) through
the gateway so that the same declarative policy bounds what leaves the box.
This blocks hyperpolymath/neurophone from meeting its data-egress obligation
(#84-3.1: sensor-derived data must be classified and rate-limited before
leaving the device on a cloud/LLM call). Without an egress mode the device
either lets every outbound call through unchecked or builds its own ad-hoc
allowlist — both of which duplicate work the gateway is otherwise the right
home for.
Branch: audit/egress-mode-scaffold — adds a draft EgressPolicy schema,
a :forward_proxy capability shape, and a Proxy.egress/2 seam (no live
forwarder yet; that is a follow-up). Includes one test fixture covering
deny-by-default + allow on a host allowlist entry.
Priority 2 — Policy schema is allowlist-shaped but not deny-by-default in spirit
policy_validator.ex requires a non-empty governance.global_verbs list and
gateway.ex's handle_request/1 does default-deny on no-match
({:error, :no_match} -> stealth/403). That part is correct. But there are
two structural weaknesses:
- No "capability" field. The DSL has
path, verbs, optional exposure
— but no first-class capability name (the boj-server cartridge vocabulary
uses cartridge / capability strings). Today the only capability
binding is via the implicit trust-level total order in SafeTrust. This
limits how chimichanga-style attenuation can be plugged in later (Priority
3).
parse_exposure is fail-open. parse_exposure("typo") -> :public is
documented as intentional, but combined with the
Map.get(route, "exposure", "public") default in policy_compiler.ex it
means a route that omits exposure is silently treated as public.
Combined with a missing capability field this means the policy DSL is
shallower than the README's "principled, schema-driven approach" claim
suggests.
Branch: audit/policy-schema-capability — adds an optional capability
field at the route level (with validator support + property test), plus a
documented opt-in policy.defaults.exposure_fail_closed: true flag that
switches parse_exposure from fail-open to fail-closed. Backwards compatible.
Priority 3 — Estate capability + discovery integration
The gateway already understands BoJ cartridges (PolicyLoader.load_from_boj_catalog/1)
which is good. What is missing:
- Chimichanga capability attenuation. There is no place in the request flow
where a parent capability token can be attenuated (narrowed) before being
passed to the backend. The X-Trust-Level header is set by the gateway
authoritatively (good), but no capability vocabulary travels with it.
- Service discovery via groove-protocol. Backend URLs are static
(config :http_capability_gateway, :backend_url). There is no hook into
groove-protocol service discovery, so estate apps that move between
hosts can't be tracked.
Both are out-of-scope for a small PR but the audit branch documents the
contract surface where they would attach.
Branch: audit/capability-discovery-contract-doc — adds
docs/CAPABILITY-INTEGRATION.md and a PROOFS_NEEDED.md entry. Pure docs,
no code change.
Priority 4 — Provenance/audit logging coverage
gateway.ex already calls VeriSimDB.audit_allow/6 and VeriSimDB.audit_deny/4
on the allow/deny paths, and log_decision/7 emits a structured Logger
message. Two gaps:
- No audit on the no-match path —
{:error, :no_match} emits a structured
log entry but does NOT call VeriSimDB.audit_deny/4. The no-match path is
the most security-relevant one (someone probing for an undeclared route),
yet it is the one missing from the audit stream.
- No audit on the unknown-method (405) path — same problem. A client
sending PROPFIND/MKCOL/random strings is dropped with a Logger.warning
but not persisted to the audit ledger.
Branch: audit/audit-no-match-and-unknown-method — calls VeriSimDB.audit_deny/4
on both paths, with a thin reason-string discriminator. Plus tests asserting
the audit cast was sent.
Priority 5 — Auth/bypass weaknesses and test coverage
Generally strong (the strip_untrusted_headers plug, the safe_verb/1
allowlist, the verify_peer mTLS listener, the is_cert_verified/1
scheme: :https-only gate). Two open items:
make_backend_request/4 uses String.to_existing_atom/1 on conn.method
(line 189 of proxy.ex) without an allowlist. By the time this is reached
safe_verb/1 has already filtered for the seven supported methods, so
this is defence-in-depth only — but the comment in gateway.ex claims the
gateway never uses to_existing_atom on user input, which is half-true.
- No fuzz test asserting that
:authenticated cannot reach an :internal
route over the plaintext HTTP listener even if a forged header is present.
security_test.exs covers header stripping at the plug layer; an explicit
end-to-end "plaintext listener + forged header" test would close the loop.
Branch: audit/plaintext-forged-header-test — adds a single regression test
plus an Atom.to_string |> Map.fetch replacement in proxy.ex.
Echo-types audit
Per estate convention (feedback_proofs_must_check_and_cross_doc_echo_types.md)
this audit checked hyperpolymath/echo-types for relevant existing proofs.
Findings: no echo-types obligation is currently load-bearing on this repo —
the trust hierarchy is mechanised in proven/SafeTrust.idr (already
referenced) and no L3 echo obligation is in scope. Recorded as
record-as-not-relevant in PROOF-NEEDS.md per estate convention.
Surprises noticed during the meander
- The repo advertises multi-protocol (
MULTI-PROTOCOL.md, grpc_handler.ex,
graphql_handler.ex) but the README is honest that handlers are stubs.
Worth a status sync.
examples/policy-dev.yaml and 15+ workflow/config files still carry
SPDX-License-Identifier: PMPL-1.0-or-later headers, which violates the
2026-06-02 estate license directive (PMPL only for the palimpsest-license
repo itself). A local working-tree sweep to MPL-2.0 is in progress but
not yet committed. Filing this here for visibility; not fixing in this
audit run to avoid clashing with that sweep.
Self-audit (2026-06-02)
A read-through of
README.adoc,lib/,examples/, andtest/against thefive concerns supplied by the estate-level auditor. Findings are ordered by
priority (worst gap first); each item has an associated audit branch
(
audit/<slug>) carrying a small DRAFT PR.Priority 1 — OUTBOUND (egress) mode is entirely absent
The repository is exclusively an inbound governance layer. Every call path
in
lib/http_capability_gateway/{gateway,proxy}.exassumes the gateway is areverse proxy in front of a single backend. There is no mode in which an
estate application can route an outbound HTTP/HTTPS call (e.g. to
api.anthropic.com,api.openai.com, the LLM provider du jour) throughthe gateway so that the same declarative policy bounds what leaves the box.
This blocks
hyperpolymath/neurophonefrom meeting its data-egress obligation(
#84-3.1: sensor-derived data must be classified and rate-limited beforeleaving the device on a cloud/LLM call). Without an egress mode the device
either lets every outbound call through unchecked or builds its own ad-hoc
allowlist — both of which duplicate work the gateway is otherwise the right
home for.
Branch:
audit/egress-mode-scaffold— adds a draftEgressPolicyschema,a
:forward_proxycapability shape, and aProxy.egress/2seam (no liveforwarder yet; that is a follow-up). Includes one test fixture covering
deny-by-default + allow on a host allowlist entry.
Priority 2 — Policy schema is allowlist-shaped but not deny-by-default in spirit
policy_validator.exrequires a non-emptygovernance.global_verbslist andgateway.ex'shandle_request/1does default-deny on no-match(
{:error, :no_match} -> stealth/403). That part is correct. But there aretwo structural weaknesses:
path,verbs, optionalexposure— but no first-class capability name (the boj-server cartridge vocabulary
uses
cartridge/capabilitystrings). Today the only capabilitybinding is via the implicit trust-level total order in
SafeTrust. Thislimits how chimichanga-style attenuation can be plugged in later (Priority
3).
parse_exposureis fail-open.parse_exposure("typo") -> :publicisdocumented as intentional, but combined with the
Map.get(route, "exposure", "public")default inpolicy_compiler.exitmeans a route that omits
exposureis silently treated as public.Combined with a missing capability field this means the policy DSL is
shallower than the README's "principled, schema-driven approach" claim
suggests.
Branch:
audit/policy-schema-capability— adds an optionalcapabilityfield at the route level (with validator support + property test), plus a
documented opt-in
policy.defaults.exposure_fail_closed: trueflag thatswitches
parse_exposurefrom fail-open to fail-closed. Backwards compatible.Priority 3 — Estate capability + discovery integration
The gateway already understands BoJ cartridges (
PolicyLoader.load_from_boj_catalog/1)which is good. What is missing:
where a parent capability token can be attenuated (narrowed) before being
passed to the backend. The
X-Trust-Levelheader is set by the gatewayauthoritatively (good), but no capability vocabulary travels with it.
(
config :http_capability_gateway, :backend_url). There is no hook intogroove-protocolservice discovery, so estate apps that move betweenhosts can't be tracked.
Both are out-of-scope for a small PR but the audit branch documents the
contract surface where they would attach.
Branch:
audit/capability-discovery-contract-doc— addsdocs/CAPABILITY-INTEGRATION.mdand aPROOFS_NEEDED.mdentry. Pure docs,no code change.
Priority 4 — Provenance/audit logging coverage
gateway.exalready callsVeriSimDB.audit_allow/6andVeriSimDB.audit_deny/4on the allow/deny paths, and
log_decision/7emits a structured Loggermessage. Two gaps:
{:error, :no_match}emits a structuredlog entry but does NOT call
VeriSimDB.audit_deny/4. The no-match path isthe most security-relevant one (someone probing for an undeclared route),
yet it is the one missing from the audit stream.
sending
PROPFIND/MKCOL/random strings is dropped with aLogger.warningbut not persisted to the audit ledger.
Branch:
audit/audit-no-match-and-unknown-method— callsVeriSimDB.audit_deny/4on both paths, with a thin reason-string discriminator. Plus tests asserting
the audit cast was sent.
Priority 5 — Auth/bypass weaknesses and test coverage
Generally strong (the
strip_untrusted_headersplug, thesafe_verb/1allowlist, the verify_peer mTLS listener, the
is_cert_verified/1scheme: :https-only gate). Two open items:make_backend_request/4usesString.to_existing_atom/1onconn.method(line 189 of
proxy.ex) without an allowlist. By the time this is reachedsafe_verb/1has already filtered for the seven supported methods, sothis is defence-in-depth only — but the comment in
gateway.exclaims thegateway never uses
to_existing_atomon user input, which is half-true.:authenticatedcannot reach an:internalroute over the plaintext HTTP listener even if a forged header is present.
security_test.exscovers header stripping at the plug layer; an explicitend-to-end "plaintext listener + forged header" test would close the loop.
Branch:
audit/plaintext-forged-header-test— adds a single regression testplus an
Atom.to_string |> Map.fetchreplacement inproxy.ex.Echo-types audit
Per estate convention (
feedback_proofs_must_check_and_cross_doc_echo_types.md)this audit checked
hyperpolymath/echo-typesfor relevant existing proofs.Findings: no echo-types obligation is currently load-bearing on this repo —
the trust hierarchy is mechanised in
proven/SafeTrust.idr(alreadyreferenced) and no L3 echo obligation is in scope. Recorded as
record-as-not-relevantinPROOF-NEEDS.mdper estate convention.Surprises noticed during the meander
MULTI-PROTOCOL.md,grpc_handler.ex,graphql_handler.ex) but the README is honest that handlers are stubs.Worth a status sync.
examples/policy-dev.yamland 15+ workflow/config files still carrySPDX-License-Identifier: PMPL-1.0-or-laterheaders, which violates the2026-06-02 estate license directive (PMPL only for the palimpsest-license
repo itself). A local working-tree sweep to MPL-2.0 is in progress but
not yet committed. Filing this here for visibility; not fixing in this
audit run to avoid clashing with that sweep.