Skip to content
Closed
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
1 change: 1 addition & 0 deletions docs/use/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Read in order for a full tour, or jump to a topic.
- [Raw MCP content blocks](./raw-content-blocks.md)
- [Secrets, values, and host approval](./secrets-and-values.md)
- [Mutating actions and confirmations](./mutating-actions.md)
- [Privacy](./privacy.md) — what Kody stores and what deployment admins can see
- [Troubleshooting](./troubleshooting.md)
- [Memory and conversation context](./memory.md)
- [Community Project mark](./community-project-mark.md) — logo for unofficial
Expand Down
45 changes: 45 additions & 0 deletions docs/use/privacy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Privacy

How Kody stores your data and what a deployment admin can see.

## What Kody stores per account

Each signed-in user gets a fully isolated assistant. Kody stores account profile
information (email and username), secrets, values, memories, packages and their
source, jobs, email inboxes and messages, chat threads, durable storage, remote
connector configuration, OAuth grants, and package invocation tokens. All of
this is scoped to your account and is not shared with other users.

## What a deployment admin can see

On shared deployments, operators can grant an admin role for account
administration. Admins see account metadata only: user id, username, email,
created and updated timestamps, and role assignments. The admin UI lists users
and roles; it does not expose user content.

## What an admin can never see

The admin role is not a data-access role. Admins cannot see:

- Secret values or secret metadata (names, scopes, allowlists)
- Package invocation tokens
- Values
- Memories
- Packages and their source
- Jobs
- Email inboxes and messages
- Chat threads
- Durable storage contents
- Remote connector configuration
- OAuth grants

None of this appears in any admin endpoint, page, or API payload — not even in
redacted or count form.

## Deployment operator access

Role-based access controls the application surface. Whoever operates the
deployment — holding the Cloudflare account, D1 database access, and
`SECRET_STORE_KEY` — sits outside any application-level control, exactly as
before admin roles existed. The admin role grants no infrastructure access, and
infrastructure access requires no admin role.
10 changes: 10 additions & 0 deletions e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,14 @@ test('smoke test covers shell, auth redirect, and login', async ({ page }) => {
await expect(
page.getByRole('link', { name: 'Secrets', exact: true }),
).toBeVisible()

await page.context().clearCookies()
await page.goto('/privacy')
await expect(page.getByRole('heading', { name: 'Privacy' })).toBeVisible()
await expect(
page.getByRole('heading', { name: 'What a deployment admin can see' }),
).toBeVisible()
await expect(
page.getByRole('heading', { name: 'What an admin can never see' }),
).toBeVisible()
})
7 changes: 7 additions & 0 deletions packages/worker/client/routes/account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
fieldLabelCss,
getPrimaryButtonCss,
inputCss,
mutedLinkCss,
primaryLinkCss,
} from '#client/styles/style-primitives.ts'
import {
Expand Down Expand Up @@ -276,6 +277,12 @@ export function AccountRoute(handle: Handle) {
</section>
</>
) : null}

<p mix={css({ margin: 0 })}>
<a href="/privacy" mix={css(mutedLinkCss)}>
Privacy
</a>
</p>
</section>
)
}
Expand Down
2 changes: 2 additions & 0 deletions packages/worker/client/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AccountSecretsRoute } from './account-secrets.tsx'
import { ConnectOauthRoute } from './connect-oauth.tsx'
import { HomeRoute } from './home.tsx'
import { LoginRoute } from './login.tsx'
import { PrivacyRoute } from './privacy.tsx'
import { OAuthAuthorizeRoute } from './oauth-authorize.tsx'
import { OAuthCallbackRoute } from './oauth-callback.tsx'
import { ResetPasswordRoute } from './reset-password.tsx'
Expand All @@ -30,6 +31,7 @@ export const clientRoutes = {
'/account/secrets/app/:appId/:secretName': <AccountSecretsRoute />,
'/account/secrets/session/:sessionId/:secretName': <AccountSecretsRoute />,
'/login': <LoginRoute />,
'/privacy': <PrivacyRoute />,
'/signup': <LoginRoute />,
'/reset-password': <ResetPasswordRoute />,
'/connect/oauth': <ConnectOauthRoute />,
Expand Down
3 changes: 3 additions & 0 deletions packages/worker/client/routes/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,9 @@ export function LoginRoute(handle: Handle) {
Forgot password?
</a>
) : null}
<a href="/privacy" mix={css(mutedLinkCss)}>
Privacy
</a>
<a href="/" mix={css(mutedLinkCss)}>
Back home
</a>
Expand Down
105 changes: 105 additions & 0 deletions packages/worker/client/routes/privacy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { type Handle, css } from 'remix/ui'
import { colors, spacing, typography } from '#client/styles/tokens.ts'
import {
cardCss,
cardTitleCss,
descriptionCss,
mutedLinkCss,
pageDescriptionCss,
pageHeaderCss,
pageTitleCss,
stackedPageCss,
} from '#client/styles/style-primitives.ts'

