An auditor asks for evidence. Your Worker already holds most of it, as declarations: which capabilities it composes, how every route verifies its caller, who authorized each management connection and for what, how old the keys are, what each database has applied, which fields are classified and which rest encrypted. The evidence pack reads those declarations together, in the deployed Worker, and reports them per control. Nothing is maintained by hand, so nothing drifts from the code.
Evidence, not a verdict
The pack never says compliant, and it never says a control was met. Each entry carries what the Worker read and one of three states:
| State | Means |
|---|---|
evidenced | The facts are present |
gap | Something is missing. The entry names what |
not-derivable | The Worker cannot say. The entry names why: a capability not composed, a scope this connection was not granted, a table or record that could not be read on this call |
An auditor weighs the evidence. The pack gives them the facts, the declaration each came from, and the SOC 2 and ISO/IEC 27001:2022 references the control answers. A control with something missing and something unreadable reports gap, and lists both. Something the Worker could not read is never reported as present.
The controls
| Control | SOC 2 | ISO 27001:2022 | What the Worker reads |
|---|---|---|---|
| Audit logging in force | CC7.2 | A.8.15 | Whether @pithy-sh/audit is composed, its binding is present, and control-plane calls are recorded. A Worker without audit reports a gap naming it |
| Every route declares how a caller is verified | CC6.1 | A.5.15 | Each route’s describeRoute verification, counted by strategy. A route with none is named |
| Management access attributed and scoped | CC6.2, CC6.3 | A.5.18 | Every control-plane connection’s principal, kind, purpose and granted scopes. One registered before those were recorded is named |
| Keys and secrets rotate | CC6.1 | A.8.24 | Each connection’s live keys and the age of the oldest, secrets past their declared cadence, and the field key’s retirement guard |
| Change history per environment | CC8.1 | A.8.32 | Each database’s applied, pending and undeclared migrations, and the last group with when it applied |
| Data classified and encrypted at rest | C1.1 | A.8.24 | The privacy classes your schemas hold beside the classes the last pithy deploy attested, and the fields marked encrypted() |
| CSRF wherever cookie mode is | CC6.6 | A.8.26 | The routes that take a session cookie, and whether a composed capability publishes the same-origin policy for them |
The table is versioned in the kit, and every pack carries mappingVersion. A reference moving, a control joining, or a rule for a state changing is a new version, so a pack from last quarter and one from today say which table they follow. A control that needs a declaration the kit does not hold yet arrives as a new version of the table, not as a guess in this one.
Reading it
GET /control-plane/evidenceThe pack sits behind its own scope, evidence:read. It is a read, so pithy dashboard connect preselects it. A connection made before it existed does not hold it, and adds it with --update, naming every scope it already holds beside the new one. No CLI command reads it, and the hosted dashboard does not render it yet, so until it does, read the route directly with a client of your own.
The response carries environment, generatedAt, the build’s version, mappingVersion, and controls[], one per row of the table above and in its order. Each entry carries control, title, soc2, iso27001, state, reasons and facts[], each fact { fact, subject?, value, source }. A value is a word, a number, a flag, a list of names or null, so a fact names and counts and never carries a row or a key. The reference has the rest of the shape.
It never reaches further than the connection may
Holding evidence:read widens no other scope. Migration state stays behind migrations:read, and secrets’ rotation count behind secrets:status:read. A connection without one gets a not-derivable reason naming the scope, and the control reports not-derivable unless it also found a gap. Field names stay behind privacy:read: the pack carries classes and counts. Keys appear as counts and ages, never key material. A principal is the email the connection records, and no audit row’s actor is copied in.
It proves, and performs nothing. Rotating a key, applying a migration and revoking a connection stay where they are, each on its own scope.