Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/agents/adding-capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ runs. Missing secrets still fail with the same "secret not found" guidance used
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
Expand Down Expand Up @@ -132,6 +138,8 @@ How to annotate:
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:

Expand Down Expand Up @@ -289,6 +297,7 @@ scope narrow:

- 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
Expand Down
4 changes: 4 additions & 0 deletions docs/agents/secret-host-approval.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ When writing capability descriptions or agent-facing docs:
- 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

Expand Down Expand Up @@ -102,6 +104,8 @@ Use it narrowly:
- 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.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"dev:mock-cloudflare": "node --env-file=packages/worker/.env ./wrangler-env.ts dev --local --config packages/mock-servers/cloudflare/wrangler.jsonc",
"deploy": "node --input-type=module -e \"if (['preview','test'].includes(process.env.CLOUDFLARE_ENV ?? '')) { await import('./tools/prepare-e2e-env.ts'); }\" && npm run build && node --env-file=packages/worker/.env ./wrangler-env.ts deploy --outdir .wrangler/sentry-bundle --upload-source-maps",
"sentry:upload-sourcemaps": "node tools/sentry-upload-sourcemaps.ts",
"build:mcp-apps": "esbuild packages/worker/client/mcp-apps/kody-ui-utils.ts --bundle --format=esm --target=es2022 --outfile=packages/worker/public/mcp-apps/kody-ui-utils.js --platform=browser --banner:js='/* eslint-disable no-undef */'",
"build:mcp-apps": "esbuild packages/worker/client/mcp-apps/kody-ui-utils.ts --bundle --format=esm --target=es2022 --outfile=packages/worker/public/mcp-apps/kody-ui-utils.js --platform=browser --banner:js='/* eslint-disable no-undef */' && oxfmt packages/worker/public/mcp-apps/kody-ui-utils.css",
"build:client:web": "esbuild packages/worker/client/entry.tsx --bundle --format=esm --target=es2022 --outdir=packages/worker/public --entry-names=client-entry --chunk-names=assets/[name] --asset-names=assets/[name] --jsx=automatic --jsx-import-source=remix/component",
"build:client": "npm run build:client:web && npm run build:mcp-apps",
"build": "nx run worker:build",
Expand Down
228 changes: 112 additions & 116 deletions packages/worker/public/mcp-apps/kody-ui-utils.css
Original file line number Diff line number Diff line change
@@ -1,176 +1,172 @@
/* packages/worker/client/mcp-apps/kody-ui-utils.css */
:root {
color-scheme: light dark;
--font-body:
ui-sans-serif,
system-ui,
sans-serif;
--font-mono:
ui-monospace,
SFMono-Regular,
Menlo,
Monaco,
Consolas,
monospace;
--spacing-1: 0.25rem;
--spacing-2: 0.5rem;
--spacing-3: 0.75rem;
--spacing-4: 1rem;
--spacing-6: 1.5rem;
--radius-2: 0.5rem;
--radius-3: 0.75rem;
--shadow-1: 0 1px 2px rgb(15 23 42 / 0.08);
--color-bg: #ffffff;
--color-surface: #f8fafc;
--color-fg: #0f172a;
--color-muted: #475569;
--color-border: #dbe2ea;
--color-accent: #2563eb;
--color-accent-contrast: #ffffff;
--color-code-bg: rgb(15 23 42 / 0.06);
color-scheme: light dark;
--font-body: ui-sans-serif, system-ui, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
--spacing-1: 0.25rem;
--spacing-2: 0.5rem;
--spacing-3: 0.75rem;
--spacing-4: 1rem;
--spacing-6: 1.5rem;
--radius-2: 0.5rem;
--radius-3: 0.75rem;
--shadow-1: 0 1px 2px rgb(15 23 42 / 0.08);
--color-bg: #ffffff;
--color-surface: #f8fafc;
--color-fg: #0f172a;
--color-muted: #475569;
--color-border: #dbe2ea;
--color-accent: #2563eb;
--color-accent-contrast: #ffffff;
--color-code-bg: rgb(15 23 42 / 0.06);
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0f172a;
--color-surface: #162033;
--color-fg: #e5eef8;
--color-muted: #a5b4c7;
--color-border: #2a3950;
--color-accent: #60a5fa;
--color-accent-contrast: #0f172a;
--color-code-bg: rgb(148 163 184 / 0.16);
}
:root {
--color-bg: #0f172a;
--color-surface: #162033;
--color-fg: #e5eef8;
--color-muted: #a5b4c7;
--color-border: #2a3950;
--color-accent: #60a5fa;
--color-accent-contrast: #0f172a;
--color-code-bg: rgb(148 163 184 / 0.16);
}
}
:where(html) {
min-height: 100%;
background: var(--color-bg);
color: var(--color-fg);
min-height: 100%;
background: var(--color-bg);
color: var(--color-fg);
}
:where(body) {
min-height: 100%;
margin: 0;
padding: var(--spacing-4);
background: var(--color-bg);
color: var(--color-fg);
font: 400 14px/1.5 var(--font-body);
min-height: 100%;
margin: 0;
padding: var(--spacing-4);
background: var(--color-bg);
color: var(--color-fg);
font: 400 14px/1.5 var(--font-body);
}
:where(body[data-kody-runtime=javascript]) {
padding: 0;
:where(body[data-kody-runtime='javascript']) {
padding: 0;
}
:where(*, *::before, *::after) {
box-sizing: border-box;
box-sizing: border-box;
}
:where(a) {
color: var(--color-accent);
color: var(--color-accent);
}
:where(p, ul, ol, dl, pre, table, blockquote, fieldset) {
margin: 0 0 var(--spacing-4);
margin: 0 0 var(--spacing-4);
}
:where(h1, h2, h3, h4, h5, h6) {
margin: 0 0 var(--spacing-3);
line-height: 1.2;
margin: 0 0 var(--spacing-3);
line-height: 1.2;
}
:where(h1) {
font-size: 1.875rem;
font-size: 1.875rem;
}
:where(h2) {
font-size: 1.5rem;
font-size: 1.5rem;
}
:where(h3) {
font-size: 1.25rem;
font-size: 1.25rem;
}
:where(ul, ol) {
padding-left: 1.5rem;
padding-left: 1.5rem;
}
:where(hr) {
border: 0;
border-top: 1px solid var(--color-border);
margin: var(--spacing-6) 0;
border: 0;
border-top: 1px solid var(--color-border);
margin: var(--spacing-6) 0;
}
:where(code, kbd, samp) {
font-family: var(--font-mono);
font-size: 0.95em;
font-family: var(--font-mono);
font-size: 0.95em;
}
:where(code) {
background: var(--color-code-bg);
border-radius: 0.375rem;
padding: 0.1rem 0.35rem;
background: var(--color-code-bg);
border-radius: 0.375rem;
padding: 0.1rem 0.35rem;
}
:where(pre) {
overflow-x: auto;
padding: var(--spacing-3);
background: var(--color-code-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-2);
overflow-x: auto;
padding: var(--spacing-3);
background: var(--color-code-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-2);
}
:where(pre code) {
background: transparent;
padding: 0;
background: transparent;
padding: 0;
}
:where(form) {
display: grid;
gap: var(--spacing-4);
display: grid;
gap: var(--spacing-4);
}
:where(label) {
display: grid;
gap: var(--spacing-2);
font-weight: 600;
display: grid;
gap: var(--spacing-2);
font-weight: 600;
}
:where(input, button, textarea, select) {
font: inherit;
font: inherit;
}
:where(input:not([type=checkbox]):not([type=radio]), textarea, select) {
width: 100%;
padding: var(--spacing-2) var(--spacing-3);
background: var(--color-surface);
color: var(--color-fg);
border: 1px solid var(--color-border);
border-radius: var(--radius-2);
box-shadow: inset 0 1px 2px rgb(15 23 42 / 0.04);
:where(input:not([type='checkbox']):not([type='radio']), textarea, select) {
width: 100%;
padding: var(--spacing-2) var(--spacing-3);
background: var(--color-surface);
color: var(--color-fg);
border: 1px solid var(--color-border);
border-radius: var(--radius-2);
box-shadow: inset 0 1px 2px rgb(15 23 42 / 0.04);
}
:where(textarea) {
min-height: 7rem;
resize: vertical;
min-height: 7rem;
resize: vertical;
}
:where(input[type=checkbox], input[type=radio]) {
accent-color: var(--color-accent);
:where(input[type='checkbox'], input[type='radio']) {
accent-color: var(--color-accent);
}
:where(button) {
width: fit-content;
padding: var(--spacing-2) var(--spacing-4);
background: var(--color-accent);
color: var(--color-accent-contrast);
border: 1px solid transparent;
border-radius: var(--radius-2);
box-shadow: var(--shadow-1);
cursor: pointer;
width: fit-content;
padding: var(--spacing-2) var(--spacing-4);
background: var(--color-accent);
color: var(--color-accent-contrast);
border: 1px solid transparent;
border-radius: var(--radius-2);
box-shadow: var(--shadow-1);
cursor: pointer;
}
:where(button:disabled, input:disabled, textarea:disabled, select:disabled) {
opacity: 0.7;
cursor: not-allowed;
}
:where(button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible) {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
opacity: 0.7;
cursor: not-allowed;
}
:where(
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible
) {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
:where(table) {
width: 100%;
border-collapse: collapse;
width: 100%;
border-collapse: collapse;
}
:where(th, td) {
padding: var(--spacing-2) var(--spacing-3);
border: 1px solid var(--color-border);
text-align: left;
padding: var(--spacing-2) var(--spacing-3);
border: 1px solid var(--color-border);
text-align: left;
}
:where(th) {
background: var(--color-surface);
background: var(--color-surface);
}
:where(blockquote) {
margin-left: 0;
padding-left: var(--spacing-4);
border-left: 3px solid var(--color-border);
color: var(--color-muted);
margin-left: 0;
padding-left: var(--spacing-4);
border-left: 3px solid var(--color-border);
color: var(--color-muted);
}
:where([data-generated-ui-root]) {
min-height: 100%;
min-height: 100%;
}
9 changes: 7 additions & 2 deletions packages/worker/src/mcp/capabilities/secrets/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ import { defineDomain } from '#mcp/capabilities/define-domain.ts'
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,
],
})
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const secretDeleteCapability = defineDomainCapability(
{
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const secretListCapability = defineDomainCapability(
{
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,
Expand Down
Loading
Loading