Expose authoritative app structure to off-host callers #26
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/app-inspect-endpoint"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
kua-appandkua-mcp-corecannot see engine-owned deploy checkouts. An app whosedeploy_diris/var/lib/kua-deploy/repos/<app>-productionis 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: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 rewritesDB_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 onboardcan register an app's database without a platform release. Reads needread, the upsert needsdeploy— 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 verbatimDB_PASS,JWT_KEY,SIGNING_SALT,SENTRY_DSNPRIVATE_KEY: |) put the secret on the following lines, invisible to a line-by-line redactorpostgres://, not mysql/redis/mongodb/amqphead -cPlus: 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 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>