Skip to content
Registry StackDocsDevelopment (unreleased)

Deploy Registry Casework

View as Markdown

If you operate Registry Casework, deploy a reviewed policy package against PostgreSQL, verify its activation, then establish the teams that can see and decide its review tasks. Casework returns a review result; the producer keeps all authority to change its subject.

  • PostgreSQL 17 or newer, with TLS and separate migration and runtime credentials.
  • An OpenID Connect issuer, the intended audience, and an explicit list of admitted clients.
  • A private HTTP listener behind a TLS proxy you operate.
  • A reviewed policy package, the matching binaries, and secret files the process can read.
  • A durable audit destination and an archive for it.
  • A directory team serving each queue reviewers work.

Review and directory state live in PostgreSQL; the audit journal lives outside it. A current-source policy also needs a read-only context provider.

Establish issuer signing-key trust and TLS trust separately. The OIDC connection guide explains issuer configuration and key discovery; the Casework runtime’s authentication.oidc block binds the accepted resource, clients, and human identity claim. Configure downstream HTTPS trust through the owning connection’s certificate setting, rather than weakening verification.

Present a resource-specific access token and select the authored profile with Registry-Casework-Profile on Casework task and directory routes. Shared producer Review routes use Registry-Review-Profile. The Casework API owns the route, token, scope, and profile requirements; a scope does not replace an authored access profile.

For a producer completion destination, follow completion destinations, then read the result through the producer’s independently authorized client. The wake carries no decision and cannot approve or apply a source operation. Acknowledge only after accepting the wake under the delivery contract; authentication, binding, and acknowledgement failures must remain visible to the dispatcher. For released 0.43 behavior, return exactly 204 No Content with an empty body after acceptance. The shared delivery contract governs the next release’s acknowledgement behavior; returning an empty 204 remains compatible with both.

Install matching casework and caseworkctl binaries on the serving and operator hosts:

Terminal window
curl -fsSL https://github.com/registrystack/registry-stack/releases/latest/download/casework-install.sh | bash
casework --version
caseworkctl --version

Read the installer before running it. Its SHA256SUMS check does not establish release authenticity; use the release verification procedure for that.

The container image carries both binaries. Its entrypoint serves Casework from /etc/registry-casework/runtime.yaml; run operator commands with the entrypoint set to /usr/local/bin/caseworkctl. Pin the image digest, mount the package, runtime file, and secret root read-only, and give the audit directory durable writable storage.

Use a new database. As your PostgreSQL administrator, create a schema-owning migration role, a separate runtime role, and the database:

CREATE ROLE casework_migration LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE
NOINHERIT NOBYPASSRLS PASSWORD '<migration-password>';
CREATE ROLE casework_runtime LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE
NOINHERIT NOBYPASSRLS PASSWORD '<runtime-password>';
CREATE DATABASE casework;

In that database, grant connection access and give the migration role the schema:

REVOKE ALL ON DATABASE casework FROM PUBLIC;
GRANT CONNECT ON DATABASE casework TO casework_migration, casework_runtime;
ALTER SCHEMA public OWNER TO casework_migration;
REVOKE ALL ON SCHEMA public FROM PUBLIC;

Only caseworkctl apply uses the migration credential, so keep it off the serving host. Apply grants the runtime role what it serves with and keeps the activation ledger read-only to it.

If apply reports casework.activation.role-mode-weakened, make the ownership, privilege, or trigger correction it names, then plan again; never give the runtime role ledger writes to get past it. When both credentials name one role, the deployment records single-role mode, and status says the runtime credential can activate packages.

The runtime file is yours, kept apart from the authored policy. This one uses mounted secrets and a TLS proxy in front:

apiVersion: id.registrystack.org/formats/casework/runtime/v1alpha1
kind: CaseworkRuntimeConfig
identity:
databaseId: casework-decisions
package:
root: /srv/registry-casework/package
listener:
bind: 10.42.0.7:8100
tlsTermination: operator-controlled-upstream
networkExposure: private-address
secretProviders:
file:
root: /run/secrets/registry-casework
database:
runtimeUrlRef: secret:file/runtime-database-url
migrationUrlRef: secret:file/migration-database-url
authentication:
oidc:
issuer: https://identity.example.test/realms/registry
audience: urn:example:casework
allowedClients: [casework-console, review-producer]
scopeClaim: scope
humanIdentity:
claim: registry_actor_kind
value: human
audit:
path: /var/lib/registry-casework/audit/casework.ndjson
hashKeyRef: secret:file/casework-audit-key

