Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,36 @@ function resetMocks() {

function createContext(userId = 'user-1') {
return {
env: { APP_DB: {} } as Env,
env: {
APP_DB: {},
BUNDLE_ARTIFACTS_KV: {
async get(_key: string, type?: 'text' | 'json') {
if (type !== 'json') return null
return {
version: 1,
sourceId: 'source-1',
repoId: 'package-package-1',
entityKind: 'package',
entityId: 'package-1',
publishedCommit: 'commit-1',
manifestPath: 'package.json',
sourceRoot: '/',
files: {
'package.json': JSON.stringify({
name: '@kentcdodds/unleashed-wifi',
exports: { '.': './src/index.ts' },
kody: {
id: 'unleashed-wifi',
description: 'Unleashed WiFi controls.',
},
}),
'src/index.ts': 'export default async function main() {}\n',
},
createdAt: '2026-05-04T00:00:00.000Z',
}
},
},
} as unknown as Env,
callerContext: {
baseUrl: 'https://heykody.dev',
user: {
Expand Down
15 changes: 14 additions & 1 deletion packages/worker/src/mcp/capabilities/packages/get-git-remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import {
parseArtifactTokenSecret,
resolveArtifactSourceRepo,
} from '#worker/repo/artifacts.ts'
import {
assertRestorablePackageSourceSnapshot,
withProductionPackageSourceSafetyPolicy,
} from '#worker/repo/source-safety-policy.ts'
import { resolveOwnedPackageSource } from './resolve-package-source.ts'

const getGitRemoteInputSchema = z.object({
Expand Down Expand Up @@ -39,8 +43,9 @@ export const getGitRemoteCapability = defineDomainCapability(
capabilityDomainNames.packages,
{
name: 'package_get_git_remote',
description:
description: withProductionPackageSourceSafetyPolicy(
'Mint a short-lived Cloudflare Artifacts git remote for coding agents with local filesystem/git access to clone a saved package into a temporary directory, edit normally, push, and publish with package_publish_external_push.',
),
keywords: [
'package',
'git',
Expand All @@ -66,6 +71,14 @@ export const getGitRemoteCapability = defineDomainCapability(
kody_id: args.kody_id,
},
})
if (args.scope === 'write') {
await assertRestorablePackageSourceSnapshot({
env: ctx.env,
userId: user.userId,
source,
operation: 'package_get_git_remote write access',
})
}
const repo = await resolveArtifactSourceRepo(ctx.env, source.repo_id)
const info = await repo.info()
if (!info?.remote) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ test('surfaces not-fast-forward refusal without force', async () => {
)
})

test('passes allow_force through to the publish pipeline', async () => {
test('passes allow_force and destructive confirmation through to the publish pipeline', async () => {
mockModule.resolveArtifactSourceHead.mockResolvedValue({
branch: 'main',
commit: 'commit-rewrite',
Expand All @@ -402,13 +402,18 @@ test('passes allow_force through to the publish pipeline', async () => {
})

await publishExternalPushCapability.handler(
{ package_id: 'package-1', allow_force: true },
{
package_id: 'package-1',
allow_force: true,
confirm_destructive_overwrite: true,
},
createContext(),
)

expect(mockModule.publishFromExternalRef).toHaveBeenCalledWith(
expect.objectContaining({
allowForce: true,
destructiveOverwriteConfirmed: true,
}),
)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,21 @@ import { getMcpUserPackageScope } from '#worker/package-registry/user-scope.ts'
import { repoSessionRpc } from '#worker/repo/repo-session-do.ts'
import { resolveArtifactSourceHead } from '#worker/repo/artifacts.ts'
import { rebuildPublishedPackageArtifactsViaRepoSession } from '#mcp/capabilities/repo/package-artifact-rebuild.ts'
import {
destructiveOverwriteConfirmationDescription,
withProductionPackageSourceSafetyPolicy,
} from '#worker/repo/source-safety-policy.ts'
import { resolveOwnedPackageSource } from './resolve-package-source.ts'

const inputSchema = z.object({
package_id: z.string().min(1).optional(),
kody_id: z.string().min(1).optional(),
allow_force: z.boolean().optional().default(false),
confirm_destructive_overwrite: z
.boolean()
.optional()
.default(false)
.describe(destructiveOverwriteConfirmationDescription),
})

const externalPublishRetryDelaysMs = [100, 500] as const
Expand Down Expand Up @@ -231,12 +240,13 @@ export const publishExternalPushCapability = defineDomainCapability(
capabilityDomainNames.packages,
{
name: 'package_publish_external_push',
description:
description: withProductionPackageSourceSafetyPolicy(
'Publish the current Artifacts git HEAD for a saved package after a package_get_git_remote clone/edit/push workflow and server-side checks pass. Published and already_published responses include bounded static dependent metadata so agents can decide whether stale kody:@ bundled snapshots need inspection or dependent republish; Kody does not republish dependents automatically.',
),
keywords: ['package', 'publish', 'git', 'artifacts', 'external', 'push'],
readOnly: false,
idempotent: true,
destructive: false,
destructive: true,
inputSchema,
outputSchema,
async handler(args, ctx) {
Expand Down Expand Up @@ -283,6 +293,7 @@ export const publishExternalPushCapability = defineDomainCapability(
newCommit,
expectedHead: newCommit,
allowForce: args.allow_force,
destructiveOverwriteConfirmed: args.confirm_destructive_overwrite,
baseUrl: ctx.callerContext.baseUrl,
rebuildPackageArtifacts: false,
expectedPackageScope,
Expand Down
25 changes: 23 additions & 2 deletions packages/worker/src/mcp/capabilities/packages/save-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { capabilityDomainNames } from '#mcp/capabilities/domain-metadata.ts'
import { requireMcpUser } from '#mcp/capabilities/meta/require-user.ts'
import { ensureEntitySource } from '#worker/repo/source-service.ts'
import { syncArtifactSourceSnapshot } from '#worker/repo/source-sync.ts'
import {
assertPackageSourceOverwriteAllowed,
destructiveOverwriteConfirmationDescription,
withProductionPackageSourceSafetyPolicy,
} from '#worker/repo/source-safety-policy.ts'
import {
getSavedPackageById,
getSavedPackageByKodyId,
Expand Down Expand Up @@ -31,6 +36,11 @@ const inputSchema = z
.describe(
'Full package file set to write. Must include package.json at the repo root.',
),
confirm_destructive_overwrite: z
.boolean()
.optional()
.default(false)
.describe(destructiveOverwriteConfirmationDescription),
})
.superRefine((value, ctx) => {
const hasPackageJson = value.files.some(
Expand Down Expand Up @@ -58,8 +68,9 @@ export const savePackageCapability = defineDomainCapability(
capabilityDomainNames.packages,
{
name: 'package_save',
description:
description: withProductionPackageSourceSafetyPolicy(
'Create or replace a saved package by writing a complete package file set. Prefer package_get_git_remote or repo sessions for iterative edits. The package repo is rooted at package.json and package.json#kody is the Kody-specific metadata block. When creating or materially changing a package, include or maintain README.md with a concise Intent section that captures the user-defined goal; ask the user if intent is unclear.',
),
keywords: [
'package',
'save',
Expand All @@ -72,7 +83,7 @@ export const savePackageCapability = defineDomainCapability(
],
readOnly: false,
idempotent: false,
destructive: false,
destructive: true,
inputSchema,
outputSchema: packageSummarySchema,
async handler(args, ctx) {
Expand Down Expand Up @@ -112,13 +123,23 @@ export const savePackageCapability = defineDomainCapability(
manifestPath: 'package.json',
requirePersistence: true,
})
if (existing) {
await assertPackageSourceOverwriteAllowed({
env: ctx.env,
userId: user.userId,
source: ensuredSource,
operation: 'package_save',
confirmed: args.confirm_destructive_overwrite,
})
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
}
await syncArtifactSourceSnapshot({
env: ctx.env,
userId: user.userId,
baseUrl: ctx.callerContext.baseUrl,
sourceId: ensuredSource.id,
bootstrapAccess: ensuredSource.bootstrapAccess ?? null,
files,
destructiveOverwriteConfirmed: args.confirm_destructive_overwrite,
})
if (!existing) {
const now = new Date().toISOString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { capabilityDomainNames } from '#mcp/capabilities/domain-metadata.ts'
import { requireMcpUser } from '#mcp/capabilities/meta/require-user.ts'
import { repoSessionRpc } from '#worker/repo/repo-session-do.ts'
import { getMcpUserPackageScope } from '#worker/package-registry/user-scope.ts'
import { withProductionPackageSourceSafetyPolicy } from '#worker/repo/source-safety-policy.ts'
import {
repoPublishSessionOutputSchema,
repoSessionIdSchema,
Expand All @@ -13,12 +14,13 @@ export const repoPublishSessionCapability = defineDomainCapability(
capabilityDomainNames.repo,
{
name: 'repo_publish_session',
description:
description: withProductionPackageSourceSafetyPolicy(
'Publish an active repo session back to the source repo after checks pass on the current tree and the base commit is still current.',
),
keywords: ['repo', 'publish', 'session', 'checks', 'artifact'],
readOnly: false,
idempotent: false,
destructive: false,
destructive: true,
inputSchema: repoSessionIdSchema,
outputSchema: repoPublishSessionOutputSchema,
async handler(args, ctx) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { withProductionPackageSourceSafetyPolicy } from '#worker/repo/source-safety-policy.ts'

export const repoRunCommandsSupportedForms = [
'git status [--short]',
'git diff',
Expand All @@ -14,15 +16,18 @@ export const repoRunCommandsSupportedForms = [
'git remote, git remote -v, git remote add <name> <url>, git remote remove <name>',
] as const

export const repoRunCommandsCapabilityDescription = [
'Run a parsed git-only workflow in a Kody-managed repo session for MCP-native edits.',
'Use this for tool-only agents without local filesystem/git access; coding agents that can clone locally should prefer `package_get_git_remote` (saved packages only) plus `package_publish_external_push`.',
'Commands are newline-separated and parsed, not shell-executed.',
'Only supported git command forms are accepted; unsupported syntax returns line-specific parse errors.',
'`git apply` requires heredoc form and a standard unified diff body with `--- a/<path>` / `+++ b/<path>` headers and `@@ ... @@` hunk markers; one heredoc may contain multiple file patches stacked back-to-back, and `/dev/null` on either side creates or deletes a file.',
'For exact whole-file replacements (especially single-file job sources, freshly generated files, or any edit where you have the full new content), prefer `repo_write_file` over `git apply`; it avoids unified-diff context drift entirely.',
'For one-file edits to non-package repo-backed scheduled job source, prefer `job_update` with a replacement `code` string; it republishes the job module without needing a repo session.',
].join(' ')
export const repoRunCommandsCapabilityDescription =
withProductionPackageSourceSafetyPolicy(
[
'Run a parsed git-only workflow in a Kody-managed repo session for MCP-native edits.',
'Use this for tool-only agents without local filesystem/git access; coding agents that can clone locally should prefer `package_get_git_remote` (saved packages only) plus `package_publish_external_push`.',
'Commands are newline-separated and parsed, not shell-executed.',
'Only supported git command forms are accepted; unsupported syntax returns line-specific parse errors.',
'`git apply` requires heredoc form and a standard unified diff body with `--- a/<path>` / `+++ b/<path>` headers and `@@ ... @@` hunk markers; one heredoc may contain multiple file patches stacked back-to-back, and `/dev/null` on either side creates or deletes a file.',
'For exact whole-file replacements (especially single-file job sources, freshly generated files, or any edit where you have the full new content), prefer `repo_write_file` over `git apply`; it avoids unified-diff context drift entirely.',
'For one-file edits to non-package repo-backed scheduled job source, prefer `job_update` with a replacement `code` string; it republishes the job module without needing a repo session.',
].join(' '),
)

export const repoRunCommandsUnsupportedSyntaxDescription = [
'Only git commands are accepted.',
Expand Down
15 changes: 15 additions & 0 deletions packages/worker/src/mcp/observability.workers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,20 @@ test('package_save capability logs success for valid invocation', async () => {
const originalInfo = console.info
const payloads: Array<string> = []
resetRepoPersistenceMocks()
repoMockModule.ensureEntitySource.mockResolvedValue({
id: 'package-package-1',
user_id: 'user-1',
entity_kind: 'package',
entity_id: 'package-1',
repo_id: 'package-package-1',
published_commit: 'published-commit-1',
indexed_commit: 'published-commit-1',
manifest_path: 'package.json',
source_root: '/',
created_at: '2026-04-13T00:00:00.000Z',
updated_at: '2026-04-13T00:00:00.000Z',
bootstrapAccess: null,
})
console.info = ((tag: unknown, json?: unknown) => {
if (tag === 'mcp-event' && typeof json === 'string') {
payloads.push(json)
Expand All @@ -253,6 +267,7 @@ test('package_save capability logs success for valid invocation', async () => {
const handler = capabilityMap['package_save'].handler
const result = await handler(
{
confirm_destructive_overwrite: true,
files: [
{
path: 'package.json',
Expand Down
Loading