@spotpatch/agent 1.2.3 → 1.3.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 +2 -1
- package/dist/index.cjs +42 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +42 -12
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Security boundaries:
|
|
|
11
11
|
- no model-controlled arbitrary shell;
|
|
12
12
|
- no browser-side API keys;
|
|
13
13
|
- bounded paths, reads, writes, Diff sizes, turns, and tool calls;
|
|
14
|
+
- rejected read paths stay unread and unmodified while the bounded Agent loop can recover through allowed discovery results;
|
|
14
15
|
- no implicit stash, reset, commit, push, publish, or deployment;
|
|
15
16
|
- review is the default apply mode.
|
|
16
17
|
|
|
@@ -22,7 +23,7 @@ Requires Node.js `>=20.19.0`.
|
|
|
22
23
|
|
|
23
24
|
业务应用应通过 [`@spotpatch/vite`](https://www.npmjs.com/package/@spotpatch/vite) 等框架适配器启用该能力。本包公开发布是为了形成可安装、可版本化的依赖图,不是独立 UI 接入入口。
|
|
24
25
|
|
|
25
|
-
安全边界包括:不向模型开放任意 Shell、不把 API Key 放入浏览器、限制路径/读写/Diff
|
|
26
|
+
安全边界包括:不向模型开放任意 Shell、不把 API Key 放入浏览器、限制路径/读写/Diff/轮次/工具调用;只读路径被拒绝时保持零读取、零修改,并允许 Agent 在既有边界内改用合法发现结果继续执行;不隐式执行 stash、reset、commit、push、发包或部署。默认应用模式必须经过审阅。
|
|
26
27
|
|
|
27
28
|
要求 Node.js `>=20.19.0`。
|
|
28
29
|
|
package/dist/index.cjs
CHANGED
|
@@ -1872,7 +1872,7 @@ var AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
1872
1872
|
}),
|
|
1873
1873
|
Object.freeze({
|
|
1874
1874
|
name: AGENT_TOOL_NAMES.readFile,
|
|
1875
|
-
description: "Read a bounded inclusive line range from one allowed UTF-8 text file.",
|
|
1875
|
+
description: "Read a bounded inclusive line range from one allowed UTF-8 text file. Choose an exact path supplied by trusted SpotPatch context or returned by list_files or search_text. A retryable TOOL_PATH_DENIED result means no file was read or changed: do not retry that path; discover an allowed path instead.",
|
|
1876
1876
|
parameters: Object.freeze({
|
|
1877
1877
|
type: "object",
|
|
1878
1878
|
properties: Object.freeze({
|
|
@@ -1926,6 +1926,11 @@ var AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
1926
1926
|
})
|
|
1927
1927
|
})
|
|
1928
1928
|
]);
|
|
1929
|
+
var AGENT_TOOL_DEFINITIONS_WITHOUT_CHECKS = Object.freeze(
|
|
1930
|
+
AGENT_TOOL_DEFINITIONS.filter(
|
|
1931
|
+
(definition) => definition.name !== AGENT_TOOL_NAMES.runCheck
|
|
1932
|
+
)
|
|
1933
|
+
);
|
|
1929
1934
|
|
|
1930
1935
|
// src/tools/tool-executor.ts
|
|
1931
1936
|
var SEARCH_READ_CONCURRENCY = 8;
|
|
@@ -2018,6 +2023,14 @@ function retryableArgumentsRejection() {
|
|
|
2018
2023
|
guidance: "No files changed. Retry once with a new tool call ID and only the declared fields and value types."
|
|
2019
2024
|
});
|
|
2020
2025
|
}
|
|
2026
|
+
function retryableReadRejection() {
|
|
2027
|
+
return Object.freeze({
|
|
2028
|
+
errorCode: import_shared15.ERROR_CODES.TOOL_PATH_DENIED,
|
|
2029
|
+
retryable: true,
|
|
2030
|
+
reason: "PATH_UNAVAILABLE",
|
|
2031
|
+
guidance: "No file was read or changed. Do not retry the same path. Use list_files or search_text, then read only an allowed path returned by that tool. Protected, external, missing, symlinked, directory, binary, and non-UTF-8 paths are unavailable."
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2021
2034
|
function createAgentToolExecutor(options) {
|
|
2022
2035
|
const cacheByTurn = /* @__PURE__ */ new Map();
|
|
2023
2036
|
const fileCatalog = createAgentFileCatalog(options.worktreeRoot);
|
|
@@ -2117,7 +2130,15 @@ function createAgentToolExecutor(options) {
|
|
|
2117
2130
|
}
|
|
2118
2131
|
case AGENT_TOOL_NAMES.readFile: {
|
|
2119
2132
|
const input = parseArguments(readFileSchema, call.arguments);
|
|
2120
|
-
|
|
2133
|
+
let file;
|
|
2134
|
+
try {
|
|
2135
|
+
file = await readTextFile(input.path);
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
if (error instanceof import_shared15.SpotPatchError && error.code === import_shared15.ERROR_CODES.TOOL_PATH_DENIED) {
|
|
2138
|
+
return retryableReadRejection();
|
|
2139
|
+
}
|
|
2140
|
+
throw error;
|
|
2141
|
+
}
|
|
2121
2142
|
const lines = file.content.split(/\r?\n/u);
|
|
2122
2143
|
const startLine = input.startLine ?? 1;
|
|
2123
2144
|
const endLine = input.endLine ?? Math.min(lines.length, startLine + 199);
|
|
@@ -2845,6 +2866,7 @@ var import_shared19 = require("@spotpatch/shared");
|
|
|
2845
2866
|
var MAX_PROJECT_CONVENTION_CHARACTERS = 3500;
|
|
2846
2867
|
var MAX_VALIDATION_CHECK_CHARACTERS = 1200;
|
|
2847
2868
|
var MINIMUM_SELECTION_CONTEXT_CHARACTERS = 1024;
|
|
2869
|
+
var VALIDATION_SYSTEM_INSTRUCTION = "- Run each relevant configured check after the final write so failures can be corrected. Do not rerun an unchanged check, and do not claim a check passed unless run_check returned a passed status.\n";
|
|
2848
2870
|
var AGENT_SYSTEM_INSTRUCTIONS = `You are editing code only inside a disposable, isolated Git worktree.
|
|
2849
2871
|
|
|
2850
2872
|
Follow these rules exactly:
|
|
@@ -2859,9 +2881,13 @@ Follow these rules exactly:
|
|
|
2859
2881
|
- Every patch must begin with 'diff --git a/<path> b/<path>', include matching '--- a/<path>' and '+++ b/<path>' headers and valid '@@' hunks. Send only the raw diff: no Markdown fences, prose, shell commands, or '*** Begin Patch' markers.
|
|
2860
2882
|
- If a write tool returns a retryable PATCH_REJECTED result, no file changed. Follow its guidance, re-read the current file, and retry once with a new tool call ID.
|
|
2861
2883
|
- If any tool returns a retryable TOOL_ARGUMENTS_INVALID result, no file changed. Retry once with a new tool call ID using only the declared fields and value types.
|
|
2884
|
+
- If read_file returns a retryable TOOL_PATH_DENIED result, no file was read or changed. Do not retry that path. Use list_files or search_text and choose an allowed path returned by the tool; never probe protected, external, generated, credential, environment, or lock files.
|
|
2862
2885
|
- Never modify credentials, environment files, lockfiles, generated output, Git metadata, or dependencies.
|
|
2863
|
-
|
|
2886
|
+
${VALIDATION_SYSTEM_INSTRUCTION.trimEnd()}
|
|
2864
2887
|
- Finish with a concise factual summary after all needed tool calls. Do not include secrets or absolute paths.`;
|
|
2888
|
+
function resolveAgentSystemInstructions(trustedFast) {
|
|
2889
|
+
return trustedFast ? AGENT_SYSTEM_INSTRUCTIONS.replace(VALIDATION_SYSTEM_INSTRUCTION, "") : AGENT_SYSTEM_INSTRUCTIONS;
|
|
2890
|
+
}
|
|
2865
2891
|
function redactedJson(value) {
|
|
2866
2892
|
return JSON.stringify(
|
|
2867
2893
|
value,
|
|
@@ -3053,7 +3079,8 @@ function composeAgentUserPrompt(annotation, maximumCharacters, context = {}) {
|
|
|
3053
3079
|
(target, index) => `Target ${String(index + 1)}:
|
|
3054
3080
|
${(0, import_shared19.redactSensitiveText)(target.instruction.trim())}`
|
|
3055
3081
|
).join("\n\n");
|
|
3056
|
-
const
|
|
3082
|
+
const trustedFastBlock = context.trustedFast ? "\n\nTrusted direct execution is enabled. Start from each target's supplied code.relativePath or source.relativePath. When an exact path is present, do not call list_files first. Read each affected file once unless a write is rejected, make the smallest exact replacement that satisfies the request, and finish immediately after the successful write. No project validation check is available in this mode." : "";
|
|
3083
|
+
const requestBlock = `${requestPrefix}${request}${trustedFastBlock}`;
|
|
3057
3084
|
const checksPrefix = "\n\nConfigured validation checks (IDs and labels only):\n<validation_checks>\n";
|
|
3058
3085
|
const checksSuffix = "\n</validation_checks>";
|
|
3059
3086
|
if (requestBlock.length + contextPrefix.length + suffix.length + MINIMUM_SELECTION_CONTEXT_CHARACTERS > maximumCharacters) {
|
|
@@ -3091,7 +3118,7 @@ function isRetryableToolFailure(result) {
|
|
|
3091
3118
|
return false;
|
|
3092
3119
|
}
|
|
3093
3120
|
const candidate = output;
|
|
3094
|
-
return candidate.retryable === true && (candidate.errorCode === import_shared20.ERROR_CODES.PATCH_REJECTED || candidate.errorCode === import_shared20.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
|
|
3121
|
+
return candidate.retryable === true && (candidate.errorCode === import_shared20.ERROR_CODES.PATCH_REJECTED || candidate.errorCode === import_shared20.ERROR_CODES.TOOL_ARGUMENTS_INVALID || candidate.errorCode === import_shared20.ERROR_CODES.TOOL_PATH_DENIED);
|
|
3095
3122
|
}
|
|
3096
3123
|
function throwIfCancelled(signal) {
|
|
3097
3124
|
if (signal.aborted) {
|
|
@@ -3156,6 +3183,8 @@ async function executeToolCalls(calls, turn, executor, callbacks, signal) {
|
|
|
3156
3183
|
return Object.freeze(results);
|
|
3157
3184
|
}
|
|
3158
3185
|
async function executeAgentChange(options) {
|
|
3186
|
+
const trustedFast = options.execution.applyMode === "trusted-auto";
|
|
3187
|
+
const activeChecks = trustedFast ? Object.freeze({}) : options.execution.checks;
|
|
3159
3188
|
const controller = new AbortController();
|
|
3160
3189
|
const unlink = linkSignal(options.signal, controller);
|
|
3161
3190
|
let jobTimedOut = false;
|
|
@@ -3187,7 +3216,7 @@ async function executeAgentChange(options) {
|
|
|
3187
3216
|
})
|
|
3188
3217
|
);
|
|
3189
3218
|
const executor = createAgentToolExecutor({
|
|
3190
|
-
checks:
|
|
3219
|
+
checks: activeChecks,
|
|
3191
3220
|
limits: options.execution.limits,
|
|
3192
3221
|
worktreeRoot: worktree.root,
|
|
3193
3222
|
onCheck(result2) {
|
|
@@ -3203,16 +3232,17 @@ async function executeAgentChange(options) {
|
|
|
3203
3232
|
provider: options.provider,
|
|
3204
3233
|
model: options.model,
|
|
3205
3234
|
credential: options.credential,
|
|
3206
|
-
instructions:
|
|
3235
|
+
instructions: resolveAgentSystemInstructions(trustedFast),
|
|
3207
3236
|
userPrompt: composeAgentUserPrompt(
|
|
3208
3237
|
options.annotation,
|
|
3209
3238
|
options.promptMaxCharacters ?? 16e3,
|
|
3210
3239
|
Object.freeze({
|
|
3211
|
-
checks:
|
|
3212
|
-
projectConventions
|
|
3240
|
+
checks: activeChecks,
|
|
3241
|
+
projectConventions,
|
|
3242
|
+
trustedFast
|
|
3213
3243
|
})
|
|
3214
3244
|
),
|
|
3215
|
-
tools: AGENT_TOOL_DEFINITIONS,
|
|
3245
|
+
tools: trustedFast ? AGENT_TOOL_DEFINITIONS_WITHOUT_CHECKS : AGENT_TOOL_DEFINITIONS,
|
|
3216
3246
|
limits: options.execution.limits,
|
|
3217
3247
|
...options.fetch === void 0 ? {} : { fetch: options.fetch }
|
|
3218
3248
|
});
|
|
@@ -3249,7 +3279,7 @@ async function executeAgentChange(options) {
|
|
|
3249
3279
|
options.callbacks?.onPhase?.(
|
|
3250
3280
|
Object.freeze({
|
|
3251
3281
|
phase: "validating",
|
|
3252
|
-
message: "Validating proposed changes."
|
|
3282
|
+
message: trustedFast ? "Preparing trusted change for direct apply." : "Validating proposed changes."
|
|
3253
3283
|
})
|
|
3254
3284
|
);
|
|
3255
3285
|
const initialChangeSet = await collectAgentChangeSet(
|
|
@@ -3258,7 +3288,7 @@ async function executeAgentChange(options) {
|
|
|
3258
3288
|
options.execution.limits,
|
|
3259
3289
|
controller.signal
|
|
3260
3290
|
);
|
|
3261
|
-
const requiredChecks = initialChangeSet.diff.length === 0 ? [] : Object.values(
|
|
3291
|
+
const requiredChecks = initialChangeSet.diff.length === 0 ? [] : Object.values(activeChecks).filter((check) => check.required);
|
|
3262
3292
|
const finalChecks = [];
|
|
3263
3293
|
let ranFinalCheck = false;
|
|
3264
3294
|
for (const check of requiredChecks) {
|