impel-cli 0.20.2-beta.1 → 0.20.4
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 +61 -68
- package/RELEASE_NOTES.md +13 -0
- package/package.json +2 -1
- package/src/agents.js +94 -13
- package/src/apps.js +26 -2
- package/src/cli.js +2 -1
- package/src/commands/launch.js +11 -2
- package/src/commands/mcp.js +30 -8
- package/src/commands/remote.js +430 -26
- package/src/directAnswer.js +44 -0
- package/src/nativeAgentTransport.js +95 -7
- package/src/remote/aws.js +38 -0
- package/src/remote/broker.js +296 -0
- package/src/remote/checkpoint.js +248 -0
- package/src/remote/contracts.js +89 -0
- package/src/remote/state.js +13 -1
- package/src/remote/telemetry.js +29 -0
- package/src/remote/transfer.js +14 -6
- package/src/remote/workspace.js +482 -0
- package/src/selfInvocation.js +6 -2
- package/src/verbatimRelay.js +64 -0
package/README.md
CHANGED
|
@@ -182,89 +182,82 @@ impel codex exec "review this repository"
|
|
|
182
182
|
|
|
183
183
|
## Remote Fargate sessions
|
|
184
184
|
|
|
185
|
-
|
|
185
|
+
Remote operations use the tenant broker. The laptop sends its PAT only to that
|
|
186
|
+
broker; the runner receives a separate run-bound credential. Production modes
|
|
187
|
+
do not call AWS and do not require an AWS profile.
|
|
186
188
|
|
|
187
|
-
|
|
188
|
-
impel remote up --provider codex
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
The command creates a one-day, provider-scoped disposable PAT, starts the
|
|
192
|
-
TTL-bounded task, installs a concrete SSH alias, transfers Git history plus the
|
|
193
|
-
tracked and nonignored working tree, restores recognized Node/Python lockfile
|
|
194
|
-
dependencies, and uploads the disposable credential only through encrypted
|
|
195
|
-
SSH. The user's stored PAT, ignored files, SSH agent, Keychain, and environment
|
|
196
|
-
are not copied. Select variables explicitly when they are genuinely required:
|
|
197
|
-
|
|
198
|
-
```sh
|
|
199
|
-
impel remote up --provider claude --env API_HOST,FEATURE_FLAG
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
Use the remote session from a terminal:
|
|
189
|
+
The public modes are deliberately asymmetric:
|
|
203
190
|
|
|
204
191
|
```sh
|
|
205
|
-
|
|
206
|
-
impel remote
|
|
207
|
-
impel remote attach --provider claude
|
|
208
|
-
```
|
|
192
|
+
# Same Codex Desktop chat, through the documented SSH run-location handoff.
|
|
193
|
+
impel remote handoff . --provider codex --mode native --session <sessions-uuid>
|
|
209
194
|
|
|
210
|
-
|
|
195
|
+
# New provider-native branch from a closed exact checkpoint.
|
|
196
|
+
impel remote dispatch . --provider codex --mode fork --session <sessions-uuid> \
|
|
197
|
+
--checkpoint <archive> --checkpoint-manifest <manifest>
|
|
198
|
+
impel remote dispatch . --provider claude --mode fork --session <sessions-uuid> \
|
|
199
|
+
--checkpoint <archive> --checkpoint-manifest <manifest>
|
|
211
200
|
|
|
212
|
-
|
|
213
|
-
impel remote
|
|
201
|
+
# Explicit non-exact context/workspace transfer.
|
|
202
|
+
impel remote dispatch . --provider claude --mode portable --session <sessions-uuid>
|
|
214
203
|
```
|
|
215
204
|
|
|
216
|
-
|
|
217
|
-
the
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
205
|
+
`--session` is the required lowercase UUID of the Impel Sessions binding for
|
|
206
|
+
the chat. It is required for native, fork, and portable broker creates. It is
|
|
207
|
+
not the provider-native Codex thread ID or Claude session ID; fork checkpoints
|
|
208
|
+
carry that separate provider identity in their closed manifest. The CLI never
|
|
209
|
+
invents a Sessions binding or substitutes a provider thread ID for one.
|
|
210
|
+
|
|
211
|
+
Every request negotiates the broker's versioned capability response before a
|
|
212
|
+
run is created. Unsupported combinations fail locally or before provisioning;
|
|
213
|
+
the CLI never changes native/fork to portable automatically. Gateway-backed
|
|
214
|
+
Claude has no native Anthropic Remote Control path. Codex App Server supports
|
|
215
|
+
fork/portable, not a Desktop native-handoff claim.
|
|
216
|
+
|
|
217
|
+
For a native request started in Codex CLI, run `/app` first so Desktop opens the
|
|
218
|
+
same saved chat. The CLI waits for explicit confirmation and then negotiates
|
|
219
|
+
the Desktop capability. It writes a run-specific SSH host whose `ProxyCommand`
|
|
220
|
+
asks the broker for a fresh single-use connection lease on every OpenSSH
|
|
221
|
+
connection. The private Ed25519 key remains local; only its public key is sent
|
|
222
|
+
to the broker. No AWS credentials or initiating PAT enter the task.
|
|
223
|
+
|
|
224
|
+
Codex Desktop and the tunnel must remain awake and online for ChatGPT Remote.
|
|
225
|
+
For laptop-independent control, use the App Server fork/portable path and the
|
|
226
|
+
Impel viewer. Once the broker reports the run as `running`, create a fresh
|
|
227
|
+
one-time browser link without persisting its exchange secret locally:
|
|
225
228
|
|
|
226
229
|
```sh
|
|
227
|
-
impel remote
|
|
228
|
-
impel remote dispatch --provider codex --session <session-id> --fork
|
|
230
|
+
impel remote viewer <run-id>
|
|
229
231
|
```
|
|
230
232
|
|
|
231
|
-
The
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
```sh
|
|
237
|
-
impel remote handoff . --provider <codex|claude> --session <session-id>
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
Unlike `up` or `dispatch`, `handoff` launches a detached headless worker inside
|
|
241
|
-
Fargate and follows the provider's structured event stream. Inference, tools,
|
|
242
|
-
commands, and edits execute in the runner; the local app only displays the
|
|
243
|
-
stream. Normal completion stops the task, revokes its disposable PAT, and
|
|
244
|
-
removes its SSH alias. To let the task continue without keeping the stream
|
|
245
|
-
open, add `--detach`, then reconnect later:
|
|
246
|
-
|
|
247
|
-
```sh
|
|
248
|
-
impel remote follow <run-id>
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
If the viewer disconnects, the detached worker keeps running and its normal
|
|
252
|
-
session hooks continue syncing the remote transcript to `impel-sessions`. The
|
|
253
|
-
task TTL remains the fallback stop and credential-revocation boundary.
|
|
254
|
-
|
|
255
|
-
Remote vendor commands run in dangerous/bypass mode as the unprivileged
|
|
256
|
-
`agent` user; the disposable Fargate task is the isolation boundary. Existing
|
|
257
|
-
Impel lifecycle hooks continue mirroring remote transcripts to
|
|
258
|
-
`impel-sessions`. Stop the task and revoke its credential when finished:
|
|
233
|
+
The exchange secret is carried only in the URL fragment and the viewer removes
|
|
234
|
+
it immediately after exchanging it for a run-bound HttpOnly cookie. Handback is
|
|
235
|
+
also explicit: select **This computer** in Desktop,
|
|
236
|
+
wait for the remote child to exit, then run:
|
|
259
237
|
|
|
260
238
|
```sh
|
|
261
|
-
impel remote
|
|
239
|
+
impel remote handback <run-id> --yes
|
|
262
240
|
```
|
|
263
241
|
|
|
264
|
-
The
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
242
|
+
The broker stops and revokes only after both the provider/SSH close signal and
|
|
243
|
+
the confirmation. A lost tunnel receives reconnect grace and is not treated as
|
|
244
|
+
successful handback. `--abandon` requests forced safe cleanup; it never starts
|
|
245
|
+
a local writer.
|
|
246
|
+
|
|
247
|
+
Brokered fork/portable runs capture an immutable workspace artifact that keeps
|
|
248
|
+
Git history and the exact staged, unstaged, deleted, untracked, executable,
|
|
249
|
+
rename, binary, and intent-to-add state distinct. Conflict, submodule, LFS,
|
|
250
|
+
unsafe-link, case-collision, and size-limit cases fail before upload. Exact
|
|
251
|
+
provider checkpoints must be closed, versioned, quiesced, and hash-verified.
|
|
252
|
+
After create assigns the run ID, the CLI rehashes the isolated staging archive,
|
|
253
|
+
adds only a canonical run-bound `manifest.json`, and uploads the closed result;
|
|
254
|
+
mirrored open session files are never labeled exact.
|
|
255
|
+
|
|
256
|
+
The former one-shot worker remains only as a short-lived rollback through
|
|
257
|
+
`--mode portable-legacy` plus `IMPEL_REMOTE_ENABLE_PORTABLE_LEGACY=1`. It is
|
|
258
|
+
never selected implicitly and is scheduled for removal on 2026-10-31 together
|
|
259
|
+
with direct AWS, PAT upload, and SSH file-tail behavior. See
|
|
260
|
+
[RELEASE_NOTES.md](RELEASE_NOTES.md).
|
|
268
261
|
|
|
269
262
|
Changing the selected tenant does not install, remove, or rewrite any other
|
|
270
263
|
tenant. It changes only the default used by CLI launches and selected-tenant
|
package/RELEASE_NOTES.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Release notes
|
|
2
|
+
|
|
3
|
+
## Native remote migration
|
|
4
|
+
|
|
5
|
+
The former `impel remote handoff` implementation started a one-shot headless
|
|
6
|
+
continuation and did not transfer a live provider session. The unqualified
|
|
7
|
+
spelling no longer invokes that worker.
|
|
8
|
+
|
|
9
|
+
Until **2026-10-31**, operators may invoke the rollback implementation only by
|
|
10
|
+
setting `IMPEL_REMOTE_ENABLE_PORTABLE_LEGACY=1` and passing
|
|
11
|
+
`--mode portable-legacy`. It is never selected implicitly. The flag, worker,
|
|
12
|
+
direct AWS/SSM path, PAT upload, SSH file tail, and its invocation counter are
|
|
13
|
+
scheduled for removal together after the native/broker acceptance gates pass.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "impel-cli",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.4",
|
|
4
4
|
"description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"./extension": "./src/extension/index.js"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
|
+
"RELEASE_NOTES.md",
|
|
14
15
|
"bin",
|
|
15
16
|
"docs/experimental-managed-cursor.md",
|
|
16
17
|
"src",
|
package/src/agents.js
CHANGED
|
@@ -19,7 +19,23 @@ import {
|
|
|
19
19
|
IMPEL_NATIVE_AGENT_MCP_TARGET,
|
|
20
20
|
impelNativeAgentMcpInvocation,
|
|
21
21
|
} from "./selfInvocation.js";
|
|
22
|
+
import {
|
|
23
|
+
adapterAnswerFaithfulCompletionGuidance,
|
|
24
|
+
adapterAnswerVerbatimCompletionGuidance,
|
|
25
|
+
claudeAnswerFaithfulCompletionGuidance,
|
|
26
|
+
claudeAnswerVerbatimCompletionGuidance,
|
|
27
|
+
usesDirectAnswer,
|
|
28
|
+
} from "./directAnswer.js";
|
|
22
29
|
import { normalizeTenantId } from "./tenants.js";
|
|
30
|
+
import {
|
|
31
|
+
adapterCallerSpawnGuidance,
|
|
32
|
+
adapterFaithfulCompletionGuidance,
|
|
33
|
+
adapterVerbatimCompletionGuidance,
|
|
34
|
+
claudeFaithfulCompletionGuidance,
|
|
35
|
+
claudeVerbatimCompletionGuidance,
|
|
36
|
+
customAgentVerbatimDescriptionLead,
|
|
37
|
+
usesVerbatimRelay,
|
|
38
|
+
} from "./verbatimRelay.js";
|
|
23
39
|
import { renameWithWindowsRetry } from "./windowsFs.js";
|
|
24
40
|
|
|
25
41
|
export const AGENT_SYNC_TTL_MS = 6 * 60 * 60 * 1000;
|
|
@@ -28,11 +44,13 @@ export const MANAGED_AGENT_MANIFEST = ".manifest.json";
|
|
|
28
44
|
export const NATIVE_AGENT_LIST_TOOL = "impel_specialists-list_native_agents";
|
|
29
45
|
export const NATIVE_AGENT_START_TOOL = "impel_specialists-start_native_agent_run";
|
|
30
46
|
export const NATIVE_AGENT_READ_TOOL = "impel_specialists-read_native_agent_run";
|
|
47
|
+
export const NATIVE_AGENT_UPSTREAM_ANSWER_TOOL = "impel_specialists-answer_native_agent";
|
|
48
|
+
export const NATIVE_AGENT_ANSWER_TOOL = "answer_native_agent";
|
|
31
49
|
export const NATIVE_AGENT_RUN_TOOL = "run_native_agent";
|
|
32
50
|
export const NATIVE_AGENT_RESUME_TOOL = "resume_native_agent_run";
|
|
33
51
|
export const NATIVE_AGENT_RECOVER_TOOL = "recover_native_agent_runs";
|
|
34
52
|
export const MANAGED_AGENT_MCP_SERVER = "impel_agent";
|
|
35
|
-
export const MANAGED_AGENT_MANIFEST_VERSION =
|
|
53
|
+
export const MANAGED_AGENT_MANIFEST_VERSION = 7;
|
|
36
54
|
|
|
37
55
|
const NATIVE_AGENT_TOOL_NAMES = [
|
|
38
56
|
NATIVE_AGENT_RUN_TOOL,
|
|
@@ -40,6 +58,9 @@ const NATIVE_AGENT_TOOL_NAMES = [
|
|
|
40
58
|
NATIVE_AGENT_RECOVER_TOOL,
|
|
41
59
|
];
|
|
42
60
|
const NATIVE_AGENT_RECOVERY_TOOL_NAMES = [NATIVE_AGENT_RESUME_TOOL, NATIVE_AGENT_RECOVER_TOOL];
|
|
61
|
+
function nativeAgentToolNames(agent) {
|
|
62
|
+
return usesDirectAnswer(agent) ? [NATIVE_AGENT_ANSWER_TOOL] : NATIVE_AGENT_TOOL_NAMES;
|
|
63
|
+
}
|
|
43
64
|
const MAX_RETIRED_AGENT_BINDINGS = 50;
|
|
44
65
|
const MAX_NATIVE_AGENT_STATE_BYTES = 512 * 1024;
|
|
45
66
|
const TERMINAL_NATIVE_AGENT_STATUSES = new Set(["succeeded", "failed", "cancelled", "canceled"]);
|
|
@@ -140,6 +161,12 @@ export function normalizeNativeAgentCatalog(payload, expectedTenantId) {
|
|
|
140
161
|
throw new Error(`native-agent catalog returned duplicate binding ${binding}`);
|
|
141
162
|
}
|
|
142
163
|
seenBindings.add(binding);
|
|
164
|
+
if (agent.verbatimRelay !== undefined && typeof agent.verbatimRelay !== "boolean") {
|
|
165
|
+
throw new Error("native-agent catalog returned an invalid verbatimRelay");
|
|
166
|
+
}
|
|
167
|
+
if (agent.directAnswer !== undefined && typeof agent.directAnswer !== "boolean") {
|
|
168
|
+
throw new Error("native-agent catalog returned an invalid directAnswer");
|
|
169
|
+
}
|
|
143
170
|
return {
|
|
144
171
|
agentId,
|
|
145
172
|
title: boundedString(agent.title, "title", { max: 160 }),
|
|
@@ -150,6 +177,8 @@ export function normalizeNativeAgentCatalog(payload, expectedTenantId) {
|
|
|
150
177
|
exclusions: stringList(agent.exclusions, "exclusions"),
|
|
151
178
|
requiredContext: stringList(agent.requiredContext, "requiredContext"),
|
|
152
179
|
sideEffects: enumString(agent.sideEffects, "sideEffects", ["read-only", "writes"]),
|
|
180
|
+
...(agent.verbatimRelay === true ? { verbatimRelay: true } : {}),
|
|
181
|
+
...(agent.directAnswer === true ? { directAnswer: true } : {}),
|
|
153
182
|
};
|
|
154
183
|
});
|
|
155
184
|
return { orgId: tenantId, agents };
|
|
@@ -559,14 +588,35 @@ function claudeAdapterInstructions(tenantId, agent) {
|
|
|
559
588
|
const sideEffectInstruction = agent.sideEffects === "writes"
|
|
560
589
|
? `The synchronized policy declares that this agent writes user or workspace data. Use this adapter only when the user explicitly selected this exact agent for the current request. If the agent was chosen automatically or the selection is ambiguous, do not start it and ask the user to select it explicitly.`
|
|
561
590
|
: `The synchronized policy declares that this agent is read-only.`;
|
|
591
|
+
const callerGuidance = usesVerbatimRelay(agent)
|
|
592
|
+
? adapterCallerSpawnGuidance("Claude")
|
|
593
|
+
: null;
|
|
594
|
+
if (usesDirectAnswer(agent)) {
|
|
595
|
+
const completionGuidance = usesVerbatimRelay(agent)
|
|
596
|
+
? claudeAnswerVerbatimCompletionGuidance()
|
|
597
|
+
: claudeAnswerFaithfulCompletionGuidance();
|
|
598
|
+
return [
|
|
599
|
+
`You are a thin transport adapter for the exact Impel native agent ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
|
|
600
|
+
...(callerGuidance ? [callerGuidance] : []),
|
|
601
|
+
`Do not perform the assigned task yourself and do not delegate to any other agent.`,
|
|
602
|
+
`Confirm that the request fits the synchronized capabilities ${JSON.stringify(agent.capabilities)} and none of the exclusions ${JSON.stringify(agent.exclusions)}.${contextRequirement}`,
|
|
603
|
+
sideEffectInstruction,
|
|
604
|
+
`Call ${nativeToolName(NATIVE_AGENT_ANSWER_TOOL)} exactly once with question set to the complete assigned task, optional context set to one string containing all supplied context, and contextKeys naming the fields present in that string.`,
|
|
605
|
+
completionGuidance,
|
|
606
|
+
].join(" ");
|
|
607
|
+
}
|
|
608
|
+
const completionGuidance = usesVerbatimRelay(agent)
|
|
609
|
+
? claudeVerbatimCompletionGuidance()
|
|
610
|
+
: claudeFaithfulCompletionGuidance();
|
|
562
611
|
return [
|
|
563
612
|
`You are a thin transport adapter for the exact Impel native agent ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
|
|
613
|
+
...(callerGuidance ? [callerGuidance] : []),
|
|
564
614
|
`Do not perform the assigned task yourself and do not delegate to any other agent.`,
|
|
565
615
|
`Confirm that the request fits the synchronized capabilities ${JSON.stringify(agent.capabilities)} and none of the exclusions ${JSON.stringify(agent.exclusions)}.${contextRequirement}`,
|
|
566
616
|
sideEffectInstruction,
|
|
567
617
|
`Call ${nativeToolName(NATIVE_AGENT_RUN_TOOL)} exactly once with task set to the complete assigned task, optional context set to one string containing all supplied context, and contextKeys naming the fields present in that string.`,
|
|
568
618
|
`If that call returns an ${JSON.stringify("impel.native-agent-run.v1")} handle, call ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} with exactly that same handle until the transport returns a terminal result. If the call is cancelled, the connection closes, or its result is unknown before you receive a handle, call ${nativeToolName(NATIVE_AGENT_RECOVER_TOOL)} with {} and resume the newest returned pending handle; if multiple handles cannot be safely associated with this request, report them instead of choosing. Never call run_native_agent again for this request, and never restart, nudge, replace, or independently poll the upstream run.`,
|
|
569
|
-
|
|
619
|
+
completionGuidance,
|
|
570
620
|
].join(" ");
|
|
571
621
|
}
|
|
572
622
|
|
|
@@ -581,15 +631,35 @@ function codexAdapterInstructions(tenantId, agent) {
|
|
|
581
631
|
const sideEffectInstruction = agent.sideEffects === "writes"
|
|
582
632
|
? `The synchronized policy declares that this agent writes user or workspace data. Use this adapter only when the user explicitly selected this exact agent for the current request. If the agent was chosen automatically or the selection is ambiguous, ask the user to select this exact agent explicitly.`
|
|
583
633
|
: `The synchronized policy declares that this agent is read-only.`;
|
|
634
|
+
const callerGuidance = usesVerbatimRelay(agent)
|
|
635
|
+
? adapterCallerSpawnGuidance("Codex")
|
|
636
|
+
: null;
|
|
637
|
+
if (usesDirectAnswer(agent)) {
|
|
638
|
+
const completionGuidance = usesVerbatimRelay(agent)
|
|
639
|
+
? adapterAnswerVerbatimCompletionGuidance()
|
|
640
|
+
: adapterAnswerFaithfulCompletionGuidance();
|
|
641
|
+
return [
|
|
642
|
+
`You are a thin transport adapter for the exact Impel native agent ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
|
|
643
|
+
...(callerGuidance ? [callerGuidance] : []),
|
|
644
|
+
`Do not perform the assigned task yourself, do not delegate to any other agent, and do not independently synthesize the result.`,
|
|
645
|
+
`Confirm that the assigned request fits the cataloged capabilities ${JSON.stringify(agent.capabilities)} and none of the exclusions ${JSON.stringify(agent.exclusions)} before answering.${contextRequirement}`,
|
|
646
|
+
sideEffectInstruction,
|
|
647
|
+
`Call ${nativeToolName(NATIVE_AGENT_ANSWER_TOOL)} exactly once with question set to the complete assigned task, optional context set to one string containing all supplied context, and contextKeys naming the fields present in that string.`,
|
|
648
|
+
completionGuidance,
|
|
649
|
+
].join("\n\n");
|
|
650
|
+
}
|
|
651
|
+
const completionGuidance = usesVerbatimRelay(agent)
|
|
652
|
+
? adapterVerbatimCompletionGuidance()
|
|
653
|
+
: adapterFaithfulCompletionGuidance();
|
|
584
654
|
return [
|
|
585
655
|
`You are a thin transport adapter for the exact Impel native agent ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
|
|
586
|
-
|
|
656
|
+
...(callerGuidance ? [callerGuidance] : []),
|
|
587
657
|
`Do not perform the assigned task yourself, do not delegate to any other agent, and do not independently synthesize or rewrite the result.`,
|
|
588
658
|
`Confirm that the assigned request fits the cataloged capabilities ${JSON.stringify(agent.capabilities)} and none of the exclusions ${JSON.stringify(agent.exclusions)} before starting.${contextRequirement}`,
|
|
589
659
|
sideEffectInstruction,
|
|
590
660
|
`Call ${nativeToolName(NATIVE_AGENT_RUN_TOOL)} exactly once with the complete task, optional context string, and supplied context keys. If it returns an ${JSON.stringify("impel.native-agent-run.v1")} handle, call ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} with exactly that same handle until the transport returns a terminal result. If the call is cancelled, the connection closes, or its result is unknown before you receive a handle, call ${nativeToolName(NATIVE_AGENT_RECOVER_TOOL)} with {} and resume the newest returned pending handle; if multiple handles cannot be safely associated with this request, report them instead of choosing. Never call run_native_agent again for this request, and never restart, nudge, replace, or independently poll the upstream run.`,
|
|
591
661
|
`While a remote wait is healthy, the parent must not message, follow up with, or interrupt this adapter.`,
|
|
592
|
-
|
|
662
|
+
completionGuidance,
|
|
593
663
|
].join("\n\n");
|
|
594
664
|
}
|
|
595
665
|
|
|
@@ -602,11 +672,21 @@ function retiredAdapterInstructions(tenantId, agent) {
|
|
|
602
672
|
].join(" ");
|
|
603
673
|
}
|
|
604
674
|
|
|
675
|
+
function customAgentDescription(tenantId, agent) {
|
|
676
|
+
const sideEffects = agent.sideEffects === "writes"
|
|
677
|
+
? " (may write user or workspace data)"
|
|
678
|
+
: " (read-only)";
|
|
679
|
+
return (usesVerbatimRelay(agent)
|
|
680
|
+
? `${customAgentVerbatimDescriptionLead()}. Runs ${agent.title} for Impel tenant ${tenantId}${sideEffects}: ${agent.description}`
|
|
681
|
+
: `Explicitly runs ${agent.title} for Impel tenant ${tenantId}${sideEffects}: ${agent.description}`
|
|
682
|
+
).slice(0, 900);
|
|
683
|
+
}
|
|
684
|
+
|
|
605
685
|
function renderClaudeAgent({ tenantId, agent, name, invocation, recoveryOnly = false }) {
|
|
606
686
|
const description = recoveryOnly
|
|
607
687
|
? `Recovery-only access to pending runs for retired Impel binding ${agent.agentId} in tenant ${tenantId}.`
|
|
608
|
-
:
|
|
609
|
-
const toolNames = recoveryOnly ? NATIVE_AGENT_RECOVERY_TOOL_NAMES :
|
|
688
|
+
: customAgentDescription(tenantId, agent);
|
|
689
|
+
const toolNames = recoveryOnly ? NATIVE_AGENT_RECOVERY_TOOL_NAMES : nativeAgentToolNames(agent);
|
|
610
690
|
const lines = [
|
|
611
691
|
"---",
|
|
612
692
|
`name: ${JSON.stringify(name)}`,
|
|
@@ -633,8 +713,8 @@ function renderClaudeAgent({ tenantId, agent, name, invocation, recoveryOnly = f
|
|
|
633
713
|
function renderCodexAgent({ tenantId, agent, name, invocation, recoveryOnly = false }) {
|
|
634
714
|
const description = recoveryOnly
|
|
635
715
|
? `Recovery-only custom agent for pending runs from retired Impel binding ${agent.agentId} in tenant ${tenantId}.`
|
|
636
|
-
:
|
|
637
|
-
const toolNames = recoveryOnly ? NATIVE_AGENT_RECOVERY_TOOL_NAMES :
|
|
716
|
+
: customAgentDescription(tenantId, agent);
|
|
717
|
+
const toolNames = recoveryOnly ? NATIVE_AGENT_RECOVERY_TOOL_NAMES : nativeAgentToolNames(agent);
|
|
638
718
|
const envEntries = Object.entries(invocation.env || {})
|
|
639
719
|
.map(([key, value]) => `${JSON.stringify(key)} = ${JSON.stringify(value)}`)
|
|
640
720
|
.join(", ");
|
|
@@ -659,7 +739,7 @@ function renderCodexAgent({ tenantId, agent, name, invocation, recoveryOnly = fa
|
|
|
659
739
|
|
|
660
740
|
function boundNativeAgentInvocation(tenantId, agent, invocation, {
|
|
661
741
|
policyFingerprint = nativeAgentPolicyFingerprint(agent),
|
|
662
|
-
|
|
742
|
+
mode = usesDirectAnswer(agent) ? "answer" : "durable",
|
|
663
743
|
} = {}) {
|
|
664
744
|
if (!invocation) {
|
|
665
745
|
return impelNativeAgentMcpInvocation({
|
|
@@ -667,7 +747,7 @@ function boundNativeAgentInvocation(tenantId, agent, invocation, {
|
|
|
667
747
|
agentId: agent.agentId,
|
|
668
748
|
scopeParam: agent.scopeParam,
|
|
669
749
|
policyFingerprint,
|
|
670
|
-
|
|
750
|
+
mode,
|
|
671
751
|
});
|
|
672
752
|
}
|
|
673
753
|
const mcpIndex = invocation.args?.lastIndexOf("mcp") ?? -1;
|
|
@@ -681,7 +761,8 @@ function boundNativeAgentInvocation(tenantId, agent, invocation, {
|
|
|
681
761
|
"--agent-id", agent.agentId,
|
|
682
762
|
"--scope-param", agent.scopeParam,
|
|
683
763
|
"--policy-fingerprint", policyFingerprint,
|
|
684
|
-
...(
|
|
764
|
+
...(mode === "recovery" ? ["--recovery-only"] : []),
|
|
765
|
+
...(mode === "answer" ? ["--answer-only"] : []),
|
|
685
766
|
],
|
|
686
767
|
};
|
|
687
768
|
}
|
|
@@ -753,7 +834,7 @@ function renderRetiredManagedAgents(client, tenantId, bindings, active, invocati
|
|
|
753
834
|
};
|
|
754
835
|
const boundInvocation = boundNativeAgentInvocation(tenantId, agent, invocation, {
|
|
755
836
|
policyFingerprint: binding.policyFingerprint,
|
|
756
|
-
|
|
837
|
+
mode: "recovery",
|
|
757
838
|
});
|
|
758
839
|
const contents = client === "claude"
|
|
759
840
|
? renderClaudeAgent({ tenantId, agent, name, invocation: boundInvocation, recoveryOnly: true })
|
|
@@ -841,7 +922,7 @@ export function syncAgentProfile({
|
|
|
841
922
|
throw new Error("generated native-agent destinations are not unique");
|
|
842
923
|
}
|
|
843
924
|
const priorFiles = new Set(prior?.files || []);
|
|
844
|
-
const priorUsesDiscoveryRoot = [2, 3, 4, 5, MANAGED_AGENT_MANIFEST_VERSION].includes(prior?.version);
|
|
925
|
+
const priorUsesDiscoveryRoot = [2, 3, 4, 5, 6, MANAGED_AGENT_MANIFEST_VERSION].includes(prior?.version);
|
|
845
926
|
|
|
846
927
|
// Native clients discover standalone definitions directly under `agents/`.
|
|
847
928
|
// Preflight every destination before writing so an unmanaged file with the
|
package/src/apps.js
CHANGED
|
@@ -22,6 +22,7 @@ import { ensureClaudeSessionHooks, ensureCodexSessionHooks } from "./sessionHook
|
|
|
22
22
|
import { ADHOC_IDENTITY, codesignIdentityArgs, desiredSigningMode, resolveSigningIdentity } from "./codesign.js";
|
|
23
23
|
import { applyImpelClaudeSandbox } from "./claudeSandbox.js";
|
|
24
24
|
import { brandedEnvironmentName, RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
25
|
+
import { IMPEL_CODEX_PARENT_DELEGATION_INSTRUCTIONS } from "./commands/launch.js";
|
|
25
26
|
|
|
26
27
|
export const CLAUDE_CONFIG_ID = "1ced0000-0000-4000-8000-000000000001";
|
|
27
28
|
const CHATGPT_CONFIG_START = `# >>> ${RUNTIME_BRAND.cli.command} app managed gateway >>>`;
|
|
@@ -276,7 +277,9 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
|
276
277
|
// and ChatGPT/Codex profiles without changing the specialist MCP server.
|
|
277
278
|
// 28: bind generated native-agent profiles to the durable local composite MCP
|
|
278
279
|
// transport and remove model-controlled upstream start/read polling.
|
|
279
|
-
|
|
280
|
+
// 29: add marker-aware custom-agent verbatim relay guidance to ChatGPT and
|
|
281
|
+
// expose direct-answer native agents through their one-shot local MCP binding.
|
|
282
|
+
export const CURRENT_CONFIG_VERSION = 29;
|
|
280
283
|
|
|
281
284
|
// Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
|
|
282
285
|
// helper rebranding, and signing. A vendored bundle is rebuilt only when this
|
|
@@ -1335,6 +1338,14 @@ function writeChatGPTConfig(
|
|
|
1335
1338
|
}
|
|
1336
1339
|
}
|
|
1337
1340
|
|
|
1341
|
+
const unmanagedToml = stripManagedChatGPTToml(currentToml);
|
|
1342
|
+
const userDeveloperInstructions = readTopLevelTomlString(unmanagedToml, "developer_instructions");
|
|
1343
|
+
const developerInstructions = userDeveloperInstructions
|
|
1344
|
+
? `${userDeveloperInstructions}\n\n${IMPEL_CODEX_PARENT_DELEGATION_INSTRUCTIONS}`
|
|
1345
|
+
: IMPEL_CODEX_PARENT_DELEGATION_INSTRUCTIONS;
|
|
1346
|
+
const preservedToml = userDeveloperInstructions
|
|
1347
|
+
? removeTopLevelTomlString(unmanagedToml, "developer_instructions")
|
|
1348
|
+
: unmanagedToml;
|
|
1338
1349
|
const managedToml = [
|
|
1339
1350
|
CHATGPT_CONFIG_START,
|
|
1340
1351
|
`# Managed by \`${RUNTIME_BRAND.cli.command} app update\`. This profile is isolated from ~/.codex.`,
|
|
@@ -1347,6 +1358,9 @@ function writeChatGPTConfig(
|
|
|
1347
1358
|
`model_catalog_json = ${tomlString(paths.chatgpt.catalog)}`,
|
|
1348
1359
|
...(selectedEffort ? [`model_reasoning_effort = ${tomlString(selectedEffort)}`] : []),
|
|
1349
1360
|
...(selectedTier ? [`service_tier = ${tomlString(selectedTier)}`] : []),
|
|
1361
|
+
...(RUNTIME_BRAND.features.agents
|
|
1362
|
+
? [`developer_instructions = ${tomlString(developerInstructions)}`]
|
|
1363
|
+
: []),
|
|
1350
1364
|
"",
|
|
1351
1365
|
// The built-in ChatGPT provider derives its inference endpoint from
|
|
1352
1366
|
// chatgpt.com even when chatgpt_base_url points at the gateway. Keep the
|
|
@@ -1383,7 +1397,7 @@ function writeChatGPTConfig(
|
|
|
1383
1397
|
] : []),
|
|
1384
1398
|
CHATGPT_CONFIG_END,
|
|
1385
1399
|
].join("\n");
|
|
1386
|
-
const mergedToml = mergeManagedChatGPTToml(
|
|
1400
|
+
const mergedToml = mergeManagedChatGPTToml(preservedToml, managedToml);
|
|
1387
1401
|
writeAtomic(configPath, hardenManagedCodexToml(mergedToml, configPath), 0o600);
|
|
1388
1402
|
secureManagedCodexHome(paths.chatgpt.codexHome);
|
|
1389
1403
|
}
|
|
@@ -1605,6 +1619,16 @@ function readTopLevelTomlString(toml, key) {
|
|
|
1605
1619
|
}
|
|
1606
1620
|
}
|
|
1607
1621
|
|
|
1622
|
+
function removeTopLevelTomlString(toml, key) {
|
|
1623
|
+
const tableIndex = toml.search(/^\s*\[/mu);
|
|
1624
|
+
const topLevelEnd = tableIndex === -1 ? toml.length : tableIndex;
|
|
1625
|
+
const topLevel = toml.slice(0, topLevelEnd).replace(
|
|
1626
|
+
new RegExp(`^\\s*${escapeRegex(key)}\\s*=\\s*"(?:[^"\\\\]|\\\\.)*"\\s*\\r?\\n?`, "mu"),
|
|
1627
|
+
"",
|
|
1628
|
+
);
|
|
1629
|
+
return topLevel + toml.slice(topLevelEnd);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1608
1632
|
/**
|
|
1609
1633
|
* Rewrite one tenant's app token helper with the current node/CLI paths.
|
|
1610
1634
|
* Cheap and Impel-owned, so install recovery can use it to heal helpers whose
|
package/src/cli.js
CHANGED
|
@@ -35,7 +35,8 @@ Get started:
|
|
|
35
35
|
Work:
|
|
36
36
|
impel claude [args...] Launch Claude Code with an isolated Impel profile
|
|
37
37
|
impel codex [args...] Launch Codex with an isolated Impel profile
|
|
38
|
-
impel remote
|
|
38
|
+
impel remote handoff|dispatch|handback Move or control provider-native sessions remotely
|
|
39
|
+
impel remote status|viewer|proxy Inspect, control, or connect to a remote run
|
|
39
40
|
impel tenant list List accessible organizations
|
|
40
41
|
impel tenant current Show the CLI's current organization
|
|
41
42
|
impel tenant use <org> Select the organization for CLI launches
|
package/src/commands/launch.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
saveConfig,
|
|
16
16
|
} from "../config.js";
|
|
17
17
|
import { impelClaudeBaseUrl, impelCrossAppClaudeBaseUrl } from "../claudeSetup.js";
|
|
18
|
+
import { parentVerbatimRelayAppendix } from "../verbatimRelay.js";
|
|
18
19
|
import { withGitEnvironment } from "../skills.js";
|
|
19
20
|
import { assertProviderScopes, ensureTenantSelection, tenantCredential } from "../tenants.js";
|
|
20
21
|
import { maybePrintUpdateNotice } from "../updates.js";
|
|
@@ -49,10 +50,18 @@ export {
|
|
|
49
50
|
|
|
50
51
|
export const IMPEL_SPECIALIST_DELEGATION_INSTRUCTIONS = `You are running in an Impel tenant-scoped session. Before starting any non-trivial task, call the Impel MCP tool list_specialists. If exactly one available specialist clearly matches the user's request, its capabilities and its exclusions, delegate the complete request by calling start_specialist_run exactly once with a stable idempotency key, then call read_specialist_run until it reaches a terminal state. When the run succeeds, use the specialist's result as your response instead of redoing the work. If no specialist clearly matches, the tools are unavailable, or the run fails, continue normally yourself. Do not delegate trivial requests, do not call a specialist excluded from the request, and never invent a specialist result.`;
|
|
51
52
|
|
|
53
|
+
export const IMPEL_CUSTOM_AGENT_VERBATIM_RELAY_APPENDIX = parentVerbatimRelayAppendix();
|
|
54
|
+
|
|
55
|
+
export const IMPEL_CLAUDE_PARENT_DELEGATION_INSTRUCTIONS =
|
|
56
|
+
`${IMPEL_SPECIALIST_DELEGATION_INSTRUCTIONS} ${IMPEL_CUSTOM_AGENT_VERBATIM_RELAY_APPENDIX}`;
|
|
57
|
+
|
|
52
58
|
export const IMPEL_CODEX_SPECIALIST_DELEGATION_INSTRUCTIONS =
|
|
53
59
|
`${IMPEL_SPECIALIST_DELEGATION_INSTRUCTIONS} ` +
|
|
54
60
|
`Codex can defer MCP tools behind tool_search. If an Impel specialist tool is not directly visible, call tool_search for its exact name before treating it as unavailable: impel_specialists-list_specialists for discovery, impel_specialists-start_specialist_run to delegate, and impel_specialists-read_specialist_run to poll the result. Use the returned tool for the same one-run delegation flow.`;
|
|
55
61
|
|
|
62
|
+
export const IMPEL_CODEX_PARENT_DELEGATION_INSTRUCTIONS =
|
|
63
|
+
`${IMPEL_CODEX_SPECIALIST_DELEGATION_INSTRUCTIONS} ${IMPEL_CUSTOM_AGENT_VERBATIM_RELAY_APPENDIX}`;
|
|
64
|
+
|
|
56
65
|
const IMPEL_CODEX_RUNTIME_OVERRIDES = [
|
|
57
66
|
// Codex models may select code mode even when the standalone
|
|
58
67
|
// `codex-code-mode-host` companion is not present in the vendor install.
|
|
@@ -64,14 +73,14 @@ const IMPEL_CODEX_RUNTIME_OVERRIDES = [
|
|
|
64
73
|
export function impelLaunchArguments(tool, argv) {
|
|
65
74
|
if (!RUNTIME_BRAND.features.agents) return [...argv];
|
|
66
75
|
if (tool === "claude") {
|
|
67
|
-
return ["--append-system-prompt",
|
|
76
|
+
return ["--append-system-prompt", IMPEL_CLAUDE_PARENT_DELEGATION_INSTRUCTIONS, ...argv];
|
|
68
77
|
}
|
|
69
78
|
if (tool === "codex") {
|
|
70
79
|
// `-c` is a Codex global option, so it must precede subcommands such as
|
|
71
80
|
// `exec`, `resume`, and `mcp`. JSON strings are valid TOML basic strings.
|
|
72
81
|
return [
|
|
73
82
|
"-c",
|
|
74
|
-
`developer_instructions=${JSON.stringify(
|
|
83
|
+
`developer_instructions=${JSON.stringify(IMPEL_CODEX_PARENT_DELEGATION_INSTRUCTIONS)}`,
|
|
75
84
|
...IMPEL_CODEX_RUNTIME_OVERRIDES.flatMap((override) => ["-c", override]),
|
|
76
85
|
...argv,
|
|
77
86
|
];
|
package/src/commands/mcp.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "../config.js";
|
|
10
10
|
import { parseFlags } from "../args.js";
|
|
11
11
|
import {
|
|
12
|
+
NATIVE_AGENT_ANSWER_TOOL,
|
|
12
13
|
NATIVE_AGENT_RECOVER_TOOL,
|
|
13
14
|
NATIVE_AGENT_RESUME_TOOL,
|
|
14
15
|
NATIVE_AGENT_RUN_TOOL,
|
|
@@ -193,8 +194,9 @@ export function runNativeAgentMcpServer({
|
|
|
193
194
|
transport,
|
|
194
195
|
input = process.stdin,
|
|
195
196
|
output = process.stdout,
|
|
196
|
-
|
|
197
|
+
mode = "durable",
|
|
197
198
|
}) {
|
|
199
|
+
if (!["durable", "recovery", "answer"].includes(mode)) throw new Error("invalid native-agent MCP mode");
|
|
198
200
|
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
199
201
|
const active = new Map();
|
|
200
202
|
const pending = new Set();
|
|
@@ -231,7 +233,9 @@ export function runNativeAgentMcpServer({
|
|
|
231
233
|
return;
|
|
232
234
|
}
|
|
233
235
|
if (message.method === "tools/list") {
|
|
234
|
-
write(nativeRpcResult(message.id, {
|
|
236
|
+
write(nativeRpcResult(message.id, {
|
|
237
|
+
tools: nativeAgentCompositeTools({ mode }),
|
|
238
|
+
}));
|
|
235
239
|
return;
|
|
236
240
|
}
|
|
237
241
|
if (message.method !== "tools/call") {
|
|
@@ -249,12 +253,18 @@ export function runNativeAgentMcpServer({
|
|
|
249
253
|
}));
|
|
250
254
|
};
|
|
251
255
|
let value;
|
|
252
|
-
if (name ===
|
|
253
|
-
if (
|
|
256
|
+
if (name === NATIVE_AGENT_ANSWER_TOOL) {
|
|
257
|
+
if (mode !== "answer") throw new Error("this native-agent binding cannot answer directly");
|
|
258
|
+
value = await transport.answer(args, { signal: controller.signal });
|
|
259
|
+
} else if (name === NATIVE_AGENT_RUN_TOOL) {
|
|
260
|
+
if (mode === "recovery") throw new Error("retired native-agent bindings cannot start new runs");
|
|
261
|
+
if (mode === "answer") throw new Error("direct-answer native-agent bindings cannot start durable runs");
|
|
254
262
|
value = await transport.run(args, { signal: controller.signal, onProgress });
|
|
255
263
|
} else if (name === NATIVE_AGENT_RESUME_TOOL) {
|
|
264
|
+
if (mode === "answer") throw new Error("direct-answer native-agent bindings have no runs to resume");
|
|
256
265
|
value = await transport.resume(args, { signal: controller.signal, onProgress });
|
|
257
266
|
} else if (name === NATIVE_AGENT_RECOVER_TOOL) {
|
|
267
|
+
if (mode === "answer") throw new Error("direct-answer native-agent bindings have no runs to recover");
|
|
258
268
|
value = transport.recover(args);
|
|
259
269
|
} else {
|
|
260
270
|
throw new Error("unsupported native-agent MCP tool");
|
|
@@ -268,7 +278,9 @@ export function runNativeAgentMcpServer({
|
|
|
268
278
|
error: {
|
|
269
279
|
code: cancelled ? -32800 : -32000,
|
|
270
280
|
message: cancelled
|
|
271
|
-
?
|
|
281
|
+
? (mode === "answer"
|
|
282
|
+
? "Direct-answer native-agent call cancelled; retry the answer request if needed."
|
|
283
|
+
: "Native-agent attachment cancelled; resume it with the same handle.")
|
|
272
284
|
: redactSecretText(error?.message || error),
|
|
273
285
|
},
|
|
274
286
|
}));
|
|
@@ -306,6 +318,7 @@ export async function cmdMcp(argv = []) {
|
|
|
306
318
|
"scope-param": { type: "string" },
|
|
307
319
|
"policy-fingerprint": { type: "string" },
|
|
308
320
|
"recovery-only": { type: "boolean" },
|
|
321
|
+
"answer-only": { type: "boolean" },
|
|
309
322
|
});
|
|
310
323
|
const targetSpecified = Object.hasOwn(flags, "target");
|
|
311
324
|
if (targetSpecified && ![TASKS_TARGET, IMPEL_NATIVE_AGENT_MCP_TARGET].includes(flags.target)) {
|
|
@@ -323,7 +336,10 @@ export async function cmdMcp(argv = []) {
|
|
|
323
336
|
}
|
|
324
337
|
}
|
|
325
338
|
if (nativeAgentTarget) {
|
|
326
|
-
const expectedFlags = [
|
|
339
|
+
const expectedFlags = [
|
|
340
|
+
"target", "tenant", "agent-id", "scope-param", "policy-fingerprint",
|
|
341
|
+
"recovery-only", "answer-only",
|
|
342
|
+
];
|
|
327
343
|
const unsupportedFlags = Object.keys(flags).filter((name) => !expectedFlags.includes(name));
|
|
328
344
|
if (unsupportedFlags.length > 0 || positionals.length > 0) {
|
|
329
345
|
throw new Error("unsupported native-agent MCP arguments");
|
|
@@ -333,10 +349,14 @@ export async function cmdMcp(argv = []) {
|
|
|
333
349
|
throw new Error(`native-agent MCP requires a fixed --${flag} argument`);
|
|
334
350
|
}
|
|
335
351
|
}
|
|
352
|
+
if (flags["recovery-only"] === true && flags["answer-only"] === true) {
|
|
353
|
+
throw new Error("native-agent MCP cannot be both recovery-only and answer-only");
|
|
354
|
+
}
|
|
336
355
|
} else if (!tasksTarget && (Object.hasOwn(flags, "agent-id")
|
|
337
356
|
|| Object.hasOwn(flags, "scope-param")
|
|
338
357
|
|| Object.hasOwn(flags, "policy-fingerprint")
|
|
339
|
-
|| Object.hasOwn(flags, "recovery-only")
|
|
358
|
+
|| Object.hasOwn(flags, "recovery-only")
|
|
359
|
+
|| Object.hasOwn(flags, "answer-only"))) {
|
|
340
360
|
throw new Error("native-agent binding flags require `--target native-agent`");
|
|
341
361
|
}
|
|
342
362
|
const config = loadConfig();
|
|
@@ -358,7 +378,9 @@ export async function cmdMcp(argv = []) {
|
|
|
358
378
|
gatewayUrl,
|
|
359
379
|
credential,
|
|
360
380
|
}),
|
|
361
|
-
|
|
381
|
+
mode: flags["recovery-only"] === true
|
|
382
|
+
? "recovery"
|
|
383
|
+
: (flags["answer-only"] === true ? "answer" : "durable"),
|
|
362
384
|
});
|
|
363
385
|
}
|
|
364
386
|
const endpoint = tasksTarget ? null : `${gatewayUrl}/mcp`;
|