Every path is absolute, and the runtime file may not pass through a symbolic link. Unknown keys and explicit nulls are refused at their position; omit a key to take its default. identity.databaseId is your stable name for this deployment: the first apply records it, and every later apply and start refuses a database that recorded another.

Each ...Ref key names a secret, never its value: secret:file/<name> resolves under secretProviders.file.root. Mount each secret as a file owned by the runtime user, mode 0400 or 0600, with no symbolic or extra hard link. operator-controlled-upstream requires allowedClients to list its clients. Ship the audit journal to your archive; Review retention and audit covers its key and rotation.

A current-source policy names a provider by its deploymentRef. Bind it to a read-only provider:

contextProviders:
facility-registry:
baseUrl: https://registry.example.test
statusProfile: casework-review-status
tokenEndpoint: https://identity.example.test/oauth2/token
clientAssertionAudience: https://identity.example.test
resource: urn:breg:facility-registry
scopes: [registry:proposal:review-status]
clientIdRef: secret:file/breg-status-client-id
clientAssertionKeyRef: secret:file/breg-status-client-key

This Base Registry Engine (BReg) status reader checks reviewability only; its statusProfile must match the authored service-status profile. Human display uses the reviewer’s own bearer, so source authorization and disclosure still apply. A Casework approval never applies a BReg Proposal.

Check the authored project against the runtime file before anything touches a database:

Terminal window
caseworkctl check ./decisions --runtime-config /etc/registry-casework/runtime.yaml

The check opens no package, database, issuer, provider, or secret. Add --environment to fill ${VAR} expressions from the current environment. A clean check does not prove live readiness.

runtime.yaml

Source preview, pre-1.0

Generated from products/casework/generated/runtime/runtime.schema.json. 77 key paths. Explained in docs/site/src/content/docs/operate/casework.mdx.

Key pathTypeRequiredAccepted valuesConstraints
apiVersionstringYes
  • id.registrystack.org/formats/casework/runtime/v1alpha1
None specified
auditThe audit journal: where it is written and the key its hashes use.objectYesNot specifiedNone specified
audit.destinationWhere audit entries go: a rotated file (the default) or stdout.stringNo
  • file
  • stdout
Schema default, where declared: "file"
None specified
audit.hashKeyRefSecret reference to the audit hash key.stringYesNot specified
  • pattern: ^(?:secret:env/[A-Z][A-Z0-9_]{0,127}|secret:file/[a-z][a-z0-9._-]{0,127})$
audit.pathThe active audit file. Required for, and only allowed with, file.stringConditionalNot specified
  • pattern: ^/+(?:(?:[^/.][^/]*|\.(?:[^/.][^/]*|\.[^/]+)?)/+)*(?:[^/.][^/]*|\.(?:[^/.][^/]*|\.[^/]+))$
audit.retentionDaysDelete rotated files older than this many days. Only allowed with file; omitted, rotated files are kept 90 days.integerNoNot specified
  • format: uint32
  • maximum: 36500
  • minimum: 1
audit.rotateBytesRotate the active file once it reaches this many bytes. Only allowed with file; omitted, the file rotates at 100 MiB.integerNoNot specified
  • format: uint64
  • maximum: 4294967295
  • minimum: 1048576
authenticationobjectYesNot specifiedNone specified
authentication.oidcThe access tokens this runtime accepts: the issuer and its keys, the clients admitted, the scope claim, and the claim that marks a human actor.objectYesNot specifiedNone specified
authentication.oidc.allowInsecureHttpPermit plain HTTP beyond loopback to the configured issuer and to a JWKS URI declared in configuration. TLS remains required unless the operator explicitly accepts this transport risk.booleanNoNot specifiedSchema default, where declared: falseNone specified
authentication.oidc.allowedClientsThe OAuth clients whose access tokens are admitted: unrestricted to admit a token from every client the issuer verifies, or a list of at least one distinct client. A runtime decides whether unrestricted is acceptable in production.array | stringYes
  • unrestricted

