Skip to content

Add secure codemode secret_set capability - #96

Merged
kentcdodds merged 5 commits into
mainfrom
cursor/codemode-skill-oauth-tokens-7a5f
Mar 30, 2026
Merged

kentcdodds merged 5 commits into
mainfrom
cursor/codemode-skill-oauth-tokens-7a5f

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Mar 29, 2026 •

Copy link
Copy Markdown
Member

Summary

  • rebase the secret_set work onto current main and keep it aligned with upstream codemode helper/runtime changes
  • add secret_set for persisting refreshed OAuth tokens and other in-execution secret values with metadata-only output
  • keep x-kody-secret inputs write-only by redacting accidental echoes from execute results, logs, and preserved error causes
  • address valid PR feedback by exporting explicit helper signatures, simplifying the node test registry typing, and preserving redacted Error.cause
  • normalize generated MCP CSS in build:mcp-apps so the full validation gate can pass consistently

Testing

  • npm run validate
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Added a write-only secret persistence capability: secrets can be saved from trusted execution and only metadata is returned; plaintext never appears in results or logs.
  • Documentation

    • Strengthened secret-handling guidance: write-only semantics for secret-marked inputs, automatic plaintext redaction in execute results/logs, and guidance to collect/rotate secrets via generated UI (not chat).
  • Tests

    • Added unit and end-to-end tests covering secret writes, redaction, cyclic-structure handling, and scoped visibility.
  • Chores

    • Made secret-input helpers publicly available and applied minor formatting, typing, and build-script refinements.

@coderabbitai

coderabbitai Bot commented Mar 29, 2026 •

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new secret_set MCP capability, marks x-kody-secret capability inputs as write-only and tracks/redacts their plaintext during execution, exports schema helper predicates for secret inputs, updates docs, and adds tests verifying secret persistence, tracking, and redaction.

Changes

Cohort / File(s) Summary
Documentation
docs/agents/adding-capabilities.md, docs/agents/secret-host-approval.md
Clarified that inputs marked x-kody-secret are write-only and must not be returned or logged; updated guidance for capability authors and handlers to avoid echoing secrets and to use generated UI for user-provided secrets.
Secrets Domain & Capabilities
packages/worker/src/mcp/capabilities/secrets/domain.ts, packages/worker/src/mcp/capabilities/secrets/secret-set.ts, packages/worker/src/mcp/capabilities/secrets/secret-list.ts, packages/worker/src/mcp/capabilities/secrets/secret-delete.ts
Added secret_set capability (schema, handler, metadata) and included it in the secrets domain; updated capability descriptions to state plaintext is never returned and to discourage pasting secrets into chat.
Execution & Provider Wiring
packages/worker/src/mcp/run-codemode-registry.ts, packages/worker/src/mcp/index.ts, packages/worker/src/mcp/tools/execute.ts
Plumbed trackSecretInputValue option through builders; collect secret-marked inputs at resolution time; track secret strings during execution and sanitize/redact them from result.result, result.logs, and result.error; updated docs/guidance strings.
Secret Input Helpers
packages/worker/src/mcp/secrets/capability-inputs.ts
Exported and typed helper predicates/accessors (isSecretInputSchema, getSchemaProperties, getArrayItemSchema, getAdditionalPropertiesSchema, isRecord) used to locate secret-marked inputs for tracking.
Tests
packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts, packages/worker/src/mcp/run-codemode-registry.node.test.ts, packages/worker/src/mcp/skills/infer-codemode-capabilities.node.test.ts
Added/updated tests: e2e saves secrets via codemode.secret_set and asserts redaction and empty logs; unit tests mock registry/executor to verify secret input tracking and redaction (including cyclic structures); inference test ensures secret_set is discovered.
Misc / Build & Formatting
packages/worker/src/mcp/ui-artifacts-repo.ts, package.json, packages/worker/public/mcp-apps/kody-ui-utils.css
Minor type/formatting tweaks: updateUiArtifact parameter type formatting, added oxfmt step to build:mcp-apps script, and CSS formatting/selector literal quoting changes.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client (Codemode)
    participant Executor as Codemode Executor
    participant Tracker as Secret Tracker
    participant Handler as Capability Handler
    participant Storage as Secret Storage
    participant Redactor as Secret Redactor

    Client->>Executor: invoke codemode.secret_set({name, value, scope, ...})
    Executor->>Tracker: trackSecretInputValue(value)
    Tracker->>Tracker: store tracked value
    Executor->>Handler: execute capability with resolved args
    Handler->>Storage: saveSecret(name, value, scope, ...)
    Storage-->>Handler: return metadata (no plaintext)
    Handler-->>Executor: return metadata object
    Executor->>Redactor: sanitize result.result, result.logs, result.error
    Redactor->>Redactor: replace tracked secret values with "[REDACTED SECRET]"
    Redactor-->>Executor: sanitized execution result
    Executor-->>Client: return redacted result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • kody#54 — Overlaps changes to run-codemode-registry.ts and provider/executor wiring; directly related to tracking/redaction plumbing.
  • kody#68 — Related to secret-resolution and execution pipeline changes (marking/tracking secret inputs and redaction).
  • kody#61 — Related to MCP secrets execution surface and secret_set flow (secret persistence and metadata handling).

Poem

🐰 I nibble code and hide the key,

a hop, a tuck — no plain-text spree.
I mark and track, then gently shred,
the echoes gone, the logs well-fed.
Safe burrows keep the secretled.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: adding a new secret_set capability to handle secure secret persistence in the codemode system.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/codemode-skill-oauth-tokens-7a5f

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@kentcdodds
kentcdodds marked this pull request as ready for review March 29, 2026 22:45
@github-actions

github-actions Bot commented Mar 29, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-96.kentcdodds.workers.dev

Worker: kody-pr-96
D1: kody-pr-96-db
KV: kody-pr-96-oauth-kv

