Skip to content

fix: remove code defaults from inter-service tokens and session secrets - #1441

Open
alfredolopez80 wants to merge 19 commits into
devfrom
fix/env-audit-secret-defaults
Open

alfredolopez80 wants to merge 19 commits into
devfrom
fix/env-audit-secret-defaults

Conversation

@alfredolopez80

@alfredolopez80 alfredolopez80 commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Secrets no longer have code defaults, and the base manifests stop resetting the webhook-proxy token.

Required secrets (no default in any NODE_ENV, trimmed, replace-with-* refused)

Service Variable
webhook-proxy WEBHOOK_PROXY_CONTROL_API_SERVICE_TOKEN
rpc-proxy RPC_PROXY_CONTROL_API_SERVICE_TOKEN, RPC_PROXY_DESKTOP_COOKIE_SECRET, RPC_PROXY_SANDBOX_UI_COOKIE_SECRET, RPC_PROXY_DESKTOP_API_TOKEN
host-context-controller CONTEXT_MAPPER_DESKTOP_API_TOKEN (outside CLERUM_DEV_MODE)
external-rest-api EXTERNAL_REST_API_CONTROL_API_SERVICE_TOKEN
  • webhook-proxy-secrets ships as an empty canary (stringData: {}), like the other token Secrets. The
    base manifest declared the token's replace-with-* value in stringData, so every kubectl apply
    (client- or server-side) reset the token that apply-inter-service-tokens.sh patched. Verified on a
    disposable cluster: the old manifest resets a patched token, and the new one keeps it.
  • Cookie secrets (desktop and sandbox-UI session HMAC keys) need at least 32 bytes.
    gen-jwt-keys.sh writes 64.
  • Desktop API token, both sides. host-context-controller skipped the desktop-status token check
    whenever its token was empty, and base never wired the token. Base HCC now reads
    inter-service-tokens/desktop-api-token. HCC refuses to start without the token outside
    CLERUM_DEV_MODE. A denied request logs one line with a reason (token_not_configured,
    missing_authorization, token_mismatch), never the value. The minikube overlay keeps its literal
    token (valueFrom: null), so the minikube renders change only in webhook-proxy-secrets.
  • control-api no longer reads CONTROL_API_INTERNAL_TOKENS (the field had no reader). It adds a test
    for the unset service-token map in production.
  • Error hints. For a value gen-jwt-keys.sh writes, the hint says to run that script only on a new
    install. On an existing install it rotates every key, so the hint points to the upgrade note's
    non-rotating repairs instead.

Public-boundary scanner (scripts/tests/test-minikube-t2-public-boundary.sh)

  • The .env* file-name gate applies at any depth. The env templates .gitignore tracks are exempt
    (.env.example, .env.test, .env.e2e.example, .env.dev.example, .env.quickstart.example), and
    their contents are still scanned.
  • New env-style patterns catch prefixed names (…_PASSWORD=, …_SECRET=, …_TOKEN(S)=, …_PASS(WD)=,
    …_ENCRYPTION_KEY=, …_SIGNING_KEY=).
    • Two deliberate exemptions: dev-…-token values, and env-style lines in test paths (test/, tests/,
      __tests__/, and JS/TS *.test.*, *.spec.*, *.integration.test.*). Env files are never exempt.
    • The old patterns are unchanged.
    • Ten existing lines (doc placeholders and similar) are flagged only if a later PR edits them. Over the
      dev history, 7 of 371 commits would have needed a reword.