No bound

or

  • minItems: 1
  • uniqueItems: true
authentication.oidc.allowedClients[]Array itemstringConditionalNot specifiedNone specified
authentication.oidc.assertionIssuersAssertion authorities each client may exchange a subject token from, keyed by client identifier. Omitted, no assertion-issuer rule applies; written, it lists at least one client, each with at least one authority. Once a client is listed, a token it exchanged is accepted only for one of that client's declared authorities, and a client that is not listed may exchange from no authority.objectNoNot specified
  • maxProperties: 64
  • minProperties: 1
authentication.oidc.assertionIssuers.*Map valuearrayNoNot specified
  • maxItems: 16
  • minItems: 1
  • propertyNames.maxLength: 128
  • propertyNames.minLength: 1
  • propertyNames.pattern: ^[^\u0000-\u001F\u007F-\u009F]+$
  • uniqueItems: true
authentication.oidc.assertionIssuers.*[]Array itemstringNoNot specified
  • maxLength: 512
  • minLength: 1
authentication.oidc.audienceThe audience every accepted access token must carry in aud.stringYesNot specified
  • maxLength: 512
  • minLength: 1
authentication.oidc.humanIdentityobjectNoNot specifiedNone specified
authentication.oidc.humanIdentity.claimstringNoNot specifiedSchema default, where declared: "registry_actor_kind"None specified
authentication.oidc.humanIdentity.valuestringNoNot specifiedSchema default, where declared: "human"None specified
authentication.oidc.issuerExact issuer accepted in access-token iss claims, an absolute https URL, or a plain http URL when allowInsecureHttp is set.stringYesNot specified
  • format: uri
  • maxLength: 2048
  • pattern: ^[Hh][Tt][Tt][Pp][Ss]?://[^/?#@\u005C\u0000-\u0020\u007F-\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]+([/?#][^\u005C\u0000-\u0020\u007F-\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]*)?$
authentication.oidc.jwksSourceWhere the issuer's signing keys come from. Absent reads the issuer's OpenID Connect discovery document.objectNoNot specifiedSchema default, where declared: {"type":"discovery"}None specified
authentication.oidc.jwksSource.documentRefAn exact secret reference: secret:file/name, resolved under secretProviders.file.root, or secret:env/NAME, resolved only when secretProviders.environment is declared.stringConditionalNot specified
  • pattern: ^(?:secret:env/[A-Z][A-Z0-9_]{0,127}|secret:file/[a-z][a-z0-9._-]{0,127})$
authentication.oidc.jwksSource.typestringYes
  • discovery
  • static
  • uri
None specified
authentication.oidc.jwksSource.uriAn absolute http or https URL with a host preserved as written, no user information, and no backslash, whitespace or control character. DNS case and equivalent IPv6 literals are permitted.stringConditionalNot specified
  • format: uri
  • maxLength: 2048
  • pattern: ^[Hh][Tt][Tt][Pp][Ss]?://[^/?#@\u005C\u0000-\u0020\u007F-\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]+([/?#][^\u005C\u0000-\u0020\u007F-\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]*)?$
authentication.oidc.maximumTokenLifetimeSecondsOptional deployment bound on the lifetime declared by an incoming access token, independently of the issuer's own policy.integerNoNot specified
  • format: uint64
  • maximum: 7200
  • minimum: 1
authentication.oidc.revokedKeyIdsSigning-key identifiers Casework refuses locally, even while the issuer still publishes them; listing a key does not revoke it at the issuer. Omitted, no key is refused; written, the list is non-empty.arrayNoNot specified
  • maxItems: 128
  • minItems: 1
  • uniqueItems: true
authentication.oidc.revokedKeyIds[]Array itemAn identifier another system issues, written exactly as its issuer gives it.stringNoNot specified
  • maxLength: 512
  • minLength: 1
  • pattern: ^[^\u0000-\u001F\u007F-\u009F]+$
