TLS proxy or service mesh
|
authenticated HTTP or MCP
|
production kernel
| scope and purpose checks
| PostgreSQL tenant transaction
| real embedding provider
| encrypted artifact store
| receipts and idempotency
|
PostgreSQL 18 + pgvector
|
separate effect worker
|
allowlisted HTTPS receivers
The runtime role is agentic_app. It is not a superuser and does not have
BYPASSRLS. Migrations run separately as the PostgreSQL administrator.
External PostgreSQL deployments require pgvector 0.8.0 or newer. The included
Compose profile pins pgvector 0.8.6.
Production releases are published as:
ghcr.io/jason-doyle/agentic-data-kernel:<version>
Set AGENTIC_DATA_IMAGE to an exact version in .env. The same image runs the
migration command, HTTP service, effect worker, and production MCP process.
Prereleases also receive the moving next tag, but production deployments
should not rely on it.
To use the published image:
docker compose --profile server pull
docker compose --profile server up --no-buildTo build the checked-out source instead:
docker compose --profile server up --buildGenerate local secrets:
.\scripts\generate-secrets.ps1Copy .env.example to .env, replace every placeholder, and configure an
OpenAI-compatible embeddings endpoint.
DATABASE_SSL is required. Set it to require for managed PostgreSQL and to
disable only for local development or an authenticated loopback database
proxy. If the provider CA is not
already in the container's trust store, set DATABASE_CA_CERT_BASE64 to the
base64 encoding of its PEM CA bundle.
Do not add SSL query parameters such as sslmode to DATABASE_URL or
MIGRATION_DATABASE_URL; use these dedicated settings.
The prod:* npm scripts load .env through Node's --env-file option.
Start PostgreSQL and create the restricted role:
docker compose up -d postgres
docker compose run --rm bootstrapCloud and Kubernetes deployments use the equivalent packaged command:
node dist\production\cli.js bootstrap-roleFrom a source checkout, npm run prod:bootstrap runs the same command with
.env.
It reads MIGRATION_DATABASE_URL and APP_DATABASE_PASSWORD, then creates or
repairs the fixed agentic_app role with no superuser, database-creation,
role-creation, inheritance, or RLS-bypass privileges.
The password must contain 16 to 256 printable ASCII characters without spaces.
Existing agentic_app role memberships or object ownership cause bootstrap to
fail rather than preserve privilege-bearing state.
Apply migrations with the administrative connection:
npm run prod:migrateCreate an API key:
node --env-file=.env dist\production\cli.js create-key `
--tenant example `
--tenant-name "Example" `
--principal catalog-agent `
--scopes data:read,data:write,inventory:admin,orders:write,effects:write `
--purposes catalog-ingestion,checkout `
--effect-currency USD `
--effect-budget 1000The token is shown once. Store it in a secret manager.
Run the HTTP service and effect worker together from source:
docker compose --profile server up --buildFor a pinned published image, omit --build and use the commands in
Choose an image.
The Compose profile publishes only the Caddy TLS endpoint at
https://localhost:8443. The Node.js service remains private on the Compose
network. Caddy uses its internal CA for the local profile. Trust or replace that
CA according to your environment before connecting clients.
The artifact-init service creates the bind-mounted artifact directory and
assigns it to the non-root application UID before the app and worker start.
Or run them directly:
npm run prod:serve
npm run prod:workerValidated reference workload templates are available for:
- Kubernetes through Helm;
- Azure Container Apps through Bicep;
- AWS ECS Fargate through OpenTofu;
- Google Kubernetes Engine through OpenTofu and Helm.
They consume existing private PostgreSQL, secret-management, network, TLS, and shared-filesystem resources rather than placing credentials in infrastructure state. Read Deployment Templates and the shared Deployment Contract.
Every production API request except liveness, readiness, and metrics requires:
Authorization: Bearer adk.<key-id>.<secret>
X-Agent-Purpose: approved-purpose
The intent envelope must contain the same tenant, principal, and purpose as the authenticated key. A mismatch is rejected.
Production routes, through the TLS proxy:
| Route | Authentication | Purpose |
|---|---|---|
GET /health/live |
No | Process liveness |
GET /health/ready |
No | Database and migration readiness |
GET /metrics |
No | Prometheus text metrics |
GET /v1/catalog |
Yes | Production capabilities |
POST /v1/execute |
Yes | One scoped Agent Intent operation |
POST /mcp |
Yes | Optional stateless Streamable HTTP MCP |
There is no production SQL route.
Set:
AGENTIC_DATA_API_KEY
AGENTIC_DATA_PURPOSE
Then run:
npm run prod:mcpThe MCP process authenticates at startup, binds every tool to that identity, and revalidates revocation, expiry, tenant status, scope, and purpose for every operation. It does not accept caller-supplied tenant or principal identities.
The production HTTP service can expose a stateless MCP endpoint at:
POST /mcp
It is disabled by default. Enable it only behind the same trusted HTTPS edge as the production API:
MCP_HTTP_ENABLED=true
MCP_HTTP_PUBLIC_ORIGIN=https://agent-data.example.com
MCP_HTTP_WRITE_ENABLED=false
Clients must send on every request:
Authorization: Bearer <ADK API key>
X-Agent-Purpose: <approved-purpose>
The public origin must contain only the exact public scheme, hostname, and
optional port. Remote MCP validates the Host header and any supplied
Origin header before authenticating. Reverse proxies must preserve the
original public Host.
Remote MCP is stateless and uses JSON responses rather than a long-lived SSE session. Authentication, revocation, expiry, tenant state, purpose, and scope are checked for every request and operation. JSON-RPC batches are rejected so one HTTP request cannot multiply work behind one rate-limit charge.
With MCP_HTTP_WRITE_ENABLED=false, the endpoint advertises only:
search_knowledge
resolve_claims
get_machine
list_effects
explain_trace
Setting it to true additionally advertises execute_operation. Use a
separate, narrowly scoped key and an external review or approval boundary
before exposing write or effect operations.
GET /mcp and DELETE /mcp are not supported in stateless JSON-response mode.
ARTIFACT_KEYRING is a JSON object whose values are base64-encoded 32-byte
keys:
{"v1":"base64-key","v2":"base64-key"}ARTIFACT_CURRENT_KEY_ID selects the key for new writes. Keep old keys in the
ring until every artifact using them has expired, been deleted, or been
reencrypted.
Artifact files use:
- a random 96-bit nonce;
- AES-256-GCM;
- tenant-derived keys using HKDF-SHA256;
- authenticated metadata binding tenant, artifact ID, media type, and content hash;
- atomic hard-link publication from a synced temporary file;
- immutable content-address verification.
Artifact metadata writes are serialized with a database advisory lock. A failed or interrupted request can leave encrypted ciphertext without metadata. This is safer than deleting a file another concurrent transaction may reference. An administrator removes aged orphan files with:
node --env-file=.env dist\production\cli.js reconcile-artifactsReconciliation requires MIGRATION_DATABASE_URL and refuses an RLS-restricted
runtime connection.
The production profile calls:
POST <EMBEDDING_BASE_URL>/embeddings
It sends the configured model, input list, and dimensions. Provider errors, timeouts, invalid result counts, and dimension mismatches fail the operation. There is no hash-vector fallback.
Configure one active embedding space:
EMBEDDING_MODEL=text-embedding-3-small
EMBEDDING_VERSION=openai-compatible-v1
EMBEDDING_DIMENSIONS=1536
EMBEDDING_DIMENSIONS accepts 1 through 2000, the HNSW limit for pgvector's
single-precision vector type. Migrations create a matching expression index
such as assertions_embedding_hnsw_768.
Every provider result must contain the configured number of finite values. Zero vectors are rejected because they cannot participate in cosine search.
The database enforces one model, version, and dimension across all assertions. Once assertions exist, changing any of those values is rejected. A model change requires an explicit re-embedding migration so vectors from different semantic spaces are never compared.
Hybrid retrieval first collects bounded vector and full-text candidate sets, then applies temporal validity, graph scope, and combined-score reranking. Candidate selection keeps those same validity and graph constraints inside the indexed scans to preserve current-state and graph-filter behavior.
Tune retrieval with:
SEARCH_CANDIDATE_LIMIT=200
HNSW_EF_SEARCH=100
HNSW_MAX_SCAN_TUPLES=20000
Larger values can improve recall under selective tenant, temporal, or graph
filters at the cost of latency and memory. SEARCH_CANDIDATE_LIMIT should
remain several times larger than the requested result limit.
Upgrading an existing 1536-dimensional database preserves its vectors and records their current model and version as the active space. Migration 002 refuses databases that contain multiple spaces or zero vectors. The column conversion and index replacement require a maintenance window on large assertion tables.
Artifact plaintext may be sent to the configured provider when an assertion uses that artifact as evidence. Use an approved private endpoint when the data must not leave your environment.
An effect is created only when:
- the API key has
effects:write; - the request purpose is approved;
- the key is active and unexpired;
- the effect amount fits within the remaining budget;
- the target uses HTTPS and its host is allowlisted.
Money values use canonical decimal strings with at most four decimal places. The API key budget and payment amount must use the same three-letter currency.
Payment requests also provide a provider status URL. After normal delivery attempts are exhausted, the worker continues status reconciliation rather than stranding the order and reserved budget.
The worker acquires a database lease and authorization fence while locking the authorizing key. Revocation that commits before this fence cancels the effect. Once the fence is committed, retries are permitted only with the same effect ID and idempotency key because the remote side may already have acted.
The worker rejects private and reserved DNS results, pins the connection to the
validated address, retains TLS verification for the original hostname, and
disables redirects.
Timeouts, retryable responses, and ambiguous acknowledgements become unknown.
They are retried with the same idempotency key up to
EFFECT_MAX_ATTEMPTS. A receiver must return a stable providerReference for
success.
The worker rotates across active tenants between leases. Multiple replicas can
run concurrently because leases use SKIP LOCKED, but PostgreSQL connection
capacity must include every replica. On shutdown, outbound requests are
aborted. Ambiguous results remain durable and are reconciled with the original
effect ID and provider idempotency key.
The worker exposes private liveness, readiness, and metrics endpoints on
WORKER_MONITOR_HOST and WORKER_MONITOR_PORT, defaulting to
127.0.0.1:4319.
Public clients cannot submit payment outcomes.
request_effect attaches an effect to an exact non-retail workflow revision
and requires active decision and directive assertions. These bindings are
stored with composite tenant FKs and causal lineage edges. They document why an
effect was requested; API-key scope, purpose, host allowlists, and budget
checks remain the authorization boundary.
Generic effects use the same leases, authorization fences, retry policy, and status reconciliation as payments. On a terminal result, the worker settles the reserved budget but does not mutate inventory or workflow state. The agent must inspect the durable effect and explicitly advance its workflow.
Migration 003 adds generic workflow metadata, authority bindings, and the
tenant-isolated lineage table. Existing retail effects retain the
retail_order_payment outcome handler.
Before adding provider-level idempotency enforcement, migration 003 checks historical effects for duplicate keys within the same tenant and provider origin. Resolve any reported collision before retrying the migration; the migration rolls back without changing the schema.
process_timers is intentionally tenant-scoped and is not an autonomous global
scheduler. Invoke it periodically through an authenticated Agent Intent client
whose key has workflows:run for that tenant. Each call locks and processes at
most 100 due timers with SKIP LOCKED; continue until the returned array is
empty. Run only one logical scheduler per tenant unless duplicate invocations
are acceptable.
RATE_LIMIT_PER_MINUTE is enforced per API process and API key. A separate
pre-authentication limiter uses the resolved client address. Set
TRUSTED_PROXY_HOPS to the exact number of trusted proxies that append
X-Forwarded-For, and keep the Node.js listener unreachable except through
those proxies. Leave it at 0 for direct connections.
Multiple API replicas multiply the built-in allowance. Production multi-replica deployments need a shared limiter at the ingress, gateway, or edge.
Create a checksum-manifested backup:
docker compose --profile server stop app worker
.\scripts\backup.ps1Restore after stopping application and worker processes:
.\scripts\restore.ps1 `
-BackupDirectory .backups\<timestamp> `
-ConfirmRestoreThe database and encrypted artifacts are one recovery unit. Backup manifests
are authenticated with BACKUP_MANIFEST_KEY, include the exact database
migration versions and checksums, and must be copied with their signature.
Backup and restore
set a database maintenance flag that every supported writer checks while
holding a transaction lock. The scripts also refuse running Compose app or
worker containers.
npm run prod:load -- `
--url https://localhost:8443 `
--tenant example `
--principal load-agent `
--purpose load-test `
--requests 1000 `
--concurrency 25The harness reports success count, requests per second, and p50, p95, and p99 latency. It is a smoke tool, not a substitute for sustained workload, fault, and recovery testing.
- Apply migrations through the administrator connection.
- Verify
/health/ready. - Confirm the runtime database role is not a superuser and has no
BYPASSRLS. - Test two-tenant isolation.
- Confirm artifact files do not contain plaintext.
- Verify the embedding endpoint and dimension.
- Configure effect allowlists and budgets.
- Start the effect worker.
- Capture
/metricsand structured logs. - Run a backup and restore drill.
- Run the authenticated load smoke test.
- Review
SECURITY.mdbefore exposing the service.