diff --git a/packages/worker/migrations/0039-source-rescue-events.sql b/packages/worker/migrations/0039-source-rescue-events.sql deleted file mode 100644 index 39627d65e8..0000000000 --- a/packages/worker/migrations/0039-source-rescue-events.sql +++ /dev/null @@ -1,30 +0,0 @@ -CREATE TABLE IF NOT EXISTS source_rescue_events ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - source_id TEXT NOT NULL, - entity_kind TEXT NOT NULL CHECK (entity_kind IN ('package')), - entity_id TEXT NOT NULL, - package_id TEXT, - kody_id TEXT, - source_repo_id TEXT NOT NULL, - recovery_kind TEXT NOT NULL CHECK (recovery_kind IN ('repo_session')), - recovered_session_id TEXT, - recovered_repo_id TEXT, - recovered_repo_name TEXT, - recovered_commit TEXT, - prior_published_commit TEXT, - source_head_before TEXT, - source_head_after TEXT, - operator_user_id TEXT NOT NULL, - operator_email TEXT, - validation_status TEXT NOT NULL CHECK (validation_status IN ('passed', 'failed')), - validation_json TEXT NOT NULL CHECK (json_valid(validation_json)), - metadata_json TEXT CHECK (metadata_json IS NULL OR json_valid(metadata_json)), - created_at TEXT NOT NULL -); - -CREATE INDEX IF NOT EXISTS idx_source_rescue_events_user_source -ON source_rescue_events(user_id, source_id, created_at); - -CREATE INDEX IF NOT EXISTS idx_source_rescue_events_package -ON source_rescue_events(user_id, package_id, created_at); diff --git a/packages/worker/migrations/0040-drop-source-rescue-events.sql b/packages/worker/migrations/0040-drop-source-rescue-events.sql new file mode 100644 index 0000000000..c81e6cafef --- /dev/null +++ b/packages/worker/migrations/0040-drop-source-rescue-events.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS source_rescue_events; diff --git a/packages/worker/src/mcp/capabilities/packages/domain.ts b/packages/worker/src/mcp/capabilities/packages/domain.ts index b79d971ea4..a7f6cc3b5c 100644 --- a/packages/worker/src/mcp/capabilities/packages/domain.ts +++ b/packages/worker/src/mcp/capabilities/packages/domain.ts @@ -9,7 +9,6 @@ import { packageDebugGetRunCapability } from './package-debug-get-run.ts' import { packageDebugListRunsCapability } from './package-debug-list-runs.ts' import { publishExternalPushCapability } from './publish-external-push.ts' import { savePackageCapability } from './save-package.ts' -import { sourceRescueCapability } from './source-rescue.ts' export const packagesDomain = defineDomain({ name: capabilityDomainNames.packages, @@ -35,7 +34,6 @@ export const packagesDomain = defineDomain({ packageDebugListRunsCapability, packageDebugGetRunCapability, publishExternalPushCapability, - sourceRescueCapability, deletePackageCapability, ], }) diff --git a/packages/worker/src/mcp/capabilities/packages/source-rescue.node.test.ts b/packages/worker/src/mcp/capabilities/packages/source-rescue.node.test.ts deleted file mode 100644 index 8574a7fbbc..0000000000 --- a/packages/worker/src/mcp/capabilities/packages/source-rescue.node.test.ts +++ /dev/null @@ -1,366 +0,0 @@ -import { expect, test, vi } from 'vitest' - -const mockModule = vi.hoisted(() => ({ - resolveOwnedPackageSource: vi.fn(), - resolveExistingArtifactSourceRepo: vi.fn(), - resolveArtifactDefaultBranchHead: vi.fn(), - listRepoSessionsBySource: vi.fn(), - listPublishedBundleArtifactsBySourceId: vi.fn(), - loadPublishedSourceSnapshot: vi.fn(), - repoSessionRpc: vi.fn(), - getMcpUserPackageScope: vi.fn(async () => '@kody'), -})) - -vi.mock('./resolve-package-source.ts', () => ({ - resolveOwnedPackageSource: (...args: Array) => - mockModule.resolveOwnedPackageSource(...args), -})) - -vi.mock('#worker/repo/artifacts.ts', () => ({ - resolveExistingArtifactSourceRepo: (...args: Array) => - mockModule.resolveExistingArtifactSourceRepo(...args), - resolveArtifactDefaultBranchHead: (...args: Array) => - mockModule.resolveArtifactDefaultBranchHead(...args), -})) - -vi.mock('#worker/repo/repo-sessions.ts', () => ({ - listRepoSessionsBySource: (...args: Array) => - mockModule.listRepoSessionsBySource(...args), -})) - -vi.mock('#worker/repo/published-bundle-artifacts-repo.ts', () => ({ - listPublishedBundleArtifactsBySourceId: (...args: Array) => - mockModule.listPublishedBundleArtifactsBySourceId(...args), -})) - -vi.mock('#worker/package-runtime/published-runtime-artifacts.ts', () => ({ - loadPublishedSourceSnapshot: (...args: Array) => - mockModule.loadPublishedSourceSnapshot(...args), -})) - -vi.mock('#worker/repo/repo-session-do.ts', () => ({ - repoSessionRpc: (...args: Array) => - mockModule.repoSessionRpc(...args), -})) - -vi.mock('#worker/package-registry/user-scope.ts', () => ({ - getMcpUserPackageScope: (...args: Array) => - mockModule.getMcpUserPackageScope(...args), -})) - -const { sourceRescueCapability } = await import('./source-rescue.ts') - -function resetMocks() { - for (const fn of Object.values(mockModule)) { - fn.mockReset() - } - mockModule.getMcpUserPackageScope.mockResolvedValue('@kody') -} - -function createContext() { - return { - env: { - APP_DB: {}, - } as unknown as Env, - callerContext: { - baseUrl: 'https://heykody.dev', - user: { - userId: 'user-1', - email: 'user@example.com', - displayName: 'User', - }, - remoteConnectors: null, - storageContext: null, - repoContext: null, - }, - } -} - -function mockPackageSource() { - mockModule.resolveOwnedPackageSource.mockResolvedValue({ - packageId: 'package-1', - kodyId: 'demo', - name: '@kody/demo', - source: { - id: 'source-1', - user_id: 'user-1', - entity_kind: 'package', - entity_id: 'package-1', - repo_id: 'repo-1', - published_commit: 'commit-1', - indexed_commit: 'commit-1', - manifest_path: 'package.json', - source_root: '/', - last_external_check_at: null, - created_at: '2026-06-06T00:00:00.000Z', - updated_at: '2026-06-06T00:00:00.000Z', - }, - }) - mockModule.resolveExistingArtifactSourceRepo.mockResolvedValue({ - info: vi.fn(), - createToken: vi.fn(), - }) - mockModule.resolveArtifactDefaultBranchHead.mockResolvedValue(null) - mockModule.loadPublishedSourceSnapshot.mockResolvedValue({ - files: { - 'package.json': '{"name":"@kody/demo"}', - 'index.ts': 'export const ok = true\n', - }, - }) - mockModule.listPublishedBundleArtifactsBySourceId.mockResolvedValue([ - { - publishedCommit: 'commit-1', - artifactKind: 'module', - artifactName: null, - entryPoint: 'index.ts', - kvKey: 'bundle-artifact:v1:source-1:commit-1:module:_:index.ts', - }, - ]) -} - -function matchingSession() { - return { - id: 'session-1', - user_id: 'user-1', - source_id: 'source-1', - session_repo_id: 'session-repo-1', - session_repo_name: 'repo-1-session-1', - session_repo_namespace: 'default', - base_commit: 'commit-1', - source_root: '/', - conversation_id: null, - status: 'active', - expires_at: null, - last_checkpoint_at: '2026-06-06T00:00:00.000Z', - last_checkpoint_commit: 'commit-1', - last_check_run_id: null, - last_check_tree_hash: null, - created_at: '2026-06-06T00:00:00.000Z', - updated_at: '2026-06-06T00:00:00.000Z', - } -} - -test('package_source_rescue inspect reports verified repo session candidates and bundle evidence', async () => { - resetMocks() - mockPackageSource() - mockModule.listRepoSessionsBySource.mockResolvedValue([matchingSession()]) - - const result = await sourceRescueCapability.handler( - { package_id: 'package-1' }, - createContext(), - ) - - expect(result.status).toBe('recoverable') - expect(result.repo_session_candidates).toEqual([ - expect.objectContaining({ - session_id: 'session-1', - recovered_commit: 'commit-1', - matches_published_commit: true, - }), - ]) - expect(result.bundle_artifact_evidence).toEqual( - expect.objectContaining({ - count_at_published_commit: 1, - note: expect.stringContaining('evidence only'), - }), - ) -}) - -test('package_source_rescue inspect reports the published matching commit as the recovery commit', async () => { - resetMocks() - mockPackageSource() - mockModule.listRepoSessionsBySource.mockResolvedValue([ - { - ...matchingSession(), - base_commit: 'commit-1', - last_checkpoint_commit: 'commit-different', - }, - ]) - - const result = await sourceRescueCapability.handler( - { package_id: 'package-1' }, - createContext(), - ) - - expect(result.status).toBe('recoverable') - expect(result.repo_session_candidates).toEqual([ - expect.objectContaining({ - session_id: 'session-1', - recovered_commit: 'commit-1', - matches_published_commit: true, - }), - ]) -}) - -test('package_source_rescue inspect blocks recovery when only bundle artifacts exist', async () => { - resetMocks() - mockPackageSource() - mockModule.listRepoSessionsBySource.mockResolvedValue([]) - - const result = await sourceRescueCapability.handler( - { package_id: 'package-1' }, - createContext(), - ) - - expect(result.status).toBe('blocked') - expect(result.repo_session_candidates).toEqual([]) - expect(result.recommended_next_action).toContain( - 'Do not use package_save or bundle artifacts to recreate source', - ) -}) - -test('package_source_rescue recover requires confirmation and delegates verified recovery', async () => { - resetMocks() - mockPackageSource() - const session = matchingSession() - mockModule.listRepoSessionsBySource.mockResolvedValue([session]) - const rpc = { - recoverSourceFromSessionCheckpoint: vi.fn(async () => ({ - status: 'recovered', - sourceId: 'source-1', - packageId: 'package-1', - recoveredCommit: 'commit-1', - priorPublishedCommit: 'commit-1', - sourceHeadBefore: null, - sourceHeadAfter: 'commit-1', - recoveredSessionId: 'session-1', - recoveredRepoId: 'session-repo-1', - recoveredRepoName: 'repo-1-session-1', - validation: { - ok: true, - runId: 'run-1', - treeHash: null, - checkedAt: '', - results: [], - }, - auditEventId: 'source-rescue-event-1', - message: 'Recovered.', - })), - } - mockModule.repoSessionRpc.mockReturnValue(rpc) - - await expect( - sourceRescueCapability.handler( - { - package_id: 'package-1', - action: 'recover', - recovery_source: { kind: 'repo_session', session_id: 'session-1' }, - }, - createContext(), - ), - ).rejects.toThrow('confirm_recovery: true') - expect(rpc.recoverSourceFromSessionCheckpoint).not.toHaveBeenCalled() - - const result = await sourceRescueCapability.handler( - { - package_id: 'package-1', - action: 'recover', - confirm_recovery: true, - recovery_source: { kind: 'repo_session', session_id: 'session-1' }, - }, - createContext(), - ) - - expect(result.status).toBe('recovered') - expect(result.source_head).toEqual( - expect.objectContaining({ - status: 'present', - commit: 'commit-1', - message: null, - }), - ) - expect(rpc.recoverSourceFromSessionCheckpoint).toHaveBeenCalledWith( - expect.objectContaining({ - sessionId: 'session-1', - sourceId: 'source-1', - userId: 'user-1', - packageId: 'package-1', - kodyId: 'demo', - recoveredCommit: 'commit-1', - operatorUserId: 'user-1', - operatorEmail: 'user@example.com', - expectedPackageScope: '@kody', - }), - ) -}) - -test('package_source_rescue recover defaults to the candidate commit that matches the published commit', async () => { - resetMocks() - mockPackageSource() - const session = { - ...matchingSession(), - base_commit: 'commit-1', - last_checkpoint_commit: 'commit-different', - } - mockModule.listRepoSessionsBySource.mockResolvedValue([session]) - const rpc = { - recoverSourceFromSessionCheckpoint: vi.fn(async () => ({ - status: 'recovered', - sourceId: 'source-1', - packageId: 'package-1', - recoveredCommit: 'commit-1', - priorPublishedCommit: 'commit-1', - sourceHeadBefore: null, - sourceHeadAfter: 'commit-1', - recoveredSessionId: 'session-1', - recoveredRepoId: 'session-repo-1', - recoveredRepoName: 'repo-1-session-1', - validation: { - ok: true, - runId: 'run-1', - treeHash: null, - checkedAt: '', - results: [], - }, - auditEventId: 'source-rescue-event-1', - message: 'Recovered.', - })), - } - mockModule.repoSessionRpc.mockReturnValue(rpc) - - await sourceRescueCapability.handler( - { - package_id: 'package-1', - action: 'recover', - confirm_recovery: true, - recovery_source: { kind: 'repo_session', session_id: 'session-1' }, - }, - createContext(), - ) - - expect(rpc.recoverSourceFromSessionCheckpoint).toHaveBeenCalledWith( - expect.objectContaining({ - recoveredCommit: 'commit-1', - }), - ) -}) - -test('package_source_rescue recover rejects sources that already have a default branch HEAD', async () => { - resetMocks() - mockPackageSource() - mockModule.resolveArtifactDefaultBranchHead.mockResolvedValue({ - remote: 'https://acct.artifacts.cloudflare.net/git/default/repo-1.git', - defaultBranch: 'main', - commit: 'commit-1', - }) - mockModule.listRepoSessionsBySource.mockResolvedValue([matchingSession()]) - const rpc = { - recoverSourceFromSessionCheckpoint: vi.fn(), - } - mockModule.repoSessionRpc.mockReturnValue(rpc) - - await expect( - sourceRescueCapability.handler( - { - package_id: 'package-1', - action: 'recover', - confirm_recovery: true, - recovery_source: { kind: 'repo_session', session_id: 'session-1' }, - }, - createContext(), - ), - ).rejects.toThrow( - 'package_source_rescue recover requires an existing source repo whose default branch has no HEAD; current status is "present".', - ) - expect(rpc.recoverSourceFromSessionCheckpoint).not.toHaveBeenCalled() -}) diff --git a/packages/worker/src/mcp/capabilities/packages/source-rescue.ts b/packages/worker/src/mcp/capabilities/packages/source-rescue.ts deleted file mode 100644 index 5b356d4215..0000000000 --- a/packages/worker/src/mcp/capabilities/packages/source-rescue.ts +++ /dev/null @@ -1,389 +0,0 @@ -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 { getMcpUserPackageScope } from '#worker/package-registry/user-scope.ts' -import { - resolveArtifactDefaultBranchHead, - resolveExistingArtifactSourceRepo, -} from '#worker/repo/artifacts.ts' -import { listPublishedBundleArtifactsBySourceId } from '#worker/repo/published-bundle-artifacts-repo.ts' -import { listRepoSessionsBySource } from '#worker/repo/repo-sessions.ts' -import { repoSessionRpc } from '#worker/repo/repo-session-do.ts' -import { loadPublishedSourceSnapshot } from '#worker/package-runtime/published-runtime-artifacts.ts' -import { type RepoSessionRow } from '#worker/repo/types.ts' -import { resolveOwnedPackageSource } from './resolve-package-source.ts' - -const recoverySourceSchema = z.object({ - kind: z.literal('repo_session'), - session_id: z - .string() - .min(1) - .describe( - 'Repo session id whose recorded checkpoint/base commit is trusted.', - ), - commit: z - .string() - .min(1) - .optional() - .describe( - 'Optional exact commit to recover. Defaults to the session checkpoint/base commit and must match entity_sources.published_commit.', - ), -}) - -const inputSchema = z.object({ - package_id: z.string().min(1).optional(), - kody_id: z.string().min(1).optional(), - action: z.enum(['inspect', 'recover']).default('inspect'), - recovery_source: recoverySourceSchema.optional(), - confirm_recovery: z - .boolean() - .default(false) - .describe( - 'Required for action: recover. Confirms the operator wants Kody to recreate the missing default-branch HEAD from the verified source candidate.', - ), - rebuild_package_artifacts: z - .boolean() - .default(true) - .describe('Rebuild package bundle artifacts after a successful recovery.'), -}) - -const outputSchema = z.toJSONSchema( - z.object({ - status: z.enum([ - 'healthy', - 'recoverable', - 'blocked', - 'recovered', - 'checks_failed', - ]), - package_id: z.string(), - kody_id: z.string(), - name: z.string(), - source_id: z.string(), - repo_id: z.string(), - published_commit: z.string().nullable(), - source_head: z - .object({ - status: z.enum(['present', 'missing', 'repo_missing', 'error']), - commit: z.string().nullable(), - default_branch: z.string().nullable(), - remote: z.string().nullable(), - message: z.string().nullable(), - }) - .optional(), - source_snapshot: z.object({ - found: z.boolean(), - file_count: z.number(), - manifest_present: z.boolean(), - message: z.string().nullable(), - }), - repo_session_candidates: z.array( - z.object({ - session_id: z.string(), - session_repo_id: z.string(), - session_repo_name: z.string(), - status: z.string(), - base_commit: z.string(), - last_checkpoint_commit: z.string().nullable(), - recovered_commit: z.string().nullable(), - matches_published_commit: z.boolean(), - created_at: z.string(), - updated_at: z.string(), - }), - ), - bundle_artifact_evidence: z.object({ - count_at_published_commit: z.number(), - items: z.array( - z.object({ - kind: z.string(), - artifact_name: z.string().nullable(), - entry_point: z.string(), - kv_key: z.string(), - }), - ), - note: z.string(), - }), - recovery: z.unknown().optional(), - recommended_next_action: z.string(), - }), -) as Record - -function buildCandidate( - session: RepoSessionRow, - publishedCommit: string | null, -) { - const recoveredCommit = - publishedCommit != null && - session.last_checkpoint_commit === publishedCommit - ? session.last_checkpoint_commit - : publishedCommit != null && session.base_commit === publishedCommit - ? session.base_commit - : null - return { - session_id: session.id, - session_repo_id: session.session_repo_id, - session_repo_name: session.session_repo_name, - status: session.status, - base_commit: session.base_commit, - last_checkpoint_commit: session.last_checkpoint_commit, - recovered_commit: recoveredCommit, - matches_published_commit: recoveredCommit != null, - created_at: session.created_at, - updated_at: session.updated_at, - } -} - -async function inspectSourceHead(env: Env, repoId: string) { - try { - const repo = await resolveExistingArtifactSourceRepo(env, repoId) - if (!repo) { - return { - status: 'repo_missing' as const, - commit: null, - default_branch: null, - remote: null, - message: `Artifact source repo "${repoId}" was not found.`, - } - } - const head = await resolveArtifactDefaultBranchHead({ repo }) - if (!head) { - return { - status: 'missing' as const, - commit: null, - default_branch: null, - remote: null, - message: `Artifact source repo "${repoId}" default branch has no HEAD.`, - } - } - return { - status: 'present' as const, - commit: head.commit, - default_branch: head.defaultBranch, - remote: head.remote, - message: null, - } - } catch (error) { - return { - status: 'error' as const, - commit: null, - default_branch: null, - remote: null, - message: error instanceof Error ? error.message : String(error), - } - } -} - -async function inspectPublishedSourceSnapshot(input: { - env: Env - userId: string - source: Parameters[0]['source'] -}) { - try { - const snapshot = await loadPublishedSourceSnapshot(input) - if (!snapshot) { - return { - found: false, - file_count: 0, - manifest_present: false, - message: 'No published source snapshot was found.', - } - } - const files = - typeof snapshot.files === 'object' && snapshot.files != null - ? snapshot.files - : {} - return { - found: true, - file_count: Object.keys(files).length, - manifest_present: typeof files[input.source.manifest_path] === 'string', - message: null, - } - } catch (error) { - return { - found: false, - file_count: 0, - manifest_present: false, - message: error instanceof Error ? error.message : String(error), - } - } -} - -export const sourceRescueCapability = defineDomainCapability( - capabilityDomainNames.packages, - { - name: 'package_source_rescue', - description: - 'Inspect and explicitly repair saved package source repos that fail normal package_get_git_remote/repo_open_session because the artifact source repo default branch has no HEAD. Recovery is non-destructive by default: it only restores an exact prior published commit from a verified repo session checkpoint/base commit, runs repo checks, records a source_rescue_events audit row, and leaves bundle artifacts as evidence rather than source.', - keywords: [ - 'package', - 'source', - 'rescue', - 'recover', - 'artifacts', - 'no head', - 'repo session', - ], - readOnly: false, - idempotent: false, - destructive: false, - inputSchema, - outputSchema, - async handler(args, ctx) { - const user = requireMcpUser(ctx.callerContext) - const { packageId, kodyId, name, source } = - await resolveOwnedPackageSource({ - db: ctx.env.APP_DB, - userId: user.userId, - args: { - package_id: args.package_id, - kody_id: args.kody_id, - }, - }) - const [sourceHead, sourceSnapshot, sessions, bundleArtifacts] = - await Promise.all([ - inspectSourceHead(ctx.env, source.repo_id), - inspectPublishedSourceSnapshot({ - env: ctx.env, - userId: user.userId, - source, - }), - listRepoSessionsBySource(ctx.env.APP_DB, { - userId: user.userId, - sourceId: source.id, - }), - listPublishedBundleArtifactsBySourceId( - ctx.env.APP_DB, - user.userId, - source.id, - ), - ]) - const candidates = sessions - .map((session) => buildCandidate(session, source.published_commit)) - .filter((session) => session.matches_published_commit) - const bundleEvidenceAtPublishedCommit = bundleArtifacts.filter( - (artifact) => artifact.publishedCommit === source.published_commit, - ) - const baseOutput = { - package_id: packageId, - kody_id: kodyId, - name, - source_id: source.id, - repo_id: source.repo_id, - published_commit: source.published_commit, - source_head: sourceHead, - source_snapshot: sourceSnapshot, - repo_session_candidates: candidates, - bundle_artifact_evidence: { - count_at_published_commit: bundleEvidenceAtPublishedCommit.length, - items: bundleEvidenceAtPublishedCommit - .slice(0, 20) - .map((artifact) => ({ - kind: artifact.artifactKind, - artifact_name: artifact.artifactName, - entry_point: artifact.entryPoint, - kv_key: artifact.kvKey, - })), - note: 'Bundle artifacts are evidence only; package_source_rescue never recreates source from bundle output.', - }, - } - if (sourceHead.status === 'present' && args.action === 'inspect') { - return { - status: 'healthy' as const, - ...baseOutput, - recommended_next_action: - 'Use normal package_get_git_remote, repo_open_session, or package_publish_external_push workflows.', - } - } - if (args.action === 'inspect') { - const recoverable = - sourceHead.status === 'missing' && candidates.length > 0 - return { - status: recoverable ? ('recoverable' as const) : ('blocked' as const), - ...baseOutput, - recommended_next_action: recoverable - ? 'Call package_source_rescue with action: recover, confirm_recovery: true, and one listed repo_session candidate.' - : 'No verified repo session checkpoint/base commit matches the prior published commit. Do not use package_save or bundle artifacts to recreate source; gather a verified source repo/session first.', - } - } - if (sourceHead.status !== 'missing') { - throw new Error( - `package_source_rescue recover requires an existing source repo whose default branch has no HEAD; current status is "${sourceHead.status}".`, - ) - } - if (args.confirm_recovery !== true) { - throw new Error( - 'package_source_rescue recover requires confirm_recovery: true after selecting a verified repo session candidate.', - ) - } - if (!args.recovery_source) { - throw new Error( - 'package_source_rescue recover requires recovery_source.kind: "repo_session" and recovery_source.session_id.', - ) - } - const selectedSession = sessions.find( - (session) => session.id === args.recovery_source?.session_id, - ) - if (!selectedSession) { - throw new Error( - `Repo session "${args.recovery_source.session_id}" was not found for this package source.`, - ) - } - const selectedCandidate = candidates.find( - (candidate) => candidate.session_id === selectedSession.id, - ) - const selectedCommit = - args.recovery_source.commit ?? selectedCandidate?.recovered_commit - if (!selectedCommit || selectedCommit !== source.published_commit) { - throw new Error( - `Selected recovery commit "${selectedCommit ?? 'none'}" does not match prior published commit "${source.published_commit ?? 'none'}".`, - ) - } - if (!selectedCandidate) { - throw new Error( - `Repo session "${selectedSession.id}" is not a verified recovery candidate for the prior published commit.`, - ) - } - const expectedPackageScope = await getMcpUserPackageScope( - ctx.env.APP_DB, - user, - ) - const recovery = await repoSessionRpc( - ctx.env, - selectedSession.id, - ).recoverSourceFromSessionCheckpoint({ - sessionId: selectedSession.id, - sourceId: source.id, - userId: user.userId, - packageId, - kodyId, - recoveredCommit: selectedCommit, - operatorUserId: user.userId, - operatorEmail: user.email, - baseUrl: ctx.callerContext.baseUrl, - rebuildPackageArtifacts: args.rebuild_package_artifacts, - expectedPackageScope, - }) - const sourceHeadAfter = - recovery.status === 'recovered' - ? { - status: 'present' as const, - commit: recovery.sourceHeadAfter, - default_branch: sourceHead.default_branch, - remote: sourceHead.remote, - message: null, - } - : sourceHead - return { - status: recovery.status, - ...baseOutput, - source_head: sourceHeadAfter, - recovery, - recommended_next_action: - recovery.status === 'recovered' - ? 'Recovery restored the missing source HEAD. Normal package source workflows can be used again.' - : 'Recovery did not publish because repo checks failed. Fix or select a different verified source candidate.', - } - }, - }, -) diff --git a/packages/worker/src/repo/publish-git-notes.ts b/packages/worker/src/repo/publish-git-notes.ts index db78074366..9940e8defc 100644 --- a/packages/worker/src/repo/publish-git-notes.ts +++ b/packages/worker/src/repo/publish-git-notes.ts @@ -15,7 +15,6 @@ export type KodyPublishGitNotePublishedBy = | 'repo_session' | 'source_bootstrap' | 'external_push' - | 'source_rescue' export type KodyPublishGitNoteChecks = { runId: string @@ -50,12 +49,7 @@ export const kodyPublishGitNotesRef = 'refs/notes/commits' export const kodyPublishGitNoteSchema = z.object({ v: z.literal(kodyPublishGitNoteVersion), publishedAt: z.string(), - publishedBy: z.enum([ - 'repo_session', - 'source_bootstrap', - 'external_push', - 'source_rescue', - ]), + publishedBy: z.enum(['repo_session', 'source_bootstrap', 'external_push']), sourceId: z.string(), entityKind: z.enum(['skill', 'app', 'job', 'package']), entityId: z.string(), diff --git a/packages/worker/src/repo/repo-session-do.node.test.ts b/packages/worker/src/repo/repo-session-do.node.test.ts index ce6216a907..1371f3e2ea 100644 --- a/packages/worker/src/repo/repo-session-do.node.test.ts +++ b/packages/worker/src/repo/repo-session-do.node.test.ts @@ -114,7 +114,6 @@ const mockModule = vi.hoisted(() => { }, })), writePublishedSourceSnapshot: vi.fn(async () => 'snapshot-key'), - insertSourceRescueEvent: vi.fn(async () => 'source-rescue-event-1'), } }) @@ -176,7 +175,6 @@ function restoreRepoSessionMockBaseline() { }, }) mockModule.writePublishedSourceSnapshot.mockResolvedValue('snapshot-key') - mockModule.insertSourceRescueEvent.mockResolvedValue('source-rescue-event-1') git.clone.mockResolvedValue({ cloned: 'ok', dir: '/session' }) git.remote.mockImplementation( @@ -333,11 +331,6 @@ vi.mock('#worker/package-runtime/published-runtime-artifacts.ts', async () => { } }) -vi.mock('./source-rescue-events.ts', () => ({ - insertSourceRescueEvent: (...args: Array) => - mockModule.insertSourceRescueEvent(...args), -})) - const { RepoSession } = await import('./repo-session-do.ts') const { insertRepoSession } = await import('./repo-sessions.ts') @@ -1107,273 +1100,6 @@ test('publishFromExternalRef rejects stale expected HEAD values', async () => { ) }) -test('recoverSourceFromSessionCheckpoint restores a missing source HEAD from a verified session commit', async () => { - setCommonSessionFixtures() - mockModule.gitState.headCommit = 'commit-base' - mockModule.getRepoSessionById.mockResolvedValue({ - id: 'session-1', - user_id: 'user-1', - source_id: 'source-1', - session_repo_id: 'session-repo-1', - session_repo_name: 'session-repo', - session_repo_namespace: 'default', - base_commit: 'commit-base', - source_root: '/', - conversation_id: 'conversation-1', - status: 'active', - expires_at: null, - last_checkpoint_at: '2026-06-06T00:00:00.000Z', - last_checkpoint_commit: 'commit-base', - last_check_run_id: null, - last_check_tree_hash: null, - created_at: '2026-06-06T00:00:00.000Z', - updated_at: '2026-06-06T00:00:00.000Z', - }) - mockModule.getEntitySourceById.mockResolvedValue({ - id: 'source-1', - user_id: 'user-1', - entity_kind: 'package', - entity_id: 'package-1', - repo_id: 'source-repo', - published_commit: 'commit-base', - indexed_commit: null, - manifest_path: 'package.json', - source_root: '/', - last_external_check_at: null, - created_at: '2026-06-06T00:00:00.000Z', - updated_at: '2026-06-06T00:00:00.000Z', - }) - mockModule.resolveArtifactDefaultBranchHead - .mockResolvedValueOnce(null) - .mockResolvedValueOnce(null) - .mockResolvedValueOnce({ - defaultBranch: 'main', - commit: 'commit-base', - remote: - 'https://acct.artifacts.cloudflare.net/git/default/source-repo.git', - }) - mockModule.workspaceGlob.mockResolvedValue([ - { path: '/session/package.json', type: 'file' }, - { path: '/session/index.ts', type: 'file' }, - ]) - mockModule.workspaceReadFile.mockImplementation(async (path: string) => { - if (path === '/session/package.json') return '{"name":"@kody/demo"}' - if (path === '/session/index.ts') return 'export const ready = true\n' - return '' - }) - const repoSession = new RepoSession(createDurableObjectState(), { - APP_DB: {}, - BUNDLE_ARTIFACTS_KV: {} as KVNamespace, - } as Env) - - const result = await repoSession.recoverSourceFromSessionCheckpoint({ - sessionId: 'session-1', - sourceId: 'source-1', - userId: 'user-1', - packageId: 'package-1', - kodyId: 'demo', - recoveredCommit: 'commit-base', - operatorUserId: 'user-1', - operatorEmail: 'user@example.com', - }) - - expect(result.status).toBe('recovered') - expect(mockModule.git.checkout).toHaveBeenCalledWith( - expect.objectContaining({ - ref: 'commit-base', - branch: 'main', - force: true, - }), - ) - expect(mockModule.git.push).toHaveBeenCalledWith( - expect.objectContaining({ - remote: 'source', - ref: 'main', - username: 'x', - password: 'art_source_secret', - }), - ) - expect(mockModule.writePublishedSourceSnapshot).toHaveBeenCalledWith( - expect.objectContaining({ - files: { - 'package.json': '{"name":"@kody/demo"}', - 'index.ts': 'export const ready = true\n', - }, - }), - ) - expect(mockModule.insertSourceRescueEvent).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - sourceId: 'source-1', - packageId: 'package-1', - kodyId: 'demo', - recoveredSessionId: 'session-1', - recoveredRepoId: 'session-repo-1', - recoveredCommit: 'commit-base', - priorPublishedCommit: 'commit-base', - operatorUserId: 'user-1', - operatorEmail: 'user@example.com', - validationStatus: 'passed', - }), - ) -}) - -test('recoverSourceFromSessionCheckpoint refuses to overwrite an existing source HEAD', async () => { - setCommonSessionFixtures() - mockModule.gitState.headCommit = 'commit-base' - mockModule.getRepoSessionById.mockResolvedValue({ - id: 'session-1', - user_id: 'user-1', - source_id: 'source-1', - session_repo_id: 'session-repo-1', - session_repo_name: 'session-repo', - session_repo_namespace: 'default', - base_commit: 'commit-base', - source_root: '/', - conversation_id: null, - status: 'active', - expires_at: null, - last_checkpoint_at: null, - last_checkpoint_commit: 'commit-base', - last_check_run_id: null, - last_check_tree_hash: null, - created_at: '2026-06-06T00:00:00.000Z', - updated_at: '2026-06-06T00:00:00.000Z', - }) - mockModule.getEntitySourceById.mockResolvedValue({ - id: 'source-1', - user_id: 'user-1', - entity_kind: 'package', - entity_id: 'package-1', - repo_id: 'source-repo', - published_commit: 'commit-base', - indexed_commit: null, - manifest_path: 'package.json', - source_root: '/', - last_external_check_at: null, - created_at: '2026-06-06T00:00:00.000Z', - updated_at: '2026-06-06T00:00:00.000Z', - }) - mockModule.resolveArtifactDefaultBranchHead.mockResolvedValueOnce({ - defaultBranch: 'main', - commit: 'commit-existing', - remote: 'https://acct.artifacts.cloudflare.net/git/default/source-repo.git', - }) - const repoSession = new RepoSession(createDurableObjectState(), createEnv()) - - await expect( - repoSession.recoverSourceFromSessionCheckpoint({ - sessionId: 'session-1', - sourceId: 'source-1', - userId: 'user-1', - packageId: 'package-1', - kodyId: 'demo', - recoveredCommit: 'commit-base', - operatorUserId: 'user-1', - }), - ).rejects.toThrow('already has HEAD "commit-existing"') - expect(mockModule.git.push).not.toHaveBeenCalled() - expect(mockModule.insertSourceRescueEvent).not.toHaveBeenCalled() -}) - -test('recoverSourceFromSessionCheckpoint rechecks missing HEAD before pushing recovered source', async () => { - setCommonSessionFixtures() - mockModule.gitState.headCommit = 'commit-base' - mockModule.getRepoSessionById.mockResolvedValue({ - id: 'session-1', - user_id: 'user-1', - source_id: 'source-1', - session_repo_id: 'session-repo-1', - session_repo_name: 'session-repo', - session_repo_namespace: 'default', - base_commit: 'commit-base', - source_root: '/', - conversation_id: null, - status: 'active', - expires_at: null, - last_checkpoint_at: null, - last_checkpoint_commit: 'commit-base', - last_check_run_id: null, - last_check_tree_hash: null, - created_at: '2026-06-06T00:00:00.000Z', - updated_at: '2026-06-06T00:00:00.000Z', - }) - mockModule.getEntitySourceById.mockResolvedValue({ - id: 'source-1', - user_id: 'user-1', - entity_kind: 'package', - entity_id: 'package-1', - repo_id: 'source-repo', - published_commit: 'commit-base', - indexed_commit: null, - manifest_path: 'package.json', - source_root: '/', - last_external_check_at: null, - created_at: '2026-06-06T00:00:00.000Z', - updated_at: '2026-06-06T00:00:00.000Z', - }) - mockModule.resolveArtifactDefaultBranchHead - .mockResolvedValueOnce(null) - .mockResolvedValueOnce({ - defaultBranch: 'main', - commit: 'commit-raced', - remote: - 'https://acct.artifacts.cloudflare.net/git/default/source-repo.git', - }) - const repoSession = new RepoSession(createDurableObjectState(), createEnv()) - - await expect( - repoSession.recoverSourceFromSessionCheckpoint({ - sessionId: 'session-1', - sourceId: 'source-1', - userId: 'user-1', - packageId: 'package-1', - kodyId: 'demo', - recoveredCommit: 'commit-base', - operatorUserId: 'user-1', - }), - ).rejects.toThrow('gained HEAD "commit-raced" before recovery publish') - expect(mockModule.git.push).not.toHaveBeenCalled() - expect(mockModule.writePublishedSourceSnapshot).not.toHaveBeenCalled() - expect(mockModule.insertSourceRescueEvent).not.toHaveBeenCalled() -}) - -test('recoverSourceFromSessionCheckpoint rejects mismatched package ids before recovery side effects', async () => { - setCommonSessionFixtures() - mockModule.getEntitySourceById.mockResolvedValue({ - id: 'source-1', - user_id: 'user-1', - entity_kind: 'package', - entity_id: 'package-1', - repo_id: 'source-repo', - published_commit: 'commit-base', - indexed_commit: null, - manifest_path: 'package.json', - source_root: '/', - last_external_check_at: null, - created_at: '2026-06-06T00:00:00.000Z', - updated_at: '2026-06-06T00:00:00.000Z', - }) - const repoSession = new RepoSession(createDurableObjectState(), createEnv()) - - await expect( - repoSession.recoverSourceFromSessionCheckpoint({ - sessionId: 'session-1', - sourceId: 'source-1', - userId: 'user-1', - packageId: 'package-other', - kodyId: 'demo', - recoveredCommit: 'commit-base', - operatorUserId: 'user-1', - }), - ).rejects.toThrow( - 'Recovery package "package-other" does not match source package "package-1".', - ) - expect(mockModule.getRepoSessionById).not.toHaveBeenCalled() - expect(mockModule.git.push).not.toHaveBeenCalled() - expect(mockModule.insertSourceRescueEvent).not.toHaveBeenCalled() -}) - test('publishFromExternalRef checks fast-forward ancestry through shell git adapter', async () => { setCommonSessionFixtures() mockModule.getEntitySourceById.mockResolvedValue({ diff --git a/packages/worker/src/repo/repo-session-do.ts b/packages/worker/src/repo/repo-session-do.ts index 2a44bc1df4..8f046a1092 100644 --- a/packages/worker/src/repo/repo-session-do.ts +++ b/packages/worker/src/repo/repo-session-do.ts @@ -58,8 +58,6 @@ import { type RepoSessionRow, type RepoSessionSearchResult, type RepoSessionTreeResult, - type SourceRescueResult, - type SourceRescueValidationResult, } from './types.ts' import { finalizePublishedEntitySource, @@ -74,14 +72,12 @@ import { attachPublishGitNoteBestEffort, buildPublishGitNote, type KodyPublishGitNoteChecks, - type KodyPublishGitNotePublishedBy, } from './publish-git-notes.ts' import { type RepoGitCommand, type RepoRunCommandsResult, parseRepoGitCommands, } from './repo-session-commands.ts' -import { insertSourceRescueEvent } from './source-rescue-events.ts' const repoSessionWorkspacePrefix = '/session' const lastCheckStatusStorageKey = 'repo-session:last-check-status' @@ -532,7 +528,7 @@ class RepoSessionBase extends DurableObject { remote: string token: string remoteName?: string - publishedBy: KodyPublishGitNotePublishedBy + publishedBy: 'repo_session' | 'source_bootstrap' | 'external_push' sessionId?: string | null conversationId?: string | null baseCommit?: string | null @@ -1658,255 +1654,6 @@ class RepoSessionBase extends DurableObject { } } - async recoverSourceFromSessionCheckpoint(input: { - sessionId: string - sourceId: string - userId: string - packageId: string - kodyId: string - recoveredCommit: string - operatorUserId: string - operatorEmail?: string | null - baseUrl?: string - rebuildPackageArtifacts?: boolean - expectedPackageScope?: string - }): Promise { - const source = await getEntitySourceById(this.env.APP_DB, input.sourceId) - if (!source || source.user_id !== input.userId) { - throw new Error('Repo source was not found for this user.') - } - if (source.entity_kind !== 'package') { - throw new Error('Source rescue is only supported for package sources.') - } - const sourcePackageId = source.entity_id - if (input.packageId !== sourcePackageId) { - throw new Error( - `Recovery package "${input.packageId}" does not match source package "${sourcePackageId}".`, - ) - } - if (!source.published_commit) { - throw new Error( - `Source "${source.id}" has no published commit to verify for rescue.`, - ) - } - if (input.recoveredCommit !== source.published_commit) { - throw new Error( - `Source rescue requires the recovered commit "${input.recoveredCommit}" to match the prior published commit "${source.published_commit}".`, - ) - } - const sessionRow = await getRepoSessionById( - this.env.APP_DB, - input.sessionId, - ) - if ( - !sessionRow || - sessionRow.user_id !== input.userId || - sessionRow.source_id !== source.id - ) { - throw new Error('Recovery repo session was not found for this source.') - } - if ( - sessionRow.last_checkpoint_commit !== input.recoveredCommit && - sessionRow.base_commit !== input.recoveredCommit - ) { - throw new Error( - `Recovery repo session "${sessionRow.id}" does not record commit "${input.recoveredCommit}" as a checkpoint or base commit.`, - ) - } - const sourceRepo = await resolveArtifactSourceRepo(this.env, source.repo_id) - const sourceInfo = await sourceRepo.info() - const sourceHeadBefore = await resolveArtifactDefaultBranchHead({ - repo: sourceRepo, - }) - if (sourceHeadBefore) { - throw new Error( - `Source rescue stopped because artifact source repo "${source.repo_id}" already has HEAD "${sourceHeadBefore.commit}". Use normal package source workflows instead.`, - ) - } - const targetBranch = sourceInfo?.defaultBranch ?? defaultSessionBranch - const sessionRepo = await resolveSessionRepo(this.env, { - namespace: sessionRow.session_repo_namespace, - name: sessionRow.session_repo_name, - }) - const sessionAccess = await ensureArtifactRepoRemote({ - repo: sessionRepo, - scope: 'read', - }) - await this.initialize({ - sessionId: sessionRow.id, - sessionRepoRemote: sessionAccess.remote, - sessionRepoToken: sessionAccess.token, - }) - await this.git.checkout({ - dir: repoSessionWorkspacePrefix, - ref: input.recoveredCommit, - branch: targetBranch, - force: true, - }) - const checkedOutCommit = await this.getHeadCommit() - if (checkedOutCommit !== input.recoveredCommit) { - throw new Error( - `Recovery repo session "${sessionRow.id}" checked out "${checkedOutCommit ?? 'unknown'}" instead of "${input.recoveredCommit}".`, - ) - } - const checks = await runRepoChecks({ - workspace: this.workspace, - manifestPath: resolveRepoWorkspacePath( - source.manifest_path, - repoSessionWorkspacePrefix, - ), - sourceRoot: resolveRepoWorkspacePath( - source.source_root || repoSessionWorkspacePrefix, - repoSessionWorkspacePrefix, - ), - env: this.env, - baseUrl: input.baseUrl ?? source.source_root, - userId: input.userId, - expectedPackageScope: input.expectedPackageScope, - }) - const validation: SourceRescueValidationResult = { - ok: checks.ok, - runId: crypto.randomUUID(), - treeHash: await this.computeTreeHash(), - checkedAt: nowIso(), - results: checks.results.map((entry) => ({ - kind: entry.kind, - ok: entry.ok, - message: entry.message, - })), - } - if (!checks.ok) { - const auditEventId = await insertSourceRescueEvent(this.env.APP_DB, { - userId: input.userId, - sourceId: source.id, - entityKind: source.entity_kind, - entityId: source.entity_id, - packageId: sourcePackageId, - kodyId: input.kodyId, - sourceRepoId: source.repo_id, - recoveryKind: 'repo_session', - recoveredSessionId: sessionRow.id, - recoveredRepoId: sessionRow.session_repo_id, - recoveredRepoName: sessionRow.session_repo_name, - recoveredCommit: input.recoveredCommit, - priorPublishedCommit: source.published_commit, - sourceHeadBefore: null, - sourceHeadAfter: null, - operatorUserId: input.operatorUserId, - operatorEmail: input.operatorEmail ?? null, - validationStatus: 'failed', - validationJson: JSON.stringify(validation), - }) - return { - status: 'checks_failed', - sourceId: source.id, - packageId: sourcePackageId, - recoveredCommit: input.recoveredCommit, - priorPublishedCommit: source.published_commit, - sourceHeadBefore: null, - recoveredSessionId: sessionRow.id, - recoveredRepoId: sessionRow.session_repo_id, - recoveredRepoName: sessionRow.session_repo_name, - validation, - auditEventId, - failedChecks: validation.results.filter((entry) => !entry.ok), - } - } - const sourceHeadBeforePush = await resolveArtifactDefaultBranchHead({ - repo: sourceRepo, - }) - if (sourceHeadBeforePush) { - throw new Error( - `Source rescue stopped because artifact source repo "${source.repo_id}" gained HEAD "${sourceHeadBeforePush.commit}" before recovery publish. Use normal package source workflows instead.`, - ) - } - const sourceAccess = await ensureArtifactRepoRemote({ - repo: sourceRepo, - scope: 'write', - }) - await this.ensureRemote({ - name: 'source', - url: buildAuthenticatedArtifactsRemote({ - remote: sourceAccess.remote, - token: sourceAccess.token, - }), - }) - await this.git.push({ - dir: repoSessionWorkspacePrefix, - remote: 'source', - ref: targetBranch, - ...buildArtifactsGitAuth({ token: sourceAccess.token }), - }) - const snapshotFiles = await this.collectWorkspaceFiles() - await finalizePublishedEntitySource({ - env: this.env, - source, - publishedCommit: input.recoveredCommit, - files: snapshotFiles, - baseUrl: input.baseUrl ?? source.source_root, - rebuildPackageArtifacts: input.rebuildPackageArtifacts ?? true, - }) - await this.attachSourcePublishGitNote({ - source, - commitOid: input.recoveredCommit, - remote: sourceAccess.remote, - token: sourceAccess.token, - remoteName: 'source', - publishedBy: 'source_rescue', - sessionId: sessionRow.id, - conversationId: sessionRow.conversation_id, - baseCommit: sessionRow.base_commit, - previousPublishedCommit: source.published_commit, - checks: { - runId: validation.runId, - treeHash: validation.treeHash, - checkedAt: validation.checkedAt, - ok: validation.ok, - results: validation.results, - }, - scope: 'repo.sourceRescue.publish-git-note', - }) - const sourceHeadAfter = - (await resolveArtifactDefaultBranchHead({ repo: sourceRepo }))?.commit ?? - input.recoveredCommit - const auditEventId = await insertSourceRescueEvent(this.env.APP_DB, { - userId: input.userId, - sourceId: source.id, - entityKind: source.entity_kind, - entityId: source.entity_id, - packageId: sourcePackageId, - kodyId: input.kodyId, - sourceRepoId: source.repo_id, - recoveryKind: 'repo_session', - recoveredSessionId: sessionRow.id, - recoveredRepoId: sessionRow.session_repo_id, - recoveredRepoName: sessionRow.session_repo_name, - recoveredCommit: input.recoveredCommit, - priorPublishedCommit: source.published_commit, - sourceHeadBefore: null, - sourceHeadAfter, - operatorUserId: input.operatorUserId, - operatorEmail: input.operatorEmail ?? null, - validationStatus: 'passed', - validationJson: JSON.stringify(validation), - }) - return { - status: 'recovered', - sourceId: source.id, - packageId: sourcePackageId, - recoveredCommit: input.recoveredCommit, - priorPublishedCommit: source.published_commit, - sourceHeadBefore: null, - sourceHeadAfter, - recoveredSessionId: sessionRow.id, - recoveredRepoId: sessionRow.session_repo_id, - recoveredRepoName: sessionRow.session_repo_name, - validation, - auditEventId, - message: `Recovered package source ${source.id} from repo session ${sessionRow.id} at ${input.recoveredCommit}.`, - } - } - async publishFromExternalRef(input: { sessionId: string sourceId: string diff --git a/packages/worker/src/repo/repo-session-rpc.ts b/packages/worker/src/repo/repo-session-rpc.ts index 738943f9af..640400b193 100644 --- a/packages/worker/src/repo/repo-session-rpc.ts +++ b/packages/worker/src/repo/repo-session-rpc.ts @@ -16,7 +16,6 @@ import { type RepoSessionRebaseResult, type RepoSessionSearchResult, type RepoSessionTreeResult, - type SourceRescueResult, } from './types.ts' export type RepoSessionRpc = { @@ -140,19 +139,6 @@ export type RepoSessionRpc = { rebuildPackageArtifacts?: boolean expectedPackageScope?: string }) => Promise - recoverSourceFromSessionCheckpoint: (payload: { - sessionId: string - sourceId: string - userId: string - packageId: string - kodyId: string - recoveredCommit: string - operatorUserId: string - operatorEmail?: string | null - baseUrl?: string - rebuildPackageArtifacts?: boolean - expectedPackageScope?: string - }) => Promise } export function repoSessionRpc(env: Env, sessionId: string): RepoSessionRpc { diff --git a/packages/worker/src/repo/source-rescue-events.ts b/packages/worker/src/repo/source-rescue-events.ts deleted file mode 100644 index 5591564bbc..0000000000 --- a/packages/worker/src/repo/source-rescue-events.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { type EntityKind } from './types.ts' - -export type InsertSourceRescueEventInput = { - userId: string - sourceId: string - entityKind: EntityKind - entityId: string - packageId: string | null - kodyId: string | null - sourceRepoId: string - recoveryKind: 'repo_session' - recoveredSessionId: string | null - recoveredRepoId: string | null - recoveredRepoName: string | null - recoveredCommit: string | null - priorPublishedCommit: string | null - sourceHeadBefore: string | null - sourceHeadAfter: string | null - operatorUserId: string - operatorEmail: string | null - validationStatus: 'passed' | 'failed' - validationJson: string - metadataJson?: string | null -} - -export async function insertSourceRescueEvent( - db: D1Database, - input: InsertSourceRescueEventInput, -) { - const id = crypto.randomUUID() - const now = new Date().toISOString() - await db - .prepare( - `INSERT INTO source_rescue_events ( - id, user_id, source_id, entity_kind, entity_id, package_id, kody_id, - source_repo_id, recovery_kind, recovered_session_id, recovered_repo_id, - recovered_repo_name, recovered_commit, prior_published_commit, - source_head_before, source_head_after, operator_user_id, operator_email, - validation_status, validation_json, metadata_json, created_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - ) - .bind( - id, - input.userId, - input.sourceId, - input.entityKind, - input.entityId, - input.packageId, - input.kodyId, - input.sourceRepoId, - input.recoveryKind, - input.recoveredSessionId, - input.recoveredRepoId, - input.recoveredRepoName, - input.recoveredCommit, - input.priorPublishedCommit, - input.sourceHeadBefore, - input.sourceHeadAfter, - input.operatorUserId, - input.operatorEmail, - input.validationStatus, - input.validationJson, - input.metadataJson ?? null, - now, - ) - .run() - return id -} diff --git a/packages/worker/src/repo/source-safety-policy.node.test.ts b/packages/worker/src/repo/source-safety-policy.node.test.ts index 2ce456206b..8cbecdc9e7 100644 --- a/packages/worker/src/repo/source-safety-policy.node.test.ts +++ b/packages/worker/src/repo/source-safety-policy.node.test.ts @@ -69,18 +69,6 @@ test('package source overwrite requires explicit destructive confirmation before ).rejects.toThrow(destructiveOverwriteConfirmationField) }) -test('package source overwrite remains blocked when destructive confirmation has no verified source backup', async () => { - await expect( - assertPackageSourceOverwriteAllowed({ - env: createEnvWithSnapshot(null), - userId: 'user-1', - source: packageSource(), - operation: 'package_save', - confirmed: true, - }), - ).rejects.toThrow('Stop and report this source recovery problem') -}) - test('restorable package source snapshot verification rejects corrupt snapshots and accepts manifest-bearing backups', async () => { await expect( assertRestorablePackageSourceSnapshot({ diff --git a/packages/worker/src/repo/types.ts b/packages/worker/src/repo/types.ts index 5554cd54d4..7701985c50 100644 --- a/packages/worker/src/repo/types.ts +++ b/packages/worker/src/repo/types.ts @@ -335,49 +335,6 @@ export type RepoSessionPublishResult = repairHint: 'repo_rebase_session' } -export type SourceRescueValidationResult = { - ok: boolean - runId: string - treeHash: string | null - checkedAt: string - results: Array<{ - kind: string - ok: boolean - message: string - }> -} - -export type SourceRescueResult = - | { - status: 'recovered' - sourceId: string - packageId: string - recoveredCommit: string - priorPublishedCommit: string - sourceHeadBefore: null - sourceHeadAfter: string - recoveredSessionId: string - recoveredRepoId: string - recoveredRepoName: string - validation: SourceRescueValidationResult - auditEventId: string - message: string - } - | { - status: 'checks_failed' - sourceId: string - packageId: string - recoveredCommit: string - priorPublishedCommit: string - sourceHeadBefore: null - recoveredSessionId: string - recoveredRepoId: string - recoveredRepoName: string - validation: SourceRescueValidationResult - auditEventId: string - failedChecks: SourceRescueValidationResult['results'] - } - export type RepoExternalPublishResult = | { status: 'already_published'