Expose authoritative app structure to off-host callers #26

Merged
kavi merged 2 commits from feat/app-inspect-endpoint into main 2026-08-10 21:30:16 +00:00
Owner

kua-app and kua-mcp-core cannot see engine-owned deploy checkouts. An app whose deploy_dir is /var/lib/kua-deploy/repos/<app>-production is readable only by the engine, so once live registry paths became authoritative every off-host caller that needed the compose file got ENOENT — including coder-core's own preflight, which made the platform unreleasable through the MCP path:

Error: ENOENT: .../coder-core-production/services/kua-services/docker-compose.yml

GET /api/v1/apps/:app/inspect (read capability) answers from the deploy host instead. It returns declared structure (compose text, services, volumes) and observed runtime structure (per-service container mounts). The mounts are the load-bearing part: they are what proves a backup directory lands on a named volume rather than the container's writable layer, which is not answerable from configuration alone.

Compose text goes through a compose-specific redactor rather than the generic redactOperationalLog. The generic one rewrites DB_PASSWORD: ${DB_PASSWORD} to [REDACTED] and so destroys exactly the vault-wiring evidence the preflight checks for. A ${VAR} is a reference; only a bare literal in that position is a secret.

Also allowlists kua-db's runtime registry routes on the structured bridge so kua-app onboard can register an app's database without a platform release. Reads need read, the upsert needs deploy — the existing rule, unchanged.

Hostile review fixes (second commit)

An independent review found five ways the new code could leak or overreach, all fixed with tests:

  • ${VAR:-fallback} was treated as a pure reference, publishing literal defaults verbatim
  • key matching missed DB_PASS, JWT_KEY, SIGNING_SALT, SENTRY_DSN
  • multi-line block scalars (PRIVATE_KEY: |) put the secret on the following lines, invisible to a line-by-line redactor
  • inline credentials were only stripped from postgres://, not mysql/redis/mongodb/amqp
  • the compose file was read whole then sliced, so an oversized file could blow the exec buffer before any limit applied — bounded at the source with head -c

Plus: compose service names are re-validated before being interpolated into the next shell command, and the structured bridge rejects ./.. path segments before matching the allowlist (the allowlist char classes permit ., so /api/v1/dbs/.. matched and was then normalised away upstream — the path requested was not the path authorised).

Verification

node --test: 25 passing, 0 failing. Verified against the real kua-ask compose: all five vault key references preserved, zero spurious redactions.

🤖 Generated with Claude Code

`kua-app` and `kua-mcp-core` cannot see engine-owned deploy checkouts. An app whose `deploy_dir` is `/var/lib/kua-deploy/repos/<app>-production` is readable only by the engine, so once live registry paths became authoritative every off-host caller that needed the compose file got ENOENT — including coder-core's own preflight, which made the platform unreleasable through the MCP path: ``` Error: ENOENT: .../coder-core-production/services/kua-services/docker-compose.yml ``` `GET /api/v1/apps/:app/inspect` (read capability) answers from the deploy host instead. It returns declared structure (compose text, services, volumes) **and** observed runtime structure (per-service container mounts). The mounts are the load-bearing part: they are what proves a backup directory lands on a named volume rather than the container's writable layer, which is not answerable from configuration alone. Compose text goes through a compose-specific redactor rather than the generic `redactOperationalLog`. The generic one rewrites `DB_PASSWORD: ${DB_PASSWORD}` to `[REDACTED]` and so destroys exactly the vault-wiring evidence the preflight checks for. A `${VAR}` is a reference; only a bare literal in that position is a secret. Also allowlists kua-db's runtime registry routes on the structured bridge so `kua-app onboard` can register an app's database without a platform release. Reads need `read`, the upsert needs `deploy` — the existing rule, unchanged. ## Hostile review fixes (second commit) An independent review found five ways the new code could leak or overreach, all fixed with tests: - `${VAR:-fallback}` was treated as a pure reference, publishing literal defaults verbatim - key matching missed `DB_PASS`, `JWT_KEY`, `SIGNING_SALT`, `SENTRY_DSN` - multi-line block scalars (`PRIVATE_KEY: |`) put the secret on the *following* lines, invisible to a line-by-line redactor - inline credentials were only stripped from `postgres://`, not mysql/redis/mongodb/amqp - the compose file was read whole then sliced, so an oversized file could blow the exec buffer before any limit applied — bounded at the source with `head -c` Plus: compose service names are re-validated before being interpolated into the next shell command, and the structured bridge rejects `.`/`..` path segments **before** matching the allowlist (the allowlist char classes permit `.`, so `/api/v1/dbs/..` matched and was then normalised away upstream — the path requested was not the path authorised). ## Verification `node --test`: 25 passing, 0 failing. Verified against the real kua-ask compose: all five vault key references preserved, zero spurious redactions. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
kua-app and kua-mcp-core cannot see engine-owned deploy checkouts. An app whose
deploy_dir is /var/lib/kua-deploy/repos/<app>-production is readable only by
this process, so once live registry paths became authoritative every off-host
caller that needed the compose file got ENOENT. That included coder-core's own
preflight, which made the platform unreleasable through the MCP path:

  Error: ENOENT: .../coder-core-production/services/kua-services/docker-compose.yml