authentication.oidc.scopeClaimstringNoNot specifiedSchema default, where declared: "registry_scopes"None specified
contextProvidersProvider transport bindings keyed by the deploymentRef casework.yaml declares.objectNoNot specifiedSchema default, where declared: {}None specified
contextProviders.*Map valueEnvironment-specific transport and reader identity. Human display always replaces it.objectNoNot specified
  • propertyNames.pattern: ^[a-z][a-z0-9_-]{0,63}$
contextProviders.*.baseUrlAn absolute http or https URL with a host preserved as written, no user information, and no backslash, whitespace or control character. DNS case and equivalent IPv6 literals are permitted.stringYesNot specified
  • format: uri
  • maxLength: 2048
  • pattern: ^[Hh][Tt][Tt][Pp][Ss]?://[^/?#@\u005C\u0000-\u0020\u007F-\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]+([/?#][^\u005C\u0000-\u0020\u007F-\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]*)?$
contextProviders.*.clientAssertionAudiencestringNoNot specifiedNone specified
contextProviders.*.clientAssertionKeyRefAn exact secret reference: secret:file/name, resolved under secretProviders.file.root, or secret:env/NAME, resolved only when secretProviders.environment is declared.stringYesNot specified
  • pattern: ^(?:secret:env/[A-Z][A-Z0-9_]{0,127}|secret:file/[a-z][a-z0-9._-]{0,127})$
contextProviders.*.clientIdRefAn exact secret reference: secret:file/name, resolved under secretProviders.file.root, or secret:env/NAME, resolved only when secretProviders.environment is declared.stringYesNot specified
  • pattern: ^(?:secret:env/[A-Z][A-Z0-9_]{0,127}|secret:file/[a-z][a-z0-9._-]{0,127})$
contextProviders.*.resourcestringNoNot specifiedNone specified
contextProviders.*.scopesarrayNoNot specifiedNone specified
contextProviders.*.scopes[]Array itemstringNoNot specifiedNone specified
contextProviders.*.statusProfilestringYesNot specifiedNone specified
contextProviders.*.taskGrantProfilestringNoNot specifiedNone specified
contextProviders.*.tokenEndpointAn absolute http or https URL with a host preserved as written, no user information, and no backslash, whitespace or control character. DNS case and equivalent IPv6 literals are permitted.stringYesNot specified
  • format: uri
  • maxLength: 2048
  • pattern: ^[Hh][Tt][Tt][Pp][Ss]?://[^/?#@\u005C\u0000-\u0020\u007F-\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]+([/?#][^\u005C\u0000-\u0020\u007F-\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]*)?$
databaseThe PostgreSQL connection a stateful runtime uses. Both URLs are secret references and may name the same secret.objectYesNot specifiedNone specified
database.migrationUrlRefSecret reference to the migration connection URL.stringYesNot specified
  • pattern: ^(?:secret:env/[A-Z][A-Z0-9_]{0,127}|secret:file/[a-z][a-z0-9._-]{0,127})$
database.runtimeUrlRefSecret reference to the least-privileged runtime connection URL.stringYesNot specified
  • pattern: ^(?:secret:env/[A-Z][A-Z0-9_]{0,127}|secret:file/[a-z][a-z0-9._-]{0,127})$
database.testOnlyPlaintextAllow a plaintext connection. Refused outside test builds.booleanNoNot specifiedSchema default, where declared: falseNone specified
database.trustedRootCertificateRefSecret reference to a PEM root certificate the connection trusts. Absent, the connection trusts the platform's root certificates.stringNoNot specified
  • pattern: ^(?:secret:env/[A-Z][A-Z0-9_]{0,127}|secret:file/[a-z][a-z0-9._-]{0,127})$
identityThe logical identity of the database this deployment activates packages in. It is chosen by the operator (for example casework-production) and never derived from a URL or a PostgreSQL database name. The first caseworkctl apply records it, and every later apply and every startup refuses a database whose recorded identity differs.objectYesNot specifiedNone specified
identity.databaseIdstringYesNot specified
  • maxLength: 256
  • minLength: 1
  • pattern: ^[^\u0000-\u001f\u007f-\u009f \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000](?:[^\u0000-\u001f\u007f-\u009f]*[^\u0000-\u001f\u007f-\u009f \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000])?$
