Skip to content
Talk to our solutions team

Platform Integration

Audience: platform developers — kis.ai engineers maintaining or extending audit.svc. You have access to the kis.ai monorepo and the chassis libraries.

You also need the HTTP API reference — the internal architecture only makes sense alongside the wire surface it serves.

HTTP request (JWT + X-Tenant)
┌──────────────────────▼────────────────────────┐
│ chassis middleware chain │
│ TenantMiddleware (sets kconst.Tenant) │
│ SecureWithJWTToken (validates JWT, │
│ cross-checks tenant) │
└──────────────────────┬────────────────────────┘
┌──────────────────────▼────────────────────────┐
│ httprouter route → httpapi.handle* │
│ │ │
│ svcapi.AuditService method │
│ │ │
│ server.Server.{Submit,Head,Seal,...} │
└──────────────────────┬────────────────────────┘
┌──────────────────────▼────────────────────────┐
│ logmgr.Manager → *Managed (pool, per-tenant) │
│ signer.VaultFactory.For(tenant) │
└──────────────────────┬────────────────────────┘
┌──────────────────────▼────────────────────────┐
│ lib-audit (Append, Seal, Read, Proofs) │
│ Disk: <base_dir>/<tenant>/<log_id>/ │
└───────────────────────────────────────────────┘
Parallel: workers.Pool ─► sealer / syncer / anchorer
(drives same *Managed pool)

No database. All state is the chassis config service + vault + the on-disk Merkle log tree.

PackageResponsibility
main.goThree lines: cmd.Execute().
cmd/Cobra wiring + svcboot lifecycle callbacks. root.go is the wiring; version.go is the version subcommand.
config/Config struct + FromOrderedMap(smap.OrderedMap) materialiser. Glob matcher used by MatchLog.
version/Build-stamped vars (Version, BuildCommit, …) and the stable Name = "audit" / ClientName = "audit" constants.
signer/VaultFactory — per-tenant key fetch from vault, in-process cache, returns a types.Signer (an auditsigner.Local wrapping the Ed25519 private key).
logmgr/Managersync.Map-style pool of open *Managed (one per (tenant, log_id)). Lazy open. Tenant-scoped snapshot for /logs. Filesystem path layout. Validation. IsEmptyLogErr shim around lib-audit’s untyped empty-log error.
server/Server implements svcapi.AuditService. Per-RPC: read tenant from ctx, delegate to logmgr + lib-audit, translate types.
svcapi/Wire types (request/response/error). Hand-written; mirrors gRPC semantics so a future gRPC binding can share the interface.
httpapi/RegisterRoutes(router, server, serviceName). Six routes, each httputil.Handle(... httputil.SecureWithJWTToken(jsonHandle(...))).
workers/Pool — three supervised goroutines (sealer / syncer / anchorer) with panic recovery + per-target exponential backoff.
design/spec.md (architectural design, predates the chassis migration — drift noted), pending.md (outstanding work tracker).
docs/This documentation tree.

Set by svcboot in cmd/root.go:

  1. svcboot.Initialize(version.Name) — registers the chassis logger in the DI container.
  2. util.InitializeEnvironment(...) (invoked inside svcboot) — loads YAML, wires the config client, registers the vault client in the container as <name>-vaultclient. No DB.
  3. BootConfigLoad(ctx, serviceCfg)cmd.bootConfigLoad parses the audit.* block via config.FromOrderedMap and stores the typed *config.Config in package-scoped rt.
  4. PreStart(ctx, serviceCfg)cmd.preStart resolves the vault client, constructs signer.VaultFactory, builds the logmgr.Manager, builds the server.Server, builds and starts the workers.Pool. Errors here cause process exit.
  5. RegisterRoutes(ctx, router)cmd.registerRoutes calls httpapi.RegisterRoutes. Routes are wrapped in SecureWithJWTToken before reaching our handlers.
  6. HTTP server starts. Health/ready probes (added by svcboot) begin returning 200.
  7. On SIGTERM/SIGINT: svcboot drains the HTTP server then invokes cmd.onShutdown, which stops the workers and closes the log manager (flushes manifests, releases file locks).
func (m *Manager) Get(ctx, tenant, logID string) (*Managed, error)
func (m *Manager) Snapshot() []*Managed
func (m *Manager) SnapshotForTenant(tenant string) []*Managed
func (m *Manager) Close() error
  • Get is the only entrypoint that creates/opens logs. Validates tenant and logID (path-safe charset, no traversal); resolves the routing rule; pulls the per-tenant signer; calls audit.Create or audit.Open. Concurrent calls race-correctly: the lock is released around audit.Create/Open, then re-taken to register; a loser closes the duplicate *audit.Log.
  • SnapshotForTenant backs /logs. It returns a copy of the per-tenant slice to keep map iteration safe.
  • Close joins all per-log close errors with errors.Join — callers see the aggregate, not just the first.
