Repository navigation
Integration: local review journal with authenticated durable-task route #2613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
groupthinking
merged 23 commits into
codex/durable-workflow-tasks
from
codex/review-journal-task-integration
Nov 10, 2026
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
f602afb
feat(integration): carry verified local journal .github/workflows/rel…
groupthinking e8c80d6
feat(integration): carry verified local journal apps/web/src/lib/acti…
groupthinking 51c9406
feat(integration): carry verified local journal apps/web/src/lib/acti…
groupthinking 89130b4
feat(integration): carry verified local journal apps/web/src/lib/rela…
groupthinking bf0af73
feat(integration): carry verified local journal docs/relay-durable-re…
groupthinking 0ac011a
feat(integration): carry verified local journal scripts/relay_harness…
groupthinking 90fc3cf
feat(integration): carry verified local journal scripts/relay_harness…
groupthinking d7c4240
feat(integration): carry verified local journal scripts/relay_harness…
groupthinking 84f8190
feat(integration): carry verified local journal scripts/relay_harness…
groupthinking b73ff6c
feat(integration): carry verified local journal tests/relay-local-rev…
groupthinking b3d770d
test(integration): carry Python journal integrity regressions
groupthinking 4f86829
feat(integration): enforce local journal policy alongside signed task…
groupthinking 270cd1f
test(integration): guard review-only execution and exact preview pers…
groupthinking dfca088
test(integration): isolate review journal mode in web suite
groupthinking 9386ffc
test(integration): exercise combined route through real local journal
groupthinking 7136639
Fix: In local-review-only mode, a cookie-authenticated POST from anot…
vercel[bot] 0f2e735
fix(review-journal): reject non-JSON local preview mutations
groupthinking ef886f6
fix(review-journal): reject nonboolean isError previews
groupthinking e52a745
fix(journal): sync recovered artifacts and index per-task history
groupthinking af037ed
fix(review-journal): validate interpreter, harness, and state before …
groupthinking 84e47fb
test(journal): enforce strict preview types, fsync recovery, and inde…
groupthinking a2fde4e
test(integration): reject non-JSON journal writes and unusable host c…
groupthinking c449008
fix(ci): avoid reserved Next.js module binding in journal preflight
groupthinking File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| name: Durable local review journal | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - 'scripts/relay_harness/**' | ||
| - 'apps/web/src/lib/*review*.mjs' | ||
| - 'apps/web/src/lib/*review*.d.mts' | ||
| - 'apps/web/src/app/api/agents/actions/route.ts' | ||
| - 'tests/*relay*' | ||
| - '.github/workflows/relay-review-journal.yml' | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| local-recovery-and-route: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 5 | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | ||
| with: | ||
| python-version: '3.12' | ||
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | ||
| with: | ||
| node-version: '24' | ||
| - name: SQLite, evidence, denial and process recovery regressions | ||
| run: python3 -m unittest discover -s tests -p test_relay_local_harness.py -v | ||
| - name: Actual local bridge and review route integration | ||
| run: node --test tests/relay-local-review.test.mjs tests/relay-review-route.test.mjs |
103 changes: 103 additions & 0 deletions
103
apps/web/src/app/api/__tests__/actions-route-journal-integration.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
| import { NextRequest } from 'next/server'; | ||
|
|
||
| const state = vi.hoisted(() => ({ | ||
| journal: null as null | { persist: ReturnType<typeof vi.fn> }, | ||
| model: vi.fn(), | ||
| execute: vi.fn(), | ||
| identity: 'trusted@example.invalid' as string | null, | ||
| })); | ||
| vi.mock('@/lib/action-review-journal.mjs', () => ({ | ||
| resolveActionReviewJournal: () => state.journal, | ||
| })); | ||
| vi.mock('@/lib/action-agent', () => ({ | ||
| AVAILABLE_TOOL_NAMES: ['create_workflow_task'], | ||
| runActionAgent: state.model, | ||
| executePreparedActions: state.execute, | ||
| })); | ||
| vi.mock('@/lib/billing/billing-context', () => ({ | ||
| resolveTrustedBillingEmail: async () => state.identity, | ||
| })); | ||
| vi.mock('@/lib/billing/entitlement-store', () => ({ | ||
| isProSubscriber: async () => false, | ||
| })); | ||
| vi.mock('@/lib/workflow-task-store', () => ({ | ||
| workflowTaskOwner: async () => null, | ||
| readWorkflowTask: vi.fn(), | ||
| })); | ||
| vi.mock('@/lib/gemini-client', () => ({ hasGeminiKey: () => false })); | ||
|
|
||
| import { POST } from '@/app/api/agents/actions/route'; | ||
|
|
||
| const action = { | ||
| tool: 'create_workflow_task', | ||
| input: { title: 'Review target' }, | ||
| status: 'pending' as const, | ||
| result: 'Prepared for review. No tool has been executed.', | ||
| isError: false, | ||
| }; | ||
| function request(body: unknown) { | ||
| return new NextRequest('http://localhost/api/agents/actions', { | ||
| method: 'POST', | ||
| headers: { 'content-type': 'application/json', origin: 'http://localhost' }, | ||
| body: JSON.stringify(body), | ||
| }); | ||
| } | ||
| describe('combined local journal and durable-task route boundary', () => { | ||
| beforeEach(() => { | ||
| state.model.mockReset(); | ||
| state.execute.mockReset(); | ||
| state.identity = 'trusted@example.invalid'; | ||
| state.journal = { persist: vi.fn().mockResolvedValue({ | ||
| version: 'relay.local-receipt.v1', taskId: 'local-task', | ||
| scope: 'local_artifact_only', state: 'complete', | ||
| artifactVerified: true, originalActionsExecuted: false, | ||
| originalTaskComplete: false, | ||
| }) }; | ||
| state.model.mockResolvedValue({ provider: 'test', actions: [action] }); | ||
| }); | ||
| afterEach(() => { state.journal = null; }); | ||
|
|
||
| it('persists exact preview fields and never issues an executable token in review-only mode', async () => { | ||
| const response = await POST(request({ mode: 'preview', transcript: 'A sufficiently long source transcript' })); | ||
| const body = await response.json(); | ||
| expect(response.status).toBe(200); | ||
| expect(body.actions).toEqual([action]); | ||
| expect(body.confirmationToken).toBeUndefined(); | ||
| expect(body.localArtifactReceipt.originalActionsExecuted).toBe(false); | ||
| expect(state.journal?.persist).toHaveBeenCalledWith([action], 'trusted@example.invalid'); | ||
| expect(state.model).toHaveBeenCalledWith(expect.objectContaining({ executeTools: false })); | ||
| expect(state.execute).not.toHaveBeenCalled(); | ||
| }); | ||
| it('denies all execute requests before invoking any action', async () => { | ||
| const response = await POST(request({ mode: 'execute', actions: [action], jobId: 'forged', confirmationToken: 'forged' })); | ||
| expect(response.status).toBe(409); | ||
| expect(state.model).not.toHaveBeenCalled(); | ||
| expect(state.execute).not.toHaveBeenCalled(); | ||
| expect(state.journal?.persist).not.toHaveBeenCalled(); | ||
| }); | ||
| it('requires trusted identity before preview and local persistence', async () => { | ||
| state.identity = null; | ||
| const response = await POST(request({ transcript: 'A sufficiently long source transcript' })); | ||
| expect(response.status).toBe(401); | ||
| expect(state.model).not.toHaveBeenCalled(); | ||
| }); | ||
| it('fails closed on journal persistence failure', async () => { | ||
| state.journal?.persist.mockRejectedValueOnce(new Error('disk unavailable')); | ||
| const response = await POST(request({ transcript: 'A sufficiently long source transcript' })); | ||
| const body = await response.json(); | ||
| expect(response.status).toBe(502); | ||
| expect(body.success).toBe(false); | ||
| expect(body.actions).toEqual([]); | ||
| expect(state.execute).not.toHaveBeenCalled(); | ||
| }); | ||
| it('preserves existing preview path when local journal is disabled', async () => { | ||
| state.journal = null; | ||
| const response = await POST(request({ transcript: 'A sufficiently long source transcript' })); | ||
| const body = await response.json(); | ||
| expect(response.status).toBe(200); | ||
| expect(body.actions).toEqual([action]); | ||
| expect(body.localArtifactReceipt).toBeUndefined(); | ||
| expect(state.execute).not.toHaveBeenCalled(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import type { AgentAction } from './action-lifecycle'; | ||
|
|
||
| export interface LocalArtifactReceipt { | ||
| version: 'relay.local-receipt.v1'; | ||
| taskId: string; | ||
| scope: 'local_artifact_only'; | ||
| state: 'complete'; | ||
| artifactVerified: true; | ||
| artifactHash: string; | ||
| contractHash: string; | ||
| historyHead: string; | ||
| originalActionsExecuted: false; | ||
| originalTaskComplete: false; | ||
| cost: { | ||
| amount: null; | ||
| currency: null; | ||
| status: 'unknown'; | ||
| note: string; | ||
| }; | ||
| } | ||
|
|
||
| export interface ActionReviewJournal { | ||
| persist(actions: AgentAction[], trustedIdentity: string | null): Promise<LocalArtifactReceipt>; | ||
| } | ||
|
|
||
| export function resolveActionReviewJournal( | ||
| env?: NodeJS.ProcessEnv, | ||
| ): ActionReviewJournal | null; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| /** Server-owned opt-in for persistent local review artifacts, never execution. */ | ||
| import 'server-only'; | ||
| import { createHash } from 'node:crypto'; | ||
| import { createLocalReviewBridge } from './relay-local-review.mjs'; | ||
|
|
||
| /** | ||
| * Only operator environment config may select the interpreter/state directory. | ||
| * A nonempty invalid mode fails closed rather than silently disabling a guard. | ||
| */ | ||
| export function resolveActionReviewJournal(env = process.env) { | ||
| const mode = env.RELAY_REVIEW_JOURNAL_MODE; | ||
| if (!mode) return null; | ||
| if (mode !== 'local-review-only') throw new Error('Invalid local review journal mode'); | ||
| if (env.VERCEL || env.AWS_LAMBDA_FUNCTION_NAME || env.K_SERVICE) { | ||
| throw new Error('Local review journal requires a persistent non-serverless host'); | ||
| } | ||
| const bridge = createLocalReviewBridge({ | ||
| stateDirectory: env.RELAY_REVIEW_STATE_DIRECTORY, | ||
| scriptsDirectory: env.RELAY_REVIEW_SCRIPTS_DIRECTORY, | ||
| pythonExecutable: env.RELAY_REVIEW_PYTHON_EXECUTABLE, | ||
| }); | ||
| return Object.freeze({ | ||
| async persist(actions, trustedIdentity) { | ||
| if (typeof trustedIdentity !== 'string' || !trustedIdentity.trim()) { | ||
| throw new Error('Trusted identity required for durable review plans'); | ||
| } | ||
| // Do not store identity/transcript or trust a caller-supplied jobId as a | ||
| // cross-user idempotency key. Exact plans are independently actor-scoped. | ||
| const actorHash = createHash('sha256').update(trustedIdentity).digest('hex'); | ||
| const key = createHash('sha256').update(JSON.stringify({ actorHash, actions })).digest('hex'); | ||
| // v1 artifacts omitted optional preview fields. Do not rewrite immutable | ||
| // evidence or reuse its receipt for the exact-plan serialization. | ||
| const result = await bridge.persistReview(actions, `review-v2-${key}`); | ||
| if (!result.localReceipt.complete || result.originalActionsExecuted !== false || | ||
| result.originalTaskComplete !== false) { | ||
| throw new Error('The local review artifact did not verify'); | ||
| } | ||
| return { | ||
| version: result.localReceipt.version, | ||
| taskId: result.taskId, | ||
| scope: 'local_artifact_only', | ||
| state: result.localReceipt.state, | ||
| artifactVerified: true, | ||
| artifactHash: result.localReceipt.evidence.sha256, | ||
| contractHash: result.localReceipt.contract_hash, | ||
| historyHead: result.localReceipt.history_head, | ||
| originalActionsExecuted: false, | ||
| originalTaskComplete: false, | ||
| cost: result.localReceipt.cost, | ||
| }; | ||
| }, | ||
| }); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.