Repository navigation
feat(publish)!: share issue receipts with finding tracking #1426
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
Open
mldangelo-oai
wants to merge
23
commits into
main
Choose a base branch
from
mdangelo/codex/shared-issue-associations
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
60ad828
feat!: share finding issue receipts across publication and tracking
codex b001ea2
fix(tracking): recover issue receipts after plugin restart
codex 34f8e16
fix(tracking): recover stored history before initial writes
codex 1aadd7a
fix(tracking): preserve fallback for inaccessible primary stores
codex 2ba3cb8
fix(tracking): defer inaccessible receipt stores to SQLite
codex 623d9a7
Merge main and append shared issue receipt migration
codex 9bb06a1
Merge main migration test expectations
codex cf868ac
Merge main and append shared receipt migration after embedding history
codex 279d395
refactor: derive publication membership from the sealed scan
codex 6dc00c3
Merge main and preserve ordered finding receipt migrations
mldangelo-oai 040a7c7
Preserve receipt ownership and integrate current workbench handling
mldangelo-oai d6c31ba
Integrate main publication refactoring with shared receipts
mldangelo-oai 0c08651
Merge main and preserve shared receipt publication diagnostics
mldangelo-oai 8615346
Preserve accepted receipt URLs and complete receipt diagnostics
mldangelo-oai d9a7671
Index finding issue ownership checks without rewriting migrations
mldangelo-oai d1fe058
Merge commit '553714fa07a133ccc37bcde018179ad6e7ed075c' into HEAD
mldangelo-oai 117e94e
Merge main and retain current workbench runtime behavior
mldangelo-oai aa9c4b1
test(deep-scan): isolate the Node capability bundle probe
mldangelo-oai e6e33be
test(package): adapt Deep Scan fixture to direct engine
mldangelo-oai fd036f9
fix(tracking): allow full-scan receipt work to complete
mldangelo-oai 85b3369
chore: merge main and resolve integration conflicts
mldangelo-oai 5da54aa
chore: merge main into shared issue receipts
codex 2dba6b2
Merge main and retain shared issue receipt integrity
codex 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
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
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
96 changes: 96 additions & 0 deletions
96
plugins/codex-security/mcp-app/src/server/finding-issue-tools.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,96 @@ | ||
| import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import * as z from "zod/v4"; | ||
| import type { JsonObject } from "../types.js"; | ||
| import { readingAnnotations, writingAnnotations } from "./tool-annotations.js"; | ||
|
|
||
| const identifier = z.string().min(1); | ||
| const destinationSchema = z.discriminatedUnion("type", [ | ||
| z | ||
| .object({ | ||
| type: z.literal("linear"), | ||
| teamId: identifier, | ||
| projectId: identifier.optional(), | ||
| }) | ||
| .strict(), | ||
| z | ||
| .object({ | ||
| type: z.literal("jira"), | ||
| cloudId: identifier, | ||
| projectId: identifier, | ||
| }) | ||
| .strict(), | ||
| z | ||
| .object({ | ||
| type: z.enum(["github-issue", "github-advisory"]), | ||
| hostname: identifier, | ||
| repository: identifier, | ||
| }) | ||
| .strict(), | ||
| ]); | ||
| const sourceSchema = z | ||
| .object({ | ||
| scanDirectory: identifier, | ||
| destination: destinationSchema, | ||
| findingIds: z.array(identifier).optional(), | ||
| }) | ||
| .strict(); | ||
| const receiptSchema = z | ||
| .object({ | ||
| findingId: identifier, | ||
| occurrenceId: identifier, | ||
| issueIdentifier: identifier, | ||
| url: identifier.optional(), | ||
| operation: z.enum(["create", "update", "reuse"]), | ||
| readback: z | ||
| .object({ | ||
| status: z.enum(["verified", "failed"]), | ||
| error: z.string().optional(), | ||
| }) | ||
| .strict() | ||
| .optional(), | ||
| }) | ||
| .strict(); | ||
|
|
||
| export function registerFindingIssueTools( | ||
| server: McpServer, | ||
| runWorkbench: ( | ||
| args: string[], | ||
| input?: string | Buffer, | ||
| ) => Promise<JsonObject>, | ||
| ): void { | ||
| const meta = { ui: { visibility: ["model"] } }; | ||
| const execute = async (action: "inspect" | "record", input: object) => { | ||
| const result = await runWorkbench( | ||
| ["finding-issues"], | ||
| JSON.stringify({ action, ...input }), | ||
|
mldangelo-oai marked this conversation as resolved.
|
||
| ); | ||
| return { | ||
| content: [{ type: "text" as const, text: JSON.stringify(result) }], | ||
| structuredContent: result, | ||
| }; | ||
| }; | ||
| server.registerTool( | ||
| "get_codex_security_finding_issues", | ||
| { | ||
| title: "Get Codex Security Finding Issues", | ||
| description: | ||
| "Validate a sealed scan and read its findings' saved issue associations for the exact destination, including earlier scan occurrences. Saved receipts are duplicate candidates; verify them through the provider before reusing an issue. Does not contact providers or require local scan history.", | ||
| inputSchema: sourceSchema, | ||
| annotations: readingAnnotations, | ||
| _meta: meta, | ||
| }, | ||
| (input) => execute("inspect", input), | ||
| ); | ||
| server.registerTool( | ||
| "record_codex_security_finding_issues", | ||
| { | ||
| title: "Record Codex Security Finding Issues", | ||
| description: | ||
| "Validate a sealed scan and save accepted provider creates or updates, or verified issue reuse, in the shared local issue history. Record the returned identity immediately; repeat the same receipt with readback status after an optional provider read. A failed read or local save does not undo an accepted write or authorize another create. Does not contact providers or modify the scan bundle.", | ||
| inputSchema: sourceSchema.extend({ receipts: z.array(receiptSchema) }), | ||
| annotations: writingAnnotations, | ||
| _meta: meta, | ||
| }, | ||
| (input) => execute("record", input), | ||
| ); | ||
| } | ||
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
171 changes: 171 additions & 0 deletions
171
plugins/codex-security/mcp-app/tests/test_finding_issue_tools.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,171 @@ | ||
| import assert from "node:assert/strict"; | ||
| import { cp, readFile, rm, stat, writeFile } from "node:fs/promises"; | ||
| import path from "node:path"; | ||
| import { Client } from "@modelcontextprotocol/sdk/client/index.js"; | ||
| import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; | ||
| import type { TextContent } from "@modelcontextprotocol/sdk/types.js"; | ||
| import { applicationRoot, buildServer } from "./build-server.ts"; | ||
| import { temporaryDirectory } from "./support/temporary-directories.ts"; | ||
|
|
||
| const pluginRoot = path.resolve(applicationRoot, ".."); | ||
| const runtimePluginRoot = process.env.CODEX_SECURITY_TEST_PLUGIN_ROOT | ||
| ? path.resolve(process.env.CODEX_SECURITY_TEST_PLUGIN_ROOT) | ||
| : path.resolve(applicationRoot, "../../../sdk/typescript/_bundled_plugin"); | ||
| const root = await temporaryDirectory("codex-security-finding-issues-mcp-"); | ||
| const scanDirectory = path.join(root, "scan"); | ||
| const stateDirectory = path.join(root, "state"); | ||
| const bundle = path.join(root, "server.cjs"); | ||
| const client = new Client({ name: "finding-issues-test", version: "1.0.0" }); | ||
|
|
||
| try { | ||
| await cp(path.join(pluginRoot, "examples", "completed-scan"), scanDirectory, { | ||
| recursive: true, | ||
| }); | ||
| await buildServer(bundle, { target: "node20" }); | ||
| await client.connect( | ||
| new StdioClientTransport({ | ||
| command: process.execPath, | ||
| args: [bundle, "--stdio"], | ||
| cwd: applicationRoot, | ||
| env: { | ||
| ...process.env, | ||
| CODEX_SECURITY_PLUGIN_ROOT: runtimePluginRoot, | ||
| CODEX_SECURITY_STATE_DIR: stateDirectory, | ||
| } as Record<string, string>, | ||
| }), | ||
| ); | ||
| const findingsPath = path.join(scanDirectory, "findings.json"); | ||
| const findingsBytes = await readFile(findingsPath, "utf8"); | ||
| const { findingId, occurrenceId } = JSON.parse(findingsBytes).findings[0]; | ||
| const manifestBytes = await readFile( | ||
| path.join(scanDirectory, "scan-manifest.json"), | ||
| ); | ||
| const source = { scanDirectory, findingIds: [findingId] }; | ||
| const jira = { type: "jira", cloudId: "example-site", projectId: "10001" }; | ||
| const linear = { type: "linear", teamId: "example-team" }; | ||
| const receipt = { | ||
| findingId, | ||
| occurrenceId, | ||
| issueIdentifier: "APP-1", | ||
| operation: "create", | ||
| }; | ||
|
|
||
| const tools = (await client.listTools()).tools; | ||
| for (const name of [ | ||
| "get_codex_security_finding_issues", | ||
| "record_codex_security_finding_issues", | ||
| ]) { | ||
| const tool = tools.find((entry) => entry.name === name); | ||
| assert.ok(tool, `Missing model-visible tool ${name}`); | ||
| assert.deepEqual(tool._meta?.ui, { visibility: ["model"] }); | ||
| assert.equal( | ||
| Object.hasOwn(tool.inputSchema.properties ?? {}, "requireHistory"), | ||
| false, | ||
| ); | ||
| } | ||
| const empty = await call("get_codex_security_finding_issues", { | ||
| ...source, | ||
| destination: jira, | ||
| }); | ||
| assert.deepEqual(empty.receipts, []); | ||
| await assert.rejects(stat(path.join(stateDirectory, "workbench.sqlite3")), { | ||
| code: "ENOENT", | ||
| }); | ||
|
|
||
| const accepted = await call("record_codex_security_finding_issues", { | ||
| ...source, | ||
| destination: jira, | ||
| receipts: [receipt], | ||
| }); | ||
| assert.equal(accepted.receipts.length, 1); | ||
| assert.equal(accepted.receipts[0].issueIdentifier, "APP-1"); | ||
| assert.equal(accepted.receipts[0].readback, undefined); | ||
| const readback = { | ||
| status: "failed", | ||
| error: `Issue read access denied. ${"x".repeat(4 * 1024 * 1024)}`, | ||
| }; | ||
| await call("record_codex_security_finding_issues", { | ||
| ...source, | ||
| destination: jira, | ||
| receipts: [{ ...receipt, readback }], | ||
| }); | ||
| const saved = await call("get_codex_security_finding_issues", { | ||
| ...source, | ||
| destination: jira, | ||
| }); | ||
| assert.equal( | ||
| saved.receipts.length, | ||
| 1, | ||
| "Readback must update the accepted receipt.", | ||
| ); | ||
| assert.equal(saved.receipts[0].operation, "create"); | ||
| assert.deepEqual(saved.receipts[0].readback, readback); | ||
| assert.deepEqual( | ||
| ( | ||
| await call("get_codex_security_finding_issues", { | ||
| ...source, | ||
| destination: { ...jira, cloudId: "another-site" }, | ||
| }) | ||
| ).receipts, | ||
| [], | ||
| ); | ||
|
|
||
| await call("record_codex_security_finding_issues", { | ||
| ...source, | ||
| destination: linear, | ||
| receipts: [ | ||
| { ...receipt, operation: "reuse", readback: { status: "verified" } }, | ||
| ], | ||
| }); | ||
| const reused = await call("get_codex_security_finding_issues", { | ||
| ...source, | ||
| destination: linear, | ||
| }); | ||
| assert.equal(reused.receipts.length, 1); | ||
| assert.equal(reused.receipts[0].operation, "reuse"); | ||
| assert.deepEqual(reused.receipts[0].readback, { status: "verified" }); | ||
| assert.deepEqual( | ||
| await readFile(path.join(scanDirectory, "scan-manifest.json")), | ||
| manifestBytes, | ||
| ); | ||
| assert.equal(await readFile(findingsPath, "utf8"), findingsBytes); | ||
|
|
||
| await writeFile(findingsPath, findingsBytes + "\n"); | ||
| for (const [name, input] of [ | ||
| ["get_codex_security_finding_issues", { ...source, destination: jira }], | ||
| [ | ||
| "record_codex_security_finding_issues", | ||
| { ...source, destination: jira, receipts: [receipt] }, | ||
| ], | ||
| ] as const) { | ||
| const result = await client.callTool({ name, arguments: input }); | ||
| assert.equal( | ||
| result.isError, | ||
| true, | ||
| "Changed sealed artifacts must fail before association access.", | ||
| ); | ||
| assert.match( | ||
| (result.content as TextContent[])[0].text, | ||
| /sealed artifact changed/, | ||
| ); | ||
| } | ||
| } finally { | ||
| await client.close(); | ||
| await rm(root, { recursive: true, force: true }); | ||
| } | ||
|
|
||
| async function call(name: string, input: Record<string, unknown>) { | ||
| const result = await client.callTool({ name, arguments: input }); | ||
| assert.notEqual( | ||
| result.isError, | ||
| true, | ||
| (result.content as TextContent[])[0]?.text, | ||
| ); | ||
| return result.structuredContent as { | ||
| receipts: { | ||
| issueIdentifier: string; | ||
| operation: string; | ||
| readback?: { status: string; error?: string }; | ||
| }[]; | ||
| }; | ||
| } |
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
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
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.