impel-cli 0.20.40 → 0.20.42
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/RELEASE_NOTES.md +15 -0
- package/package.json +1 -1
- package/src/agents.js +367 -41
- package/src/apps.js +2 -1
- package/src/cli.js +6 -0
- package/src/commands/apps.js +4 -2
- package/src/commands/launch.js +26 -0
- package/src/commands/mcp.js +86 -3
- package/src/commands/native.js +285 -0
- package/src/commands/status.js +9 -4
- package/src/managedProfileVersion.js +4 -0
- package/src/nativeAgentTelemetry.js +4 -0
- package/src/nativeAgentTransport.js +138 -15
- package/src/nativeInterception.js +133 -0
- package/src/windowsApps.js +122 -10
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Release notes
|
|
2
2
|
|
|
3
|
+
## 0.20.41 — Windows onboarding: converge over an already-installed Store ChatGPT
|
|
4
|
+
|
|
5
|
+
- Accepts winget's UPDATE_NOT_APPLICABLE result during setup as well as
|
|
6
|
+
update, gated by the pin-exact package probe, ending the unconvergeable
|
|
7
|
+
first-run failure loop on machines where the ChatGPT Store app already
|
|
8
|
+
exists (and, downstream, the managed sign-in loop it caused).
|
|
9
|
+
- Registers a machine-wide staged Store package for the current user
|
|
10
|
+
(download-free, no elevation) when winget sees it but the per-user probe
|
|
11
|
+
does not, still failing closed through the pin-exact probe.
|
|
12
|
+
- Resolves the app-open path strictly through the pinned finder so an
|
|
13
|
+
off-pin Store build can never be staged as the managed app.
|
|
14
|
+
- Names installed-vs-pinned versions in vendor-install diagnostics with a
|
|
15
|
+
measured direction, adds the 0x8A15002B winget hint, and makes Windows
|
|
16
|
+
status require the staged managed app rather than config presence alone.
|
|
17
|
+
|
|
3
18
|
## 0.20.40 — Field-report fixes: launcher drift, sync ownership, Codex capability, one-run update
|
|
4
19
|
|
|
5
20
|
- Survives Claude auto-update drift: `impel claude` resolves the exact reviewed
|
package/package.json
CHANGED
package/src/agents.js
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV,
|
|
22
22
|
IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES,
|
|
23
23
|
IMPEL_NATIVE_AGENT_MCP_TARGET,
|
|
24
|
+
impelCliInvocation,
|
|
24
25
|
impelNativeAgentMcpInvocation,
|
|
25
26
|
} from "./selfInvocation.js";
|
|
26
27
|
import {
|
|
@@ -31,6 +32,11 @@ import {
|
|
|
31
32
|
usesDirectAnswer,
|
|
32
33
|
} from "./directAnswer.js";
|
|
33
34
|
import { normalizeTenantId } from "./tenants.js";
|
|
35
|
+
import {
|
|
36
|
+
ensureClaudeNativeInterceptHook,
|
|
37
|
+
nativeInterceptEnabled,
|
|
38
|
+
nativeInterceptTimeoutMs,
|
|
39
|
+
} from "./nativeInterception.js";
|
|
34
40
|
import {
|
|
35
41
|
adapterCallerSpawnGuidance,
|
|
36
42
|
adapterFaithfulCompletionGuidance,
|
|
@@ -56,7 +62,9 @@ export const NATIVE_AGENT_RESUME_TOOL = "resume_native_agent_run";
|
|
|
56
62
|
export const NATIVE_AGENT_RECOVER_TOOL = "recover_native_agent_runs";
|
|
57
63
|
export const NATIVE_AGENT_CONTINUATION_SCHEMA = "impel.native-agent-continuation.v1";
|
|
58
64
|
export const MANAGED_AGENT_MCP_SERVER = "impel_agent";
|
|
59
|
-
export const MANAGED_AGENT_MANIFEST_VERSION =
|
|
65
|
+
export const MANAGED_AGENT_MANIFEST_VERSION = 23;
|
|
66
|
+
export const IMPEL_NATIVE_PARENT_DIRECT_ENV = "IMPEL_NATIVE_PARENT_DIRECT";
|
|
67
|
+
export const IMPEL_NATIVE_SLASH_COMMANDS_ENV = "IMPEL_NATIVE_SLASH_COMMANDS";
|
|
60
68
|
|
|
61
69
|
// The host model only selects the fixed MCP tool and faithfully returns its
|
|
62
70
|
// result. Spark minimizes those transport-only turns while the selected Eve
|
|
@@ -77,6 +85,18 @@ function nativeAgentToolNames(agent) {
|
|
|
77
85
|
function eagerNativeAgentTransportEnabled() {
|
|
78
86
|
return process.env.IMPEL_NATIVE_EAGER_TRANSPORT !== "0";
|
|
79
87
|
}
|
|
88
|
+
|
|
89
|
+
function enabledProfileFlag(environment, name) {
|
|
90
|
+
return ["1", "true"].includes(String(environment?.[name] || "").toLowerCase());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function nativeParentDirectEnabled(environment = process.env) {
|
|
94
|
+
return enabledProfileFlag(environment, IMPEL_NATIVE_PARENT_DIRECT_ENV);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function nativeSlashCommandsEnabled(environment = process.env) {
|
|
98
|
+
return enabledProfileFlag(environment, IMPEL_NATIVE_SLASH_COMMANDS_ENV);
|
|
99
|
+
}
|
|
80
100
|
const MAX_RETIRED_AGENT_BINDINGS = 50;
|
|
81
101
|
const MAX_NATIVE_AGENT_STATE_BYTES = 512 * 1024;
|
|
82
102
|
const TERMINAL_NATIVE_AGENT_STATUSES = new Set(["succeeded", "failed", "cancelled", "canceled"]);
|
|
@@ -780,6 +800,32 @@ function renderClaudeAgent({ tenantId, agent, name, invocation, recoveryOnly = f
|
|
|
780
800
|
};
|
|
781
801
|
}
|
|
782
802
|
|
|
803
|
+
function claudeParentDirectInstructions(tenantId, agent) {
|
|
804
|
+
const attribution = `Response from managed agent ${JSON.stringify(agent.title)} (${agent.agentId}):`;
|
|
805
|
+
return [
|
|
806
|
+
`The user explicitly selected the managed agent ${JSON.stringify(agent.title)} (${agent.agentId}) for tenant ${JSON.stringify(tenantId)}.`,
|
|
807
|
+
`Call ${nativeToolName(NATIVE_AGENT_ANSWER_TOOL)} exactly once with question set to the user's complete request. Do not spawn a relay subagent and do not perform the request yourself.`,
|
|
808
|
+
`If the answer returns an object whose schema is exactly ${JSON.stringify(NATIVE_AGENT_CONTINUATION_SCHEMA)}, pass that complete object unchanged as the handle to ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} until terminal; after a continuation exists, never call the answer tool again.`,
|
|
809
|
+
`On success, present the attribution line ${JSON.stringify(attribution)}, followed by the returned finalText verbatim with no rewriting, Markdown changes, or independent synthesis. On failure, attribute the failure to the same named managed agent and do not invent a replacement answer.`,
|
|
810
|
+
].join(" ");
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function claudeParentDirectDefinition({ tenantId, agent, invocation }) {
|
|
814
|
+
return {
|
|
815
|
+
prompt: claudeParentDirectInstructions(tenantId, agent),
|
|
816
|
+
permissionMode: "bypassPermissions",
|
|
817
|
+
tools: [NATIVE_AGENT_ANSWER_TOOL, NATIVE_AGENT_RESUME_TOOL].map((tool) => nativeToolName(tool)),
|
|
818
|
+
mcpServers: [{
|
|
819
|
+
[MANAGED_AGENT_MCP_SERVER]: {
|
|
820
|
+
type: "stdio",
|
|
821
|
+
command: invocation.command,
|
|
822
|
+
args: [...invocation.args],
|
|
823
|
+
env: { ...(invocation.env || {}) },
|
|
824
|
+
},
|
|
825
|
+
}],
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
783
829
|
function codexInvocationEnvironment(invocation, { durableProfile = false } = {}) {
|
|
784
830
|
const entries = Object.entries(invocation.env || {});
|
|
785
831
|
const transient = new Set(IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES);
|
|
@@ -867,34 +913,42 @@ function renderCodexProfile(options) {
|
|
|
867
913
|
function boundNativeAgentInvocation(tenantId, agent, invocation, {
|
|
868
914
|
policyFingerprint = nativeAgentPolicyFingerprint(agent),
|
|
869
915
|
mode = usesDirectAnswer(agent) ? "answer" : "durable",
|
|
916
|
+
answerToolAttribution = false,
|
|
870
917
|
} = {}) {
|
|
871
|
-
|
|
872
|
-
|
|
918
|
+
const bound = !invocation
|
|
919
|
+
? impelNativeAgentMcpInvocation({
|
|
873
920
|
tenantId,
|
|
874
921
|
agentId: agent.agentId,
|
|
875
922
|
scopeParam: agent.scopeParam,
|
|
876
923
|
policyFingerprint,
|
|
877
924
|
mode,
|
|
878
|
-
})
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
925
|
+
})
|
|
926
|
+
: (() => {
|
|
927
|
+
const mcpIndex = invocation.args?.lastIndexOf("mcp") ?? -1;
|
|
928
|
+
if (mcpIndex < 0) throw new Error("native-agent MCP invocation has no mcp command");
|
|
929
|
+
return {
|
|
930
|
+
...invocation,
|
|
931
|
+
args: [
|
|
932
|
+
...invocation.args.slice(0, mcpIndex + 1),
|
|
933
|
+
"--target", IMPEL_NATIVE_AGENT_MCP_TARGET,
|
|
934
|
+
"--tenant", tenantId,
|
|
935
|
+
"--agent-id", agent.agentId,
|
|
936
|
+
"--scope-param", agent.scopeParam,
|
|
937
|
+
"--policy-fingerprint", policyFingerprint,
|
|
938
|
+
...(mode === "recovery" ? ["--recovery-only"] : []),
|
|
939
|
+
...(mode === "answer" ? ["--answer-only"] : []),
|
|
940
|
+
],
|
|
941
|
+
};
|
|
942
|
+
})();
|
|
943
|
+
return answerToolAttribution
|
|
944
|
+
? { ...bound, args: [...bound.args, "--agent-title", agent.title] }
|
|
945
|
+
: bound;
|
|
895
946
|
}
|
|
896
947
|
|
|
897
|
-
export function renderManagedAgents(client, tenantId, agents, invocation = null, {
|
|
948
|
+
export function renderManagedAgents(client, tenantId, agents, invocation = null, {
|
|
949
|
+
directCodeMode = true,
|
|
950
|
+
nativeParentDirect = false,
|
|
951
|
+
} = {}) {
|
|
898
952
|
if (client !== "claude" && client !== "codex") throw new Error(`unknown agent client ${client}`);
|
|
899
953
|
const normalizedTenant = normalizeTenantId(tenantId);
|
|
900
954
|
const fileStems = generatedAgentFileStems(normalizedTenant, agents);
|
|
@@ -908,12 +962,20 @@ export function renderManagedAgents(client, tenantId, agents, invocation = null,
|
|
|
908
962
|
// builtin and remove it from the @ mention picker. Claude names are already
|
|
909
963
|
// collision-safe, filesystem-safe identifiers, so use them as filenames.
|
|
910
964
|
const fileStem = client === "claude" ? name : fileStems[index];
|
|
911
|
-
const
|
|
912
|
-
const
|
|
965
|
+
const parentDirect = client === "claude" && nativeParentDirect && usesDirectAnswer(agent);
|
|
966
|
+
const boundInvocation = boundNativeAgentInvocation(normalizedTenant, agent, invocation, {
|
|
967
|
+
answerToolAttribution: parentDirect,
|
|
968
|
+
});
|
|
969
|
+
const claudeRendered = client === "claude" && !parentDirect
|
|
913
970
|
? renderClaudeAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation })
|
|
914
971
|
: null;
|
|
972
|
+
const parentLaunchDefinition = parentDirect
|
|
973
|
+
? claudeParentDirectDefinition({ tenantId: normalizedTenant, agent, invocation: boundInvocation })
|
|
974
|
+
: null;
|
|
915
975
|
const contents = claudeRendered?.contents
|
|
916
|
-
??
|
|
976
|
+
?? (client === "codex"
|
|
977
|
+
? renderCodexAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation, directCodeMode })
|
|
978
|
+
: null);
|
|
917
979
|
const profileName = client === "codex" ? fileStem : null;
|
|
918
980
|
return {
|
|
919
981
|
agentId: agent.agentId,
|
|
@@ -923,9 +985,10 @@ export function renderManagedAgents(client, tenantId, agents, invocation = null,
|
|
|
923
985
|
policyFingerprint: nativeAgentPolicyFingerprint(agent),
|
|
924
986
|
retired: false,
|
|
925
987
|
name,
|
|
926
|
-
fileName: `${fileStem}${extension}`,
|
|
988
|
+
fileName: parentDirect ? null : `${fileStem}${extension}`,
|
|
927
989
|
contents,
|
|
928
990
|
...(claudeRendered ? { launchDefinition: claudeRendered.launchDefinition } : {}),
|
|
991
|
+
...(parentLaunchDefinition ? { parentLaunchDefinition } : {}),
|
|
929
992
|
...(profileName ? {
|
|
930
993
|
profileName,
|
|
931
994
|
profileFileName: `${profileName}.config.toml`,
|
|
@@ -941,6 +1004,61 @@ export function renderManagedAgents(client, tenantId, agents, invocation = null,
|
|
|
941
1004
|
});
|
|
942
1005
|
}
|
|
943
1006
|
|
|
1007
|
+
function shellQuote(value) {
|
|
1008
|
+
return `'${String(value).replaceAll("'", `'"'"'`)}'`;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function claudeSlashCommandName(agent, generatedAgentName) {
|
|
1012
|
+
const base = `ask-${generatedAgentName}`;
|
|
1013
|
+
if (base.length <= 63) return base;
|
|
1014
|
+
const prefix = generatedAgentName.slice(0, 50).replace(/-+$/u, "") || "agent";
|
|
1015
|
+
return `ask-${prefix}-${agentBindingHash(agent)}`;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function renderClaudeSlashCommand(tenantId, agent, commandName, invocation) {
|
|
1019
|
+
const tokens = [invocation.command, ...invocation.args].map(shellQuote).join(" ");
|
|
1020
|
+
const bashCommand = `${tokens} --prompt "$ARGUMENTS" --json`;
|
|
1021
|
+
const attribution = `Response from managed agent ${JSON.stringify(agent.title)} (${agent.agentId}):`;
|
|
1022
|
+
return [
|
|
1023
|
+
"---",
|
|
1024
|
+
`description: ${JSON.stringify(`Ask managed agent ${agent.title} (${agent.agentId}) in Impel tenant ${tenantId}; its rendered answer is explicitly attributed.`)}`,
|
|
1025
|
+
`argument-hint: ${JSON.stringify("<task>")}`,
|
|
1026
|
+
`allowed-tools: ${JSON.stringify(`Bash(${tokens}:*)`)}`,
|
|
1027
|
+
"---",
|
|
1028
|
+
"",
|
|
1029
|
+
`The JSON below came from managed agent ${JSON.stringify(agent.title)} (${agent.agentId}), not from the host model.`,
|
|
1030
|
+
`Run the fixed command exactly once. If ok is true, output the attribution line ${JSON.stringify(attribution)} followed by finalText verbatim. If ok is false, attribute the reported error to the same managed agent. Do not perform, rewrite, or independently answer the task.`,
|
|
1031
|
+
"",
|
|
1032
|
+
`!\`${bashCommand}\``,
|
|
1033
|
+
"",
|
|
1034
|
+
].join("\n");
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
export function renderManagedClaudeSlashCommands(tenantId, agents, invocation = null) {
|
|
1038
|
+
const normalizedTenant = normalizeTenantId(tenantId);
|
|
1039
|
+
const names = generatedClientAgentNames("claude", agents);
|
|
1040
|
+
return agents.flatMap((agent, index) => {
|
|
1041
|
+
if (!usesDirectAnswer(agent)) return [];
|
|
1042
|
+
const commandName = claudeSlashCommandName(agent, names[index]);
|
|
1043
|
+
const commandInvocation = invocation || impelCliInvocation([
|
|
1044
|
+
"native", "answer",
|
|
1045
|
+
"--tenant", normalizedTenant,
|
|
1046
|
+
"--agent", agent.agentId,
|
|
1047
|
+
]);
|
|
1048
|
+
return [{
|
|
1049
|
+
agentId: agent.agentId,
|
|
1050
|
+
name: commandName,
|
|
1051
|
+
fileName: `${commandName}.md`,
|
|
1052
|
+
contents: renderClaudeSlashCommand(
|
|
1053
|
+
normalizedTenant,
|
|
1054
|
+
agent,
|
|
1055
|
+
commandName,
|
|
1056
|
+
commandInvocation,
|
|
1057
|
+
),
|
|
1058
|
+
}];
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
|
|
944
1062
|
function renderRetiredManagedAgents(client, tenantId, bindings, active, invocation = null, { directCodeMode = true } = {}) {
|
|
945
1063
|
const usedNames = new Set(active.map(({ name }) => name));
|
|
946
1064
|
const usedFiles = new Set(active.map(({ fileName }) => fileName));
|
|
@@ -1154,6 +1272,54 @@ function validateClaudeLaunchDefinition(record, tenantId) {
|
|
|
1154
1272
|
return definition;
|
|
1155
1273
|
}
|
|
1156
1274
|
|
|
1275
|
+
function validateClaudeParentLaunchDefinition(record, tenantId) {
|
|
1276
|
+
const definition = record.parentLaunchDefinition;
|
|
1277
|
+
const expectedTools = [NATIVE_AGENT_ANSWER_TOOL, NATIVE_AGENT_RESUME_TOOL]
|
|
1278
|
+
.map((name) => nativeToolName(name));
|
|
1279
|
+
if (!exactObjectKeys(definition, ["prompt", "permissionMode", "tools", "mcpServers"])
|
|
1280
|
+
|| definition.prompt !== claudeParentDirectInstructions(tenantId, record)
|
|
1281
|
+
|| definition.permissionMode !== "bypassPermissions"
|
|
1282
|
+
|| JSON.stringify(definition.tools) !== JSON.stringify(expectedTools)
|
|
1283
|
+
|| !Array.isArray(definition.mcpServers)
|
|
1284
|
+
|| definition.mcpServers.length !== 1
|
|
1285
|
+
|| !exactObjectKeys(definition.mcpServers[0], [MANAGED_AGENT_MCP_SERVER])) {
|
|
1286
|
+
throw new Error("the managed Claude parent-direct definition is invalid; run `impel agents sync claude`");
|
|
1287
|
+
}
|
|
1288
|
+
const server = definition.mcpServers[0][MANAGED_AGENT_MCP_SERVER];
|
|
1289
|
+
const allowedEnvironment = new Set([IMPEL_MANAGED_MCP_ENV, ...IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES]);
|
|
1290
|
+
if (!exactObjectKeys(server, ["type", "command", "args", "env"])
|
|
1291
|
+
|| server.type !== "stdio"
|
|
1292
|
+
|| typeof server.command !== "string"
|
|
1293
|
+
|| !server.command
|
|
1294
|
+
|| !Array.isArray(server.args)
|
|
1295
|
+
|| !server.args.every((argument) => typeof argument === "string")
|
|
1296
|
+
|| !server.env
|
|
1297
|
+
|| typeof server.env !== "object"
|
|
1298
|
+
|| Array.isArray(server.env)
|
|
1299
|
+
|| server.env[IMPEL_MANAGED_MCP_ENV] !== "1"
|
|
1300
|
+
|| !Object.entries(server.env).every(([key, value]) =>
|
|
1301
|
+
allowedEnvironment.has(key)
|
|
1302
|
+
&& typeof value === "string"
|
|
1303
|
+
&& value.length <= 4096
|
|
1304
|
+
&& redactCredentialText(value) === value
|
|
1305
|
+
)) {
|
|
1306
|
+
throw new Error("the managed Claude parent-direct MCP definition is invalid; run `impel agents sync claude`");
|
|
1307
|
+
}
|
|
1308
|
+
const trustedInvocation = impelNativeAgentMcpInvocation({
|
|
1309
|
+
tenantId,
|
|
1310
|
+
agentId: record.agentId,
|
|
1311
|
+
scopeParam: record.scopeParam,
|
|
1312
|
+
policyFingerprint: record.policyFingerprint,
|
|
1313
|
+
mode: "answer",
|
|
1314
|
+
});
|
|
1315
|
+
const trustedArgs = [...trustedInvocation.args, "--agent-title", record.title];
|
|
1316
|
+
if (server.command !== trustedInvocation.command
|
|
1317
|
+
|| JSON.stringify(server.args) !== JSON.stringify(trustedArgs)) {
|
|
1318
|
+
throw new Error("the managed Claude parent-direct binding is invalid; run `impel agents sync claude`");
|
|
1319
|
+
}
|
|
1320
|
+
return definition;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1157
1323
|
// A selector absent from the Impel manifest remains Claude-native. A matching
|
|
1158
1324
|
// record is integrity checked, then re-declared under a random per-launch name
|
|
1159
1325
|
// through Claude's higher-priority --agents source. This prevents project,
|
|
@@ -1192,11 +1358,34 @@ export function resolveManagedClaudeAgent(root, tenantId, selector) {
|
|
|
1192
1358
|
const [record] = matches;
|
|
1193
1359
|
if (typeof record.agentId !== "string"
|
|
1194
1360
|
|| !SAFE_AGENT_ID_RE.test(record.agentId)
|
|
1361
|
+
|| typeof record.title !== "string"
|
|
1362
|
+
|| !record.title.trim()
|
|
1363
|
+
|| record.title.length > 160
|
|
1195
1364
|
|| typeof record.scopeParam !== "string"
|
|
1196
1365
|
|| !SAFE_SCOPE_PARAM_RE.test(record.scopeParam)
|
|
1197
1366
|
|| !/^[a-f0-9]{64}$/u.test(record.policyFingerprint || "")) {
|
|
1198
1367
|
throw new Error("the managed Claude binding metadata is invalid; run `impel agents sync claude`");
|
|
1199
1368
|
}
|
|
1369
|
+
if (record.launchMode === "parent-direct") {
|
|
1370
|
+
if (!manifest.nativeParentDirect) {
|
|
1371
|
+
throw new Error("the managed Claude parent-direct binding is stale; run `impel agents sync claude`");
|
|
1372
|
+
}
|
|
1373
|
+
const parentLaunchDefinition = validateClaudeParentLaunchDefinition(record, normalizedTenant);
|
|
1374
|
+
const server = parentLaunchDefinition.mcpServers[0][MANAGED_AGENT_MCP_SERVER];
|
|
1375
|
+
return {
|
|
1376
|
+
...record,
|
|
1377
|
+
fileName: null,
|
|
1378
|
+
path: null,
|
|
1379
|
+
parentDirect: true,
|
|
1380
|
+
parentLaunchDefinition,
|
|
1381
|
+
parentMcpConfigJson: JSON.stringify({
|
|
1382
|
+
mcpServers: { [MANAGED_AGENT_MCP_SERVER]: server },
|
|
1383
|
+
}),
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
if (record.launchMode !== undefined) {
|
|
1387
|
+
throw new Error("the managed Claude launch mode is invalid; run `impel agents sync claude`");
|
|
1388
|
+
}
|
|
1200
1389
|
const fileName = `${record.name}.md`;
|
|
1201
1390
|
if (path.basename(fileName) !== fileName || !manifest.files.includes(fileName)) {
|
|
1202
1391
|
throw new Error("the managed Claude agent mapping is invalid; run `impel agents sync claude`");
|
|
@@ -1229,6 +1418,59 @@ export function resolveManagedClaudeAgent(root, tenantId, selector) {
|
|
|
1229
1418
|
};
|
|
1230
1419
|
}
|
|
1231
1420
|
|
|
1421
|
+
/**
|
|
1422
|
+
* Resolve a synchronized, read-only Claude binding that is allowed to use the
|
|
1423
|
+
* bounded direct-answer transport. The public `impel native answer` command
|
|
1424
|
+
* deliberately trusts the integrity-tracked profile rather than accepting
|
|
1425
|
+
* model- or user-supplied scope/fingerprint routing arguments.
|
|
1426
|
+
*/
|
|
1427
|
+
export function resolveManagedClaudeAnswerBinding(root, tenantId, selector) {
|
|
1428
|
+
const resolved = resolveManagedClaudeAgent(root, tenantId, selector);
|
|
1429
|
+
if (!resolved) {
|
|
1430
|
+
throw new Error(
|
|
1431
|
+
`managed Claude agent ${JSON.stringify(selector)} was not found for tenant ${JSON.stringify(normalizeTenantId(tenantId))}`,
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
const definition = resolved.parentDirect
|
|
1435
|
+
? resolved.parentLaunchDefinition
|
|
1436
|
+
: resolved.launchDefinition;
|
|
1437
|
+
const server = definition?.mcpServers?.[0]?.[MANAGED_AGENT_MCP_SERVER];
|
|
1438
|
+
if (!Array.isArray(server?.args) || !server.args.includes("--answer-only")) {
|
|
1439
|
+
throw new Error(
|
|
1440
|
+
`managed Claude agent ${JSON.stringify(selector)} is not configured for direct answers`,
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
return {
|
|
1444
|
+
tenantId: normalizeTenantId(tenantId),
|
|
1445
|
+
agentId: resolved.agentId,
|
|
1446
|
+
title: resolved.title,
|
|
1447
|
+
name: resolved.name,
|
|
1448
|
+
scopeParam: resolved.scopeParam,
|
|
1449
|
+
policyFingerprint: resolved.policyFingerprint,
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
export function managedClaudeAnswerBindings(root, tenantId) {
|
|
1454
|
+
const normalizedTenant = normalizeTenantId(tenantId);
|
|
1455
|
+
const manifest = readManagedAgentManifest(root);
|
|
1456
|
+
if (!manifest
|
|
1457
|
+
|| manifest.version !== MANAGED_AGENT_MANIFEST_VERSION
|
|
1458
|
+
|| manifest.client !== "claude"
|
|
1459
|
+
|| manifest.tenantId !== normalizedTenant
|
|
1460
|
+
|| !Array.isArray(manifest.agents)) {
|
|
1461
|
+
return [];
|
|
1462
|
+
}
|
|
1463
|
+
return manifest.agents.flatMap((record) => {
|
|
1464
|
+
if (!record || record.retired || typeof record.agentId !== "string") return [];
|
|
1465
|
+
const definition = record.launchMode === "parent-direct"
|
|
1466
|
+
? record.parentLaunchDefinition
|
|
1467
|
+
: record.launchDefinition;
|
|
1468
|
+
const args = definition?.mcpServers?.[0]?.[MANAGED_AGENT_MCP_SERVER]?.args;
|
|
1469
|
+
if (!Array.isArray(args) || !args.includes("--answer-only")) return [];
|
|
1470
|
+
return [resolveManagedClaudeAnswerBinding(root, normalizedTenant, record.agentId)];
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1232
1474
|
export function resolveManagedCodexAgentProfile(root, tenantId, selector) {
|
|
1233
1475
|
const normalizedTenant = normalizeTenantId(tenantId);
|
|
1234
1476
|
const normalizedSelector = boundedString(selector, "agent selector", { max: 512 });
|
|
@@ -1279,12 +1521,29 @@ function profileIsFresh(profile, tenantId, now, ttlMs) {
|
|
|
1279
1521
|
|| Array.isArray(manifest.contentDigests)) return false;
|
|
1280
1522
|
const expectedDirectProfiles = profile.client === "codex" && profile.directProfiles !== false;
|
|
1281
1523
|
const expectedDirectCodeMode = profile.client === "codex" && profile.directCodeMode !== false;
|
|
1524
|
+
const expectedNativeParentDirect = profile.client === "claude"
|
|
1525
|
+
&& (profile.nativeParentDirect ?? nativeParentDirectEnabled());
|
|
1526
|
+
const expectedNativeSlashCommands = profile.client === "claude"
|
|
1527
|
+
&& (profile.nativeSlashCommands ?? nativeSlashCommandsEnabled());
|
|
1528
|
+
const expectedNativeIntercept = profile.client === "claude"
|
|
1529
|
+
&& (profile.nativeIntercept ?? nativeInterceptEnabled());
|
|
1530
|
+
const expectedNativeInterceptTimeoutMs = expectedNativeIntercept
|
|
1531
|
+
? (profile.nativeInterceptTimeoutMs ?? nativeInterceptTimeoutMs())
|
|
1532
|
+
: null;
|
|
1282
1533
|
if (manifest.directProfiles !== expectedDirectProfiles
|
|
1283
|
-
|| manifest.directCodeMode !== expectedDirectCodeMode
|
|
1534
|
+
|| manifest.directCodeMode !== expectedDirectCodeMode
|
|
1535
|
+
|| Boolean(manifest.nativeParentDirect) !== expectedNativeParentDirect
|
|
1536
|
+
|| Boolean(manifest.nativeSlashCommands) !== expectedNativeSlashCommands
|
|
1537
|
+
|| Boolean(manifest.nativeIntercept) !== expectedNativeIntercept
|
|
1538
|
+
|| (expectedNativeIntercept
|
|
1539
|
+
&& manifest.nativeInterceptTimeoutMs !== expectedNativeInterceptTimeoutMs)) return false;
|
|
1284
1540
|
const syncedAt = Date.parse(manifest.syncedAt || "");
|
|
1285
1541
|
if (!Number.isFinite(syncedAt) || now - syncedAt >= ttlMs) return false;
|
|
1286
1542
|
const artifacts = [
|
|
1287
1543
|
...manifest.files.map((fileName) => `agents/${fileName}`),
|
|
1544
|
+
...(Array.isArray(manifest.commands)
|
|
1545
|
+
? manifest.commands.map((fileName) => `commands/${fileName}`)
|
|
1546
|
+
: []),
|
|
1288
1547
|
...(Array.isArray(manifest.profiles) ? manifest.profiles : []),
|
|
1289
1548
|
];
|
|
1290
1549
|
// Renderer changes are invalidated by the manifest version/capability fields
|
|
@@ -1306,10 +1565,15 @@ export function syncAgentProfile({
|
|
|
1306
1565
|
agents,
|
|
1307
1566
|
directProfiles = client === "codex",
|
|
1308
1567
|
directCodeMode = client === "codex",
|
|
1568
|
+
nativeParentDirect = client === "claude" && nativeParentDirectEnabled(),
|
|
1569
|
+
nativeSlashCommands = client === "claude" && nativeSlashCommandsEnabled(),
|
|
1570
|
+
nativeIntercept = client === "claude" && nativeInterceptEnabled(),
|
|
1571
|
+
nativeInterceptTimeoutMs: interceptTimeoutMs = nativeIntercept ? nativeInterceptTimeoutMs() : null,
|
|
1309
1572
|
now = Date.now(),
|
|
1310
1573
|
nativeAgentRunsRoot = path.join(CONFIG_DIR, "native-agent-runs"),
|
|
1311
1574
|
}) {
|
|
1312
1575
|
const agentsDir = path.join(root, "agents");
|
|
1576
|
+
const commandsDir = path.join(root, "commands");
|
|
1313
1577
|
for (const candidate of [root, agentsDir]) {
|
|
1314
1578
|
if (fs.existsSync(candidate) && fs.lstatSync(candidate).isSymbolicLink()) {
|
|
1315
1579
|
throw new Error(`refusing to use symlinked native-agent path ${candidate}`);
|
|
@@ -1319,7 +1583,15 @@ export function syncAgentProfile({
|
|
|
1319
1583
|
privateDirectory(managedDir);
|
|
1320
1584
|
const manifestPath = path.join(managedDir, MANAGED_AGENT_MANIFEST);
|
|
1321
1585
|
const prior = readManifest(manifestPath);
|
|
1322
|
-
|
|
1586
|
+
if ((nativeSlashCommands || prior?.commands?.length > 0)
|
|
1587
|
+
&& fs.existsSync(commandsDir)
|
|
1588
|
+
&& fs.lstatSync(commandsDir).isSymbolicLink()) {
|
|
1589
|
+
throw new Error(`refusing to use symlinked native-agent path ${commandsDir}`);
|
|
1590
|
+
}
|
|
1591
|
+
const active = renderManagedAgents(client, tenantId, agents, null, {
|
|
1592
|
+
directCodeMode,
|
|
1593
|
+
nativeParentDirect,
|
|
1594
|
+
});
|
|
1323
1595
|
const activeBindings = new Set(active.map((agent) =>
|
|
1324
1596
|
`${agent.agentId}\0${agent.scopeParam}\0${agent.policyFingerprint}`
|
|
1325
1597
|
));
|
|
@@ -1329,14 +1601,22 @@ export function syncAgentProfile({
|
|
|
1329
1601
|
));
|
|
1330
1602
|
const retired = renderRetiredManagedAgents(client, tenantId, retiredBindings, active, null, { directCodeMode });
|
|
1331
1603
|
const rendered = [...active, ...retired];
|
|
1604
|
+
const commands = client === "claude" && nativeSlashCommands
|
|
1605
|
+
? renderManagedClaudeSlashCommands(tenantId, agents)
|
|
1606
|
+
: [];
|
|
1607
|
+
const renderedFiles = rendered.filter(({ fileName }) => typeof fileName === "string");
|
|
1332
1608
|
if (new Set(rendered.map(({ name }) => name)).size !== rendered.length
|
|
1333
|
-
|| new Set(
|
|
1609
|
+
|| new Set(renderedFiles.map(({ fileName }) => fileName)).size !== renderedFiles.length
|
|
1334
1610
|
|| (client === "codex" && directProfiles
|
|
1335
1611
|
&& new Set(rendered.map(({ profileFileName }) => profileFileName)).size !== rendered.length)) {
|
|
1336
1612
|
throw new Error("generated native-agent destinations are not unique");
|
|
1337
1613
|
}
|
|
1614
|
+
if (new Set(commands.map(({ fileName }) => fileName)).size !== commands.length) {
|
|
1615
|
+
throw new Error("generated native-agent slash command destinations are not unique");
|
|
1616
|
+
}
|
|
1338
1617
|
const priorFiles = new Set(prior?.files || []);
|
|
1339
1618
|
const priorProfiles = new Set(prior?.profiles || []);
|
|
1619
|
+
const priorCommands = new Set(prior?.commands || []);
|
|
1340
1620
|
const priorUsesDiscoveryRoot = Number.isInteger(prior?.version)
|
|
1341
1621
|
&& prior.version >= 2
|
|
1342
1622
|
&& prior.version <= MANAGED_AGENT_MANIFEST_VERSION;
|
|
@@ -1362,14 +1642,16 @@ export function syncAgentProfile({
|
|
|
1362
1642
|
// Preflight every destination before writing so an unmanaged file with the
|
|
1363
1643
|
// same generated name is never overwritten.
|
|
1364
1644
|
for (const agent of rendered) {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
if (fs.
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1645
|
+
if (agent.fileName) {
|
|
1646
|
+
const destination = path.join(agentsDir, agent.fileName);
|
|
1647
|
+
if (fs.existsSync(destination)) {
|
|
1648
|
+
if (fs.lstatSync(destination).isSymbolicLink()) {
|
|
1649
|
+
throw new Error(`refusing to overwrite symlinked native-agent file ${destination}`);
|
|
1650
|
+
}
|
|
1651
|
+
if ((!priorOwnsManagedFiles || !priorFiles.has(agent.fileName))
|
|
1652
|
+
&& !reclaimable(`agents/${agent.fileName}`, destination)) {
|
|
1653
|
+
throw new Error(`refusing to overwrite unmanaged native-agent file ${destination}`);
|
|
1654
|
+
}
|
|
1373
1655
|
}
|
|
1374
1656
|
}
|
|
1375
1657
|
if (client === "codex" && directProfiles) {
|
|
@@ -1385,11 +1667,27 @@ export function syncAgentProfile({
|
|
|
1385
1667
|
}
|
|
1386
1668
|
}
|
|
1387
1669
|
}
|
|
1670
|
+
for (const command of commands) {
|
|
1671
|
+
const destination = path.join(commandsDir, command.fileName);
|
|
1672
|
+
if (fs.existsSync(destination)) {
|
|
1673
|
+
if (fs.lstatSync(destination).isSymbolicLink()) {
|
|
1674
|
+
throw new Error(`refusing to overwrite symlinked native-agent slash command ${destination}`);
|
|
1675
|
+
}
|
|
1676
|
+
if ((!priorOwnsManagedFiles || !priorCommands.has(command.fileName))
|
|
1677
|
+
&& !reclaimable(`commands/${command.fileName}`, destination)) {
|
|
1678
|
+
throw new Error(`refusing to overwrite unmanaged native-agent slash command ${destination}`);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1388
1682
|
for (const agent of rendered) {
|
|
1389
|
-
atomicPrivateWrite(path.join(agentsDir, agent.fileName), agent.contents);
|
|
1683
|
+
if (agent.fileName) atomicPrivateWrite(path.join(agentsDir, agent.fileName), agent.contents);
|
|
1390
1684
|
if (client === "codex" && directProfiles) atomicPrivateWrite(path.join(root, agent.profileFileName), agent.profileContents);
|
|
1391
1685
|
}
|
|
1392
|
-
const
|
|
1686
|
+
for (const command of commands) {
|
|
1687
|
+
atomicPrivateWrite(path.join(commandsDir, command.fileName), command.contents);
|
|
1688
|
+
}
|
|
1689
|
+
const currentFiles = new Set(renderedFiles.map((agent) => agent.fileName));
|
|
1690
|
+
const currentCommands = new Set(commands.map((command) => command.fileName));
|
|
1393
1691
|
const currentProfiles = new Set(client === "codex" && directProfiles
|
|
1394
1692
|
? rendered.map((agent) => agent.profileFileName)
|
|
1395
1693
|
: []);
|
|
@@ -1422,6 +1720,15 @@ export function syncAgentProfile({
|
|
|
1422
1720
|
fs.rmSync(path.join(root, stale), { force: true });
|
|
1423
1721
|
}
|
|
1424
1722
|
}
|
|
1723
|
+
for (const stale of client === "claude" ? prior?.commands || [] : []) {
|
|
1724
|
+
if (
|
|
1725
|
+
staleRemovable(stale, `commands/${stale}`, path.join(commandsDir, stale))
|
|
1726
|
+
&& stale.endsWith(".md")
|
|
1727
|
+
&& !currentCommands.has(stale)
|
|
1728
|
+
) {
|
|
1729
|
+
fs.rmSync(path.join(commandsDir, stale), { force: true });
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1425
1732
|
if (!priorUsesDiscoveryRoot) {
|
|
1426
1733
|
for (const legacy of prior?.files || []) {
|
|
1427
1734
|
if (
|
|
@@ -1433,20 +1740,36 @@ export function syncAgentProfile({
|
|
|
1433
1740
|
}
|
|
1434
1741
|
}
|
|
1435
1742
|
}
|
|
1743
|
+
if (client === "claude") {
|
|
1744
|
+
ensureClaudeNativeInterceptHook(root, tenantId, {
|
|
1745
|
+
enabled: nativeIntercept,
|
|
1746
|
+
...(nativeIntercept ? { timeoutMs: interceptTimeoutMs } : {}),
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1436
1749
|
const contentDigests = Object.fromEntries(rendered.flatMap((agent) => [
|
|
1437
|
-
[`agents/${agent.fileName}`, contentDigest(agent.contents)],
|
|
1750
|
+
...(agent.fileName ? [[`agents/${agent.fileName}`, contentDigest(agent.contents)]] : []),
|
|
1438
1751
|
...(client === "codex" && directProfiles
|
|
1439
1752
|
? [[agent.profileFileName, contentDigest(agent.profileContents)]]
|
|
1440
1753
|
: []),
|
|
1441
|
-
]).
|
|
1754
|
+
]).concat(commands.map((command) => [
|
|
1755
|
+
`commands/${command.fileName}`,
|
|
1756
|
+
contentDigest(command.contents),
|
|
1757
|
+
])).sort(([left], [right]) => left.localeCompare(right)));
|
|
1442
1758
|
atomicPrivateWrite(manifestPath, `${JSON.stringify({
|
|
1443
1759
|
version: MANAGED_AGENT_MANIFEST_VERSION,
|
|
1444
1760
|
tenantId,
|
|
1445
1761
|
client,
|
|
1446
1762
|
directProfiles: client === "codex" && directProfiles,
|
|
1447
1763
|
directCodeMode: client === "codex" && directCodeMode,
|
|
1764
|
+
...(client === "claude" && nativeParentDirect ? { nativeParentDirect: true } : {}),
|
|
1765
|
+
...(client === "claude" && nativeSlashCommands ? { nativeSlashCommands: true } : {}),
|
|
1766
|
+
...(client === "claude" && nativeIntercept ? {
|
|
1767
|
+
nativeIntercept: true,
|
|
1768
|
+
nativeInterceptTimeoutMs: interceptTimeoutMs,
|
|
1769
|
+
} : {}),
|
|
1448
1770
|
syncedAt: new Date(now).toISOString(),
|
|
1449
1771
|
files: [...currentFiles].sort(),
|
|
1772
|
+
...(client === "claude" && nativeSlashCommands ? { commands: [...currentCommands].sort() } : {}),
|
|
1450
1773
|
profiles: [...currentProfiles].sort(),
|
|
1451
1774
|
contentDigests,
|
|
1452
1775
|
agents: rendered.map((agent) => ({
|
|
@@ -1458,7 +1781,9 @@ export function syncAgentProfile({
|
|
|
1458
1781
|
retired: agent.retired,
|
|
1459
1782
|
name: agent.name,
|
|
1460
1783
|
...(client === "claude" ? {
|
|
1461
|
-
|
|
1784
|
+
...(agent.parentLaunchDefinition
|
|
1785
|
+
? { launchMode: "parent-direct", parentLaunchDefinition: agent.parentLaunchDefinition }
|
|
1786
|
+
: { launchDefinition: agent.launchDefinition }),
|
|
1462
1787
|
} : {}),
|
|
1463
1788
|
...(client === "codex" && directProfiles ? {
|
|
1464
1789
|
profileName: agent.profileName,
|
|
@@ -1474,6 +1799,7 @@ export function syncAgentProfile({
|
|
|
1474
1799
|
count: active.length,
|
|
1475
1800
|
recoveryCount: retired.length,
|
|
1476
1801
|
files: [...currentFiles],
|
|
1802
|
+
...(client === "claude" && nativeSlashCommands ? { commands: [...currentCommands] } : {}),
|
|
1477
1803
|
profiles: [...currentProfiles],
|
|
1478
1804
|
};
|
|
1479
1805
|
}
|
package/src/apps.js
CHANGED
|
@@ -25,6 +25,7 @@ import { ensureClaudeSessionHooks, ensureCodexSessionHooks } from "./sessionHook
|
|
|
25
25
|
import { ADHOC_IDENTITY, codesignIdentityArgs, desiredSigningMode, resolveSigningIdentity } from "./codesign.js";
|
|
26
26
|
import { applyImpelClaudeSandbox } from "./claudeSandbox.js";
|
|
27
27
|
import { brandedEnvironmentName, RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
28
|
+
import { CURRENT_CONFIG_VERSION } from "./managedProfileVersion.js";
|
|
28
29
|
import { IMPEL_CODEX_PARENT_DELEGATION_INSTRUCTIONS } from "./commands/launch.js";
|
|
29
30
|
import {
|
|
30
31
|
desktopTasksAssetPaths,
|
|
@@ -301,7 +302,7 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
|
301
302
|
// 35: enable the Code Mode host in managed desktop profiles (pinned Codex
|
|
302
303
|
// fails closed on code_mode_only models without it) and serve CLI model
|
|
303
304
|
// catalogs from the shared registry so efforts and tiers cannot drift.
|
|
304
|
-
export
|
|
305
|
+
export { CURRENT_CONFIG_VERSION };
|
|
305
306
|
|
|
306
307
|
// Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
|
|
307
308
|
// helper rebranding, and signing. A vendored bundle is rebuilt only when this
|