speakeasy-api/gram/.agents/skills/gram-audit-logging/SKILL.md
gram-audit-logging
Concepts, external interfaces, and conventions for Gram's audit logging subsystem — the internal Go API for recording actor/action/subject events and the `/rpc/auditlogs.*` management API that exposes them. Activate whenever the task involves recording or exposing audit events (adding or changing audit coverage on a service, introducing a new audited subject or action, writing tests that assert an event was recorded, changing how entries are displayed or filtered).
- Source repository stars
- 266
- Declared platforms
- 0
- Static risk flags
- 0
- Last source update
- 2026-08-25
- Source checked
- 2026-08-25
Decision brief
What it does: where it fits
Actor. The principal that caused the event — a urn.Principal carrying a type (user, role, service account) and an id, with optional display name and slug for human rendering.
Not for
- Tasks that require unconfirmed production actions or broad system permissions.
- Environments where the pinned source and install steps cannot be inspected.
Compatibility matrix
Platform support, with evidence labels
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
Inspect first. Install second.
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/speakeasy-api/gram --skill ".agents/skills/gram-audit-logging"Inspect the Agent Skill "gram-audit-logging" from https://github.com/speakeasy-api/gram/blob/8af0601cf530721aaa686c2718d76e7990226d33/.agents/skills/gram-audit-logging/SKILL.md at commit 8af0601cf530721aaa686c2718d76e7990226d33. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
What the source asks the agent to do
- 01
How to audit a handler in a service
When a handler mutates a resource whose subject already has Action/Log definitions, the handler opts in by calling the existing Log function. The call has to happen inside the same dbtx as the mutation so the audit row and the state it describes commit together.
Open the service's impl.go and locate the handler. Confirm the handler already uses a transaction; if not, wrap the repo calls in s.db.Begin(ctx) → defer o11y.NoLogDefer(rollback) → dbtx.Commit(ctx).After the primary repo writes succeed and before Commit, call the subject's audit.Log with a populated LogEvent. Pass the same dbtx the repo writes used so the audit row commits atomically with them.Build the actor from contextvalues.GetAuthContext(ctx) — typically urn.NewPrincipal(urn.PrincipalTypeUser, authCtx.UserID), plus authCtx.Email for ActorDisplayName. Principal types live in server/internal/urn. Fill in t… - 02
How to audit a cascading delete of child resources
Use this when deleting a parent resource also soft-deletes child rows of an independently audited subject (e.g. deleting an mcpserver cascades to its mcpendpoints). The parent's Log is not enough — every affected child row must produce its own audit entry under the child subject…
Make the cascade query return the affected rows. SQLc queries scoped by parent id should be :many with RETURNING so the caller can iterate the deleted children. If the existing query is :exec, change it and regenerate (…In the parent handler, after the cascade query succeeds and inside the same dbtx, loop over the returned rows and call the child subject's audit.Log once per row. Populate the child's URN, display name, and slug from th…Emit the parent's audit.Log after the per-child loop so cause precedes effect in the timeline. Both still commit atomically with the cascade. - 03
How to add a new action to an existing subject
Use this when the subject already has a file but you're introducing a new verb.
In the subject's file, add an Action constant alongside the existing ones.Add a LogEvent struct with the fields the caller needs to supply. At minimum: OrganizationID, ProjectID (zero value for org-scoped subjects), Actor, ActorDisplayName, ActorSlug, plus the subject URN (URN urn.) and any a…Add a Log function that translates the event into repo.InsertAuditLogParams, passes any snapshots through marshalAuditPayload (which handles nil internally), then calls l.log(ctx, dbtx, auditEntry{Params: entry, OutboxE… - 04
How to add a new audited subject
Use this when introducing an entirely new kind of resource that doesn't map onto any existing subject file.
Add a subjectType constant to events.go.Add a new var = outbox.NewEventDefevents.AuditLogCreatedPayload to server/internal/outbox/events/auditlog.go. Use the v1 suffix. Then run mise run gen:webhooks-server to update cataloggen.go and cataloggen.yaml.Create server/internal/audit/.go following the subject-file convention, and populate it with the Action constants, LogEvent structs, and Log functions. Each Log function passes auditEntry{Params: entry, OutboxEvent: eve… - 05
How to update an existing action or subject
1. Renaming an Action value is a breaking change for consumers of auditlogs.list that filter on action strings. Avoid it; add a new action and dual-write if a behaviour rename is needed. 2. Adding a new field to a LogEvent struct is safe — update every call site (exhaustruct wil…
Renaming an Action value is a breaking change for consumers of auditlogs.list that filter on action strings. Avoid it; add a new action and dual-write if a behaviour rename is needed.Adding a new field to a LogEvent struct is safe — update every call site (exhaustruct will flag missed ones).Changing the shape of the snapshot payload (the concrete type referenced by SnapshotBefore / SnapshotAfter) is safe for new rows only; old rows retain the old shape. If consumers parse snapshots, version the payload ins…
Permission review
Static risk signals and limitations
No configured static risk pattern was detected
This is not proof of safety. Runtime behavior, indirect dependencies, and hidden external systems are outside the static scan.
Evidence record
Why each signal appears
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 91/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 266 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
Provenance and original SKILL.md
- Repository
- speakeasy-api/gram
- Skill path
- .agents/skills/gram-audit-logging/SKILL.md
- Commit
- 8af0601cf530721aaa686c2718d76e7990226d33
- License
- AGPL-3.0
- Collected
- 2026-08-25
- Default branch
- main
View the original SKILL.md
Audit logging is how Gram records who did what to which resource. Every meaningful mutation on a project- or org-scoped resource is expected to produce one audit entry per affected row, written inside the same database transaction as the mutation so events can't drift from the state they describe. Entries are exposed to Gram users through the auditlogs management API.
Concepts and terminology
Actor. The principal that caused the event — a urn.Principal carrying a type (user, role, service account) and an id, with optional display name and slug for human rendering.
Subject. The resource the event is about — identified by a subject type (e.g. remote_mcp_server, access_role) and a subject id.
Action. What happened to the subject. Each subject declares its own set of actions, typically one per mutating verb on the subject's life cycle.
Before/after snapshot. Optional opaque JSON payloads describing the subject's state. Populated on updates, left empty on creates and deletes unless the snapshot is independently useful.
Metadata. Optional JSON bag for contextual fields that are not part of the subject's state.
Scoping. Every entry belongs to exactly one organization and zero or one projects. Org-scoped subjects (roles, members) carry no project id; project-scoped subjects carry the project UUID.
Atomicity. Audit entries are written inside the same database transaction as the mutation they describe, so the state and the record of the state commit together or not at all.
Outbox event. Every audit entry also publishes a typed webhook event to the outbox (same transaction). The event type is subject-specific — e.g. audit_log.deployment_event_v1 for deployment actions, audit_log.project_event_v1 for project actions — allowing webhook subscribers to filter by subject domain rather than receiving all audit activity under the single legacy audit_log.created type. The _v1 suffix signals the version; new event definitions must use this suffix. All subject event vars are declared in server/internal/outbox/events/audit_log.go.
Server
Audit logging lives in server/internal/audit/ with its management-API surface defined in server/design/auditlogs/. Callers are other services whose handlers emit events; the auditlogs Goa service reads them back.
Conventions
Where types live. The package-private subjectType string type and every subjectType* constant live in server/internal/audit/events.go. The public Action string type and the marshalAuditPayload snapshot helper live in the same file. The subject-type const block is kept alphabetised.
Subject type naming. Subject type values are short snake_case strings (e.g. remote_mcp_server, access_role). Constants follow the subjectType<Name> pattern.
Action naming. Values follow <subject-slug>:<verb> (e.g. remote-mcp:create, access_role:update). Verbs are typically create / update / delete; subjects may add feature-specific verbs (toolset:attach_oauth_proxy).
Subject files. One Go file per subject under server/internal/audit/, named after the subject in plural form (e.g. remotemcpservers.go, toolsets.go). Each file owns that subject's Action* constants, its Log*Event payload structs, and its Log* functions. Do not merge subjects into a shared file.
Log*Event and Log* naming. One Log<Verb>Event struct plus one Log<Verb> function per action, declared in the subject file. Log* functions take (ctx, dbtx repo.DBTX, event Log*Event) error so the audit insert is atomic with the caller's mutation. Internally each Log* function constructs a repo.InsertAuditLogParams and then calls l.log(ctx, dbtx, auditEntry{Params: entry, OutboxEvent: events.<Subject>}) — it does not call repo.New(dbtx).InsertAuditLog directly.
Subject identifier fields. Event structs carry the subject's identifier as a URN type, not a raw uuid.UUID. Field name is <Subject>URN (e.g. KeyURN urn.APIKey, McpServerURN urn.McpServer), and the Log* function populates SubjectID from event.<Subject>URN.ID.String(). If no URN type exists yet, add one under server/internal/urn/ before introducing the event struct — see server/internal/urn/api_key.go for the template.
Snapshot fields. Update event structs declare snapshot fields as <Subject>SnapshotBefore / <Subject>SnapshotAfter with concrete pointer types (e.g. *types.Toolset, *types.McpServer). Do not use any or bare SnapshotBefore / SnapshotAfter — the typed form keeps marshalAuditPayload callers honest about the shape being persisted. Pass the view through directly unless a specific field on the type needs stripping for size or sensitivity reasons (see toolsets.go for the one clone-and-strip case).
Per-row events for bulk mutations. A single bulk SQL statement that touches N rows of an audited subject produces N audit entries — one per row — not one entry that covers the batch. This is what makes the audit log a faithful reconstruction of each subject's life cycle and what lets auditlogs.list filter to a specific subject id. The most common place this gets missed is cascading soft-deletes that fan out from a parent delete; see "How to audit a cascading delete of child resources" under "Jobs to be done".
Non-generated files
| File | Purpose |
|---|---|
server/design/auditlogs/design.go | Goa design for the auditlogs service. Regenerates server/gen/auditlogs/ and server/gen/http/auditlogs/ via mise run gen:goa-server. |
server/internal/audit/<subject>.go | One file per subject (e.g. access.go, remotemcpservers.go, toolsets.go). |
server/internal/audit/audittest/helpers.go | Test helpers other packages use to assert audit events. |
server/internal/audit/audittest/queries.sql | SQLc queries backing the test helpers. Regenerates server/internal/audit/audittest/repo/ via mise run gen:sqlc-server. |
server/internal/audit/events.go | Top-level declarations shared across every subject. |
server/internal/audit/logger.go | Logger type, auditEntry struct, and the internal l.log() method that inserts the DB row then calls appendToOutbox. |
server/internal/audit/outbox.go | appendToOutbox — translates the inserted audit row into an AuditLogCreatedPayload and publishes to the outbox under the subject-specific event def. |
server/internal/auditapi/impl.go | Implementation of the /rpc/auditlogs.* Goa service (reads). Lives in its own package to keep the audit writer surface free of auth/sessions and mv so any service can call audit.Log* without import cycles. |
server/internal/audit/queries.sql | SQLc queries for the audit log table. Regenerates server/internal/audit/repo/ via mise run gen:sqlc-server. |
server/internal/auditapi/{setup_test,list_test,listfacets_test}.go | Tests for the auditlogs management API. |
server/internal/outbox/events/audit_log.go | Per-subject *outbox.EventDef vars (e.g. events.Deployment, events.Project). Add a new var here when introducing a new audited subject. |
Generated files
Files under server/gen/** and any repo/ subdirectory carry a DO NOT EDIT header.
| Path | Generator |
|---|---|
server/gen/auditlogs/, server/gen/http/auditlogs/ | mise run gen:goa-server from server/design/auditlogs/design.go. |
server/internal/audit/audittest/repo/ | mise run gen:sqlc-server from server/internal/audit/audittest/queries.sql (separate stanza in server/database/sqlc.yaml). |
server/internal/audit/repo/ | mise run gen:sqlc-server from server/internal/audit/queries.sql (via the audit stanza in server/database/sqlc.yaml). |
server/internal/outbox/events/catalog_gen.go, catalog_gen.yaml | mise run gen:webhooks-server from server/cmd/gen-webhooks. Run after adding a new event def to audit_log.go. |
Server-client contract
Audit entries are surfaced to Gram users through a small, fixed set of endpoints. New actions and subject types appear automatically — facets are computed from the rows that exist, so there is no registration step outside the Go code.
HTTP routes (design: server/design/auditlogs/design.go):
GET /rpc/auditlogs.list— paginated list; supports cursor,project_slug,actor_id, andactionfilters.GET /rpc/auditlogs.listFacets— returns the set of actors and actions that actually appear, for UI facet pickers.
Generated client surfaces — regenerated by mise run gen:goa-server then mise run gen:sdk:
- TypeScript SDK:
client/dashboard/src/sdk/src/funcs/auditlogs*.ts,client/dashboard/src/sdk/src/react-query/auditlogs*.ts, plus models underclient/dashboard/src/sdk/src/models/. - CLI bindings:
server/gen/http/cli/gram/cli.go.
Jobs to be done
How to audit a handler in a service
When a handler mutates a resource whose subject already has Action*/Log* definitions, the handler opts in by calling the existing Log* function. The call has to happen inside the same dbtx as the mutation so the audit row and the state it describes commit together.
- Open the service's
impl.goand locate the handler. Confirm the handler already uses a transaction; if not, wrap the repo calls ins.db.Begin(ctx)→defer o11y.NoLogDefer(rollback)→dbtx.Commit(ctx). - After the primary repo writes succeed and before
Commit, call the subject'saudit.Log<Verb>with a populatedLog<Verb>Event. Pass the samedbtxthe repo writes used so the audit row commits atomically with them. - Build the actor from
contextvalues.GetAuthContext(ctx)— typicallyurn.NewPrincipal(urn.PrincipalTypeUser, authCtx.UserID), plusauthCtx.EmailforActorDisplayName. Principal types live inserver/internal/urn. Fill in the subject-specific identifier fields (subject id, display name, slug) from the repo row you just wrote. - For updates, populate the typed snapshot fields (
<Subject>SnapshotBefore/<Subject>SnapshotAfter) with the pre- and post-mutation state. For creates and deletes, leave them nil unless the snapshot is independently useful. - Treat audit-log failures as
oops.CodeUnexpected. Audit logging is not optional — if it fails, fail the request. - Add a test that asserts the event was recorded (see "How to assert audit events in tests" below).
How to audit a cascading delete of child resources
Use this when deleting a parent resource also soft-deletes child rows of an independently audited subject (e.g. deleting an mcp_server cascades to its mcp_endpoints). The parent's Log<Verb> is not enough — every affected child row must produce its own audit entry under the child subject's action.
- Make the cascade query return the affected rows. SQLc queries scoped by parent id should be
:manywithRETURNING *so the caller can iterate the deleted children. If the existing query is:exec, change it and regenerate (mise run gen:sqlc-server). - In the parent handler, after the cascade query succeeds and inside the same
dbtx, loop over the returned rows and call the child subject'saudit.Log<Verb>once per row. Populate the child's URN, display name, and slug from the returned row — not from the parent. - Emit the parent's
audit.Log<Verb>after the per-child loop so cause precedes effect in the timeline. Both still commit atomically with the cascade. - In tests, capture baseline counts for both the parent and the child action, exercise the handler, and assert the child count grew by exactly the number of cascaded rows. A single +1 assertion on the parent action will not catch a regression where the per-child events stop being emitted.
How to add a new action to an existing subject
Use this when the subject already has a file but you're introducing a new verb.
- In the subject's file, add an
Action<Subject><Verb>constant alongside the existing ones. - Add a
Log<Subject><Verb>Eventstruct with the fields the caller needs to supply. At minimum:OrganizationID,ProjectID(zero value for org-scoped subjects),Actor,ActorDisplayName,ActorSlug, plus the subject URN (<Subject>URN urn.<Subject>) and any additional display name / slug fields the subject needs. Updates additionally carry typed snapshot fields (<Subject>SnapshotBefore/<Subject>SnapshotAfterwith concrete pointer types — e.g.*types.<Subject>). - Add a
Log<Subject><Verb>function that translates the event intorepo.InsertAuditLogParams, passes any snapshots throughmarshalAuditPayload(which handles nil internally), then callsl.log(ctx, dbtx, auditEntry{Params: entry, OutboxEvent: events.<Subject>}). Do not callrepo.New(dbtx).InsertAuditLogdirectly —l.logdoes that and also handles the outbox publication. - Call the new function from the handler as described under "How to audit a handler in a service".
No schema change, no codegen step — facet queries pick up the new action automatically.
How to add a new audited subject
Use this when introducing an entirely new kind of resource that doesn't map onto any existing subject file.
- Add a
subjectType<Name>constant toevents.go. - Add a new
var <Subject> = outbox.NewEventDef[events.AuditLogCreatedPayload]("audit_log.<subject>_event_v1", "...")toserver/internal/outbox/events/audit_log.go. Use the_v1suffix. Then runmise run gen:webhooks-serverto updatecatalog_gen.goandcatalog_gen.yaml. - Create
server/internal/audit/<subject>.gofollowing the subject-file convention, and populate it with theAction*constants,Log*Eventstructs, andLog*functions. EachLog*function passesauditEntry{Params: entry, OutboxEvent: events.<Subject>}tol.log. - Call the new
Log*functions from the owning service's handlers.
How to update an existing action or subject
- Renaming an
Actionvalue is a breaking change for consumers ofauditlogs.listthat filter on action strings. Avoid it; add a new action and dual-write if a behaviour rename is needed. - Adding a new field to a
Log*Eventstruct is safe — update every call site (exhaustructwill flag missed ones). - Changing the shape of the snapshot payload (the concrete type referenced by
<Subject>SnapshotBefore/<Subject>SnapshotAfter) is safe for new rows only; old rows retain the old shape. If consumers parse snapshots, version the payload inside the JSON. - Do not edit the string values of
subjectType*constants; the same breakage argument as action renames applies.
How to assert audit events in tests
Use audittest helpers in the service's test package — do not query the audit tables directly.
- Capture a baseline count with
audittest.AuditLogCountByAction(ctx, conn, audit.Action<Foo>). - Exercise the handler.
- Assert
after == before + 1(or the expected delta). - For snapshot correctness, fetch the row with
audittest.LatestAuditLogByActionand decodeMetadata,BeforeSnapshot, orAfterSnapshotwithaudittest.DecodeAuditData.
Relevant mise tasks
| Task | Purpose |
|---|---|
mise run gen:goa-server | Regenerate server/gen/auditlogs/** whenever you edit server/design/auditlogs/design.go. |
mise run gen:sdk | Regenerate the TypeScript SDK and CLI bindings after a Goa design change. |
mise run gen:sqlc-server | Regenerate server/internal/audit/repo/ and audittest/repo/. Run whenever you change queries.sql in either place. Requires mise run infra:start (sqlc connects to the local Postgres to type-check queries). |
mise run gen:webhooks-server | Regenerate catalog_gen.go and catalog_gen.yaml after adding a new event def to server/internal/outbox/events/audit_log.go. |
mise run lint:server | golangci-lint including exhaustruct — keep struct literals complete when adding fields to Log*Event. |
mise run test:server | Runs the full server test suite. Takes the same arguments as go test (e.g. ./internal/audit/... ./internal/remotemcp/...). |
Maintaining this skill
This file documents conventions that evolve over time. Adding a new action, subject, or filter is already covered by "Jobs to be done" — those don't require skill edits. Structural changes do. Update this skill in the same commit when you make any of the following kinds of changes:
- Reorganising per-subject files (moving away from one-file-per-subject, merging subjects, renaming the plural convention).
- Renaming or reshaping
Log*Eventfields, or changing theLog*function signature. - Replacing
marshalAuditPayloador changing how snapshots are encoded. - Moving audit code out of
server/internal/audit/. - Changing how facets are computed — today they derive from row data with no registration; if that changes, the "no registration step" claim stops being true.
- Adding a new audit-relevant mise task that belongs on the cheat sheet.
- Introducing a new top-level concept (a new principal type for actors, a new kind of subject scoping beyond org/project, etc.).
Cross-references
gram-management-api— theauditlogsservice is itself a management API; adding a new endpoint or filter follows that skill's flow.gram-rbac—access_role,access_member, and other RBAC mutations are audited viaserver/internal/audit/access.go.golang—oopserror wrapping,sloglogging, transaction patterns, and the black-boxsetup_test.goconvention used by audit tests and service tests.postgresql— when adding or changing SQLc queries inaudit/queries.sqloraudittest/queries.sql.mise-tasks— when modifying the generator scripts under.mise-tasks/gen/.
Frequently asked questions
What to verify before installation and use
What does the gram-audit-logging source document cover?
Actor. The principal that caused the event — a urn.Principal carrying a type (user, role, service account) and an id, with optional display name and slug for human rendering.
How do I install gram-audit-logging?
The source record exposes this install command: npx skills add https://github.com/speakeasy-api/gram --skill ".agents/skills/gram-audit-logging". Inspect the command and pinned source before running it.
Alternatives
Compare before choosing
coreyhaines31/marketingskills
ab-testing
When the user wants to plan, design, or implement an A/B test or experiment, or build a growth experimentation program. Also use when the user mentions "A/B test," "split test," "experiment," "test this change," "variant copy," "multivariate test," "hypothesis," "should I test this," "which version is better," "test two versions," "statistical significance," "how long should I run this test," "growth experiments," "experiment velocity," "experiment backlog," "ICE score," "experimentation program
garrytan/gbrain
bulk-ingestion
End-to-end discipline for turning any large data source (audio libraries, email takeouts, document corpora, chat exports, API dumps) into brain pages at scale. The lifecycle spine: SCHEMA → ACCESS → TRIAL → EVALUATE → IMPROVE → CODIFY → TEST → SKILLIFY → BULK → MONITOR. State is tracked in a durable JSON manifest (see MANIFEST-PATTERN.md) so any crash, session boundary, or subagent fan-out resumes from ground truth instead of memory.
alirezarezvani/claude-skills
app-store-optimization
App Store Optimization (ASO) toolkit for researching keywords, analyzing competitor rankings, generating metadata suggestions, and improving app visibility on Apple App Store and Google Play Store. Use when the user asks about ASO, app store rankings, app metadata, app titles and descriptions, app store listings, app visibility, or mobile app marketing on iOS or Android. Supports keyword research and scoring, competitor keyword analysis, metadata optimization, A/B test planning, launch checklist
dotnet/skills
migrate-vstest-to-mtp
Migrates .NET test projects from VSTest to Microsoft.Testing.Platform (MTP). Use when user asks to "migrate to MTP", "switch from VSTest", "enable Microsoft.Testing.Platform", "use MTP runner", set OutputType=Exe only for test projects in Directory.Build.props, or mentions EnableMSTestRunner, EnableNUnitRunner, or UseMicrosoftTestingPlatformRunner. USE FOR: MTP behavioral differences vs VSTest (exit code 8, zero tests discovered, --ignore-exit-code, TESTINGPLATFORM_EXITCODE_IGNORE); centralizing