func (f *VaultFactory) For(ctx, tenant string) (types.Signer, error)
func (f *VaultFactory) Invalidate(tenant string)
  • For does an RWLock-protected cache lookup, falls through to vc.GetSecret(ctx, path) on miss, parses the PEM (PKCS#8 or raw 64-byte block), and caches an auditsigner.Local.
  • Invalidate is the rotation hook — not wired to any caller yet.
func (p *Pool) Start() // resets backoff state + starts goroutines
func (p *Pool) Stop() // signals + waits for exit
// (internal) p.dueNow / p.recordSuccess / p.recordFailure

Worker structure (see supervise / runLoop / safeTick):

supervise ─► runLoop ─► safeTick ─► <tick fn>
^ ^ ^
| | └ tick-scope panic recovery
| └ loop-scope panic recovery
└ outer pause + restart loop

dueNow(key, now, cadence) is the single gate every tick consults before doing work — it checks both the success cadence and the failure backoff window.

  1. Add the wire types and an interface method in svcapi/svcapi.go.
  2. Implement on *server.Server in server/server.go — start with tenantOf(ctx), then delegate to logmgr or directly to lib-audit via *Managed.
  3. Register the route in httpapi.RegisterRoutes with httputil.Handle(... httputil.SecureWithJWTToken(jsonHandle(...))).
  4. Document in docs/api-reference.md and docs/openapi.yaml.
  1. Add the kind to config.SyncTarget validation (currently none — kinds are open strings).
  2. Add a case in workers.buildSyncer(t) returning an audsync.Syncer impl.
  3. lib-audit must ship the impl (audit/sync.S3, .GCS, .Azure are stubs today). Until then, the worker logs the error and backs off.
  1. Add the kind/URL to your YAML’s audit.anchor[*].targets.
  2. lib-audit must ship a real anchor.Anchor impl — currently all anchor kinds return “not implemented.” Until then, workers.anchorTick logs would anchor (stub).

The chassis already enforces JWT-tenant ↔ X-Tenant agreement. For finer authz (e.g. service A can append to auth/, service B can only read):

  1. Pull the JWT claim you care about in server.tenantOf (e.g. ctx.Value(kconst.ClaimsUserCode)).
  2. Add an authorization block to the config schema (per-tenant + per-log_id + per-principal claim).
  3. Gate each server.Server method against the block before calling logmgr.Get.

Skeleton sketched in design/pending.md under “API / protocol — Sub-tenant ACLs.”

  1. Add a tick constant and a <name>Tick(ctx, now) method on *Pool following the existing pattern.
  2. In Pool.Start, p.wg.Add(1) and go p.supervise(ctx, "<name>", <name>Tick, …).
  3. Use p.dueNow(key, now, cadence) + recordSuccess/Failure so you inherit the backoff behaviour for free.
Terminal window
cd $KISAI_REPO/baas/audit
# 1. Build
go build -o dist/audit-svc.bin .
# 2. Edit the dev config in place
$EDITOR .audit-svc.yaml
# 3. Run (uses .audit-svc.yaml by default — that's DefaultCfg)
./dist/audit-svc.bin

To skip vault for fast iteration, point vault_path_template at a local KV-shim or stub out signer.VaultFactory.For in a fork. A proper “inline signer” mode (private key in the YAML, dev-only) is not in the codebase — add it under signer.NewLocalFromConfig(cfg) if you need it.

Build + vet + test:

Terminal window
go build ./...
go vet ./...
go test ./... # no tests yet — see testing doc
  • Logging: logger.Logger from lib-zero/logger. Field-style via WithStrFields("k", v, ...).Info("msg"). Don’t import the stdlib log/slog.
  • Config: read from smap.OrderedMap with dotted-key access (om.Get("audit.base_dir").String()). For slices use smap.GetMapSlice(om.Get("audit.routing")).
  • Errors: prefer errors.Is/As. Where a sentinel is missing upstream (e.g. lib-audit’s empty-log error), wrap in a service helper like logmgr.IsEmptyLogErr so callers don’t string-match. Internal errors translate to svcapi.ErrInternal(...) at the service boundary.
  • Context: thread ctx through every call. Never context.TODO().
  • No globals: the one exception is the rt *runtime struct in cmd/root.go that carries state across svcboot callbacks. Avoid growing it.

There are none yet. The recommended structure is in the end-to-end testing doc.

Tracked in design/pending.md. Top items: write tests, wire VaultFactory.Invalidate for key rotation, lib-audit upstream gaps (S3 syncer, anchor impls), gRPC binding, sub-tenant ACLs, GET /pubkey, multi-replica affinity story.