export function PrivacyRoute(_handle: Handle) {
return () => (
<section mix={css(pageCss)}>
<header mix={css(pageHeaderCss)}>
<h1 mix={css(pageTitleCss)}>Privacy</h1>
<p mix={css(pageDescriptionCss)}>
How Kody stores your data and what a deployment admin can see.
</p>
</header>

<section mix={css(cardCss)}>
<h2 mix={css(cardTitleCss)}>What Kody stores per account</h2>
<p mix={css(descriptionCss)}>
Each signed-in user gets a fully isolated assistant. Kody stores
account profile information (email and username), secrets, values,
memories, packages and their source, jobs, email inboxes and messages,
chat threads, durable storage, remote connector configuration, OAuth
grants, and package invocation tokens. All of this is scoped to your
account and is not shared with other users.
</p>
</section>

<section mix={css(cardCss)}>
<h2 mix={css(cardTitleCss)}>What a deployment admin can see</h2>
<p mix={css(descriptionCss)}>
On shared deployments, operators can grant an admin role for account
administration. Admins see account metadata only: user id, username,
email, created and updated timestamps, and role assignments. The admin
UI lists users and roles; it does not expose user content.
</p>
</section>

<section mix={css(cardCss)}>
<h2 mix={css(cardTitleCss)}>What an admin can never see</h2>
<p mix={css(descriptionCss)}>
The admin role is not a data-access role. Admins cannot see:
</p>
<ul mix={css(listCss)}>
<li>Secret values or secret metadata (names, scopes, allowlists)</li>
<li>Package invocation tokens</li>
<li>Values</li>
<li>Memories</li>
<li>Packages and their source</li>
<li>Jobs</li>
<li>Email inboxes and messages</li>
<li>Chat threads</li>
<li>Durable storage contents</li>
<li>Remote connector configuration</li>
<li>OAuth grants</li>
</ul>
<p mix={css(descriptionCss)}>
None of this appears in any admin endpoint, page, or API payload — not
even in redacted or count form.
</p>
</section>

<section mix={css(cardCss)}>
<h2 mix={css(cardTitleCss)}>Deployment operator access</h2>
<p mix={css(descriptionCss)}>
Role-based access controls the application surface. Whoever operates
the deployment — holding the Cloudflare account, D1 database access,
and <code>SECRET_STORE_KEY</code> — sits outside any application-level
control, exactly as before admin roles existed. The admin role grants
no infrastructure access, and infrastructure access requires no admin
role.
</p>
</section>

<p mix={css({ margin: 0 })}>
<a href="/" mix={css(mutedLinkCss)}>
Back home
</a>
</p>
</section>
)
}

const pageCss = {
...stackedPageCss,
maxWidth: '42rem',
margin: '0 auto',
}

const listCss = {
margin: `${spacing.sm} 0 0`,
paddingLeft: spacing.lg,
color: colors.text,
display: 'grid',
gap: spacing.xs,
fontSize: typography.fontSize.sm,
lineHeight: 1.6,
}
11 changes: 11 additions & 0 deletions packages/worker/src/app/handlers/privacy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { type Action } from 'remix/router'
import { Layout } from '#app/layout.ts'
import { render } from '#app/render.ts'
import { type routes } from '#app/routes.ts'

export const privacy = {
middleware: [],
async handler() {
return render(Layout({}))
},
} satisfies Action<typeof routes.privacy>
2 changes: 2 additions & 0 deletions packages/worker/src/app/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { createConnectOauthHandler } from '#app/handlers/connect-oauth.ts'
import { createHealthHandler } from '#app/handlers/health.ts'
import { home } from '#app/handlers/home.ts'
import { login } from '#app/handlers/login.ts'
import { privacy } from '#app/handlers/privacy.ts'
import { logout } from '#app/handlers/logout.ts'
import {
createPasswordResetConfirmHandler,
Expand All @@ -48,6 +49,7 @@ export function createAppRouter(appEnv: AppEnv) {
home,
health: createHealthHandler(appEnv),
login,
privacy,
signup,
account,
accountDelete: createAccountDeleteHandler(appEnv as unknown as Env),
Expand Down
1 change: 1 addition & 0 deletions packages/worker/src/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const routes = route({
accountProfileApiPost: post('/account/profile.json'),
health: '/health',
login: '/login',
privacy: '/privacy',
signup: '/signup',
account: '/account',
accountDelete: post('/account/delete'),
Expand Down