@mikode13/harness 4.0.0 → 5.0.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/README.md +285 -20
- package/dist/agent/domain/agent.d.ts +12 -0
- package/dist/agent/domain/agent.js.map +1 -1
- package/dist/agent/domain/approval.d.ts +14 -0
- package/dist/agent/domain/approval.js +28 -3
- package/dist/agent/domain/approval.js.map +1 -1
- package/dist/agent/domain/runContext.d.ts +31 -0
- package/dist/agent/domain/runContext.js +47 -0
- package/dist/agent/domain/runContext.js.map +1 -0
- package/dist/diff/domain/unifiedDiff.d.ts +19 -0
- package/dist/diff/domain/unifiedDiff.js +166 -0
- package/dist/diff/domain/unifiedDiff.js.map +1 -0
- package/dist/engines/domain/model/historyFollower.d.ts +41 -0
- package/dist/engines/domain/model/historyFollower.js +158 -0
- package/dist/engines/domain/model/historyFollower.js.map +1 -0
- package/dist/engines/domain/model/llmAgent.d.ts +42 -4
- package/dist/engines/domain/model/llmAgent.js +224 -36
- package/dist/engines/domain/model/llmAgent.js.map +1 -1
- package/dist/factory/infrastructure/agentFactory.d.ts +14 -0
- package/dist/factory/infrastructure/agentFactory.js +20 -2
- package/dist/factory/infrastructure/agentFactory.js.map +1 -1
- package/dist/factory/infrastructure/agentLLMFactory.d.ts +119 -14
- package/dist/factory/infrastructure/agentLLMFactory.js +284 -57
- package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/llm/domain/conversation.d.ts +44 -4
- package/dist/llm/domain/conversation.js +56 -7
- package/dist/llm/domain/conversation.js.map +1 -1
- package/dist/llm/domain/llm.d.ts +3 -1
- package/dist/llm/domain/llm.js.map +1 -1
- package/dist/llm/domain/tool.d.ts +13 -0
- package/dist/llm/domain/tool.js +12 -1
- package/dist/llm/domain/tool.js.map +1 -1
- package/dist/llm/infrastructure/claudeLLMClient.d.ts +3 -2
- package/dist/llm/infrastructure/claudeLLMClient.js +38 -10
- package/dist/llm/infrastructure/claudeLLMClient.js.map +1 -1
- package/dist/llm/infrastructure/openAILLMClient.d.ts +2 -1
- package/dist/llm/infrastructure/openAILLMClient.js +66 -16
- package/dist/llm/infrastructure/openAILLMClient.js.map +1 -1
- package/dist/orchestration/domain/model/orchestratorAgent.d.ts +11 -2
- package/dist/orchestration/domain/model/orchestratorAgent.js +43 -4
- package/dist/orchestration/domain/model/orchestratorAgent.js.map +1 -1
- package/dist/recovery/domain/fileChanges.d.ts +31 -0
- package/dist/recovery/domain/fileChanges.js +48 -0
- package/dist/recovery/domain/fileChanges.js.map +1 -0
- package/dist/recovery/domain/history.d.ts +114 -0
- package/dist/recovery/domain/history.js +62 -0
- package/dist/recovery/domain/history.js.map +1 -0
- package/dist/recovery/domain/recoveryStore.d.ts +233 -0
- package/dist/recovery/domain/recoveryStore.js +32 -0
- package/dist/recovery/domain/recoveryStore.js.map +1 -0
- package/dist/recovery/domain/retention.d.ts +33 -0
- package/dist/recovery/domain/retention.js +90 -0
- package/dist/recovery/domain/retention.js.map +1 -0
- package/dist/recovery/domain/runChanges.d.ts +37 -0
- package/dist/recovery/domain/runChanges.js +109 -0
- package/dist/recovery/domain/runChanges.js.map +1 -0
- package/dist/recovery/domain/runTree.d.ts +32 -0
- package/dist/recovery/domain/runTree.js +90 -0
- package/dist/recovery/domain/runTree.js.map +1 -0
- package/dist/recovery/infrastructure/createHistory.d.ts +11 -0
- package/dist/recovery/infrastructure/createHistory.js +17 -0
- package/dist/recovery/infrastructure/createHistory.js.map +1 -0
- package/dist/recovery/infrastructure/fileRecoveryStore.d.ts +133 -0
- package/dist/recovery/infrastructure/fileRecoveryStore.js +1043 -0
- package/dist/recovery/infrastructure/fileRecoveryStore.js.map +1 -0
- package/dist/recovery/infrastructure/workspaceFiles.d.ts +30 -0
- package/dist/recovery/infrastructure/workspaceFiles.js +125 -0
- package/dist/recovery/infrastructure/workspaceFiles.js.map +1 -0
- package/dist/tools/domain/accessPolicy.d.ts +30 -0
- package/dist/tools/domain/accessPolicy.js +11 -0
- package/dist/tools/domain/accessPolicy.js.map +1 -0
- package/dist/tools/domain/commands.d.ts +93 -0
- package/dist/tools/domain/commands.js +191 -0
- package/dist/tools/domain/commands.js.map +1 -0
- package/dist/tools/domain/editResponse.d.ts +14 -0
- package/dist/tools/domain/editResponse.js +79 -0
- package/dist/tools/domain/editResponse.js.map +1 -0
- package/dist/tools/domain/fileEdits.d.ts +64 -0
- package/dist/tools/domain/fileEdits.js +21 -0
- package/dist/tools/domain/fileEdits.js.map +1 -0
- package/dist/tools/domain/lineLength.d.ts +7 -0
- package/dist/tools/domain/lineLength.js +26 -0
- package/dist/tools/domain/lineLength.js.map +1 -0
- package/dist/tools/domain/preparedCall.d.ts +31 -0
- package/dist/tools/domain/preparedCall.js +14 -0
- package/dist/tools/domain/preparedCall.js.map +1 -0
- package/dist/tools/domain/readRegistry.d.ts +46 -0
- package/dist/tools/domain/readRegistry.js +62 -0
- package/dist/tools/domain/readRegistry.js.map +1 -0
- package/dist/tools/domain/textLines.d.ts +26 -0
- package/dist/tools/domain/textLines.js +44 -0
- package/dist/tools/domain/textLines.js.map +1 -0
- package/dist/tools/domain/v4aPatch.d.ts +17 -0
- package/dist/tools/domain/v4aPatch.js +236 -0
- package/dist/tools/domain/v4aPatch.js.map +1 -0
- package/dist/tools/infrastructure/boundedWorkspace.d.ts +11 -2
- package/dist/tools/infrastructure/boundedWorkspace.js +9 -1
- package/dist/tools/infrastructure/boundedWorkspace.js.map +1 -1
- package/dist/tools/infrastructure/commandCheckpoint.d.ts +55 -0
- package/dist/tools/infrastructure/commandCheckpoint.js +178 -0
- package/dist/tools/infrastructure/commandCheckpoint.js.map +1 -0
- package/dist/tools/infrastructure/commandProcess.d.ts +47 -0
- package/dist/tools/infrastructure/commandProcess.js +217 -0
- package/dist/tools/infrastructure/commandProcess.js.map +1 -0
- package/dist/tools/infrastructure/commandTool.d.ts +36 -0
- package/dist/tools/infrastructure/commandTool.js +254 -0
- package/dist/tools/infrastructure/commandTool.js.map +1 -0
- package/dist/tools/infrastructure/createWorkspace.d.ts +10 -0
- package/dist/tools/infrastructure/createWorkspace.js +11 -3
- package/dist/tools/infrastructure/createWorkspace.js.map +1 -1
- package/dist/tools/infrastructure/defineTool.d.ts +14 -0
- package/dist/tools/infrastructure/defineTool.js +28 -5
- package/dist/tools/infrastructure/defineTool.js.map +1 -1
- package/dist/tools/infrastructure/editFormats.d.ts +22 -0
- package/dist/tools/infrastructure/editFormats.js +211 -0
- package/dist/tools/infrastructure/editFormats.js.map +1 -0
- package/dist/tools/infrastructure/fileEditor.d.ts +64 -0
- package/dist/tools/infrastructure/fileEditor.js +306 -0
- package/dist/tools/infrastructure/fileEditor.js.map +1 -0
- package/dist/tools/infrastructure/fileTools.d.ts +81 -0
- package/dist/tools/infrastructure/fileTools.js +152 -0
- package/dist/tools/infrastructure/fileTools.js.map +1 -0
- package/dist/tools/infrastructure/gitIgnoreRules.d.ts +15 -0
- package/dist/tools/infrastructure/gitIgnoreRules.js +74 -0
- package/dist/tools/infrastructure/gitIgnoreRules.js.map +1 -0
- package/dist/tools/infrastructure/gitWorkspace.d.ts +1 -1
- package/dist/tools/infrastructure/gitWorkspace.js +1 -1
- package/dist/tools/infrastructure/gitWorkspace.js.map +1 -1
- package/dist/tools/infrastructure/ignoreFileChanges.d.ts +14 -0
- package/dist/tools/infrastructure/ignoreFileChanges.js +62 -0
- package/dist/tools/infrastructure/ignoreFileChanges.js.map +1 -0
- package/dist/tools/infrastructure/policyWorkspace.d.ts +52 -0
- package/dist/tools/infrastructure/policyWorkspace.js +32 -0
- package/dist/tools/infrastructure/policyWorkspace.js.map +1 -0
- package/dist/tools/infrastructure/ripgrepWorkspace.d.ts +2 -1
- package/dist/tools/infrastructure/ripgrepWorkspace.js +1 -1
- package/dist/tools/infrastructure/ripgrepWorkspace.js.map +1 -1
- package/dist/tools/infrastructure/rootsAccessPolicy.d.ts +100 -0
- package/dist/tools/infrastructure/rootsAccessPolicy.js +301 -0
- package/dist/tools/infrastructure/rootsAccessPolicy.js.map +1 -0
- package/dist/tools/infrastructure/runProcess.d.ts +4 -1
- package/dist/tools/infrastructure/runProcess.js +4 -2
- package/dist/tools/infrastructure/runProcess.js.map +1 -1
- package/dist/tools/infrastructure/secretPaths.d.ts +8 -0
- package/dist/tools/infrastructure/secretPaths.js +76 -0
- package/dist/tools/infrastructure/secretPaths.js.map +1 -0
- package/dist/tools/infrastructure/showChangesTool.d.ts +12 -0
- package/dist/tools/infrastructure/showChangesTool.js +43 -0
- package/dist/tools/infrastructure/showChangesTool.js.map +1 -0
- package/dist/tools/infrastructure/trackedEdits.d.ts +33 -0
- package/dist/tools/infrastructure/trackedEdits.js +70 -0
- package/dist/tools/infrastructure/trackedEdits.js.map +1 -0
- package/dist/tools/infrastructure/trackedReadFile.d.ts +21 -0
- package/dist/tools/infrastructure/trackedReadFile.js +78 -0
- package/dist/tools/infrastructure/trackedReadFile.js.map +1 -0
- package/dist/tools/infrastructure/workspaceTools.d.ts +28 -0
- package/dist/tools/infrastructure/workspaceTools.js +53 -40
- package/dist/tools/infrastructure/workspaceTools.js.map +1 -1
- package/dist/tools/infrastructure/workspaceWrites.d.ts +23 -0
- package/dist/tools/infrastructure/workspaceWrites.js +42 -0
- package/dist/tools/infrastructure/workspaceWrites.js.map +1 -0
- package/dist/tools/infrastructure/writeSession.d.ts +35 -0
- package/dist/tools/infrastructure/writeSession.js +81 -0
- package/dist/tools/infrastructure/writeSession.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileEditor.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/fileEditor.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomBytes } from 'node:crypto';\nimport {\n\tchmod,\n\tlink,\n\tlstat,\n\tmkdir,\n\tmkdtemp,\n\topen,\n\treadFile,\n\trename,\n\trm,\n\trmdir,\n\tstat,\n\tunlink,\n} from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { basename, dirname, join } from 'node:path';\nimport type { FileState, RunJournal } from '#src/recovery/domain/recoveryStore';\nimport type { ToolRisk } from '#src/agent/domain/approval';\nimport type { AccessPolicy, AllowedPath } from '../domain/accessPolicy.ts';\nimport {\n\tEditRefusedError,\n\tEditUnconfirmedError,\n\ttype FileChange,\n\ttype PreparedEdit,\n} from '../domain/fileEdits.ts';\nimport { isIgnoreFile, widensIgnoreRules } from './ignoreFileChanges.ts';\nimport type { WriteSession } from './writeSession.ts';\n\n// Larger than any source file a model should rewrite whole, and small enough to keep a copy.\nconst defaultMaxFileBytes = 5 * 1024 * 1024;\n\nfunction hashOf(content: Buffer): string {\n\treturn createHash('sha256').update(content).digest('hex');\n}\n\nfunction code(error: unknown): string {\n\treturn (error as NodeJS.ErrnoException).code ?? 'an unknown error';\n}\n\nfunction sameState(a: FileState, b: FileState): boolean {\n\tif (!a.exists || !b.exists) return a.exists === b.exists;\n\treturn a.hash === b.hash && a.mode === b.mode;\n}\n\nasync function syncDirectory(path: string): Promise<void> {\n\tconst directory = await open(path, 'r');\n\ttry {\n\t\tawait directory.sync();\n\t} finally {\n\t\tawait directory.close();\n\t}\n}\n\nlet newFileMode: Promise<number> | undefined;\n\n/**\n * The mode a new file gets in this process: 0o666 less the umask. Measured once by creating\n * one, because reading the umask directly briefly changes it for every thread.\n */\nfunction getNewFileMode(): Promise<number> {\n\tnewFileMode ??= (async () => {\n\t\tconst folder = await mkdtemp(join(tmpdir(), 'mikode-harness-mode-'));\n\t\ttry {\n\t\t\tconst probe = join(folder, 'probe');\n\t\t\tawait (await open(probe, 'wx', 0o666)).close();\n\t\t\treturn (await stat(probe)).mode & 0o7777;\n\t\t} finally {\n\t\t\tawait rm(folder, { recursive: true, force: true });\n\t\t}\n\t})();\n\treturn newFileMode;\n}\n\n/** The folders between the nearest existing one and `folder`, outermost first. */\nasync function missingFolders(folder: string): Promise<string[]> {\n\tconst missing: string[] = [];\n\tfor (let current = folder; ; current = dirname(current)) {\n\t\tconst exists = await lstat(current).then(\n\t\t\t() => true,\n\t\t\t(error: unknown) => {\n\t\t\t\tif (code(error) === 'ENOENT') return false;\n\t\t\t\tthrow error;\n\t\t\t},\n\t\t);\n\t\tif (exists || dirname(current) === current) return missing;\n\t\tmissing.unshift(current);\n\t}\n}\n\n/**\n * The risk of a prepared change. A change git or the run's record can undo is `mutating`; one\n * that widens `.gitignore` is `destructive`, so it waits for the user, because it would open\n * paths that every tool keeps closed.\n */\nexport function editRisk(prepared: PreparedEdit): ToolRisk {\n\treturn prepared.widensIgnoreRules ? 'destructive' : 'mutating';\n}\n\n/**\n * Makes the changes the edit tools ask for, under the access policy and the run's recovery\n * journal. `prepare` checks a change and fixes what it will do; `apply` makes it, in this order:\n *\n * 1. Check the policy again, and that the file still holds what was prepared.\n * 2. Store the content it replaces and the content it writes, and record the change as\n * prepared. If either fails, nothing is touched.\n * 3. Write: a new file appears whole and only if nothing took its place meanwhile; a replaced\n * one is swapped for its new version in one rename; a deleted one is unlinked.\n * 4. Record the change as applied, or as abandoned when the write failed and left the file as\n * it was. A change the record cannot confirm stops the session and throws\n * `EditUnconfirmedError`, never a success or a refusal.\n *\n * Every message names the file as the model wrote it, never by its place on the host.\n */\nexport class FileEditor {\n\tprivate readonly policy: AccessPolicy;\n\tprivate readonly session: WriteSession;\n\tprivate readonly maxFileBytes: number;\n\n\tconstructor({\n\t\tpolicy,\n\t\tsession,\n\t\tmaxFileBytes = defaultMaxFileBytes,\n\t}: {\n\t\tpolicy: AccessPolicy;\n\t\tsession: WriteSession;\n\t\tmaxFileBytes?: number;\n\t}) {\n\t\tthis.policy = policy;\n\t\tthis.session = session;\n\t\tthis.maxFileBytes = maxFileBytes;\n\t}\n\n\t/**\n\t * What `path` holds now, checked as a change to it would be: an edit format works its new\n\t * content out from this, and an edit tool compares it with the version the model read.\n\t */\n\tasync current(\n\t\tpath: string,\n\t\tsignal: AbortSignal,\n\t): Promise<{ target: AllowedPath; state: FileState; content?: Buffer }> {\n\t\tconst target = await this.policy.check(path, 'write', signal);\n\t\treturn { target, ...(await this.read(target.absolute, path)) };\n\t}\n\n\tasync prepare(change: FileChange, signal: AbortSignal): Promise<PreparedEdit> {\n\t\tconst { path } = change;\n\t\tconst target = await this.policy.check(path, 'write', signal);\n\t\tconst { state, content: previous } = await this.read(target.absolute, path);\n\n\t\tif (change.kind === 'create') {\n\t\t\tif (state.exists) {\n\t\t\t\tthrow new EditRefusedError(`\"${path}\" already exists; read it and change it instead`);\n\t\t\t}\n\t\t} else {\n\t\t\tif (!state.exists) throw new EditRefusedError(`\"${path}\" does not exist`);\n\t\t\tif (change.expected !== undefined && change.expected !== state.hash) {\n\t\t\t\tthrow new EditRefusedError(`\"${path}\" changed since it was read; read it again`);\n\t\t\t}\n\t\t}\n\n\t\tconst content = change.kind === 'delete' ? undefined : change.content;\n\t\tif (content && content.length > this.maxFileBytes) {\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t`The new content of \"${path}\" is larger than ${String(this.maxFileBytes / 1024 / 1024)} MB`,\n\t\t\t);\n\t\t}\n\t\tif (content && state.exists && hashOf(content) === state.hash) {\n\t\t\tthrow new EditRefusedError(`\"${path}\" already holds that content; nothing to change`);\n\t\t}\n\n\t\t// Copies all the way down: what is approved must not change while the approval waits.\n\t\treturn Object.freeze({\n\t\t\tkind: change.kind,\n\t\t\tpath,\n\t\t\ttarget: Object.freeze({ ...target }),\n\t\t\tbefore: Object.freeze({ ...state }),\n\t\t\tcontent: content && Buffer.from(content),\n\t\t\tcontentHash: content && hashOf(content),\n\t\t\twidensIgnoreRules: isIgnoreFile(target.absolute) && widensIgnoreRules(previous, content),\n\t\t});\n\t}\n\n\tasync apply(prepared: PreparedEdit, signal: AbortSignal): Promise<void> {\n\t\tconst { path } = prepared;\n\t\tsignal.throwIfAborted();\n\t\t// A Buffer's bytes cannot be frozen: copied, then checked, so what is written is what was\n\t\t// approved, whatever happens to `prepared.content` from here on.\n\t\tconst content = prepared.content && Buffer.from(prepared.content);\n\t\tif ((content && hashOf(content)) !== prepared.contentHash) {\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t`The content prepared for \"${path}\" changed after it was prepared; prepare the change again`,\n\t\t\t);\n\t\t}\n\t\tconst target = await this.policy.check(path, 'write', signal);\n\t\tif (target.absolute !== prepared.target.absolute) {\n\t\t\tthrow new EditRefusedError(`\"${path}\" now leads somewhere else; prepare the change again`);\n\t\t}\n\t\tconst current = await this.read(target.absolute, path);\n\t\tif (!sameState(current.state, prepared.before)) {\n\t\t\tthrow new EditRefusedError(`\"${path}\" changed after this change was prepared; read it again`);\n\t\t}\n\n\t\t// Checked again before the first effect: starting the journal takes the workspace.\n\t\tsignal.throwIfAborted();\n\t\tconst journal = await this.session.journalForChange();\n\t\tconst after = await this.record(journal, path, current.content, content, prepared.before);\n\t\tconst createdFolders =\n\t\t\tprepared.kind === 'create' ? await missingFolders(dirname(target.absolute)) : [];\n\t\tconst sequence = await this.journal(path, () =>\n\t\t\tjournal.prepare({\n\t\t\t\tpath: target.absolute,\n\t\t\t\tbefore: current.state,\n\t\t\t\tafter,\n\t\t\t\t...(createdFolders.length > 0 ? { createdFolders } : {}),\n\t\t\t}),\n\t\t);\n\n\t\t// The last moment to stop: past here the file changes.\n\t\tif (signal.aborted) {\n\t\t\tawait journal.abandoned(sequence);\n\t\t\tsignal.throwIfAborted();\n\t\t}\n\n\t\ttry {\n\t\t\tawait this.write(prepared, content, target.absolute);\n\t\t} catch (error) {\n\t\t\tawait this.settleFailedWrite({\n\t\t\t\tjournal,\n\t\t\t\tsequence,\n\t\t\t\tkind: prepared.kind,\n\t\t\t\tpath,\n\t\t\t\ttarget: target.absolute,\n\t\t\t\tcurrent,\n\t\t\t\tafter,\n\t\t\t\tcreatedFolders,\n\t\t\t\terror,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tawait this.markApplied(journal, sequence, path);\n\t}\n\n\t/**\n\t * Records a change that was made. If that fails, its prepared line still holds both states, so\n\t * an undo can tell; but the record cannot be trusted to continue, and the caller must know.\n\t */\n\tprivate async markApplied(journal: RunJournal, sequence: number, path: string): Promise<void> {\n\t\ttry {\n\t\t\tawait journal.applied(sequence);\n\t\t} catch (error) {\n\t\t\tthis.session.stop('Writing stopped: the harness could not record a change it made');\n\t\t\tthrow new EditUnconfirmedError(\n\t\t\t\t`\"${path}\" was changed, but the harness could not record it; writing has stopped for this run`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\t}\n\n\t/** What `path` holds now. Only a regular file within the size limit can be changed. */\n\tprivate async read(\n\t\tabsolute: string,\n\t\tpath: string,\n\t): Promise<{ state: FileState; content?: Buffer }> {\n\t\tlet stats;\n\t\ttry {\n\t\t\tstats = await lstat(absolute);\n\t\t} catch (error) {\n\t\t\tif (code(error) === 'ENOENT') return { state: { exists: false } };\n\t\t\tthrow new EditRefusedError(`Could not read \"${path}\" (${code(error)})`, { cause: error });\n\t\t}\n\t\tif (!stats.isFile()) throw new EditRefusedError(`\"${path}\" is not a regular file`);\n\t\tif (stats.size > this.maxFileBytes) {\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t`\"${path}\" is larger than ${String(this.maxFileBytes / 1024 / 1024)} MB, too large to change`,\n\t\t\t);\n\t\t}\n\n\t\tlet content: Buffer;\n\t\ttry {\n\t\t\tcontent = await readFile(absolute);\n\t\t} catch (error) {\n\t\t\tthrow new EditRefusedError(`Could not read \"${path}\" (${code(error)})`, { cause: error });\n\t\t}\n\t\treturn {\n\t\t\tstate: { exists: true, hash: hashOf(content), mode: stats.mode & 0o7777 },\n\t\t\tcontent,\n\t\t};\n\t}\n\n\t/** Stores what the change replaces and what it writes, and returns the state it leaves. */\n\tprivate async record(\n\t\tjournal: RunJournal,\n\t\tpath: string,\n\t\tprevious: Buffer | undefined,\n\t\tcontent: Buffer | undefined,\n\t\tbefore: FileState,\n\t): Promise<FileState> {\n\t\tif (previous) await this.journal(path, () => journal.saveContent(previous));\n\t\tif (!content) return { exists: false };\n\n\t\tconst hash = await this.journal(path, () => journal.saveContent(content));\n\t\treturn { exists: true, hash, mode: before.exists ? before.mode : await getNewFileMode() };\n\t}\n\n\tprivate async journal<Result>(path: string, step: () => Promise<Result>): Promise<Result> {\n\t\ttry {\n\t\t\treturn await step();\n\t\t} catch (error) {\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t`The harness could not record how to undo the change to \"${path}\", so it was not changed`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate async write(\n\t\t{ kind, before }: PreparedEdit,\n\t\tcontent: Buffer | undefined,\n\t\tabsolute: string,\n\t): Promise<void> {\n\t\tif (kind === 'delete') {\n\t\t\tawait unlink(absolute);\n\t\t\tawait syncDirectory(dirname(absolute));\n\t\t\treturn;\n\t\t}\n\n\t\tawait mkdir(dirname(absolute), { recursive: true });\n\t\t// Written beside the file, so the final step is one rename or link on the same disk.\n\t\tconst temporary = join(\n\t\t\tdirname(absolute),\n\t\t\t`.${basename(absolute)}.${randomBytes(6).toString('hex')}.mikode-tmp`,\n\t\t);\n\t\ttry {\n\t\t\tconst handle = await open(temporary, 'wx');\n\t\t\ttry {\n\t\t\t\tawait handle.writeFile(content ?? Buffer.alloc(0));\n\t\t\t\tawait handle.sync();\n\t\t\t} finally {\n\t\t\t\tawait handle.close();\n\t\t\t}\n\t\t\tif (before.exists) await chmod(temporary, before.mode);\n\n\t\t\tif (kind === 'create') {\n\t\t\t\t// A link fails if something appeared at the path meanwhile; a rename would replace it.\n\t\t\t\tawait link(temporary, absolute);\n\t\t\t} else {\n\t\t\t\tawait rename(temporary, absolute);\n\t\t\t}\n\t\t} finally {\n\t\t\tawait rm(temporary, { force: true });\n\t\t}\n\t\tawait syncDirectory(dirname(absolute));\n\t}\n\n\t/**\n\t * A write threw. If the file still holds what it held, nothing happened and the change is\n\t * abandoned; if it holds the new state, the change happened anyway. Anything else cannot be\n\t * explained, so the session stops writing.\n\t */\n\tprivate async settleFailedWrite({\n\t\tjournal,\n\t\tsequence,\n\t\tkind,\n\t\tpath,\n\t\ttarget,\n\t\tcurrent,\n\t\tafter,\n\t\tcreatedFolders,\n\t\terror,\n\t}: {\n\t\tjournal: RunJournal;\n\t\tsequence: number;\n\t\tkind: PreparedEdit['kind'];\n\t\tpath: string;\n\t\ttarget: string;\n\t\tcurrent: { state: FileState };\n\t\tafter: FileState;\n\t\tcreatedFolders: string[];\n\t\terror: unknown;\n\t}): Promise<void> {\n\t\tconst now = await this.read(target, path).catch(() => undefined);\n\t\tif (now && sameState(now.state, after)) {\n\t\t\tawait this.markApplied(journal, sequence, path);\n\t\t\treturn;\n\t\t}\n\t\t// A link that found the path taken made no change: what is there now is someone else's.\n\t\tconst appeared = kind === 'create' && code(error) === 'EEXIST';\n\t\tif (appeared || (now && sameState(now.state, current.state))) {\n\t\t\t// Innermost first, and only while empty: anything else in them is not this run's.\n\t\t\tfor (const folder of [...createdFolders].reverse()) {\n\t\t\t\tawait rmdir(folder).catch(() => undefined);\n\t\t\t}\n\t\t\tawait journal.abandoned(sequence).catch(() => undefined);\n\t\t\tthrow new EditRefusedError(\n\t\t\t\tappeared\n\t\t\t\t\t? `\"${path}\" appeared while it was being created; read it before changing it`\n\t\t\t\t\t: `Could not write \"${path}\" (${code(error)}); it was not changed`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\n\t\tthis.session.stop(\n\t\t\t`Writing stopped: the harness could not tell whether \"${path}\" was changed; ask the user to check it`,\n\t\t);\n\t\tthrow new EditUnconfirmedError(\n\t\t\t`Could not tell whether \"${path}\" was changed (${code(error)}); writing has stopped for this run`,\n\t\t\t{ cause: error },\n\t\t);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ToolDefinition } from '#src/llm/domain/tool';
|
|
2
|
+
import type { RecoveryStore } from '#src/recovery/domain/recoveryStore';
|
|
3
|
+
import type { ILogger } from '#src/shared/domain/logger';
|
|
4
|
+
import type { AccessPolicy, WorkspaceRoot } from '../domain/accessPolicy.ts';
|
|
5
|
+
import type { Commands } from '../domain/commands.ts';
|
|
6
|
+
import type { AgentTool } from '../domain/preparedCall.ts';
|
|
7
|
+
import type { Workspace } from '../domain/workspace.ts';
|
|
8
|
+
import type { CoveredFiles } from './commandCheckpoint.ts';
|
|
9
|
+
import { type SecretRules } from './rootsAccessPolicy.ts';
|
|
10
|
+
import { WorkspaceWrites } from './workspaceWrites.ts';
|
|
11
|
+
/** The folders an agent works on, and what it may do in each. */
|
|
12
|
+
export interface WorkspaceOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Each folder `read` or `write`. The first is where a relative path starts, and the one whose
|
|
15
|
+
* history of runs is kept; a write to another root is recorded in it too.
|
|
16
|
+
*/
|
|
17
|
+
roots: WorkspaceRoot[];
|
|
18
|
+
/** More files to keep closed as secrets, and exact files to open. */
|
|
19
|
+
secrets?: SecretRules;
|
|
20
|
+
/**
|
|
21
|
+
* Where the history of runs is kept, relative to the working directory if not absolute.
|
|
22
|
+
* Defaults to the platform's state directory. No tool may reach it, even inside a root.
|
|
23
|
+
*/
|
|
24
|
+
stateDirectory?: string;
|
|
25
|
+
}
|
|
26
|
+
declare const fileToolBrand: unique symbol;
|
|
27
|
+
/**
|
|
28
|
+
* A tool the harness builds for files, from `createFileTools`. It goes to an agent from
|
|
29
|
+
* `createLLMAgent` beside the consumer's own tools; what it does inside is the harness's.
|
|
30
|
+
*/
|
|
31
|
+
export type FileTool = ToolDefinition & {
|
|
32
|
+
readonly [fileToolBrand]: true;
|
|
33
|
+
};
|
|
34
|
+
/** What the file tools of one workspace share: one policy, one history, one record of writes. */
|
|
35
|
+
export interface OpenedWorkspace {
|
|
36
|
+
readonly roots: WorkspaceRoot[];
|
|
37
|
+
readonly policy: AccessPolicy;
|
|
38
|
+
readonly store: RecoveryStore;
|
|
39
|
+
/** The read side, with the policy's word on every path. */
|
|
40
|
+
readonly read: Workspace;
|
|
41
|
+
readonly writes: WorkspaceWrites;
|
|
42
|
+
readonly writable: boolean;
|
|
43
|
+
/** The real path of the first root. */
|
|
44
|
+
readonly base: string;
|
|
45
|
+
/** What a command's checkpoint captures and compares: see `CoveredFiles`. */
|
|
46
|
+
readonly covered: CoveredFiles;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Where a workspace's history is kept, as one real path: made if missing, then resolved, links
|
|
50
|
+
* included. The store and the access policy must name the same folder, or a tool could reach the
|
|
51
|
+
* history the policy means to protect. A relative path is from the working directory.
|
|
52
|
+
*
|
|
53
|
+
* @throws {UnrecoverableError} when the folder cannot be made or reached.
|
|
54
|
+
*/
|
|
55
|
+
export declare function stateDirectoryOf({ stateDirectory }: WorkspaceOptions): Promise<string>;
|
|
56
|
+
/**
|
|
57
|
+
* The recovery store of a workspace, at its first root.
|
|
58
|
+
*
|
|
59
|
+
* @throws {InvalidAgentConfigError} when the workspace has no root.
|
|
60
|
+
* @throws {UnrecoverableError} when the store cannot be opened.
|
|
61
|
+
*/
|
|
62
|
+
export declare function openStore(workspace: WorkspaceOptions, directory: string): Promise<RecoveryStore>;
|
|
63
|
+
export declare function openWorkspace(workspace: WorkspaceOptions, logger: ILogger): Promise<OpenedWorkspace>;
|
|
64
|
+
/**
|
|
65
|
+
* `runCommand` over an opened workspace: see `createCommandTool`.
|
|
66
|
+
*
|
|
67
|
+
* @throws {InvalidAgentConfigError} for an invalid configuration, or on Windows.
|
|
68
|
+
*/
|
|
69
|
+
export declare function commandToolFor(workspace: OpenedWorkspace, commands: Commands): Promise<AgentTool>;
|
|
70
|
+
/**
|
|
71
|
+
* The file tools of a provider's executor, over one workspace. Every one reads through the access
|
|
72
|
+
* policy, and an edit starts only from a version the conversation read.
|
|
73
|
+
*
|
|
74
|
+
* - OpenAI: `listFiles`, `searchText`, `readFile`, and its native `apply_patch`.
|
|
75
|
+
* - Anthropic: `listFiles`, `searchText`, its native text editor, whose `view` reads, and
|
|
76
|
+
* `delete_file`.
|
|
77
|
+
*
|
|
78
|
+
* Without a `write` root they only read: `readFile` in place of the editor and the patch.
|
|
79
|
+
*/
|
|
80
|
+
export declare function fileToolsFor(provider: 'anthropic' | 'openai', workspace: OpenedWorkspace): AgentTool[];
|
|
81
|
+
export {};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { mkdir, realpath } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { defaultStateDirectory, FileRecoveryStore, } from '#src/recovery/infrastructure/fileRecoveryStore';
|
|
4
|
+
import { InvalidAgentConfigError, UnrecoverableError } from '#src/shared/domain/errors';
|
|
5
|
+
import { describeFailure } from '#src/shared/domain/providerFailure';
|
|
6
|
+
import { createCommandTool } from "./commandTool.js";
|
|
7
|
+
import { chooseWorkspace } from "./createWorkspace.js";
|
|
8
|
+
import { createApplyPatchTool, createDeleteFileTool, createTextEditorTool } from "./editFormats.js";
|
|
9
|
+
import { GitIgnoreRules } from "./gitIgnoreRules.js";
|
|
10
|
+
import { PolicyWorkspace } from "./policyWorkspace.js";
|
|
11
|
+
import { RootsAccessPolicy } from "./rootsAccessPolicy.js";
|
|
12
|
+
import { createTrackedReadFile } from "./trackedReadFile.js";
|
|
13
|
+
import { createWorkspaceTools } from "./workspaceTools.js";
|
|
14
|
+
import { WorkspaceWrites } from "./workspaceWrites.js";
|
|
15
|
+
/**
|
|
16
|
+
* Where a workspace's history is kept, as one real path: made if missing, then resolved, links
|
|
17
|
+
* included. The store and the access policy must name the same folder, or a tool could reach the
|
|
18
|
+
* history the policy means to protect. A relative path is from the working directory.
|
|
19
|
+
*
|
|
20
|
+
* @throws {UnrecoverableError} when the folder cannot be made or reached.
|
|
21
|
+
*/
|
|
22
|
+
export async function stateDirectoryOf({ stateDirectory }) {
|
|
23
|
+
const directory = resolve(stateDirectory ?? defaultStateDirectory());
|
|
24
|
+
try {
|
|
25
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
26
|
+
return await realpath(directory);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
throw new UnrecoverableError('The folder for the workspace history could not be made', {
|
|
30
|
+
cause: describeFailure(error),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The recovery store of a workspace, at its first root.
|
|
36
|
+
*
|
|
37
|
+
* @throws {InvalidAgentConfigError} when the workspace has no root.
|
|
38
|
+
* @throws {UnrecoverableError} when the store cannot be opened.
|
|
39
|
+
*/
|
|
40
|
+
export async function openStore(workspace, directory) {
|
|
41
|
+
const [first] = workspace.roots;
|
|
42
|
+
if (!first)
|
|
43
|
+
throw new InvalidAgentConfigError('A workspace needs at least one root');
|
|
44
|
+
try {
|
|
45
|
+
return await FileRecoveryStore.open({ root: resolve(first.path), directory });
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
throw new UnrecoverableError('The workspace history could not be opened', {
|
|
49
|
+
cause: describeFailure(error),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export async function openWorkspace(workspace, logger) {
|
|
54
|
+
const { roots, secrets } = workspace;
|
|
55
|
+
const stateDirectory = await stateDirectoryOf(workspace);
|
|
56
|
+
const policy = await RootsAccessPolicy.create({
|
|
57
|
+
roots,
|
|
58
|
+
...(secrets ? { secrets } : {}),
|
|
59
|
+
ignoreRules: new GitIgnoreRules(),
|
|
60
|
+
// The history holds the source a run replaced: no tool may read or write it.
|
|
61
|
+
protectedPaths: [stateDirectory],
|
|
62
|
+
});
|
|
63
|
+
const store = await openStore(workspace, stateDirectory);
|
|
64
|
+
// Real, as the policy's paths are, so a file's path can be judged by name.
|
|
65
|
+
const root = await realpath(resolve(roots[0]?.path ?? '.'));
|
|
66
|
+
const inner = await chooseWorkspace({
|
|
67
|
+
root,
|
|
68
|
+
logger,
|
|
69
|
+
// Left out before a search counts or stores a match, so no total, limit or error can tell
|
|
70
|
+
// that a secret matched. The policy still decides each path after it.
|
|
71
|
+
hidden: path => policy.hidesFromReading(join(root, path)),
|
|
72
|
+
});
|
|
73
|
+
const read = new PolicyWorkspace({ inner, policy });
|
|
74
|
+
return {
|
|
75
|
+
roots,
|
|
76
|
+
policy,
|
|
77
|
+
store,
|
|
78
|
+
read,
|
|
79
|
+
writes: new WorkspaceWrites({ policy, store }),
|
|
80
|
+
writable: roots.some(({ access }) => access === 'write'),
|
|
81
|
+
base: root,
|
|
82
|
+
covered: coveredFiles(roots, policy, logger),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Lists every file of the `write` roots that `.gitignore` does not exclude, less what the policy
|
|
87
|
+
* closes to writing by name, before anything is read. Each root is listed by its own program,
|
|
88
|
+
* opened on the first listing.
|
|
89
|
+
*/
|
|
90
|
+
function coveredFiles(roots, policy, logger) {
|
|
91
|
+
let listers;
|
|
92
|
+
const open = () => Promise.all(roots
|
|
93
|
+
.filter(({ access }) => access === 'write')
|
|
94
|
+
.map(async ({ path }) => {
|
|
95
|
+
const root = await realpath(resolve(path));
|
|
96
|
+
const workspace = await chooseWorkspace({
|
|
97
|
+
root,
|
|
98
|
+
logger,
|
|
99
|
+
hidden: relativePath => policy.closedToWriting(join(root, relativePath)),
|
|
100
|
+
});
|
|
101
|
+
return { root, workspace };
|
|
102
|
+
}));
|
|
103
|
+
return async (signal) => {
|
|
104
|
+
listers ??= open().catch((error) => {
|
|
105
|
+
// Not kept: the next command tries again.
|
|
106
|
+
listers = undefined;
|
|
107
|
+
throw error;
|
|
108
|
+
});
|
|
109
|
+
const files = new Set();
|
|
110
|
+
for (const { root, workspace } of await listers) {
|
|
111
|
+
const listed = await workspace.listFiles({ limit: Number.MAX_SAFE_INTEGER }, signal);
|
|
112
|
+
for (const path of listed.files)
|
|
113
|
+
files.add(join(root, path));
|
|
114
|
+
}
|
|
115
|
+
return [...files];
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* `runCommand` over an opened workspace: see `createCommandTool`.
|
|
120
|
+
*
|
|
121
|
+
* @throws {InvalidAgentConfigError} for an invalid configuration, or on Windows.
|
|
122
|
+
*/
|
|
123
|
+
export function commandToolFor(workspace, commands) {
|
|
124
|
+
const { policy, writes, covered, base } = workspace;
|
|
125
|
+
return createCommandTool({ commands, policy, writes, covered, base });
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The file tools of a provider's executor, over one workspace. Every one reads through the access
|
|
129
|
+
* policy, and an edit starts only from a version the conversation read.
|
|
130
|
+
*
|
|
131
|
+
* - OpenAI: `listFiles`, `searchText`, `readFile`, and its native `apply_patch`.
|
|
132
|
+
* - Anthropic: `listFiles`, `searchText`, its native text editor, whose `view` reads, and
|
|
133
|
+
* `delete_file`.
|
|
134
|
+
*
|
|
135
|
+
* Without a `write` root they only read: `readFile` in place of the editor and the patch.
|
|
136
|
+
*/
|
|
137
|
+
export function fileToolsFor(provider, workspace) {
|
|
138
|
+
const { policy, read, writes, writable } = workspace;
|
|
139
|
+
const [listFiles, searchText] = createWorkspaceTools(read).filter(tool => tool.name === 'listFiles' || tool.name === 'searchText');
|
|
140
|
+
const finding = [listFiles, searchText].filter(tool => tool !== undefined);
|
|
141
|
+
const readFile = createTrackedReadFile(policy);
|
|
142
|
+
if (!writable)
|
|
143
|
+
return [...finding, readFile];
|
|
144
|
+
if (provider === 'openai')
|
|
145
|
+
return [...finding, readFile, createApplyPatchTool(writes)];
|
|
146
|
+
return [
|
|
147
|
+
...finding,
|
|
148
|
+
createTextEditorTool({ writes, policy, workspace: read }),
|
|
149
|
+
createDeleteFileTool(writes),
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=fileTools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileTools.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/fileTools.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdir, realpath } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport type { ToolDefinition } from '#src/llm/domain/tool';\nimport type { RecoveryStore } from '#src/recovery/domain/recoveryStore';\nimport {\n\tdefaultStateDirectory,\n\tFileRecoveryStore,\n} from '#src/recovery/infrastructure/fileRecoveryStore';\nimport { InvalidAgentConfigError, UnrecoverableError } from '#src/shared/domain/errors';\nimport type { ILogger } from '#src/shared/domain/logger';\nimport { describeFailure } from '#src/shared/domain/providerFailure';\nimport type { AccessPolicy, WorkspaceRoot } from '../domain/accessPolicy.ts';\nimport type { Commands } from '../domain/commands.ts';\nimport type { AgentTool } from '../domain/preparedCall.ts';\nimport type { Workspace } from '../domain/workspace.ts';\nimport type { CoveredFiles } from './commandCheckpoint.ts';\nimport { createCommandTool } from './commandTool.ts';\nimport { chooseWorkspace } from './createWorkspace.ts';\nimport { createApplyPatchTool, createDeleteFileTool, createTextEditorTool } from './editFormats.ts';\nimport { GitIgnoreRules } from './gitIgnoreRules.ts';\nimport { PolicyWorkspace } from './policyWorkspace.ts';\nimport { RootsAccessPolicy, type SecretRules } from './rootsAccessPolicy.ts';\nimport { createTrackedReadFile } from './trackedReadFile.ts';\nimport { createWorkspaceTools } from './workspaceTools.ts';\nimport { WorkspaceWrites } from './workspaceWrites.ts';\n\n/** The folders an agent works on, and what it may do in each. */\nexport interface WorkspaceOptions {\n\t/**\n\t * Each folder `read` or `write`. The first is where a relative path starts, and the one whose\n\t * history of runs is kept; a write to another root is recorded in it too.\n\t */\n\troots: WorkspaceRoot[];\n\t/** More files to keep closed as secrets, and exact files to open. */\n\tsecrets?: SecretRules;\n\t/**\n\t * Where the history of runs is kept, relative to the working directory if not absolute.\n\t * Defaults to the platform's state directory. No tool may reach it, even inside a root.\n\t */\n\tstateDirectory?: string;\n}\n\ndeclare const fileToolBrand: unique symbol;\n\n/**\n * A tool the harness builds for files, from `createFileTools`. It goes to an agent from\n * `createLLMAgent` beside the consumer's own tools; what it does inside is the harness's.\n */\nexport type FileTool = ToolDefinition & { readonly [fileToolBrand]: true };\n\n/** What the file tools of one workspace share: one policy, one history, one record of writes. */\nexport interface OpenedWorkspace {\n\treadonly roots: WorkspaceRoot[];\n\treadonly policy: AccessPolicy;\n\treadonly store: RecoveryStore;\n\t/** The read side, with the policy's word on every path. */\n\treadonly read: Workspace;\n\treadonly writes: WorkspaceWrites;\n\treadonly writable: boolean;\n\t/** The real path of the first root. */\n\treadonly base: string;\n\t/** What a command's checkpoint captures and compares: see `CoveredFiles`. */\n\treadonly covered: CoveredFiles;\n}\n\n/**\n * Where a workspace's history is kept, as one real path: made if missing, then resolved, links\n * included. The store and the access policy must name the same folder, or a tool could reach the\n * history the policy means to protect. A relative path is from the working directory.\n *\n * @throws {UnrecoverableError} when the folder cannot be made or reached.\n */\nexport async function stateDirectoryOf({ stateDirectory }: WorkspaceOptions): Promise<string> {\n\tconst directory = resolve(stateDirectory ?? defaultStateDirectory());\n\ttry {\n\t\tawait mkdir(directory, { recursive: true, mode: 0o700 });\n\t\treturn await realpath(directory);\n\t} catch (error) {\n\t\tthrow new UnrecoverableError('The folder for the workspace history could not be made', {\n\t\t\tcause: describeFailure(error),\n\t\t});\n\t}\n}\n\n/**\n * The recovery store of a workspace, at its first root.\n *\n * @throws {InvalidAgentConfigError} when the workspace has no root.\n * @throws {UnrecoverableError} when the store cannot be opened.\n */\nexport async function openStore(\n\tworkspace: WorkspaceOptions,\n\tdirectory: string,\n): Promise<RecoveryStore> {\n\tconst [first] = workspace.roots;\n\tif (!first) throw new InvalidAgentConfigError('A workspace needs at least one root');\n\ttry {\n\t\treturn await FileRecoveryStore.open({ root: resolve(first.path), directory });\n\t} catch (error) {\n\t\tthrow new UnrecoverableError('The workspace history could not be opened', {\n\t\t\tcause: describeFailure(error),\n\t\t});\n\t}\n}\n\nexport async function openWorkspace(\n\tworkspace: WorkspaceOptions,\n\tlogger: ILogger,\n): Promise<OpenedWorkspace> {\n\tconst { roots, secrets } = workspace;\n\tconst stateDirectory = await stateDirectoryOf(workspace);\n\tconst policy = await RootsAccessPolicy.create({\n\t\troots,\n\t\t...(secrets ? { secrets } : {}),\n\t\tignoreRules: new GitIgnoreRules(),\n\t\t// The history holds the source a run replaced: no tool may read or write it.\n\t\tprotectedPaths: [stateDirectory],\n\t});\n\tconst store = await openStore(workspace, stateDirectory);\n\t// Real, as the policy's paths are, so a file's path can be judged by name.\n\tconst root = await realpath(resolve(roots[0]?.path ?? '.'));\n\tconst inner = await chooseWorkspace({\n\t\troot,\n\t\tlogger,\n\t\t// Left out before a search counts or stores a match, so no total, limit or error can tell\n\t\t// that a secret matched. The policy still decides each path after it.\n\t\thidden: path => policy.hidesFromReading(join(root, path)),\n\t});\n\tconst read = new PolicyWorkspace({ inner, policy });\n\treturn {\n\t\troots,\n\t\tpolicy,\n\t\tstore,\n\t\tread,\n\t\twrites: new WorkspaceWrites({ policy, store }),\n\t\twritable: roots.some(({ access }) => access === 'write'),\n\t\tbase: root,\n\t\tcovered: coveredFiles(roots, policy, logger),\n\t};\n}\n\n/**\n * Lists every file of the `write` roots that `.gitignore` does not exclude, less what the policy\n * closes to writing by name, before anything is read. Each root is listed by its own program,\n * opened on the first listing.\n */\nfunction coveredFiles(\n\troots: WorkspaceRoot[],\n\tpolicy: RootsAccessPolicy,\n\tlogger: ILogger,\n): CoveredFiles {\n\tlet listers: Promise<{ root: string; workspace: Workspace }[]> | undefined;\n\tconst open = () =>\n\t\tPromise.all(\n\t\t\troots\n\t\t\t\t.filter(({ access }) => access === 'write')\n\t\t\t\t.map(async ({ path }) => {\n\t\t\t\t\tconst root = await realpath(resolve(path));\n\t\t\t\t\tconst workspace = await chooseWorkspace({\n\t\t\t\t\t\troot,\n\t\t\t\t\t\tlogger,\n\t\t\t\t\t\thidden: relativePath => policy.closedToWriting(join(root, relativePath)),\n\t\t\t\t\t});\n\t\t\t\t\treturn { root, workspace };\n\t\t\t\t}),\n\t\t);\n\n\treturn async signal => {\n\t\tlisters ??= open().catch((error: unknown) => {\n\t\t\t// Not kept: the next command tries again.\n\t\t\tlisters = undefined;\n\t\t\tthrow error;\n\t\t});\n\t\tconst files = new Set<string>();\n\t\tfor (const { root, workspace } of await listers) {\n\t\t\tconst listed = await workspace.listFiles({ limit: Number.MAX_SAFE_INTEGER }, signal);\n\t\t\tfor (const path of listed.files) files.add(join(root, path));\n\t\t}\n\t\treturn [...files];\n\t};\n}\n\n/**\n * `runCommand` over an opened workspace: see `createCommandTool`.\n *\n * @throws {InvalidAgentConfigError} for an invalid configuration, or on Windows.\n */\nexport function commandToolFor(workspace: OpenedWorkspace, commands: Commands): Promise<AgentTool> {\n\tconst { policy, writes, covered, base } = workspace;\n\treturn createCommandTool({ commands, policy, writes, covered, base });\n}\n\n/**\n * The file tools of a provider's executor, over one workspace. Every one reads through the access\n * policy, and an edit starts only from a version the conversation read.\n *\n * - OpenAI: `listFiles`, `searchText`, `readFile`, and its native `apply_patch`.\n * - Anthropic: `listFiles`, `searchText`, its native text editor, whose `view` reads, and\n * `delete_file`.\n *\n * Without a `write` root they only read: `readFile` in place of the editor and the patch.\n */\nexport function fileToolsFor(\n\tprovider: 'anthropic' | 'openai',\n\tworkspace: OpenedWorkspace,\n): AgentTool[] {\n\tconst { policy, read, writes, writable } = workspace;\n\tconst [listFiles, searchText] = createWorkspaceTools(read).filter(\n\t\ttool => tool.name === 'listFiles' || tool.name === 'searchText',\n\t);\n\tconst finding: AgentTool[] = [listFiles, searchText].filter(tool => tool !== undefined);\n\tconst readFile = createTrackedReadFile(policy);\n\tif (!writable) return [...finding, readFile];\n\tif (provider === 'openai') return [...finding, readFile, createApplyPatchTool(writes)];\n\treturn [\n\t\t...finding,\n\t\tcreateTextEditorTool({ writes, policy, workspace: read }),\n\t\tcreateDeleteFileTool(writes),\n\t];\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Whether `.gitignore` excludes a path, which may not exist yet. */
|
|
2
|
+
export interface IgnoreRules {
|
|
3
|
+
/** `relative` is relative to `root` and written with `/`. */
|
|
4
|
+
isIgnored(root: string, relative: string, signal: AbortSignal): Promise<boolean>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Asks git, which knows every source of ignore rules: nested `.gitignore` files,
|
|
8
|
+
* `.git/info/exclude` and the user's global excludes. `--no-index` makes a tracked file that a
|
|
9
|
+
* rule names count as ignored too, as `RipgrepWorkspace` and `GitWorkspace` already treat it.
|
|
10
|
+
*/
|
|
11
|
+
export declare class GitIgnoreRules implements IgnoreRules {
|
|
12
|
+
private readonly repositories;
|
|
13
|
+
isIgnored(root: string, relative: string, signal: AbortSignal): Promise<boolean>;
|
|
14
|
+
private isRepository;
|
|
15
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { rmSync } from 'node:fs';
|
|
2
|
+
import { mkdtemp } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { firstLine, runProcess } from "./runProcess.js";
|
|
6
|
+
/**
|
|
7
|
+
* git's environment without the variables that point it elsewhere: `GIT_DIR` or
|
|
8
|
+
* `GIT_WORK_TREE` inherited from the host would make it answer about another repository.
|
|
9
|
+
*/
|
|
10
|
+
function gitEnvironment() {
|
|
11
|
+
return Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('GIT_')));
|
|
12
|
+
}
|
|
13
|
+
let emptyRepository;
|
|
14
|
+
/**
|
|
15
|
+
* A private, empty git directory for folders that are not repositories: `check-ignore` needs
|
|
16
|
+
* one, and with it git still reads every `.gitignore` in the folder and the user's global
|
|
17
|
+
* excludes. Created once per process and removed when the process exits.
|
|
18
|
+
*/
|
|
19
|
+
function getEmptyRepository(signal) {
|
|
20
|
+
emptyRepository ??= (async () => {
|
|
21
|
+
const directory = await mkdtemp(join(tmpdir(), 'mikode-harness-git-'));
|
|
22
|
+
process.once('exit', () => {
|
|
23
|
+
rmSync(directory, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
const { exitCode, stderr } = await runProcess('git', ['init', '--bare', '--quiet', directory], {
|
|
26
|
+
cwd: directory,
|
|
27
|
+
signal,
|
|
28
|
+
env: gitEnvironment(),
|
|
29
|
+
});
|
|
30
|
+
if (exitCode !== 0) {
|
|
31
|
+
throw new Error(`git could not prepare an ignore check: ${firstLine(stderr)}`);
|
|
32
|
+
}
|
|
33
|
+
return directory;
|
|
34
|
+
})();
|
|
35
|
+
// A failed attempt is not kept, so the next check tries again.
|
|
36
|
+
emptyRepository.catch(() => {
|
|
37
|
+
emptyRepository = undefined;
|
|
38
|
+
});
|
|
39
|
+
return emptyRepository;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Asks git, which knows every source of ignore rules: nested `.gitignore` files,
|
|
43
|
+
* `.git/info/exclude` and the user's global excludes. `--no-index` makes a tracked file that a
|
|
44
|
+
* rule names count as ignored too, as `RipgrepWorkspace` and `GitWorkspace` already treat it.
|
|
45
|
+
*/
|
|
46
|
+
export class GitIgnoreRules {
|
|
47
|
+
repositories = new Map();
|
|
48
|
+
async isIgnored(root, relative, signal) {
|
|
49
|
+
const location = (await this.isRepository(root, signal))
|
|
50
|
+
? []
|
|
51
|
+
: ['--git-dir', await getEmptyRepository(signal), '--work-tree', root];
|
|
52
|
+
const { exitCode, stderr } = await runProcess('git', [...location, 'check-ignore', '--no-index', '--quiet', '--', relative], { cwd: root, signal, env: gitEnvironment() });
|
|
53
|
+
// 0 means ignored and 1 means not; anything else is git failing to answer.
|
|
54
|
+
if (exitCode === 0)
|
|
55
|
+
return true;
|
|
56
|
+
if (exitCode === 1)
|
|
57
|
+
return false;
|
|
58
|
+
throw new Error(`git could not check whether the path is ignored: ${firstLine(stderr)}`);
|
|
59
|
+
}
|
|
60
|
+
isRepository(root, signal) {
|
|
61
|
+
let known = this.repositories.get(root);
|
|
62
|
+
if (!known) {
|
|
63
|
+
known = runProcess('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
64
|
+
cwd: root,
|
|
65
|
+
signal,
|
|
66
|
+
env: gitEnvironment(),
|
|
67
|
+
}).then(({ exitCode, stdout }) => exitCode === 0 && stdout.trim() === 'true');
|
|
68
|
+
this.repositories.set(root, known);
|
|
69
|
+
known.catch(() => this.repositories.delete(root));
|
|
70
|
+
}
|
|
71
|
+
return known;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=gitIgnoreRules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitIgnoreRules.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/gitIgnoreRules.ts"],"names":[],"mappings":"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","sourcesContent":["import { rmSync } from 'node:fs';\nimport { mkdtemp } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport { firstLine, runProcess } from './runProcess.ts';\n\n/** Whether `.gitignore` excludes a path, which may not exist yet. */\nexport interface IgnoreRules {\n\t/** `relative` is relative to `root` and written with `/`. */\n\tisIgnored(root: string, relative: string, signal: AbortSignal): Promise<boolean>;\n}\n\n/**\n * git's environment without the variables that point it elsewhere: `GIT_DIR` or\n * `GIT_WORK_TREE` inherited from the host would make it answer about another repository.\n */\nfunction gitEnvironment(): NodeJS.ProcessEnv {\n\treturn Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('GIT_')));\n}\n\nlet emptyRepository: Promise<string> | undefined;\n\n/**\n * A private, empty git directory for folders that are not repositories: `check-ignore` needs\n * one, and with it git still reads every `.gitignore` in the folder and the user's global\n * excludes. Created once per process and removed when the process exits.\n */\nfunction getEmptyRepository(signal: AbortSignal): Promise<string> {\n\temptyRepository ??= (async () => {\n\t\tconst directory = await mkdtemp(join(tmpdir(), 'mikode-harness-git-'));\n\t\tprocess.once('exit', () => {\n\t\t\trmSync(directory, { recursive: true, force: true });\n\t\t});\n\t\tconst { exitCode, stderr } = await runProcess('git', ['init', '--bare', '--quiet', directory], {\n\t\t\tcwd: directory,\n\t\t\tsignal,\n\t\t\tenv: gitEnvironment(),\n\t\t});\n\t\tif (exitCode !== 0) {\n\t\t\tthrow new Error(`git could not prepare an ignore check: ${firstLine(stderr)}`);\n\t\t}\n\t\treturn directory;\n\t})();\n\t// A failed attempt is not kept, so the next check tries again.\n\temptyRepository.catch(() => {\n\t\temptyRepository = undefined;\n\t});\n\treturn emptyRepository;\n}\n\n/**\n * Asks git, which knows every source of ignore rules: nested `.gitignore` files,\n * `.git/info/exclude` and the user's global excludes. `--no-index` makes a tracked file that a\n * rule names count as ignored too, as `RipgrepWorkspace` and `GitWorkspace` already treat it.\n */\nexport class GitIgnoreRules implements IgnoreRules {\n\tprivate readonly repositories = new Map<string, Promise<boolean>>();\n\n\tasync isIgnored(root: string, relative: string, signal: AbortSignal): Promise<boolean> {\n\t\tconst location = (await this.isRepository(root, signal))\n\t\t\t? []\n\t\t\t: ['--git-dir', await getEmptyRepository(signal), '--work-tree', root];\n\t\tconst { exitCode, stderr } = await runProcess(\n\t\t\t'git',\n\t\t\t[...location, 'check-ignore', '--no-index', '--quiet', '--', relative],\n\t\t\t{ cwd: root, signal, env: gitEnvironment() },\n\t\t);\n\t\t// 0 means ignored and 1 means not; anything else is git failing to answer.\n\t\tif (exitCode === 0) return true;\n\t\tif (exitCode === 1) return false;\n\t\tthrow new Error(`git could not check whether the path is ignored: ${firstLine(stderr)}`);\n\t}\n\n\tprivate isRepository(root: string, signal: AbortSignal): Promise<boolean> {\n\t\tlet known = this.repositories.get(root);\n\t\tif (!known) {\n\t\t\tknown = runProcess('git', ['rev-parse', '--is-inside-work-tree'], {\n\t\t\t\tcwd: root,\n\t\t\t\tsignal,\n\t\t\t\tenv: gitEnvironment(),\n\t\t\t}).then(({ exitCode, stdout }) => exitCode === 0 && stdout.trim() === 'true');\n\t\t\tthis.repositories.set(root, known);\n\t\t\tknown.catch(() => this.repositories.delete(root));\n\t\t}\n\t\treturn known;\n\t}\n}\n"]}
|
|
@@ -6,7 +6,7 @@ import { BoundedWorkspace } from './boundedWorkspace.ts';
|
|
|
6
6
|
* operation fails.
|
|
7
7
|
*/
|
|
8
8
|
export declare class GitWorkspace extends BoundedWorkspace {
|
|
9
|
-
protected
|
|
9
|
+
protected listUnignored(signal: AbortSignal): Promise<string[]>;
|
|
10
10
|
protected findMatches(pattern: string, ignoreCase: boolean, signal: AbortSignal, keep: (match: TextMatch) => void): Promise<void>;
|
|
11
11
|
private listFiles0;
|
|
12
12
|
private git;
|
|
@@ -6,7 +6,7 @@ import { firstLine, runProcess } from "./runProcess.js";
|
|
|
6
6
|
* operation fails.
|
|
7
7
|
*/
|
|
8
8
|
export class GitWorkspace extends BoundedWorkspace {
|
|
9
|
-
async
|
|
9
|
+
async listUnignored(signal) {
|
|
10
10
|
// Tracked files and new ones not yet added; then the tracked ones `.gitignore` matches
|
|
11
11
|
// anyway, such as a `.env` committed before it was ignored. ripgrep hides those, and the
|
|
12
12
|
// fallback must not show what the preferred implementation hides.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gitWorkspace.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/gitWorkspace.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAExD;;;;GAIG;AACH,MAAM,OAAO,YAAa,SAAQ,gBAAgB;IACvC,KAAK,CAAC,
|
|
1
|
+
{"version":3,"file":"gitWorkspace.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/gitWorkspace.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TextMatch } from '../domain/workspace.ts';\nimport { BoundedWorkspace } from './boundedWorkspace.ts';\nimport { firstLine, runProcess } from './runProcess.ts';\n\n/**\n * The fallback `Workspace`, on the git the repository already has. git applies `.gitignore`\n * by itself, even to an explicit path, but only inside a repository: outside one, every\n * operation fails.\n */\nexport class GitWorkspace extends BoundedWorkspace {\n\tprotected async listUnignored(signal: AbortSignal): Promise<string[]> {\n\t\t// Tracked files and new ones not yet added; then the tracked ones `.gitignore` matches\n\t\t// anyway, such as a `.env` committed before it was ignored. ripgrep hides those, and the\n\t\t// fallback must not show what the preferred implementation hides.\n\t\tconst [listed, ignored] = await Promise.all([\n\t\t\tthis.listFiles0(['--cached', '--others', '--exclude-standard'], signal),\n\t\t\tthis.listFiles0(['--cached', '--ignored', '--exclude-standard'], signal),\n\t\t]);\n\t\tconst hidden = new Set(ignored);\n\n\t\treturn listed.filter(path => !hidden.has(path));\n\t}\n\n\tprotected async findMatches(\n\t\tpattern: string,\n\t\tignoreCase: boolean,\n\t\tsignal: AbortSignal,\n\t\tkeep: (match: TextMatch) => void,\n\t): Promise<void> {\n\t\tconst { stderr, exitCode } = await this.git(\n\t\t\t[\n\t\t\t\t'grep',\n\t\t\t\t// Untracked files too, or the agent could not search a file it has just created.\n\t\t\t\t'--untracked',\n\t\t\t\t// Binary files left out, as ripgrep does.\n\t\t\t\t'-I',\n\t\t\t\t'--line-number',\n\t\t\t\t// path NUL line NUL text, so a colon in a path cannot split it.\n\t\t\t\t'-z',\n\t\t\t\t// Perl syntax, so `\\d` and `\\b` mean what they mean to ripgrep.\n\t\t\t\t'-P',\n\t\t\t\t...(ignoreCase ? ['--ignore-case'] : []),\n\t\t\t\t// `-e`, so a pattern starting with a dash is never read as an option.\n\t\t\t\t'-e',\n\t\t\t\tpattern,\n\t\t\t],\n\t\t\tsignal,\n\t\t\tline => {\n\t\t\t\tconst [path, number, ...text] = line.split('\\0');\n\t\t\t\tif (path && number) {\n\t\t\t\t\tkeep({ path, line: Number(number), text: text.join('\\0').replace(/\\r$/, '') });\n\t\t\t\t}\n\t\t\t},\n\t\t);\n\t\t// 1 means no match; anything above is a failure, such as a pattern that does not compile.\n\t\tif (exitCode > 1) {\n\t\t\tthrow new Error(`git could not search for the pattern: ${firstLine(stderr)}`);\n\t\t}\n\t}\n\n\tprivate async listFiles0(options: string[], signal: AbortSignal): Promise<string[]> {\n\t\tconst { stdout, stderr, exitCode } = await this.git(['ls-files', '-z', ...options], signal);\n\t\tif (exitCode !== 0) {\n\t\t\tthrow new Error(`git could not list the files: ${firstLine(stderr)}`);\n\t\t}\n\n\t\treturn stdout.split('\\0').filter(path => path !== '');\n\t}\n\n\tprivate git(args: string[], signal: AbortSignal, onLine?: (line: string) => void) {\n\t\t// A user's settings must not change the output this parses: colour codes would end up in\n\t\t// the text, `grep.column` adds a field, and `grep.fullName` makes paths relative to the\n\t\t// repository instead of the root.\n\t\treturn runProcess(\n\t\t\t'git',\n\t\t\t['-c', 'color.ui=never', '-c', 'grep.column=false', '-c', 'grep.fullName=false', ...args],\n\t\t\t{ cwd: this.root, signal, onLine },\n\t\t);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether changing an ignore file from `before` to `after` can make a path visible that it hid.
|
|
3
|
+
* Only adding plain rules, anywhere, narrows. Any other change can widen:
|
|
4
|
+
*
|
|
5
|
+
* - a rule removed, which a deleted file does to all of them;
|
|
6
|
+
* - a negation added, such as `!.env`;
|
|
7
|
+
* - rules reordered, because the last rule that matches a path decides.
|
|
8
|
+
*
|
|
9
|
+
* It judges the text, not every path, so it errs towards "widens": a change it cannot prove
|
|
10
|
+
* narrowing is treated as widening. `undefined` stands for a file that does not exist.
|
|
11
|
+
*/
|
|
12
|
+
export declare function widensIgnoreRules(before: Buffer | undefined, after: Buffer | undefined): boolean;
|
|
13
|
+
/** Whether a file is one git reads ignore rules from, by its name. */
|
|
14
|
+
export declare function isIgnoreFile(path: string): boolean;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A rule as git reads it: trailing spaces dropped unless a backslash escapes them. Nothing else
|
|
3
|
+
* is trimmed, so a trailing tab stays part of the pattern, as it does for git.
|
|
4
|
+
*/
|
|
5
|
+
function trimTrailingSpaces(line) {
|
|
6
|
+
let lastSpace;
|
|
7
|
+
for (let index = 0; index < line.length; index++) {
|
|
8
|
+
const character = line[index];
|
|
9
|
+
if (character === ' ') {
|
|
10
|
+
lastSpace ??= index;
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
// An escaped character, a space too, is part of the pattern.
|
|
14
|
+
if (character === '\\')
|
|
15
|
+
index++;
|
|
16
|
+
lastSpace = undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return line.slice(0, lastSpace);
|
|
20
|
+
}
|
|
21
|
+
/** The rules of an ignore file, in order, as git reads them: no blank lines, no comments. */
|
|
22
|
+
function rulesOf(content) {
|
|
23
|
+
if (!content)
|
|
24
|
+
return [];
|
|
25
|
+
return (content
|
|
26
|
+
.toString('utf8')
|
|
27
|
+
// git skips a UTF-8 byte order mark at the start of the file, and only there.
|
|
28
|
+
.replace(/^\uFEFF/, '')
|
|
29
|
+
.split(/\r?\n/)
|
|
30
|
+
.map(trimTrailingSpaces)
|
|
31
|
+
.filter(line => line !== '' && !line.startsWith('#')));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether changing an ignore file from `before` to `after` can make a path visible that it hid.
|
|
35
|
+
* Only adding plain rules, anywhere, narrows. Any other change can widen:
|
|
36
|
+
*
|
|
37
|
+
* - a rule removed, which a deleted file does to all of them;
|
|
38
|
+
* - a negation added, such as `!.env`;
|
|
39
|
+
* - rules reordered, because the last rule that matches a path decides.
|
|
40
|
+
*
|
|
41
|
+
* It judges the text, not every path, so it errs towards "widens": a change it cannot prove
|
|
42
|
+
* narrowing is treated as widening. `undefined` stands for a file that does not exist.
|
|
43
|
+
*/
|
|
44
|
+
export function widensIgnoreRules(before, after) {
|
|
45
|
+
const kept = rulesOf(before);
|
|
46
|
+
let next = 0;
|
|
47
|
+
for (const rule of rulesOf(after)) {
|
|
48
|
+
if (rule === kept[next]) {
|
|
49
|
+
next++;
|
|
50
|
+
}
|
|
51
|
+
else if (rule.startsWith('!')) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Every earlier rule must still be there, in the same order.
|
|
56
|
+
return next < kept.length;
|
|
57
|
+
}
|
|
58
|
+
/** Whether a file is one git reads ignore rules from, by its name. */
|
|
59
|
+
export function isIgnoreFile(path) {
|
|
60
|
+
return (path.split(/[\\/]/).at(-1) ?? '').toLowerCase() === '.gitignore';
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=ignoreFileChanges.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ignoreFileChanges.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/ignoreFileChanges.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,SAAS,kBAAkB,CAAC,IAAY;IACvC,IAAI,SAA6B,CAAC;IAClC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;QAClD,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9B,IAAI,SAAS,KAAK,GAAG,EAAE,CAAC;YACvB,SAAS,KAAK,KAAK,CAAC;QACrB,CAAC;aAAM,CAAC;YACP,6DAA6D;YAC7D,IAAI,SAAS,KAAK,IAAI;gBAAE,KAAK,EAAE,CAAC;YAChC,SAAS,GAAG,SAAS,CAAC;QACvB,CAAC;IACF,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;AACjC,CAAC;AAED,6FAA6F;AAC7F,SAAS,OAAO,CAAC,OAA2B;IAC3C,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,OAAO,CACN,OAAO;SACL,QAAQ,CAAC,MAAM,CAAC;QACjB,8EAA8E;SAC7E,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC;SACtB,KAAK,CAAC,OAAO,CAAC;SACd,GAAG,CAAC,kBAAkB,CAAC;SACvB,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CACtD,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAA0B,EAAE,KAAyB;IACtF,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7B,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACnC,IAAI,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,IAAI,EAAE,CAAC;QACR,CAAC;aAAM,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACjC,OAAO,IAAI,CAAC;QACb,CAAC;IACF,CAAC;IACD,6DAA6D;IAC7D,OAAO,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC;AAC3B,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,YAAY,CAAC,IAAY;IACxC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,KAAK,YAAY,CAAC;AAC1E,CAAC","sourcesContent":["/**\n * A rule as git reads it: trailing spaces dropped unless a backslash escapes them. Nothing else\n * is trimmed, so a trailing tab stays part of the pattern, as it does for git.\n */\nfunction trimTrailingSpaces(line: string): string {\n\tlet lastSpace: number | undefined;\n\tfor (let index = 0; index < line.length; index++) {\n\t\tconst character = line[index];\n\t\tif (character === ' ') {\n\t\t\tlastSpace ??= index;\n\t\t} else {\n\t\t\t// An escaped character, a space too, is part of the pattern.\n\t\t\tif (character === '\\\\') index++;\n\t\t\tlastSpace = undefined;\n\t\t}\n\t}\n\treturn line.slice(0, lastSpace);\n}\n\n/** The rules of an ignore file, in order, as git reads them: no blank lines, no comments. */\nfunction rulesOf(content: Buffer | undefined): string[] {\n\tif (!content) return [];\n\treturn (\n\t\tcontent\n\t\t\t.toString('utf8')\n\t\t\t// git skips a UTF-8 byte order mark at the start of the file, and only there.\n\t\t\t.replace(/^\\uFEFF/, '')\n\t\t\t.split(/\\r?\\n/)\n\t\t\t.map(trimTrailingSpaces)\n\t\t\t.filter(line => line !== '' && !line.startsWith('#'))\n\t);\n}\n\n/**\n * Whether changing an ignore file from `before` to `after` can make a path visible that it hid.\n * Only adding plain rules, anywhere, narrows. Any other change can widen:\n *\n * - a rule removed, which a deleted file does to all of them;\n * - a negation added, such as `!.env`;\n * - rules reordered, because the last rule that matches a path decides.\n *\n * It judges the text, not every path, so it errs towards \"widens\": a change it cannot prove\n * narrowing is treated as widening. `undefined` stands for a file that does not exist.\n */\nexport function widensIgnoreRules(before: Buffer | undefined, after: Buffer | undefined): boolean {\n\tconst kept = rulesOf(before);\n\tlet next = 0;\n\tfor (const rule of rulesOf(after)) {\n\t\tif (rule === kept[next]) {\n\t\t\tnext++;\n\t\t} else if (rule.startsWith('!')) {\n\t\t\treturn true;\n\t\t}\n\t}\n\t// Every earlier rule must still be there, in the same order.\n\treturn next < kept.length;\n}\n\n/** Whether a file is one git reads ignore rules from, by its name. */\nexport function isIgnoreFile(path: string): boolean {\n\treturn (path.split(/[\\\\/]/).at(-1) ?? '').toLowerCase() === '.gitignore';\n}\n"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { AccessPolicy } from '../domain/accessPolicy.ts';
|
|
2
|
+
import type { TextMatch, Workspace } from '../domain/workspace.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The read `Workspace` of the file tools, under their access policy.
|
|
5
|
+
*
|
|
6
|
+
* `listFiles` and `searchText` rely on the workspace beneath, which is built with the policy's
|
|
7
|
+
* `hidesFromReading` as its `hidden` filter. That filter leaves out a protected file, or a secret
|
|
8
|
+
* the host did not open, before anything is counted or stored, so no page, total, limit or error
|
|
9
|
+
* can tell that one matched. Its candidates are already free of what `.gitignore` excludes, are
|
|
10
|
+
* inside the root, and are never reached through a link: the program that lists them does not
|
|
11
|
+
* follow links, and links are not regular files. Running the whole policy on every path a
|
|
12
|
+
* listing returns would add nothing for these paths, and would cost one `git check-ignore` each.
|
|
13
|
+
*
|
|
14
|
+
* `readFile` is given a path by the model, which can be anything, so the whole policy decides it.
|
|
15
|
+
*/
|
|
16
|
+
export declare class PolicyWorkspace implements Workspace {
|
|
17
|
+
private readonly inner;
|
|
18
|
+
private readonly policy;
|
|
19
|
+
constructor({ inner, policy }: {
|
|
20
|
+
inner: Workspace;
|
|
21
|
+
policy: AccessPolicy;
|
|
22
|
+
});
|
|
23
|
+
listFiles(query: {
|
|
24
|
+
path?: string;
|
|
25
|
+
glob?: string;
|
|
26
|
+
limit: number;
|
|
27
|
+
}, signal: AbortSignal): Promise<{
|
|
28
|
+
files: string[];
|
|
29
|
+
total: number;
|
|
30
|
+
truncated: boolean;
|
|
31
|
+
}>;
|
|
32
|
+
searchText(query: {
|
|
33
|
+
pattern: string;
|
|
34
|
+
ignoreCase: boolean;
|
|
35
|
+
path?: string;
|
|
36
|
+
glob?: string;
|
|
37
|
+
limit: number;
|
|
38
|
+
}, signal: AbortSignal): Promise<{
|
|
39
|
+
matches: TextMatch[];
|
|
40
|
+
total: number;
|
|
41
|
+
truncated: boolean;
|
|
42
|
+
}>;
|
|
43
|
+
readFile(query: {
|
|
44
|
+
path: string;
|
|
45
|
+
fromLine: number;
|
|
46
|
+
lineCount: number;
|
|
47
|
+
}, signal: AbortSignal): Promise<{
|
|
48
|
+
lines: string[];
|
|
49
|
+
totalLines: number;
|
|
50
|
+
truncated: boolean;
|
|
51
|
+
}>;
|
|
52
|
+
}
|