kindstringYes
  • CaseworkRuntimeConfig
None specified
listenerThe listener of a runtime that declares its TLS termination and network exposure.objectYesNot specifiedNone specified
listener.bindSocket address the runtime listens on, written host:port with an IP address host ([addr]:port for IPv6).stringYesNot specified
  • maxLength: 128
  • minLength: 1
listener.networkExposureThe operator-declared private network placement of an HTTP listener.stringNo
  • container-private
  • private-address
Schema default, where declared: "private-address"
None specified
listener.tlsTerminationDeclares the trusted transport boundary for a runtime's plaintext HTTP listener. Production listeners require operator-controlled upstream TLS termination; direct plaintext is limited to the explicit loopback-only development mode.stringYes
  • development-loopback
  • operator-controlled-upstream
None specified
metricsListenerOperator-private listener for /metrics and /version. It is separate from the API listener so the counters and the active package digest never appear on the surface the public contract describes.objectNoNot specifiedNone specified
metricsListener.bindstringYesNot specifiedNone specified
packageobjectYesNot specifiedNone specified
package.expectedDigestsha256: label of the package digest, the digest of the package's SHA256SUMS file. When set, the runtime refuses to start on any other package, and on an authored project that has no SHA256SUMS. Omitted, the runtime starts on whichever package root holds.stringNoNot specified
  • pattern: ^sha256:[0-9a-f]{64}$
package.rootAbsolute path of the package directory.stringYesNot specified
  • pattern: ^/
reviewCompletionDestinationsCompletion destinations keyed by the destination id a review producer names in casework.yaml, kept as written.objectNoNot specifiedNone specified
reviewCompletionDestinations.*Map valueobjectNoNot specified
  • propertyNames.maxLength: 512
  • propertyNames.minLength: 1
  • propertyNames.pattern: ^[^\u0000-\u001F\u007F-\u009F]+$
reviewCompletionDestinations.*.attemptTimeoutMillisecondsintegerNoNot specifiedSchema default, where declared: 5000
  • format: uint64
  • maximum: 30000
  • minimum: 100
