maka-agent 0.2.0-dev.47.20260922 → 0.2.0-dev.48.20260923
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/dist/runtime-host-capability-provider-command.js +25 -6
- package/dist/tui-mcp-control.js +37 -24
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/acp-executor-plugin/dist/acp-errors.js +26 -0
- package/node_modules/@maka/acp-executor-plugin/dist/acp-filesystem.js +103 -0
- package/node_modules/@maka/acp-executor-plugin/dist/acp-process.js +119 -0
- package/node_modules/@maka/acp-executor-plugin/dist/acp-projection.js +146 -0
- package/node_modules/@maka/acp-executor-plugin/dist/index.js +653 -0
- package/node_modules/@maka/acp-executor-plugin/dist/plugin.mjs +24446 -0
- package/node_modules/@maka/acp-executor-plugin/package.json +15 -0
- package/node_modules/@maka/antigravity-acp-plugin/dist/index.js +88 -0
- package/node_modules/@maka/antigravity-acp-plugin/dist/model-presentation.js +53 -0
- package/node_modules/@maka/antigravity-acp-plugin/dist/plugin.mjs +115 -0
- package/node_modules/@maka/antigravity-acp-plugin/package.json +15 -0
- package/node_modules/@maka/core/dist/executor-catalog.js +106 -0
- package/node_modules/@maka/core/dist/mcp.js +4 -1
- package/node_modules/@maka/core/dist/model-metadata.js +28 -1
- package/node_modules/@maka/core/dist/runtime-event-store.js +25 -0
- package/node_modules/@maka/core/dist/runtime-invocation.js +5 -0
- package/node_modules/@maka/core/dist/runtime-policy/credential-vault-codec.js +6 -0
- package/node_modules/@maka/core/dist/runtime-policy/policy-codec.js +21 -0
- package/node_modules/@maka/core/dist/session.js +2 -0
- package/node_modules/@maka/core/dist/settings.js +1 -0
- package/node_modules/@maka/core/dist/tool-ledger-scanner.js +365 -104
- package/node_modules/@maka/core/dist/turn-origin.js +14 -0
- package/node_modules/@maka/core/package.json +2 -0
- package/node_modules/@maka/mcp/dist/credential-coordinator.js +6 -1
- package/node_modules/@maka/mcp/dist/index.js +31 -5
- package/node_modules/@maka/mcp/dist/oauth.js +54 -6
- package/node_modules/@maka/runtime/dist/agent-run-inspect.js +1 -1
- package/node_modules/@maka/runtime/dist/conversation-copy.js +56 -1
- package/node_modules/@maka/runtime/dist/mcp-tools.js +9 -2
- package/node_modules/@maka/runtime/dist/plugin-executor-backend.js +116 -3
- package/node_modules/@maka/runtime/dist/plugin-executor-service.js +212 -16
- package/node_modules/@maka/runtime/dist/runtime-ledger-repair.js +2 -2
- package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +3 -0
- package/node_modules/@maka/runtime/dist/session-manager.js +170 -125
- package/node_modules/@maka/runtime/dist/shell-run-manager.js +3 -0
- package/node_modules/@maka/runtime/dist/stream-graph-coordinator.js +29 -4
- package/node_modules/@maka/runtime-host/dist/candidate-entry.js +8 -5
- package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
- package/node_modules/@maka/runtime-host/dist/client/reconnect-lifecycle.js +8 -1
- package/node_modules/@maka/runtime-host/dist/client/session-catalog-summary.js +6 -1
- package/node_modules/@maka/runtime-host/dist/composition-identity.js +19 -0
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +10 -2
- package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +34 -0
- package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +40 -2
- package/node_modules/@maka/runtime-host/dist/server/builtin-external-agent-plugins.js +188 -0
- package/node_modules/@maka/runtime-host/dist/server/candidate.js +12 -7
- package/node_modules/@maka/runtime-host/dist/server/client-capability-recovery.js +44 -46
- package/node_modules/@maka/runtime-host/dist/server/context-coordinator.js +2 -0
- package/node_modules/@maka/runtime-host/dist/server/execution-composition-factory.js +1 -1
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +159 -19
- package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +3 -0
- package/node_modules/@maka/runtime-host/dist/server/extension-bundle.js +4 -0
- package/node_modules/@maka/runtime-host/dist/server/host-composition-source.js +47 -0
- package/node_modules/@maka/runtime-host/dist/server/host-composition.js +1 -29
- package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +2 -9
- package/node_modules/@maka/runtime-host/dist/server/hosted-execution-recovery.js +75 -9
- package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +4 -0
- package/node_modules/@maka/runtime-host/dist/server/interaction-coordinator.js +45 -0
- package/node_modules/@maka/runtime-host/dist/server/jev-routing-model.js +157 -0
- package/node_modules/@maka/runtime-host/dist/server/listener-set.js +2 -2
- package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +4 -0
- package/node_modules/@maka/runtime-host/dist/server/plugin-platform-coordinator.js +11 -1
- package/node_modules/@maka/runtime-host/dist/server/process-lifecycle.js +1 -1
- package/node_modules/@maka/runtime-host/dist/server/process-termination-error.js +27 -0
- package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +91 -12
- package/node_modules/@maka/runtime-host/dist/server/runtime-policy-coordinator.js +2 -0
- package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +99 -12
- package/node_modules/@maka/runtime-host/dist/server/session-effect-coordinator.js +12 -6
- package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +33 -0
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +4 -0
- package/node_modules/@maka/runtime-host/dist/server/workhub-result-coordinator.js +173 -0
- package/node_modules/@maka/runtime-host/dist/server/workhub-result-runtime.js +293 -0
- package/node_modules/@maka/runtime-host/package.json +2 -0
- package/node_modules/@maka/storage/dist/agent-run-store-contract.js +8 -0
- package/node_modules/@maka/storage/dist/execution-stores.js +15 -3
- package/node_modules/@maka/storage/dist/mcp-config-store.js +91 -2
- package/node_modules/@maka/storage/dist/operational-state-store.js +15 -1
- package/node_modules/@maka/storage/dist/plan-authority.js +2 -0
- package/node_modules/@maka/storage/dist/plan-store.js +30 -0
- package/node_modules/@maka/storage/dist/runtime-event-persistence.js +1 -0
- package/node_modules/@maka/storage/dist/runtime-policy/credential-vault-document.js +4 -1
- package/node_modules/@maka/storage/dist/runtime-policy/policy-document.js +2 -0
- package/node_modules/@maka/storage/dist/session-store-values.js +16 -4
- package/node_modules/@maka/storage/dist/shell-run-authority.js +1 -0
- package/node_modules/@maka/storage/dist/shell-run-store.js +15 -0
- package/node_modules/@maka/storage/dist/sqlite-runtime-schema.js +74 -1
- package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +724 -84
- package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +16 -0
- package/package.json +1 -1
|
@@ -22,7 +22,7 @@ import { homedir } from 'node:os';
|
|
|
22
22
|
import { dirname, join, resolve } from 'node:path';
|
|
23
23
|
import { createCredentialMcpOAuthStorage, formatMcpDiagnosticText, McpClientManager, } from '@maka/mcp';
|
|
24
24
|
import { createFileCredentialStore } from '@maka/storage/credential-store';
|
|
25
|
-
import { normalizeMcpConfig } from '@maka/storage/mcp-config-store';
|
|
25
|
+
import { normalizeMcpConfig, subscribeMcpConfigFileChanges } from '@maka/storage/mcp-config-store';
|
|
26
26
|
import { connectRemoteRuntimeHost, loadOrCreateRuntimeHostClientInstanceId, remoteRuntimeHostUnavailableError, RuntimeHostPermanentReconnectError, startRuntimeHostCapabilityProviderService, } from '@maka/runtime-host/client';
|
|
27
27
|
import { INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID, RUNTIME_HOST_COMPATIBILITY_EPOCH, RUNTIME_HOST_PROTOCOL_VERSION, } from '@maka/runtime-host/protocol';
|
|
28
28
|
import { runRuntimeHostProcessLifecycle } from '@maka/runtime-host/server';
|
|
@@ -39,11 +39,14 @@ export async function runRuntimeHostCapabilityProviderCli(options) {
|
|
|
39
39
|
const credential = process.env[credentialEnv];
|
|
40
40
|
if (!credential)
|
|
41
41
|
throw new Error(`Runtime Host access credential is missing from ${credentialEnv}`);
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
const readConfig = async () => {
|
|
43
|
+
const configText = await readFile(configPath, 'utf8');
|
|
44
|
+
if (Buffer.byteLength(configText, 'utf8') > MAX_MCP_CONFIG_BYTES) {
|
|
45
|
+
throw new Error('MCP config exceeds 1 MiB');
|
|
46
|
+
}
|
|
47
|
+
return normalizeMcpConfig(JSON.parse(configText));
|
|
48
|
+
};
|
|
49
|
+
const config = await readConfig();
|
|
47
50
|
const clientInstanceId = await loadOrCreateRuntimeHostClientInstanceId(identityPath);
|
|
48
51
|
const manager = new McpClientManager({
|
|
49
52
|
clientName: 'maka-capability-provider',
|
|
@@ -55,6 +58,21 @@ export async function runRuntimeHostCapabilityProviderCli(options) {
|
|
|
55
58
|
oauthStorage: createCredentialMcpOAuthStorage(createFileCredentialStore(dirname(configPath))),
|
|
56
59
|
});
|
|
57
60
|
await manager.sync(config);
|
|
61
|
+
// Desktop and the TUI edit this file while the provider runs. One change is
|
|
62
|
+
// read and applied at a time, so an older read never lands last.
|
|
63
|
+
let following = Promise.resolve();
|
|
64
|
+
const stopFollowing = subscribeMcpConfigFileChanges(configPath, (error) => {
|
|
65
|
+
if (error) {
|
|
66
|
+
process.stderr.write(`MCP config is no longer followed: ${error.message}\n`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
following = following
|
|
70
|
+
.then(async () => manager.sync(await readConfig()))
|
|
71
|
+
.catch((failure) => {
|
|
72
|
+
const message = failure instanceof Error ? failure.message : String(failure);
|
|
73
|
+
process.stderr.write(`MCP config change was not applied: ${message}\n`);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
58
76
|
let service;
|
|
59
77
|
let publishedRevision = manager.toolSnapshot().revision;
|
|
60
78
|
const disposeChanges = manager.onChange(() => {
|
|
@@ -100,6 +118,7 @@ export async function runRuntimeHostCapabilityProviderCli(options) {
|
|
|
100
118
|
}
|
|
101
119
|
finally {
|
|
102
120
|
clearInterval(reconnectTimer);
|
|
121
|
+
stopFollowing();
|
|
103
122
|
disposeChanges();
|
|
104
123
|
await service?.close().catch(() => undefined);
|
|
105
124
|
await manager.close();
|
package/dist/tui-mcp-control.js
CHANGED
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
* under the License.
|
|
18
18
|
*/
|
|
19
19
|
import { createHash, randomUUID } from 'node:crypto';
|
|
20
|
-
import { MCP_CONFIG_VERSION,
|
|
20
|
+
import { MCP_CONFIG_VERSION, resolveMcpProtocolPreference, } from '@maka/core/mcp';
|
|
21
21
|
import { createCredentialMcpOAuthStorage, McpClientManager } from '@maka/mcp';
|
|
22
22
|
import { createFileCredentialStore } from '@maka/storage/credential-store';
|
|
23
|
-
import { AtomicFileWriteCommitUnknownError, createMcpConfigStore,
|
|
23
|
+
import { AtomicFileWriteCommitUnknownError, createMcpConfigStore, updateMcpConfiguration, McpConfigurationValidationError, McpConfigSourceError, normalizeMcpConfig, normalizeMcpImport, } from '@maka/storage/mcp-config-store';
|
|
24
24
|
import { createMcpCapabilityProvider } from './mcp-capability-provider.js';
|
|
25
25
|
import { McpCapabilityPublication } from './mcp-capability-publication.js';
|
|
26
26
|
const RUNTIME_HOST_CREDENTIAL_ENV = 'MAKA_RUNTIME_HOST_ACCESS_CREDENTIAL';
|
|
@@ -43,6 +43,7 @@ class TuiMcpControllerImpl {
|
|
|
43
43
|
#listeners = new Set();
|
|
44
44
|
#disposeManagerChange;
|
|
45
45
|
#disposeConnectionAvailability;
|
|
46
|
+
#disposeConfigChanges;
|
|
46
47
|
#initialization;
|
|
47
48
|
#availability = { kind: 'unavailable' };
|
|
48
49
|
#closed = false;
|
|
@@ -112,6 +113,17 @@ class TuiMcpControllerImpl {
|
|
|
112
113
|
this.#requestPublication();
|
|
113
114
|
}
|
|
114
115
|
});
|
|
116
|
+
// Desktop edits the same file. A change during startup waits for it
|
|
117
|
+
// rather than being dropped, since startup may have read the file first.
|
|
118
|
+
// Failing to follow leaves the TUI's own edits working, so it stays quiet
|
|
119
|
+
// rather than print over the screen.
|
|
120
|
+
this.#disposeConfigChanges = deps.configStore.subscribeChanges((error) => {
|
|
121
|
+
if (error)
|
|
122
|
+
return;
|
|
123
|
+
void this.#initialization
|
|
124
|
+
.then(() => this.#serializeAction(() => this.#followConfigChange()))
|
|
125
|
+
.catch(() => undefined);
|
|
126
|
+
});
|
|
115
127
|
this.#initialization = this.#initialize();
|
|
116
128
|
}
|
|
117
129
|
snapshot() {
|
|
@@ -176,6 +188,7 @@ class TuiMcpControllerImpl {
|
|
|
176
188
|
this.#closed = true;
|
|
177
189
|
this.#disposeManagerChange();
|
|
178
190
|
this.#disposeConnectionAvailability();
|
|
191
|
+
this.#disposeConfigChanges();
|
|
179
192
|
this.#listeners.clear();
|
|
180
193
|
this.#preparedImport = undefined;
|
|
181
194
|
const publicationClosing = this.#publication.close().catch(() => undefined);
|
|
@@ -264,44 +277,32 @@ class TuiMcpControllerImpl {
|
|
|
264
277
|
async #commitMutation(action) {
|
|
265
278
|
let committed;
|
|
266
279
|
try {
|
|
267
|
-
committed = await this.#deps.configStore
|
|
268
|
-
if (this.#closed)
|
|
280
|
+
committed = await updateMcpConfiguration(this.#deps.configStore, (current) => {
|
|
281
|
+
if (this.#closed)
|
|
269
282
|
throw new TuiMcpMutationError({ status: 'failed', reason: 'closed' });
|
|
270
|
-
}
|
|
271
283
|
const prepared = this.#prepareMutation(current, action);
|
|
272
284
|
if ('status' in prepared)
|
|
273
285
|
throw new TuiMcpMutationError(prepared);
|
|
274
|
-
|
|
286
|
+
return prepared.next;
|
|
287
|
+
}, async (serverId, previous) => {
|
|
275
288
|
try {
|
|
276
|
-
|
|
289
|
+
await this.#deps.manager.forgetServerCredentials(serverId, previous);
|
|
277
290
|
}
|
|
278
291
|
catch {
|
|
279
|
-
throw new TuiMcpMutationError({ status: 'failed', reason: 'invalid-config' });
|
|
280
|
-
}
|
|
281
|
-
try {
|
|
282
|
-
for (const [serverId, previous] of Object.entries(current.mcpServers)) {
|
|
283
|
-
if (!mcpConfigChangeRetiresCredentials(previous, next.mcpServers[serverId]))
|
|
284
|
-
continue;
|
|
285
|
-
await this.#deps.manager.forgetServerCredentials(serverId, previous);
|
|
286
|
-
if (this.#closed) {
|
|
287
|
-
throw new TuiMcpMutationError({ status: 'failed', reason: 'closed' });
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
catch (error) {
|
|
292
|
-
if (error instanceof TuiMcpMutationError)
|
|
293
|
-
throw error;
|
|
294
292
|
throw new TuiMcpMutationError({
|
|
295
293
|
status: 'failed',
|
|
296
294
|
reason: 'credential-cleanup-failed',
|
|
297
295
|
});
|
|
298
296
|
}
|
|
299
|
-
|
|
297
|
+
if (this.#closed)
|
|
298
|
+
throw new TuiMcpMutationError({ status: 'failed', reason: 'closed' });
|
|
300
299
|
});
|
|
301
300
|
}
|
|
302
301
|
catch (error) {
|
|
303
302
|
if (error instanceof TuiMcpMutationError)
|
|
304
303
|
return error.result;
|
|
304
|
+
if (error instanceof McpConfigurationValidationError)
|
|
305
|
+
return { status: 'failed', reason: 'invalid-config' };
|
|
305
306
|
if (error instanceof AtomicFileWriteCommitUnknownError) {
|
|
306
307
|
// The transform has already published, including any credential
|
|
307
308
|
// retirement. Reload its authority; never replay those effects.
|
|
@@ -336,12 +337,24 @@ class TuiMcpControllerImpl {
|
|
|
336
337
|
}
|
|
337
338
|
return { status: 'failed', reason: 'persist-failed' };
|
|
338
339
|
}
|
|
340
|
+
this.#preparedImport = undefined;
|
|
339
341
|
return (await this.#synchronizeCommittedConfig(committed)).result;
|
|
340
342
|
}
|
|
343
|
+
async #followConfigChange() {
|
|
344
|
+
if (this.#closed)
|
|
345
|
+
return;
|
|
346
|
+
if (this.#snapshot.initialization === 'error')
|
|
347
|
+
return this.#initialize();
|
|
348
|
+
const latest = await this.#deps.configStore.get();
|
|
349
|
+
// An import preview survives: its commit re-checks each server it replaces.
|
|
350
|
+
if (this.#snapshot.configuration !== 'ready' ||
|
|
351
|
+
JSON.stringify(latest) !== JSON.stringify(this.#config)) {
|
|
352
|
+
await this.#synchronizeCommittedConfig(latest);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
341
355
|
async #synchronizeCommittedConfig(committed) {
|
|
342
356
|
if (this.#closed)
|
|
343
357
|
return { result: { status: 'failed', reason: 'closed' } };
|
|
344
|
-
this.#preparedImport = undefined;
|
|
345
358
|
this.#config = cloneConfig(committed);
|
|
346
359
|
this.#updateSnapshot({ configuration: 'synchronizing' });
|
|
347
360
|
this.#refreshManagerSnapshot();
|
|
Binary file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
+
* or more contributor license agreements. See the NOTICE file
|
|
4
|
+
* distributed with this work for additional information
|
|
5
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
6
|
+
* to you under the Apache License, Version 2.0 (the
|
|
7
|
+
* "License"); you may not use this file except in compliance
|
|
8
|
+
* with the License. You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing,
|
|
13
|
+
* software distributed under the License is distributed on an
|
|
14
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
+
* KIND, either express or implied. See the License for the
|
|
16
|
+
* specific language governing permissions and limitations
|
|
17
|
+
* under the License.
|
|
18
|
+
*/
|
|
19
|
+
export class AcpRuntimeError extends Error {
|
|
20
|
+
code;
|
|
21
|
+
constructor(message, code) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.code = code;
|
|
24
|
+
this.name = 'AcpRuntimeError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
+
* or more contributor license agreements. See the NOTICE file
|
|
4
|
+
* distributed with this work for additional information
|
|
5
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
6
|
+
* to you under the Apache License, Version 2.0 (the
|
|
7
|
+
* "License"); you may not use this file except in compliance
|
|
8
|
+
* with the License. You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing,
|
|
13
|
+
* software distributed under the License is distributed on an
|
|
14
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
+
* KIND, either express or implied. See the License for the
|
|
16
|
+
* specific language governing permissions and limitations
|
|
17
|
+
* under the License.
|
|
18
|
+
*/
|
|
19
|
+
import { constants } from 'node:fs';
|
|
20
|
+
import { lstat, open, realpath, stat } from 'node:fs/promises';
|
|
21
|
+
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
22
|
+
const MAX_TEXT_FILE_BYTES = 8 * 1024 * 1024;
|
|
23
|
+
export async function readWorkspaceTextFile(cwd, path, line, limit) {
|
|
24
|
+
if (line != null && (!Number.isSafeInteger(line) || line < 1))
|
|
25
|
+
throw new Error('ACP file line must be a positive integer');
|
|
26
|
+
if (limit != null && (!Number.isSafeInteger(limit) || limit < 0))
|
|
27
|
+
throw new Error('ACP file limit must be a non-negative integer');
|
|
28
|
+
const file = await openWorkspaceFile(cwd, path, false);
|
|
29
|
+
try {
|
|
30
|
+
const info = await file.stat();
|
|
31
|
+
if (info.size > MAX_TEXT_FILE_BYTES)
|
|
32
|
+
throw new Error('ACP text file is too large');
|
|
33
|
+
const text = await file.readFile('utf8');
|
|
34
|
+
const start = line == null ? 0 : line - 1;
|
|
35
|
+
return line != null || limit != null
|
|
36
|
+
? text
|
|
37
|
+
.split('\n')
|
|
38
|
+
.slice(start, limit == null ? undefined : start + limit)
|
|
39
|
+
.join('\n')
|
|
40
|
+
: text;
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await file.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function writeWorkspaceTextFile(cwd, path, content) {
|
|
47
|
+
if (Buffer.byteLength(content) > MAX_TEXT_FILE_BYTES)
|
|
48
|
+
throw new Error('ACP text file is too large');
|
|
49
|
+
const file = await openWorkspaceFile(cwd, path, true);
|
|
50
|
+
try {
|
|
51
|
+
// Validation happens against this exact open file descriptor before any
|
|
52
|
+
// existing content is truncated, so later path swaps cannot redirect data.
|
|
53
|
+
await file.truncate(0);
|
|
54
|
+
await file.writeFile(content, 'utf8');
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
await file.close();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function openWorkspaceFile(cwd, path, forWrite) {
|
|
61
|
+
if (!isAbsolute(path))
|
|
62
|
+
throw new Error('ACP file path must be absolute');
|
|
63
|
+
const root = await realpath(cwd);
|
|
64
|
+
const candidate = resolve(path);
|
|
65
|
+
if (forWrite) {
|
|
66
|
+
const parent = await realpath(dirname(candidate));
|
|
67
|
+
assertContained(root, parent);
|
|
68
|
+
const entry = await lstat(candidate).catch((error) => {
|
|
69
|
+
if (error.code !== 'ENOENT')
|
|
70
|
+
throw error;
|
|
71
|
+
return undefined;
|
|
72
|
+
});
|
|
73
|
+
if (entry?.isSymbolicLink())
|
|
74
|
+
throw new Error('ACP file path could not be resolved');
|
|
75
|
+
}
|
|
76
|
+
const flags = forWrite
|
|
77
|
+
? constants.O_WRONLY | constants.O_CREAT | (constants.O_NOFOLLOW ?? 0)
|
|
78
|
+
: constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);
|
|
79
|
+
const file = await open(candidate, flags, 0o600);
|
|
80
|
+
try {
|
|
81
|
+
const opened = await file.stat();
|
|
82
|
+
if (!opened.isFile())
|
|
83
|
+
throw new Error('ACP file path is not a regular file');
|
|
84
|
+
// Resolve and compare after opening. If the path or an ancestor changed
|
|
85
|
+
// between lookup and open, either containment or file identity no longer
|
|
86
|
+
// matches. Operations after this point use only the verified descriptor.
|
|
87
|
+
const canonical = await realpath(candidate);
|
|
88
|
+
assertContained(root, canonical);
|
|
89
|
+
const current = await stat(canonical);
|
|
90
|
+
if (opened.dev !== current.dev || opened.ino !== current.ino)
|
|
91
|
+
throw new Error('ACP file path changed during validation');
|
|
92
|
+
return file;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
await file.close();
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function assertContained(root, candidate) {
|
|
100
|
+
const relation = relative(root, candidate);
|
|
101
|
+
if (relation === '..' || relation.startsWith('../') || isAbsolute(relation))
|
|
102
|
+
throw new Error('ACP file path leaves the workspace');
|
|
103
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
+
* or more contributor license agreements. See the NOTICE file
|
|
4
|
+
* distributed with this work for additional information
|
|
5
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
6
|
+
* to you under the Apache License, Version 2.0 (the
|
|
7
|
+
* "License"); you may not use this file except in compliance
|
|
8
|
+
* with the License. You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing,
|
|
13
|
+
* software distributed under the License is distributed on an
|
|
14
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
+
* KIND, either express or implied. See the License for the
|
|
16
|
+
* specific language governing permissions and limitations
|
|
17
|
+
* under the License.
|
|
18
|
+
*/
|
|
19
|
+
import { spawn } from 'node:child_process';
|
|
20
|
+
import { Readable, Writable } from 'node:stream';
|
|
21
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
22
|
+
import { client, ndJsonStream, } from '@agentclientprotocol/sdk';
|
|
23
|
+
import { terminateProcessTree } from '@maka/runtime/process-tree-terminator';
|
|
24
|
+
import { AcpRuntimeError } from './acp-errors.js';
|
|
25
|
+
const PROCESS_EXIT_TIMEOUT_MS = 2_000;
|
|
26
|
+
export function createAcpConnection(input) {
|
|
27
|
+
let child;
|
|
28
|
+
try {
|
|
29
|
+
child = spawn(input.executable, [...(input.args ?? [])], {
|
|
30
|
+
cwd: input.cwd,
|
|
31
|
+
env: input.env,
|
|
32
|
+
stdio: 'pipe',
|
|
33
|
+
detached: true,
|
|
34
|
+
shell: false,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new AcpRuntimeError('ACP executable is unavailable', 'acp_executable_unavailable');
|
|
39
|
+
}
|
|
40
|
+
let disposing = false;
|
|
41
|
+
let disposed = false;
|
|
42
|
+
let disposal;
|
|
43
|
+
let rejectFailure;
|
|
44
|
+
const failed = new Promise((_resolve, reject) => {
|
|
45
|
+
rejectFailure = reject;
|
|
46
|
+
});
|
|
47
|
+
void failed.catch(() => undefined);
|
|
48
|
+
const fail = () => {
|
|
49
|
+
if (!disposing)
|
|
50
|
+
rejectFailure(new Error('ACP connection failed'));
|
|
51
|
+
};
|
|
52
|
+
child.once('error', fail);
|
|
53
|
+
child.stdin.once('error', fail);
|
|
54
|
+
child.stderr.once('error', fail);
|
|
55
|
+
child.stderr.on('data', () => undefined);
|
|
56
|
+
child.once('close', fail);
|
|
57
|
+
const app = client({ name: input.clientName });
|
|
58
|
+
input.configureClient(app);
|
|
59
|
+
const connection = app.connect(ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout)));
|
|
60
|
+
void connection.closed.then(fail, fail);
|
|
61
|
+
return {
|
|
62
|
+
connection,
|
|
63
|
+
failed,
|
|
64
|
+
dispose() {
|
|
65
|
+
if (disposed)
|
|
66
|
+
return Promise.resolve();
|
|
67
|
+
if (disposal)
|
|
68
|
+
return disposal;
|
|
69
|
+
disposing = true;
|
|
70
|
+
disposal = terminate(child, connection).then(() => {
|
|
71
|
+
disposed = true;
|
|
72
|
+
}, (error) => {
|
|
73
|
+
disposal = undefined;
|
|
74
|
+
throw error;
|
|
75
|
+
});
|
|
76
|
+
return disposal;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function terminate(child, connection) {
|
|
81
|
+
const pid = child.pid;
|
|
82
|
+
const alive = () => {
|
|
83
|
+
if (!pid)
|
|
84
|
+
return false;
|
|
85
|
+
if (process.platform === 'win32')
|
|
86
|
+
return child.exitCode === null && child.signalCode === null;
|
|
87
|
+
try {
|
|
88
|
+
process.kill(-pid, 0);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
return error.code !== 'ESRCH';
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
if (pid) {
|
|
97
|
+
await terminateProcessTree({ pid, signal: 'SIGTERM', fallback: () => child.kill('SIGTERM') });
|
|
98
|
+
for (let elapsed = 0; elapsed < PROCESS_EXIT_TIMEOUT_MS && alive(); elapsed += 50)
|
|
99
|
+
await delay(50);
|
|
100
|
+
if (alive()) {
|
|
101
|
+
await terminateProcessTree({
|
|
102
|
+
pid,
|
|
103
|
+
signal: 'SIGKILL',
|
|
104
|
+
fallback: () => child.kill('SIGKILL'),
|
|
105
|
+
});
|
|
106
|
+
for (let elapsed = 0; elapsed < PROCESS_EXIT_TIMEOUT_MS && alive(); elapsed += 50)
|
|
107
|
+
await delay(50);
|
|
108
|
+
}
|
|
109
|
+
if (alive())
|
|
110
|
+
throw new Error('ACP process cleanup failed');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
connection.close();
|
|
115
|
+
child.stdin.destroy();
|
|
116
|
+
child.stdout.destroy();
|
|
117
|
+
child.stderr.destroy();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
+
* or more contributor license agreements. See the NOTICE file
|
|
4
|
+
* distributed with this work for additional information
|
|
5
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
6
|
+
* to you under the Apache License, Version 2.0 (the
|
|
7
|
+
* "License"); you may not use this file except in compliance
|
|
8
|
+
* with the License. You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing,
|
|
13
|
+
* software distributed under the License is distributed on an
|
|
14
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
+
* KIND, either express or implied. See the License for the
|
|
16
|
+
* specific language governing permissions and limitations
|
|
17
|
+
* under the License.
|
|
18
|
+
*/
|
|
19
|
+
const MAX_EVENT_TEXT = 8_192;
|
|
20
|
+
const MAX_TOOL_RESULT_DIFF = 1024 * 1024;
|
|
21
|
+
export function promptText(request) {
|
|
22
|
+
const sections = [request.text];
|
|
23
|
+
if (request.instructions)
|
|
24
|
+
sections.push(`Agent instructions:\n${request.instructions}`);
|
|
25
|
+
for (const quote of request.quotes ?? [])
|
|
26
|
+
sections.push(`Quoted context:\n${quote.text}`);
|
|
27
|
+
for (const reference of request.directoryReferences ?? [])
|
|
28
|
+
sections.push(`Project directory reference: ${reference.path}`);
|
|
29
|
+
return sections.filter(Boolean).join('\n\n');
|
|
30
|
+
}
|
|
31
|
+
export function emitText(context, type, text) {
|
|
32
|
+
const safeText = text.replaceAll('\r', '');
|
|
33
|
+
for (let offset = 0; offset < safeText.length; offset += MAX_EVENT_TEXT) {
|
|
34
|
+
const chunk = safeText.slice(offset, offset + MAX_EVENT_TEXT);
|
|
35
|
+
context.emit({ type, text: chunk });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function emitToolOutput(context, toolCallId, text) {
|
|
39
|
+
const safeText = text.replaceAll('\r', '');
|
|
40
|
+
for (let offset = 0; offset < safeText.length; offset += MAX_EVENT_TEXT) {
|
|
41
|
+
context.emit({
|
|
42
|
+
type: 'tool_output_delta',
|
|
43
|
+
toolCallId,
|
|
44
|
+
text: safeText.slice(offset, offset + MAX_EVENT_TEXT),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function projectToolResult(content, rawOutput) {
|
|
49
|
+
const diffs = content.flatMap((item) => item.type === 'diff'
|
|
50
|
+
? [
|
|
51
|
+
{
|
|
52
|
+
path: item.path,
|
|
53
|
+
diff: createWholeFileDiff(item.path, item.oldText ?? '', item.newText ?? ''),
|
|
54
|
+
},
|
|
55
|
+
]
|
|
56
|
+
: []);
|
|
57
|
+
const combinedDiff = diffs.map(({ diff }) => diff).join('');
|
|
58
|
+
if (diffs.length && diffs.length <= 64 && combinedDiff.length <= MAX_TOOL_RESULT_DIFF)
|
|
59
|
+
return {
|
|
60
|
+
kind: 'file_diff',
|
|
61
|
+
paths: diffs.map(({ path }) => path),
|
|
62
|
+
diff: combinedDiff,
|
|
63
|
+
};
|
|
64
|
+
if (diffs.length)
|
|
65
|
+
return {
|
|
66
|
+
kind: 'text',
|
|
67
|
+
text: boundedText(`${diffs.map(({ path }) => `Updated ${path}`).join('\n')}\nDiff omitted because it exceeds the executor event limit.`),
|
|
68
|
+
};
|
|
69
|
+
return { kind: 'text', text: boundedText(summarizeToolResult(content, rawOutput)) };
|
|
70
|
+
}
|
|
71
|
+
function createWholeFileDiff(path, oldText, newText) {
|
|
72
|
+
const oldFile = diffLines(oldText);
|
|
73
|
+
const newFile = diffLines(newText);
|
|
74
|
+
const oldLines = oldFile.lines;
|
|
75
|
+
const newLines = newFile.lines;
|
|
76
|
+
return ([
|
|
77
|
+
`--- a/${path}`,
|
|
78
|
+
`+++ b/${path}`,
|
|
79
|
+
`@@ -${oldLines.length ? 1 : 0},${oldLines.length} +${newLines.length ? 1 : 0},${newLines.length} @@`,
|
|
80
|
+
...diffSide(oldLines, '-', oldFile.endsWithNewline),
|
|
81
|
+
...diffSide(newLines, '+', newFile.endsWithNewline),
|
|
82
|
+
].join('\n') + '\n');
|
|
83
|
+
}
|
|
84
|
+
function diffLines(text) {
|
|
85
|
+
const normalized = text.replaceAll('\r', '');
|
|
86
|
+
if (!normalized)
|
|
87
|
+
return { lines: [], endsWithNewline: true };
|
|
88
|
+
const endsWithNewline = normalized.endsWith('\n');
|
|
89
|
+
const lines = normalized.split('\n');
|
|
90
|
+
if (endsWithNewline)
|
|
91
|
+
lines.pop();
|
|
92
|
+
return { lines, endsWithNewline };
|
|
93
|
+
}
|
|
94
|
+
function diffSide(lines, prefix, endsWithNewline) {
|
|
95
|
+
return lines.flatMap((line, index) => [
|
|
96
|
+
`${prefix}${line}`,
|
|
97
|
+
...(index === lines.length - 1 && !endsWithNewline ? ['\'] : []),
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
export function summarizeToolContent(content) {
|
|
101
|
+
return content
|
|
102
|
+
.map((item) => {
|
|
103
|
+
if (item.type === 'diff')
|
|
104
|
+
return `Updated ${item.path}`;
|
|
105
|
+
if (item.type === 'terminal')
|
|
106
|
+
return item.terminalId ? `Terminal ${item.terminalId}` : '';
|
|
107
|
+
return item.content.type === 'text' ? item.content.text : '';
|
|
108
|
+
})
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.join('\n');
|
|
111
|
+
}
|
|
112
|
+
function summarizeToolResult(content, rawOutput) {
|
|
113
|
+
const summary = summarizeToolContent(content);
|
|
114
|
+
if (summary)
|
|
115
|
+
return summary;
|
|
116
|
+
if (rawOutput === undefined)
|
|
117
|
+
return '';
|
|
118
|
+
try {
|
|
119
|
+
return JSON.stringify(rawOutput);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return 'External tool completed';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function boundedText(value) {
|
|
126
|
+
const safe = value.replace(/[\0\r]/gu, '');
|
|
127
|
+
return safe.length <= MAX_EVENT_TEXT ? safe : `${safe.slice(0, MAX_EVENT_TEXT - 1)}…`;
|
|
128
|
+
}
|
|
129
|
+
export function toolName(value) {
|
|
130
|
+
return value.replace(/[\0\r\n]/gu, '').slice(0, 256) || 'external_tool';
|
|
131
|
+
}
|
|
132
|
+
export function activityKind(kind) {
|
|
133
|
+
if (kind === 'read')
|
|
134
|
+
return 'read';
|
|
135
|
+
if (kind === 'edit' || kind === 'delete' || kind === 'move')
|
|
136
|
+
return 'edit';
|
|
137
|
+
if (kind === 'search')
|
|
138
|
+
return 'search';
|
|
139
|
+
if (kind === 'fetch')
|
|
140
|
+
return 'webfetch';
|
|
141
|
+
if (kind === 'execute')
|
|
142
|
+
return 'command';
|
|
143
|
+
if (kind === 'think')
|
|
144
|
+
return 'explore';
|
|
145
|
+
return 'tool';
|
|
146
|
+
}
|