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.
1. Architecture in one diagram
Section titled “1. Architecture in one diagram” 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.
2. Package map
Section titled “2. Package map”| Package | Responsibility |
|---|---|
main.go | Three 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/ | Manager — sync.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. |
3. Lifecycle
Section titled “3. Lifecycle”Set by svcboot in cmd/root.go:
svcboot.Initialize(version.Name)— registers the chassis logger in the DI container.util.InitializeEnvironment(...)(invoked inside svcboot) — loads YAML, wires the config client, registers the vault client in the container as<name>-vaultclient. No DB.BootConfigLoad(ctx, serviceCfg)—cmd.bootConfigLoadparses theaudit.*block viaconfig.FromOrderedMapand stores the typed*config.Configin package-scopedrt.PreStart(ctx, serviceCfg)—cmd.preStartresolves the vault client, constructssigner.VaultFactory, builds thelogmgr.Manager, builds theserver.Server, builds and starts theworkers.Pool. Errors here cause process exit.RegisterRoutes(ctx, router)—cmd.registerRoutescallshttpapi.RegisterRoutes. Routes are wrapped inSecureWithJWTTokenbefore reaching our handlers.- HTTP server starts. Health/ready probes (added by svcboot) begin returning 200.
- 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).
4. Key contracts
Section titled “4. Key contracts”logmgr.Manager
Section titled “logmgr.Manager”func (m *Manager) Get(ctx, tenant, logID string) (*Managed, error)func (m *Manager) Snapshot() []*Managedfunc (m *Manager) SnapshotForTenant(tenant string) []*Managedfunc (m *Manager) Close() errorGetis the only entrypoint that creates/opens logs. ValidatestenantandlogID(path-safe charset, no traversal); resolves the routing rule; pulls the per-tenant signer; callsaudit.Createoraudit.Open. Concurrent calls race-correctly: the lock is released aroundaudit.Create/Open, then re-taken to register; a loser closes the duplicate*audit.Log.SnapshotForTenantbacks/logs. It returns a copy of the per-tenant slice to keep map iteration safe.Closejoins all per-log close errors witherrors.Join— callers see the aggregate, not just the first.
signer.VaultFactory
Section titled “signer.VaultFactory”func (f *VaultFactory) For(ctx, tenant string) (types.Signer, error)func (f *VaultFactory) Invalidate(tenant string)Fordoes an RWLock-protected cache lookup, falls through tovc.GetSecret(ctx, path)on miss, parses the PEM (PKCS#8 or raw 64-byte block), and caches anauditsigner.Local.Invalidateis the rotation hook — not wired to any caller yet.
workers.Pool
Section titled “workers.Pool”func (p *Pool) Start() // resets backoff state + starts goroutinesfunc (p *Pool) Stop() // signals + waits for exit// (internal) p.dueNow / p.recordSuccess / p.recordFailureWorker structure (see supervise / runLoop / safeTick):
supervise ─► runLoop ─► safeTick ─► <tick fn> ^ ^ ^ | | └ tick-scope panic recovery | └ loop-scope panic recovery └ outer pause + restart loopdueNow(key, now, cadence) is the single gate every tick consults
before doing work — it checks both the success cadence and the
failure backoff window.
5. Where to add things
Section titled “5. Where to add things”A new HTTP endpoint
Section titled “A new HTTP endpoint”- Add the wire types and an interface method in
svcapi/svcapi.go. - Implement on
*server.Serverinserver/server.go— start withtenantOf(ctx), then delegate tologmgror directly tolib-auditvia*Managed. - Register the route in
httpapi.RegisterRouteswithhttputil.Handle(... httputil.SecureWithJWTToken(jsonHandle(...))). - Document in
docs/api-reference.mdanddocs/openapi.yaml.
A new sync target
Section titled “A new sync target”- Add the kind to
config.SyncTargetvalidation (currently none — kinds are open strings). - Add a case in
workers.buildSyncer(t)returning anaudsync.Syncerimpl. - lib-audit must ship the impl (
audit/sync.S3,.GCS,.Azureare stubs today). Until then, the worker logs the error and backs off.
A new anchor target
Section titled “A new anchor target”- Add the kind/URL to your YAML’s
audit.anchor[*].targets. - lib-audit must ship a real
anchor.Anchorimpl — currently all anchor kinds return “not implemented.” Until then,workers.anchorTicklogswould anchor (stub).
Per-tenant / sub-tenant authorisation
Section titled “Per-tenant / sub-tenant authorisation”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):
- Pull the JWT claim you care about in
server.tenantOf(e.g.ctx.Value(kconst.ClaimsUserCode)). - Add an authorization block to the config schema (per-tenant + per-log_id + per-principal claim).
- Gate each
server.Servermethod against the block before callinglogmgr.Get.
Skeleton sketched in design/pending.md under “API / protocol —
Sub-tenant ACLs.”
A new background worker
Section titled “A new background worker”- Add a
tickconstant and a<name>Tick(ctx, now)method on*Poolfollowing the existing pattern. - In
Pool.Start,p.wg.Add(1)andgo p.supervise(ctx, "<name>", <name>Tick, …). - Use
p.dueNow(key, now, cadence)+recordSuccess/Failureso you inherit the backoff behaviour for free.
6. Local dev workflow
Section titled “6. Local dev workflow”cd $KISAI_REPO/baas/audit
# 1. Buildgo 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.binTo 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:
go build ./...go vet ./...go test ./... # no tests yet — see testing doc7. Code conventions
Section titled “7. Code conventions”- Logging:
logger.Loggerfromlib-zero/logger. Field-style viaWithStrFields("k", v, ...).Info("msg"). Don’t import the stdliblog/slog. - Config: read from
smap.OrderedMapwith dotted-key access (om.Get("audit.base_dir").String()). For slices usesmap.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 likelogmgr.IsEmptyLogErrso callers don’t string-match. Internal errors translate tosvcapi.ErrInternal(...)at the service boundary. - Context: thread
ctxthrough every call. Nevercontext.TODO(). - No globals: the one exception is the
rt *runtimestruct incmd/root.gothat carries state across svcboot callbacks. Avoid growing it.
8. Tests
Section titled “8. Tests”There are none yet. The recommended structure is in the end-to-end testing doc.
9. Pending and known gaps
Section titled “9. Pending and known gaps”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.