reviewCompletionDestinations.*.authHow a completion destination presents its secret. Without header the secret is sent as Authorization: Bearer; with one, the raw secret is the value of that header and no Authorization header is sent.objectConditionalNot specifiedNone specified
reviewCompletionDestinations.*.auth.headerstringNoNot specified
  • maxLength: 64
  • minLength: 1
  • pattern: ^[!#$%&'*+.^_`|~0-9A-Za-z-]+$
reviewCompletionDestinations.*.auth.secretRefAn exact secret reference: secret:file/name, resolved under secretProviders.file.root, or secret:env/NAME, resolved only when secretProviders.environment is declared.stringYesNot specified
  • pattern: ^(?:secret:env/[A-Z][A-Z0-9_]{0,127}|secret:file/[a-z][a-z0-9._-]{0,127})$
reviewCompletionDestinations.*.bearerTokenRefShorthand for auth with no header: the secret is presented as Authorization: Bearer. Exactly one of this and auth is configured.stringConditionalNot specified
  • pattern: ^(?:secret:env/[A-Z][A-Z0-9_]{0,127}|secret:file/[a-z][a-z0-9._-]{0,127})$
reviewCompletionDestinations.*.maximumAttemptsintegerNoNot specifiedSchema default, where declared: 12
  • format: uint32
  • maximum: 100
  • minimum: 1
reviewCompletionDestinations.*.retryDelaySecondsintegerNoNot specifiedSchema default, where declared: 30
  • format: uint64
  • maximum: 86400
  • minimum: 1
reviewCompletionDestinations.*.urlAn absolute http or https URL with a host preserved as written, no user information, and no backslash, whitespace or control character. DNS case and equivalent IPv6 literals are permitted.stringYesNot specified
  • format: uri
  • maxLength: 2048
  • pattern: ^[Hh][Tt][Tt][Pp][Ss]?://[^/?#@\u005C\u0000-\u0020\u007F-\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]+([/?#][^\u005C\u0000-\u0020\u007F-\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]*)?$
secretProvidersThe secret providers a runtime enables. A reference is resolved only by a provider declared here: secret:file/name under file.root, and secret:env/NAME only when environment: {} is present.objectYesNot specifiedNone specified
secretProviders.environmentEnables secret:env/NAME references, read from the process environment. Declared as an empty mapping: environment: {}.objectConditionalNot specifiedNone specified
secretProviders.fileEnables secret:file/name references, read from files under root.objectConditionalNot specifiedNone specified
secretProviders.file.rootAbsolute directory holding one file per secret. Each file must be a regular file owned by the runtime user, mode 0400 or 0600, with exactly one hard link.stringConditionalNot specified
  • pattern: ^/
taskAuthorityobjectNoNot specifiedNone specified
taskAuthority.exchangeAudiencestringYesNot specifiedNone specified
taskAuthority.issuerAn absolute http or https URL with a host preserved as written, no user information, and no backslash, whitespace or control character. DNS case and equivalent IPv6 literals are permitted.stringYesNot specified
  • format: uri
  • maxLength: 2048
  • pattern: ^[Hh][Tt][Tt][Pp][Ss]?://[^/?#@\u005C\u0000-\u0020\u007F-\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]+([/?#][^\u005C\u0000-\u0020\u007F-\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]*)?$
taskAuthority.signingKeyRefAn exact secret reference: secret:file/name, resolved under secretProviders.file.root, or secret:env/NAME, resolved only when secretProviders.environment is declared.stringYesNot specified
  • pattern: ^(?:secret:env/[A-Z][A-Z0-9_]{0,127}|secret:file/[a-z][a-z0-9._-]{0,127})$
taskAuthority.statusClientsService client IDs mapped to their one protected resource audience.objectYesNot specifiedNone specified
taskAuthority.statusClients.*Map valuestringNoNot specified
  • propertyNames.maxLength: 512
  • propertyNames.minLength: 1
  • propertyNames.pattern: ^[^\u0000-\u001F\u007F-\u009F]+$

Verify the package before it opens the listener

Section titled “Verify the package before it opens the listener”

Write the package with caseworkctl package, install it in its own directory, and set package.root to it. Record its packageDigest in your change record and set package.expectedDigest to the same value, so that replacing files under package.root cannot change what a restart loads. Never edit a package in place: apply and startup both verify every file against SHA256SUMS, and an authored directory without one is refused.

Keep earlier package directories for recovery, and plan every activation, rollbacks included.

An accepted review keeps the policy closure it was admitted under. A candidate that removes a policy, producer, profile, team, queue, provider, or context contract an open review still needs is refused with casework.activation.stranded-work, and so is a change to pinned policy content under the same policy version. There is no override. Let the affected reviews finish, have their producer cancel them, or prepare a successor that keeps what they need.

From the operator host, read the plan, apply it with the migration credential, and read the result:

Terminal window
caseworkctl plan --runtime-config /etc/registry-casework/runtime.yaml
caseworkctl apply --runtime-config /etc/registry-casework/runtime.yaml \
--operator-reference CHANGE-1234 --backup-reference nightly-before-activation
caseworkctl status --runtime-config /etc/registry-casework/runtime.yaml

plan writes nothing; read its package digest, pending schema versions, role mode, and refusals. Applying the package that is already active is refused with casework.activation.already-active. --operator-reference is stored as a keyed hash; --backup-reference is stored as text, so use a snapshot identifier without personal data.

If apply returns casework.activation.applied-unaudited, the activation committed but its audit entry did not: restore audit, read status, and record the recovery. A runtime role that cannot read the ledger gets casework.activation.ledger-unreadable; apply restores its grants. Exit codes are 0 for success, 1 for a refusal, 2 for usage, and 3 for an operational failure.

Then serve as the runtime user:

Terminal window
casework --runtime-config /etc/registry-casework/runtime.yaml serve
curl --fail http://10.42.0.7:8100/ready

Startup refuses a database whose active package is not the configured one. /ready checks the database and audit writer only; check source context and queue staffing with real identities.

The current contract refuses retired Casework schemas with casework.activation.retired-schema, a database a pre-1.0 release wrote with casework.activation.beta-schema, and a database newer than the binary with casework.activation.schema-newer. Keep such a database with the release that wrote it and provision a fresh one; for a pre-1.0 database, follow the upgrade runbook. Never delete ledger rows or run migration SQL by hand to get past either refusal.

A restore returns review state to its backup point, and the external audit journal must not be rewound. Follow Casework restore and reconciliation before reviewers resume.

The API listener serves no metrics. Open a private telemetry listener for them:

metricsListener:
bind: 127.0.0.1:9100

It serves /metrics and /version only, on a loopback or private address that is not the API listener’s. Metrics count open review requests, open tasks, and due tasks, with no subject identifiers. /version reports the binary version and the package digest the process serves, so a rollout can confirm what each process runs.

A fresh deployment has an empty directory, so no reviewer sees a task until an administrator bootstraps the first team and its served queues. Staff the Casework directory gives the requests. Each directory write names the revision it expects, and membership and queue service are live authority for old and new tasks alike.

The inbox policy in casework.yaml bounds each task-list, next-task, and holdings read. Defaults are maximumCandidateScan: 100, maximumSourceReads: 25, and pageDeadlineMilliseconds: 2000; counts go up to 10,000 and the deadline to 30,000. An ordinary due-date or subject-order query selects a bounded database page before it reads current source visibility, so it never scans the whole queue; concealed candidates still spend the scan budget. Filtering or sorting by live source reference scans the whole bounded queue, because its keys come from source reads; an exact subject or queue selector narrows it, a smaller page does not.

Within one page, several task slots for one request share one source observation. Each new page repeats the source and Directory checks, and a continuation charges its anchor observation to the same source budget, so a budget of one observation cannot advance past a current-source anchor. A BReg human context reads each chunk in one bounded HTTP batch under the original caller and source profile; every subject in it still passes its own disclosure and audit checks.

An ordinary read that exhausts its candidate, source, or time budget is refused whole with review.query-too-broad: narrow the query or reduce the page limit. A source call that reaches the deadline can instead report the source unavailable. Supervision and own-decision feeds return bounded partial pages with opaque continuations; follow the cursor with the same caller, profile, and query, and start the walk again after the cursor expires or its anchor leaves your scope. Measure representative source latency before raising budgets. A caseload preview refuses more than 100 claimed tasks; split it by queue or move tasks one at a time.

Run the operator preflight as the runtime user:

Terminal window
caseworkctl doctor --runtime-config /etc/registry-casework/runtime.yaml

Doctor checks the configuration, the package, the database activation, open reviews against the configured package, and that the audit path is writable. It accepts the package already active. It warns when the serving teams cannot supply the distinct reviewers a stage requires; add eligible members and rerun it to confirm the warning clears. It opens no listener and proves no issuer or provider access; use /ready and an authenticated task read for those.

A running process keeps the package it started with and does not recheck the activation ledger, so a predecessor left serving after apply goes on admitting work under the old package. Take it out of your proxy’s routing, stop every process serving it, then plan, apply, and start the successor at once. The successor repeats the pinned-work check before it opens its listener. If it refuses with casework.activation.stranded-work, plan and apply the predecessor again, serve it until the named reviews finish or are cancelled, and retry the change. Change an active package walks the plan and apply against a local session.

SymptomRecovery
platform.runtime-config.pathGive the absolute path of the runtime file, without . or .. components.
Startup names a different active packagePlan and apply the configured package, or point package.root at the active one.
review.policy-mismatch on admissionReconcile the producer’s policy pin with the deployed policy.
A current-source task is concealed or unavailableCheck the reviewer’s source profile, the context binding, and the provider. Never use the status credential for human display.
review.query-too-broadNarrow the queue or subject, reduce the page limit, review the inbox budgets, or split a caseload move.
precondition.failedRead the task or directory again and take a fresh preview where one is required.
service.unavailable and /ready returns 503Repair the database or the audit path, space, or permissions and restart. Retry an interrupted write with its original key.