impel-cli 0.19.3 → 0.20.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/package.json +6 -5
- package/src/agents.js +186 -11
- package/src/apps.js +102 -10
- package/src/cliProfiles.js +47 -8
- package/src/commands/launch.js +11 -2
- package/src/commands/mcp.js +201 -17
- package/src/commands/remote.js +166 -36
- package/src/directAnswer.js +59 -0
- package/src/selfInvocation.js +27 -0
- package/src/verbatimRelay.js +64 -0
package/README.md
CHANGED
|
@@ -228,6 +228,30 @@ impel remote dispatch --provider claude --session <session-id> --fork
|
|
|
228
228
|
impel remote dispatch --provider codex --session <session-id> --fork
|
|
229
229
|
```
|
|
230
230
|
|
|
231
|
+
The installable `remote-control` skill turns this into an in-app handoff. Ask
|
|
232
|
+
Claude Code or Codex to run the current session remotely, or invoke the skill
|
|
233
|
+
directly, and it resolves the current Claude session/Codex thread id before
|
|
234
|
+
running:
|
|
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
|
+
|
|
231
255
|
Remote vendor commands run in dangerous/bypass mode as the unprivileged
|
|
232
256
|
`agent` user; the disposable Fargate task is the isolation boundary. Existing
|
|
233
257
|
Impel lifecycle hooks continue mirroring remote transcripts to
|
|
@@ -326,6 +350,37 @@ impel tasks delete IMP-123 --yes
|
|
|
326
350
|
Use `--json` when exact machine-readable fields matter. Gateway-only members
|
|
327
351
|
cannot use task CRUD.
|
|
328
352
|
|
|
353
|
+
### Native Tasks MCP App
|
|
354
|
+
|
|
355
|
+
Setup and update register a second tenant-bound MCP server named
|
|
356
|
+
`impel-tasks` in Impel-managed Claude and Codex/ChatGPT profiles. It is separate
|
|
357
|
+
from the existing `impel` specialist server: removing or disabling Tasks does
|
|
358
|
+
not change specialist routing or approvals.
|
|
359
|
+
|
|
360
|
+
The managed entry launches an absolute, upgrade-stable invocation equivalent
|
|
361
|
+
to:
|
|
362
|
+
|
|
363
|
+
```sh
|
|
364
|
+
impel mcp --target tasks --tenant <fixed-tenant>
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
This is an internal transport command, not an interactive PAT interface. The
|
|
368
|
+
child process reads the PAT from Impel's private config, sends it only as the
|
|
369
|
+
control-plane HTTP bearer credential, and fixes tenant selection in the
|
|
370
|
+
transport header. Generated Claude/Codex MCP entries contain neither the PAT
|
|
371
|
+
nor a tenant-derived credential.
|
|
372
|
+
|
|
373
|
+
- The pinned Impel Claude Desktop can render the standard MCP Apps Tasks
|
|
374
|
+
resource when its host supports that protocol.
|
|
375
|
+
- Claude Code and stable Codex receive complete headless task tools and text
|
|
376
|
+
results; no experimental Codex MCP Apps flag is enabled.
|
|
377
|
+
- Profiles preserve unrelated MCP servers. If a user-authored server already
|
|
378
|
+
owns the `impel-tasks` name, setup/update fails with remediation instead of
|
|
379
|
+
overwriting it.
|
|
380
|
+
- The Tasks bridge accepts Streamable HTTP's required JSON/SSE media types but
|
|
381
|
+
deliberately requires bounded JSON responses from the control plane. It
|
|
382
|
+
does not establish an SSE session or persist protocol state.
|
|
383
|
+
|
|
329
384
|
## PAT lifecycle
|
|
330
385
|
|
|
331
386
|
Minting and revocation remain explicit account operations:
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "impel-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0-beta.0",
|
|
4
4
|
"description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"packageManager": "pnpm@11.17.0",
|
|
6
7
|
"bin": {
|
|
7
8
|
"impel": "bin/impel.js"
|
|
8
9
|
},
|
|
@@ -19,6 +20,9 @@
|
|
|
19
20
|
"engines": {
|
|
20
21
|
"node": ">=18.0.0"
|
|
21
22
|
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "node --test"
|
|
25
|
+
},
|
|
22
26
|
"license": "UNLICENSED",
|
|
23
27
|
"publishConfig": {
|
|
24
28
|
"access": "public",
|
|
@@ -27,8 +31,5 @@
|
|
|
27
31
|
"repository": {
|
|
28
32
|
"type": "git",
|
|
29
33
|
"url": "git+https://github.com/UseImpel/impel-cli.git"
|
|
30
|
-
},
|
|
31
|
-
"scripts": {
|
|
32
|
-
"test": "node --test"
|
|
33
34
|
}
|
|
34
|
-
}
|
|
35
|
+
}
|
package/src/agents.js
CHANGED
|
@@ -10,8 +10,24 @@ import os from "node:os";
|
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
|
|
12
12
|
import { normalizeGatewayUrl, redactSecretText } from "./config.js";
|
|
13
|
+
import {
|
|
14
|
+
adapterAnswerHardCompletionGuidance,
|
|
15
|
+
adapterAnswerSoftCompletionGuidance,
|
|
16
|
+
claudeAnswerHardCompletionGuidance,
|
|
17
|
+
claudeAnswerSoftCompletionGuidance,
|
|
18
|
+
usesDirectAnswer,
|
|
19
|
+
} from "./directAnswer.js";
|
|
13
20
|
import { impelMcpInvocation } from "./selfInvocation.js";
|
|
14
21
|
import { normalizeTenantId } from "./tenants.js";
|
|
22
|
+
import {
|
|
23
|
+
adapterCallerSpawnGuidance,
|
|
24
|
+
adapterHardCompletionGuidance,
|
|
25
|
+
adapterSoftCompletionGuidance,
|
|
26
|
+
claudeHardCompletionGuidance,
|
|
27
|
+
claudeSoftCompletionGuidance,
|
|
28
|
+
customAgentVerbatimDescriptionLead,
|
|
29
|
+
usesVerbatimRelay,
|
|
30
|
+
} from "./verbatimRelay.js";
|
|
15
31
|
import { renameWithWindowsRetry } from "./windowsFs.js";
|
|
16
32
|
|
|
17
33
|
export const AGENT_SYNC_TTL_MS = 6 * 60 * 60 * 1000;
|
|
@@ -20,14 +36,22 @@ export const MANAGED_AGENT_MANIFEST = ".manifest.json";
|
|
|
20
36
|
export const NATIVE_AGENT_LIST_TOOL = "impel_specialists-list_native_agents";
|
|
21
37
|
export const NATIVE_AGENT_START_TOOL = "impel_specialists-start_native_agent_run";
|
|
22
38
|
export const NATIVE_AGENT_READ_TOOL = "impel_specialists-read_native_agent_run";
|
|
39
|
+
export const NATIVE_AGENT_ANSWER_TOOL = "impel_specialists-answer_native_agent";
|
|
23
40
|
export const MANAGED_AGENT_MCP_SERVER = "impel_agent";
|
|
24
|
-
export const MANAGED_AGENT_MANIFEST_VERSION =
|
|
41
|
+
export const MANAGED_AGENT_MANIFEST_VERSION = 6;
|
|
25
42
|
|
|
26
|
-
const
|
|
43
|
+
const NATIVE_AGENT_SESSION_TOOL_NAMES = [
|
|
27
44
|
NATIVE_AGENT_LIST_TOOL,
|
|
28
45
|
NATIVE_AGENT_START_TOOL,
|
|
29
46
|
NATIVE_AGENT_READ_TOOL,
|
|
30
47
|
];
|
|
48
|
+
|
|
49
|
+
function nativeAgentToolNames(agent) {
|
|
50
|
+
return usesDirectAnswer(agent)
|
|
51
|
+
? [NATIVE_AGENT_ANSWER_TOOL]
|
|
52
|
+
: NATIVE_AGENT_SESSION_TOOL_NAMES;
|
|
53
|
+
}
|
|
54
|
+
|
|
31
55
|
const SAFE_AGENT_ID_RE = /^[A-Za-z0-9_.:-]{1,160}$/u;
|
|
32
56
|
const SAFE_SCOPE_PARAM_RE = /^[A-Za-z0-9_.:-]{1,160}$/u;
|
|
33
57
|
const MAX_CATALOG_ITEMS = 500;
|
|
@@ -125,6 +149,12 @@ export function normalizeNativeAgentCatalog(payload, expectedTenantId) {
|
|
|
125
149
|
throw new Error(`native-agent catalog returned duplicate binding ${binding}`);
|
|
126
150
|
}
|
|
127
151
|
seenBindings.add(binding);
|
|
152
|
+
if (agent.verbatimRelay !== undefined && typeof agent.verbatimRelay !== "boolean") {
|
|
153
|
+
throw new Error("native-agent catalog returned an invalid verbatimRelay");
|
|
154
|
+
}
|
|
155
|
+
if (agent.directAnswer !== undefined && typeof agent.directAnswer !== "boolean") {
|
|
156
|
+
throw new Error("native-agent catalog returned an invalid directAnswer");
|
|
157
|
+
}
|
|
128
158
|
return {
|
|
129
159
|
agentId,
|
|
130
160
|
title: boundedString(agent.title, "title", { max: 160 }),
|
|
@@ -135,6 +165,8 @@ export function normalizeNativeAgentCatalog(payload, expectedTenantId) {
|
|
|
135
165
|
exclusions: stringList(agent.exclusions, "exclusions"),
|
|
136
166
|
requiredContext: stringList(agent.requiredContext, "requiredContext"),
|
|
137
167
|
sideEffects: enumString(agent.sideEffects, "sideEffects", ["read-only", "writes"]),
|
|
168
|
+
...(agent.verbatimRelay === true ? { verbatimRelay: true } : {}),
|
|
169
|
+
...(agent.directAnswer === true ? { directAnswer: true } : {}),
|
|
138
170
|
};
|
|
139
171
|
});
|
|
140
172
|
return { orgId: tenantId, agents };
|
|
@@ -329,14 +361,35 @@ function claudeAdapterInstructions(tenantId, agent) {
|
|
|
329
361
|
const sideEffectInstruction = agent.sideEffects === "writes"
|
|
330
362
|
? `The catalog 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; that explicit selection is the required side-effect confirmation, so pass confirmedSideEffects true. If the agent was chosen automatically or the selection is ambiguous, do not start it and ask the user to select it explicitly.`
|
|
331
363
|
: `The catalog declares that this agent is read-only; omit confirmedSideEffects.`;
|
|
364
|
+
const callerSpawnGuidance = usesVerbatimRelay(agent)
|
|
365
|
+
? adapterCallerSpawnGuidance("Claude")
|
|
366
|
+
: null;
|
|
367
|
+
if (usesDirectAnswer(agent)) {
|
|
368
|
+
const completionGuidance = usesVerbatimRelay(agent)
|
|
369
|
+
? claudeAnswerHardCompletionGuidance()
|
|
370
|
+
: claudeAnswerSoftCompletionGuidance();
|
|
371
|
+
return [
|
|
372
|
+
`You are a thin transport adapter for the exact Impel native agent ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
|
|
373
|
+
...(callerSpawnGuidance ? [callerSpawnGuidance] : []),
|
|
374
|
+
`Do not perform the assigned task yourself, do not delegate to any other agent, and do not call Impel start_native_agent_run or read_native_agent_run.`,
|
|
375
|
+
`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}`,
|
|
376
|
+
sideEffectInstruction,
|
|
377
|
+
`Call ${toolName(NATIVE_AGENT_ANSWER_TOOL)} exactly once with agentId ${JSON.stringify(agent.agentId)}, scopeParam ${JSON.stringify(agent.scopeParam)}, question set to the complete assigned task, optional context set to one string containing all supplied context (omit it when no context was supplied), contextKeys naming the context fields present in that string, and confirmedSideEffects as directed above.`,
|
|
378
|
+
completionGuidance,
|
|
379
|
+
].join(" ");
|
|
380
|
+
}
|
|
381
|
+
const completionGuidance = usesVerbatimRelay(agent)
|
|
382
|
+
? claudeHardCompletionGuidance()
|
|
383
|
+
: claudeSoftCompletionGuidance();
|
|
332
384
|
return [
|
|
333
385
|
`You are a thin transport adapter for the exact Impel native agent ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
|
|
386
|
+
...(callerSpawnGuidance ? [callerSpawnGuidance] : []),
|
|
334
387
|
`Do not perform the assigned task yourself and do not delegate to any other agent.`,
|
|
335
388
|
`First call ${toolName(NATIVE_AGENT_LIST_TOOL)} and verify that the exact agentId is still available with sideEffects ${JSON.stringify(agent.sideEffects)}. If it is unavailable or its policy excludes the request, stop with that explicit error.`,
|
|
336
389
|
sideEffectInstruction,
|
|
337
390
|
`Call ${toolName(NATIVE_AGENT_START_TOOL)} exactly once with agentId ${JSON.stringify(agent.agentId)}, scopeParam ${JSON.stringify(agent.scopeParam)}, task set to the complete assigned task, optional context set to one string containing all supplied context (omit it when no context was supplied), contextKeys naming the context fields present in that string, confirmedSideEffects as directed above, and one stable idempotencyKey that you reuse for this logical task.${contextRequirement}`,
|
|
338
391
|
`Then call ${toolName(NATIVE_AGENT_READ_TOOL)} with the returned runId and waitSeconds 20 until the run reaches a terminal state.`,
|
|
339
|
-
|
|
392
|
+
completionGuidance,
|
|
340
393
|
].join(" ");
|
|
341
394
|
}
|
|
342
395
|
|
|
@@ -349,7 +402,96 @@ const CODEX_CONTEXT_PLACEHOLDER = "__IMPEL_OPTIONAL_CONTEXT_STRING_OR_NULL_JSON_
|
|
|
349
402
|
const CODEX_CONTEXT_KEYS_PLACEHOLDER = "__IMPEL_SUPPLIED_CONTEXT_KEYS_JSON__";
|
|
350
403
|
const CODEX_IDEMPOTENCY_KEY_PLACEHOLDER = "__IMPEL_LOGICAL_INVOCATION_IDEMPOTENCY_KEY_JSON__";
|
|
351
404
|
|
|
405
|
+
function renderCodexDirectAnswerOrchestration(tenantId, agent) {
|
|
406
|
+
const expectedTenantId = normalizeTenantId(tenantId);
|
|
407
|
+
const nestedToolName = (toolName) => nativeToolName(toolName).replaceAll("-", "_");
|
|
408
|
+
const answerTool = nestedToolName(NATIVE_AGENT_ANSWER_TOOL);
|
|
409
|
+
return [
|
|
410
|
+
'// @exec: {"yield_time_ms": 30000, "max_output_tokens": 30000}',
|
|
411
|
+
`const assignedTask = ${CODEX_TASK_PLACEHOLDER};`,
|
|
412
|
+
`const suppliedContext = ${CODEX_CONTEXT_PLACEHOLDER};`,
|
|
413
|
+
`const suppliedContextKeys = ${CODEX_CONTEXT_KEYS_PLACEHOLDER};`,
|
|
414
|
+
`const idempotencyKey = ${CODEX_IDEMPOTENCY_KEY_PLACEHOLDER};`,
|
|
415
|
+
`const expectedTenantId = ${JSON.stringify(expectedTenantId)};`,
|
|
416
|
+
`const expectedAgent = ${JSON.stringify(agent)};`,
|
|
417
|
+
`const answerToolName = ${JSON.stringify(answerTool)};`,
|
|
418
|
+
"",
|
|
419
|
+
"function contentText(response) {",
|
|
420
|
+
' return response?.content?.find((item) => item?.type === "text" && typeof item.text === "string")?.text;',
|
|
421
|
+
"}",
|
|
422
|
+
"",
|
|
423
|
+
"function toolPayload(response, label) {",
|
|
424
|
+
' if (!response || typeof response !== "object") throw new Error(label + " returned no result");',
|
|
425
|
+
' if (response.isError) throw new Error(contentText(response) || label + " failed");',
|
|
426
|
+
' if (response.structuredContent && typeof response.structuredContent === "object") return response.structuredContent;',
|
|
427
|
+
" const serialized = contentText(response);",
|
|
428
|
+
" if (serialized !== undefined) {",
|
|
429
|
+
" try {",
|
|
430
|
+
" return JSON.parse(serialized);",
|
|
431
|
+
" } catch {",
|
|
432
|
+
' throw new Error(label + " returned invalid JSON");',
|
|
433
|
+
" }",
|
|
434
|
+
" }",
|
|
435
|
+
" return response;",
|
|
436
|
+
"}",
|
|
437
|
+
"",
|
|
438
|
+
"function extractAnswerFinalText(payload) {",
|
|
439
|
+
' if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;',
|
|
440
|
+
' for (const key of ["forUser", "answer"]) {',
|
|
441
|
+
" const value = payload[key];",
|
|
442
|
+
' if (typeof value === "string" && value.trim()) return value;',
|
|
443
|
+
" }",
|
|
444
|
+
" return null;",
|
|
445
|
+
"}",
|
|
446
|
+
"",
|
|
447
|
+
"function errorValue(error, fallback) {",
|
|
448
|
+
" if (error === undefined || error === null) return fallback;",
|
|
449
|
+
' if (error instanceof Error) return error.message;',
|
|
450
|
+
" return error;",
|
|
451
|
+
"}",
|
|
452
|
+
"",
|
|
453
|
+
"async function orchestrate() {",
|
|
454
|
+
" try {",
|
|
455
|
+
' if (typeof assignedTask !== "string" || !assignedTask.trim()) throw new Error("the complete assigned task is required");',
|
|
456
|
+
' if (suppliedContext !== null && typeof suppliedContext !== "string") throw new Error("supplied context must be a string or null");',
|
|
457
|
+
' if (expectedAgent.requiredContext.length && (typeof suppliedContext !== "string" || !suppliedContext.trim())) {',
|
|
458
|
+
' throw new Error("nonblank supplied context is required for required context keys");',
|
|
459
|
+
" }",
|
|
460
|
+
' if (!Array.isArray(suppliedContextKeys)) throw new Error("supplied context keys must be an array");',
|
|
461
|
+
" const missingContext = expectedAgent.requiredContext.filter((key) =>",
|
|
462
|
+
" !suppliedContextKeys.includes(key)",
|
|
463
|
+
" );",
|
|
464
|
+
' if (missingContext.length) throw new Error("missing required context: " + missingContext.join(", "));',
|
|
465
|
+
"",
|
|
466
|
+
" const answerArguments = {",
|
|
467
|
+
" agentId: expectedAgent.agentId,",
|
|
468
|
+
" scopeParam: expectedAgent.scopeParam,",
|
|
469
|
+
" question: assignedTask,",
|
|
470
|
+
" contextKeys: suppliedContextKeys,",
|
|
471
|
+
" };",
|
|
472
|
+
" if (suppliedContext !== null) answerArguments.context = suppliedContext;",
|
|
473
|
+
' if (expectedAgent.sideEffects === "writes") answerArguments.confirmedSideEffects = true;',
|
|
474
|
+
" const answered = toolPayload(await tools[answerToolName](answerArguments), \"native-agent answer\");",
|
|
475
|
+
" const finalText = extractAnswerFinalText(answered);",
|
|
476
|
+
' if (typeof finalText !== "string") throw new Error("native-agent answer returned no forUser/answer");',
|
|
477
|
+
" text(finalText);",
|
|
478
|
+
" } catch (error) {",
|
|
479
|
+
" text(JSON.stringify({",
|
|
480
|
+
' error: errorValue(error, "native-agent answer adapter failed"),',
|
|
481
|
+
" tenantId: expectedTenantId,",
|
|
482
|
+
" agentId: expectedAgent.agentId,",
|
|
483
|
+
" }));",
|
|
484
|
+
" }",
|
|
485
|
+
"}",
|
|
486
|
+
"",
|
|
487
|
+
"await orchestrate();",
|
|
488
|
+
].join("\n");
|
|
489
|
+
}
|
|
490
|
+
|
|
352
491
|
export function renderCodexAdapterOrchestration(tenantId, agent) {
|
|
492
|
+
if (usesDirectAnswer(agent)) {
|
|
493
|
+
return renderCodexDirectAnswerOrchestration(tenantId, agent);
|
|
494
|
+
}
|
|
353
495
|
const expectedTenantId = normalizeTenantId(tenantId);
|
|
354
496
|
const nestedToolName = (toolName) => nativeToolName(toolName).replaceAll("-", "_");
|
|
355
497
|
const listTool = nestedToolName(NATIVE_AGENT_LIST_TOOL);
|
|
@@ -494,29 +636,61 @@ function codexAdapterInstructions(tenantId, agent) {
|
|
|
494
636
|
? `The catalog 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; that explicit selection is the required side-effect confirmation. If the agent was chosen automatically or the selection is ambiguous, do not run the orchestration and ask the user to select this exact agent explicitly.`
|
|
495
637
|
: `The catalog declares that this agent is read-only; the orchestration omits confirmedSideEffects.`;
|
|
496
638
|
const source = renderCodexAdapterOrchestration(tenantId, agent);
|
|
639
|
+
const callerSpawnGuidance = usesVerbatimRelay(agent)
|
|
640
|
+
? adapterCallerSpawnGuidance("Codex")
|
|
641
|
+
: null;
|
|
642
|
+
if (usesDirectAnswer(agent)) {
|
|
643
|
+
const completionGuidance = usesVerbatimRelay(agent)
|
|
644
|
+
? adapterAnswerHardCompletionGuidance()
|
|
645
|
+
: adapterAnswerSoftCompletionGuidance();
|
|
646
|
+
return [
|
|
647
|
+
`You are a thin transport adapter for the exact Impel native agent ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
|
|
648
|
+
...(callerSpawnGuidance ? [callerSpawnGuidance] : []),
|
|
649
|
+
`Do not perform the assigned task yourself, do not delegate to any other agent, do not independently synthesize or rewrite the result, and do not call Impel start_native_agent_run or read_native_agent_run.`,
|
|
650
|
+
`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}`,
|
|
651
|
+
sideEffectInstruction,
|
|
652
|
+
`Invoke functions.exec exactly once for the orchestration below. Do not call the MCP tools directly. Replace ${CODEX_TASK_PLACEHOLDER} with a JSON string literal for the complete assigned task. Replace ${CODEX_CONTEXT_PLACEHOLDER} with one JSON string literal containing all supplied caller context, or with null when no context was supplied; never use an object or array. Replace ${CODEX_CONTEXT_KEYS_PLACEHOLDER} with a JSON array naming the context fields present in that string, or [] when context is absent. Replace ${CODEX_IDEMPOTENCY_KEY_PLACEHOLDER} with one new opaque idempotency key for this logical invocation (unused by the answer tool but kept for adapter parity): choose it exactly once. Then pass the raw JavaScript without Markdown fences.`,
|
|
653
|
+
`The JavaScript validates required context and calls Impel answer_native_agent exactly once, returning forUser (else answer). If functions.exec yields a running cell, use functions.wait with max_tokens 30000 only to resume that same orchestration; never start another orchestration or open an Impel/Eve session.`,
|
|
654
|
+
completionGuidance,
|
|
655
|
+
"",
|
|
656
|
+
source,
|
|
657
|
+
].join("\n\n");
|
|
658
|
+
}
|
|
659
|
+
const completionGuidance = usesVerbatimRelay(agent)
|
|
660
|
+
? adapterHardCompletionGuidance()
|
|
661
|
+
: adapterSoftCompletionGuidance();
|
|
497
662
|
return [
|
|
498
663
|
`You are a thin transport adapter for the exact Impel native agent ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
|
|
499
|
-
|
|
664
|
+
...(callerSpawnGuidance ? [callerSpawnGuidance] : []),
|
|
500
665
|
`Do not perform the assigned task yourself, do not delegate to any other agent, and do not independently synthesize or rewrite the result.`,
|
|
501
666
|
`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}`,
|
|
502
667
|
sideEffectInstruction,
|
|
503
668
|
`Invoke functions.exec exactly once for the orchestration below. Do not call the MCP tools directly or select a separate MCP call for any poll. Replace ${CODEX_TASK_PLACEHOLDER} with a JSON string literal for the complete assigned task. Replace ${CODEX_CONTEXT_PLACEHOLDER} with one JSON string literal containing all supplied caller context, or with null when no context was supplied; never use an object or array. Replace ${CODEX_CONTEXT_KEYS_PLACEHOLDER} with a JSON array naming the context fields present in that string, or [] when context is absent. Replace ${CODEX_IDEMPOTENCY_KEY_PLACEHOLDER} with one new opaque idempotency key for this logical invocation: choose it exactly once, reuse it unchanged for any retry of this invocation, and never reuse it for a separate request even when task and context are identical. Then pass the raw JavaScript without Markdown fences.`,
|
|
504
669
|
`The JavaScript validates the exact tenant, agent binding, and catalog policy; passes the one stable logical-invocation idempotencyKey; starts exactly once; and polls deterministically with the compatible 20-second server wait until status is succeeded or failed. If functions.exec yields a running cell, use functions.wait with max_tokens 30000 only to resume that same orchestration; never start another orchestration or poll the MCP tool yourself.`,
|
|
505
|
-
|
|
670
|
+
completionGuidance,
|
|
506
671
|
"",
|
|
507
672
|
source,
|
|
508
673
|
].join("\n\n");
|
|
509
674
|
}
|
|
510
675
|
|
|
676
|
+
function customAgentDescription(tenantId, agent) {
|
|
677
|
+
const sideEffectsLabel = agent.sideEffects === "writes" ? " (may write user or workspace data)" : " (read-only)";
|
|
678
|
+
return (usesVerbatimRelay(agent)
|
|
679
|
+
? `${customAgentVerbatimDescriptionLead()}. Runs ${agent.title} for Impel tenant ${tenantId}${sideEffectsLabel}: ${agent.description}`
|
|
680
|
+
: `Explicitly runs ${agent.title} for Impel tenant ${tenantId}${sideEffectsLabel}: ${agent.description}`
|
|
681
|
+
).slice(0, 900);
|
|
682
|
+
}
|
|
683
|
+
|
|
511
684
|
function renderClaudeAgent({ tenantId, agent, name, invocation }) {
|
|
512
|
-
const description =
|
|
685
|
+
const description = customAgentDescription(tenantId, agent);
|
|
686
|
+
const toolNames = nativeAgentToolNames(agent);
|
|
513
687
|
const lines = [
|
|
514
688
|
"---",
|
|
515
689
|
`name: ${JSON.stringify(name)}`,
|
|
516
690
|
`description: ${JSON.stringify(description)}`,
|
|
517
691
|
"model: inherit",
|
|
518
692
|
"tools:",
|
|
519
|
-
...
|
|
693
|
+
...toolNames.map((tool) => ` - ${JSON.stringify(nativeToolName(tool))}`),
|
|
520
694
|
"mcpServers:",
|
|
521
695
|
` - ${MANAGED_AGENT_MCP_SERVER}:`,
|
|
522
696
|
" type: stdio",
|
|
@@ -534,7 +708,8 @@ function renderClaudeAgent({ tenantId, agent, name, invocation }) {
|
|
|
534
708
|
}
|
|
535
709
|
|
|
536
710
|
function renderCodexAgent({ tenantId, agent, name, invocation }) {
|
|
537
|
-
const description =
|
|
711
|
+
const description = customAgentDescription(tenantId, agent);
|
|
712
|
+
const toolNames = nativeAgentToolNames(agent);
|
|
538
713
|
const envEntries = Object.entries(invocation.env || {})
|
|
539
714
|
.map(([key, value]) => `${JSON.stringify(key)} = ${JSON.stringify(value)}`)
|
|
540
715
|
.join(", ");
|
|
@@ -547,11 +722,11 @@ function renderCodexAgent({ tenantId, agent, name, invocation }) {
|
|
|
547
722
|
`[mcp_servers.${MANAGED_AGENT_MCP_SERVER}]`,
|
|
548
723
|
`command = ${JSON.stringify(invocation.command)}`,
|
|
549
724
|
`args = [${invocation.args.map((argument) => JSON.stringify(argument)).join(", ")}]`,
|
|
550
|
-
`enabled_tools = [${
|
|
725
|
+
`enabled_tools = [${toolNames.map((tool) => JSON.stringify(tool)).join(", ")}]`,
|
|
551
726
|
...(envEntries ? [`env = { ${envEntries} }`] : []),
|
|
552
727
|
"",
|
|
553
728
|
];
|
|
554
|
-
for (const tool of
|
|
729
|
+
for (const tool of toolNames) {
|
|
555
730
|
lines.push(`[mcp_servers.${MANAGED_AGENT_MCP_SERVER}.tools.${JSON.stringify(tool)}]`, 'approval_mode = "approve"', "");
|
|
556
731
|
}
|
|
557
732
|
return lines.join("\n");
|
|
@@ -628,7 +803,7 @@ export function syncAgentProfile({ client, root, label, tenantId, agents, now =
|
|
|
628
803
|
const prior = readManifest(manifestPath);
|
|
629
804
|
const rendered = renderManagedAgents(client, tenantId, agents);
|
|
630
805
|
const priorFiles = new Set(prior?.files || []);
|
|
631
|
-
const priorUsesDiscoveryRoot = [2, 3, MANAGED_AGENT_MANIFEST_VERSION].includes(prior?.version);
|
|
806
|
+
const priorUsesDiscoveryRoot = [2, 3, 4, 5, MANAGED_AGENT_MANIFEST_VERSION].includes(prior?.version);
|
|
632
807
|
|
|
633
808
|
// Native clients discover standalone definitions directly under `agents/`.
|
|
634
809
|
// Preflight every destination before writing so an unmanaged file with the
|
package/src/apps.js
CHANGED
|
@@ -10,12 +10,19 @@ import {
|
|
|
10
10
|
secureManagedCodexHome,
|
|
11
11
|
} from "./codexSecurity.js";
|
|
12
12
|
import { normalizeTenantId } from "./tenants.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
IMPEL_CLI_ENTRYPOINT,
|
|
15
|
+
IMPEL_TASKS_MCP_SERVER_NAME,
|
|
16
|
+
impelCliInvocation,
|
|
17
|
+
impelTasksMcpInvocation,
|
|
18
|
+
isImpelTasksMcpInvocation,
|
|
19
|
+
} from "./selfInvocation.js";
|
|
14
20
|
import { crossAppModelsEnabled, redactSecretText } from "./config.js";
|
|
15
21
|
import { ensureClaudeSessionHooks, ensureCodexSessionHooks } from "./sessionHooks.js";
|
|
16
22
|
import { ADHOC_IDENTITY, codesignIdentityArgs, desiredSigningMode, resolveSigningIdentity } from "./codesign.js";
|
|
17
23
|
import { applyImpelClaudeSandbox } from "./claudeSandbox.js";
|
|
18
24
|
import { brandedEnvironmentName, RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
25
|
+
import { IMPEL_CODEX_PARENT_DELEGATION_INSTRUCTIONS } from "./commands/launch.js";
|
|
19
26
|
|
|
20
27
|
export const CLAUDE_CONFIG_ID = "1ced0000-0000-4000-8000-000000000001";
|
|
21
28
|
const CHATGPT_CONFIG_START = `# >>> ${RUNTIME_BRAND.cli.command} app managed gateway >>>`;
|
|
@@ -266,7 +273,11 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
|
266
273
|
// 26: remember the managed provider defaults and move legacy GPT-5.5 profiles
|
|
267
274
|
// onto GPT-5.6 Sol so ChatGPT's compact Work picker does not fall back to its
|
|
268
275
|
// unsupported-selection "Reset to default" treatment.
|
|
269
|
-
|
|
276
|
+
// 27: register the separate tenant-bound Tasks MCP server in managed Claude
|
|
277
|
+
// and ChatGPT/Codex profiles without changing the specialist MCP server.
|
|
278
|
+
// 28: add managed ChatGPT parent delegation instructions, including custom-agent
|
|
279
|
+
// verbatim relay opt-in and directAnswer one-shot MCP answer routing.
|
|
280
|
+
export const CURRENT_CONFIG_VERSION = 28;
|
|
270
281
|
|
|
271
282
|
// Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
|
|
272
283
|
// helper rebranding, and signing. A vendored bundle is rebuilt only when this
|
|
@@ -807,7 +818,7 @@ export function installManagedAppFiles({
|
|
|
807
818
|
}
|
|
808
819
|
if (target === "claude") {
|
|
809
820
|
migrateLegacyClaudeAppSessions(paths.claude.userData, homeDir);
|
|
810
|
-
writeClaudeNative3PSelection(paths);
|
|
821
|
+
writeClaudeNative3PSelection(paths, config);
|
|
811
822
|
writeClaudeCodeSettings(paths);
|
|
812
823
|
writeClaudeConfig(paths, config, models);
|
|
813
824
|
if (RUNTIME_BRAND.features.sessions) ensureClaudeSessionHooks(paths.claude.userData, config.tenantId, "claude_desktop");
|
|
@@ -1153,7 +1164,7 @@ function writeClaudeCodeSettings(paths) {
|
|
|
1153
1164
|
writeAtomic(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 0o600);
|
|
1154
1165
|
}
|
|
1155
1166
|
|
|
1156
|
-
function writeClaudeNative3PSelection(paths) {
|
|
1167
|
+
function writeClaudeNative3PSelection(paths, config) {
|
|
1157
1168
|
const configPath = path.join(paths.claude.userData, "claude_desktop_config.json");
|
|
1158
1169
|
let current = {};
|
|
1159
1170
|
if (fs.existsSync(configPath)) {
|
|
@@ -1166,15 +1177,64 @@ function writeClaudeNative3PSelection(paths) {
|
|
|
1166
1177
|
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
1167
1178
|
throw new Error(`Claude desktop config must contain a JSON object: ${configPath}`);
|
|
1168
1179
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1180
|
+
if (Object.hasOwn(current, "mcpServers") && (
|
|
1181
|
+
!current.mcpServers
|
|
1182
|
+
|| typeof current.mcpServers !== "object"
|
|
1183
|
+
|| Array.isArray(current.mcpServers)
|
|
1184
|
+
)) {
|
|
1185
|
+
throw new Error(`Claude desktop config has an invalid mcpServers value: ${configPath}`);
|
|
1186
|
+
}
|
|
1187
|
+
const currentMcpServers = current.mcpServers
|
|
1188
|
+
&& typeof current.mcpServers === "object"
|
|
1189
|
+
&& !Array.isArray(current.mcpServers)
|
|
1190
|
+
? current.mcpServers
|
|
1191
|
+
: {};
|
|
1192
|
+
const currentTasksServer = currentMcpServers[IMPEL_TASKS_MCP_SERVER_NAME];
|
|
1193
|
+
if (RUNTIME_BRAND.features.mcp && config.tenantId) {
|
|
1194
|
+
if (Object.hasOwn(currentMcpServers, IMPEL_TASKS_MCP_SERVER_NAME)
|
|
1195
|
+
&& !isImpelTasksMcpInvocation(currentTasksServer)) {
|
|
1196
|
+
throw new Error(
|
|
1197
|
+
`${configPath} already has an mcpServers.${IMPEL_TASKS_MCP_SERVER_NAME} entry `
|
|
1198
|
+
+ "that wasn't written by impel-cli. Remove or rename it, then re-run."
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Mirror the pinned app's native "select 3P" persistence contract and add
|
|
1204
|
+
// only the tenant-bound Tasks entry we own. Claude-owned preferences and
|
|
1205
|
+
// foreign MCP entries remain untouched.
|
|
1173
1206
|
const next = { ...current, deploymentMode: "3p" };
|
|
1174
1207
|
delete next.awaitingSignIn;
|
|
1208
|
+
if (RUNTIME_BRAND.features.mcp && config.tenantId) {
|
|
1209
|
+
next.mcpServers = {
|
|
1210
|
+
...currentMcpServers,
|
|
1211
|
+
[IMPEL_TASKS_MCP_SERVER_NAME]: impelTasksMcpInvocation(config.tenantId),
|
|
1212
|
+
};
|
|
1213
|
+
} else if (isImpelTasksMcpInvocation(currentTasksServer)) {
|
|
1214
|
+
next.mcpServers = { ...currentMcpServers };
|
|
1215
|
+
delete next.mcpServers[IMPEL_TASKS_MCP_SERVER_NAME];
|
|
1216
|
+
if (Object.keys(next.mcpServers).length === 0) delete next.mcpServers;
|
|
1217
|
+
}
|
|
1175
1218
|
writeAtomic(configPath, `${JSON.stringify(next, null, 2)}\n`, 0o600);
|
|
1176
1219
|
}
|
|
1177
1220
|
|
|
1221
|
+
function tasksInvocationFromBaseMcp(mcp, tenantId) {
|
|
1222
|
+
if (!mcp || typeof mcp.command !== "string" || !Array.isArray(mcp.args)) {
|
|
1223
|
+
return impelTasksMcpInvocation(tenantId);
|
|
1224
|
+
}
|
|
1225
|
+
const mcpIndex = mcp.args.lastIndexOf("mcp");
|
|
1226
|
+
if (mcpIndex === -1) return impelTasksMcpInvocation(tenantId);
|
|
1227
|
+
return {
|
|
1228
|
+
command: mcp.command,
|
|
1229
|
+
args: [
|
|
1230
|
+
...mcp.args.slice(0, mcpIndex + 1),
|
|
1231
|
+
"--target",
|
|
1232
|
+
"tasks",
|
|
1233
|
+
...mcp.args.slice(mcpIndex + 1),
|
|
1234
|
+
],
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1178
1238
|
function writeChatGPTConfig(
|
|
1179
1239
|
paths,
|
|
1180
1240
|
config,
|
|
@@ -1256,6 +1316,25 @@ function writeChatGPTConfig(
|
|
|
1256
1316
|
command: mcpInvocation.command,
|
|
1257
1317
|
args: [...mcpInvocation.args],
|
|
1258
1318
|
};
|
|
1319
|
+
const tasksMcp = invocations?.tasksMcp
|
|
1320
|
+
|| tasksInvocationFromBaseMcp(mcp, config.tenantId);
|
|
1321
|
+
|
|
1322
|
+
if (RUNTIME_BRAND.features.mcp && config.tenantId) {
|
|
1323
|
+
const unmanagedToml = stripManagedChatGPTToml(currentToml);
|
|
1324
|
+
const tasksName = escapeRegex(IMPEL_TASKS_MCP_SERVER_NAME);
|
|
1325
|
+
const mcpServersKey = `(?:mcp_servers|"mcp_servers"|'mcp_servers')`;
|
|
1326
|
+
const tasksKey = `(?:${tasksName}|"${tasksName}"|'${tasksName}')`;
|
|
1327
|
+
const foreignTasksTable = new RegExp(
|
|
1328
|
+
`^[ \\t]*\\[[ \\t]*${mcpServersKey}[ \\t]*\\.[ \\t]*${tasksKey}(?:[ \\t]*\\.[ \\t]*|[ \\t]*\\])`,
|
|
1329
|
+
"mu",
|
|
1330
|
+
);
|
|
1331
|
+
if (foreignTasksTable.test(unmanagedToml)) {
|
|
1332
|
+
throw new Error(
|
|
1333
|
+
`${configPath} already has an mcp_servers.${IMPEL_TASKS_MCP_SERVER_NAME} table `
|
|
1334
|
+
+ "outside the impel-cli managed block. Remove or rename it, then re-run."
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1259
1338
|
|
|
1260
1339
|
const managedToml = [
|
|
1261
1340
|
CHATGPT_CONFIG_START,
|
|
@@ -1269,6 +1348,9 @@ function writeChatGPTConfig(
|
|
|
1269
1348
|
`model_catalog_json = ${tomlString(paths.chatgpt.catalog)}`,
|
|
1270
1349
|
...(selectedEffort ? [`model_reasoning_effort = ${tomlString(selectedEffort)}`] : []),
|
|
1271
1350
|
...(selectedTier ? [`service_tier = ${tomlString(selectedTier)}`] : []),
|
|
1351
|
+
...(RUNTIME_BRAND.features.agents
|
|
1352
|
+
? [`developer_instructions = ${tomlString(IMPEL_CODEX_PARENT_DELEGATION_INSTRUCTIONS)}`]
|
|
1353
|
+
: []),
|
|
1272
1354
|
"",
|
|
1273
1355
|
// The built-in ChatGPT provider derives its inference endpoint from
|
|
1274
1356
|
// chatgpt.com even when chatgpt_base_url points at the gateway. Keep the
|
|
@@ -1296,6 +1378,12 @@ function writeChatGPTConfig(
|
|
|
1296
1378
|
`[mcp_servers.${RUNTIME_BRAND.cli.providerId}]`,
|
|
1297
1379
|
`command = ${tomlString(mcp.command)}`,
|
|
1298
1380
|
`args = [${mcp.args.map((argument) => tomlString(argument)).join(", ")}]`,
|
|
1381
|
+
...(config.tenantId ? [
|
|
1382
|
+
"",
|
|
1383
|
+
`[mcp_servers.${IMPEL_TASKS_MCP_SERVER_NAME}]`,
|
|
1384
|
+
`command = ${tomlString(tasksMcp.command)}`,
|
|
1385
|
+
`args = [${tasksMcp.args.map((argument) => tomlString(argument)).join(", ")}]`,
|
|
1386
|
+
] : []),
|
|
1299
1387
|
] : []),
|
|
1300
1388
|
CHATGPT_CONFIG_END,
|
|
1301
1389
|
].join("\n");
|
|
@@ -1469,7 +1557,7 @@ function readVendorCodexModels(vendorPath) {
|
|
|
1469
1557
|
return new Map();
|
|
1470
1558
|
}
|
|
1471
1559
|
|
|
1472
|
-
function
|
|
1560
|
+
function stripManagedChatGPTToml(current) {
|
|
1473
1561
|
let remainder = current;
|
|
1474
1562
|
const start = current.indexOf(CHATGPT_CONFIG_START);
|
|
1475
1563
|
const end = current.indexOf(CHATGPT_CONFIG_END);
|
|
@@ -1479,7 +1567,11 @@ function mergeManagedChatGPTToml(current, managed) {
|
|
|
1479
1567
|
const legacyEnd = current.match(/refresh_interval_ms = \d+\r?\n/u);
|
|
1480
1568
|
if (legacyEnd?.index != null) remainder = current.slice(legacyEnd.index + legacyEnd[0].length);
|
|
1481
1569
|
}
|
|
1482
|
-
|
|
1570
|
+
return remainder.replace(/^\s+/u, "");
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
function mergeManagedChatGPTToml(current, managed) {
|
|
1574
|
+
const remainder = stripManagedChatGPTToml(current);
|
|
1483
1575
|
return `${managed}\n${remainder ? `\n${remainder.replace(/\s*$/u, "")}\n` : ""}`;
|
|
1484
1576
|
}
|
|
1485
1577
|
|