GET /api/v1/apps/:app/inspect answers from the deploy host instead. It returns
declared structure (compose text, services, volumes) AND observed runtime
structure (per-service container mounts). The mounts are the load-bearing part:
they are what proves a backup directory lands on a named volume rather than the
container's writable layer, which is not answerable from configuration alone.

Compose text goes through a compose-specific redactor rather than the generic
redactOperationalLog. The generic one rewrites `DB_PASSWORD: ${DB_PASSWORD}` to
[REDACTED] and so destroys exactly the vault-wiring evidence the preflight
checks for. A ${VAR} is a reference; only a bare literal in that position is a
secret, and only those are stripped.

Also allowlists kua-db's runtime registry routes on the structured bridge so
`kua-app onboard` can register an app's database without a platform release.
Reads need 'read', the upsert needs 'deploy' — the existing rule, unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An independent review of the inspect endpoint found five real ways the new code
could leak or overreach. All are fixed here with tests:

- `${VAR:-fallback}` was treated as a pure reference, so a literal default —
  exactly where a forgotten development secret sits — was published verbatim.
  Only `${VAR}` and `${VAR:-}` are references now.
- Key matching missed short and unconventional names (DB_PASS, JWT_KEY,
  SIGNING_SALT, SENTRY_DSN). KEY/PASS are matched as substrings now. Over-
  redaction is free here because the key NAME is always preserved, and that is
  what the preflight matches on; missing a key is not free.
- Multi-line block scalars (`PRIVATE_KEY: |`) put the secret on the FOLLOWING
  lines, so a line-by-line redactor never saw it. The whole indented block is
  dropped now.
- Inline credentials were only stripped from postgres:// URLs; mysql, redis,
  mongodb and amqp DSNs went through untouched. Any scheme now.
- The compose file was read whole and sliced afterwards, so an oversized file
  could blow the exec buffer before any limit applied. Bounded at the source
  with `head -c`.

A URL under a secret-ish key is credential- and query-stripped rather than
blanket-redacted, so `${VAR}` references inside it survive — blanket redaction
would erase the vault-wiring evidence the preflight depends on.

Two smaller ones: compose service names are re-validated before being
interpolated into the next shell command rather than trusted because they came
back from the previous one, and the structured bridge rejects '.'/'..' path
segments BEFORE matching the allowlist. The allowlist character classes permit
'.', so `/api/v1/dbs/..` matched and was then normalised away by the upstream
fetch URL — the path finally requested was not the path that was authorised.

Verified against the real kua-ask compose: all five vault key references
preserved, zero spurious redactions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kavi merged commit 7a8dfc8136 into main 2026-08-10 21:30:16 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
kavi/kua-deploy!26
No description provided.