apex-code 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/dist/client/transcript.d.ts.map +1 -1
- package/dist/client/transcript.js +1 -1
- package/dist/client/transcript.js.map +1 -1
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +2 -1
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/delegation/runtime.d.ts.map +1 -1
- package/dist/core/delegation/runtime.js +2 -2
- package/dist/core/delegation/runtime.js.map +1 -1
- package/dist/core/durable-state/daemon.d.ts.map +1 -1
- package/dist/core/durable-state/daemon.js +1 -1
- package/dist/core/durable-state/daemon.js.map +1 -1
- package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
- package/dist/core/export-html/tool-renderer.js +3 -0
- package/dist/core/export-html/tool-renderer.js.map +1 -1
- package/dist/core/extensions/types.d.ts +8 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/keybindings.d.ts +2 -2
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +1 -1
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/mcp/mcp-tool.d.ts.map +1 -1
- package/dist/core/mcp/mcp-tool.js +2 -4
- package/dist/core/mcp/mcp-tool.js.map +1 -1
- package/dist/core/mcp/oauth/authorize.d.ts.map +1 -1
- package/dist/core/mcp/oauth/authorize.js +7 -2
- package/dist/core/mcp/oauth/authorize.js.map +1 -1
- package/dist/core/mcp/runtime.d.ts +2 -2
- package/dist/core/mcp/runtime.d.ts.map +1 -1
- package/dist/core/mcp/runtime.js +8 -3
- package/dist/core/mcp/runtime.js.map +1 -1
- package/dist/core/permissions/store.d.ts +7 -2
- package/dist/core/permissions/store.d.ts.map +1 -1
- package/dist/core/permissions/store.js +6 -1
- package/dist/core/permissions/store.js.map +1 -1
- package/dist/core/project-resources.d.ts +12 -0
- package/dist/core/project-resources.d.ts.map +1 -1
- package/dist/core/project-resources.js +19 -0
- package/dist/core/project-resources.js.map +1 -1
- package/dist/core/provider-composer.d.ts.map +1 -1
- package/dist/core/provider-composer.js +10 -1
- package/dist/core/provider-composer.js.map +1 -1
- package/dist/core/resource-loader.d.ts +6 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +9 -3
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +3 -3
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts +16 -3
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +38 -15
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/bash.d.ts +9 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +29 -16
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +50 -10
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/path-utils.d.ts +9 -0
- package/dist/core/tools/path-utils.d.ts.map +1 -1
- package/dist/core/tools/path-utils.js +235 -10
- package/dist/core/tools/path-utils.js.map +1 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +3 -3
- package/dist/core/tools/write.js.map +1 -1
- package/dist/core/workspace/state.d.ts.map +1 -1
- package/dist/core/workspace/state.js +1 -1
- package/dist/core/workspace/state.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +3 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/diff.d.ts.map +1 -1
- package/dist/modes/interactive/components/diff.js +2 -2
- package/dist/modes/interactive/components/diff.js.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts +7 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.js +3 -2
- package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
- package/dist/modes/interactive/components/model-row.d.ts +84 -0
- package/dist/modes/interactive/components/model-row.d.ts.map +1 -0
- package/dist/modes/interactive/components/model-row.js +206 -0
- package/dist/modes/interactive/components/model-row.js.map +1 -0
- package/dist/modes/interactive/components/model-selector.d.ts +39 -4
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +158 -27
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +2 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +8 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +53 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +100 -27
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +16 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +19 -1
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/tsconfig.json +3 -0
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +1 -1
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/syntax-highlight.d.ts.map +1 -1
- package/dist/utils/syntax-highlight.js +22 -22
- package/dist/utils/syntax-highlight.js.map +1 -1
- package/docs/models.md +1 -1
- package/examples/extensions/gondolin/package-lock.json +3 -3
- package/examples/extensions/minimal-mode.ts +6 -6
- package/examples/extensions/sandbox/package-lock.json +4 -4
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/sdk/10-settings.ts +2 -2
- package/examples/sdk/README.md +1 -1
- package/npm-shrinkwrap.json +19 -34
- package/package.json +6 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit.js","sourceRoot":"","sources":["../../../src/core/tools/edit.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { Box, Container, Spacer, Text } from \"@earendil-works/pi-tui\";\nimport type { AgentTool } from \"apex-code-agent-core\";\nimport { constants } from \"fs\";\nimport { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from \"fs/promises\";\nimport { type Static, Type } from \"typebox\";\nimport { renderDiff } from \"../../modes/interactive/components/diff.ts\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport { splitBom } from \"../../utils/text.ts\";\nimport { getExperimentalToolSampling } from \"../experimental.ts\";\nimport { getPreparedPathOperation, type PreparedPathOperation } from \"../permissions/operations.ts\";\nimport type { PermissionPreview } from \"../permissions/responder.ts\";\nimport type { ApexToolDefinition, EvidenceRecord } from \"./contract.ts\";\nimport type { DiagnosticsOperations, DiagnosticsOutcome } from \"./diagnostics.ts\";\nimport { diagnosticEvidenceForPath, formatDiagnosticsOutcome } from \"./diagnostics.ts\";\nimport {\n\tapplyEditsToNormalizedContent,\n\tcomputeEditsDiff,\n\tcomputeEditsDiffFromContent,\n\tdetectLineEnding,\n\ttype Edit,\n\ttype EditDiffError,\n\ttype EditDiffResult,\n\tgenerateDiffString,\n\tgenerateUnifiedPatch,\n\tnormalizeToLF,\n\trestoreLineEndings,\n} from \"./edit-diff.ts\";\nimport { withFileMutationQueue } from \"./file-mutation-queue.ts\";\nimport { createPathPermissionSpec } from \"./path-permission.ts\";\nimport { readPreparedPath, resolveToCwd, writePreparedPath } from \"./path-utils.ts\";\nimport { renderToolPath, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\ntype EditPreview = EditDiffResult | EditDiffError;\n\ntype EditRenderState = {\n\tcallComponent?: EditCallRenderComponent;\n};\n\nconst replaceEditSchema = Type.Object(\n\t{\n\t\toldText: Type.String({\n\t\t\tdescription:\n\t\t\t\t\"Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call.\",\n\t\t}),\n\t\tnewText: Type.String({ description: \"Replacement text for this targeted edit.\" }),\n\t},\n\t{},\n);\n\nconst editSchema = Type.Object(\n\t{\n\t\tpath: Type.String({ description: \"Path to the file to edit (relative or absolute)\" }),\n\t\tedits: Type.Array(replaceEditSchema, {\n\t\t\tdescription:\n\t\t\t\t\"One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead.\",\n\t\t}),\n\t},\n\t{},\n);\n\nexport const editToolSystemPromptContribution = {\n\tsnippet: \"Make precise file edits with exact text replacement, including multiple disjoint edits in one call\",\n\tguidelines: [\n\t\t\"Use edit for precise changes (edits[].oldText must match exactly)\",\n\t\t\"When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls\",\n\t\t\"Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.\",\n\t\t\"Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.\",\n\t],\n} as const;\n\nexport type EditToolInput = Static<typeof editSchema>;\ntype LegacyEditToolInput = EditToolInput & {\n\toldText?: unknown;\n\tnewText?: unknown;\n};\n\nfunction sha256(value: string): string {\n\treturn createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\ntype SingleEditInput = { oldText: string; newText: string };\n\nfunction isSingleEditInput(value: unknown): value is SingleEditInput {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) {\n\t\treturn false;\n\t}\n\n\tconst edit = value as Record<string, unknown>;\n\treturn typeof edit.oldText === \"string\" && typeof edit.newText === \"string\";\n}\n\nexport interface EditToolDetails {\n\t/** Display-oriented diff of the changes made */\n\tdiff: string;\n\t/** Standard unified patch of the changes made */\n\tpatch: string;\n\t/** Line number of the first change in the new file (for editor navigation) */\n\tfirstChangedLine?: number;\n\t/** Present only when `EditToolOptions.diagnosticsOperations` is configured (LSP.4). */\n\tdiagnostics?: DiagnosticsOutcome;\n}\n\n/**\n * Pluggable operations for the edit tool.\n * Override these to delegate file editing to remote systems (for example SSH).\n */\nexport interface EditOperations {\n\t/** Read file contents as a Buffer */\n\treadFile: (absolutePath: string) => Promise<Buffer>;\n\t/** Write content to a file */\n\twriteFile: (absolutePath: string, content: string) => Promise<void>;\n\t/** Check if file is readable and writable (throw if not) */\n\taccess: (absolutePath: string) => Promise<void>;\n\t/**\n\t * Execute a gate-prepared edit no-follow against the authorized target.\n\t * Required for gated calls: when a prepared operation exists, pathname reads\n\t * and writes are never used, so an unset implementation fails closed.\n\t */\n\treadPreparedFile?: (operation: PreparedPathOperation) => Buffer;\n\twritePreparedFile?: (operation: PreparedPathOperation, content: Buffer) => void;\n}\n\nconst defaultEditOperations: EditOperations = {\n\treadFile: (path) => fsReadFile(path),\n\twriteFile: (path, content) => fsWriteFile(path, content, \"utf-8\"),\n\taccess: (path) => fsAccess(path, constants.R_OK | constants.W_OK),\n\treadPreparedFile: (operation) => readPreparedPath(operation),\n\twritePreparedFile: (operation, content) => writePreparedPath(operation, content),\n};\n\nexport interface EditToolOptions {\n\t/** Custom operations for file editing. Default: local filesystem */\n\toperations?: EditOperations;\n\t/**\n\t * Optional post-mutation diagnostics (LSP.4). Absent by default -- byte-identical\n\t * existing behavior. When present, called inside `withFileMutationQueue` after the\n\t * write succeeds and before the queue releases, matching `TestOperations`/\n\t * `GrepOperations`' injection pattern (`test.ts:28`, `grep.ts:58`).\n\t */\n\tdiagnosticsOperations?: DiagnosticsOperations;\n}\n\n/** A preview is a courtesy, so it never costs more than a screen or two of reading. */\nconst PREVIEW_MAX_BYTES = 512 * 1024;\nconst PREVIEW_MAX_LINES = 40;\n\n/**\n * Describe the edit for the person deciding whether to allow it.\n *\n * Reads through the operation `prepareCall` stored rather than resolving the\n * pathname again, so a target swapped after authorization fails here exactly as\n * it fails the write. `readPreparedPath` throws in that case and the gate turns\n * the throw into a stated reason, which is the outcome a reader needs: better a\n * prompt that admits it cannot show the change than a diff of the wrong file.\n */\nfunction previewEdit(params: EditToolInput): PermissionPreview {\n\tconst prepared = getPreparedPathOperation(params);\n\tif (!prepared) return { kind: \"unavailable\", reason: \"No authorized target was prepared for this call\" };\n\tif (prepared.kind === \"path-new\") {\n\t\treturn { kind: \"summary\", lines: [`Create ${params.path}`] };\n\t}\n\n\tconst buffer = readPreparedPath(prepared, PREVIEW_MAX_BYTES + 1);\n\tif (buffer.byteLength > PREVIEW_MAX_BYTES) {\n\t\treturn { kind: \"unavailable\", reason: `File is larger than ${PREVIEW_MAX_BYTES / 1024}KB, so no diff is shown` };\n\t}\n\tif (buffer.includes(0)) {\n\t\treturn { kind: \"unavailable\", reason: \"File is binary, so there is no diff to show\" };\n\t}\n\n\tconst result = computeEditsDiffFromContent(params.path, buffer.toString(\"utf-8\"), params.edits as Edit[]);\n\tif (\"error\" in result) return { kind: \"unavailable\", reason: result.error };\n\n\tconst lines = result.diff.split(\"\\n\");\n\tconst shown = lines.slice(0, PREVIEW_MAX_LINES);\n\treturn { kind: \"diff\", path: params.path, lines: shown, omittedLines: lines.length - shown.length };\n}\n\nfunction prepareEditArguments(input: unknown): EditToolInput {\n\tif (!input || typeof input !== \"object\") {\n\t\treturn input as EditToolInput;\n\t}\n\n\tconst args = input as Record<string, unknown>;\n\n\t// Some models (Opus 4.6, GLM-5.1) send edits as a JSON string instead of an array.\n\t// Others send a single edit object instead of a one-element edits array.\n\tif (typeof args.edits === \"string\") {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(args.edits);\n\t\t\tif (Array.isArray(parsed)) {\n\t\t\t\targs.edits = parsed;\n\t\t\t} else if (isSingleEditInput(parsed)) {\n\t\t\t\targs.edits = [parsed];\n\t\t\t}\n\t\t} catch {}\n\t} else if (isSingleEditInput(args.edits)) {\n\t\targs.edits = [args.edits];\n\t}\n\n\tconst legacy = args as LegacyEditToolInput;\n\tif (typeof legacy.oldText !== \"string\" || typeof legacy.newText !== \"string\") {\n\t\treturn args as EditToolInput;\n\t}\n\n\tconst edits = Array.isArray(legacy.edits) ? [...legacy.edits] : [];\n\tedits.push({ oldText: legacy.oldText, newText: legacy.newText });\n\tconst { oldText: _oldText, newText: _newText, ...rest } = legacy;\n\treturn { ...rest, edits } as EditToolInput;\n}\n\nfunction validateEditInput(input: EditToolInput): { path: string; edits: Edit[] } {\n\tif (!Array.isArray(input.edits) || input.edits.length === 0) {\n\t\tthrow new Error(\"Edit tool input is invalid. edits must contain at least one replacement.\");\n\t}\n\treturn { path: input.path, edits: input.edits };\n}\n\ntype RenderableEditArgs = {\n\tpath?: string;\n\tfile_path?: string;\n\tedits?: Edit[];\n\toldText?: string;\n\tnewText?: string;\n};\n\ntype EditToolResultLike = {\n\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\tdetails?: EditToolDetails;\n};\n\ntype EditCallRenderComponent = Box & {\n\tpreview?: EditPreview;\n\tpreviewArgsKey?: string;\n\tpreviewPending?: boolean;\n\tsettledError?: boolean;\n};\n\nfunction createEditCallRenderComponent(): EditCallRenderComponent {\n\treturn Object.assign(new Box(1, 1, (text: string) => text), {\n\t\tpreview: undefined as EditPreview | undefined,\n\t\tpreviewArgsKey: undefined as string | undefined,\n\t\tpreviewPending: false,\n\t\tsettledError: false,\n\t});\n}\n\nfunction getEditCallRenderComponent(state: EditRenderState, lastComponent: unknown): EditCallRenderComponent {\n\tif (lastComponent instanceof Box) {\n\t\tconst component = lastComponent as EditCallRenderComponent;\n\t\tstate.callComponent = component;\n\t\treturn component;\n\t}\n\tif (state.callComponent) {\n\t\treturn state.callComponent;\n\t}\n\tconst component = createEditCallRenderComponent();\n\tstate.callComponent = component;\n\treturn component;\n}\n\nfunction getRenderablePreviewInput(args: RenderableEditArgs | undefined): { path: string; edits: Edit[] } | null {\n\tif (!args) {\n\t\treturn null;\n\t}\n\n\tconst path = typeof args.path === \"string\" ? args.path : typeof args.file_path === \"string\" ? args.file_path : null;\n\tif (!path) {\n\t\treturn null;\n\t}\n\n\tif (\n\t\tArray.isArray(args.edits) &&\n\t\targs.edits.length > 0 &&\n\t\targs.edits.every((edit) => typeof edit?.oldText === \"string\" && typeof edit?.newText === \"string\")\n\t) {\n\t\treturn { path, edits: args.edits };\n\t}\n\n\tif (typeof args.oldText === \"string\" && typeof args.newText === \"string\") {\n\t\treturn { path, edits: [{ oldText: args.oldText, newText: args.newText }] };\n\t}\n\n\treturn null;\n}\n\nfunction formatEditCall(args: RenderableEditArgs | undefined, theme: Theme, cwd: string): string {\n\tconst pathDisplay = renderToolPath(str(args?.file_path ?? args?.path), theme, cwd);\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"edit\"))} ${pathDisplay}`;\n}\n\nfunction formatEditResult(\n\targs: RenderableEditArgs | undefined,\n\tpreview: EditPreview | undefined,\n\tresult: EditToolResultLike,\n\ttheme: Theme,\n\tisError: boolean,\n): string | undefined {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst previewDiff = preview && !(\"error\" in preview) ? preview.diff : undefined;\n\tconst previewError = preview && \"error\" in preview ? preview.error : undefined;\n\tif (isError) {\n\t\tconst errorText = result.content\n\t\t\t.filter((c) => c.type === \"text\")\n\t\t\t.map((c) => c.text || \"\")\n\t\t\t.join(\"\\n\");\n\t\tif (!errorText || errorText === previewError) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn theme.fg(\"error\", errorText);\n\t}\n\n\tconst resultDiff = result.details?.diff;\n\tif (resultDiff && resultDiff !== previewDiff) {\n\t\treturn renderDiff(resultDiff, { filePath: rawPath ?? undefined });\n\t}\n\n\treturn undefined;\n}\n\nfunction getEditHeaderBg(\n\tpreview: EditPreview | undefined,\n\tsettledError: boolean | undefined,\n\ttheme: Theme,\n): (text: string) => string {\n\tif (preview) {\n\t\tif (\"error\" in preview) {\n\t\t\treturn (text: string) => theme.bg(\"toolErrorBg\", text);\n\t\t}\n\t\treturn (text: string) => theme.bg(\"toolSuccessBg\", text);\n\t}\n\tif (settledError) {\n\t\treturn (text: string) => theme.bg(\"toolErrorBg\", text);\n\t}\n\treturn (text: string) => theme.bg(\"toolPendingBg\", text);\n}\n\nfunction buildEditCallComponent(\n\tcomponent: EditCallRenderComponent,\n\targs: RenderableEditArgs | undefined,\n\ttheme: Theme,\n\tcwd: string,\n): EditCallRenderComponent {\n\tcomponent.setBgFn(getEditHeaderBg(component.preview, component.settledError, theme));\n\tcomponent.clear();\n\tcomponent.addChild(new Text(formatEditCall(args, theme, cwd), 0, 0));\n\n\tif (!component.preview) {\n\t\treturn component;\n\t}\n\n\tconst body =\n\t\t\"error\" in component.preview ? theme.fg(\"error\", component.preview.error) : renderDiff(component.preview.diff);\n\tcomponent.addChild(new Spacer(1));\n\tcomponent.addChild(new Text(body, 0, 0));\n\treturn component;\n}\n\nfunction setEditPreview(\n\tcomponent: EditCallRenderComponent,\n\tpreview: EditPreview,\n\targsKey: string | undefined,\n): boolean {\n\tconst current = component.preview;\n\tconst changed =\n\t\tcurrent === undefined ||\n\t\t(\"error\" in current && \"error\" in preview\n\t\t\t? current.error !== preview.error\n\t\t\t: \"error\" in current !== \"error\" in preview) ||\n\t\t(!(\"error\" in current) &&\n\t\t\t!(\"error\" in preview) &&\n\t\t\t(current.diff !== preview.diff || current.firstChangedLine !== preview.firstChangedLine));\n\tcomponent.preview = preview;\n\tcomponent.previewArgsKey = argsKey;\n\tcomponent.previewPending = false;\n\treturn changed;\n}\n\nexport function createEditToolDefinition(\n\tcwd: string,\n\toptions?: EditToolOptions,\n): ApexToolDefinition<typeof editSchema, EditToolDetails | undefined, EditRenderState> {\n\tconst ops = options?.operations ?? defaultEditOperations;\n\tconst diagnosticsOperations = options?.diagnosticsOperations;\n\treturn {\n\t\tname: \"edit\",\n\t\tlabel: \"edit\",\n\t\tdescription:\n\t\t\t\"Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.\",\n\t\tpromptSnippet: editToolSystemPromptContribution.snippet,\n\t\tpromptGuidelines: [...editToolSystemPromptContribution.guidelines],\n\t\tparameters: editSchema,\n\t\tcontract: {\n\t\t\tcapabilities: new Set([\"fs.read\", \"fs.write\"]),\n\t\t\tpermission: createPathPermissionSpec({\n\t\t\t\tcwd,\n\t\t\t\tdefaultBehavior: \"ask\",\n\t\t\t\tverb: \"Edit\",\n\t\t\t\tgetPath: (params) => params.path,\n\t\t\t\tpreviewCall: (params) => previewEdit(params as EditToolInput),\n\t\t\t}),\n\t\t\tcontext: { resultRecoverable: true, deferSchema: false },\n\t\t\tevidence: {\n\t\t\t\temits: new Set(diagnosticsOperations ? [\"diff\", \"diagnostic\"] : [\"diff\"]),\n\t\t\t\tcapture: (params, result) => {\n\t\t\t\t\tconst patch = result.details?.patch;\n\t\t\t\t\tconst records: EvidenceRecord[] = patch\n\t\t\t\t\t\t? [{ kind: \"diff\", path: params.path, patchHash: sha256(patch) }]\n\t\t\t\t\t\t: [{ kind: \"diff\", path: params.path }];\n\t\t\t\t\tconst diagnostics = result.details?.diagnostics;\n\t\t\t\t\tif (diagnostics) records.push(diagnosticEvidenceForPath(params.path, diagnostics));\n\t\t\t\t\treturn records;\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tconstrainedSampling: getExperimentalToolSampling(),\n\t\trenderShell: \"self\",\n\t\tprepareArguments: prepareEditArguments,\n\t\tasync execute(_toolCallId, input: EditToolInput, signal?: AbortSignal, _onUpdate?, _ctx?) {\n\t\t\tconst { path, edits } = validateEditInput(input);\n\t\t\tconst prepared = getPreparedPathOperation(input);\n\t\t\tconst absolutePath = prepared?.path.value ?? resolveToCwd(path, cwd);\n\t\t\tif (prepared && (!ops.readPreparedFile || !ops.writePreparedFile)) {\n\t\t\t\tthrow new Error(\"No safe prepared edit operation is available for this gated edit\");\n\t\t\t}\n\n\t\t\treturn withFileMutationQueue(absolutePath, async () => {\n\t\t\t\t// Do not reject from an abort event listener here: that would release the\n\t\t\t\t// mutation queue while an in-flight filesystem operation may still finish.\n\t\t\t\t// Checking signal.aborted after each await observes the same aborts while\n\t\t\t\t// keeping the queue locked until the current operation has settled.\n\t\t\t\tconst throwIfAborted = (): void => {\n\t\t\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\t};\n\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\t// Check if file exists (skipped for a gated call: the prepared operation\n\t\t\t\t// already proves existence, and the no-follow read enforces the identity).\n\t\t\t\ttry {\n\t\t\t\t\tif (!prepared) await ops.access(absolutePath);\n\t\t\t\t} catch (error: unknown) {\n\t\t\t\t\tthrowIfAborted();\n\t\t\t\t\tconst errorMessage =\n\t\t\t\t\t\terror instanceof Error && \"code\" in error ? `Error code: ${error.code}` : String(error);\n\t\t\t\t\tthrow new Error(`Could not edit file: ${path}. ${errorMessage}.`);\n\t\t\t\t}\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\t// Read the file.\n\t\t\t\tconst buffer = prepared ? ops.readPreparedFile!(prepared) : await ops.readFile(absolutePath);\n\t\t\t\tconst rawContent = buffer.toString(\"utf-8\");\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\t// Strip BOM before matching. The model will not include an invisible BOM in oldText.\n\t\t\t\tconst { bom, text: content } = splitBom(rawContent);\n\t\t\t\tconst originalEnding = detectLineEnding(content);\n\t\t\t\tconst normalizedContent = normalizeToLF(content);\n\t\t\t\tconst { baseContent, newContent } = applyEditsToNormalizedContent(normalizedContent, edits, path);\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\tconst finalContent = bom + restoreLineEndings(newContent, originalEnding);\n\t\t\t\tif (prepared) ops.writePreparedFile!(prepared, Buffer.from(finalContent, \"utf-8\"));\n\t\t\t\telse await ops.writeFile(absolutePath, finalContent);\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\tconst diffResult = generateDiffString(baseContent, newContent);\n\t\t\t\tconst patch = generateUnifiedPatch(path, baseContent, newContent);\n\t\t\t\tconst successText = `Successfully replaced ${edits.length} block(s) in ${path}.`;\n\n\t\t\t\tif (diagnosticsOperations) {\n\t\t\t\t\tconst diagnostics = await diagnosticsOperations.afterMutation(absolutePath, signal);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: `${successText}\\n\\n${formatDiagnosticsOutcome(diagnostics)}` }],\n\t\t\t\t\t\tdetails: { diff: diffResult.diff, patch, firstChangedLine: diffResult.firstChangedLine, diagnostics },\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: successText }],\n\t\t\t\t\tdetails: { diff: diffResult.diff, patch, firstChangedLine: diffResult.firstChangedLine },\n\t\t\t\t};\n\t\t\t});\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst component = getEditCallRenderComponent(context.state, context.lastComponent);\n\t\t\tconst previewInput = getRenderablePreviewInput(args as RenderableEditArgs | undefined);\n\t\t\tconst argsKey = previewInput\n\t\t\t\t? JSON.stringify({ path: previewInput.path, edits: previewInput.edits })\n\t\t\t\t: undefined;\n\n\t\t\tif (component.previewArgsKey !== argsKey) {\n\t\t\t\tcomponent.preview = undefined;\n\t\t\t\tcomponent.previewArgsKey = argsKey;\n\t\t\t\tcomponent.previewPending = false;\n\t\t\t\tcomponent.settledError = false;\n\t\t\t}\n\n\t\t\tif (context.argsComplete && previewInput && !component.preview && !component.previewPending) {\n\t\t\t\tcomponent.previewPending = true;\n\t\t\t\tconst requestKey = argsKey;\n\t\t\t\tvoid computeEditsDiff(previewInput.path, previewInput.edits, context.cwd).then((preview) => {\n\t\t\t\t\tif (component.previewArgsKey === requestKey) {\n\t\t\t\t\t\tsetEditPreview(component, preview, requestKey);\n\t\t\t\t\t\tcontext.invalidate();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn buildEditCallComponent(component, args, theme, context.cwd);\n\t\t},\n\t\trenderResult(result, _options, theme, context) {\n\t\t\tconst callComponent = context.state.callComponent;\n\t\t\tconst previewInput = getRenderablePreviewInput(context.args as RenderableEditArgs | undefined);\n\t\t\tconst argsKey = previewInput\n\t\t\t\t? JSON.stringify({ path: previewInput.path, edits: previewInput.edits })\n\t\t\t\t: undefined;\n\t\t\tconst typedResult = result as EditToolResultLike;\n\t\t\tconst resultDiff = !context.isError ? typedResult.details?.diff : undefined;\n\t\t\tlet changed = false;\n\t\t\tif (callComponent) {\n\t\t\t\tif (typeof resultDiff === \"string\") {\n\t\t\t\t\tchanged =\n\t\t\t\t\t\tsetEditPreview(\n\t\t\t\t\t\t\tcallComponent,\n\t\t\t\t\t\t\t{ diff: resultDiff, firstChangedLine: typedResult.details?.firstChangedLine },\n\t\t\t\t\t\t\targsKey,\n\t\t\t\t\t\t) || changed;\n\t\t\t\t}\n\t\t\t\tif (callComponent.settledError !== context.isError) {\n\t\t\t\t\tcallComponent.settledError = context.isError;\n\t\t\t\t\tchanged = true;\n\t\t\t\t}\n\t\t\t\tif (changed) {\n\t\t\t\t\tbuildEditCallComponent(\n\t\t\t\t\t\tcallComponent,\n\t\t\t\t\t\tcontext.args as RenderableEditArgs | undefined,\n\t\t\t\t\t\ttheme,\n\t\t\t\t\t\tcontext.cwd,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst output = formatEditResult(context.args, callComponent?.preview, typedResult, theme, context.isError);\n\t\t\tconst component = (context.lastComponent as Container | undefined) ?? new Container();\n\t\t\tcomponent.clear();\n\t\t\tif (!output) {\n\t\t\t\treturn component;\n\t\t\t}\n\t\t\tcomponent.addChild(new Spacer(1));\n\t\t\tcomponent.addChild(new Text(output, 1, 0));\n\t\t\treturn component;\n\t\t},\n\t};\n}\n\nexport function createEditTool(cwd: string, options?: EditToolOptions): AgentTool<typeof editSchema> {\n\treturn wrapToolDefinition(createEditToolDefinition(cwd, options));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"edit.js","sourceRoot":"","sources":["../../../src/core/tools/edit.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { Box, Container, Spacer, Text } from \"@earendil-works/pi-tui\";\nimport type { AgentTool } from \"apex-code-agent-core\";\nimport { constants } from \"fs\";\nimport { access as fsAccess, readFile as fsReadFile } from \"fs/promises\";\nimport { type Static, Type } from \"typebox\";\nimport { renderDiff } from \"../../modes/interactive/components/diff.ts\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport { splitBom } from \"../../utils/text.ts\";\nimport { getExperimentalToolSampling } from \"../experimental.ts\";\nimport { getPreparedPathOperation, type PreparedPathOperation } from \"../permissions/operations.ts\";\nimport type { PermissionPreview } from \"../permissions/responder.ts\";\nimport type { ApexToolDefinition, EvidenceRecord } from \"./contract.ts\";\nimport type { DiagnosticsOperations, DiagnosticsOutcome } from \"./diagnostics.ts\";\nimport { diagnosticEvidenceForPath, formatDiagnosticsOutcome } from \"./diagnostics.ts\";\nimport {\n\tapplyEditsToNormalizedContent,\n\tcomputeEditsDiff,\n\tcomputeEditsDiffFromContent,\n\tdetectLineEnding,\n\ttype Edit,\n\ttype EditDiffError,\n\ttype EditDiffResult,\n\tgenerateDiffString,\n\tgenerateUnifiedPatch,\n\tnormalizeToLF,\n\trestoreLineEndings,\n} from \"./edit-diff.ts\";\nimport { withFileMutationQueue } from \"./file-mutation-queue.ts\";\nimport { createPathPermissionSpec } from \"./path-permission.ts\";\nimport { readPreparedPath, resolveToCwd, writePathAtomically, writePreparedPath } from \"./path-utils.ts\";\nimport { renderToolPath, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\ntype EditPreview = EditDiffResult | EditDiffError;\n\ntype EditRenderState = {\n\tcallComponent?: EditCallRenderComponent;\n};\n\nconst replaceEditSchema = Type.Object(\n\t{\n\t\toldText: Type.String({\n\t\t\tdescription:\n\t\t\t\t\"Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call.\",\n\t\t}),\n\t\tnewText: Type.String({ description: \"Replacement text for this targeted edit.\" }),\n\t},\n\t{},\n);\n\nconst editSchema = Type.Object(\n\t{\n\t\tpath: Type.String({ description: \"Path to the file to edit (relative or absolute)\" }),\n\t\tedits: Type.Array(replaceEditSchema, {\n\t\t\tdescription:\n\t\t\t\t\"One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead.\",\n\t\t}),\n\t},\n\t{},\n);\n\nexport const editToolSystemPromptContribution = {\n\tsnippet: \"Make precise file edits with exact text replacement, including multiple disjoint edits in one call\",\n\tguidelines: [\n\t\t\"Use edit for precise changes (edits[].oldText must match exactly)\",\n\t\t\"When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls\",\n\t\t\"Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.\",\n\t\t\"Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.\",\n\t],\n} as const;\n\nexport type EditToolInput = Static<typeof editSchema>;\ntype LegacyEditToolInput = EditToolInput & {\n\toldText?: unknown;\n\tnewText?: unknown;\n};\n\nfunction sha256(value: string): string {\n\treturn createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\ntype SingleEditInput = { oldText: string; newText: string };\n\nfunction isSingleEditInput(value: unknown): value is SingleEditInput {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) {\n\t\treturn false;\n\t}\n\n\tconst edit = value as Record<string, unknown>;\n\treturn typeof edit.oldText === \"string\" && typeof edit.newText === \"string\";\n}\n\nexport interface EditToolDetails {\n\t/** Display-oriented diff of the changes made */\n\tdiff: string;\n\t/** Standard unified patch of the changes made */\n\tpatch: string;\n\t/** Line number of the first change in the new file (for editor navigation) */\n\tfirstChangedLine?: number;\n\t/** Present only when `EditToolOptions.diagnosticsOperations` is configured (LSP.4). */\n\tdiagnostics?: DiagnosticsOutcome;\n}\n\n/**\n * Pluggable operations for the edit tool.\n * Override these to delegate file editing to remote systems (for example SSH).\n */\nexport interface EditOperations {\n\t/** Read file contents as a Buffer */\n\treadFile: (absolutePath: string) => Promise<Buffer>;\n\t/** Write content to a file */\n\twriteFile: (absolutePath: string, content: string) => Promise<void>;\n\t/** Check if file is readable and writable (throw if not) */\n\taccess: (absolutePath: string) => Promise<void>;\n\t/**\n\t * Execute a gate-prepared edit no-follow against the authorized target.\n\t * Required for gated calls: when a prepared operation exists, pathname reads\n\t * and writes are never used, so an unset implementation fails closed.\n\t */\n\treadPreparedFile?: (operation: PreparedPathOperation) => Buffer;\n\twritePreparedFile?: (operation: PreparedPathOperation, content: Buffer) => void;\n}\n\nconst defaultEditOperations: EditOperations = {\n\treadFile: (path) => fsReadFile(path),\n\twriteFile: async (path, content) => writePathAtomically(path, content),\n\taccess: (path) => fsAccess(path, constants.R_OK | constants.W_OK),\n\treadPreparedFile: (operation) => readPreparedPath(operation),\n\twritePreparedFile: (operation, content) => writePreparedPath(operation, content),\n};\n\nexport interface EditToolOptions {\n\t/** Custom operations for file editing. Default: local filesystem */\n\toperations?: EditOperations;\n\t/**\n\t * Optional post-mutation diagnostics (LSP.4). Absent by default -- byte-identical\n\t * existing behavior. When present, called inside `withFileMutationQueue` after the\n\t * write succeeds and before the queue releases, matching `TestOperations`/\n\t * `GrepOperations`' injection pattern (`test.ts:28`, `grep.ts:58`).\n\t */\n\tdiagnosticsOperations?: DiagnosticsOperations;\n}\n\n/** A preview is a courtesy, so it never costs more than a screen or two of reading. */\nconst PREVIEW_MAX_BYTES = 512 * 1024;\nconst PREVIEW_MAX_LINES = 40;\n\n/**\n * Describe the edit for the person deciding whether to allow it.\n *\n * Reads through the operation `prepareCall` stored rather than resolving the\n * pathname again, so a target swapped after authorization fails here exactly as\n * it fails the write. `readPreparedPath` throws in that case and the gate turns\n * the throw into a stated reason, which is the outcome a reader needs: better a\n * prompt that admits it cannot show the change than a diff of the wrong file.\n */\nfunction previewEdit(params: EditToolInput): PermissionPreview {\n\tconst prepared = getPreparedPathOperation(params);\n\tif (!prepared) return { kind: \"unavailable\", reason: \"No authorized target was prepared for this call\" };\n\tif (prepared.kind === \"path-new\") {\n\t\treturn { kind: \"summary\", lines: [`Create ${params.path}`] };\n\t}\n\n\tconst buffer = readPreparedPath(prepared, PREVIEW_MAX_BYTES + 1);\n\tif (buffer.byteLength > PREVIEW_MAX_BYTES) {\n\t\treturn { kind: \"unavailable\", reason: `File is larger than ${PREVIEW_MAX_BYTES / 1024}KB, so no diff is shown` };\n\t}\n\tif (buffer.includes(0)) {\n\t\treturn { kind: \"unavailable\", reason: \"File is binary, so there is no diff to show\" };\n\t}\n\n\tconst result = computeEditsDiffFromContent(params.path, buffer.toString(\"utf-8\"), params.edits as Edit[]);\n\tif (\"error\" in result) return { kind: \"unavailable\", reason: result.error };\n\n\tconst lines = result.diff.split(\"\\n\");\n\tconst shown = lines.slice(0, PREVIEW_MAX_LINES);\n\treturn { kind: \"diff\", path: params.path, lines: shown, omittedLines: lines.length - shown.length };\n}\n\nfunction prepareEditArguments(input: unknown): EditToolInput {\n\tif (!input || typeof input !== \"object\") {\n\t\treturn input as EditToolInput;\n\t}\n\n\tconst args = input as Record<string, unknown>;\n\n\t// Some models (Opus 4.6, GLM-5.1) send edits as a JSON string instead of an array.\n\t// Others send a single edit object instead of a one-element edits array.\n\tif (typeof args.edits === \"string\") {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(args.edits);\n\t\t\tif (Array.isArray(parsed)) {\n\t\t\t\targs.edits = parsed;\n\t\t\t} else if (isSingleEditInput(parsed)) {\n\t\t\t\targs.edits = [parsed];\n\t\t\t}\n\t\t} catch {}\n\t} else if (isSingleEditInput(args.edits)) {\n\t\targs.edits = [args.edits];\n\t}\n\n\tconst legacy = args as LegacyEditToolInput;\n\tif (typeof legacy.oldText !== \"string\" || typeof legacy.newText !== \"string\") {\n\t\treturn args as EditToolInput;\n\t}\n\n\tconst edits = Array.isArray(legacy.edits) ? [...legacy.edits] : [];\n\tedits.push({ oldText: legacy.oldText, newText: legacy.newText });\n\tconst { oldText: _oldText, newText: _newText, ...rest } = legacy;\n\treturn { ...rest, edits } as EditToolInput;\n}\n\nfunction validateEditInput(input: EditToolInput): { path: string; edits: Edit[] } {\n\tif (!Array.isArray(input.edits) || input.edits.length === 0) {\n\t\tthrow new Error(\"Edit tool input is invalid. edits must contain at least one replacement.\");\n\t}\n\treturn { path: input.path, edits: input.edits };\n}\n\ntype RenderableEditArgs = {\n\tpath?: string;\n\tfile_path?: string;\n\tedits?: Edit[];\n\toldText?: string;\n\tnewText?: string;\n};\n\ntype EditToolResultLike = {\n\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\tdetails?: EditToolDetails;\n};\n\ntype EditCallRenderComponent = Box & {\n\tpreview?: EditPreview;\n\tpreviewArgsKey?: string;\n\tpreviewPending?: boolean;\n\tsettledError?: boolean;\n};\n\nfunction createEditCallRenderComponent(): EditCallRenderComponent {\n\treturn Object.assign(new Box(1, 1, (text: string) => text), {\n\t\tpreview: undefined as EditPreview | undefined,\n\t\tpreviewArgsKey: undefined as string | undefined,\n\t\tpreviewPending: false,\n\t\tsettledError: false,\n\t});\n}\n\nfunction getEditCallRenderComponent(state: EditRenderState, lastComponent: unknown): EditCallRenderComponent {\n\tif (lastComponent instanceof Box) {\n\t\tconst component = lastComponent as EditCallRenderComponent;\n\t\tstate.callComponent = component;\n\t\treturn component;\n\t}\n\tif (state.callComponent) {\n\t\treturn state.callComponent;\n\t}\n\tconst component = createEditCallRenderComponent();\n\tstate.callComponent = component;\n\treturn component;\n}\n\nfunction getRenderablePreviewInput(args: RenderableEditArgs | undefined): { path: string; edits: Edit[] } | null {\n\tif (!args) {\n\t\treturn null;\n\t}\n\n\tconst path = typeof args.path === \"string\" ? args.path : typeof args.file_path === \"string\" ? args.file_path : null;\n\tif (!path) {\n\t\treturn null;\n\t}\n\n\tif (\n\t\tArray.isArray(args.edits) &&\n\t\targs.edits.length > 0 &&\n\t\targs.edits.every((edit) => typeof edit?.oldText === \"string\" && typeof edit?.newText === \"string\")\n\t) {\n\t\treturn { path, edits: args.edits };\n\t}\n\n\tif (typeof args.oldText === \"string\" && typeof args.newText === \"string\") {\n\t\treturn { path, edits: [{ oldText: args.oldText, newText: args.newText }] };\n\t}\n\n\treturn null;\n}\n\n/**\n * Added and removed line counts read off a unified diff.\n *\n * `+++`/`---` file headers open with the same characters as content lines, so\n * they are skipped explicitly rather than by counting the first two lines,\n * which a diff without headers would not have.\n */\nfunction countChangedLines(diff: string): { added: number; removed: number } {\n\tlet added = 0;\n\tlet removed = 0;\n\tfor (const line of diff.split(\"\\n\")) {\n\t\tif (line.startsWith(\"+++\") || line.startsWith(\"---\")) continue;\n\t\tif (line.startsWith(\"+\")) added++;\n\t\telse if (line.startsWith(\"-\")) removed++;\n\t}\n\treturn { added, removed };\n}\n\n/** The one line a collapsed diff leaves behind, so the edit is still legible as an edit. */\nfunction formatChangeSummary(diff: string, theme: Theme): string {\n\tconst { added, removed } = countChangedLines(diff);\n\tconst parts: string[] = [];\n\tif (added > 0) parts.push(theme.fg(\"toolDiffAdded\", `+${added}`));\n\tif (removed > 0) parts.push(theme.fg(\"toolDiffRemoved\", `-${removed}`));\n\tif (parts.length === 0) return theme.fg(\"dim\", \"no line changes\");\n\treturn parts.join(theme.fg(\"dim\", \" \"));\n}\n\nfunction formatEditCall(args: RenderableEditArgs | undefined, theme: Theme, cwd: string): string {\n\tconst pathDisplay = renderToolPath(str(args?.file_path ?? args?.path), theme, cwd);\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"edit\"))} ${pathDisplay}`;\n}\n\nfunction formatEditResult(\n\targs: RenderableEditArgs | undefined,\n\tpreview: EditPreview | undefined,\n\tresult: EditToolResultLike,\n\ttheme: Theme,\n\tisError: boolean,\n\tdiffsExpanded: boolean,\n): string | undefined {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst previewDiff = preview && !(\"error\" in preview) ? preview.diff : undefined;\n\tconst previewError = preview && \"error\" in preview ? preview.error : undefined;\n\tif (isError) {\n\t\tconst errorText = result.content\n\t\t\t.filter((c) => c.type === \"text\")\n\t\t\t.map((c) => c.text || \"\")\n\t\t\t.join(\"\\n\");\n\t\tif (!errorText || errorText === previewError) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn theme.fg(\"error\", errorText);\n\t}\n\n\tconst resultDiff = result.details?.diff;\n\tif (resultDiff && resultDiff !== previewDiff) {\n\t\treturn diffsExpanded\n\t\t\t? renderDiff(resultDiff, { filePath: rawPath ?? undefined })\n\t\t\t: formatChangeSummary(resultDiff, theme);\n\t}\n\n\treturn undefined;\n}\n\nfunction getEditHeaderBg(\n\tpreview: EditPreview | undefined,\n\tsettledError: boolean | undefined,\n\ttheme: Theme,\n): (text: string) => string {\n\tif (preview) {\n\t\tif (\"error\" in preview) {\n\t\t\treturn (text: string) => theme.bg(\"toolErrorBg\", text);\n\t\t}\n\t\treturn (text: string) => theme.bg(\"toolSuccessBg\", text);\n\t}\n\tif (settledError) {\n\t\treturn (text: string) => theme.bg(\"toolErrorBg\", text);\n\t}\n\treturn (text: string) => theme.bg(\"toolPendingBg\", text);\n}\n\nfunction buildEditCallComponent(\n\tcomponent: EditCallRenderComponent,\n\targs: RenderableEditArgs | undefined,\n\ttheme: Theme,\n\tcwd: string,\n\tdiffsExpanded: boolean,\n): EditCallRenderComponent {\n\tcomponent.setBgFn(getEditHeaderBg(component.preview, component.settledError, theme));\n\tcomponent.clear();\n\tcomponent.addChild(new Text(formatEditCall(args, theme, cwd), 0, 0));\n\n\tif (!component.preview) {\n\t\treturn component;\n\t}\n\n\t// An error always shows, whatever the detail level. A collapsed diff still\n\t// leaves its counts behind, so the row never becomes a bare filename.\n\tconst body =\n\t\t\"error\" in component.preview\n\t\t\t? theme.fg(\"error\", component.preview.error)\n\t\t\t: diffsExpanded\n\t\t\t\t? renderDiff(component.preview.diff)\n\t\t\t\t: formatChangeSummary(component.preview.diff, theme);\n\tcomponent.addChild(new Spacer(1));\n\tcomponent.addChild(new Text(body, 0, 0));\n\treturn component;\n}\n\nfunction setEditPreview(\n\tcomponent: EditCallRenderComponent,\n\tpreview: EditPreview,\n\targsKey: string | undefined,\n): boolean {\n\tconst current = component.preview;\n\tconst changed =\n\t\tcurrent === undefined ||\n\t\t(\"error\" in current && \"error\" in preview\n\t\t\t? current.error !== preview.error\n\t\t\t: \"error\" in current !== \"error\" in preview) ||\n\t\t(!(\"error\" in current) &&\n\t\t\t!(\"error\" in preview) &&\n\t\t\t(current.diff !== preview.diff || current.firstChangedLine !== preview.firstChangedLine));\n\tcomponent.preview = preview;\n\tcomponent.previewArgsKey = argsKey;\n\tcomponent.previewPending = false;\n\treturn changed;\n}\n\nexport function createEditToolDefinition(\n\tcwd: string,\n\toptions?: EditToolOptions,\n): ApexToolDefinition<typeof editSchema, EditToolDetails | undefined, EditRenderState> {\n\tconst ops = options?.operations ?? defaultEditOperations;\n\tconst diagnosticsOperations = options?.diagnosticsOperations;\n\treturn {\n\t\tname: \"edit\",\n\t\tlabel: \"edit\",\n\t\tdescription:\n\t\t\t\"Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.\",\n\t\tpromptSnippet: editToolSystemPromptContribution.snippet,\n\t\tpromptGuidelines: [...editToolSystemPromptContribution.guidelines],\n\t\tparameters: editSchema,\n\t\tcontract: {\n\t\t\tcapabilities: new Set([\"fs.read\", \"fs.write\"]),\n\t\t\tpermission: createPathPermissionSpec({\n\t\t\t\tcwd,\n\t\t\t\tdefaultBehavior: \"ask\",\n\t\t\t\tverb: \"Edit\",\n\t\t\t\tgetPath: (params) => params.path,\n\t\t\t\tpreviewCall: (params) => previewEdit(params as EditToolInput),\n\t\t\t}),\n\t\t\tcontext: { resultRecoverable: true, deferSchema: false },\n\t\t\tevidence: {\n\t\t\t\temits: new Set(diagnosticsOperations ? [\"diff\", \"diagnostic\"] : [\"diff\"]),\n\t\t\t\tcapture: (params, result) => {\n\t\t\t\t\tconst patch = result.details?.patch;\n\t\t\t\t\tconst records: EvidenceRecord[] = patch\n\t\t\t\t\t\t? [{ kind: \"diff\", path: params.path, patchHash: sha256(patch) }]\n\t\t\t\t\t\t: [{ kind: \"diff\", path: params.path }];\n\t\t\t\t\tconst diagnostics = result.details?.diagnostics;\n\t\t\t\t\tif (diagnostics) records.push(diagnosticEvidenceForPath(params.path, diagnostics));\n\t\t\t\t\treturn records;\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tconstrainedSampling: getExperimentalToolSampling(),\n\t\trenderShell: \"self\",\n\t\tprepareArguments: prepareEditArguments,\n\t\tasync execute(_toolCallId, input: EditToolInput, signal?: AbortSignal, _onUpdate?, _ctx?) {\n\t\t\tconst { path, edits } = validateEditInput(input);\n\t\t\tconst prepared = getPreparedPathOperation(input);\n\t\t\tconst absolutePath = prepared?.path.value ?? resolveToCwd(path, cwd);\n\t\t\tif (prepared && (!ops.readPreparedFile || !ops.writePreparedFile)) {\n\t\t\t\tthrow new Error(\"No safe prepared edit operation is available for this gated edit\");\n\t\t\t}\n\n\t\t\treturn withFileMutationQueue(absolutePath, async () => {\n\t\t\t\t// Do not reject from an abort event listener here: that would release the\n\t\t\t\t// mutation queue while an in-flight filesystem operation may still finish.\n\t\t\t\t// Checking signal.aborted after each await observes the same aborts while\n\t\t\t\t// keeping the queue locked until the current operation has settled.\n\t\t\t\tconst throwIfAborted = (): void => {\n\t\t\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\t};\n\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\t// Check if file exists (skipped for a gated call: the prepared operation\n\t\t\t\t// already proves existence, and the no-follow read enforces the identity).\n\t\t\t\ttry {\n\t\t\t\t\tif (!prepared) await ops.access(absolutePath);\n\t\t\t\t} catch (error: unknown) {\n\t\t\t\t\tthrowIfAborted();\n\t\t\t\t\tconst errorMessage =\n\t\t\t\t\t\terror instanceof Error && \"code\" in error ? `Error code: ${error.code}` : String(error);\n\t\t\t\t\tthrow new Error(`Could not edit file: ${path}. ${errorMessage}.`);\n\t\t\t\t}\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\t// Read the file.\n\t\t\t\tconst buffer = prepared ? ops.readPreparedFile!(prepared) : await ops.readFile(absolutePath);\n\t\t\t\tconst rawContent = buffer.toString(\"utf-8\");\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\t// Strip BOM before matching. The model will not include an invisible BOM in oldText.\n\t\t\t\tconst { bom, text: content } = splitBom(rawContent);\n\t\t\t\tconst originalEnding = detectLineEnding(content);\n\t\t\t\tconst normalizedContent = normalizeToLF(content);\n\t\t\t\tconst { baseContent, newContent } = applyEditsToNormalizedContent(normalizedContent, edits, path);\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\tconst finalContent = bom + restoreLineEndings(newContent, originalEnding);\n\t\t\t\tif (prepared) ops.writePreparedFile!(prepared, Buffer.from(finalContent, \"utf-8\"));\n\t\t\t\telse await ops.writeFile(absolutePath, finalContent);\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\tconst diffResult = generateDiffString(baseContent, newContent);\n\t\t\t\tconst patch = generateUnifiedPatch(path, baseContent, newContent);\n\t\t\t\tconst successText = `Successfully replaced ${edits.length} block(s) in ${path}.`;\n\n\t\t\t\tif (diagnosticsOperations) {\n\t\t\t\t\tconst diagnostics = await diagnosticsOperations.afterMutation(absolutePath, signal);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: `${successText}\\n\\n${formatDiagnosticsOutcome(diagnostics)}` }],\n\t\t\t\t\t\tdetails: { diff: diffResult.diff, patch, firstChangedLine: diffResult.firstChangedLine, diagnostics },\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: successText }],\n\t\t\t\t\tdetails: { diff: diffResult.diff, patch, firstChangedLine: diffResult.firstChangedLine },\n\t\t\t\t};\n\t\t\t});\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst component = getEditCallRenderComponent(context.state, context.lastComponent);\n\t\t\tconst previewInput = getRenderablePreviewInput(args as RenderableEditArgs | undefined);\n\t\t\tconst argsKey = previewInput\n\t\t\t\t? JSON.stringify({ path: previewInput.path, edits: previewInput.edits })\n\t\t\t\t: undefined;\n\n\t\t\tif (component.previewArgsKey !== argsKey) {\n\t\t\t\tcomponent.preview = undefined;\n\t\t\t\tcomponent.previewArgsKey = argsKey;\n\t\t\t\tcomponent.previewPending = false;\n\t\t\t\tcomponent.settledError = false;\n\t\t\t}\n\n\t\t\tif (context.argsComplete && previewInput && !component.preview && !component.previewPending) {\n\t\t\t\tcomponent.previewPending = true;\n\t\t\t\tconst requestKey = argsKey;\n\t\t\t\tvoid computeEditsDiff(previewInput.path, previewInput.edits, context.cwd).then((preview) => {\n\t\t\t\t\tif (component.previewArgsKey === requestKey) {\n\t\t\t\t\t\tsetEditPreview(component, preview, requestKey);\n\t\t\t\t\t\tcontext.invalidate();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn buildEditCallComponent(component, args, theme, context.cwd, context.editDiffsExpanded);\n\t\t},\n\t\trenderResult(result, _options, theme, context) {\n\t\t\tconst callComponent = context.state.callComponent;\n\t\t\tconst previewInput = getRenderablePreviewInput(context.args as RenderableEditArgs | undefined);\n\t\t\tconst argsKey = previewInput\n\t\t\t\t? JSON.stringify({ path: previewInput.path, edits: previewInput.edits })\n\t\t\t\t: undefined;\n\t\t\tconst typedResult = result as EditToolResultLike;\n\t\t\tconst resultDiff = !context.isError ? typedResult.details?.diff : undefined;\n\t\t\tlet changed = false;\n\t\t\tif (callComponent) {\n\t\t\t\tif (typeof resultDiff === \"string\") {\n\t\t\t\t\tchanged =\n\t\t\t\t\t\tsetEditPreview(\n\t\t\t\t\t\t\tcallComponent,\n\t\t\t\t\t\t\t{ diff: resultDiff, firstChangedLine: typedResult.details?.firstChangedLine },\n\t\t\t\t\t\t\targsKey,\n\t\t\t\t\t\t) || changed;\n\t\t\t\t}\n\t\t\t\tif (callComponent.settledError !== context.isError) {\n\t\t\t\t\tcallComponent.settledError = context.isError;\n\t\t\t\t\tchanged = true;\n\t\t\t\t}\n\t\t\t\tif (changed) {\n\t\t\t\t\tbuildEditCallComponent(\n\t\t\t\t\t\tcallComponent,\n\t\t\t\t\t\tcontext.args as RenderableEditArgs | undefined,\n\t\t\t\t\t\ttheme,\n\t\t\t\t\t\tcontext.cwd,\n\t\t\t\t\t\tcontext.editDiffsExpanded,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst output = formatEditResult(\n\t\t\t\tcontext.args,\n\t\t\t\tcallComponent?.preview,\n\t\t\t\ttypedResult,\n\t\t\t\ttheme,\n\t\t\t\tcontext.isError,\n\t\t\t\tcontext.editDiffsExpanded,\n\t\t\t);\n\t\t\tconst component = (context.lastComponent as Container | undefined) ?? new Container();\n\t\t\tcomponent.clear();\n\t\t\tif (!output) {\n\t\t\t\treturn component;\n\t\t\t}\n\t\t\tcomponent.addChild(new Spacer(1));\n\t\t\tcomponent.addChild(new Text(output, 1, 0));\n\t\t\treturn component;\n\t\t},\n\t};\n}\n\nexport function createEditTool(cwd: string, options?: EditToolOptions): AgentTool<typeof editSchema> {\n\treturn wrapToolDefinition(createEditToolDefinition(cwd, options));\n}\n"]}
|
|
@@ -22,6 +22,15 @@ export declare class PreparedTargetChangedError extends Error {
|
|
|
22
22
|
* replaced parent or a pre-placed symlink is never followed.
|
|
23
23
|
*/
|
|
24
24
|
export declare function writePreparedPath(operation: PreparedPathOperation, content: string | Buffer): void;
|
|
25
|
+
/**
|
|
26
|
+
* Atomic publish for an ungated write, where no prepared operation exists to pin the target.
|
|
27
|
+
*
|
|
28
|
+
* `writePreparedPath` is the gated path and the one that matters; this is its counterpart for
|
|
29
|
+
* a tool invoked outside the permission gate, so that neither route can leave a half-written
|
|
30
|
+
* file. It makes no containment claim: with no authorization there is no authorized target to
|
|
31
|
+
* hold the write to, and the no-follow walk would have nothing to check against.
|
|
32
|
+
*/
|
|
33
|
+
export declare function writePathAtomically(filePath: string, content: string | Buffer): void;
|
|
25
34
|
export declare function pathExists(filePath: string): Promise<boolean>;
|
|
26
35
|
export declare function expandPath(filePath: string): string;
|
|
27
36
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../../../src/core/tools/path-utils.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAI1E,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,qBAAqB,CAazF;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,qBAAqB,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CA6B5F;AAED,iFAAiF;AACjF,qBAAa,0BAA2B,SAAQ,KAAK;CAAG;AA2GxD;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,qBAAqB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAsClG;AA0BD,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnE;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAWlE;AAMD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAgCrE;AAED,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgCzF","sourcesContent":["import {\n\taccessSync,\n\tcloseSync,\n\tconstants,\n\texistsSync,\n\tfstatSync,\n\tftruncateSync,\n\tmkdirSync,\n\topenSync,\n\treadSync,\n\trealpathSync,\n\tstatSync,\n\twriteSync,\n} from \"node:fs\";\nimport { access } from \"node:fs/promises\";\nimport { basename, dirname, isAbsolute, join, parse, sep } from \"node:path\";\nimport { normalizePath, resolvePath } from \"../../utils/paths.ts\";\nimport type { PreparedPathOperation } from \"../permissions/operations.ts\";\n\nconst NARROW_NO_BREAK_SPACE = \"\\u202F\";\n\nexport function preparePathOperation(filePath: string, cwd: string): PreparedPathOperation {\n\tconst canonicalPath = resolveToCwd(filePath, cwd);\n\tif (!isAbsolute(canonicalPath)) throw new Error(\"Canonical path must be absolute\");\n\ttry {\n\t\tconst stats = statSync(canonicalPath);\n\t\treturn {\n\t\t\tkind: \"path-existing\",\n\t\t\tpath: { kind: \"canonical-path\", value: canonicalPath },\n\t\t\tidentity: { device: stats.dev, inode: stats.ino },\n\t\t};\n\t} catch {\n\t\treturn { kind: \"path-new\", path: { kind: \"canonical-path\", value: canonicalPath } };\n\t}\n}\n\n/**\n * Read the authorized target through a no-follow final-component open, verifying\n * the descriptor identity captured at authorization before any byte is returned.\n * A replaced alias or an injected symlink never matches and the read aborts.\n */\nexport function readPreparedPath(operation: PreparedPathOperation, maxBytes?: number): Buffer {\n\tif (operation.kind !== \"path-existing\") {\n\t\tthrow new PreparedTargetChangedError(\"Authorized read target did not exist when permission was checked\");\n\t}\n\treturn withTargetDirectory(operation, { createMissingDirectories: false }, (parent, name) => {\n\t\tlet fd: number;\n\t\ttry {\n\t\t\tfd = openUnder(parent, name, constants.O_RDONLY | constants.O_NOFOLLOW);\n\t\t} catch (error) {\n\t\t\tthrow new PreparedTargetChangedError(\"Authorized read target changed before execution\", { cause: error });\n\t\t}\n\t\ttry {\n\t\t\tconst stats = fstatSync(fd);\n\t\t\tif (stats.dev !== operation.identity.device || stats.ino !== operation.identity.inode) {\n\t\t\t\tthrow new PreparedTargetChangedError(\"Authorized read target changed before execution\");\n\t\t\t}\n\t\t\tconst length = maxBytes === undefined ? stats.size : Math.min(stats.size, maxBytes);\n\t\t\tconst buffer = Buffer.alloc(length);\n\t\t\tlet offset = 0;\n\t\t\twhile (offset < buffer.length) {\n\t\t\t\tconst read = readSync(fd, buffer, offset, buffer.length - offset, null);\n\t\t\t\tif (read === 0) break;\n\t\t\t\toffset += read;\n\t\t\t}\n\t\t\treturn offset === buffer.length ? buffer : buffer.subarray(0, offset);\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t}\n\t});\n}\n\n/** The authorized target changed between permission evaluation and execution. */\nexport class PreparedTargetChangedError extends Error {}\n\nconst PROC_FD = \"/proc/self/fd\";\nlet procFdAvailable: boolean | undefined;\n\n/** /proc lets an already-open directory fd act as the parent of the next open, so a swapped intermediate directory cannot be traversed. */\nfunction useProcFd(): boolean {\n\tif (procFdAvailable === undefined) {\n\t\ttry {\n\t\t\tprocFdAvailable = existsSync(`${PROC_FD}/self`);\n\t\t} catch {\n\t\t\tprocFdAvailable = false;\n\t\t}\n\t}\n\treturn procFdAvailable;\n}\n\ninterface OpenDirectory {\n\tfd: number;\n\t/** Fallback parent path for platforms without /proc/self/fd. */\n\tpath: string;\n}\n\nfunction openUnder(parent: OpenDirectory, name: string, flags: number, mode?: number): number {\n\tif (useProcFd()) return openSync(`${PROC_FD}/${parent.fd}/${name}`, flags, mode);\n\treturn openSync(join(parent.path, name), flags, mode);\n}\n\nfunction mkdirUnder(parent: OpenDirectory, name: string): void {\n\tif (useProcFd()) mkdirSync(`${PROC_FD}/${parent.fd}/${name}`);\n\telse mkdirSync(join(parent.path, name));\n}\n\nfunction openChildDirectory(parent: OpenDirectory, name: string): OpenDirectory {\n\tconst flags = constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW;\n\tconst fd = openUnder(parent, name, flags);\n\tcloseSync(parent.fd);\n\treturn { fd, path: join(parent.path, name) };\n}\n\nfunction writeAll(fd: number, content: string | Buffer): void {\n\tconst buffer = typeof content === \"string\" ? Buffer.from(content, \"utf-8\") : content;\n\tlet written = 0;\n\twhile (written < buffer.length) {\n\t\twritten += writeSync(fd, buffer, written, buffer.length - written);\n\t}\n}\n\nfunction isMissingEntryError(error: unknown): boolean {\n\treturn (\n\t\ttypeof error === \"object\" && error !== null && \"code\" in error && (error as { code?: unknown }).code === \"ENOENT\"\n\t);\n}\n\n/**\n * Walk every directory component of the authorized path no-follow, holding one\n * directory fd across the walk, then run the caller's final-component step against\n * that walked parent. Any intermediate that is not a real directory — a swapped\n * symlink, a removed component — aborts before the target is touched.\n */\nfunction withTargetDirectory<T>(\n\toperation: PreparedPathOperation,\n\toptions: { createMissingDirectories: boolean },\n\trun: (parent: OpenDirectory, name: string) => T,\n): T {\n\tconst root = parse(operation.path.value).root;\n\tconst components = operation.path.value\n\t\t.slice(root.length)\n\t\t.split(sep)\n\t\t.filter((component) => component.length > 0);\n\tconst name = components[components.length - 1];\n\tif (name === undefined) throw new PreparedTargetChangedError(\"Authorized path has no final component\");\n\tlet parent: OpenDirectory = { fd: openSync(root, constants.O_RDONLY | constants.O_DIRECTORY), path: root };\n\tconst closeParent = (): void => {\n\t\ttry {\n\t\t\tcloseSync(parent.fd);\n\t\t} catch {}\n\t};\n\tfor (const component of components.slice(0, -1)) {\n\t\ttry {\n\t\t\tparent = openChildDirectory(parent, component);\n\t\t} catch (error) {\n\t\t\tif (options.createMissingDirectories && isMissingEntryError(error)) {\n\t\t\t\ttry {\n\t\t\t\t\tmkdirUnder(parent, component);\n\t\t\t\t\tparent = openChildDirectory(parent, component);\n\t\t\t\t\tcontinue;\n\t\t\t\t} catch (mkdirError) {\n\t\t\t\t\tcloseParent();\n\t\t\t\t\tthrow new PreparedTargetChangedError(`Authorized write parent \"${component}\" changed before execution`, {\n\t\t\t\t\t\tcause: mkdirError,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tcloseParent();\n\t\t\tthrow new PreparedTargetChangedError(`Authorized path component \"${component}\" changed before execution`, {\n\t\t\t\tcause: error,\n\t\t\t});\n\t\t}\n\t}\n\ttry {\n\t\treturn run(parent, name);\n\t} finally {\n\t\tcloseParent();\n\t}\n}\n\n/**\n * Execute an authorized write against the prepared target.\n *\n * - `path-existing`: the final component is opened `O_NOFOLLOW`, its descriptor\n * identity is compared with the identity captured at authorization, and only\n * then is the file truncated and written through that descriptor. A swapped\n * alias, an injected symlink, or a redirected parent never matches and the\n * write aborts before any byte changes.\n * - `path-new`: missing parents are created no-follow under the walked directory\n * descriptor, and the file is created `O_CREAT | O_EXCL | O_NOFOLLOW`, so a\n * replaced parent or a pre-placed symlink is never followed.\n */\nexport function writePreparedPath(operation: PreparedPathOperation, content: string | Buffer): void {\n\twithTargetDirectory(operation, { createMissingDirectories: operation.kind === \"path-new\" }, (parent, name) => {\n\t\tif (operation.kind === \"path-existing\") {\n\t\t\tlet fd: number;\n\t\t\ttry {\n\t\t\t\tfd = openUnder(parent, name, constants.O_WRONLY | constants.O_NOFOLLOW);\n\t\t\t} catch (error) {\n\t\t\t\tthrow new PreparedTargetChangedError(\"Authorized write target changed before execution\", { cause: error });\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst stats = fstatSync(fd);\n\t\t\t\tif (stats.dev !== operation.identity.device || stats.ino !== operation.identity.inode) {\n\t\t\t\t\tthrow new PreparedTargetChangedError(\"Authorized write target changed before execution\");\n\t\t\t\t}\n\t\t\t\tftruncateSync(fd, 0);\n\t\t\t\twriteAll(fd, content);\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tlet fd: number;\n\t\ttry {\n\t\t\tfd = openUnder(\n\t\t\t\tparent,\n\t\t\t\tname,\n\t\t\t\tconstants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,\n\t\t\t\t0o600,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tthrow new PreparedTargetChangedError(\"Authorized new-file target changed before execution\", { cause: error });\n\t\t}\n\t\ttry {\n\t\t\twriteAll(fd, content);\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t}\n\t});\n}\n\nfunction tryMacOSScreenshotPath(filePath: string): string {\n\treturn filePath.replace(/ (AM|PM)\\./gi, `${NARROW_NO_BREAK_SPACE}$1.`);\n}\n\nfunction tryNFDVariant(filePath: string): string {\n\t// macOS stores filenames in NFD (decomposed) form, try converting user input to NFD\n\treturn filePath.normalize(\"NFD\");\n}\n\nfunction tryCurlyQuoteVariant(filePath: string): string {\n\t// macOS uses U+2019 (right single quotation mark) in screenshot names like \"Capture d'écran\"\n\t// Users typically type U+0027 (straight apostrophe)\n\treturn filePath.replace(/'/g, \"\\u2019\");\n}\n\nfunction fileExists(filePath: string): boolean {\n\ttry {\n\t\taccessSync(filePath, constants.F_OK);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nexport async function pathExists(filePath: string): Promise<boolean> {\n\ttry {\n\t\tawait access(filePath, constants.F_OK);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nexport function expandPath(filePath: string): string {\n\treturn normalizePath(filePath, { normalizeUnicodeSpaces: true, stripAtPrefix: true });\n}\n\n/**\n * Resolve a path relative to the given cwd, handling ~ expansion and absolute\n * paths. The result is the single target used by both permission matching and\n * filesystem execution.\n */\nexport function resolveToCwd(filePath: string, cwd: string): string {\n\tconst requested = resolvePath(filePath, cwd, { normalizeUnicodeSpaces: true, stripAtPrefix: true });\n\ttry {\n\t\treturn realpathSync(requested);\n\t} catch {\n\t\ttry {\n\t\t\treturn joinCanonicalParent(requested);\n\t\t} catch {\n\t\t\treturn requested;\n\t\t}\n\t}\n}\n\nfunction joinCanonicalParent(requested: string): string {\n\treturn join(realpathSync(dirname(requested)), basename(requested));\n}\n\nexport function resolveReadPath(filePath: string, cwd: string): string {\n\tconst resolved = resolveToCwd(filePath, cwd);\n\n\tif (fileExists(resolved)) {\n\t\treturn resolved;\n\t}\n\n\t// Try macOS AM/PM variant (narrow no-break space before AM/PM)\n\tconst amPmVariant = tryMacOSScreenshotPath(resolved);\n\tif (amPmVariant !== resolved && fileExists(amPmVariant)) {\n\t\treturn amPmVariant;\n\t}\n\n\t// Try NFD variant (macOS stores filenames in NFD form)\n\tconst nfdVariant = tryNFDVariant(resolved);\n\tif (nfdVariant !== resolved && fileExists(nfdVariant)) {\n\t\treturn nfdVariant;\n\t}\n\n\t// Try curly quote variant (macOS uses U+2019 in screenshot names)\n\tconst curlyVariant = tryCurlyQuoteVariant(resolved);\n\tif (curlyVariant !== resolved && fileExists(curlyVariant)) {\n\t\treturn curlyVariant;\n\t}\n\n\t// Try combined NFD + curly quote (for French macOS screenshots like \"Capture d'écran\")\n\tconst nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant);\n\tif (nfdCurlyVariant !== resolved && fileExists(nfdCurlyVariant)) {\n\t\treturn nfdCurlyVariant;\n\t}\n\n\treturn resolved;\n}\n\nexport async function resolveReadPathAsync(filePath: string, cwd: string): Promise<string> {\n\tconst resolved = resolveToCwd(filePath, cwd);\n\n\tif (await pathExists(resolved)) {\n\t\treturn resolved;\n\t}\n\n\t// Try macOS AM/PM variant (narrow no-break space before AM/PM)\n\tconst amPmVariant = tryMacOSScreenshotPath(resolved);\n\tif (amPmVariant !== resolved && (await pathExists(amPmVariant))) {\n\t\treturn amPmVariant;\n\t}\n\n\t// Try NFD variant (macOS stores filenames in NFD form)\n\tconst nfdVariant = tryNFDVariant(resolved);\n\tif (nfdVariant !== resolved && (await pathExists(nfdVariant))) {\n\t\treturn nfdVariant;\n\t}\n\n\t// Try curly quote variant (macOS uses U+2019 in screenshot names)\n\tconst curlyVariant = tryCurlyQuoteVariant(resolved);\n\tif (curlyVariant !== resolved && (await pathExists(curlyVariant))) {\n\t\treturn curlyVariant;\n\t}\n\n\t// Try combined NFD + curly quote (for French macOS screenshots like \"Capture d'écran\")\n\tconst nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant);\n\tif (nfdCurlyVariant !== resolved && (await pathExists(nfdCurlyVariant))) {\n\t\treturn nfdCurlyVariant;\n\t}\n\n\treturn resolved;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../../../src/core/tools/path-utils.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAgB,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAIxF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,qBAAqB,CAazF;AAwBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,qBAAqB,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CA2B5F;AAED,iFAAiF;AACjF,qBAAa,0BAA2B,SAAQ,KAAK;CAAG;AAkPxD;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,qBAAqB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgDlG;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAwCpF;AA0BD,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnE;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAWlE;AAMD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAgCrE;AAED,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgCzF","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { Stats } from \"node:fs\";\nimport {\n\taccessSync,\n\tcloseSync,\n\tconstants,\n\texistsSync,\n\tfchmodSync,\n\tfstatSync,\n\tftruncateSync,\n\tmkdirSync,\n\topenSync,\n\treadSync,\n\trealpathSync,\n\trenameSync,\n\tstatSync,\n\tunlinkSync,\n\twriteSync,\n} from \"node:fs\";\nimport { access } from \"node:fs/promises\";\nimport { basename, dirname, isAbsolute, join, parse, sep } from \"node:path\";\nimport { normalizePath, resolvePath } from \"../../utils/paths.ts\";\nimport type { FileIdentity, PreparedPathOperation } from \"../permissions/operations.ts\";\n\nconst NARROW_NO_BREAK_SPACE = \"\\u202F\";\n\nexport function preparePathOperation(filePath: string, cwd: string): PreparedPathOperation {\n\tconst canonicalPath = resolveToCwd(filePath, cwd);\n\tif (!isAbsolute(canonicalPath)) throw new Error(\"Canonical path must be absolute\");\n\ttry {\n\t\tconst stats = statSync(canonicalPath);\n\t\treturn {\n\t\t\tkind: \"path-existing\",\n\t\t\tpath: { kind: \"canonical-path\", value: canonicalPath },\n\t\t\tidentity: { device: stats.dev, inode: stats.ino },\n\t\t};\n\t} catch {\n\t\treturn { kind: \"path-new\", path: { kind: \"canonical-path\", value: canonicalPath } };\n\t}\n}\n\n/**\n * Verify a descriptor's identity against the one captured at authorization.\n *\n * A zero inode is not an identity. POSIX reserves inode 0 for \"no file\", and Node reports 0\n * on Windows when the file index is unavailable for the handle. Comparing two of them\n * succeeds trivially, which would turn this check -- the only thing standing between an\n * authorized target and one swapped underneath it -- into a no-op at exactly the moment the\n * platform could not say what we are holding. An unverifiable target is not an authorized\n * one, so refuse rather than proceed.\n *\n * The mismatched case keeps its own wording: \"changed\" and \"could not be verified\" are\n * different facts, and an operator reading the refusal should be able to tell them apart.\n */\nfunction verifyPreparedIdentity(subject: \"read\" | \"write\", stats: Stats, identity: FileIdentity): void {\n\tif (stats.ino === 0 || identity.inode === 0) {\n\t\tthrow new PreparedTargetChangedError(`Authorized ${subject} target identity could not be verified`);\n\t}\n\tif (stats.dev !== identity.device || stats.ino !== identity.inode) {\n\t\tthrow new PreparedTargetChangedError(`Authorized ${subject} target changed before execution`);\n\t}\n}\n\n/**\n * Read the authorized target through a no-follow final-component open, verifying\n * the descriptor identity captured at authorization before any byte is returned.\n * A replaced alias or an injected symlink never matches and the read aborts.\n */\nexport function readPreparedPath(operation: PreparedPathOperation, maxBytes?: number): Buffer {\n\tif (operation.kind !== \"path-existing\") {\n\t\tthrow new PreparedTargetChangedError(\"Authorized read target did not exist when permission was checked\");\n\t}\n\treturn withTargetDirectory(operation, { createMissingDirectories: false }, (parent, name) => {\n\t\tlet fd: number;\n\t\ttry {\n\t\t\tfd = openUnder(parent, name, constants.O_RDONLY | constants.O_NOFOLLOW);\n\t\t} catch (error) {\n\t\t\tthrow new PreparedTargetChangedError(\"Authorized read target changed before execution\", { cause: error });\n\t\t}\n\t\ttry {\n\t\t\tconst stats = fstatSync(fd);\n\t\t\tverifyPreparedIdentity(\"read\", stats, operation.identity);\n\t\t\tconst length = maxBytes === undefined ? stats.size : Math.min(stats.size, maxBytes);\n\t\t\tconst buffer = Buffer.alloc(length);\n\t\t\tlet offset = 0;\n\t\t\twhile (offset < buffer.length) {\n\t\t\t\tconst read = readSync(fd, buffer, offset, buffer.length - offset, null);\n\t\t\t\tif (read === 0) break;\n\t\t\t\toffset += read;\n\t\t\t}\n\t\t\treturn offset === buffer.length ? buffer : buffer.subarray(0, offset);\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t}\n\t});\n}\n\n/** The authorized target changed between permission evaluation and execution. */\nexport class PreparedTargetChangedError extends Error {}\n\nconst PROC_FD = \"/proc/self/fd\";\nlet procFdAvailable: boolean | undefined;\n\n/** /proc lets an already-open directory fd act as the parent of the next open, so a swapped intermediate directory cannot be traversed. */\nfunction useProcFd(): boolean {\n\tif (procFdAvailable === undefined) {\n\t\ttry {\n\t\t\tprocFdAvailable = existsSync(`${PROC_FD}/self`);\n\t\t} catch {\n\t\t\tprocFdAvailable = false;\n\t\t}\n\t}\n\treturn procFdAvailable;\n}\n\ninterface OpenDirectory {\n\tfd: number;\n\t/** Fallback parent path for platforms without /proc/self/fd. */\n\tpath: string;\n}\n\nfunction openUnder(parent: OpenDirectory, name: string, flags: number, mode?: number): number {\n\tif (useProcFd()) return openSync(`${PROC_FD}/${parent.fd}/${name}`, flags, mode);\n\treturn openSync(join(parent.path, name), flags, mode);\n}\n\nfunction renameUnder(parent: OpenDirectory, from: string, to: string): void {\n\tif (useProcFd()) renameSync(`${PROC_FD}/${parent.fd}/${from}`, `${PROC_FD}/${parent.fd}/${to}`);\n\telse renameSync(join(parent.path, from), join(parent.path, to));\n}\n\nfunction unlinkUnder(parent: OpenDirectory, name: string): void {\n\ttry {\n\t\tif (useProcFd()) unlinkSync(`${PROC_FD}/${parent.fd}/${name}`);\n\t\telse unlinkSync(join(parent.path, name));\n\t} catch {}\n}\n\n/**\n * The name of the sibling a publish stages into.\n *\n * It carries enough of the destination to be recognizable if a crash ever strands one, and\n * a pid and a random suffix so two publishes in one directory cannot collide. The base is\n * truncated to fit: a destination already at the filesystem's per-component limit would\n * otherwise produce a staging name past it, and fail a write that truncate-in-place handled.\n * 255 is the limit on every filesystem this runs on that has one at all.\n */\nfunction stagingName(name: string): string {\n\tconst suffix = `.apex-${process.pid}-${randomUUID().slice(0, 8)}`;\n\tconst budget = 255 - suffix.length - 1;\n\treturn `.${name.slice(0, Math.max(1, budget))}${suffix}`;\n}\n\n/**\n * Write `content` to a sibling of `name` and rename it over `name`.\n *\n * The rename is the publish. Until it runs, `name` still refers to the old inode with all\n * of its old bytes, so a crash, a full disk, or a kill mid-write destroys nothing. The\n * caller has already proved that `name` refers to the authorized target; this only decides\n * how the bytes arrive.\n *\n * `mode` is the destination's, captured from the descriptor the identity check used, so a\n * publish does not quietly reset a file's permissions to the temp file's. Ownership is not\n * carried: a rename installs a file this process created, so a target owned by someone else\n * and merely writable by us changes owner. That is inherent to publishing by rename and is\n * the same trade the session store already makes.\n *\n * One guarantee narrows, and it is worth stating plainly rather than leaving for a reader to\n * derive. ADR 0029's identity check still runs and still aborts on a mismatch, but it now\n * proves the name referred to the authorized file at the moment of the check rather than at\n * the moment of the write. Writing through the checked descriptor could not be redirected by\n * a later swap; a rename can be, because it resolves the name again. The window is between\n * the identity check and the rename, both anchored to the same validated parent descriptor,\n * and exploiting it needs a local attacker placing a file at that exact name in a directory\n * the call was already authorized to write. That is traded for crash safety, which is not a\n * race: it is what every interrupted write did.\n *\n * Windows refuses a rename onto a destination another handle holds open, and an ordinary read\n * handle is enough to trigger it. An earlier version of this comment called that an acceptable\n * trade. Three-OS CI disproved it: the case is an editor, a watcher, or a language server with\n * the file open, which is the common case rather than an exotic one, and failing the write\n * there is not a trade a user opted into. It falls back to `writeInPlace` instead, so Windows\n * gets atomic publish whenever it can and a working write when it cannot.\n */\nfunction publishByRename(parent: OpenDirectory, name: string, content: string | Buffer, mode: number): boolean {\n\tconst temporary = stagingName(name);\n\tlet fd: number;\n\ttry {\n\t\tfd = openUnder(\n\t\t\tparent,\n\t\t\ttemporary,\n\t\t\tconstants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,\n\t\t\tmode,\n\t\t);\n\t} catch (error) {\n\t\tthrow new PreparedTargetChangedError(\"Authorized write could not stage its replacement\", { cause: error });\n\t}\n\ttry {\n\t\twriteAll(fd, content);\n\t\t// The create mode passes through umask; the destination's mode does not.\n\t\tfchmodSync(fd, mode);\n\t\tcloseSync(fd);\n\t} catch (error) {\n\t\ttry {\n\t\t\tcloseSync(fd);\n\t\t} catch {}\n\t\tunlinkUnder(parent, temporary);\n\t\tthrow error;\n\t}\n\ttry {\n\t\trenameUnder(parent, temporary, name);\n\t\treturn true;\n\t} catch (error) {\n\t\tunlinkUnder(parent, temporary);\n\t\tif (isWindowsSharingViolation(error)) return false;\n\t\tthrow new PreparedTargetChangedError(\"Authorized write target changed before execution\", { cause: error });\n\t}\n}\n\n/**\n * Windows refuses to rename onto a destination another handle holds open, and an ordinary\n * read handle is enough. An editor, a watcher, or a language server with the file open is\n * the common case, not an exotic one, so a rename that cannot land there has to fall back\n * rather than fail the write.\n *\n * Three OS CI found this. The Linux and macOS jobs pass, because POSIX renames over an open\n * file happily.\n */\nfunction isWindowsSharingViolation(error: unknown): boolean {\n\tif (process.platform !== \"win32\") return false;\n\tconst code = (error as { code?: unknown } | null)?.code;\n\treturn code === \"EPERM\" || code === \"EACCES\" || code === \"EBUSY\";\n}\n\n/**\n * The pre-rename publish, kept for the one case that cannot use a rename.\n *\n * This is the truncate-then-write window the rename exists to close, so it runs only when\n * Windows has refused the rename. Crash safety is lost for that write; the alternative is\n * refusing to write a file something else has open, which is worse and is not a trade a\n * user opted into. The identity check is re-run against a freshly opened descriptor, so the\n * weaker durability does not come with a weaker target guarantee.\n */\nfunction writeInPlace(parent: OpenDirectory, name: string, content: string | Buffer, identity?: FileIdentity): void {\n\tlet fd: number;\n\ttry {\n\t\tfd = openUnder(parent, name, constants.O_WRONLY | constants.O_NOFOLLOW);\n\t} catch (error) {\n\t\tthrow new PreparedTargetChangedError(\"Authorized write target changed before execution\", { cause: error });\n\t}\n\ttry {\n\t\tif (identity) {\n\t\t\tverifyPreparedIdentity(\"write\", fstatSync(fd), identity);\n\t\t}\n\t\tftruncateSync(fd, 0);\n\t\twriteAll(fd, content);\n\t} finally {\n\t\tcloseSync(fd);\n\t}\n}\n\nfunction mkdirUnder(parent: OpenDirectory, name: string): void {\n\tif (useProcFd()) mkdirSync(`${PROC_FD}/${parent.fd}/${name}`);\n\telse mkdirSync(join(parent.path, name));\n}\n\nfunction openChildDirectory(parent: OpenDirectory, name: string): OpenDirectory {\n\tconst flags = constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW;\n\tconst fd = openUnder(parent, name, flags);\n\tcloseSync(parent.fd);\n\treturn { fd, path: join(parent.path, name) };\n}\n\nfunction writeAll(fd: number, content: string | Buffer): void {\n\tconst buffer = typeof content === \"string\" ? Buffer.from(content, \"utf-8\") : content;\n\tlet written = 0;\n\twhile (written < buffer.length) {\n\t\twritten += writeSync(fd, buffer, written, buffer.length - written);\n\t}\n}\n\nfunction isMissingEntryError(error: unknown): boolean {\n\treturn (\n\t\ttypeof error === \"object\" && error !== null && \"code\" in error && (error as { code?: unknown }).code === \"ENOENT\"\n\t);\n}\n\n/**\n * Walk every directory component of the authorized path no-follow, holding one\n * directory fd across the walk, then run the caller's final-component step against\n * that walked parent. Any intermediate that is not a real directory — a swapped\n * symlink, a removed component — aborts before the target is touched.\n */\nfunction withTargetDirectory<T>(\n\toperation: PreparedPathOperation,\n\toptions: { createMissingDirectories: boolean },\n\trun: (parent: OpenDirectory, name: string) => T,\n): T {\n\tconst root = parse(operation.path.value).root;\n\tconst components = operation.path.value\n\t\t.slice(root.length)\n\t\t.split(sep)\n\t\t.filter((component) => component.length > 0);\n\tconst name = components[components.length - 1];\n\tif (name === undefined) throw new PreparedTargetChangedError(\"Authorized path has no final component\");\n\tlet parent: OpenDirectory = { fd: openSync(root, constants.O_RDONLY | constants.O_DIRECTORY), path: root };\n\tconst closeParent = (): void => {\n\t\ttry {\n\t\t\tcloseSync(parent.fd);\n\t\t} catch {}\n\t};\n\tfor (const component of components.slice(0, -1)) {\n\t\ttry {\n\t\t\tparent = openChildDirectory(parent, component);\n\t\t} catch (error) {\n\t\t\tif (options.createMissingDirectories && isMissingEntryError(error)) {\n\t\t\t\ttry {\n\t\t\t\t\tmkdirUnder(parent, component);\n\t\t\t\t\tparent = openChildDirectory(parent, component);\n\t\t\t\t\tcontinue;\n\t\t\t\t} catch (mkdirError) {\n\t\t\t\t\tcloseParent();\n\t\t\t\t\tthrow new PreparedTargetChangedError(`Authorized write parent \"${component}\" changed before execution`, {\n\t\t\t\t\t\tcause: mkdirError,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tcloseParent();\n\t\t\tthrow new PreparedTargetChangedError(`Authorized path component \"${component}\" changed before execution`, {\n\t\t\t\tcause: error,\n\t\t\t});\n\t\t}\n\t}\n\ttry {\n\t\treturn run(parent, name);\n\t} finally {\n\t\tcloseParent();\n\t}\n}\n\n/**\n * Execute an authorized write against the prepared target.\n *\n * - `path-existing`: the final component is opened `O_NOFOLLOW`, its descriptor\n * identity is compared with the identity captured at authorization, and only\n * then is the file truncated and written through that descriptor. A swapped\n * alias, an injected symlink, or a redirected parent never matches and the\n * write aborts before any byte changes.\n * - `path-new`: missing parents are created no-follow under the walked directory\n * descriptor, and the file is created `O_CREAT | O_EXCL | O_NOFOLLOW`, so a\n * replaced parent or a pre-placed symlink is never followed.\n */\nexport function writePreparedPath(operation: PreparedPathOperation, content: string | Buffer): void {\n\twithTargetDirectory(operation, { createMissingDirectories: operation.kind === \"path-new\" }, (parent, name) => {\n\t\tif (operation.kind === \"path-existing\") {\n\t\t\tlet fd: number;\n\t\t\ttry {\n\t\t\t\tfd = openUnder(parent, name, constants.O_WRONLY | constants.O_NOFOLLOW);\n\t\t\t} catch (error) {\n\t\t\t\tthrow new PreparedTargetChangedError(\"Authorized write target changed before execution\", { cause: error });\n\t\t\t}\n\t\t\tlet mode: number;\n\t\t\ttry {\n\t\t\t\tconst stats = fstatSync(fd);\n\t\t\t\tverifyPreparedIdentity(\"write\", stats, operation.identity);\n\t\t\t\tmode = stats.mode & 0o7777;\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t\tif (!publishByRename(parent, name, content, mode)) {\n\t\t\t\twriteInPlace(parent, name, content, operation.identity);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\t// The exclusive create is what proves the name was still absent, which is the whole\n\t\t// authorization for a new-file write. It reserves the name; the rename below then\n\t\t// replaces that empty placeholder, so the name never holds a half-written file.\n\t\tlet fd: number;\n\t\ttry {\n\t\t\tfd = openUnder(\n\t\t\t\tparent,\n\t\t\t\tname,\n\t\t\t\tconstants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,\n\t\t\t\t0o600,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tthrow new PreparedTargetChangedError(\"Authorized new-file target changed before execution\", { cause: error });\n\t\t}\n\t\tcloseSync(fd);\n\t\ttry {\n\t\t\t// The reservation this just created is ours, so there is no prior identity to\n\t\t\t// re-check if Windows refuses the rename onto it.\n\t\t\tif (!publishByRename(parent, name, content, 0o600)) {\n\t\t\t\twriteInPlace(parent, name, content);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tunlinkUnder(parent, name);\n\t\t\tthrow error;\n\t\t}\n\t});\n}\n\n/**\n * Atomic publish for an ungated write, where no prepared operation exists to pin the target.\n *\n * `writePreparedPath` is the gated path and the one that matters; this is its counterpart for\n * a tool invoked outside the permission gate, so that neither route can leave a half-written\n * file. It makes no containment claim: with no authorization there is no authorized target to\n * hold the write to, and the no-follow walk would have nothing to check against.\n */\nexport function writePathAtomically(filePath: string, content: string | Buffer): void {\n\tconst temporary = join(dirname(filePath), stagingName(basename(filePath)));\n\t// Only an existing destination has a mode worth carrying. For a new file there is\n\t// nothing to capture, and chmod'ing to the 0o666 open default would force past the\n\t// caller's umask and publish a world-writable file, which `writeFile` never did.\n\tlet destinationMode: number | undefined;\n\ttry {\n\t\tdestinationMode = statSync(filePath).mode & 0o7777;\n\t} catch {}\n\tconst fd = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, destinationMode ?? 0o666);\n\ttry {\n\t\twriteAll(fd, content);\n\t\tif (destinationMode !== undefined) fchmodSync(fd, destinationMode);\n\t\tcloseSync(fd);\n\t} catch (error) {\n\t\ttry {\n\t\t\tcloseSync(fd);\n\t\t} catch {}\n\t\ttry {\n\t\t\tunlinkSync(temporary);\n\t\t} catch {}\n\t\tthrow error;\n\t}\n\ttry {\n\t\trenameSync(temporary, filePath);\n\t} catch (error) {\n\t\ttry {\n\t\t\tunlinkSync(temporary);\n\t\t} catch {}\n\t\t// Same Windows sharing case as the gated path, and the same answer: a file something\n\t\t// else has open is still a file the caller asked to write.\n\t\tif (!isWindowsSharingViolation(error)) throw error;\n\t\tconst fd = openSync(filePath, constants.O_WRONLY | constants.O_CREAT, destinationMode ?? 0o666);\n\t\ttry {\n\t\t\tftruncateSync(fd, 0);\n\t\t\twriteAll(fd, content);\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t}\n\t}\n}\n\nfunction tryMacOSScreenshotPath(filePath: string): string {\n\treturn filePath.replace(/ (AM|PM)\\./gi, `${NARROW_NO_BREAK_SPACE}$1.`);\n}\n\nfunction tryNFDVariant(filePath: string): string {\n\t// macOS stores filenames in NFD (decomposed) form, try converting user input to NFD\n\treturn filePath.normalize(\"NFD\");\n}\n\nfunction tryCurlyQuoteVariant(filePath: string): string {\n\t// macOS uses U+2019 (right single quotation mark) in screenshot names like \"Capture d'écran\"\n\t// Users typically type U+0027 (straight apostrophe)\n\treturn filePath.replace(/'/g, \"\\u2019\");\n}\n\nfunction fileExists(filePath: string): boolean {\n\ttry {\n\t\taccessSync(filePath, constants.F_OK);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nexport async function pathExists(filePath: string): Promise<boolean> {\n\ttry {\n\t\tawait access(filePath, constants.F_OK);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nexport function expandPath(filePath: string): string {\n\treturn normalizePath(filePath, { normalizeUnicodeSpaces: true, stripAtPrefix: true });\n}\n\n/**\n * Resolve a path relative to the given cwd, handling ~ expansion and absolute\n * paths. The result is the single target used by both permission matching and\n * filesystem execution.\n */\nexport function resolveToCwd(filePath: string, cwd: string): string {\n\tconst requested = resolvePath(filePath, cwd, { normalizeUnicodeSpaces: true, stripAtPrefix: true });\n\ttry {\n\t\treturn realpathSync(requested);\n\t} catch {\n\t\ttry {\n\t\t\treturn joinCanonicalParent(requested);\n\t\t} catch {\n\t\t\treturn requested;\n\t\t}\n\t}\n}\n\nfunction joinCanonicalParent(requested: string): string {\n\treturn join(realpathSync(dirname(requested)), basename(requested));\n}\n\nexport function resolveReadPath(filePath: string, cwd: string): string {\n\tconst resolved = resolveToCwd(filePath, cwd);\n\n\tif (fileExists(resolved)) {\n\t\treturn resolved;\n\t}\n\n\t// Try macOS AM/PM variant (narrow no-break space before AM/PM)\n\tconst amPmVariant = tryMacOSScreenshotPath(resolved);\n\tif (amPmVariant !== resolved && fileExists(amPmVariant)) {\n\t\treturn amPmVariant;\n\t}\n\n\t// Try NFD variant (macOS stores filenames in NFD form)\n\tconst nfdVariant = tryNFDVariant(resolved);\n\tif (nfdVariant !== resolved && fileExists(nfdVariant)) {\n\t\treturn nfdVariant;\n\t}\n\n\t// Try curly quote variant (macOS uses U+2019 in screenshot names)\n\tconst curlyVariant = tryCurlyQuoteVariant(resolved);\n\tif (curlyVariant !== resolved && fileExists(curlyVariant)) {\n\t\treturn curlyVariant;\n\t}\n\n\t// Try combined NFD + curly quote (for French macOS screenshots like \"Capture d'écran\")\n\tconst nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant);\n\tif (nfdCurlyVariant !== resolved && fileExists(nfdCurlyVariant)) {\n\t\treturn nfdCurlyVariant;\n\t}\n\n\treturn resolved;\n}\n\nexport async function resolveReadPathAsync(filePath: string, cwd: string): Promise<string> {\n\tconst resolved = resolveToCwd(filePath, cwd);\n\n\tif (await pathExists(resolved)) {\n\t\treturn resolved;\n\t}\n\n\t// Try macOS AM/PM variant (narrow no-break space before AM/PM)\n\tconst amPmVariant = tryMacOSScreenshotPath(resolved);\n\tif (amPmVariant !== resolved && (await pathExists(amPmVariant))) {\n\t\treturn amPmVariant;\n\t}\n\n\t// Try NFD variant (macOS stores filenames in NFD form)\n\tconst nfdVariant = tryNFDVariant(resolved);\n\tif (nfdVariant !== resolved && (await pathExists(nfdVariant))) {\n\t\treturn nfdVariant;\n\t}\n\n\t// Try curly quote variant (macOS uses U+2019 in screenshot names)\n\tconst curlyVariant = tryCurlyQuoteVariant(resolved);\n\tif (curlyVariant !== resolved && (await pathExists(curlyVariant))) {\n\t\treturn curlyVariant;\n\t}\n\n\t// Try combined NFD + curly quote (for French macOS screenshots like \"Capture d'écran\")\n\tconst nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant);\n\tif (nfdCurlyVariant !== resolved && (await pathExists(nfdCurlyVariant))) {\n\t\treturn nfdCurlyVariant;\n\t}\n\n\treturn resolved;\n}\n"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { accessSync, closeSync, constants, existsSync, fchmodSync, fstatSync, ftruncateSync, mkdirSync, openSync, readSync, realpathSync, renameSync, statSync, unlinkSync, writeSync, } from "node:fs";
|
|
2
3
|
import { access } from "node:fs/promises";
|
|
3
4
|
import { basename, dirname, isAbsolute, join, parse, sep } from "node:path";
|
|
4
5
|
import { normalizePath, resolvePath } from "../../utils/paths.js";
|
|
@@ -19,6 +20,27 @@ export function preparePathOperation(filePath, cwd) {
|
|
|
19
20
|
return { kind: "path-new", path: { kind: "canonical-path", value: canonicalPath } };
|
|
20
21
|
}
|
|
21
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Verify a descriptor's identity against the one captured at authorization.
|
|
25
|
+
*
|
|
26
|
+
* A zero inode is not an identity. POSIX reserves inode 0 for "no file", and Node reports 0
|
|
27
|
+
* on Windows when the file index is unavailable for the handle. Comparing two of them
|
|
28
|
+
* succeeds trivially, which would turn this check -- the only thing standing between an
|
|
29
|
+
* authorized target and one swapped underneath it -- into a no-op at exactly the moment the
|
|
30
|
+
* platform could not say what we are holding. An unverifiable target is not an authorized
|
|
31
|
+
* one, so refuse rather than proceed.
|
|
32
|
+
*
|
|
33
|
+
* The mismatched case keeps its own wording: "changed" and "could not be verified" are
|
|
34
|
+
* different facts, and an operator reading the refusal should be able to tell them apart.
|
|
35
|
+
*/
|
|
36
|
+
function verifyPreparedIdentity(subject, stats, identity) {
|
|
37
|
+
if (stats.ino === 0 || identity.inode === 0) {
|
|
38
|
+
throw new PreparedTargetChangedError(`Authorized ${subject} target identity could not be verified`);
|
|
39
|
+
}
|
|
40
|
+
if (stats.dev !== identity.device || stats.ino !== identity.inode) {
|
|
41
|
+
throw new PreparedTargetChangedError(`Authorized ${subject} target changed before execution`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
22
44
|
/**
|
|
23
45
|
* Read the authorized target through a no-follow final-component open, verifying
|
|
24
46
|
* the descriptor identity captured at authorization before any byte is returned.
|
|
@@ -38,9 +60,7 @@ export function readPreparedPath(operation, maxBytes) {
|
|
|
38
60
|
}
|
|
39
61
|
try {
|
|
40
62
|
const stats = fstatSync(fd);
|
|
41
|
-
|
|
42
|
-
throw new PreparedTargetChangedError("Authorized read target changed before execution");
|
|
43
|
-
}
|
|
63
|
+
verifyPreparedIdentity("read", stats, operation.identity);
|
|
44
64
|
const length = maxBytes === undefined ? stats.size : Math.min(stats.size, maxBytes);
|
|
45
65
|
const buffer = Buffer.alloc(length);
|
|
46
66
|
let offset = 0;
|
|
@@ -79,6 +99,143 @@ function openUnder(parent, name, flags, mode) {
|
|
|
79
99
|
return openSync(`${PROC_FD}/${parent.fd}/${name}`, flags, mode);
|
|
80
100
|
return openSync(join(parent.path, name), flags, mode);
|
|
81
101
|
}
|
|
102
|
+
function renameUnder(parent, from, to) {
|
|
103
|
+
if (useProcFd())
|
|
104
|
+
renameSync(`${PROC_FD}/${parent.fd}/${from}`, `${PROC_FD}/${parent.fd}/${to}`);
|
|
105
|
+
else
|
|
106
|
+
renameSync(join(parent.path, from), join(parent.path, to));
|
|
107
|
+
}
|
|
108
|
+
function unlinkUnder(parent, name) {
|
|
109
|
+
try {
|
|
110
|
+
if (useProcFd())
|
|
111
|
+
unlinkSync(`${PROC_FD}/${parent.fd}/${name}`);
|
|
112
|
+
else
|
|
113
|
+
unlinkSync(join(parent.path, name));
|
|
114
|
+
}
|
|
115
|
+
catch { }
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The name of the sibling a publish stages into.
|
|
119
|
+
*
|
|
120
|
+
* It carries enough of the destination to be recognizable if a crash ever strands one, and
|
|
121
|
+
* a pid and a random suffix so two publishes in one directory cannot collide. The base is
|
|
122
|
+
* truncated to fit: a destination already at the filesystem's per-component limit would
|
|
123
|
+
* otherwise produce a staging name past it, and fail a write that truncate-in-place handled.
|
|
124
|
+
* 255 is the limit on every filesystem this runs on that has one at all.
|
|
125
|
+
*/
|
|
126
|
+
function stagingName(name) {
|
|
127
|
+
const suffix = `.apex-${process.pid}-${randomUUID().slice(0, 8)}`;
|
|
128
|
+
const budget = 255 - suffix.length - 1;
|
|
129
|
+
return `.${name.slice(0, Math.max(1, budget))}${suffix}`;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Write `content` to a sibling of `name` and rename it over `name`.
|
|
133
|
+
*
|
|
134
|
+
* The rename is the publish. Until it runs, `name` still refers to the old inode with all
|
|
135
|
+
* of its old bytes, so a crash, a full disk, or a kill mid-write destroys nothing. The
|
|
136
|
+
* caller has already proved that `name` refers to the authorized target; this only decides
|
|
137
|
+
* how the bytes arrive.
|
|
138
|
+
*
|
|
139
|
+
* `mode` is the destination's, captured from the descriptor the identity check used, so a
|
|
140
|
+
* publish does not quietly reset a file's permissions to the temp file's. Ownership is not
|
|
141
|
+
* carried: a rename installs a file this process created, so a target owned by someone else
|
|
142
|
+
* and merely writable by us changes owner. That is inherent to publishing by rename and is
|
|
143
|
+
* the same trade the session store already makes.
|
|
144
|
+
*
|
|
145
|
+
* One guarantee narrows, and it is worth stating plainly rather than leaving for a reader to
|
|
146
|
+
* derive. ADR 0029's identity check still runs and still aborts on a mismatch, but it now
|
|
147
|
+
* proves the name referred to the authorized file at the moment of the check rather than at
|
|
148
|
+
* the moment of the write. Writing through the checked descriptor could not be redirected by
|
|
149
|
+
* a later swap; a rename can be, because it resolves the name again. The window is between
|
|
150
|
+
* the identity check and the rename, both anchored to the same validated parent descriptor,
|
|
151
|
+
* and exploiting it needs a local attacker placing a file at that exact name in a directory
|
|
152
|
+
* the call was already authorized to write. That is traded for crash safety, which is not a
|
|
153
|
+
* race: it is what every interrupted write did.
|
|
154
|
+
*
|
|
155
|
+
* Windows refuses a rename onto a destination another handle holds open, and an ordinary read
|
|
156
|
+
* handle is enough to trigger it. An earlier version of this comment called that an acceptable
|
|
157
|
+
* trade. Three-OS CI disproved it: the case is an editor, a watcher, or a language server with
|
|
158
|
+
* the file open, which is the common case rather than an exotic one, and failing the write
|
|
159
|
+
* there is not a trade a user opted into. It falls back to `writeInPlace` instead, so Windows
|
|
160
|
+
* gets atomic publish whenever it can and a working write when it cannot.
|
|
161
|
+
*/
|
|
162
|
+
function publishByRename(parent, name, content, mode) {
|
|
163
|
+
const temporary = stagingName(name);
|
|
164
|
+
let fd;
|
|
165
|
+
try {
|
|
166
|
+
fd = openUnder(parent, temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, mode);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
throw new PreparedTargetChangedError("Authorized write could not stage its replacement", { cause: error });
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
writeAll(fd, content);
|
|
173
|
+
// The create mode passes through umask; the destination's mode does not.
|
|
174
|
+
fchmodSync(fd, mode);
|
|
175
|
+
closeSync(fd);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
try {
|
|
179
|
+
closeSync(fd);
|
|
180
|
+
}
|
|
181
|
+
catch { }
|
|
182
|
+
unlinkUnder(parent, temporary);
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
renameUnder(parent, temporary, name);
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
unlinkUnder(parent, temporary);
|
|
191
|
+
if (isWindowsSharingViolation(error))
|
|
192
|
+
return false;
|
|
193
|
+
throw new PreparedTargetChangedError("Authorized write target changed before execution", { cause: error });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Windows refuses to rename onto a destination another handle holds open, and an ordinary
|
|
198
|
+
* read handle is enough. An editor, a watcher, or a language server with the file open is
|
|
199
|
+
* the common case, not an exotic one, so a rename that cannot land there has to fall back
|
|
200
|
+
* rather than fail the write.
|
|
201
|
+
*
|
|
202
|
+
* Three OS CI found this. The Linux and macOS jobs pass, because POSIX renames over an open
|
|
203
|
+
* file happily.
|
|
204
|
+
*/
|
|
205
|
+
function isWindowsSharingViolation(error) {
|
|
206
|
+
if (process.platform !== "win32")
|
|
207
|
+
return false;
|
|
208
|
+
const code = error?.code;
|
|
209
|
+
return code === "EPERM" || code === "EACCES" || code === "EBUSY";
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The pre-rename publish, kept for the one case that cannot use a rename.
|
|
213
|
+
*
|
|
214
|
+
* This is the truncate-then-write window the rename exists to close, so it runs only when
|
|
215
|
+
* Windows has refused the rename. Crash safety is lost for that write; the alternative is
|
|
216
|
+
* refusing to write a file something else has open, which is worse and is not a trade a
|
|
217
|
+
* user opted into. The identity check is re-run against a freshly opened descriptor, so the
|
|
218
|
+
* weaker durability does not come with a weaker target guarantee.
|
|
219
|
+
*/
|
|
220
|
+
function writeInPlace(parent, name, content, identity) {
|
|
221
|
+
let fd;
|
|
222
|
+
try {
|
|
223
|
+
fd = openUnder(parent, name, constants.O_WRONLY | constants.O_NOFOLLOW);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
throw new PreparedTargetChangedError("Authorized write target changed before execution", { cause: error });
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
if (identity) {
|
|
230
|
+
verifyPreparedIdentity("write", fstatSync(fd), identity);
|
|
231
|
+
}
|
|
232
|
+
ftruncateSync(fd, 0);
|
|
233
|
+
writeAll(fd, content);
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
closeSync(fd);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
82
239
|
function mkdirUnder(parent, name) {
|
|
83
240
|
if (useProcFd())
|
|
84
241
|
mkdirSync(`${PROC_FD}/${parent.fd}/${name}`);
|
|
@@ -176,19 +333,23 @@ export function writePreparedPath(operation, content) {
|
|
|
176
333
|
catch (error) {
|
|
177
334
|
throw new PreparedTargetChangedError("Authorized write target changed before execution", { cause: error });
|
|
178
335
|
}
|
|
336
|
+
let mode;
|
|
179
337
|
try {
|
|
180
338
|
const stats = fstatSync(fd);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
ftruncateSync(fd, 0);
|
|
185
|
-
writeAll(fd, content);
|
|
339
|
+
verifyPreparedIdentity("write", stats, operation.identity);
|
|
340
|
+
mode = stats.mode & 0o7777;
|
|
186
341
|
}
|
|
187
342
|
finally {
|
|
188
343
|
closeSync(fd);
|
|
189
344
|
}
|
|
345
|
+
if (!publishByRename(parent, name, content, mode)) {
|
|
346
|
+
writeInPlace(parent, name, content, operation.identity);
|
|
347
|
+
}
|
|
190
348
|
return;
|
|
191
349
|
}
|
|
350
|
+
// The exclusive create is what proves the name was still absent, which is the whole
|
|
351
|
+
// authorization for a new-file write. It reserves the name; the rename below then
|
|
352
|
+
// replaces that empty placeholder, so the name never holds a half-written file.
|
|
192
353
|
let fd;
|
|
193
354
|
try {
|
|
194
355
|
fd = openUnder(parent, name, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
@@ -196,13 +357,77 @@ export function writePreparedPath(operation, content) {
|
|
|
196
357
|
catch (error) {
|
|
197
358
|
throw new PreparedTargetChangedError("Authorized new-file target changed before execution", { cause: error });
|
|
198
359
|
}
|
|
360
|
+
closeSync(fd);
|
|
199
361
|
try {
|
|
362
|
+
// The reservation this just created is ours, so there is no prior identity to
|
|
363
|
+
// re-check if Windows refuses the rename onto it.
|
|
364
|
+
if (!publishByRename(parent, name, content, 0o600)) {
|
|
365
|
+
writeInPlace(parent, name, content);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
unlinkUnder(parent, name);
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Atomic publish for an ungated write, where no prepared operation exists to pin the target.
|
|
376
|
+
*
|
|
377
|
+
* `writePreparedPath` is the gated path and the one that matters; this is its counterpart for
|
|
378
|
+
* a tool invoked outside the permission gate, so that neither route can leave a half-written
|
|
379
|
+
* file. It makes no containment claim: with no authorization there is no authorized target to
|
|
380
|
+
* hold the write to, and the no-follow walk would have nothing to check against.
|
|
381
|
+
*/
|
|
382
|
+
export function writePathAtomically(filePath, content) {
|
|
383
|
+
const temporary = join(dirname(filePath), stagingName(basename(filePath)));
|
|
384
|
+
// Only an existing destination has a mode worth carrying. For a new file there is
|
|
385
|
+
// nothing to capture, and chmod'ing to the 0o666 open default would force past the
|
|
386
|
+
// caller's umask and publish a world-writable file, which `writeFile` never did.
|
|
387
|
+
let destinationMode;
|
|
388
|
+
try {
|
|
389
|
+
destinationMode = statSync(filePath).mode & 0o7777;
|
|
390
|
+
}
|
|
391
|
+
catch { }
|
|
392
|
+
const fd = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, destinationMode ?? 0o666);
|
|
393
|
+
try {
|
|
394
|
+
writeAll(fd, content);
|
|
395
|
+
if (destinationMode !== undefined)
|
|
396
|
+
fchmodSync(fd, destinationMode);
|
|
397
|
+
closeSync(fd);
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
try {
|
|
401
|
+
closeSync(fd);
|
|
402
|
+
}
|
|
403
|
+
catch { }
|
|
404
|
+
try {
|
|
405
|
+
unlinkSync(temporary);
|
|
406
|
+
}
|
|
407
|
+
catch { }
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
renameSync(temporary, filePath);
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
try {
|
|
415
|
+
unlinkSync(temporary);
|
|
416
|
+
}
|
|
417
|
+
catch { }
|
|
418
|
+
// Same Windows sharing case as the gated path, and the same answer: a file something
|
|
419
|
+
// else has open is still a file the caller asked to write.
|
|
420
|
+
if (!isWindowsSharingViolation(error))
|
|
421
|
+
throw error;
|
|
422
|
+
const fd = openSync(filePath, constants.O_WRONLY | constants.O_CREAT, destinationMode ?? 0o666);
|
|
423
|
+
try {
|
|
424
|
+
ftruncateSync(fd, 0);
|
|
200
425
|
writeAll(fd, content);
|
|
201
426
|
}
|
|
202
427
|
finally {
|
|
203
428
|
closeSync(fd);
|
|
204
429
|
}
|
|
205
|
-
}
|
|
430
|
+
}
|
|
206
431
|
}
|
|
207
432
|
function tryMacOSScreenshotPath(filePath) {
|
|
208
433
|
return filePath.replace(/ (AM|PM)\./gi, `${NARROW_NO_BREAK_SPACE}$1.`);
|