@ai-sdk/harness-acp 0.0.0 → 1.0.1
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/LICENSE +13 -0
- package/README.md +69 -3
- package/dist/bridge/host-tool-mcp.mjs +302 -0
- package/dist/bridge/host-tool-mcp.mjs.map +1 -0
- package/dist/bridge/index.mjs +3317 -0
- package/dist/bridge/index.mjs.map +1 -0
- package/dist/bridge/package.json +12 -0
- package/dist/bridge/pnpm-lock.yaml +816 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +2089 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -10
- package/src/acp-auth.ts +254 -0
- package/src/acp-bridge-protocol.ts +13 -0
- package/src/acp-harness.ts +107 -0
- package/src/index.ts +19 -0
- package/src/v1/acp-v1-bridge-protocol.ts +151 -0
- package/src/v1/acp-v1-harness.ts +1461 -0
- package/src/v1/acp-v1-lifecycle.ts +115 -0
- package/src/v1/acp-v1-prompt.ts +122 -0
- package/src/v1/acp-v1-settings.ts +97 -0
- package/src/v1/acp-v1-skills.ts +228 -0
- package/src/v1/acp-v1-turn-start-config.ts +93 -0
- package/src/v1/bridge/acp-diagnostics.ts +83 -0
- package/src/v1/bridge/acp-stream-capture.ts +152 -0
- package/src/v1/bridge/acp-v1-bridge-environment.ts +95 -0
- package/src/v1/bridge/agent-stderr-monitor.ts +52 -0
- package/src/v1/bridge/create-emit-stream-event.ts +97 -0
- package/src/v1/bridge/host-tool-correlation.ts +640 -0
- package/src/v1/bridge/host-tool-mcp-server.ts +180 -0
- package/src/v1/bridge/host-tool-mcp.ts +185 -0
- package/src/v1/bridge/host-tool-relay-client.ts +53 -0
- package/src/v1/bridge/host-tool-relay.ts +510 -0
- package/src/v1/bridge/index.ts +714 -0
- package/src/v1/bridge/package.json +12 -0
- package/src/v1/bridge/permission-controller.ts +152 -0
- package/src/v1/bridge/permission-mode.ts +163 -0
- package/src/v1/bridge/pnpm-lock.yaml +816 -0
- package/src/v1/bridge/profile-values.ts +97 -0
- package/src/v1/bridge/protocol-configuration.ts +146 -0
- package/src/v1/bridge/recovered-session.ts +145 -0
- package/src/v1/bridge/refresh-host-tool-catalog.ts +33 -0
- package/src/v1/bridge/session-lifecycle.ts +83 -0
- package/src/v1/bridge/stream-translator.ts +901 -0
- package/src/v1/implementation.ts +308 -0
- package/src/v1/index.ts +16 -0
- package/src/version.ts +6 -0
- package/index.js +0 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { HarnessV1PermissionMode } from '@ai-sdk/harness';
|
|
2
|
+
import type { BridgeTurn } from '@ai-sdk/harness/bridge';
|
|
3
|
+
import type {
|
|
4
|
+
RequestPermissionRequest,
|
|
5
|
+
RequestPermissionResponse,
|
|
6
|
+
ToolCallUpdate,
|
|
7
|
+
} from '@agentclientprotocol/sdk';
|
|
8
|
+
|
|
9
|
+
type PendingPermission = {
|
|
10
|
+
cancel(): void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let permissionRequestCounter = 0;
|
|
14
|
+
|
|
15
|
+
export function createACPPermissionController({
|
|
16
|
+
turn,
|
|
17
|
+
sessionId,
|
|
18
|
+
permissionMode,
|
|
19
|
+
hasPermissionModeMapping,
|
|
20
|
+
emitToolCall,
|
|
21
|
+
claimHostToolPermission,
|
|
22
|
+
}: {
|
|
23
|
+
turn: BridgeTurn;
|
|
24
|
+
sessionId: string;
|
|
25
|
+
permissionMode: HarnessV1PermissionMode;
|
|
26
|
+
hasPermissionModeMapping: boolean;
|
|
27
|
+
emitToolCall: (options: { toolCall: ToolCallUpdate }) => void;
|
|
28
|
+
claimHostToolPermission: (options: { toolCall: ToolCallUpdate }) => boolean;
|
|
29
|
+
}): {
|
|
30
|
+
requestPermission(
|
|
31
|
+
request: RequestPermissionRequest,
|
|
32
|
+
): Promise<RequestPermissionResponse>;
|
|
33
|
+
cancelAll(): void;
|
|
34
|
+
} {
|
|
35
|
+
const pendingPermissions = new Map<string, PendingPermission>();
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
async requestPermission(request) {
|
|
39
|
+
if (request.sessionId !== sessionId) {
|
|
40
|
+
turn.emitWarning({
|
|
41
|
+
message:
|
|
42
|
+
`ACP permission request for session ${JSON.stringify(request.sessionId)} was cancelled ` +
|
|
43
|
+
`because the active session is ${JSON.stringify(sessionId)}.`,
|
|
44
|
+
});
|
|
45
|
+
return cancelled();
|
|
46
|
+
}
|
|
47
|
+
const allowOnce = request.options.find(
|
|
48
|
+
option => option.kind === 'allow_once',
|
|
49
|
+
);
|
|
50
|
+
const rejectOnce = request.options.find(
|
|
51
|
+
option => option.kind === 'reject_once',
|
|
52
|
+
);
|
|
53
|
+
if (allowOnce == null || rejectOnce == null) {
|
|
54
|
+
const missing = [
|
|
55
|
+
...(allowOnce == null ? ['allow_once'] : []),
|
|
56
|
+
...(rejectOnce == null ? ['reject_once'] : []),
|
|
57
|
+
];
|
|
58
|
+
turn.emitWarning({
|
|
59
|
+
message:
|
|
60
|
+
`ACP permission request for tool call ${JSON.stringify(request.toolCall.toolCallId)} ` +
|
|
61
|
+
`was cancelled because it did not advertise ${missing.join(' and ')}.`,
|
|
62
|
+
});
|
|
63
|
+
return cancelled();
|
|
64
|
+
}
|
|
65
|
+
if (claimHostToolPermission({ toolCall: request.toolCall })) {
|
|
66
|
+
return {
|
|
67
|
+
outcome: {
|
|
68
|
+
outcome: 'selected',
|
|
69
|
+
optionId: allowOnce.optionId,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (
|
|
74
|
+
!hasPermissionModeMapping &&
|
|
75
|
+
shouldAutoApprove({
|
|
76
|
+
permissionMode,
|
|
77
|
+
kind: request.toolCall.kind,
|
|
78
|
+
})
|
|
79
|
+
) {
|
|
80
|
+
return {
|
|
81
|
+
outcome: {
|
|
82
|
+
outcome: 'selected',
|
|
83
|
+
optionId: allowOnce.optionId,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const approvalId = `acp-permission-${++permissionRequestCounter}`;
|
|
89
|
+
let cancelPermission!: () => void;
|
|
90
|
+
const cancellation = new Promise<undefined>(resolve => {
|
|
91
|
+
cancelPermission = () => resolve(undefined);
|
|
92
|
+
});
|
|
93
|
+
pendingPermissions.set(approvalId, {
|
|
94
|
+
cancel: cancelPermission,
|
|
95
|
+
});
|
|
96
|
+
const approval = turn.requestToolApproval(approvalId);
|
|
97
|
+
emitToolCall({ toolCall: request.toolCall });
|
|
98
|
+
turn.emit({
|
|
99
|
+
type: 'tool-approval-request',
|
|
100
|
+
approvalId,
|
|
101
|
+
toolCallId: request.toolCall.toolCallId,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const response = await Promise.race([approval, cancellation]);
|
|
106
|
+
if (response == null) return cancelled();
|
|
107
|
+
return {
|
|
108
|
+
outcome: {
|
|
109
|
+
outcome: 'selected',
|
|
110
|
+
optionId: response.approved
|
|
111
|
+
? allowOnce.optionId
|
|
112
|
+
: rejectOnce.optionId,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
} finally {
|
|
116
|
+
pendingPermissions.delete(approvalId);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
cancelAll() {
|
|
120
|
+
for (const pending of pendingPermissions.values()) {
|
|
121
|
+
pending.cancel();
|
|
122
|
+
}
|
|
123
|
+
pendingPermissions.clear();
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function shouldAutoApprove({
|
|
129
|
+
permissionMode,
|
|
130
|
+
kind,
|
|
131
|
+
}: {
|
|
132
|
+
permissionMode: HarnessV1PermissionMode;
|
|
133
|
+
kind: ToolCallUpdate['kind'];
|
|
134
|
+
}): boolean {
|
|
135
|
+
if (permissionMode === 'allow-all') return true;
|
|
136
|
+
if (
|
|
137
|
+
kind === 'read' ||
|
|
138
|
+
kind === 'search' ||
|
|
139
|
+
kind === 'think' ||
|
|
140
|
+
kind === 'fetch'
|
|
141
|
+
) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
return (
|
|
145
|
+
permissionMode === 'allow-edits' &&
|
|
146
|
+
(kind === 'edit' || kind === 'delete' || kind === 'move')
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function cancelled(): RequestPermissionResponse {
|
|
151
|
+
return { outcome: { outcome: 'cancelled' } };
|
|
152
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { HarnessV1PermissionMode } from '@ai-sdk/harness';
|
|
2
|
+
import { HarnessBridgeCapabilityUnsupportedError } from '@ai-sdk/harness/bridge';
|
|
3
|
+
import * as acp from '@agentclientprotocol/sdk';
|
|
4
|
+
import type {
|
|
5
|
+
ACPPermissionModeMapping,
|
|
6
|
+
ACPPermissionModeTarget,
|
|
7
|
+
} from '../acp-v1-settings';
|
|
8
|
+
|
|
9
|
+
const PERMISSION_MODES = [
|
|
10
|
+
'allow-reads',
|
|
11
|
+
'allow-edits',
|
|
12
|
+
'allow-all',
|
|
13
|
+
] as const satisfies ReadonlyArray<HarnessV1PermissionMode>;
|
|
14
|
+
|
|
15
|
+
type ACPSessionConfiguration = Pick<
|
|
16
|
+
acp.NewSessionResponse,
|
|
17
|
+
'modes' | 'configOptions'
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
export async function configureACPPermissionMode({
|
|
21
|
+
agent,
|
|
22
|
+
sessionId,
|
|
23
|
+
sessionConfiguration,
|
|
24
|
+
permissionModeMapping,
|
|
25
|
+
permissionMode,
|
|
26
|
+
harnessId,
|
|
27
|
+
}: {
|
|
28
|
+
agent: acp.ClientContext;
|
|
29
|
+
sessionId: string;
|
|
30
|
+
sessionConfiguration: ACPSessionConfiguration;
|
|
31
|
+
permissionModeMapping: ACPPermissionModeMapping;
|
|
32
|
+
permissionMode: HarnessV1PermissionMode;
|
|
33
|
+
harnessId: string;
|
|
34
|
+
}): Promise<void> {
|
|
35
|
+
const target = permissionModeMapping[permissionMode];
|
|
36
|
+
if (target == null) {
|
|
37
|
+
throw unsupported({
|
|
38
|
+
harnessId,
|
|
39
|
+
message:
|
|
40
|
+
`Permission mode ${JSON.stringify(permissionMode)} is not supported by this ACP harness.` +
|
|
41
|
+
(permissionModeMapping['allow-all'] == null
|
|
42
|
+
? ''
|
|
43
|
+
: ` Use permissionMode: 'allow-all'.`),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const mappedPermissionMode of PERMISSION_MODES) {
|
|
48
|
+
const mappedTarget = permissionModeMapping[mappedPermissionMode];
|
|
49
|
+
if (mappedTarget != null) {
|
|
50
|
+
validateTarget({
|
|
51
|
+
target: mappedTarget,
|
|
52
|
+
permissionMode: mappedPermissionMode,
|
|
53
|
+
sessionConfiguration,
|
|
54
|
+
harnessId,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (target.type === 'session-mode') {
|
|
60
|
+
await agent.request(acp.methods.agent.session.setMode, {
|
|
61
|
+
sessionId,
|
|
62
|
+
modeId: target.modeId,
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
await agent.request(acp.methods.agent.session.setConfigOption, {
|
|
67
|
+
sessionId,
|
|
68
|
+
configId: target.configId,
|
|
69
|
+
value: target.value,
|
|
70
|
+
...(typeof target.value === 'boolean' ? { type: 'boolean' as const } : {}),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function validateTarget({
|
|
75
|
+
target,
|
|
76
|
+
permissionMode,
|
|
77
|
+
sessionConfiguration,
|
|
78
|
+
harnessId,
|
|
79
|
+
}: {
|
|
80
|
+
target: ACPPermissionModeTarget;
|
|
81
|
+
permissionMode: HarnessV1PermissionMode;
|
|
82
|
+
sessionConfiguration: ACPSessionConfiguration;
|
|
83
|
+
harnessId: string;
|
|
84
|
+
}): void {
|
|
85
|
+
if (target.type === 'session-mode') {
|
|
86
|
+
const availableModeIds =
|
|
87
|
+
sessionConfiguration.modes?.availableModes.map(mode => mode.id) ?? [];
|
|
88
|
+
if (!availableModeIds.includes(target.modeId)) {
|
|
89
|
+
throw unsupported({
|
|
90
|
+
harnessId,
|
|
91
|
+
message:
|
|
92
|
+
`ACP permission mapping for ${JSON.stringify(permissionMode)} requires session mode ` +
|
|
93
|
+
`${JSON.stringify(target.modeId)}, but the agent advertised ` +
|
|
94
|
+
`${formatChoices({ values: availableModeIds })}.`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const configOption = sessionConfiguration.configOptions?.find(
|
|
101
|
+
option => option.id === target.configId,
|
|
102
|
+
);
|
|
103
|
+
if (configOption == null) {
|
|
104
|
+
const availableConfigIds =
|
|
105
|
+
sessionConfiguration.configOptions?.map(option => option.id) ?? [];
|
|
106
|
+
throw unsupported({
|
|
107
|
+
harnessId,
|
|
108
|
+
message:
|
|
109
|
+
`ACP permission mapping for ${JSON.stringify(permissionMode)} requires session configuration ` +
|
|
110
|
+
`${JSON.stringify(target.configId)}, but the agent advertised ` +
|
|
111
|
+
`${formatChoices({ values: availableConfigIds })}.`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (configOption.type === 'boolean') {
|
|
115
|
+
if (typeof target.value !== 'boolean') {
|
|
116
|
+
throw unsupported({
|
|
117
|
+
harnessId,
|
|
118
|
+
message:
|
|
119
|
+
`ACP permission mapping for ${JSON.stringify(permissionMode)} assigns string value ` +
|
|
120
|
+
`${JSON.stringify(target.value)} to boolean session configuration ${JSON.stringify(target.configId)}.`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (typeof target.value !== 'string') {
|
|
126
|
+
throw unsupported({
|
|
127
|
+
harnessId,
|
|
128
|
+
message:
|
|
129
|
+
`ACP permission mapping for ${JSON.stringify(permissionMode)} assigns boolean value ` +
|
|
130
|
+
`${JSON.stringify(target.value)} to select session configuration ${JSON.stringify(target.configId)}.`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const availableValues = configOption.options.flatMap(option =>
|
|
134
|
+
'options' in option
|
|
135
|
+
? option.options.map(value => value.value)
|
|
136
|
+
: [option.value],
|
|
137
|
+
);
|
|
138
|
+
if (!availableValues.includes(target.value)) {
|
|
139
|
+
throw unsupported({
|
|
140
|
+
harnessId,
|
|
141
|
+
message:
|
|
142
|
+
`ACP permission mapping for ${JSON.stringify(permissionMode)} requires value ` +
|
|
143
|
+
`${JSON.stringify(target.value)} for session configuration ${JSON.stringify(target.configId)}, ` +
|
|
144
|
+
`but the agent advertised ${formatChoices({ values: availableValues })}.`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function formatChoices({ values }: { values: ReadonlyArray<string> }): string {
|
|
150
|
+
return values.length === 0
|
|
151
|
+
? 'no matching choices'
|
|
152
|
+
: values.map(value => JSON.stringify(value)).join(', ');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function unsupported({
|
|
156
|
+
harnessId,
|
|
157
|
+
message,
|
|
158
|
+
}: {
|
|
159
|
+
harnessId: string;
|
|
160
|
+
message: string;
|
|
161
|
+
}): HarnessBridgeCapabilityUnsupportedError {
|
|
162
|
+
return new HarnessBridgeCapabilityUnsupportedError({ harnessId, message });
|
|
163
|
+
}
|