@akira-tl/forgerelay 0.5.4 → 0.5.6
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 +24 -0
- package/capabilities/batch-execution/GUIDE.md +18 -0
- package/dist/activity/audit-store.js +15 -1
- package/dist/activity/lifecycle.js +2 -0
- package/dist/activity/query-service.js +45 -3
- package/dist/capabilities.js +9 -0
- package/dist/capability-registry.js +28 -0
- package/dist/db/migrations.js +23 -0
- package/dist/file-mutations.js +33 -2
- package/dist/mcp/server-instructions.js +3 -3
- package/dist/operations/batch/executor.js +216 -0
- package/dist/operations/batch/scheduler.js +108 -0
- package/dist/operations/batch/types.js +76 -0
- package/dist/operations/bulk-mutation.js +41 -0
- package/dist/operations/bulk-read.js +28 -0
- package/dist/operations/core-operation-executor.js +30 -0
- package/dist/operations/native-bulk-mutations.js +134 -0
- package/dist/pi-tools.js +36 -0
- package/dist/server.js +832 -493
- package/package.json +2 -2
- package/scripts/release/tag-local-ci.mjs +144 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.5.6] - 2026-08-17
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Repaired historical `bash_output_streams` SQLite schemas that had already recorded the Bash audit migration before later audit columns were added, preventing Bash completion from crashing ForgeRelay with `no such column: error` and avoiding ambiguous Host timeouts after commands had already executed.
|
|
12
|
+
- Made the background-completion retention regression wait for the process to actually finish before crossing its retention window, removing a Windows-only timing race from the release suite.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- The `release-tag-local-ci` Hook now runs the full cloud-equivalent verification surface on an isolated Node 22.19 / npm 10.9.3 checkout before allowing a stable tag push, instead of only checking a previously recorded proof whose parity suite could omit server-level tests.
|
|
17
|
+
|
|
18
|
+
## [0.5.5] - 2026-08-15
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Added native multi-target `read`, `edit`, and `delete` operations so Agents can read several files or apply one validated edit/delete intent across multiple paths in a single interaction; bulk mutations preflight every target before the first filesystem change and report mutation-phase partial failures without claiming transactional rollback.
|
|
23
|
+
- Added the `batch.execute` capability for 1–100 heterogeneous Read/Write/Edit/Rename/Delete/Bash/Capability tasks with caller-controlled concurrency from 1–10, stable input-order results, continue-on-error execution, conflict-aware scheduling, conservative Bash/serial-Capability exclusivity, and Host cancellation that never invents Activities for queued work that did not start.
|
|
24
|
+
- Added durable parent/child Activity relationships and aggregate summaries for native bulk and Batch execution, while preserving lazy child detail, compact Bash responses plus stable `outputId`, and restart-safe local audit/query behavior.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Capability definitions now declare and advertise an explicit Batch policy (`parallel`, `serial`, or `unsupported`); `hooks.check` and `code.intelligence` are parallel, `review.changes` is serial, while Host-native artifact download and recursive `batch.execute` use are unsupported inside a Batch.
|
|
29
|
+
- Core work operations now share one internal execution seam so single MCP calls and Batch children use the same path validation, Hooks, Activity lifecycle, logging, cancellation, and result semantics instead of duplicating tool handlers.
|
|
30
|
+
|
|
7
31
|
## [0.5.4] - 2026-08-15
|
|
8
32
|
|
|
9
33
|
### Fixed
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Batch execution
|
|
2
|
+
|
|
3
|
+
Use `capability` with `name="batch.execute"` when several independent ForgeRelay core operations can be completed in one Agent interaction.
|
|
4
|
+
|
|
5
|
+
## Contract
|
|
6
|
+
|
|
7
|
+
- Every batch belongs to one already-open `workspaceId`.
|
|
8
|
+
- Supply 1–100 tasks. Every task requires a unique stable `id`.
|
|
9
|
+
- Supported operations are `read`, `write`, `edit`, `rename`, `delete`, `bash.run`, and `capability.run`.
|
|
10
|
+
- Core tasks are single-target operations. Use native `read(paths)`, `edit(paths)`, or `delete(paths)` for one homogeneous operation over multiple targets instead of nesting bulk groups inside a batch.
|
|
11
|
+
- `concurrency` may be 1–10. When omitted, ForgeRelay uses `min(task count, 10)`.
|
|
12
|
+
- Independent tasks may run concurrently. Conflicting filesystem mutations are serialized automatically. `bash.run` is treated conservatively as exclusive work because a shell command may modify arbitrary workspace state.
|
|
13
|
+
- Capability definitions explicitly advertise a batch policy: `parallel`, `serial`, or `unsupported`. Parallel capabilities may run concurrently; serial capabilities run exclusively in v0.5.5; unsupported capabilities return a task-level error while preserving the failed child Activity.
|
|
14
|
+
- One task failure does not stop independent tasks. Results are returned in the same order as the input task list.
|
|
15
|
+
- Host cancellation stops launching queued tasks and is propagated to already-running tasks.
|
|
16
|
+
- Batch execution does not support nested batches, Workspace lifecycle calls, Activity query/control calls, or Bash process-control actions.
|
|
17
|
+
|
|
18
|
+
Each actual task retains its normal ForgeRelay Hooks, Activity audit, validation, and result semantics. The Batch parent is an aggregate Activity and does not execute Tool Hooks itself.
|
|
@@ -14,6 +14,15 @@ export class ActivityAuditStore {
|
|
|
14
14
|
if (existing.length > 0) {
|
|
15
15
|
throw new Error(`Activity ${input.activityId} already has audit events.`);
|
|
16
16
|
}
|
|
17
|
+
if (input.parentActivityId) {
|
|
18
|
+
const parent = this.getActivity(input.parentActivityId);
|
|
19
|
+
if (!parent) {
|
|
20
|
+
throw new Error(`Unknown parent Activity: ${input.parentActivityId}.`);
|
|
21
|
+
}
|
|
22
|
+
if (parent.turnId !== input.turnId) {
|
|
23
|
+
throw new Error(`Parent Activity ${input.parentActivityId} belongs to Host Turn ${parent.turnId}, not ${input.turnId}.`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
17
26
|
}
|
|
18
27
|
else if (existing.length === 0 || existing[0]?.event_type !== "started") {
|
|
19
28
|
throw new Error(`Activity ${input.activityId} must start before recording ${input.type}.`);
|
|
@@ -28,6 +37,7 @@ export class ActivityAuditStore {
|
|
|
28
37
|
sequence,
|
|
29
38
|
event_type,
|
|
30
39
|
turn_id,
|
|
40
|
+
parent_activity_id,
|
|
31
41
|
conversation_scope_id,
|
|
32
42
|
tool,
|
|
33
43
|
workspace_id,
|
|
@@ -40,7 +50,7 @@ export class ActivityAuditStore {
|
|
|
40
50
|
result_json,
|
|
41
51
|
error,
|
|
42
52
|
created_at
|
|
43
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(row.id, row.activity_id, row.sequence, row.event_type, row.turn_id, row.conversation_scope_id, row.tool, row.workspace_id, row.workspace_root, row.workspace_mode, row.workspace_source_root, row.workspace_branch, row.workspace_target_branch, row.request_json, row.result_json, row.error, row.created_at);
|
|
53
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(row.id, row.activity_id, row.sequence, row.event_type, row.turn_id, row.parent_activity_id, row.conversation_scope_id, row.tool, row.workspace_id, row.workspace_root, row.workspace_mode, row.workspace_source_root, row.workspace_branch, row.workspace_target_branch, row.request_json, row.result_json, row.error, row.created_at);
|
|
44
54
|
return rowToEvent(row);
|
|
45
55
|
})();
|
|
46
56
|
}
|
|
@@ -104,6 +114,7 @@ export class ActivityAuditStore {
|
|
|
104
114
|
return {
|
|
105
115
|
activityId: started.activityId,
|
|
106
116
|
turnId: started.turnId,
|
|
117
|
+
...(started.parentActivityId ? { parentActivityId: started.parentActivityId } : {}),
|
|
107
118
|
...(started.conversationScopeId ? { conversationScopeId: started.conversationScopeId } : {}),
|
|
108
119
|
tool: started.tool,
|
|
109
120
|
workspace: started.workspace,
|
|
@@ -132,6 +143,7 @@ function eventInputToRow(input, identity) {
|
|
|
132
143
|
sequence: identity.sequence,
|
|
133
144
|
event_type: input.type,
|
|
134
145
|
turn_id: input.turnId,
|
|
146
|
+
parent_activity_id: input.parentActivityId ?? null,
|
|
135
147
|
conversation_scope_id: input.conversationScopeId ?? null,
|
|
136
148
|
tool: input.tool,
|
|
137
149
|
workspace_id: input.workspace.id ?? null,
|
|
@@ -152,6 +164,7 @@ function eventInputToRow(input, identity) {
|
|
|
152
164
|
sequence: identity.sequence,
|
|
153
165
|
event_type: input.type,
|
|
154
166
|
turn_id: null,
|
|
167
|
+
parent_activity_id: null,
|
|
155
168
|
conversation_scope_id: null,
|
|
156
169
|
tool: null,
|
|
157
170
|
workspace_id: null,
|
|
@@ -182,6 +195,7 @@ function rowToEvent(row) {
|
|
|
182
195
|
...base,
|
|
183
196
|
type: "started",
|
|
184
197
|
turnId: row.turn_id,
|
|
198
|
+
...(row.parent_activity_id ? { parentActivityId: row.parent_activity_id } : {}),
|
|
185
199
|
...(row.conversation_scope_id ? { conversationScopeId: row.conversation_scope_id } : {}),
|
|
186
200
|
tool: row.tool,
|
|
187
201
|
workspace: {
|
|
@@ -53,6 +53,7 @@ export class ActivityLifecycle {
|
|
|
53
53
|
type: "started",
|
|
54
54
|
activityId,
|
|
55
55
|
turnId,
|
|
56
|
+
...(options.parentActivityId ? { parentActivityId: options.parentActivityId } : {}),
|
|
56
57
|
...(options.conversationScopeId ? { conversationScopeId: options.conversationScopeId } : {}),
|
|
57
58
|
tool: options.tool,
|
|
58
59
|
workspace: options.workspace,
|
|
@@ -61,6 +62,7 @@ export class ActivityLifecycle {
|
|
|
61
62
|
return {
|
|
62
63
|
activityId,
|
|
63
64
|
turnId,
|
|
65
|
+
...(options.parentActivityId ? { parentActivityId: options.parentActivityId } : {}),
|
|
64
66
|
...(options.conversationScopeId ? { conversationScopeId: options.conversationScopeId } : {}),
|
|
65
67
|
};
|
|
66
68
|
}
|
|
@@ -17,7 +17,7 @@ export class ActivityQueryService {
|
|
|
17
17
|
snapshot(turnId, knownRevision) {
|
|
18
18
|
this.requireTurn(turnId);
|
|
19
19
|
const revision = this.audit.turnRevision(turnId);
|
|
20
|
-
const activities = this.
|
|
20
|
+
const activities = this.summaries(turnId);
|
|
21
21
|
const state = aggregateState(activities);
|
|
22
22
|
const changed = knownRevision === undefined || knownRevision !== revision;
|
|
23
23
|
return {
|
|
@@ -34,7 +34,8 @@ export class ActivityQueryService {
|
|
|
34
34
|
if (!record || record.turnId !== turnId) {
|
|
35
35
|
throw new Error(`Unknown Activity ${activityId} in Host Turn ${turnId}.`);
|
|
36
36
|
}
|
|
37
|
-
const activity =
|
|
37
|
+
const activity = this.summaries(turnId)
|
|
38
|
+
.find((summary) => summary.activityId === activityId) ?? toSummary(record);
|
|
38
39
|
if (!activity.detailAvailable) {
|
|
39
40
|
throw new Error(`Activity ${activityId} is summary-complete and has no lazy detail.`);
|
|
40
41
|
}
|
|
@@ -69,6 +70,30 @@ export class ActivityQueryService {
|
|
|
69
70
|
...(output.finishedAt !== undefined ? { finishedAt: output.finishedAt } : {}),
|
|
70
71
|
};
|
|
71
72
|
}
|
|
73
|
+
summaries(turnId) {
|
|
74
|
+
const records = this.audit.listActivitiesByTurn(turnId);
|
|
75
|
+
const summaries = records.map(toSummary);
|
|
76
|
+
const children = new Map();
|
|
77
|
+
for (const summary of summaries) {
|
|
78
|
+
if (!summary.parentActivityId)
|
|
79
|
+
continue;
|
|
80
|
+
const aggregate = children.get(summary.parentActivityId) ?? {
|
|
81
|
+
total: 0,
|
|
82
|
+
working: 0,
|
|
83
|
+
done: 0,
|
|
84
|
+
error: 0,
|
|
85
|
+
};
|
|
86
|
+
aggregate.total += 1;
|
|
87
|
+
aggregate[summary.status] += 1;
|
|
88
|
+
children.set(summary.parentActivityId, aggregate);
|
|
89
|
+
}
|
|
90
|
+
return summaries.map((summary) => {
|
|
91
|
+
const aggregate = children.get(summary.activityId);
|
|
92
|
+
return aggregate
|
|
93
|
+
? { ...summary, detailAvailable: false, children: aggregate }
|
|
94
|
+
: summary;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
72
97
|
requireTurn(turnId) {
|
|
73
98
|
if (!this.turns.get(turnId))
|
|
74
99
|
throw new Error(`Unknown Host Turn: ${turnId}.`);
|
|
@@ -88,15 +113,18 @@ function toSummary(record) {
|
|
|
88
113
|
?? numberField(result, "wallTimeMs")
|
|
89
114
|
?? elapsedMs(record.startedAt, record.updatedAt, record.state);
|
|
90
115
|
const bashLike = record.tool === "bash" || record.tool === "exec_command" || record.tool === "bash_result";
|
|
116
|
+
const bulkGroup = arrayField(request, "paths") !== undefined &&
|
|
117
|
+
(record.tool === "read" || record.tool === "edit" || record.tool === "delete");
|
|
91
118
|
return {
|
|
92
119
|
activityId: record.activityId,
|
|
120
|
+
...(record.parentActivityId ? { parentActivityId: record.parentActivityId } : {}),
|
|
93
121
|
tool: record.tool,
|
|
94
122
|
kind: activityKind(record.tool),
|
|
95
123
|
status: activityStatus(record.state),
|
|
96
124
|
state: record.state,
|
|
97
125
|
title: activityTitle(record.tool),
|
|
98
126
|
target: activityTarget(record, request, result, structured),
|
|
99
|
-
detailAvailable: record.tool !== "rename" && record.tool !== "delete",
|
|
127
|
+
detailAvailable: !bulkGroup && record.tool !== "rename" && record.tool !== "delete" && record.tool !== "batch",
|
|
100
128
|
...(record.workspace.id ? { workspaceId: record.workspace.id } : {}),
|
|
101
129
|
...(processId !== undefined ? { processId } : {}),
|
|
102
130
|
...(outputId !== undefined ? { outputId } : {}),
|
|
@@ -124,6 +152,8 @@ function activityKind(tool) {
|
|
|
124
152
|
return "shell";
|
|
125
153
|
if (tool === "capability")
|
|
126
154
|
return "capability";
|
|
155
|
+
if (tool === "batch")
|
|
156
|
+
return "batch";
|
|
127
157
|
return "tool";
|
|
128
158
|
}
|
|
129
159
|
function activityTitle(tool) {
|
|
@@ -138,12 +168,24 @@ function activityTitle(tool) {
|
|
|
138
168
|
exec_command: "Command",
|
|
139
169
|
bash_result: "Bash result",
|
|
140
170
|
capability: "Capability",
|
|
171
|
+
batch: "Batch",
|
|
141
172
|
};
|
|
142
173
|
return titles[tool] ?? tool;
|
|
143
174
|
}
|
|
144
175
|
function activityTarget(record, request, result, structured) {
|
|
145
176
|
if (record.tool === "bash" || record.tool === "exec_command")
|
|
146
177
|
return "Shell command";
|
|
178
|
+
if (record.tool === "batch") {
|
|
179
|
+
const tasks = arrayField(request, "tasks");
|
|
180
|
+
return `${tasks?.length ?? 0} tasks`;
|
|
181
|
+
}
|
|
182
|
+
const paths = arrayField(request, "paths");
|
|
183
|
+
if (paths && paths.length > 0) {
|
|
184
|
+
if (record.tool === "read" || record.tool === "edit")
|
|
185
|
+
return `${paths.length} files`;
|
|
186
|
+
if (record.tool === "delete")
|
|
187
|
+
return `${paths.length} paths`;
|
|
188
|
+
}
|
|
147
189
|
if (record.tool === "bash_result") {
|
|
148
190
|
const processId = numberField(result, "processId") ?? numberField(request, "processId");
|
|
149
191
|
const exitCode = numberField(result, "exitCode");
|
package/dist/capabilities.js
CHANGED
|
@@ -39,6 +39,12 @@ const CAPABILITY_GUIDE_DEFINITIONS = [
|
|
|
39
39
|
description: "Read-only semantic code navigation backed by external Language servers.",
|
|
40
40
|
whenToRead: "Read before using code.intelligence or configuring Language servers.",
|
|
41
41
|
},
|
|
42
|
+
{
|
|
43
|
+
name: "batch-execution",
|
|
44
|
+
description: "One-call execution of multiple independent ForgeRelay core operations.",
|
|
45
|
+
whenToRead: "Read before using batch.execute for heterogeneous multi-operation work.",
|
|
46
|
+
enabled: (config) => config.toolMode !== "codex",
|
|
47
|
+
},
|
|
42
48
|
];
|
|
43
49
|
function capabilityGuidesDir() {
|
|
44
50
|
return fileURLToPath(new URL("../capabilities", import.meta.url));
|
|
@@ -82,6 +88,9 @@ export function buildCapabilityFingerprint(config, version, context = {}) {
|
|
|
82
88
|
"capability-guides.read",
|
|
83
89
|
"code.intelligence",
|
|
84
90
|
];
|
|
91
|
+
if (config.toolMode !== "codex") {
|
|
92
|
+
capabilities.push("batch.execute");
|
|
93
|
+
}
|
|
85
94
|
if (config.subagents) {
|
|
86
95
|
capabilities.push("subagent.profiles");
|
|
87
96
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { MAX_CODE_INTELLIGENCE_RESULT_LIMIT, } from "./lsp/code-intelligence-types.js";
|
|
3
|
+
import { batchExecuteInputSchema, } from "./operations/batch/types.js";
|
|
3
4
|
export class CapabilityError extends Error {
|
|
4
5
|
code;
|
|
5
6
|
constructor(code, message) {
|
|
@@ -25,6 +26,7 @@ export class CapabilityRegistry {
|
|
|
25
26
|
name: definition.name,
|
|
26
27
|
description: definition.description,
|
|
27
28
|
available,
|
|
29
|
+
batchPolicy: definition.batchPolicy,
|
|
28
30
|
...(!available && unavailableReason ? { unavailableReason } : {}),
|
|
29
31
|
guide: {
|
|
30
32
|
name: definition.guideName,
|
|
@@ -65,6 +67,9 @@ export class CapabilityRegistry {
|
|
|
65
67
|
if (!catalogEntry.available) {
|
|
66
68
|
throw new CapabilityError("capability_unavailable", `Capability ${name} is unavailable${catalogEntry.unavailableReason ? `: ${catalogEntry.unavailableReason}` : "."}`);
|
|
67
69
|
}
|
|
70
|
+
if (options.batch && definition.batchPolicy === "unsupported") {
|
|
71
|
+
throw new CapabilityError("capability_batch_unsupported", `Capability ${name} is not supported inside batch.execute.`);
|
|
72
|
+
}
|
|
68
73
|
if (options.nativeFile !== undefined && !definition.nativeFileArgument) {
|
|
69
74
|
throw new CapabilityError("invalid_arguments", `Capability ${name} does not accept a Host-native file value.`);
|
|
70
75
|
}
|
|
@@ -87,6 +92,9 @@ export class CapabilityRegistry {
|
|
|
87
92
|
throw new CapabilityError("execution_failed", `Capability ${name} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
88
93
|
}
|
|
89
94
|
}
|
|
95
|
+
batchPolicy(name) {
|
|
96
|
+
return this.definitions.get(name)?.batchPolicy;
|
|
97
|
+
}
|
|
90
98
|
requireDefinition(name) {
|
|
91
99
|
const definition = this.definitions.get(name);
|
|
92
100
|
if (!definition) {
|
|
@@ -105,6 +113,7 @@ export class CapabilityRegistry {
|
|
|
105
113
|
name: definition.name,
|
|
106
114
|
description: definition.description,
|
|
107
115
|
available,
|
|
116
|
+
batchPolicy: definition.batchPolicy,
|
|
108
117
|
...(!available && unavailableReason ? { unavailableReason } : {}),
|
|
109
118
|
guide: {
|
|
110
119
|
name: definition.guideName,
|
|
@@ -152,6 +161,7 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
152
161
|
description: "Validate the active ForgeRelay Hook configuration for this workspace.",
|
|
153
162
|
guideName: "lifecycle-hooks",
|
|
154
163
|
readGuideBeforeFirstUse: true,
|
|
164
|
+
batchPolicy: "parallel",
|
|
155
165
|
inputSchema: hooksCheckInput,
|
|
156
166
|
availability: () => ({ available: true }),
|
|
157
167
|
run: async (_input, context) => ({
|
|
@@ -167,6 +177,7 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
167
177
|
description: "Review accumulated workspace changes from the Git-backed review checkpoint.",
|
|
168
178
|
guideName: "artifacts-review",
|
|
169
179
|
readGuideBeforeFirstUse: true,
|
|
180
|
+
batchPolicy: "serial",
|
|
170
181
|
inputSchema: z.object({}).strict(),
|
|
171
182
|
availability: () => ({
|
|
172
183
|
available: dependencies.reviewChanges?.available ?? false,
|
|
@@ -181,6 +192,7 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
181
192
|
description: "Read semantic code information through an available Language server without changing the Workspace.",
|
|
182
193
|
guideName: "code-intelligence",
|
|
183
194
|
readGuideBeforeFirstUse: true,
|
|
195
|
+
batchPolicy: "parallel",
|
|
184
196
|
inputSchema: codeIntelligenceInput,
|
|
185
197
|
availability: () => ({
|
|
186
198
|
available: dependencies.codeIntelligence?.available ?? false,
|
|
@@ -189,12 +201,28 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
189
201
|
run: async (input, context, options) => dependencies.codeIntelligence.run(input, context, options),
|
|
190
202
|
}]
|
|
191
203
|
: []),
|
|
204
|
+
...(dependencies.batchExecute
|
|
205
|
+
? [{
|
|
206
|
+
name: "batch.execute",
|
|
207
|
+
description: "Execute multiple independent ForgeRelay core operations in one Agent interaction.",
|
|
208
|
+
guideName: "batch-execution",
|
|
209
|
+
readGuideBeforeFirstUse: true,
|
|
210
|
+
batchPolicy: "unsupported",
|
|
211
|
+
inputSchema: batchExecuteInputSchema,
|
|
212
|
+
availability: () => ({
|
|
213
|
+
available: dependencies.batchExecute?.available ?? false,
|
|
214
|
+
reason: dependencies.batchExecute?.unavailableReason,
|
|
215
|
+
}),
|
|
216
|
+
run: async (input, context, options) => dependencies.batchExecute.run(input, context, options),
|
|
217
|
+
}]
|
|
218
|
+
: []),
|
|
192
219
|
...(dependencies.downloadArtifact
|
|
193
220
|
? [{
|
|
194
221
|
name: "artifact.download",
|
|
195
222
|
description: "Save one Host-native file into a workspace-relative destination without overwriting.",
|
|
196
223
|
guideName: "artifacts-review",
|
|
197
224
|
readGuideBeforeFirstUse: true,
|
|
225
|
+
batchPolicy: "unsupported",
|
|
198
226
|
inputSchema: z.object({
|
|
199
227
|
file: z.strictObject({
|
|
200
228
|
download_url: z.string(),
|
package/dist/db/migrations.js
CHANGED
|
@@ -49,6 +49,16 @@ const migrations = [
|
|
|
49
49
|
name: "activity-host-turns",
|
|
50
50
|
up: migrateActivityHostTurns,
|
|
51
51
|
},
|
|
52
|
+
{
|
|
53
|
+
version: 11,
|
|
54
|
+
name: "activity-parent-child",
|
|
55
|
+
up: migrateActivityParentChild,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
version: 12,
|
|
59
|
+
name: "bash-output-audit-columns",
|
|
60
|
+
up: migrateBashOutputAuditColumns,
|
|
61
|
+
},
|
|
52
62
|
];
|
|
53
63
|
export function migrateDatabase(sqlite) {
|
|
54
64
|
const migrate = sqlite.transaction(() => {
|
|
@@ -316,6 +326,19 @@ function migrateActivityHostTurns(sqlite) {
|
|
|
316
326
|
on activity_host_turns(created_at desc);
|
|
317
327
|
`);
|
|
318
328
|
}
|
|
329
|
+
function migrateActivityParentChild(sqlite) {
|
|
330
|
+
sqlite.exec(`
|
|
331
|
+
alter table activity_audit_events add column parent_activity_id text;
|
|
332
|
+
|
|
333
|
+
create index if not exists activity_audit_events_parent_idx
|
|
334
|
+
on activity_audit_events(parent_activity_id, created_at);
|
|
335
|
+
`);
|
|
336
|
+
}
|
|
337
|
+
function migrateBashOutputAuditColumns(sqlite) {
|
|
338
|
+
addColumnIfMissing(sqlite, "bash_output_streams", "error", "text");
|
|
339
|
+
addColumnIfMissing(sqlite, "bash_output_streams", "returned", "integer not null default 0");
|
|
340
|
+
addColumnIfMissing(sqlite, "bash_output_streams", "completion_claimed_at", "text");
|
|
341
|
+
}
|
|
319
342
|
function addColumnIfMissing(sqlite, table, column, definition) {
|
|
320
343
|
const columns = sqlite.prepare(`pragma table_info(${table})`).all();
|
|
321
344
|
if (columns.some((existingColumn) => existingColumn.name === column))
|
package/dist/file-mutations.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { lstat, realpath, rename, rm, rmdir, unlink } from "node:fs/promises";
|
|
1
|
+
import { lstat, readdir, realpath, rename, rm, rmdir, unlink } from "node:fs/promises";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { expandHomePath, resolveCanonicalAllowedPath } from "./roots.js";
|
|
3
|
+
import { expandHomePath, isPathInsideRoot, resolveCanonicalAllowedPath } from "./roots.js";
|
|
4
4
|
export async function renamePath(input, context) {
|
|
5
5
|
const source = await resolveCanonicalAllowedPath(input.path, context.cwd, context.allowedRoots);
|
|
6
6
|
await assertNotAllowedRootItself(source, context.allowedRoots, input.path);
|
|
@@ -10,6 +10,37 @@ export async function renamePath(input, context) {
|
|
|
10
10
|
await rename(source, destination);
|
|
11
11
|
return { path: input.path, newPath: input.newPath };
|
|
12
12
|
}
|
|
13
|
+
export async function preflightDeletePaths(inputs, context) {
|
|
14
|
+
const targets = await Promise.all(inputs.map(async (input) => {
|
|
15
|
+
const path = await resolveCanonicalAllowedPath(input.path, context.cwd, context.allowedRoots);
|
|
16
|
+
await assertNotAllowedRootItself(path, context.allowedRoots, input.path);
|
|
17
|
+
const entry = await lstat(path);
|
|
18
|
+
const recursive = input.recursive ?? false;
|
|
19
|
+
if (entry.isDirectory() && !recursive) {
|
|
20
|
+
const entries = await readdir(path);
|
|
21
|
+
if (entries.length > 0) {
|
|
22
|
+
throw new Error(`Directory is non-empty: ${input.path}. Use recursive=true to delete it.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
inputPath: input.path,
|
|
27
|
+
canonicalPath: await realpath(path),
|
|
28
|
+
};
|
|
29
|
+
}));
|
|
30
|
+
for (let index = 0; index < targets.length; index += 1) {
|
|
31
|
+
const current = targets[index];
|
|
32
|
+
for (let otherIndex = index + 1; otherIndex < targets.length; otherIndex += 1) {
|
|
33
|
+
const other = targets[otherIndex];
|
|
34
|
+
if (current.canonicalPath === other.canonicalPath) {
|
|
35
|
+
throw new Error(`Bulk delete targets overlap: ${current.inputPath} and ${other.inputPath} resolve to the same path.`);
|
|
36
|
+
}
|
|
37
|
+
if (isPathInsideRoot(other.canonicalPath, current.canonicalPath) ||
|
|
38
|
+
isPathInsideRoot(current.canonicalPath, other.canonicalPath)) {
|
|
39
|
+
throw new Error(`Bulk delete targets overlap as ancestor/descendant paths: ${current.inputPath} and ${other.inputPath}.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
13
44
|
export async function deletePath(input, context) {
|
|
14
45
|
const path = await resolveCanonicalAllowedPath(input.path, context.cwd, context.allowedRoots);
|
|
15
46
|
await assertNotAllowedRootItself(path, context.allowedRoots, input.path);
|
|
@@ -24,11 +24,11 @@ export function buildToolDescriptions(config) {
|
|
|
24
24
|
? ""
|
|
25
25
|
: " Use shell commands for search and directory inspection instead of dedicated MCP search tools.";
|
|
26
26
|
return {
|
|
27
|
-
read: `Read
|
|
27
|
+
read: `Read one file or multiple files inside an open workspace or the OS temp directory. Use path for one target or paths for multiple targets; offset/limit apply to every target in a bulk read. Instruction files and advertised capability guides returned by ${toolNames.openWorkspace} are also readable when applicable.${skillCapability} Only advertised entry files and files under already-loaded advertised directories are readable outside the normal roots. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
28
28
|
write: `Create or completely overwrite a file inside an open workspace or the OS temp directory. Workspace paths may be relative; OS temp paths may be absolute. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
29
|
-
edit: `Edit one file inside an open workspace or the OS temp directory by replacing exact text blocks. Each oldText must match a unique, non-overlapping region of the original file. Workspace paths may be relative; OS temp paths may be absolute. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
29
|
+
edit: `Edit one file or multiple files inside an open workspace or the OS temp directory by replacing exact text blocks. Use path for one target or paths for multiple targets; a bulk Edit applies the same edits to every file and preflights all targets before the first mutation. Each oldText must match a unique, non-overlapping region of the original file. Workspace paths may be relative; OS temp paths may be absolute. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
30
30
|
rename: `Rename or move one file or directory inside an open workspace or the OS temp directory without overwriting an existing destination. Source and destination must both remain inside the permitted file roots. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
31
|
-
delete: `Delete one
|
|
31
|
+
delete: `Delete one path or multiple paths inside an open workspace or the OS temp directory. Use path for one target or paths for multiple targets; a bulk Delete preflights all targets before deleting anything. Non-empty directories require recursive=true. An allowed root itself cannot be deleted. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
32
32
|
applyPatch: `Apply one Codex-style patch inside an open workspace or the OS temp directory. Supports adding, overwriting, updating, deleting, and moving files. Workspace paths must remain relative; absolute paths are accepted only inside the OS temp directory. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
33
33
|
shell: `Run or manage a shell process inside an open workspace.${shellSurface} Commands execute with the local user's authority; workspace containment does not make shell execution a sandbox. For action=run, yieldTimeMs is only the feedback wait (default 10000ms; 0 returns a processId immediately) and optional timeoutMs is the independent total execution limit. action=process polls/waits for incremental output, writes input, resizes a PTY, or interrupts by processId. Keep explicit waits below the Host request deadline; use 60000ms only when supported. Completed background results may be attached to a later result for the same workspaceId. Call ${toolNames.openWorkspace} first and pass workspaceId. Expose this capability only behind strong authentication.`,
|
|
34
34
|
shellCommand: "Shell command to run with the local user's authority.",
|