impel-cli 0.18.16 → 0.18.18-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/package.json +1 -1
- package/src/agents.js +37 -7
- package/src/apps.js +114 -2
- package/src/commands/launch.js +11 -2
- package/src/verbatimRelay.js +64 -0
package/package.json
CHANGED
package/src/agents.js
CHANGED
|
@@ -12,6 +12,15 @@ import path from "node:path";
|
|
|
12
12
|
import { normalizeGatewayUrl, redactSecretText } from "./config.js";
|
|
13
13
|
import { impelMcpInvocation } from "./selfInvocation.js";
|
|
14
14
|
import { normalizeTenantId } from "./tenants.js";
|
|
15
|
+
import {
|
|
16
|
+
adapterCallerSpawnGuidance,
|
|
17
|
+
adapterHardCompletionGuidance,
|
|
18
|
+
adapterSoftCompletionGuidance,
|
|
19
|
+
claudeHardCompletionGuidance,
|
|
20
|
+
claudeSoftCompletionGuidance,
|
|
21
|
+
customAgentVerbatimDescriptionLead,
|
|
22
|
+
usesVerbatimRelay,
|
|
23
|
+
} from "./verbatimRelay.js";
|
|
15
24
|
import { renameWithWindowsRetry } from "./windowsFs.js";
|
|
16
25
|
|
|
17
26
|
export const AGENT_SYNC_TTL_MS = 6 * 60 * 60 * 1000;
|
|
@@ -21,7 +30,7 @@ export const NATIVE_AGENT_LIST_TOOL = "impel_specialists-list_native_agents";
|
|
|
21
30
|
export const NATIVE_AGENT_START_TOOL = "impel_specialists-start_native_agent_run";
|
|
22
31
|
export const NATIVE_AGENT_READ_TOOL = "impel_specialists-read_native_agent_run";
|
|
23
32
|
export const MANAGED_AGENT_MCP_SERVER = "impel_agent";
|
|
24
|
-
export const MANAGED_AGENT_MANIFEST_VERSION =
|
|
33
|
+
export const MANAGED_AGENT_MANIFEST_VERSION = 5;
|
|
25
34
|
|
|
26
35
|
const NATIVE_AGENT_TOOL_NAMES = [
|
|
27
36
|
NATIVE_AGENT_LIST_TOOL,
|
|
@@ -125,6 +134,9 @@ export function normalizeNativeAgentCatalog(payload, expectedTenantId) {
|
|
|
125
134
|
throw new Error(`native-agent catalog returned duplicate binding ${binding}`);
|
|
126
135
|
}
|
|
127
136
|
seenBindings.add(binding);
|
|
137
|
+
if (agent.verbatimRelay !== undefined && typeof agent.verbatimRelay !== "boolean") {
|
|
138
|
+
throw new Error("native-agent catalog returned an invalid verbatimRelay");
|
|
139
|
+
}
|
|
128
140
|
return {
|
|
129
141
|
agentId,
|
|
130
142
|
title: boundedString(agent.title, "title", { max: 160 }),
|
|
@@ -135,6 +147,7 @@ export function normalizeNativeAgentCatalog(payload, expectedTenantId) {
|
|
|
135
147
|
exclusions: stringList(agent.exclusions, "exclusions"),
|
|
136
148
|
requiredContext: stringList(agent.requiredContext, "requiredContext"),
|
|
137
149
|
sideEffects: enumString(agent.sideEffects, "sideEffects", ["read-only", "writes"]),
|
|
150
|
+
...(agent.verbatimRelay === true ? { verbatimRelay: true } : {}),
|
|
138
151
|
};
|
|
139
152
|
});
|
|
140
153
|
return { orgId: tenantId, agents };
|
|
@@ -329,6 +342,9 @@ function claudeAdapterInstructions(tenantId, agent) {
|
|
|
329
342
|
const sideEffectInstruction = agent.sideEffects === "writes"
|
|
330
343
|
? `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
344
|
: `The catalog declares that this agent is read-only; omit confirmedSideEffects.`;
|
|
345
|
+
const completionGuidance = usesVerbatimRelay(agent)
|
|
346
|
+
? claudeHardCompletionGuidance()
|
|
347
|
+
: claudeSoftCompletionGuidance();
|
|
332
348
|
return [
|
|
333
349
|
`You are a thin transport adapter for the exact Impel native agent ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
|
|
334
350
|
`Do not perform the assigned task yourself and do not delegate to any other agent.`,
|
|
@@ -336,7 +352,7 @@ function claudeAdapterInstructions(tenantId, agent) {
|
|
|
336
352
|
sideEffectInstruction,
|
|
337
353
|
`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
354
|
`Then call ${toolName(NATIVE_AGENT_READ_TOOL)} with the returned runId and waitSeconds 20 until the run reaches a terminal state.`,
|
|
339
|
-
|
|
355
|
+
completionGuidance,
|
|
340
356
|
].join(" ");
|
|
341
357
|
}
|
|
342
358
|
|
|
@@ -494,22 +510,36 @@ function codexAdapterInstructions(tenantId, agent) {
|
|
|
494
510
|
? `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
511
|
: `The catalog declares that this agent is read-only; the orchestration omits confirmedSideEffects.`;
|
|
496
512
|
const source = renderCodexAdapterOrchestration(tenantId, agent);
|
|
513
|
+
const callerSpawnGuidance = usesVerbatimRelay(agent)
|
|
514
|
+
? adapterCallerSpawnGuidance("Codex")
|
|
515
|
+
: null;
|
|
516
|
+
const completionGuidance = usesVerbatimRelay(agent)
|
|
517
|
+
? adapterHardCompletionGuidance()
|
|
518
|
+
: adapterSoftCompletionGuidance();
|
|
497
519
|
return [
|
|
498
520
|
`You are a thin transport adapter for the exact Impel native agent ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
|
|
499
|
-
|
|
521
|
+
...(callerSpawnGuidance ? [callerSpawnGuidance] : []),
|
|
500
522
|
`Do not perform the assigned task yourself, do not delegate to any other agent, and do not independently synthesize or rewrite the result.`,
|
|
501
523
|
`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
524
|
sideEffectInstruction,
|
|
503
525
|
`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
526
|
`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
|
-
|
|
527
|
+
completionGuidance,
|
|
506
528
|
"",
|
|
507
529
|
source,
|
|
508
530
|
].join("\n\n");
|
|
509
531
|
}
|
|
510
532
|
|
|
533
|
+
function customAgentDescription(tenantId, agent) {
|
|
534
|
+
const sideEffectsLabel = agent.sideEffects === "writes" ? " (may write user or workspace data)" : " (read-only)";
|
|
535
|
+
return (usesVerbatimRelay(agent)
|
|
536
|
+
? `${customAgentVerbatimDescriptionLead()}. Runs ${agent.title} for Impel tenant ${tenantId}${sideEffectsLabel}: ${agent.description}`
|
|
537
|
+
: `Explicitly runs ${agent.title} for Impel tenant ${tenantId}${sideEffectsLabel}: ${agent.description}`
|
|
538
|
+
).slice(0, 900);
|
|
539
|
+
}
|
|
540
|
+
|
|
511
541
|
function renderClaudeAgent({ tenantId, agent, name, invocation }) {
|
|
512
|
-
const description =
|
|
542
|
+
const description = customAgentDescription(tenantId, agent);
|
|
513
543
|
const lines = [
|
|
514
544
|
"---",
|
|
515
545
|
`name: ${JSON.stringify(name)}`,
|
|
@@ -534,7 +564,7 @@ function renderClaudeAgent({ tenantId, agent, name, invocation }) {
|
|
|
534
564
|
}
|
|
535
565
|
|
|
536
566
|
function renderCodexAgent({ tenantId, agent, name, invocation }) {
|
|
537
|
-
const description =
|
|
567
|
+
const description = customAgentDescription(tenantId, agent);
|
|
538
568
|
const envEntries = Object.entries(invocation.env || {})
|
|
539
569
|
.map(([key, value]) => `${JSON.stringify(key)} = ${JSON.stringify(value)}`)
|
|
540
570
|
.join(", ");
|
|
@@ -628,7 +658,7 @@ export function syncAgentProfile({ client, root, label, tenantId, agents, now =
|
|
|
628
658
|
const prior = readManifest(manifestPath);
|
|
629
659
|
const rendered = renderManagedAgents(client, tenantId, agents);
|
|
630
660
|
const priorFiles = new Set(prior?.files || []);
|
|
631
|
-
const priorUsesDiscoveryRoot = [2, 3, MANAGED_AGENT_MANIFEST_VERSION].includes(prior?.version);
|
|
661
|
+
const priorUsesDiscoveryRoot = [2, 3, 4, MANAGED_AGENT_MANIFEST_VERSION].includes(prior?.version);
|
|
632
662
|
|
|
633
663
|
// Native clients discover standalone definitions directly under `agents/`.
|
|
634
664
|
// Preflight every destination before writing so an unmanaged file with the
|
package/src/apps.js
CHANGED
|
@@ -16,6 +16,7 @@ import { ensureClaudeSessionHooks, ensureCodexSessionHooks } from "./sessionHook
|
|
|
16
16
|
import { ADHOC_IDENTITY, codesignIdentityArgs, desiredSigningMode, resolveSigningIdentity } from "./codesign.js";
|
|
17
17
|
import { applyImpelClaudeSandbox } from "./claudeSandbox.js";
|
|
18
18
|
import { brandedEnvironmentName, RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
19
|
+
import { IMPEL_CODEX_PARENT_DELEGATION_INSTRUCTIONS } from "./commands/launch.js";
|
|
19
20
|
|
|
20
21
|
export const CLAUDE_CONFIG_ID = "1ced0000-0000-4000-8000-000000000001";
|
|
21
22
|
const CHATGPT_CONFIG_START = `# >>> ${RUNTIME_BRAND.cli.command} app managed gateway >>>`;
|
|
@@ -130,6 +131,10 @@ const MAX_REASONING_LEVEL = { effort: "max", description: "Maximum reasoning dep
|
|
|
130
131
|
const ULTRA_REASONING_LEVEL = { effort: "ultra", description: "Maximum reasoning with automatic task delegation" };
|
|
131
132
|
const FAST_SERVICE_TIER = { id: "priority", name: "Fast", description: "1.5x speed, increased usage" };
|
|
132
133
|
const FAST_MODE_AUTH_GATE = /([A-Za-z_$][\w$]*)=([A-Za-z_$][\w$]*)&&!([A-Za-z_$][\w$]*)&&([A-Za-z_$][\w$]*)!=null&&\4\?\.requirements\?\.featureRequirements\?\.fast_mode!==!1/gu;
|
|
134
|
+
const CHATGPT_THREAD_START_PROVIDER = /serviceName:([A-Za-z_$][\w$]*)\.serviceName\?\?this\.options\.defaultServiceName,threadSource:\1\.threadSource===void 0\?`user`:\1\.threadSource/gu;
|
|
135
|
+
const CHATGPT_THREAD_FORK_PROVIDER = /let ([A-Za-z_$][\w$]*)=\{\.\.\.([A-Za-z_$][\w$]*),threadSource:\2\.threadSource===void 0\?`user`:\2\.threadSource\}/gu;
|
|
136
|
+
const CHATGPT_THREAD_RESUME_PROVIDER = /modelProvider:null,cwd:([A-Za-z_$][\w$]*)\?\.cwd\?\?null,approvalPolicy:null,sandbox:null,config:\1\?\.config\?\?null,developerInstructions:\1\?\.developerInstructions\?\?void 0/gu;
|
|
137
|
+
const CHATGPT_STRUCTURED_PROVIDER_FALLBACK = /allowProviderModelFallback:!0/gu;
|
|
133
138
|
const CHATGPT_RESOURCE_ROOT = "linked_vendor_resources_with_patched_asar";
|
|
134
139
|
const CLAUDE_PLAN_USAGE_GUARD = "if(!PSe()){cz();return}if(!Ge().hasOrgPolicyBackend())return;if((r=ar())";
|
|
135
140
|
const IMPEL_CLAUDE_PLAN_USAGE_GUARD = `if(!PSe()){cz();return}${"".padEnd("if(!Ge().hasOrgPolicyBackend())return;".length, " ")}if((r=ar())`;
|
|
@@ -262,7 +267,8 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
|
262
267
|
// 26: remember the managed provider defaults and move legacy GPT-5.5 profiles
|
|
263
268
|
// onto GPT-5.6 Sol so ChatGPT's compact Work picker does not fall back to its
|
|
264
269
|
// unsupported-selection "Reset to default" treatment.
|
|
265
|
-
|
|
270
|
+
// 27: add managed ChatGPT parent delegation instructions, including custom-agent verbatim relay opt-in.
|
|
271
|
+
export const CURRENT_CONFIG_VERSION = 27;
|
|
266
272
|
|
|
267
273
|
// Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
|
|
268
274
|
// helper rebranding, and signing. A vendored bundle is rebuilt only when this
|
|
@@ -271,7 +277,7 @@ export const CURRENT_CONFIG_VERSION = 26;
|
|
|
271
277
|
// — which is what made every `impel update` re-trigger macOS permission
|
|
272
278
|
// prompts. Bump this ONLY when a code change alters the bytes of a built
|
|
273
279
|
// bundle; leave it alone for changes that don't touch bundle contents.
|
|
274
|
-
export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-
|
|
280
|
+
export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-08-01.1";
|
|
275
281
|
|
|
276
282
|
/** Parse the tenant's install manifest, or null when absent/corrupt. */
|
|
277
283
|
export function readTenantManifest(homeDir = os.homedir(), tenantId = null) {
|
|
@@ -349,6 +355,10 @@ export function bundleIsCurrent(status, {
|
|
|
349
355
|
compatibility.vendorSignaturesPreserved === true
|
|
350
356
|
&& compatibility.resourceRoot === CHATGPT_RESOURCE_ROOT
|
|
351
357
|
&& compatibility.patches?.fastModeForGatewayAuth > 0
|
|
358
|
+
&& compatibility.patches?.threadStartProviderOverride === 1
|
|
359
|
+
&& compatibility.patches?.threadForkProviderOverride === 1
|
|
360
|
+
&& compatibility.patches?.threadResumeProviderOverride === 1
|
|
361
|
+
&& compatibility.patches?.structuredProviderFallbackDisabled === 2
|
|
352
362
|
&& compatibility.patches?.tenantDisplayNamePreload === 2
|
|
353
363
|
)),
|
|
354
364
|
);
|
|
@@ -1261,6 +1271,9 @@ function writeChatGPTConfig(
|
|
|
1261
1271
|
`model_catalog_json = ${tomlString(paths.chatgpt.catalog)}`,
|
|
1262
1272
|
...(selectedEffort ? [`model_reasoning_effort = ${tomlString(selectedEffort)}`] : []),
|
|
1263
1273
|
...(selectedTier ? [`service_tier = ${tomlString(selectedTier)}`] : []),
|
|
1274
|
+
...(RUNTIME_BRAND.features.agents
|
|
1275
|
+
? [`developer_instructions = ${tomlString(IMPEL_CODEX_PARENT_DELEGATION_INSTRUCTIONS)}`]
|
|
1276
|
+
: []),
|
|
1264
1277
|
"",
|
|
1265
1278
|
// The built-in ChatGPT provider derives its inference endpoint from
|
|
1266
1279
|
// chatgpt.com even when chatgpt_base_url points at the gateway. Keep the
|
|
@@ -1729,6 +1742,8 @@ function writeVendoredChatGPTBundleLocked(paths, vendorPath, gatewayUrl, homeDir
|
|
|
1729
1742
|
linkVendoredChatGPTResources(staging, vendorBundleName);
|
|
1730
1743
|
const runtimeAsarPath = path.join(staging, "Contents", "Resources", "app.asar");
|
|
1731
1744
|
const fastModePatchCount = patchFastModeAuthGate(runtimeAsarPath);
|
|
1745
|
+
const threadProviderPatches = patchChatGPTThreadRouting(runtimeAsarPath);
|
|
1746
|
+
const structuredFallbackPatchCount = patchChatGPTStructuredProviderFallback(runtimeAsarPath);
|
|
1732
1747
|
const asarHash = asarHeaderHash(runtimeAsarPath);
|
|
1733
1748
|
const vendorExecutable = path.join(vendorBundle, "Contents", "MacOS", executableName);
|
|
1734
1749
|
const signingIdentity = resolveVendoredAppSigningIdentity(vendorExecutable);
|
|
@@ -1773,6 +1788,10 @@ function writeVendoredChatGPTBundleLocked(paths, vendorPath, gatewayUrl, homeDir
|
|
|
1773
1788
|
browserData: paths.chatgpt.browserData,
|
|
1774
1789
|
patches: {
|
|
1775
1790
|
fastModeForGatewayAuth: fastModePatchCount,
|
|
1791
|
+
threadStartProviderOverride: threadProviderPatches.start,
|
|
1792
|
+
threadForkProviderOverride: threadProviderPatches.fork,
|
|
1793
|
+
threadResumeProviderOverride: threadProviderPatches.resume,
|
|
1794
|
+
structuredProviderFallbackDisabled: structuredFallbackPatchCount,
|
|
1776
1795
|
desktopAPIForGatewayAuth: 0,
|
|
1777
1796
|
tenantDisplayNamePreload: 2,
|
|
1778
1797
|
},
|
|
@@ -1995,6 +2014,99 @@ function patchFastModeAuthGate(asarPath) {
|
|
|
1995
2014
|
return applyFixedWidthAsarPatches(asarPath, archive, patches, "ChatGPT/Codex Fast-mode eligibility contract");
|
|
1996
2015
|
}
|
|
1997
2016
|
|
|
2017
|
+
/**
|
|
2018
|
+
* Force every desktop app-server thread request onto the tenant's configured
|
|
2019
|
+
* Impel provider. The pinned renderer passes null for internal metadata turns,
|
|
2020
|
+
* which otherwise lets those turns escape to native OpenAI authentication.
|
|
2021
|
+
*/
|
|
2022
|
+
function patchChatGPTThreadRouting(asarPath) {
|
|
2023
|
+
const archive = fs.readFileSync(asarPath);
|
|
2024
|
+
const source = archive.toString("latin1");
|
|
2025
|
+
const provider = JSON.stringify(RUNTIME_BRAND.cli.providerId);
|
|
2026
|
+
const contracts = [
|
|
2027
|
+
{
|
|
2028
|
+
name: "start",
|
|
2029
|
+
pattern: CHATGPT_THREAD_START_PROVIDER,
|
|
2030
|
+
replacement: (match) => (
|
|
2031
|
+
`modelProvider:${provider},serviceName:${match[1]}.serviceName??this.options.defaultServiceName,`
|
|
2032
|
+
+ `threadSource:${match[1]}.threadSource??\`user\``
|
|
2033
|
+
),
|
|
2034
|
+
},
|
|
2035
|
+
{
|
|
2036
|
+
name: "fork",
|
|
2037
|
+
pattern: CHATGPT_THREAD_FORK_PROVIDER,
|
|
2038
|
+
replacement: (match) => (
|
|
2039
|
+
`let ${match[1]}={...${match[2]},modelProvider:${provider},`
|
|
2040
|
+
+ `threadSource:${match[2]}.threadSource??\`user\`}`
|
|
2041
|
+
),
|
|
2042
|
+
},
|
|
2043
|
+
{
|
|
2044
|
+
name: "resume",
|
|
2045
|
+
pattern: CHATGPT_THREAD_RESUME_PROVIDER,
|
|
2046
|
+
replacement: (match) => (
|
|
2047
|
+
`modelProvider:${provider},cwd:${match[1]}?.cwd??null,approvalPolicy:null,sandbox:null,`
|
|
2048
|
+
+ `config:${match[1]}?.config??null,developerInstructions:${match[1]}?.developerInstructions`
|
|
2049
|
+
),
|
|
2050
|
+
},
|
|
2051
|
+
];
|
|
2052
|
+
const counts = {};
|
|
2053
|
+
const patches = [];
|
|
2054
|
+
for (const contract of contracts) {
|
|
2055
|
+
const matches = [...source.matchAll(contract.pattern)];
|
|
2056
|
+
if (matches.length !== 1) {
|
|
2057
|
+
throw new Error(
|
|
2058
|
+
`ChatGPT/Codex thread-${contract.name} provider patch expected 1 match, found ${matches.length}`,
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
const match = matches[0];
|
|
2062
|
+
const replacement = contract.replacement(match);
|
|
2063
|
+
if (replacement.length > match[0].length) {
|
|
2064
|
+
throw new Error(
|
|
2065
|
+
`ChatGPT/Codex thread-${contract.name} provider replacement no longer fits the vendor ASAR contract`,
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
patches.push({
|
|
2069
|
+
offset: match.index,
|
|
2070
|
+
original: match[0],
|
|
2071
|
+
replacement: replacement.padEnd(match[0].length, " "),
|
|
2072
|
+
});
|
|
2073
|
+
counts[contract.name] = 1;
|
|
2074
|
+
}
|
|
2075
|
+
applyFixedWidthAsarPatches(
|
|
2076
|
+
asarPath,
|
|
2077
|
+
archive,
|
|
2078
|
+
patches,
|
|
2079
|
+
"ChatGPT/Codex thread-provider routing contract",
|
|
2080
|
+
);
|
|
2081
|
+
return counts;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
/**
|
|
2085
|
+
* Structured title, description, and classification turns opt into falling
|
|
2086
|
+
* back to another provider in the pinned renderer. Keep those turns fail
|
|
2087
|
+
* closed on Impel by changing the two reviewed boolean literals in place.
|
|
2088
|
+
*/
|
|
2089
|
+
function patchChatGPTStructuredProviderFallback(asarPath) {
|
|
2090
|
+
const archive = fs.readFileSync(asarPath);
|
|
2091
|
+
const source = archive.toString("latin1");
|
|
2092
|
+
const matches = [...source.matchAll(CHATGPT_STRUCTURED_PROVIDER_FALLBACK)];
|
|
2093
|
+
if (matches.length !== 2) {
|
|
2094
|
+
throw new Error(
|
|
2095
|
+
`ChatGPT/Codex structured provider-fallback patch expected 2 matches, found ${matches.length}`,
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
return applyFixedWidthAsarPatches(
|
|
2099
|
+
asarPath,
|
|
2100
|
+
archive,
|
|
2101
|
+
matches.map((match) => ({
|
|
2102
|
+
offset: match.index,
|
|
2103
|
+
original: match[0],
|
|
2104
|
+
replacement: "allowProviderModelFallback:!1",
|
|
2105
|
+
})),
|
|
2106
|
+
"ChatGPT/Codex structured provider-fallback contract",
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
|
|
1998
2110
|
function applyFixedWidthAsarPatches(asarPath, archive, patches, label) {
|
|
1999
2111
|
if (patches.length === 0) {
|
|
2000
2112
|
throw new Error(`this desktop app version has an unsupported ${label}; update impel-cli before installing it`);
|
package/src/commands/launch.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
resolveDefaultGateway,
|
|
15
15
|
saveConfig,
|
|
16
16
|
} from "../config.js";
|
|
17
|
+
import { parentVerbatimRelayAppendix } from "../verbatimRelay.js";
|
|
17
18
|
import { impelClaudeBaseUrl, impelCrossAppClaudeBaseUrl } from "../claudeSetup.js";
|
|
18
19
|
import { withGitEnvironment } from "../skills.js";
|
|
19
20
|
import { assertProviderScopes, ensureTenantSelection, tenantCredential } from "../tenants.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
|
];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const VERBATIM_RELAY_OPT_IN_MARKER = "verbatimRelay enabled";
|
|
2
|
+
|
|
3
|
+
export const VERBATIM_SPAWN_REQUIREMENT = 'fork_turns="none"';
|
|
4
|
+
|
|
5
|
+
export const VERBATIM_FINAL_TEXT_CONSTRAINTS =
|
|
6
|
+
"no preface, rewriting, Markdown changes, or independent synthesis";
|
|
7
|
+
|
|
8
|
+
export function usesVerbatimRelay(agent) {
|
|
9
|
+
return agent?.verbatimRelay === true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function parentVerbatimRelayAppendix() {
|
|
13
|
+
return (
|
|
14
|
+
`When an explicit custom agent's catalog-derived description declares that verbatimRelay is enabled, ` +
|
|
15
|
+
`spawn it with ${VERBATIM_SPAWN_REQUIREMENT} and relay its finalText verbatim with ${VERBATIM_FINAL_TEXT_CONSTRAINTS}; ` +
|
|
16
|
+
"preserve Sources sections and citations exactly. " +
|
|
17
|
+
"Custom agents without that declaration keep the default delegation behavior."
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function customAgentVerbatimDescriptionLead() {
|
|
22
|
+
return (
|
|
23
|
+
`Explicit custom agent with ${VERBATIM_RELAY_OPT_IN_MARKER}: ` +
|
|
24
|
+
`callers must use ${VERBATIM_SPAWN_REQUIREMENT} and relay its result verbatim`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function adapterCallerSpawnGuidance(clientLabel) {
|
|
29
|
+
return (
|
|
30
|
+
`Callers must spawn this explicit custom ${clientLabel} agent with ${VERBATIM_SPAWN_REQUIREMENT} ` +
|
|
31
|
+
"and must relay your result verbatim; this is caller guidance and cannot enforce host spawn behavior."
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function adapterHardCompletionGuidance() {
|
|
36
|
+
return (
|
|
37
|
+
`After the orchestration completes, return its single text output verbatim with ${VERBATIM_FINAL_TEXT_CONSTRAINTS}. ` +
|
|
38
|
+
"A successful output is result.finalText exactly. A failure output preserves the durable runId, output, and error."
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function adapterSoftCompletionGuidance() {
|
|
43
|
+
return (
|
|
44
|
+
"After the orchestration completes, return its single text output as the answer. " +
|
|
45
|
+
"A successful output is result.finalText. A failure output preserves the durable runId, output, and error. " +
|
|
46
|
+
"Never invent or independently synthesize a replacement result."
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function claudeHardCompletionGuidance() {
|
|
51
|
+
return (
|
|
52
|
+
`When it succeeds, return result.finalText exactly with ${VERBATIM_FINAL_TEXT_CONSTRAINTS}. ` +
|
|
53
|
+
"When it fails, return the durable runId, preserved output, and error. " +
|
|
54
|
+
"Never invent or independently synthesize a replacement result."
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function claudeSoftCompletionGuidance() {
|
|
59
|
+
return (
|
|
60
|
+
"When it succeeds, return result.finalText faithfully as the answer. " +
|
|
61
|
+
"When it fails, return the durable runId, preserved output, and error. " +
|
|
62
|
+
"Never invent or independently synthesize a replacement result."
|
|
63
|
+
);
|
|
64
|
+
}
|