Docs. The upgrade note (docs/deploy/releases-and-upgrades.md, "Required secrets without code
defaults") covers:

  • a read-only pre-upgrade check and three non-rotating repairs for the desktop token;
  • when to run the token script;
  • a one-time re-login for a cookie secret that ends in a newline.

It also updates production.md, minikube.md, the READMEs, rpc-proxy/.env.example and the
.env.dev.example header. The .env.dev.example IMAP keys keep their names: the infra target
make gcp-dev-apply-secrets reads them.

Deploy precondition

Deploy the base manifests of this PR no later than its webhook-proxy image. Pairing a new webhook-proxy
image with base manifests from before this PR puts the replace-with-* value back into
webhook-proxy-secrets on every apply, and the new image refuses to start with it. Run
apply-inter-service-tokens.sh for this release, before or after the first apply.

Out of scope (follow-ups)

  • Moving host-context-controller's direct console.* calls to its structured logger: separate issue.
    It affects stack traces, LOG_LEVEL filtering, key redaction and the documented [HCC] FATAL text.
  • apply-inter-service-tokens.sh still writes CONTROL_API_INTERNAL_TOKENS, which nothing reads.
  • The customer kit docs in the infra repository describe the old placeholder re-apply.

Verification

Package and contract checks ran on f3e2e0b82, the last commit with code changes. The later commits
merge dev and do not touch the files this PR changes. The tests/e2e version conflict was resolved
to the higher version. T0 and T2 ran on head 18813a699.

Check Result
Package suites (build, tsc --noEmit, tests) webhook-proxy 30, rpc-proxy 1226, host-context-controller 2934, external-rest-api 567 (pinned suites), control-api 8838 (pinned suites and cases)
Security E2E rpc-proxy 25, external-rest-api 10
T2 contract suite, token and codex contract tests pass under bash 3.2 and bash 5
Public boundary on this branch PUBLIC_BOUNDARY_PASS
kubectl kustomize renders (minikube ×4, base) only webhook-proxy-secrets differs; one HCC token entry
Disposable-cluster check of the token repairs and of apply with stringData (client- and server-side) all expected
Mutations every new test goes red when its fix is removed
T0 (18813a699) pass
T2 (make minikube-t2, fresh branch profile, 18813a699) MINIKUBE_T2_PASS: full bootstrap, pre-gate-sync, T1 Real PostgreSQL 1064/1064 tests in 132/132 files, NP-08 Host-to-HCC authorization pass, port-forwards pass

🤖 Generated with Claude Code

alfredolopez80 and others added 6 commits November 11, 2026 20:58
webhook-proxy read WEBHOOK_PROXY_CONTROL_API_SERVICE_TOKEN with
`|| 'dev-webhook-proxy-token'`. Its image sets no NODE_ENV, so a cluster
with the variable unset authenticated to control-api with that public
string, and the base Secret's `replace-with-*` placeholder was sent as the
bearer token, failing every registry lookup at request time instead of at
startup.

loadConfig() now requires a non-empty token and rejects `replace-with-*`
placeholders, naming deploy/scripts/apply-inter-service-tokens.sh. Local
copies of requiredNonEmpty (codex-llm-proxy) and assertNotPlaceholder
(rpc-proxy) follow the repository convention of no shared config package.

Minikube is unaffected: kustomize applies the placeholder, then
apply-inter-service-tokens.sh patches the Secret and restarts
webhook-proxy; no readiness wait targets it before that patch.

Tests: test/config.test.ts covers unset, empty, whitespace-only and
placeholder values, with the configured-value case as the witness.
Mutations (restore the default, drop the placeholder check, constant
token) each turn it red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… unset token map

config.internalTokens parsed CONTROL_API_INTERNAL_TOKENS with a default of
`dev-external-rest-api-token,dev-rpc-proxy-token`. Nothing reads the field,
and the production `^dev-` guard covers only internalServiceTokens, so it
was the last dev token default outside that guard. Remove the field and
parseInternalTokens. Its only effect was a placeholder check on a list that
apply-inter-service-tokens.sh fills with the same tokens it writes to the
validated service-token map. The script keeps writing the key; the
manifest comments now say control-api no longer reads it.

Orphans checked: internalTokens, parseInternalTokens,
CONTROL_API_INTERNAL_TOKENS. Removed: the field, its parser and the
"read by control-api" comments. Remaining references are the script
(writer) and the shell contract tests that assert it.

config.prod.test.ts adds the case the built-in default map creates: with
CONTROL_API_INTERNAL_SERVICE_TOKENS unset in production, import is rejected
as a default dev internal service token. A new witness case shows the
production fixture alone imports. Disabling the dev-token loop turns the
new case red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…secrets in every NODE_ENV

rpc-proxy read its control-api service token and both cookie signing
secrets, and external-rest-api its service token, through
requiredOrDevDefault: outside NODE_ENV=production each fell back to a
public `dev-*` string. The shipped images bake NODE_ENV=production, but a
single env entry overrides that, and the cookie secrets have no second
control: rpc-proxy signs and verifies those sessions itself, so a known
default lets anyone forge a Desktop or sandbox UI session.

These four reads now use the existing required() plus the placeholder
check, with no default in any NODE_ENV. The placeholder message names the
script that writes each value (apply-inter-service-tokens.sh for tokens,
gen-jwt-keys.sh for cookie secrets). Non-secret requiredOrDevDefault reads
(URLs, issuer, audience, namespace, CORS) are unchanged. Deployed images
already required these values, so clusters do not change.

Test harness: both testJwtKey.setup.ts files supply random values; the
rpc-proxy E2E spawn passes both cookie secrets; the two control-api suites
that import external-rest-api config in-process stub its token.

Tests: rpc-proxy config.test.ts and external-rest-api config.secrets.test.ts
cover unset and placeholder values on the non-production path, with a
read-back witness. Mutations (restore each dev default, drop each cookie
placeholder check, constant secret, E2E spawn without cookie secrets)
each turn them red.

Docs: READMEs list the required secrets and the local exports;
docs/deploy/minikube.md says where minikube's cookie secrets come from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
List the five secrets that now stop startup when unset, empty or a
`replace-with-*` placeholder, with the script that writes each in a
cluster. webhook-proxy is the cluster-visible change: apply the manifests,
then run apply-inter-service-tokens.sh. rpc-proxy and external-rest-api
images already required these values. control-api no longer reads
CONTROL_API_INTERNAL_TOKENS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nv.dev.example consumer

rpc-proxy/.env.example is the list a developer exports before `npm run dev`.
Add RPC_PROXY_DESKTOP_COOKIE_SECRET and RPC_PROXY_SANDBOX_UI_COOKIE_SECRET,
which rpc-proxy now requires with no code default, as `replace-with-*`
placeholders that startup rejects until replaced. test/envExample.test.ts
asserts every required secret is listed; a parser that finds no keys turns
its witness red.

.env.dev.example: the IMAP keys were reported as mismatched with
channel-reader's CLERUM_EMAIL_USERNAME/PASSWORD. They are correct: the file
feeds `make gcp-dev-apply-secrets` in keyper-labs/evenfire-infra, which
writes CLERUM_EMAIL_IMAP_USER/PASS to the email-username/email-password
Secret keys channel-reader reads in a cluster. The header now names that
consumer and explains why the names stay, so they are not renamed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…undary

.gitignore ignores `.env` and `.env.*` but tracks five templates on
purpose (`!.env.example`, `!.env.test`, `!.env.e2e.example`,
`!.env.dev.example`, `!.env.quickstart.example`). The public-boundary
scanner exempted only names ending in `.env.example` or `.env.test`, so
any change to `.env.dev.example`, `.env.quickstart.example` or
`desktop-app/.env.e2e.example` failed T0 and T2 as a "sensitive file name"
although the file is public by design.

The scanner now also exempts those exact template file names, mirroring
.gitignore. Every other `.env.*` name is still rejected, and template
contents are still scanned for credentials.

test-minikube-t2-contract.sh covers it: the three templates pass;
`.env.dev`, `.env.dev.example.bak` and `.env.staging.example` are still
rejected; a credential inside `.env.dev.example` is still reported.
Removing the exemption, or widening it to every `.env*` name, turns the
new cases red. The full contract suite passes under bash 3.2 and bash 5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alfredolopez80
alfredolopez80 marked this pull request as ready for review November 11, 2026 21:07
alfredolopez80 and others added 11 commits November 12, 2026 10:03
The base webhook-proxy-secrets Secret declared
WEBHOOK_PROXY_CONTROL_API_SERVICE_TOKEN with a replace-with-* value in
stringData. stringData is write-only, so a client-side kubectl apply sent
the key again on every apply and reset the token that
apply-inter-service-tokens.sh had written with kubectl patch. Since this
PR webhook-proxy refuses that value at startup, so every overlay apply not
followed by the script left webhook-proxy crash-looping, and every run of
the script minted a new token and restarted all consumers.

The Secret now ships `stringData: {}`, like the canary Secrets in
deploy/base/control-plane/secrets-canary.yaml. The token key is outside the
apply envelope, so applies keep it. Until the script has run, webhook-proxy
exits with "WEBHOOK_PROXY_CONTROL_API_SERVICE_TOKEN must be set".

Texts corrected to match the code: the script's token and env-var lists
(webhook-proxy, codex-llm-proxy and grok-llm-proxy tokens), the canary
comments that required the script to run before the apply and cited a
workflow file that is not in this repository, the webhook-proxy config
comment and README, and the title of the placeholder test.

Test: scripts/tests/test-inter-service-tokens.sh checks the manifest (the
Secret ships `stringData: {}`, declares no token key, and no base Secret
holds a replace-with- value) and two script cases through new stub
branches: a real token in the Secret is kept, and a placeholder is replaced
by a generated token on both the webhook-proxy and control-api sides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndary

The public-boundary scanner tested the whole lower-cased path against
`.env` and `.env.*`, so the gate fired only at the repository root. A
nested `rpc-proxy/.env.local` or `deploy/.env.dev` passed, although
.gitignore ignores such names at any depth and the scanner's own comment
claimed every other `.env.*` name was rejected. The accept case for
`desktop-app/.env.e2e.example` could not fail for the same reason.

The gate now calls `is_env_file_name` on the last path component. The
template exemption, which already worked on the file name, is unchanged,
so the tracked templates at any depth still pass. A deletion of a
committed nested env file is reported by name, as a root deletion is.

Contract suite: nested reject cases (`rpc-proxy/.env.local`,
`rpc-proxy/.env.prod`, `deploy/.env.dev`, `a/b/.env`), a nested
`rpc-proxy/.env.example` accept case, and a nested deletion case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ic boundary

Both credential patterns of the public-boundary scanner start with `\b`
before the key word, and `\b` does not match between `_` and a letter. So
`GCP_DB_PASSWORD=...`, `..._COOKIE_SECRET=...` and `..._SERVICE_TOKEN=...`
were never matched, although this PR lifted the name gate for three more
env templates on the argument that their contents are scanned.

Widening the two existing patterns would scan code rather than env files
(measured: 41 findings in 11 of the last 80 merges of dev, mostly
JavaScript assignments and object keys). The existing patterns stay
byte-identical. Two new patterns cover env-style assignments of a
SCREAMING_SNAKE name ending in API_KEY, PASS, PASSWD, PASSWORD, SECRET,
TOKEN, TOKENS, PRIVATE_KEY, ENCRYPTION_KEY or SIGNING_KEY: a line that is
an assignment (`export` and spaces around `=` allowed), or an inline
`NAME=value` that does not follow `.`. The value needs 8 or more
characters and no `<` or `>`; the unquoted form excludes brackets and
backticks and must end at whitespace, a quote or the end of the line.

Exemptions of the new patterns only: a `replace-with-*` value, a value
with no letter or digit, a control-api public development token
`dev-...-token`, and a line in a source fixture path (`is_source_fixture`:
a `test`, `tests` or `__tests__` directory at any depth, or a JS/TS
`.test`, `.spec` or `.integration.test` file) unless the file is an env
file. Not covered: a key word in the middle of the name, the YAML form,
an unquoted value containing a bracket, and values under 8 characters.

Measured on the last 80 first-parent merges of dev: 2 merges with 3
findings, all documentation placeholders in mcp-host/README.md. On the
tracked tree: 10 more existing lines would be flagged if a later change
edits them. The PR's own diff passes.

Contract suite: 15 reject cases (prefixed names, punctuation, quoted
values with spaces, spaces around `=`, nested and test-directory env
templates, a non-JS `.test.py` file, and the PASS, PASSWD, TOKENS,
ENCRYPTION_KEY and SIGNING_KEY names) and 16 accept cases (placeholders,
empty values, `_FILE`, `_NAME`, `_TTL_MS`, `_HASH`, `_KEY_ID`,
`_TOKENS_FILE` and `_PASS_THROUGH` names, development tokens, variables,
angle-bracket placeholders, Python code and a shell test fixture).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ookie secrets

The secret readers of rpc-proxy and external-rest-api built on
`required()`, which refuses only an unset or empty variable. So a
whitespace-only secret, a ` replace-with-*` placeholder with a leading
space, and a 1-byte cookie HMAC key all loaded. No length check existed
for the two cookie secrets, although the minikube ConfigMap claimed one.

The fix is in the secret readers, not in `required()`, which also serves
`requiredOrDevDefault` and the PEM key resolvers:

- rpc-proxy `requiredSecret` trims the value, treats an empty result as
  unset, checks the placeholder on the trimmed value, and returns the
  trimmed value, as webhook-proxy does. It takes a minimum length in
  bytes; RPC_PROXY_DESKTOP_COOKIE_SECRET and
  RPC_PROXY_SANDBOX_UI_COOKIE_SECRET require 32 bytes (the HS256 key
  size). The service token has no minimum. Messages never echo the value
  or its length.
- external-rest-api trims its control-api service token the same way.

A cookie secret with a trailing newline in a hand-made Secret is now
trimmed, so its HMAC key changes once and existing session cookies stop
verifying. gen-jwt-keys.sh and the minikube overlay write values without
surrounding whitespace.

The rpc-proxy security E2E spawned the service with 25- and 28-byte
cookie secrets; they are now 38 and 41 bytes. The minikube ConfigMap
comment and the two READMEs state the minimum and the trim.

Tests: empty, whitespace-only, leading-space placeholder and trimmed-read
cases for every secret; for the cookie secrets a 31-byte refusal that
does not echo the value, a 32-byte accept, and a 16-character, 32-byte
UTF-8 accept; a 9-byte service token accept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wire it in base

host-context-controller skipped the service-token check on
GET /api/v1/desktop/<hostRef> whenever CONTEXT_MAPPER_DESKTOP_API_TOKEN
was empty, using "the token is empty" as a stand-in for dev mode, and base
never gave HCC that variable. So every cluster built from deploy/base ran
the endpoint without authentication, although gen-jwt-keys.sh writes the
token to both inter-service-tokens/desktop-api-token and rpc-proxy-secrets.

host-context-controller:
- The check is skipped only in CLERUM_DEV_MODE with no token configured.
  Outside dev mode an empty token denies every request, and main.ts
  refuses to start (`[HCC] FATAL`, exit 1) when the token is unset, empty,
  whitespace-only or a replace-with-* placeholder. The check runs after
  the HMAC secret check, so the HMAC error is reported when both are
  missing. The value is trimmed, as rpc-proxy trims its copy.
- A denial logs one structured line through the service logger
  (`desktop status request denied`, reason token_not_configured,
  missing_authorization or token_mismatch); it never carries the token,
  the header or a length.

rpc-proxy: RPC_PROXY_DESKTOP_API_TOKEN goes through requiredSecret
(trimmed, no minimum length) and the Authorization header is always sent.

Manifests: base HCC reads CONTEXT_MAPPER_DESKTOP_API_TOKEN from Secret
inter-service-tokens, key desktop-api-token. The minikube patch keeps its
literal, equal to the rpc-proxy ConfigMap value, and adds
`valueFrom: null` so the inherited source is removed.

Tests: a 13-row table for the desktop endpoint (dev mode x token x
header, plus a dev-mode placeholder row) with the log line, its exact
fields and a liveness witness; the LOG_LEVEL=warn case; a spawn test of
main.ts for the start-up refusal and its order; the token assertion and
the config trim; rpc-proxy secret cases, the header on both HCC calls and
the .env.example list; a manifest contract in
test-inter-service-tokens.sh for the base wiring, the writer and the
minikube literals. The security E2E and the dev JWT verifier contract set
the new variable.

orphans checked: rpc-proxy main.ts production warning, HCC main.ts
warning — removed; config.desktopApiToken readers kept (desktopProxy.ts
x2, server.ts, main.ts).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
releases-and-upgrades.md:
- The note now lists seven secrets, adds the rpc-proxy and
  host-context-controller desktop API token rows, and says that
  whitespace-only values are refused and surrounding whitespace is trimmed.
- webhook-proxy: an unset variable fell back to dev-webhook-proxy-token
  only outside Kubernetes; in a cluster the pod received the base
  Secret's replace-with-* value. Base now ships the Secret empty, so applies
  keep the token. The first apply of this release leaves the previous value,
  so apply-inter-service-tokens.sh runs once after it.
- rpc-proxy: deployed pods did change. They now refuse placeholder and
  whitespace-only secrets, cookie secrets under 32 bytes and a missing
  desktop API token. A trimmed cookie secret changes its HMAC key once,
  so users sign in again.
- Desktop API token: a read-only pre-upgrade check, and three repairs that
  do not rotate other keys (copy from rpc-proxy, copy to rpc-proxy, write
  one new value to both). gen-jwt-keys.sh rotates every key, so it is not
  the repair.

production.md: operators delete the three secret keys from their copy of
the minikube rpc-proxy-config instead of replacing them, because
gen-jwt-keys.sh writes all three to rpc-proxy-secrets and rpc-proxy reads
the Secret after the ConfigMap; cookie secrets need 32 bytes. Step 3 says
gen-jwt-keys.sh writes the desktop API token to both Secrets, both services
require it, and the repair commands are in the upgrade note.

minikube.md: the ConfigMap also carries the desktop API token, and HCC's
copy is the literal in patches/dynamic-images.yaml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* origin/dev: (27 commits)
  test(desktop-app): pin the order of a removal and a replace on one chat
  test(desktop-app): name catalog isolation tests by behaviour
  docs(desktop-app): state what a failed write of a kept turn leaves
  test(desktop-app): name rejected-send tests by behaviour
  test(desktop-app): fix rejected-send test titles and a witness
  docs(desktop-app): describe what each send failure reason and flag records
  docs(desktop-app): state what the removal rule and a withdrawal cover
  fix(desktop-app): release a send's activity entry on every exit
  fix(desktop-app): keep a held send that was posted and failed without a rejection
  refactor(desktop-app): share the durable write of a kept turn
  fix(desktop-app): show the failure sentence for a send kept behind Recover input
  test(desktop-app): cover the tombstone and sender checks of chat:removeMessages
  test(desktop-app): cover the unprotected guards of the rejected-send path
  test(desktop-app): cover the attachment guards of a rejected send
  refactor(desktop-app): simplify the rejected-send path
  docs(desktop-app): describe connector rows for an agent the catalog does not list
  test(desktop-app): state why the second catalog differs
  fix(desktop-app): return a rejected send to the composer
  feat(desktop-app): remove unanswered local turns from the chat store
  test(desktop-app): cover a held agent restored from a workspace snapshot
  ...
…h for one missing secret

The start-up errors for a missing or placeholder cookie secret, desktop
API token or HCC desktop token said to run deploy/scripts/gen-jwt-keys.sh.
On an existing install that script rotates every key and resets
control-postgres, which the upgrade note tells operators not to do.
verify-secrets.sh listed only the regenerate-everything targets, and it
now runs against base HCC through the new secretKeyRef.

- rpc-proxy: the hint for values gen-jwt-keys.sh writes says to run it
  only on a new install and points an existing install to "Required
  secrets without code defaults" in docs/deploy/releases-and-upgrades.md.
  Tokens written by apply-inter-service-tokens.sh keep their hint, because
  that script keeps existing values.
- host-context-controller: the desktop token hint says the same.
- verify-secrets.sh: one missing key on a running install goes to the
  upgrade note's non-rotating repair; the gen-keys targets are marked as
  regenerating every key.
- Upgrade note: seven secrets "become required" (five had a development
  value, the two desktop tokens an empty default), and the token script
  may run before or after the first apply; before leaves no window in
  which webhook-proxy stops (verified on a disposable cluster).

Tests: rpc-proxy config.test.ts (3 placeholder rows and 2 short-cookie
rows) and HCC desktopApiToken.test.ts (2 rows) assert the warning and the
note's section name. Dropping the warning in either service turns them
red. rpc-proxy 1226 and HCC 2934 tests pass, and tsc --noEmit is clean in
both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* origin/dev:
  test(mcp-host): bound the retention sweep test on worker CPU time, not wall time
  test(mcp-host): restore the alias guard under the #1433 wrap; correct the 401 test text
  test(mcp-host): adapt the #1433 tests to the OpenAI family composition on dev
  docs(mcp-host): correct comments on the text repair and the 400 classification
  fix(mcp-host): report Anthropic's own message for a rejected HTTP 400
  test(mcp-host): cover shared sub-objects, every request method and the new error literal
  test(mcp-host): cover the repair and the 400 classification on every provider arm
  fix(mcp-host): classify Vertex and Bedrock HTTP 400s as rejected requests
  fix(mcp-host): repair request text without recursion and refuse colliding keys
  fix(mcp-host): classify an OpenAI-family or Anthropic API-key HTTP 400 as a rejected request
  fix(llm-proxy): report an upstream 400 as upstream_rejected
  fix(mcp-host): repair lone UTF-16 surrogates before LLM dispatch
…efaults

Conflicts: tests/e2e/package.json and package-lock.json differed only in
the package version (0.1.109 here, 0.1.107 on dev); kept the higher one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Mircala

Mircala commented Nov 12, 2026

Copy link
Copy Markdown

Evenfire Reviewer — Round 1

Reviewed head: 18813a699dbbbe07ea2735fcd93888bea3f2552a

Decision: Approve with follow-ups

The secret defaults are gone and the wiring matches the description. webhook-proxy refuses an unset or replace-with-* token, and the base Secret no longer rewrites that key on apply. rpc-proxy and external-rest-api trim their secrets and refuse placeholders; cookie secrets need 32 bytes. host-context-controller now requires the desktop token outside dev mode, and the minikube render keeps one literal on both sides with the inherited secretKeyRef removed. I do not have a merge-blocking defect.

Must fix

No must-fix items found.

Verified

  • kustomize build of deploy/base and deploy/overlays/minikube (kustomize 5.4.3): one HCC CONTEXT_MAPPER_DESKTOP_API_TOKEN, from the Secret in base and the shared literal in minikube; webhook-proxy-secrets is stringData: {}.
  • scripts/tests/test-inter-service-tokens.sh passes on this head.
  • The production control-api test that unsets CONTROL_API_INTERNAL_SERVICE_TOKENS hits the real dev-* guard. Removing internalTokens removes a field with no reader.
  • CI on this head (build/test/typecheck and CodeQL) is green.

Follow-ups

  • R1-M1 — Medium — host-context-controller/src/server.ts:407 — the desktop token is compared with ===. Use timingSafeEqual on equal-length buffers, as control-api does for its service tokens. The route is cluster-internal, and the old code compared the same way, so this does not block the merge.
  • R1-L1 — Low — scripts/tests/test-minikube-t2-contract.sh — the new env-style cases write a new file and diff it unstaged, so the scanner never sees a + line. The accept cases cannot fail. Stage the fixture (or diff the index) and confirm one reject case goes red when the two new patterns are removed.

Validation gaps

  • Package suites were not re-run in this review. CI on the reviewed head is green; the counts in the PR body are the author's, taken at f3e2e0b82.
  • Previous-head regression proof: not verified in this environment.

PR description

Accurate. The deploy precondition is real: a new webhook-proxy image on the old base manifest gets the placeholder written back and then refuses to start.

Pre-existing

Pre-existing: 2 Low — non-blocking for this PR.

Next action

Merge is reasonable. Land R1-M1 before or immediately after, and fix the contract-suite diff in R1-L1 so the new scanner patterns are actually tested.

Full review archive: pr-reviews/pr-1441/r1-26-11-12.md

— Review performed by Evenfire Reviewer

alfredolopez80 and others added 2 commits November 12, 2026 20:55
Review finding R1-M1 on PR #1441 (head 18813a6): checkDesktopAuth
compared the Authorization header with `===`, which stops at the first
differing character. The comparison predates this PR (`!==` on dev).

The whole header is now compared with crypto.timingSafeEqual through a
module-private safeEqual. Headers of another length return false before
the comparison, since timingSafeEqual throws on unequal lengths and the
token length is public (gen-jwt-keys.sh writes 64 hex characters).
Buffers are UTF-16LE so two different strings never encode to equal
bytes; with UTF-8, distinct unpaired surrogates would compare equal.
The dev-mode skip, the three denial reasons, the log line and the 401
body are unchanged.

Tests: 76 cases in server.test.ts. 37 header rows (dev mode on/off,
scheme and whitespace variants, lengths, empty and missing header, no
token, unpaired surrogate) give the same verdict on 18813a6 and on
this commit. The same rows assert that timingSafeEqual is called exactly
when the lengths match and that its result is the verdict; two forced
results show the decision follows it. On 18813a6 those 39 fail.
Mutations: revert to `===` 39 red, no length guard 67 red, auth check
disabled 90 red. HCC 3010 tests pass and tsc --noEmit is clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… token

Review finding L2-2 on b29c555: the comment on the `compared` field
said the header is compared whenever it has the expected length. The two
no-token rows have that length and are not compared, because
checkDesktopAuth checks `token &&` first. Comment only; no test changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants