| author | |
| committer | |
| log | aa5a144d04c5067b9344289c6bc6f28a8acb9765 |
| tree | 9b9499df10b6fce045c53e56d26bcb4554581fee |
| parent | c6d1f92ba9f2a820cdbee71ff6b9499d1886bba6 |
7 files changed, 48 insertions(+), 21 deletions(-)
.plans/16c-pr89-remediation-checklist.md+4-4| ... | ... | @@ -286,8 +286,8 @@ Counts: active `51` (`valid=33`, `partially-valid=18`), closed-invalid `6` |
| 286 | 286 | - Threads: PRRT_kwDORLtfbc5wj4cM |
| 287 | 287 | - Audit note: -ilc shell invocation can pollute captured PATH output. |
| 288 | 288 | |
| 289 | - [ ] `C029` `parseFileUrlHref` already decodes the path (line 46), but `safeDecode` is called again here, corrupting filenames containing `%` sequences. Consider skipping the decode when `fileUrlTarget` is non-null. | |
| 290 | - Status: `TODO` | |
| 289 | - [x] `C029` `parseFileUrlHref` already decodes the path (line 46), but `safeDecode` is called again here, corrupting filenames containing `%` sequences. Consider skipping the decode when `fileUrlTarget` is non-null. | |
| 290 | - Status: `DONE` | |
| 291 | 291 | - Verdict: `valid` |
| 292 | 292 | - Severity: `Medium` |
| 293 | 293 | - Area: `Edge-case parsing/platform behavior` |
| ... | ... | @@ -295,8 +295,8 @@ Counts: active `51` (`valid=33`, `partially-valid=18`), closed-invalid `6` |
| 295 | 295 | - Threads: PRRT_kwDORLtfbc5wnVsU |
| 296 | 296 | - Audit note: file URL decoding still double-decodes in one path. |
| 297 | 297 | |
| 298 | - [ ] `C030` `EXTERNAL_SCHEME_PATTERN` matches `script.ts:10` as a scheme because `.ts:` looks like `scheme:`. Consider requiring `://` after the colon, or checking that what follows the colon is not just digits. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary> | |
| 299 | - Status: `TODO` | |
| 298 | - [x] `C030` `EXTERNAL_SCHEME_PATTERN` matches `script.ts:10` as a scheme because `.ts:` looks like `scheme:`. Consider requiring `://` after the colon, or checking that what follows the colon is not just digits. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary> | |
| 299 | - Status: `DONE` | |
| 300 | 300 | - Verdict: `valid` |
| 301 | 301 | - Severity: `Medium` |
| 302 | 302 | - Area: `Edge-case parsing/platform behavior` |
apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts+4-8| ... | ... | @@ -14,10 +14,6 @@ import { CheckpointDiffQueryLive } from "./CheckpointDiffQuery.ts"; |
| 14 | 14 | import { CheckpointStore, type CheckpointStoreShape } from "../Services/CheckpointStore.ts"; |
| 15 | 15 | import { CheckpointDiffQuery } from "../Services/CheckpointDiffQuery.ts"; |
| 16 | 16 | |
| 17 | const asProjectId = (value: string): ProjectId => ProjectId.makeUnsafe(value); | |
| 18 | const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); | |
| 19 | const asCheckpointRef = (value: string): CheckpointRef => CheckpointRef.makeUnsafe(value); | |
| 20 | ||
| 21 | 17 | function makeSnapshot(input: { |
| 22 | 18 | readonly projectId: ProjectId; |
| 23 | 19 | readonly threadId: ThreadId; |
| ... | ... | @@ -49,7 +45,7 @@ function makeSnapshot(input: { |
| 49 | 45 | model: "gpt-5-codex", |
| 50 | 46 | branch: null, |
| 51 | 47 | worktreePath: input.worktreePath, |
| 52 | latestTurnId: asTurnId("turn-1"), | |
| 48 | latestTurnId: TurnId.makeUnsafe("turn-1"), | |
| 53 | 49 | createdAt: "2026-01-01T00:00:00.000Z", |
| 54 | 50 | updatedAt: "2026-01-01T00:00:00.000Z", |
| 55 | 51 | deletedAt: null, |
| ... | ... | @@ -57,7 +53,7 @@ function makeSnapshot(input: { |
| 57 | 53 | activities: [], |
| 58 | 54 | checkpoints: [ |
| 59 | 55 | { |
| 60 | turnId: asTurnId("turn-1"), | |
| 56 | turnId: TurnId.makeUnsafe("turn-1"), | |
| 61 | 57 | checkpointTurnCount: input.checkpointTurnCount, |
| 62 | 58 | checkpointRef: input.checkpointRef, |
| 63 | 59 | status: "ready", |
| ... | ... | @@ -74,9 +70,9 @@ function makeSnapshot(input: { |
| 74 | 70 | |
| 75 | 71 | describe("CheckpointDiffQueryLive", () => { |
| 76 | 72 | it("computes diffs using canonical turn-0 checkpoint refs", async () => { |
| 77 | const projectId = asProjectId("project-1"); | |
| 73 | const projectId = ProjectId.makeUnsafe("project-1"); | |
| 78 | 74 | const threadId = ThreadId.makeUnsafe("thread-1"); |
| 79 | const toCheckpointRef = asCheckpointRef("refs/t3/checkpoints/thread-1/turn/1"); | |
| 75 | const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); | |
| 80 | 76 | const hasCheckpointRefCalls: Array<CheckpointRef> = []; |
| 81 | 77 | const diffCheckpointsCalls: Array<{ |
| 82 | 78 | readonly fromCheckpointRef: CheckpointRef; |
apps/server/src/codexAppServerManager.ts+1-1| ... | ... | @@ -986,7 +986,7 @@ function readResumeThreadId(input: ProviderSessionStartInput): ProviderThreadId |
| 986 | 986 | typeof input.resumeCursor !== "object" || |
| 987 | 987 | Array.isArray(input.resumeCursor) |
| 988 | 988 | ) { |
| 989 | return undefined; | |
| 989 | return input.resumeThreadId; | |
| 990 | 990 | } |
| 991 | 991 | const rawThreadId = (input.resumeCursor as Record<string, unknown>).threadId; |
| 992 | 992 | return typeof rawThreadId === "string" ? toProviderThreadId(rawThreadId) : input.resumeThreadId; |
apps/server/src/open.ts+11-4| ... | ... | @@ -85,10 +85,17 @@ export const resolveEditorLaunch = Effect.fnUntraced(function* ( |
| 85 | 85 | |
| 86 | 86 | export const launchDetached = (launch: EditorLaunch) => |
| 87 | 87 | Effect.callback<void, OpenError>((resume) => { |
| 88 | const child = spawn(launch.command, [...launch.args], { | |
| 89 | detached: true, | |
| 90 | stdio: "ignore", | |
| 91 | }); | |
| 88 | let child; | |
| 89 | try { | |
| 90 | child = spawn(launch.command, [...launch.args], { | |
| 91 | detached: true, | |
| 92 | stdio: "ignore", | |
| 93 | }); | |
| 94 | } catch (error) { | |
| 95 | return resume( | |
| 96 | Effect.fail(new OpenError({ message: "failed to spawn detached process", cause: error })), | |
| 97 | ); | |
| 98 | } | |
| 92 | 99 | |
| 93 | 100 | const handleSpawn = () => { |
| 94 | 101 | child.unref(); |
apps/server/src/terminal/Layers/Manager.ts+4-1| ... | ... | @@ -1033,7 +1033,10 @@ export const TerminalManagerLive = Layer.effect( |
| 1033 | 1033 | const logsDir = join(stateDir, "logs", "terminals"); |
| 1034 | 1034 | |
| 1035 | 1035 | const ptyAdapter = yield* PtyAdapter; |
| 1036 | const runtime = new TerminalManagerRuntime({ logsDir, ptyAdapter }); | |
| 1036 | const runtime = yield* Effect.acquireRelease( | |
| 1037 | Effect.sync(() => new TerminalManagerRuntime({ logsDir, ptyAdapter })), | |
| 1038 | (r) => Effect.sync(() => r.dispose()), | |
| 1039 | ); | |
| 1037 | 1040 | |
| 1038 | 1041 | return { |
| 1039 | 1042 | open: (input) => Effect.promise(() => runtime.open(input)), |
apps/web/src/markdown-links.test.ts+12| ... | ... | @@ -15,6 +15,12 @@ describe("resolveMarkdownFileLinkTarget", () => { |
| 15 | 15 | ); |
| 16 | 16 | }); |
| 17 | 17 | |
| 18 | it("does not treat filename line references as external schemes", () => { | |
| 19 | expect(resolveMarkdownFileLinkTarget("script.ts:10", "/Users/julius/project")).toBe( | |
| 20 | "/Users/julius/project/script.ts:10", | |
| 21 | ); | |
| 22 | }); | |
| 23 | ||
| 18 | 24 | it("resolves bare file names against cwd", () => { |
| 19 | 25 | expect(resolveMarkdownFileLinkTarget("AGENTS.md", "/Users/julius/project")).toBe( |
| 20 | 26 | "/Users/julius/project/AGENTS.md", |
| ... | ... | @@ -31,6 +37,12 @@ describe("resolveMarkdownFileLinkTarget", () => { |
| 31 | 37 | expect(resolveMarkdownFileLinkTarget("https://example.com/docs")).toBeNull(); |
| 32 | 38 | }); |
| 33 | 39 | |
| 40 | it("does not double-decode file URLs", () => { | |
| 41 | expect(resolveMarkdownFileLinkTarget("file:///Users/julius/project/file%2520name.md")).toBe( | |
| 42 | "/Users/julius/project/file%20name.md", | |
| 43 | ); | |
| 44 | }); | |
| 45 | ||
| 34 | 46 | it("does not treat app routes as file links", () => { |
| 35 | 47 | expect(resolveMarkdownFileLinkTarget("/chat/settings")).toBeNull(); |
| 36 | 48 | }); |
apps/web/src/markdown-links.ts+12-3| ... | ... | @@ -2,11 +2,12 @@ import { resolvePathLinkTarget } from "./terminal-links"; |
| 2 | 2 | |
| 3 | 3 | const WINDOWS_DRIVE_PATH_PATTERN = /^[A-Za-z]:[\\/]/; |
| 4 | 4 | const WINDOWS_UNC_PATH_PATTERN = /^\\\\/; |
| 5 | const EXTERNAL_SCHEME_PATTERN = /^[A-Za-z][A-Za-z0-9+.-]*:/; | |
| 5 | const EXTERNAL_SCHEME_PATTERN = /^([A-Za-z][A-Za-z0-9+.-]*):(.*)$/; | |
| 6 | 6 | const RELATIVE_PATH_PREFIX_PATTERN = /^(~\/|\.{1,2}\/)/; |
| 7 | 7 | const RELATIVE_FILE_PATH_PATTERN = /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+(?::\d+){0,2}$/; |
| 8 | 8 | const RELATIVE_FILE_NAME_PATTERN = /^[A-Za-z0-9._-]+\.[A-Za-z0-9_-]+(?::\d+){0,2}$/; |
| 9 | 9 | const POSITION_SUFFIX_PATTERN = /:\d+(?::\d+)?$/; |
| 10 | const POSITION_ONLY_PATTERN = /^\d+(?::\d+)?$/; | |
| 10 | 11 | const POSIX_FILE_ROOT_PREFIXES = [ |
| 11 | 12 | "/Users/", |
| 12 | 13 | "/home/", |
| ... | ... | @@ -89,6 +90,14 @@ function isRelativePath(path: string): boolean { |
| 89 | 90 | ); |
| 90 | 91 | } |
| 91 | 92 | |
| 93 | function hasExternalScheme(path: string): boolean { | |
| 94 | const match = path.match(EXTERNAL_SCHEME_PATTERN); | |
| 95 | if (!match) return false; | |
| 96 | const rest = match[2] ?? ""; | |
| 97 | if (rest.startsWith("//")) return true; | |
| 98 | return !POSITION_ONLY_PATTERN.test(rest); | |
| 99 | } | |
| 100 | ||
| 92 | 101 | export function resolveMarkdownFileLinkTarget( |
| 93 | 102 | href: string | undefined, |
| 94 | 103 | cwd?: string, |
| ... | ... | @@ -101,14 +110,14 @@ export function resolveMarkdownFileLinkTarget( |
| 101 | 110 | ? parseFileUrlHref(rawHref) |
| 102 | 111 | : null; |
| 103 | 112 | const source = fileUrlTarget ?? stripSearchAndHash(rawHref); |
| 104 | const decodedPath = safeDecode(source.path.trim()); | |
| 113 | const decodedPath = fileUrlTarget ? source.path.trim() : safeDecode(source.path.trim()); | |
| 105 | 114 | const decodedHash = safeDecode(source.hash.trim()); |
| 106 | 115 | |
| 107 | 116 | if (decodedPath.length === 0) return null; |
| 108 | 117 | if ( |
| 109 | 118 | !WINDOWS_DRIVE_PATH_PATTERN.test(decodedPath) && |
| 110 | 119 | !WINDOWS_UNC_PATH_PATTERN.test(decodedPath) && |
| 111 | EXTERNAL_SCHEME_PATTERN.test(decodedPath) | |
| 120 | hasExternalScheme(decodedPath) | |
| 112 | 121 | ) { |
| 113 | 122 | return null; |
| 114 | 123 | } |