By nghxni

Technical article

Action catalog, bounded approval, and operation audit

A LightESB service declares what it can do — Actions with contracts, side effects, and exposure — and the bundled CLI derives a deterministic catalog without starting anything. At runtime an exact allowlist controls eligibility, bounded approval sessions bind sensitive work to an Action and file scope, and append-only audits retain evidence without payloads or secrets.

A catalog entry is not a permission

Each service declares its Actions in service.config.properties: ids, interaction pattern, side effects, idempotency, retry and approval semantics, exposure. The declaration travels with the service directory, so the catalog can be derived and validated without starting anything — a service without actions.ids is simply skipped in batch mode.

The crucial boundary: agentCallable=true only admits an Action into the candidate set. It grants no invocation permission — authentication, allowlist, approval, and audit remain independent layers that the execution side must enforce.

Derive offline, serve from memory

Offline validation

action validate checks one service version directory — declarations, schemas, enum values, safe placeholders — and writes canonical JSON to stdout only.

Deterministic index

action build scans the two-level app root and atomically writes the index outside any service directory. It is a deletable, rebuildable artifact — never hand-edit it or copy it back.

RUNNING-only snapshot

With lightesb.action-catalog.enabled=true, only service versions that reached RUNNING publish Actions. A failed new generation hides the Action; restoring a missing schema lets the catalog self-heal.

Digest-only credentials

lightesb.action-security stores only the SHA-256 digest of each bearer token; the raw token stays in the caller's secret store. catalog-read, action-admin, and action-execute never inherit from one another.

Two commands cover the offline loop — validate one service, then build the deterministic index:

java -jar lightesb-cli.jar action validate \
  --service-dir lightesb-camel-app/{serviceName}/{serviceVersion}

java -jar lightesb-cli.jar action build \
  --app-root lightesb-camel-app \
  --out build/action-index.json \
  --yes

The allowlist narrows, never widens

Qualifying an Action for controlled invocation takes all four switches, and every policy endpoint demands an exact action-admin bearer. A policy pins one caller + actionId + serviceVersion; the caller is derived server-side from the submitted credentialName, never self-reported by the client. add/enable re-validate that the descriptor is agent-exposed, callable, VALID, and AVAILABLE — disable still works when the catalog is down, so you can always tighten.

lightesb.action-catalog.enabled=true
lightesb.action-security.enabled=true
lightesb.action-audit.enabled=true
lightesb.action-allowlist.enabled=true

Creating a policy submits only the server-side credentialName — never a caller, token, or digest — and each change commits in the same transaction as its required audit event:

curl -H 'Authorization: Bearer <action-admin-token>' \
  -H 'Content-Type: application/json' \
  -d '{"credentialName":"agent-executor","actionId":"payment.lookup","serviceVersion":"v1"}' \
  http://localhost:8080/api/actions/policies/allowlist

Approval binds an Action to an exact change scope

A task session is not a bearer token and does not execute an Action. It records an externally approved boundary: service, version, Action, side-effect ceiling, input-policy digest, exact allowed files, TTL, and transition/execution limits. Caller, approver, status, source digests, and scope digest are derived by the server and cannot be supplied by the client.

Managed route apply requires the session ID and latest scope digest together. Any live-file drift makes the session STALE; automation must not fall back to an ordinary apply. A stale session can only be revoked to close the task, and the next change needs a fresh session and approval.

lightesb ai route apply --file build/OrderSrv-v1.0.0-candidate/OrderSrv-route.xml \
  --save-remote --service-name OrderSrv --service-version v1.0.0 \
  --route-file-name OrderSrv-route.xml \
  --resource-file common.config.properties \
  --resource-file service.config.properties \
  --action-session-id '<sessionId>' \
  --expected-scope-digest '<currentScopeDigest>' \
  --yes --output json

Append-only audit, redacted by design

With lightesb.action-audit.enabled=true, successful catalog reads append fixed security events on a best-effort basis — an audit storage failure never changes the query result. Policy changes use required audit in the same transaction, so an audit failure rolls the policy change back. The table is append-and-query only: no cleanup, update, delete, retention, or archive API.

curl -H 'Authorization: Bearer <original-token>' \
  'http://localhost:8080/api/actions/audit-events?caller=ops-reader&eventType=catalog_get&result=success&limit=50'

Events carry fixed columns — caller, actionId, digests, eventType, result — and never request/response bodies, headers, raw tokens, or free-form details:

{
  "items": [
    {
      "auditId": "75f24a91e2394f87b82040f19642ad93",
      "caller": "ops-reader",
      "actionId": "payment.lookup",
      "serviceVersion": "v1",
      "sourceDigest": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
      "eventType": "CATALOG_GET",
      "result": "SUCCESS",
      "createdAt": "2026-08-11T08:00:00Z"
    }
  ]
}

Boundaries and troubleshooting

Security enabled with an empty credential list fails closed: every Action path returns 401. Reading audit requires the exact action-admin role — 403 otherwise; a duplicate add returns 409 ACTION_ALLOWLIST_CONFLICT, an ineligible target 422 ACTION_ALLOWLIST_ACTION_INELIGIBLE. Paging is bound to filters and revision: when hasMore=true, pass nextCursor back unchanged (or --expected-revision on the CLI), and restart from page one after filters change or a hot reload produces a new revision.

Action catalog guideAllowlist APIApproval sessions