Mocks:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/worker/src/mcp/run-codemode-registry.node.test.ts`:
- Around line 278-282: Replace the inline inline typeof import(...) type
annotation with a top-level type import: add a top-level "import type" for
getCapabilityRegistryForContext from '#mcp/capabilities/registry' and then
change the annotation to use Awaited<ReturnType<typeof
getCapabilityRegistryForContext>> (or import a dedicated return type if
available) instead of the inline typeof import(...) expression so the type is
clearer and easier to maintain; update the annotation near the current cast that
references getCapabilityRegistryForContext.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bc2dfed6-1908-40d9-8e1a-655b7cc51dfa

📥 Commits

Reviewing files that changed from the base of the PR and between fe3eb20 and 88a79b8.

📒 Files selected for processing (12)
  • docs/agents/adding-capabilities.md
  • docs/agents/secret-host-approval.md
  • packages/worker/src/mcp/capabilities/secrets/domain.ts
  • packages/worker/src/mcp/capabilities/secrets/secret-delete.ts
  • packages/worker/src/mcp/capabilities/secrets/secret-list.ts
  • packages/worker/src/mcp/capabilities/secrets/secret-set.ts
  • packages/worker/src/mcp/index.ts
  • packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts
  • packages/worker/src/mcp/run-codemode-registry.node.test.ts
  • packages/worker/src/mcp/run-codemode-registry.ts
  • packages/worker/src/mcp/skills/infer-codemode-capabilities.node.test.ts
  • packages/worker/src/mcp/tools/execute.ts

Comment thread packages/worker/src/mcp/run-codemode-registry.node.test.ts Outdated

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Non-string log entries bypass secret redaction
    • Switched log entry redaction to the recursive redactor so structured log entries now have secrets sanitized.
  • ✅ Fixed: Duplicated JSON schema traversal helpers across files
    • Exported the schema traversal helpers from capability-inputs.ts and reused them in run-codemode-registry.ts to remove duplication.
Preview (c243e5cbaa)
diff --git a/docs/agents/adding-capabilities.md b/docs/agents/adding-capabilities.md
--- a/docs/agents/adding-capabilities.md
+++ b/docs/agents/adding-capabilities.md
@@ -104,6 +104,12 @@
 elsewhere, and secret-bearing capability inputs still require an authenticated
 user.
 
+Those inputs are also treated as write-only for the rest of that execution:
+once plaintext crosses an `x-kody-secret` capability boundary, Kody redacts that
+plaintext from later execute results and logs before returning them to the
+caller. Capability authors should still avoid returning or logging secret
+material, but the runtime adds this extra defense-in-depth layer.
+
 When a secret has an `allowed_capabilities` policy, Kody also checks that the
 current capability name is explicitly listed before resolving the placeholder.
 An empty `allowed_capabilities` list means no capability is allowed to resolve
@@ -132,6 +138,8 @@
 3. Leave non-secret fields unannotated.
 4. Document the intended use in the capability description when it may not be
    obvious.
+5. If the capability persists or hands off a secret value, make the description
+   say that the input is write-only and must never be returned to chat.
 
 Example:
 
@@ -289,6 +297,7 @@
 
 - annotate only the exact credential fields, not the whole object
 - prefer this for local persistence or device-side credential flows
+- treat those inputs as write-only even if the runtime redacts accidental echoes
 - tell users which capability names should be added to a secret's
   `allowed_capabilities` policy when a workflow depends on restricted secrets
 - avoid using it for generic remote API wrappers where fetch-time host approval

diff --git a/docs/agents/secret-host-approval.md b/docs/agents/secret-host-approval.md
--- a/docs/agents/secret-host-approval.md
+++ b/docs/agents/secret-host-approval.md
@@ -73,6 +73,8 @@
 - say explicitly that only the authenticated account admin UI can approve hosts
 - say explicitly that only the authenticated account admin UI can restrict which
   capabilities may consume a secret directly
+- say explicitly that secret-bearing capability inputs are write-only from the
+  model's perspective and must not be echoed in execute results or logs
 - tell agents to inspect secret metadata before making a secret-bearing request
 - tell agents to surface the approval link and stop on deny
 
@@ -102,6 +104,8 @@
 - mark only the exact sensitive fields
 - prefer it for local persistence or device-side credential handoff
 - require an authenticated user, just like fetch-time secret placeholders
+- never return or log the resolved plaintext after it crosses an
+  `x-kody-secret` capability boundary
 
 Do not treat `x-kody-secret` as a generic replacement for execute-time
 `fetch(...)` placeholders.

diff --git a/packages/worker/src/mcp/capabilities/secrets/domain.ts b/packages/worker/src/mcp/capabilities/secrets/domain.ts
--- a/packages/worker/src/mcp/capabilities/secrets/domain.ts
+++ b/packages/worker/src/mcp/capabilities/secrets/domain.ts
@@ -2,11 +2,16 @@
 import { capabilityDomainNames } from '#mcp/capabilities/domain-metadata.ts'
 import { secretDeleteCapability } from './secret-delete.ts'
 import { secretListCapability } from './secret-list.ts'
+import { secretSetCapability } from './secret-set.ts'
 
 export const secretsDomain = defineDomain({
 	name: capabilityDomainNames.secrets,
 	description:
-		'Server-side secret references that can be discovered by name, listed with allowed-host metadata, and deleted without placing raw secret values into prompts or saved app source.',
+		'Server-side secret references that can be created, listed, and deleted without placing raw secret values into prompts, execute results, or saved app source.',
 	keywords: ['secret', 'credentials', 'reference', 'secure input'],
-	capabilities: [secretListCapability, secretDeleteCapability],
+	capabilities: [
+		secretListCapability,
+		secretSetCapability,
+		secretDeleteCapability,
+	],
 })

diff --git a/packages/worker/src/mcp/capabilities/secrets/secret-delete.ts b/packages/worker/src/mcp/capabilities/secrets/secret-delete.ts
--- a/packages/worker/src/mcp/capabilities/secrets/secret-delete.ts
+++ b/packages/worker/src/mcp/capabilities/secrets/secret-delete.ts
@@ -11,7 +11,7 @@
 	{
 		name: 'secret_delete',
 		description:
-			'Delete an existing secret reference for the signed-in user. Use this when a secret should no longer be available to execute-time code.',
+			'Delete an existing secret reference for the signed-in user without revealing its plaintext value. Never ask the user to paste a secret, token, API key, password, or credential into chat; use generated UI to collect missing user-provided secrets safely. Use this when a secret should no longer be available to execute-time code.',
 		keywords: ['secret', 'delete', 'remove', 'revoke', 'credential'],
 		readOnly: false,
 		idempotent: false,

diff --git a/packages/worker/src/mcp/capabilities/secrets/secret-list.ts b/packages/worker/src/mcp/capabilities/secrets/secret-list.ts
--- a/packages/worker/src/mcp/capabilities/secrets/secret-list.ts
+++ b/packages/worker/src/mcp/capabilities/secrets/secret-list.ts
@@ -12,7 +12,7 @@
 	{
 		name: 'secret_list',
 		description:
-			'List available secret references for the signed-in user without revealing secret values. When scope is omitted, results include every accessible scope in precedence order. Use `codemode.secret_list({ scope })` inside execute-time code when you want the same metadata, including allowed hosts and allowed capabilities, from the sandbox.',
+			'List available secret references for the signed-in user without revealing secret values. When scope is omitted, results include every accessible scope in precedence order. Use `codemode.secret_list({ scope })` inside execute-time code when you want the same metadata, including allowed hosts and allowed capabilities, from the sandbox. Never return a secret value from execute, and never ask the user to paste a secret, token, API key, password, or credential into chat; use generated UI to collect missing secrets safely.',
 		keywords: ['secret', 'list', 'discovery', 'metadata', 'credentials'],
 		readOnly: true,
 		idempotent: true,

diff --git a/packages/worker/src/mcp/capabilities/secrets/secret-set.ts b/packages/worker/src/mcp/capabilities/secrets/secret-set.ts
new file mode 100644
--- /dev/null
+++ b/packages/worker/src/mcp/capabilities/secrets/secret-set.ts
@@ -1,0 +1,73 @@
+import { markSecretInputFields } from '@kody-internal/shared/secret-input-schema.ts'
+import { z } from 'zod'
+import { defineDomainCapability } from '#mcp/capabilities/define-domain-capability.ts'
+import { capabilityDomainNames } from '#mcp/capabilities/domain-metadata.ts'
+import { requireMcpUser } from '#mcp/capabilities/meta/require-user.ts'
+import { type CapabilityContext } from '#mcp/capabilities/types.ts'
+import { saveSecret } from '#mcp/secrets/service.ts'
+import { secretScopeValues } from '#mcp/secrets/types.ts'
+import { secretMetadataSchema } from './shared.ts'
+
+const secretSetInputSchema = z.object({
+	name: z.string().min(1).describe('Secret name to create or update.'),
+	value: z
+		.string()
+		.min(1)
+		.describe(
+			'Secret value to persist. This field is write-only and must never be returned to the caller.',
+		),
+	description: z
+		.string()
+		.optional()
+		.describe('Optional human-readable description of the secret.'),
+	scope: z
+		.enum(secretScopeValues)
+		.describe('Storage scope that owns the secret.'),
+})
+
+const secretSetCapabilityInputJsonSchema = markSecretInputFields(
+	z.toJSONSchema(secretSetInputSchema) as Record<string, unknown>,
+	['value'],
+) as Record<string, unknown>
+
+export const secretSetCapability = defineDomainCapability(
+	capabilityDomainNames.secrets,
+	{
+		name: 'secret_set',
+		description:
+			'Create or update a stored secret reference for the signed-in user without ever returning the plaintext value. Use this only for server-side persistence of secret values that are already available inside trusted execution, such as refreshed OAuth tokens. Never ask the user to paste a secret, token, API key, password, or credential into chat; use generated UI to collect missing user-provided secrets safely. Saving a secret value does not authorize outbound host use or direct capability access.',
+		keywords: ['secret', 'persist', 'store', 'oauth', 'token', 'credential'],
+		readOnly: false,
+		idempotent: false,
+		destructive: false,
+		inputSchema: secretSetCapabilityInputJsonSchema,
+		outputSchema: secretMetadataSchema,
+		async handler(args, ctx: CapabilityContext) {
+			const parsed = secretSetInputSchema.parse(args)
+			const user = requireMcpUser(ctx.callerContext)
+			const saved = await saveSecret({
+				env: ctx.env,
+				userId: user.userId,
+				scope: parsed.scope,
+				name: parsed.name,
+				value: parsed.value,
+				description: parsed.description ?? '',
+				storageContext: {
+					sessionId: ctx.callerContext.storageContext?.sessionId ?? null,
+					appId: ctx.callerContext.storageContext?.appId ?? null,
+				},
+			})
+			return {
+				name: saved.name,
+				scope: saved.scope,
+				description: saved.description,
+				app_id: saved.appId,
+				allowed_hosts: saved.allowedHosts,
+				allowed_capabilities: saved.allowedCapabilities,
+				created_at: saved.createdAt,
+				updated_at: saved.updatedAt,
+				ttl_ms: saved.ttlMs,
+			}
+		},
+	},
+)

diff --git a/packages/worker/src/mcp/index.ts b/packages/worker/src/mcp/index.ts
--- a/packages/worker/src/mcp/index.ts
+++ b/packages/worker/src/mcp/index.ts
@@ -37,6 +37,7 @@
 - When a saved skill declares parameters, pass values via meta_run_skill params; the codemode can read them from the params variable.
 - Use 'ui_save_app' to persist reusable UI source for later reopening via 'app_id'. Saved apps are user-scoped UI artifacts, not codemode skills.
 - Use \`codemode.secret_list(args)\` during execute-time code to list secret metadata only; it does not return plaintext values.
+- Use \`codemode.secret_set(args)\` only to persist secret values that are already available inside trusted execution, such as refreshed OAuth tokens. It returns metadata only and never returns plaintext values.
 
 Kody source repository
 - Kody (this app and MCP server) is developed at https://github.com/kentcdodds/kody. When you launch a Cursor Cloud Agent to improve Kody itself, use that repository URL (unless the user explicitly points you at another fork or repo).
@@ -81,12 +82,14 @@
 - When chaining calls, read fields from the previous result using its outputSchema.
 - Chain multiple calls, use conditionals, and return structured results.
 - Use \`await codemode.secret_list({})\` or \`await codemode.secret_list({ scope: 'app' })\` when you need secret metadata such as names, descriptions, scopes, allowed hosts, and allowed capabilities from the sandbox.
+- Use \`await codemode.secret_set({ name, value, scope, description? })\` only when a capability or fetch flow has already produced a secret value inside execution and you need to persist it without showing it to the model or returning it to the caller.
 - Use \`await codemode.value_get({ name })\` or \`await codemode.value_list({ scope })\` for readable non-secret configuration that generated UI code should be able to store and read back later.
 - Use normal \`fetch(...)\` for outbound HTTP. To inject a stored secret, place a placeholder such as \`{{secret:cloudflareToken}}\` or \`{{secret:cloudflareToken|scope=user}}\` in the URL, headers, or request body; the host resolves it server-side and blocks unapproved destinations.
 - Some capability input fields also accept secret placeholders. When an input schema marks a string field with \`x-kody-secret: true\`, you may pass \`{{secret:name}}\` or \`{{secret:name|scope=user}}\` there instead of a raw value. If that secret has an allowed-capabilities policy, the current capability name must be on the allowlist.
 - Secret placeholders are not general-purpose string interpolation. Do not use \`execute\` to build a string or object that merely returns \`{{secret:...}}\`; those placeholders only resolve in secret-aware fetch paths or capability inputs that explicitly opt into \`x-kody-secret\`.
-- Saving or updating a secret does not authorize sending it anywhere. If a fetch fails because a host is not approved for that secret, ask the user whether to open the approval link and approve that host in the web app.
-- Secrets are intentionally not readable or updatable through \`codemode\`. Never ask the user to paste a secret into chat; use generated UI flows such as \`saveSecret(...)\` when the user needs to provide or rotate a value, and use \`codemode.secret_delete(...)\` only when removing a stored secret reference.
+- Saving or updating a secret does not authorize sending it anywhere or passing it into arbitrary capabilities. If a fetch fails because a host is not approved for that secret, ask the user whether to open the approval link and approve that host in the web app.
+- Never ask the user to paste a secret into chat. Use generated UI flows when the user needs to provide or rotate a value themselves. \`codemode.secret_set(...)\` is for persisting secret values that are already available inside execution, not for collecting them from chat.
+- Secret values that pass through \`x-kody-secret\` capability inputs are treated as write-only: they may be persisted or handed to the host-side capability, but they must never be returned in execute results or logs.
 - Your code must be an async arrow function that returns the result.
 - Example: const result = await codemode[capabilityName](args)
 

diff --git a/packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts b/packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts
--- a/packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts
+++ b/packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts
@@ -1391,6 +1391,76 @@
 	expect(executePayload.result?.userSecretNames).toContain('globalApiKey')
 	expect(executePayload.result?.sessionSecretNames).toContain('ephemeralCode')
 
+	const refreshedAccessToken = `spotify-access-${crypto.randomUUID()}`
+	const refreshedRefreshToken = `spotify-refresh-${crypto.randomUUID()}`
+	const secretSetExecuteResponse = await fetch(executeUrl!, {
+		method: 'POST',
+		headers: {
+			Authorization: `Bearer ${token}`,
+			'Content-Type': 'application/json',
+			Accept: 'application/json',
+		},
+		body: JSON.stringify({
+			code: `async () => {
+				const savedAccessToken = await codemode.secret_set({
+					name: 'spotifyAccessToken',
+					value: ${JSON.stringify(refreshedAccessToken)},
+					description: 'Spotify OAuth access token',
+					scope: 'user',
+				})
+				const savedRefreshToken = await codemode.secret_set({
+					name: 'spotifyRefreshToken',
+					value: ${JSON.stringify(refreshedRefreshToken)},
+					description: 'Spotify OAuth refresh token',
+					scope: 'user',
+				})
+				return {
+					savedAccessToken,
+					savedRefreshToken,
+					leakedValue: ${JSON.stringify(refreshedAccessToken)},
+				}
+			}`,
+		}),
+	})
+	if (!secretSetExecuteResponse.ok) {
+		throw new Error(await secretSetExecuteResponse.text())
+	}
+	const secretSetExecutePayload =
+		(await secretSetExecuteResponse.json()) as {
+			ok?: boolean
+			result?: {
+				savedAccessToken?: Record<string, unknown>
+				savedRefreshToken?: Record<string, unknown>
+				leakedValue?: string
+			}
+			logs?: Array<string>
+		}
+	expect(secretSetExecutePayload.ok).toBe(true)
+	expect(secretSetExecutePayload.result?.savedAccessToken).toEqual({
+		name: 'spotifyAccessToken',
+		scope: 'user',
+		description: 'Spotify OAuth access token',
+		app_id: null,
+		allowed_hosts: [],
+		allowed_capabilities: [],
+		created_at: expect.any(String),
+		updated_at: expect.any(String),
+		ttl_ms: null,
+	})
+	expect(secretSetExecutePayload.result?.savedRefreshToken).toEqual({
+		name: 'spotifyRefreshToken',
+		scope: 'user',
+		description: 'Spotify OAuth refresh token',
+		app_id: null,
+		allowed_hosts: [],
+		allowed_capabilities: [],
+		created_at: expect.any(String),
+		updated_at: expect.any(String),
+		ttl_ms: null,
+	})
+	expect(secretSetExecutePayload.result?.leakedValue).toBe('[REDACTED SECRET]')
+	expect(secretSetExecutePayload.logs ?? []).toEqual([])
+
 	const listSecretsResponse = await fetch(`${secretsUrl!}?scope=app`, {
 		headers: {
 			Authorization: `Bearer ${token}`,
@@ -1711,6 +1781,10 @@
 		(match) => match.type === 'secret' && match.name === 'globalApiKey',
 	)
 	expect(userSecretMatch?.description).toBe('Reusable cross-app API key')
+	const spotifySecretMatch = matches.find(
+		(match) => match.type === 'secret' && match.name === 'spotifyAccessToken',
+	)
+	expect(spotifySecretMatch?.description).toBe('Spotify OAuth access token')
 	const sessionSecretMatch = matches.find(
 		(match) => match.type === 'secret' && match.name === 'ephemeralCode',
 	)

diff --git a/packages/worker/src/mcp/run-codemode-registry.node.test.ts b/packages/worker/src/mcp/run-codemode-registry.node.test.ts
--- a/packages/worker/src/mcp/run-codemode-registry.node.test.ts
+++ b/packages/worker/src/mcp/run-codemode-registry.node.test.ts
@@ -180,3 +180,127 @@
 		resolveSecretSpy.mockRestore()
 	}
 })
+
+test('buildCodemodeFns tracks values that crossed secret-marked capability inputs', async () => {
+	const env = {} as Env
+	const callerContext = createMcpCallerContext({
+		baseUrl: 'https://heykody.dev',
+		user: { userId: 'user-123' },
+	})
+	const trackedSecretValues: Array<string> = []
+
+	const getRegistrySpy = vi
+		.spyOn(await import('#mcp/capabilities/registry.ts'), 'getCapabilityRegistryForContext')
+		.mockResolvedValue({
+			capabilityDomains: [],
+			capabilityDomainDescriptionsByName: {} as Record<string, string>,
+			capabilityHandlers: {},
+			capabilityList: [
+				{
+					name: 'secret_set',
+					domain: 'secrets',
+					description: 'Store a secret.',
+					keywords: [],
+					readOnly: false,
+					idempotent: false,
+					destructive: false,
+					inputSchema: {
+						type: 'object',
+						properties: {
+							name: { type: 'string' },
+							value: { type: 'string', 'x-kody-secret': true },
+						},
+						required: ['name', 'value'],
+					},
+					outputSchema: {
+						type: 'object',
+						properties: {
+							name: { type: 'string' },
+						},
+					},
+					async handler(args: Record<string, unknown>) {
+						return {
+							name: args.name,
+						}
+					},
+				},
+			],
+			capabilityMap: {
+				secret_set: {
+					name: 'secret_set',
+					domain: 'secrets',
+					description: 'Store a secret.',
+					keywords: [],
+					readOnly: false,
+					idempotent: false,
+					destructive: false,
+					inputSchema: {
+						type: 'object',
+						properties: {
+							name: { type: 'string' },
+							value: { type: 'string', 'x-kody-secret': true },
+						},
+						required: ['name', 'value'],
+					},
+					outputSchema: {
+						type: 'object',
+						properties: {
+							name: { type: 'string' },
+						},
+					},
+					async handler(args: Record<string, unknown>) {
+						return {
+							name: args.name,
+						}
+					},
+				},
+			},
+			capabilitySpecs: {},
+			capabilityToolDescriptors: {
+				secret_set: {
+					description: 'Store a secret.',
+					inputSchema: {
+						type: 'object',
+						properties: {
+							name: { type: 'string' },
+							value: { type: 'string', 'x-kody-secret': true },
+						},
+						required: ['name', 'value'],
+					},
+					outputSchema: {
+						type: 'object',
+						properties: {
+							name: { type: 'string' },
+						},
+					},
+				},
+			},
+		} as Awaited<
+			ReturnType<
+				typeof import('#mcp/capabilities/registry.ts').getCapabilityRegistryForContext
+			>
+		>)
+
+	try {
+		const codemode = await buildCodemodeFns(
+			env,
+			callerContext,
+			{
+				trackSecretInputValue(value) {
+					trackedSecretValues.push(value)
+				},
+			},
+		)
+	const result = await codemode.secret_set({
+			name: 'spotifyAccessToken',
+			value: 'fresh-access-token',
+		})
+
+	expect(result).toEqual({
+			name: 'spotifyAccessToken',
+		})
+		expect(trackedSecretValues).toEqual(['fresh-access-token'])
+	} finally {
+		getRegistrySpy.mockRestore()
+	}
+})

diff --git a/packages/worker/src/mcp/run-codemode-registry.ts b/packages/worker/src/mcp/run-codemode-registry.ts
--- a/packages/worker/src/mcp/run-codemode-registry.ts
+++ b/packages/worker/src/mcp/run-codemode-registry.ts
@@ -5,8 +5,15 @@
 } from '@cloudflare/codemode'
 import { exports as workerExports } from 'cloudflare:workers'
 import { type McpCallerContext } from '@kody-internal/shared/chat.ts'
-import { resolveCapabilityInputSecrets } from '#mcp/secrets/capability-inputs.ts'
 import {
+	getAdditionalPropertiesSchema,
+	getArrayItemSchema,
+	getSchemaProperties,
+	isRecord,
+	isSecretInputSchema,
+	resolveCapabilityInputSecrets,
+} from '#mcp/secrets/capability-inputs.ts'
+import {
 	capabilityInputSecretAuthRequiredMessage,
 	createCapabilitySecretAccessDeniedMessage,
 	createCapabilitySecretAccessDeniedBatchMessage,
@@ -26,6 +33,7 @@
 			secret: ReferencedSecret,
 			capabilityName: string,
 		) => Promise<string>
+		trackSecretInputValue?: (value: string) => void
 	},
 ) {
 	const { capabilityMap } = await getCapabilityRegistryForContext({
@@ -49,6 +57,11 @@
 					resolveSecretValue: (secret) =>
 						resolveSecretValue(secret, capability.name),
 				})
+				collectSecretInputValues({
+					schema: capability.inputSchema,
+					value: resolvedArgs,
+					track: options?.trackSecretInputValue,
+				})
 				return capability.handler(resolvedArgs as Record<string, unknown>, {
 					env,
 					callerContext,
@@ -61,8 +74,11 @@
 export async function buildCodemodeProvider(
 	env: Env,
 	callerContext: McpCallerContext,
+	options?: {
+		trackSecretInputValue?: (value: string) => void
+	},
 ): Promise<ResolvedProvider> {
-	const tools = await buildCodemodeFns(env, callerContext)
+	const tools = await buildCodemodeFns(env, callerContext, options)
 	const provider: ToolProvider = {
 		name: 'codemode',
 		tools: Object.fromEntries(
@@ -128,6 +144,7 @@
 	executorExports?: typeof workerExports,
 ) {
 	const { createExecuteExecutor } = await import('#mcp/executor.ts')
+	const secretRedactor = createExecutionSecretRedactor()
 	const normalizedStorageContext = normalizeStorageContext(
 		callerContext.storageContext ?? null,
 	)
@@ -140,20 +157,30 @@
 			storageContext: normalizedStorageContext,
 		},
 	})
-	const provider = await buildCodemodeProvider(env, callerContext)
+	const provider = await buildCodemodeProvider(env, callerContext, {
+		trackSecretInputValue: (value) => {
+			secretRedactor.track(value)
+		},
+	})
 	const wrapped =
 		params !== undefined
 			? await buildParameterizedSkillCode(code, params)
 			: code
 	const result = await executor.execute(wrapped, [provider])
-	if (!result.error) return result
+	const sanitizedResult = secretRedactor.sanitizeExecuteResult(result)
+	if (!result.error) return sanitizedResult
 	const batchMessage = await rewriteCapabilitySecretError({
 		error: result.error,
 		code: wrapped,
 		env,
 		callerContext,
 	})
-	return batchMessage ? { ...result, error: batchMessage } : result
+	return batchMessage
+		? {
+				...sanitizedResult,
+				error: secretRedactor.redactUnknown(batchMessage),
+			}
+		: sanitizedResult
 }
 
 async function rewriteCapabilitySecretError(input: {
@@ -208,6 +235,119 @@
 	).sort((left, right) => left.localeCompare(right))
 }
 
+const redactedSecretText = '[REDACTED SECRET]'
+
+function createExecutionSecretRedactor() {
+	const secretValues = new Set<string>()
+	return {
+		track(value: string) {
+			if (value.length > 0) {
+				secretValues.add(value)
+			}
+		},
+		redactUnknown(value: unknown) {
+			return redactUnknownSecretValues(value, secretValues)
+		},
+		sanitizeExecuteResult<T extends { result?: unknown; logs?: unknown; error?: unknown }>(
+			result: T,
+		): T {
+			return {
+				...result,
+				result: redactUnknownSecretValues(result.result, secretValues),
+				logs: Array.isArray(result.logs)
+					? result.logs.map((entry) =>
+							redactUnknownSecretValues(entry, secretValues),
+						)
+					: result.logs,
+				error: redactUnknownSecretValues(result.error, secretValues),
+			}
+		},
+	}
+}
+
+function collectSecretInputValues(input: {
+	schema: unknown
+	value: unknown
+	track?: (value: string) => void
+}) {
+	if (!input.track) return
+	visitSecretInputValue(input.schema, input.value, input.track)
+}
+
+function visitSecretInputValue(
+	schema: unknown,
+	value: unknown,
+	track: (value: string) => void,
+) {
+	if (typeof value === 'string' && isSecretInputSchema(schema)) {
+		track(value)
+		return
+	}
+	if (Array.isArray(value)) {
+		const itemSchema = getArrayItemSchema(schema)
+		if (!itemSchema) return
+		for (const item of value) {
+			visitSecretInputValue(itemSchema, item, track)
+		}
+		return
+	}
+	if (!isRecord(value)) return
+	const propertySchemas = getSchemaProperties(schema)
+	const additionalProperties = getAdditionalPropertiesSchema(schema)
+	if (!propertySchemas && !additionalProperties) return
+	for (const [key, entryValue] of Object.entries(value)) {
+		const entrySchema = propertySchemas?.[key] ?? additionalProperties
+		if (!entrySchema) continue
+		visitSecretInputValue(entrySchema, entryValue, track)
+	}
+}
+
+function redactUnknownSecretValues(
+	value: unknown,
+	secretValues: ReadonlySet<string>,
+): unknown {
+	if (secretValues.size === 0) return value
+	if (typeof value === 'string') {
+		return redactSecretValuesInString(value, secretValues)
+	}
+	if (value instanceof Error) {
+		const next = new Error(
+			redactSecretValuesInString(value.message, secretValues),
+		)
+		next.name = value.name
+		if (value.stack) {
+			next.stack = redactSecretValuesInString(value.stack, secretValues)
+		}
+		return next
+	}
+	if (Array.isArray(value)) {
+		return value.map((entry) => redactUnknownSecretValues(entry, secretValues))
+	}
+	if (isRecord(value)) {
+		return Object.fromEntries(
+			Object.entries(value).map(([key, entry]) => [
+				key,
+				redactUnknownSecretValues(entry, secretValues),
+			]),
+		)
+	}
+	return value
+}
+
+function redactSecretValuesInString(
+	value: string,
+	secretValues: ReadonlySet<string>,
+) {
+	if (secretValues.size === 0 || value.length === 0) return value
+	let nextValue = value
+	for (const secretValue of [...secretValues].sort(
+		(left, right) => right.length - left.length,
+	)) {
+		nextValue = nextValue.replaceAll(secretValue, redactedSecretText)
+	}
+	return nextValue
+}
+
 function normalizeStorageContext(
 	storageContext: McpCallerContext['storageContext'] | null,
 ) {

diff --git a/packages/worker/src/mcp/secrets/capability-inputs.ts b/packages/worker/src/mcp/secrets/capability-inputs.ts
--- a/packages/worker/src/mcp/secrets/capability-inputs.ts
+++ b/packages/worker/src/mcp/secrets/capability-inputs.ts
@@ -81,29 +81,29 @@
 	return replaceSecretPlaceholders(value, replacements)
 }
 
-function isSecretInputSchema(schema: unknown) {
+export function isSecretInputSchema(schema: unknown) {
 	if (!isRecord(schema)) return false
 	return schema[secretInputSchemaFlag] === true
 }
 
-function getSchemaProperties(schema: unknown) {
+export function getSchemaProperties(schema: unknown) {
 	if (!isRecord(schema)) return null
 	const properties = schema.properties
 	return isRecord(properties) ? properties : null
 }
 
-function getArrayItemSchema(schema: unknown) {
+export function getArrayItemSchema(schema: unknown) {
 	if (!isRecord(schema)) return null
 	const items = schema.items
 	return items === undefined || Array.isArray(items) ? null : items
 }
 
-function getAdditionalPropertiesSchema(schema: unknown) {
+export function getAdditionalPropertiesSchema(schema: unknown) {
 	if (!isRecord(schema)) return null
 	const additionalProperties = schema.additionalProperties
 	return isRecord(additionalProperties) ? additionalProperties : null
 }
 
-function isRecord(value: unknown): value is Record<string, unknown> {
+export function isRecord(value: unknown): value is Record<string, unknown> {
 	return value !== null && typeof value === 'object' && !Array.isArray(value)
 }

diff --git a/packages/worker/src/mcp/skills/infer-codemode-capabilities.node.test.ts b/packages/worker/src/mcp/skills/infer-codemode-capabilities.node.test.ts
--- a/packages/worker/src/mcp/skills/infer-codemode-capabilities.node.test.ts
+++ b/packages/worker/src/mcp/skills/infer-codemode-capabilities.node.test.ts
@@ -24,6 +24,21 @@
 	expect(staticNames).toContain('github_rest')
 })
 
+test('infers secret_set member access', () => {
+	const src = `async () => {
+    const refreshedToken = 'token-from-refresh';
+    await codemode.secret_set({
+      name: 'spotifyAccessToken',
+      value: refreshedToken,
+      scope: 'user',
+      description: 'Spotify OAuth access token',
+    });
+  }`
+	const { staticNames, inferencePartial } = inferCodemodeCapabilities(src)
+	expect(inferencePartial).toBe(false)
+	expect(staticNames).toContain('secret_set')
+})
+
 test('marks partial for computed non-literal codemode access', () => {
 	const program = parse(
 		`async () => { const x = 'ui_save_app'; return await codemode[x]({}) }`,

diff --git a/packages/worker/src/mcp/tools/execute.ts b/packages/worker/src/mcp/tools/execute.ts
--- a/packages/worker/src/mcp/tools/execute.ts
+++ b/packages/worker/src/mcp/tools/execute.ts
@@ -55,7 +55,9 @@
 Secrets:
 - Never ask the user to paste secrets, tokens, API keys, passwords, OAuth codes, client secrets, or other credentials into chat. If a workflow needs a secret value that is not already stored, use generated UI to collect and save it instead.
 - Use \`await codemode.secret_list({})\` to inspect available secret metadata before building a request. The result is metadata only and does not reveal secret values; it includes allowed hosts and allowed capability names.
+- Use \`await codemode.secret_set({ name, value, scope, description })\` only to persist a secret value that is already available inside trusted execution, such as a refreshed OAuth token. The \`value\` input is write-only; execute results and logs redact secret-marked capability inputs before they are returned.
 - Pass \`scope\` to narrow results, for example \`await codemode.secret_list({ scope: 'app' })\`.
+- Deleting or saving a secret never grants outbound host approval or capability approval. Those policies are still managed through the authenticated account secrets UI.
 - Do not expect \`codemode.secret_get(...)\`, \`codemode.secret_require(...)\`, or any injected \`secrets\` helper to be available in execute-time code.
 
 Values:

Comment thread packages/worker/src/mcp/run-codemode-registry.ts
Comment thread packages/worker/src/mcp/run-codemode-registry.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/worker/src/mcp/secrets/capability-inputs.ts (1)

84-109: Consider explicit return types for newly exported helpers.

Now that these are module-level exports, explicit return types would lock API shape and reduce accidental contract drift from inference changes.

♻️ Suggested API-clarity diff
-export function isSecretInputSchema(schema: unknown) {
+export function isSecretInputSchema(schema: unknown): boolean {
 	if (!isRecord(schema)) return false
 	return schema[secretInputSchemaFlag] === true
 }

-export function getSchemaProperties(schema: unknown) {
+export function getSchemaProperties(
+	schema: unknown,
+): Record<string, unknown> | null {
 	if (!isRecord(schema)) return null
 	const properties = schema.properties
 	return isRecord(properties) ? properties : null
 }

-export function getArrayItemSchema(schema: unknown) {
+export function getArrayItemSchema(schema: unknown): unknown | null {
 	if (!isRecord(schema)) return null
 	const items = schema.items
 	return items === undefined || Array.isArray(items) ? null : items
 }

-export function getAdditionalPropertiesSchema(schema: unknown) {
+export function getAdditionalPropertiesSchema(
+	schema: unknown,
+): Record<string, unknown> | null {
 	if (!isRecord(schema)) return null
 	const additionalProperties = schema.additionalProperties
 	return isRecord(additionalProperties) ? additionalProperties : null
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/secrets/capability-inputs.ts` around lines 84 - 109,
Add explicit return types to the exported helper functions to lock the API
shape: change isSecretInputSchema to return boolean; getSchemaProperties,
getArrayItemSchema and getAdditionalPropertiesSchema to return Record<string,
unknown> | null; leave isRecord as the existing type predicate (value is
Record<string, unknown>)—update the function signatures for isSecretInputSchema,
getSchemaProperties, getArrayItemSchema and getAdditionalPropertiesSchema
accordingly so their exported types are explicit and stable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/worker/src/mcp/secrets/capability-inputs.ts`:
- Around line 84-109: Add explicit return types to the exported helper functions
to lock the API shape: change isSecretInputSchema to return boolean;
getSchemaProperties, getArrayItemSchema and getAdditionalPropertiesSchema to
return Record<string, unknown> | null; leave isRecord as the existing type
predicate (value is Record<string, unknown>)—update the function signatures for
isSecretInputSchema, getSchemaProperties, getArrayItemSchema and
getAdditionalPropertiesSchema accordingly so their exported types are explicit
and stable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 74df436b-c973-45be-a391-a4442a29fea8

📥 Commits

Reviewing files that changed from the base of the PR and between 88a79b8 and c243e5c.

📒 Files selected for processing (2)
  • packages/worker/src/mcp/run-codemode-registry.ts
  • packages/worker/src/mcp/secrets/capability-inputs.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/worker/src/mcp/run-codemode-registry.ts

Comment thread packages/worker/src/mcp/run-codemode-registry.ts

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Unused redactUnknown method is dead code
    • Removed the unused redactUnknown method from the redactor since it has no call sites and is redundant with the existing internal helper.

Comment thread packages/worker/src/mcp/run-codemode-registry.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/worker/src/mcp/run-codemode-registry.ts (1)

311-319: Consider preserving Error.cause if present.

When reconstructing the Error, the cause property (which may contain additional context) is not carried over. If secrets could appear in cause, it would also need redaction.

♻️ Optional enhancement
 	if (value instanceof Error) {
 		const next = new Error(
 			redactSecretValuesInString(value.message, secretValues),
+			value.cause !== undefined
+				? { cause: redactUnknownSecretValues(value.cause, secretValues) }
+				: undefined,
 		)
 		next.name = value.name
 		if (value.stack) {
 			next.stack = redactSecretValuesInString(value.stack, secretValues)
 		}
 		return next
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/run-codemode-registry.ts` around lines 311 - 319, The
Error reconstruction in the value instanceof Error branch drops the original
Error.cause; update that block (the code that creates next from value) to
preserve and redact cause: if value.cause exists, set next.cause to a redacted
version — for string causes run redactSecretValuesInString(value.cause,
secretValues), for Error causes recursively reconstruct a new Error whose
message/name/stack are redacted (same pattern used for value), and for other
types convert to string and redact; ensure you reuse redactSecretValuesInString
and attach the redacted cause to next.cause so the original causal context is
preserved but secrets are removed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/worker/src/mcp/run-codemode-registry.ts`:
- Around line 251-261: sanitizeExecuteResult currently calls
redactUnknownSecretValues for the error field which yields unknown and breaks
the ExecuteResult.error type; change the error handling to treat result.error
explicitly (if typeof result.error === 'string' then call
redactSecretValuesInString(result.error, secretValues) and return that string,
otherwise return undefined), referencing sanitizeExecuteResult,
ExecuteResult.error, redactUnknownSecretValues and redactSecretValuesInString so
you only use redactUnknownSecretValues where its unknown return is acceptable
and use redactSecretValuesInString for the error string to satisfy the string |
undefined type.

---

Nitpick comments:
In `@packages/worker/src/mcp/run-codemode-registry.ts`:
- Around line 311-319: The Error reconstruction in the value instanceof Error
branch drops the original Error.cause; update that block (the code that creates
next from value) to preserve and redact cause: if value.cause exists, set
next.cause to a redacted version — for string causes run
redactSecretValuesInString(value.cause, secretValues), for Error causes
recursively reconstruct a new Error whose message/name/stack are redacted (same
pattern used for value), and for other types convert to string and redact;
ensure you reuse redactSecretValuesInString and attach the redacted cause to
next.cause so the original causal context is preserved but secrets are removed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 36089a83-14bd-43fd-a530-877bc1e3a151

📥 Commits

Reviewing files that changed from the base of the PR and between c243e5c and ff2f062.

📒 Files selected for processing (1)
  • packages/worker/src/mcp/run-codemode-registry.ts

Comment thread packages/worker/src/mcp/run-codemode-registry.ts
@cursor
cursor Bot force-pushed the cursor/codemode-skill-oauth-tokens-7a5f branch from ff2f062 to b812034 Compare March 30, 2026 00:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/worker/src/mcp/run-codemode-registry.ts`:
- Around line 345-350: The current branch in redactUnknownSecretValues leaves
object property names unchanged (in the isRecord handling), so secrets used as
keys can leak; update the Object.entries mapping inside
redactUnknownSecretValues to also redact each property name by passing the key
through the same redaction routine (use redactUnknownSecretValues(key,
secretValues) or the single-value redactor if available) and return
[redactedKey, redactUnknownSecretValues(entry, secretValues)] so both keys and
values are sanitized; keep using isRecord and secretValues to locate this
change.
- Around line 321-353: The redactUnknownSecretValues walker isn't cycle-safe and
will recurse forever on self-referential values; update
redactUnknownSecretValues to track visited objects (e.g., WeakSet for
objects/arrays/Errors) and return previously-processed references to avoid
re-traversal, ensuring you mark entries before recursing into Error.cause,
arrays, and records so cycles don't re-enter; keep using
redactSecretValuesInString for strings and apply redaction in-place or by
returning the same object identity when previously seen. Also add a regression
test that calls the secret-marked capability and returns a self-referential
object/array/error from ExecuteResult.result to assert no stack overflow and
that redaction still replaces secrets in strings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 877ec6dc-2714-4be9-9f12-bbefade99759

