Conversation
…lted idempotency + recipient policy Adds the in-VM Phantom email-tool upgrade for operator-subsidized Resend email per Phase 10 architect (`phantom-cloud-deploy/local/2026-05-01-phase10-resend-architect.md`). What changed: - src/config/secret-names.ts (new, 67 LOC). Canonical phantom-side mirror of phantomd's AllowedSecretNames; exports the wire-stable constant RESEND_API_KEY_SECRET_NAME = "resend_api_key" matching phantomd PR #32. - src/email/key-fetcher.ts (new, 250 LOC). Gateway-backed ResendKeyFetcher (15-min cache, 401 invalidation, structured error kinds; mirrors src/config/metadata-fetcher.ts pattern) plus EnvKeyFetcher fallback for local dev / OSS Docker. - src/email/recipient-policy.ts (new, 156 LOC). owner / unrestricted / list policy with safe parsing; "workspace" mode reserved for v1.5+ and rejected today; default is owner-only when env is unset. - src/email/metrics.ts (new, 116 LOC). phantom_email_send_total{outcome, purpose} prom-client counter with 8 outcomes (the 7 error_kind values plus ok). Owns its own private Registry (matches Slack precedent). - src/email/tool.ts (extended, +337 LOC). Replaces process.env.RESEND_API_KEY read with the key-fetcher path; adds 3-tag set (tenant_id, agent_id, purpose), tenant-salted sha256 idempotency-key derivation per architect §9.6, and the 7-kind error taxonomy per architect §6.8. Lazy SDK import preserved. Existing daily cap and from-address invariants preserved. - src/index.ts (extended). Wires the new deps; merges Slack and Email registries via setMetricsRegistryProvider's array form. - src/core/server.ts (extended). /metrics route accepts a single registry or an array; concatenates text expositions by newline. - src/channels/slack-channel-factory.ts: docstring cross-reference to the new shared mirror; the slack-only AllowedSecretNamesMirror stays for backward-compat with existing tests. - 5 test files (1334 LOC across key-fetcher, recipient-policy, metrics, tool, and secret-names). 90 new test cases covering the 7 error_kind taxonomy, tenant-salt cross-tenant collision defense, plaintext-leak guards across every error path, cache TTL eviction, recipient policy parsing, and Resend tag sanitization. - CLAUDE.md: new Email section documenting the env-var contract, the 7 error_kind values, and the cross-repo wire spec; cross-repo invariant section updated for the new mirror home. Cross-repo invariant: the secret name string `"resend_api_key"` MUST be byte-equal across phantomd PR #32 (internal/secrets/types.go), this PR's src/config/secret-names.ts and src/email/key-fetcher.ts. Drift surfaces as HTTP 404 from the metadata gateway, which the EmailTool surfaces as error_kind = "key_unavailable". Tests: bun test 2210 pass / 0 fail across 162 files; bun run lint clean; bun run typecheck clean.
Member
Author
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
SummaryI can’t perform the requested Given that limitation, I did not make any repository changes, did not create a commit, and did not open a follow-up PR. What to do next
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The in-VM Phantom EmailTool upgrade for Phase 10 (operator-subsidized Resend transactional email). Adds the metadata-gateway key-fetch path, recipient-policy gate, tenant-salted idempotency, three-tag cost-attribution, and a Prometheus counter on top of the existing 109-LOC
phantom_send_emailtool. Architect doc: Phase 10 architect (§3.4 Option B storage decision, §6 EmailTool surface, §6.8 sevenerror_kindvalues, §7 cost attribution, §9.6 tenant-salted idempotency).What changed
src/config/secret-names.ts(new, 67 LOC). Canonical phantom-side mirror of phantomd'sAllowedSecretNames; exports the wire-stable constantRESEND_API_KEY_SECRET_NAME = "resend_api_key".src/email/key-fetcher.ts(new, 250 LOC). Gateway-backedResendKeyFetcherwith a 15-minute cache, 401 cache invalidation, and structured error kinds; mirrorssrc/config/metadata-fetcher.ts. PlusEnvKeyFetcherfallback for local dev / OSS Docker without a gateway.src/email/recipient-policy.ts(new, 156 LOC).owner/unrestricted/listmodes; safe parsing;workspacemode reserved for v1.5+ and explicitly rejected today.src/email/metrics.ts(new, 116 LOC).phantom_email_send_total{outcome, purpose}prom-client counter, 8 outcomes (the 7error_kindvalues plusok), private registry per emitter.src/email/tool.ts(extended, +337 LOC). Replacesprocess.env.RESEND_API_KEYread with the key-fetcher path; adds three-tag set (tenant_id,agent_id,purpose), tenant-salted sha256 idempotency-key derivation, and the seven-kind error taxonomy. Lazy Resend SDK import preserved. Daily cap and from-address invariants preserved.src/index.tswires the new deps and merges Slack + Email registries via the array form ofsetMetricsRegistryProvider.src/core/server.ts/metricsroute accepts one registry or an array and concatenates the text expositions.src/channels/slack-channel-factory.tsdocstring cross-reference to the new shared mirror; the slack-onlyAllowedSecretNamesMirrorstays for backward compatibility with existing tests.CLAUDE.mdnew Email section documenting the env-var contract (PHANTOM_OWNER_EMAIL,PHANTOM_TENANT_ID,PHANTOM_EMAIL_RECIPIENTS_ALLOWED,PHANTOM_EMAIL_DAILY_CAP,METADATA_BASE_URL), the sevenerror_kindvalues, and the cross-repo wire spec.Total: 14 files changed, 2204 insertions(+), 108 deletions(-).
Cross-repo contract
The wire-stable secret name string
"resend_api_key"MUST be byte-equal across:internal/secrets/types.go::AllowedSecretNames).src/config/secret-names.ts::RESEND_API_KEY_SECRET_NAMEandAllowedSecretNamesMirror.src/email/key-fetcher.ts(consumes the constant).Drift surfaces as HTTP 404 from the metadata gateway (the gateway maps
ErrInvalidNameto 404 to defeat name enumeration), which the EmailTool surfaces aserror_kind = "key_unavailable". The phantom-side mirror tests (src/config/__tests__/secret-names.test.ts) plus phantomd'sTestAllowedSecretNames_KnowsResendKeypin the symmetric assertions.The tenant-salted idempotency input string is
${PHANTOM_TENANT_ID}:${normalizedTo}:${subject}:${utcDate}(architect §9.6). The salt defends Resend's TEAM-scoped idempotency-key namespace from cross-tenant collision when multiple tenants share the same operator-side Resend key.Failure-mode coverage
The seven
error_kindtaxonomy maps every tool path to exactly one outcome:recipient_denied: address violatedPHANTOM_EMAIL_RECIPIENTS_ALLOWED. No Resend POST is made.rate_limited_local: per-day soft cap hit. No Resend POST is made.key_unavailable: metadata gateway returned 404 / 5xx / network error.rate_limited_resend: Resend returned 429.validation_error: Resend returned 422 (forwards the upstream message so the agent can suggest a fix).service_down: Resend returned 5xx, the SDK threw, or noidin the response.auth_failed: Resend returned 401 (cached key was revoked) OR the gateway rejected the fetch with 401. The fetcher invalidates its cache so the next attempt refetches a rotated key.The local cap counter increments only on confirmed
okso policy denials and Resend errors do not consume the agent's daily budget. Test coverage exercises every kind end-to-end including a defensive thrown-sender path and a cross-tenant idempotency collision check.Plaintext-leak guards
Verified across the test suite:
console.log,console.warn, or any structured log line. The cache holds the value privately on the fetcher instance; logs only carry the secret NAME plus HTTP status.key_unavailable,auth_failed, andservice_downpaths. The defensive thrown-sender catch maps toservice_downwith a fixed message rather than echoing the exception (which can carry the API key in HTTP-client error formatting).validation_errorpath forwards the upstream Resend message verbatim because the agent needs the field detail to suggest a fix; the upstream sender is responsible for not embedding the key in that message (Resend's SDK does not).The
key-fetcher.test.tsincludes an explicit test that capturesconsole.logandconsole.warnacross the cache-hit path and asserts the secret value never appears.Architect doc link
phantom-cloud-deploy/local/2026-05-01-phase10-resend-architect.md(Phase 10, dated 2026-05-01). Drives the seven-kind taxonomy (§6.8), the storage decision (§3.4 Option B metadata-gateway fetch), the cache TTL (§3.4 + §6.6 fifteen minutes), the cost-attribution channels (§7), and the tenant-salt idempotency derivation (§9.6).Test plan
bun testclean: 2210 pass / 0 fail across 162 files (90 new tests across 5 files).bun run lintclean.bun run typecheckclean."resend_api_key"matches phantomd PR docs: fix MCP setup instructions and add Claude Desktop support #32'sAllowedSecretNamesentry.ok.@codex reviewafter push.PHASE10_LIVE_SMOKE=1; not run in CI (architect §11.3); operator runs at release time.