📥 Commits

Reviewing files that changed from the base of the PR and between ff2f062 and b812034.

📒 Files selected for processing (15)
  • docs/agents/adding-capabilities.md
  • docs/agents/secret-host-approval.md
  • package.json
  • packages/worker/src/mcp/capabilities/secrets/domain.ts
  • packages/worker/src/mcp/capabilities/secrets/secret-delete.ts
  • packages/worker/src/mcp/capabilities/secrets/secret-list.ts
  • packages/worker/src/mcp/capabilities/secrets/secret-set.ts
  • packages/worker/src/mcp/index.ts
  • packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts
  • packages/worker/src/mcp/run-codemode-registry.node.test.ts
  • packages/worker/src/mcp/run-codemode-registry.ts
  • packages/worker/src/mcp/secrets/capability-inputs.ts
  • packages/worker/src/mcp/skills/infer-codemode-capabilities.node.test.ts
  • packages/worker/src/mcp/tools/execute.ts
  • packages/worker/src/mcp/ui-artifacts-repo.ts
✅ Files skipped from review due to trivial changes (6)
  • packages/worker/src/mcp/capabilities/secrets/secret-delete.ts
  • packages/worker/src/mcp/skills/infer-codemode-capabilities.node.test.ts
  • packages/worker/src/mcp/capabilities/secrets/secret-list.ts
  • packages/worker/src/mcp/ui-artifacts-repo.ts
  • packages/worker/src/mcp/tools/execute.ts
  • docs/agents/adding-capabilities.md
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/worker/src/mcp/index.ts
  • docs/agents/secret-host-approval.md
  • packages/worker/src/mcp/capabilities/secrets/domain.ts
  • packages/worker/src/mcp/secrets/capability-inputs.ts
  • packages/worker/src/mcp/capabilities/secrets/secret-set.ts
  • packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts
  • packages/worker/src/mcp/run-codemode-registry.node.test.ts

Comment thread packages/worker/src/mcp/run-codemode-registry.ts
Comment thread packages/worker/src/mcp/run-codemode-registry.ts Outdated
cursoragent and others added 5 commits March 29, 2026 18:01
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Sanitize secret-bearing keys as well as values in execute results, and make the redaction walker cycle-safe so self-referential results cannot recurse forever.

Made-with: Cursor
@kentcdodds
kentcdodds force-pushed the cursor/codemode-skill-oauth-tokens-7a5f branch from b812034 to d6562c7 Compare March 30, 2026 01:08
@kentcdodds
kentcdodds merged commit 8c9d83f into main Mar 30, 2026
8 of 9 checks passed
@kentcdodds
kentcdodds deleted the cursor/codemode-skill-oauth-tokens-7a5f branch April 17, 2026 00:59
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