impel-cli 0.20.41 → 0.20.43
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/scripts/profile-native-codex.mjs +604 -57
- package/src/agents.js +557 -45
- package/src/apps.js +2 -1
- package/src/cli.js +7 -0
- package/src/commands/agents.js +1 -0
- package/src/commands/launch.js +57 -7
- package/src/commands/mcp.js +86 -3
- package/src/commands/native.js +285 -0
- package/src/managedProfileVersion.js +4 -0
- package/src/nativeAgentTelemetry.js +4 -0
- package/src/nativeAgentTransport.js +147 -17
- package/src/nativeInterception.js +133 -0
- package/src/selfInvocation.js +20 -1
package/src/agents.js
CHANGED
|
@@ -21,7 +21,10 @@ 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
|
+
IMPEL_NATIVE_BENCHMARK_ENV,
|
|
25
|
+
impelCliInvocation,
|
|
24
26
|
impelNativeAgentMcpInvocation,
|
|
27
|
+
nativeBenchmarkHeaderValue,
|
|
25
28
|
} from "./selfInvocation.js";
|
|
26
29
|
import {
|
|
27
30
|
adapterAnswerFaithfulCompletionGuidance,
|
|
@@ -31,6 +34,11 @@ import {
|
|
|
31
34
|
usesDirectAnswer,
|
|
32
35
|
} from "./directAnswer.js";
|
|
33
36
|
import { normalizeTenantId } from "./tenants.js";
|
|
37
|
+
import {
|
|
38
|
+
ensureClaudeNativeInterceptHook,
|
|
39
|
+
nativeInterceptEnabled,
|
|
40
|
+
nativeInterceptTimeoutMs,
|
|
41
|
+
} from "./nativeInterception.js";
|
|
34
42
|
import {
|
|
35
43
|
adapterCallerSpawnGuidance,
|
|
36
44
|
adapterFaithfulCompletionGuidance,
|
|
@@ -56,7 +64,9 @@ export const NATIVE_AGENT_RESUME_TOOL = "resume_native_agent_run";
|
|
|
56
64
|
export const NATIVE_AGENT_RECOVER_TOOL = "recover_native_agent_runs";
|
|
57
65
|
export const NATIVE_AGENT_CONTINUATION_SCHEMA = "impel.native-agent-continuation.v1";
|
|
58
66
|
export const MANAGED_AGENT_MCP_SERVER = "impel_agent";
|
|
59
|
-
export const MANAGED_AGENT_MANIFEST_VERSION =
|
|
67
|
+
export const MANAGED_AGENT_MANIFEST_VERSION = 23;
|
|
68
|
+
export const IMPEL_NATIVE_PARENT_DIRECT_ENV = "IMPEL_NATIVE_PARENT_DIRECT";
|
|
69
|
+
export const IMPEL_NATIVE_SLASH_COMMANDS_ENV = "IMPEL_NATIVE_SLASH_COMMANDS";
|
|
60
70
|
|
|
61
71
|
// The host model only selects the fixed MCP tool and faithfully returns its
|
|
62
72
|
// result. Spark minimizes those transport-only turns while the selected Eve
|
|
@@ -77,6 +87,18 @@ function nativeAgentToolNames(agent) {
|
|
|
77
87
|
function eagerNativeAgentTransportEnabled() {
|
|
78
88
|
return process.env.IMPEL_NATIVE_EAGER_TRANSPORT !== "0";
|
|
79
89
|
}
|
|
90
|
+
|
|
91
|
+
function enabledProfileFlag(environment, name) {
|
|
92
|
+
return ["1", "true"].includes(String(environment?.[name] || "").toLowerCase());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function nativeParentDirectEnabled(environment = process.env) {
|
|
96
|
+
return enabledProfileFlag(environment, IMPEL_NATIVE_PARENT_DIRECT_ENV);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function nativeSlashCommandsEnabled(environment = process.env) {
|
|
100
|
+
return enabledProfileFlag(environment, IMPEL_NATIVE_SLASH_COMMANDS_ENV);
|
|
101
|
+
}
|
|
80
102
|
const MAX_RETIRED_AGENT_BINDINGS = 50;
|
|
81
103
|
const MAX_NATIVE_AGENT_STATE_BYTES = 512 * 1024;
|
|
82
104
|
const TERMINAL_NATIVE_AGENT_STATUSES = new Set(["succeeded", "failed", "cancelled", "canceled"]);
|
|
@@ -650,6 +672,116 @@ function nativeToolName(toolName) {
|
|
|
650
672
|
return `${nativeToolNamespace()}__${toolName}`;
|
|
651
673
|
}
|
|
652
674
|
|
|
675
|
+
const CLAUDE_PARENT_DIRECT_TOOL_PERMISSIONS = Object.freeze([
|
|
676
|
+
NATIVE_AGENT_ANSWER_TOOL,
|
|
677
|
+
NATIVE_AGENT_RESUME_TOOL,
|
|
678
|
+
].map((tool) => nativeToolName(tool)));
|
|
679
|
+
|
|
680
|
+
function parentDirectPermissionTools(renderedAgents) {
|
|
681
|
+
const trusted = new Set(CLAUDE_PARENT_DIRECT_TOOL_PERMISSIONS);
|
|
682
|
+
const tools = [...new Set(renderedAgents.flatMap((agent) =>
|
|
683
|
+
agent.parentLaunchDefinition?.tools || []
|
|
684
|
+
))];
|
|
685
|
+
if (!tools.every((tool) => trusted.has(tool))) {
|
|
686
|
+
throw new Error("the managed Claude parent-direct permission grant is invalid");
|
|
687
|
+
}
|
|
688
|
+
return tools;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function priorParentDirectPermissionGrants(prior, ownsManagedProfile) {
|
|
692
|
+
const grants = ownsManagedProfile ? prior?.nativeParentDirectPermissionGrants : null;
|
|
693
|
+
const trusted = new Set(CLAUDE_PARENT_DIRECT_TOOL_PERMISSIONS);
|
|
694
|
+
if (!Array.isArray(grants)
|
|
695
|
+
|| new Set(grants).size !== grants.length
|
|
696
|
+
|| !grants.every((tool) => trusted.has(tool))) return [];
|
|
697
|
+
return [...grants];
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function readClaudeSettingsForPermissions(settingsPath) {
|
|
701
|
+
let raw;
|
|
702
|
+
try {
|
|
703
|
+
const stat = fs.lstatSync(settingsPath);
|
|
704
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
705
|
+
throw new Error(`refusing to update unsafe Claude settings path ${settingsPath}`);
|
|
706
|
+
}
|
|
707
|
+
raw = fs.readFileSync(settingsPath, "utf8");
|
|
708
|
+
} catch (error) {
|
|
709
|
+
if (error?.code === "ENOENT") return { settings: {} };
|
|
710
|
+
throw error;
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
const settings = raw.trim() ? JSON.parse(raw) : {};
|
|
714
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) throw new Error();
|
|
715
|
+
return { settings };
|
|
716
|
+
} catch {
|
|
717
|
+
throw new Error(`${settingsPath} exists but isn't a valid JSON object. Fix or remove it, then re-run.`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function syncClaudeParentDirectPermissions(root, desiredTools, priorGrants) {
|
|
722
|
+
if (desiredTools.length === 0 && priorGrants.length === 0) return [];
|
|
723
|
+
|
|
724
|
+
const settingsPath = path.join(root, "settings.json");
|
|
725
|
+
const { settings } = readClaudeSettingsForPermissions(settingsPath);
|
|
726
|
+
const permissionsExisted = Object.hasOwn(settings, "permissions");
|
|
727
|
+
if (permissionsExisted && (
|
|
728
|
+
!settings.permissions
|
|
729
|
+
|| typeof settings.permissions !== "object"
|
|
730
|
+
|| Array.isArray(settings.permissions)
|
|
731
|
+
)) {
|
|
732
|
+
throw new Error(`${settingsPath} has an invalid permissions value. Fix or remove it, then re-run.`);
|
|
733
|
+
}
|
|
734
|
+
const permissions = permissionsExisted ? { ...settings.permissions } : {};
|
|
735
|
+
if (Object.hasOwn(permissions, "allow") && (
|
|
736
|
+
!Array.isArray(permissions.allow)
|
|
737
|
+
|| !permissions.allow.every((rule) => typeof rule === "string")
|
|
738
|
+
)) {
|
|
739
|
+
throw new Error(`${settingsPath} has an invalid permissions.allow value. Fix or remove it, then re-run.`);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const desired = new Set(desiredTools);
|
|
743
|
+
const previouslyManaged = new Set(priorGrants);
|
|
744
|
+
const allow = Array.isArray(permissions.allow) ? [...permissions.allow] : [];
|
|
745
|
+
const nextAllow = allow.filter((rule) => !previouslyManaged.has(rule) || desired.has(rule));
|
|
746
|
+
const managedGrants = [];
|
|
747
|
+
for (const tool of desiredTools) {
|
|
748
|
+
if (previouslyManaged.has(tool)) {
|
|
749
|
+
managedGrants.push(tool);
|
|
750
|
+
if (!nextAllow.includes(tool)) nextAllow.push(tool);
|
|
751
|
+
} else if (!nextAllow.includes(tool)) {
|
|
752
|
+
managedGrants.push(tool);
|
|
753
|
+
nextAllow.push(tool);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// permissions.allow is Claude's ordinary pre-approval surface. Record only
|
|
758
|
+
// entries this sync actually adds, so turning the experiment back off can
|
|
759
|
+
// remove them without claiming any operator-authored permission rule.
|
|
760
|
+
if (JSON.stringify(allow) !== JSON.stringify(nextAllow)) {
|
|
761
|
+
settings.permissions = { ...permissions, allow: nextAllow };
|
|
762
|
+
atomicPrivateWrite(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
763
|
+
}
|
|
764
|
+
return managedGrants;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function claudeParentDirectPermissionsCurrent(root, manifest) {
|
|
768
|
+
try {
|
|
769
|
+
const desiredTools = parentDirectPermissionTools(
|
|
770
|
+
Array.isArray(manifest.agents) ? manifest.agents.map((record) => ({
|
|
771
|
+
parentLaunchDefinition: record?.launchMode === "parent-direct"
|
|
772
|
+
? record.parentLaunchDefinition
|
|
773
|
+
: null,
|
|
774
|
+
})) : [],
|
|
775
|
+
);
|
|
776
|
+
if (desiredTools.length === 0) return true;
|
|
777
|
+
const { settings } = readClaudeSettingsForPermissions(path.join(root, "settings.json"));
|
|
778
|
+
const allow = settings.permissions?.allow;
|
|
779
|
+
return Array.isArray(allow) && desiredTools.every((tool) => allow.includes(tool));
|
|
780
|
+
} catch {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
653
785
|
function codexAdapterInstructions(tenantId, agent) {
|
|
654
786
|
const contextRequirement = agent.requiredContext.length
|
|
655
787
|
? ` Required context keys are ${JSON.stringify(agent.requiredContext)}; if any are absent, ask for them before starting the run.`
|
|
@@ -780,11 +912,61 @@ function renderClaudeAgent({ tenantId, agent, name, invocation, recoveryOnly = f
|
|
|
780
912
|
};
|
|
781
913
|
}
|
|
782
914
|
|
|
915
|
+
function claudeParentDirectInstructions(tenantId, agent) {
|
|
916
|
+
const attribution = `Response from managed agent ${JSON.stringify(agent.title)} (${agent.agentId}):`;
|
|
917
|
+
return [
|
|
918
|
+
`The user explicitly selected the managed agent ${JSON.stringify(agent.title)} (${agent.agentId}) for tenant ${JSON.stringify(tenantId)}.`,
|
|
919
|
+
`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.`,
|
|
920
|
+
`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.`,
|
|
921
|
+
`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.`,
|
|
922
|
+
].join(" ");
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function claudeParentDirectDefinition({ tenantId, agent, invocation }) {
|
|
926
|
+
return {
|
|
927
|
+
prompt: claudeParentDirectInstructions(tenantId, agent),
|
|
928
|
+
permissionMode: "bypassPermissions",
|
|
929
|
+
tools: [...CLAUDE_PARENT_DIRECT_TOOL_PERMISSIONS],
|
|
930
|
+
mcpServers: [{
|
|
931
|
+
[MANAGED_AGENT_MCP_SERVER]: {
|
|
932
|
+
type: "stdio",
|
|
933
|
+
command: invocation.command,
|
|
934
|
+
args: [...invocation.args],
|
|
935
|
+
env: { ...(invocation.env || {}) },
|
|
936
|
+
},
|
|
937
|
+
}],
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function nativeAgentTelemetryEnvironment(environment = process.env) {
|
|
942
|
+
return Object.fromEntries(IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES.flatMap((name) => {
|
|
943
|
+
const value = environment?.[name];
|
|
944
|
+
return typeof value === "string" && value.length > 0 ? [[name, value]] : [];
|
|
945
|
+
}));
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function telemetryEnvironmentMatches(recorded, expected) {
|
|
949
|
+
const normalizedRecorded = recorded === undefined ? {} : recorded;
|
|
950
|
+
return normalizedRecorded
|
|
951
|
+
&& typeof normalizedRecorded === "object"
|
|
952
|
+
&& !Array.isArray(normalizedRecorded)
|
|
953
|
+
&& JSON.stringify(normalizedRecorded) === JSON.stringify(expected);
|
|
954
|
+
}
|
|
955
|
+
|
|
783
956
|
function codexInvocationEnvironment(invocation, { durableProfile = false } = {}) {
|
|
784
957
|
const entries = Object.entries(invocation.env || {});
|
|
785
958
|
const transient = new Set(IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES);
|
|
959
|
+
// Ordinary durable profiles must never retain a one-off telemetry path.
|
|
960
|
+
// The native Codex profiler is different: it generates a throwaway profile
|
|
961
|
+
// for the whole benchmark cohort and needs Codex's explicit MCP environment
|
|
962
|
+
// map to carry that cohort path into the server process. The isolated root is
|
|
963
|
+
// removed after the cohort, so benchmark-tagged telemetry cannot linger in
|
|
964
|
+
// an operator's managed profile.
|
|
965
|
+
const isolatedBenchmark = nativeBenchmarkHeaderValue(invocation.env || {}) !== null;
|
|
786
966
|
return [
|
|
787
|
-
...(durableProfile
|
|
967
|
+
...(durableProfile && !isolatedBenchmark
|
|
968
|
+
? entries.filter(([key]) => !transient.has(key))
|
|
969
|
+
: entries),
|
|
788
970
|
[IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV, String(CODEX_NATIVE_AGENT_ATTACHMENT_WINDOW_MS)],
|
|
789
971
|
];
|
|
790
972
|
}
|
|
@@ -867,34 +1049,44 @@ function renderCodexProfile(options) {
|
|
|
867
1049
|
function boundNativeAgentInvocation(tenantId, agent, invocation, {
|
|
868
1050
|
policyFingerprint = nativeAgentPolicyFingerprint(agent),
|
|
869
1051
|
mode = usesDirectAnswer(agent) ? "answer" : "durable",
|
|
1052
|
+
answerToolAttribution = false,
|
|
1053
|
+
environment = process.env,
|
|
870
1054
|
} = {}) {
|
|
871
|
-
|
|
872
|
-
|
|
1055
|
+
const bound = !invocation
|
|
1056
|
+
? impelNativeAgentMcpInvocation({
|
|
873
1057
|
tenantId,
|
|
874
1058
|
agentId: agent.agentId,
|
|
875
1059
|
scopeParam: agent.scopeParam,
|
|
876
1060
|
policyFingerprint,
|
|
877
1061
|
mode,
|
|
878
|
-
})
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1062
|
+
}, { environment })
|
|
1063
|
+
: (() => {
|
|
1064
|
+
const mcpIndex = invocation.args?.lastIndexOf("mcp") ?? -1;
|
|
1065
|
+
if (mcpIndex < 0) throw new Error("native-agent MCP invocation has no mcp command");
|
|
1066
|
+
return {
|
|
1067
|
+
...invocation,
|
|
1068
|
+
args: [
|
|
1069
|
+
...invocation.args.slice(0, mcpIndex + 1),
|
|
1070
|
+
"--target", IMPEL_NATIVE_AGENT_MCP_TARGET,
|
|
1071
|
+
"--tenant", tenantId,
|
|
1072
|
+
"--agent-id", agent.agentId,
|
|
1073
|
+
"--scope-param", agent.scopeParam,
|
|
1074
|
+
"--policy-fingerprint", policyFingerprint,
|
|
1075
|
+
...(mode === "recovery" ? ["--recovery-only"] : []),
|
|
1076
|
+
...(mode === "answer" ? ["--answer-only"] : []),
|
|
1077
|
+
],
|
|
1078
|
+
};
|
|
1079
|
+
})();
|
|
1080
|
+
return answerToolAttribution
|
|
1081
|
+
? { ...bound, args: [...bound.args, "--agent-title", agent.title] }
|
|
1082
|
+
: bound;
|
|
895
1083
|
}
|
|
896
1084
|
|
|
897
|
-
export function renderManagedAgents(client, tenantId, agents, invocation = null, {
|
|
1085
|
+
export function renderManagedAgents(client, tenantId, agents, invocation = null, {
|
|
1086
|
+
directCodeMode = true,
|
|
1087
|
+
nativeParentDirect = false,
|
|
1088
|
+
environment = process.env,
|
|
1089
|
+
} = {}) {
|
|
898
1090
|
if (client !== "claude" && client !== "codex") throw new Error(`unknown agent client ${client}`);
|
|
899
1091
|
const normalizedTenant = normalizeTenantId(tenantId);
|
|
900
1092
|
const fileStems = generatedAgentFileStems(normalizedTenant, agents);
|
|
@@ -908,12 +1100,21 @@ export function renderManagedAgents(client, tenantId, agents, invocation = null,
|
|
|
908
1100
|
// builtin and remove it from the @ mention picker. Claude names are already
|
|
909
1101
|
// collision-safe, filesystem-safe identifiers, so use them as filenames.
|
|
910
1102
|
const fileStem = client === "claude" ? name : fileStems[index];
|
|
911
|
-
const
|
|
912
|
-
const
|
|
1103
|
+
const parentDirect = client === "claude" && nativeParentDirect && usesDirectAnswer(agent);
|
|
1104
|
+
const boundInvocation = boundNativeAgentInvocation(normalizedTenant, agent, invocation, {
|
|
1105
|
+
answerToolAttribution: parentDirect,
|
|
1106
|
+
environment,
|
|
1107
|
+
});
|
|
1108
|
+
const claudeRendered = client === "claude" && !parentDirect
|
|
913
1109
|
? renderClaudeAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation })
|
|
914
1110
|
: null;
|
|
1111
|
+
const parentLaunchDefinition = parentDirect
|
|
1112
|
+
? claudeParentDirectDefinition({ tenantId: normalizedTenant, agent, invocation: boundInvocation })
|
|
1113
|
+
: null;
|
|
915
1114
|
const contents = claudeRendered?.contents
|
|
916
|
-
??
|
|
1115
|
+
?? (client === "codex"
|
|
1116
|
+
? renderCodexAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation, directCodeMode })
|
|
1117
|
+
: null);
|
|
917
1118
|
const profileName = client === "codex" ? fileStem : null;
|
|
918
1119
|
return {
|
|
919
1120
|
agentId: agent.agentId,
|
|
@@ -923,9 +1124,10 @@ export function renderManagedAgents(client, tenantId, agents, invocation = null,
|
|
|
923
1124
|
policyFingerprint: nativeAgentPolicyFingerprint(agent),
|
|
924
1125
|
retired: false,
|
|
925
1126
|
name,
|
|
926
|
-
fileName: `${fileStem}${extension}`,
|
|
1127
|
+
fileName: parentDirect ? null : `${fileStem}${extension}`,
|
|
927
1128
|
contents,
|
|
928
1129
|
...(claudeRendered ? { launchDefinition: claudeRendered.launchDefinition } : {}),
|
|
1130
|
+
...(parentLaunchDefinition ? { parentLaunchDefinition } : {}),
|
|
929
1131
|
...(profileName ? {
|
|
930
1132
|
profileName,
|
|
931
1133
|
profileFileName: `${profileName}.config.toml`,
|
|
@@ -941,7 +1143,65 @@ export function renderManagedAgents(client, tenantId, agents, invocation = null,
|
|
|
941
1143
|
});
|
|
942
1144
|
}
|
|
943
1145
|
|
|
944
|
-
function
|
|
1146
|
+
function shellQuote(value) {
|
|
1147
|
+
return `'${String(value).replaceAll("'", `'"'"'`)}'`;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function claudeSlashCommandName(agent, generatedAgentName) {
|
|
1151
|
+
const base = `ask-${generatedAgentName}`;
|
|
1152
|
+
if (base.length <= 63) return base;
|
|
1153
|
+
const prefix = generatedAgentName.slice(0, 50).replace(/-+$/u, "") || "agent";
|
|
1154
|
+
return `ask-${prefix}-${agentBindingHash(agent)}`;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function renderClaudeSlashCommand(tenantId, agent, commandName, invocation) {
|
|
1158
|
+
const tokens = [invocation.command, ...invocation.args].map(shellQuote).join(" ");
|
|
1159
|
+
const bashCommand = `${tokens} --prompt "$ARGUMENTS" --json`;
|
|
1160
|
+
const attribution = `Response from managed agent ${JSON.stringify(agent.title)} (${agent.agentId}):`;
|
|
1161
|
+
return [
|
|
1162
|
+
"---",
|
|
1163
|
+
`description: ${JSON.stringify(`Ask managed agent ${agent.title} (${agent.agentId}) in Impel tenant ${tenantId}; its rendered answer is explicitly attributed.`)}`,
|
|
1164
|
+
`argument-hint: ${JSON.stringify("<task>")}`,
|
|
1165
|
+
`allowed-tools: ${JSON.stringify(`Bash(${tokens}:*)`)}`,
|
|
1166
|
+
"---",
|
|
1167
|
+
"",
|
|
1168
|
+
`The JSON below came from managed agent ${JSON.stringify(agent.title)} (${agent.agentId}), not from the host model.`,
|
|
1169
|
+
`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.`,
|
|
1170
|
+
"",
|
|
1171
|
+
`!\`${bashCommand}\``,
|
|
1172
|
+
"",
|
|
1173
|
+
].join("\n");
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
export function renderManagedClaudeSlashCommands(tenantId, agents, invocation = null) {
|
|
1177
|
+
const normalizedTenant = normalizeTenantId(tenantId);
|
|
1178
|
+
const names = generatedClientAgentNames("claude", agents);
|
|
1179
|
+
return agents.flatMap((agent, index) => {
|
|
1180
|
+
if (!usesDirectAnswer(agent)) return [];
|
|
1181
|
+
const commandName = claudeSlashCommandName(agent, names[index]);
|
|
1182
|
+
const commandInvocation = invocation || impelCliInvocation([
|
|
1183
|
+
"native", "answer",
|
|
1184
|
+
"--tenant", normalizedTenant,
|
|
1185
|
+
"--agent", agent.agentId,
|
|
1186
|
+
]);
|
|
1187
|
+
return [{
|
|
1188
|
+
agentId: agent.agentId,
|
|
1189
|
+
name: commandName,
|
|
1190
|
+
fileName: `${commandName}.md`,
|
|
1191
|
+
contents: renderClaudeSlashCommand(
|
|
1192
|
+
normalizedTenant,
|
|
1193
|
+
agent,
|
|
1194
|
+
commandName,
|
|
1195
|
+
commandInvocation,
|
|
1196
|
+
),
|
|
1197
|
+
}];
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function renderRetiredManagedAgents(client, tenantId, bindings, active, invocation = null, {
|
|
1202
|
+
directCodeMode = true,
|
|
1203
|
+
environment = process.env,
|
|
1204
|
+
} = {}) {
|
|
945
1205
|
const usedNames = new Set(active.map(({ name }) => name));
|
|
946
1206
|
const usedFiles = new Set(active.map(({ fileName }) => fileName));
|
|
947
1207
|
return bindings.map((binding) => {
|
|
@@ -979,6 +1239,7 @@ function renderRetiredManagedAgents(client, tenantId, bindings, active, invocati
|
|
|
979
1239
|
const boundInvocation = boundNativeAgentInvocation(tenantId, agent, invocation, {
|
|
980
1240
|
policyFingerprint: binding.policyFingerprint,
|
|
981
1241
|
mode: "recovery",
|
|
1242
|
+
environment,
|
|
982
1243
|
});
|
|
983
1244
|
const claudeRendered = client === "claude"
|
|
984
1245
|
? renderClaudeAgent({ tenantId, agent, name, invocation: boundInvocation, recoveryOnly: true })
|
|
@@ -1115,7 +1376,11 @@ function validateClaudeLaunchDefinition(record, tenantId) {
|
|
|
1115
1376
|
throw new Error("the managed Claude launch definition is invalid; run `impel agents sync claude`");
|
|
1116
1377
|
}
|
|
1117
1378
|
const server = definition.mcpServers[0][MANAGED_AGENT_MCP_SERVER];
|
|
1118
|
-
const allowedEnvironment = new Set([
|
|
1379
|
+
const allowedEnvironment = new Set([
|
|
1380
|
+
IMPEL_MANAGED_MCP_ENV,
|
|
1381
|
+
...IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES,
|
|
1382
|
+
IMPEL_NATIVE_BENCHMARK_ENV,
|
|
1383
|
+
]);
|
|
1119
1384
|
if (!exactObjectKeys(server, ["type", "command", "args", "env"])
|
|
1120
1385
|
|| server.type !== "stdio"
|
|
1121
1386
|
|| typeof server.command !== "string"
|
|
@@ -1154,6 +1419,57 @@ function validateClaudeLaunchDefinition(record, tenantId) {
|
|
|
1154
1419
|
return definition;
|
|
1155
1420
|
}
|
|
1156
1421
|
|
|
1422
|
+
function validateClaudeParentLaunchDefinition(record, tenantId) {
|
|
1423
|
+
const definition = record.parentLaunchDefinition;
|
|
1424
|
+
const expectedTools = CLAUDE_PARENT_DIRECT_TOOL_PERMISSIONS;
|
|
1425
|
+
if (!exactObjectKeys(definition, ["prompt", "permissionMode", "tools", "mcpServers"])
|
|
1426
|
+
|| definition.prompt !== claudeParentDirectInstructions(tenantId, record)
|
|
1427
|
+
|| definition.permissionMode !== "bypassPermissions"
|
|
1428
|
+
|| JSON.stringify(definition.tools) !== JSON.stringify(expectedTools)
|
|
1429
|
+
|| !Array.isArray(definition.mcpServers)
|
|
1430
|
+
|| definition.mcpServers.length !== 1
|
|
1431
|
+
|| !exactObjectKeys(definition.mcpServers[0], [MANAGED_AGENT_MCP_SERVER])) {
|
|
1432
|
+
throw new Error("the managed Claude parent-direct definition is invalid; run `impel agents sync claude`");
|
|
1433
|
+
}
|
|
1434
|
+
const server = definition.mcpServers[0][MANAGED_AGENT_MCP_SERVER];
|
|
1435
|
+
const allowedEnvironment = new Set([
|
|
1436
|
+
IMPEL_MANAGED_MCP_ENV,
|
|
1437
|
+
...IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES,
|
|
1438
|
+
IMPEL_NATIVE_BENCHMARK_ENV,
|
|
1439
|
+
]);
|
|
1440
|
+
if (!exactObjectKeys(server, ["type", "command", "args", "env"])
|
|
1441
|
+
|| server.type !== "stdio"
|
|
1442
|
+
|| typeof server.command !== "string"
|
|
1443
|
+
|| !server.command
|
|
1444
|
+
|| !Array.isArray(server.args)
|
|
1445
|
+
|| !server.args.every((argument) => typeof argument === "string")
|
|
1446
|
+
|| !server.env
|
|
1447
|
+
|| typeof server.env !== "object"
|
|
1448
|
+
|| Array.isArray(server.env)
|
|
1449
|
+
|| server.env[IMPEL_MANAGED_MCP_ENV] !== "1"
|
|
1450
|
+
|| !Object.entries(server.env).every(([key, value]) =>
|
|
1451
|
+
allowedEnvironment.has(key)
|
|
1452
|
+
&& typeof value === "string"
|
|
1453
|
+
&& value.length <= 4096
|
|
1454
|
+
&& redactCredentialText(value) === value
|
|
1455
|
+
)) {
|
|
1456
|
+
throw new Error("the managed Claude parent-direct MCP definition is invalid; run `impel agents sync claude`");
|
|
1457
|
+
}
|
|
1458
|
+
const trustedInvocation = impelNativeAgentMcpInvocation({
|
|
1459
|
+
tenantId,
|
|
1460
|
+
agentId: record.agentId,
|
|
1461
|
+
scopeParam: record.scopeParam,
|
|
1462
|
+
policyFingerprint: record.policyFingerprint,
|
|
1463
|
+
mode: "answer",
|
|
1464
|
+
});
|
|
1465
|
+
const trustedArgs = [...trustedInvocation.args, "--agent-title", record.title];
|
|
1466
|
+
if (server.command !== trustedInvocation.command
|
|
1467
|
+
|| JSON.stringify(server.args) !== JSON.stringify(trustedArgs)) {
|
|
1468
|
+
throw new Error("the managed Claude parent-direct binding is invalid; run `impel agents sync claude`");
|
|
1469
|
+
}
|
|
1470
|
+
return definition;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1157
1473
|
// A selector absent from the Impel manifest remains Claude-native. A matching
|
|
1158
1474
|
// record is integrity checked, then re-declared under a random per-launch name
|
|
1159
1475
|
// through Claude's higher-priority --agents source. This prevents project,
|
|
@@ -1192,11 +1508,34 @@ export function resolveManagedClaudeAgent(root, tenantId, selector) {
|
|
|
1192
1508
|
const [record] = matches;
|
|
1193
1509
|
if (typeof record.agentId !== "string"
|
|
1194
1510
|
|| !SAFE_AGENT_ID_RE.test(record.agentId)
|
|
1511
|
+
|| typeof record.title !== "string"
|
|
1512
|
+
|| !record.title.trim()
|
|
1513
|
+
|| record.title.length > 160
|
|
1195
1514
|
|| typeof record.scopeParam !== "string"
|
|
1196
1515
|
|| !SAFE_SCOPE_PARAM_RE.test(record.scopeParam)
|
|
1197
1516
|
|| !/^[a-f0-9]{64}$/u.test(record.policyFingerprint || "")) {
|
|
1198
1517
|
throw new Error("the managed Claude binding metadata is invalid; run `impel agents sync claude`");
|
|
1199
1518
|
}
|
|
1519
|
+
if (record.launchMode === "parent-direct") {
|
|
1520
|
+
if (!manifest.nativeParentDirect) {
|
|
1521
|
+
throw new Error("the managed Claude parent-direct binding is stale; run `impel agents sync claude`");
|
|
1522
|
+
}
|
|
1523
|
+
const parentLaunchDefinition = validateClaudeParentLaunchDefinition(record, normalizedTenant);
|
|
1524
|
+
const server = parentLaunchDefinition.mcpServers[0][MANAGED_AGENT_MCP_SERVER];
|
|
1525
|
+
return {
|
|
1526
|
+
...record,
|
|
1527
|
+
fileName: null,
|
|
1528
|
+
path: null,
|
|
1529
|
+
parentDirect: true,
|
|
1530
|
+
parentLaunchDefinition,
|
|
1531
|
+
parentMcpConfigJson: JSON.stringify({
|
|
1532
|
+
mcpServers: { [MANAGED_AGENT_MCP_SERVER]: server },
|
|
1533
|
+
}),
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
if (record.launchMode !== undefined) {
|
|
1537
|
+
throw new Error("the managed Claude launch mode is invalid; run `impel agents sync claude`");
|
|
1538
|
+
}
|
|
1200
1539
|
const fileName = `${record.name}.md`;
|
|
1201
1540
|
if (path.basename(fileName) !== fileName || !manifest.files.includes(fileName)) {
|
|
1202
1541
|
throw new Error("the managed Claude agent mapping is invalid; run `impel agents sync claude`");
|
|
@@ -1229,6 +1568,59 @@ export function resolveManagedClaudeAgent(root, tenantId, selector) {
|
|
|
1229
1568
|
};
|
|
1230
1569
|
}
|
|
1231
1570
|
|
|
1571
|
+
/**
|
|
1572
|
+
* Resolve a synchronized, read-only Claude binding that is allowed to use the
|
|
1573
|
+
* bounded direct-answer transport. The public `impel native answer` command
|
|
1574
|
+
* deliberately trusts the integrity-tracked profile rather than accepting
|
|
1575
|
+
* model- or user-supplied scope/fingerprint routing arguments.
|
|
1576
|
+
*/
|
|
1577
|
+
export function resolveManagedClaudeAnswerBinding(root, tenantId, selector) {
|
|
1578
|
+
const resolved = resolveManagedClaudeAgent(root, tenantId, selector);
|
|
1579
|
+
if (!resolved) {
|
|
1580
|
+
throw new Error(
|
|
1581
|
+
`managed Claude agent ${JSON.stringify(selector)} was not found for tenant ${JSON.stringify(normalizeTenantId(tenantId))}`,
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
const definition = resolved.parentDirect
|
|
1585
|
+
? resolved.parentLaunchDefinition
|
|
1586
|
+
: resolved.launchDefinition;
|
|
1587
|
+
const server = definition?.mcpServers?.[0]?.[MANAGED_AGENT_MCP_SERVER];
|
|
1588
|
+
if (!Array.isArray(server?.args) || !server.args.includes("--answer-only")) {
|
|
1589
|
+
throw new Error(
|
|
1590
|
+
`managed Claude agent ${JSON.stringify(selector)} is not configured for direct answers`,
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1593
|
+
return {
|
|
1594
|
+
tenantId: normalizeTenantId(tenantId),
|
|
1595
|
+
agentId: resolved.agentId,
|
|
1596
|
+
title: resolved.title,
|
|
1597
|
+
name: resolved.name,
|
|
1598
|
+
scopeParam: resolved.scopeParam,
|
|
1599
|
+
policyFingerprint: resolved.policyFingerprint,
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
export function managedClaudeAnswerBindings(root, tenantId) {
|
|
1604
|
+
const normalizedTenant = normalizeTenantId(tenantId);
|
|
1605
|
+
const manifest = readManagedAgentManifest(root);
|
|
1606
|
+
if (!manifest
|
|
1607
|
+
|| manifest.version !== MANAGED_AGENT_MANIFEST_VERSION
|
|
1608
|
+
|| manifest.client !== "claude"
|
|
1609
|
+
|| manifest.tenantId !== normalizedTenant
|
|
1610
|
+
|| !Array.isArray(manifest.agents)) {
|
|
1611
|
+
return [];
|
|
1612
|
+
}
|
|
1613
|
+
return manifest.agents.flatMap((record) => {
|
|
1614
|
+
if (!record || record.retired || typeof record.agentId !== "string") return [];
|
|
1615
|
+
const definition = record.launchMode === "parent-direct"
|
|
1616
|
+
? record.parentLaunchDefinition
|
|
1617
|
+
: record.launchDefinition;
|
|
1618
|
+
const args = definition?.mcpServers?.[0]?.[MANAGED_AGENT_MCP_SERVER]?.args;
|
|
1619
|
+
if (!Array.isArray(args) || !args.includes("--answer-only")) return [];
|
|
1620
|
+
return [resolveManagedClaudeAnswerBinding(root, normalizedTenant, record.agentId)];
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1232
1624
|
export function resolveManagedCodexAgentProfile(root, tenantId, selector) {
|
|
1233
1625
|
const normalizedTenant = normalizeTenantId(tenantId);
|
|
1234
1626
|
const normalizedSelector = boundedString(selector, "agent selector", { max: 512 });
|
|
@@ -1279,12 +1671,43 @@ function profileIsFresh(profile, tenantId, now, ttlMs) {
|
|
|
1279
1671
|
|| Array.isArray(manifest.contentDigests)) return false;
|
|
1280
1672
|
const expectedDirectProfiles = profile.client === "codex" && profile.directProfiles !== false;
|
|
1281
1673
|
const expectedDirectCodeMode = profile.client === "codex" && profile.directCodeMode !== false;
|
|
1674
|
+
const expectedNativeParentDirect = profile.client === "claude"
|
|
1675
|
+
&& (profile.nativeParentDirect ?? nativeParentDirectEnabled());
|
|
1676
|
+
const expectedNativeSlashCommands = profile.client === "claude"
|
|
1677
|
+
&& (profile.nativeSlashCommands ?? nativeSlashCommandsEnabled());
|
|
1678
|
+
const expectedNativeIntercept = profile.client === "claude"
|
|
1679
|
+
&& (profile.nativeIntercept ?? nativeInterceptEnabled());
|
|
1680
|
+
const expectedNativeInterceptTimeoutMs = expectedNativeIntercept
|
|
1681
|
+
? (profile.nativeInterceptTimeoutMs ?? nativeInterceptTimeoutMs())
|
|
1682
|
+
: null;
|
|
1683
|
+
const expectedTelemetryEnvironment = nativeAgentTelemetryEnvironment(
|
|
1684
|
+
profile.environment ?? process.env,
|
|
1685
|
+
);
|
|
1686
|
+
const expectedBenchmark = nativeBenchmarkHeaderValue(
|
|
1687
|
+
profile.environment ?? process.env,
|
|
1688
|
+
) !== null;
|
|
1282
1689
|
if (manifest.directProfiles !== expectedDirectProfiles
|
|
1283
|
-
|| manifest.directCodeMode !== expectedDirectCodeMode
|
|
1690
|
+
|| manifest.directCodeMode !== expectedDirectCodeMode
|
|
1691
|
+
|| Boolean(manifest.nativeParentDirect) !== expectedNativeParentDirect
|
|
1692
|
+
|| Boolean(manifest.nativeSlashCommands) !== expectedNativeSlashCommands
|
|
1693
|
+
|| Boolean(manifest.nativeIntercept) !== expectedNativeIntercept
|
|
1694
|
+
|| !telemetryEnvironmentMatches(
|
|
1695
|
+
manifest.nativeAgentTelemetryEnvironment,
|
|
1696
|
+
expectedTelemetryEnvironment,
|
|
1697
|
+
)
|
|
1698
|
+
|| Boolean(manifest.nativeBenchmark) !== expectedBenchmark
|
|
1699
|
+
|| (expectedNativeIntercept
|
|
1700
|
+
&& manifest.nativeInterceptTimeoutMs !== expectedNativeInterceptTimeoutMs)) return false;
|
|
1701
|
+
if (expectedNativeParentDirect && !claudeParentDirectPermissionsCurrent(profile.root, manifest)) {
|
|
1702
|
+
return false;
|
|
1703
|
+
}
|
|
1284
1704
|
const syncedAt = Date.parse(manifest.syncedAt || "");
|
|
1285
1705
|
if (!Number.isFinite(syncedAt) || now - syncedAt >= ttlMs) return false;
|
|
1286
1706
|
const artifacts = [
|
|
1287
1707
|
...manifest.files.map((fileName) => `agents/${fileName}`),
|
|
1708
|
+
...(Array.isArray(manifest.commands)
|
|
1709
|
+
? manifest.commands.map((fileName) => `commands/${fileName}`)
|
|
1710
|
+
: []),
|
|
1288
1711
|
...(Array.isArray(manifest.profiles) ? manifest.profiles : []),
|
|
1289
1712
|
];
|
|
1290
1713
|
// Renderer changes are invalidated by the manifest version/capability fields
|
|
@@ -1306,10 +1729,17 @@ export function syncAgentProfile({
|
|
|
1306
1729
|
agents,
|
|
1307
1730
|
directProfiles = client === "codex",
|
|
1308
1731
|
directCodeMode = client === "codex",
|
|
1732
|
+
nativeParentDirect = client === "claude" && nativeParentDirectEnabled(),
|
|
1733
|
+
nativeSlashCommands = client === "claude" && nativeSlashCommandsEnabled(),
|
|
1734
|
+
nativeIntercept = client === "claude" && nativeInterceptEnabled(),
|
|
1735
|
+
nativeInterceptTimeoutMs: interceptTimeoutMs = nativeIntercept ? nativeInterceptTimeoutMs() : null,
|
|
1736
|
+
environment = process.env,
|
|
1737
|
+
invocation = null,
|
|
1309
1738
|
now = Date.now(),
|
|
1310
1739
|
nativeAgentRunsRoot = path.join(CONFIG_DIR, "native-agent-runs"),
|
|
1311
1740
|
}) {
|
|
1312
1741
|
const agentsDir = path.join(root, "agents");
|
|
1742
|
+
const commandsDir = path.join(root, "commands");
|
|
1313
1743
|
for (const candidate of [root, agentsDir]) {
|
|
1314
1744
|
if (fs.existsSync(candidate) && fs.lstatSync(candidate).isSymbolicLink()) {
|
|
1315
1745
|
throw new Error(`refusing to use symlinked native-agent path ${candidate}`);
|
|
@@ -1319,7 +1749,16 @@ export function syncAgentProfile({
|
|
|
1319
1749
|
privateDirectory(managedDir);
|
|
1320
1750
|
const manifestPath = path.join(managedDir, MANAGED_AGENT_MANIFEST);
|
|
1321
1751
|
const prior = readManifest(manifestPath);
|
|
1322
|
-
|
|
1752
|
+
if ((nativeSlashCommands || prior?.commands?.length > 0)
|
|
1753
|
+
&& fs.existsSync(commandsDir)
|
|
1754
|
+
&& fs.lstatSync(commandsDir).isSymbolicLink()) {
|
|
1755
|
+
throw new Error(`refusing to use symlinked native-agent path ${commandsDir}`);
|
|
1756
|
+
}
|
|
1757
|
+
const active = renderManagedAgents(client, tenantId, agents, invocation, {
|
|
1758
|
+
directCodeMode,
|
|
1759
|
+
nativeParentDirect,
|
|
1760
|
+
environment,
|
|
1761
|
+
});
|
|
1323
1762
|
const activeBindings = new Set(active.map((agent) =>
|
|
1324
1763
|
`${agent.agentId}\0${agent.scopeParam}\0${agent.policyFingerprint}`
|
|
1325
1764
|
));
|
|
@@ -1327,16 +1766,27 @@ export function syncAgentProfile({
|
|
|
1327
1766
|
.filter((binding) => !activeBindings.has(
|
|
1328
1767
|
`${binding.agentId}\0${binding.scopeParam}\0${binding.policyFingerprint}`,
|
|
1329
1768
|
));
|
|
1330
|
-
const retired = renderRetiredManagedAgents(client, tenantId, retiredBindings, active,
|
|
1769
|
+
const retired = renderRetiredManagedAgents(client, tenantId, retiredBindings, active, invocation, {
|
|
1770
|
+
directCodeMode,
|
|
1771
|
+
environment,
|
|
1772
|
+
});
|
|
1331
1773
|
const rendered = [...active, ...retired];
|
|
1774
|
+
const commands = client === "claude" && nativeSlashCommands
|
|
1775
|
+
? renderManagedClaudeSlashCommands(tenantId, agents)
|
|
1776
|
+
: [];
|
|
1777
|
+
const renderedFiles = rendered.filter(({ fileName }) => typeof fileName === "string");
|
|
1332
1778
|
if (new Set(rendered.map(({ name }) => name)).size !== rendered.length
|
|
1333
|
-
|| new Set(
|
|
1779
|
+
|| new Set(renderedFiles.map(({ fileName }) => fileName)).size !== renderedFiles.length
|
|
1334
1780
|
|| (client === "codex" && directProfiles
|
|
1335
1781
|
&& new Set(rendered.map(({ profileFileName }) => profileFileName)).size !== rendered.length)) {
|
|
1336
1782
|
throw new Error("generated native-agent destinations are not unique");
|
|
1337
1783
|
}
|
|
1784
|
+
if (new Set(commands.map(({ fileName }) => fileName)).size !== commands.length) {
|
|
1785
|
+
throw new Error("generated native-agent slash command destinations are not unique");
|
|
1786
|
+
}
|
|
1338
1787
|
const priorFiles = new Set(prior?.files || []);
|
|
1339
1788
|
const priorProfiles = new Set(prior?.profiles || []);
|
|
1789
|
+
const priorCommands = new Set(prior?.commands || []);
|
|
1340
1790
|
const priorUsesDiscoveryRoot = Number.isInteger(prior?.version)
|
|
1341
1791
|
&& prior.version >= 2
|
|
1342
1792
|
&& prior.version <= MANAGED_AGENT_MANIFEST_VERSION;
|
|
@@ -1362,14 +1812,16 @@ export function syncAgentProfile({
|
|
|
1362
1812
|
// Preflight every destination before writing so an unmanaged file with the
|
|
1363
1813
|
// same generated name is never overwritten.
|
|
1364
1814
|
for (const agent of rendered) {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
if (fs.
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1815
|
+
if (agent.fileName) {
|
|
1816
|
+
const destination = path.join(agentsDir, agent.fileName);
|
|
1817
|
+
if (fs.existsSync(destination)) {
|
|
1818
|
+
if (fs.lstatSync(destination).isSymbolicLink()) {
|
|
1819
|
+
throw new Error(`refusing to overwrite symlinked native-agent file ${destination}`);
|
|
1820
|
+
}
|
|
1821
|
+
if ((!priorOwnsManagedFiles || !priorFiles.has(agent.fileName))
|
|
1822
|
+
&& !reclaimable(`agents/${agent.fileName}`, destination)) {
|
|
1823
|
+
throw new Error(`refusing to overwrite unmanaged native-agent file ${destination}`);
|
|
1824
|
+
}
|
|
1373
1825
|
}
|
|
1374
1826
|
}
|
|
1375
1827
|
if (client === "codex" && directProfiles) {
|
|
@@ -1385,11 +1837,34 @@ export function syncAgentProfile({
|
|
|
1385
1837
|
}
|
|
1386
1838
|
}
|
|
1387
1839
|
}
|
|
1840
|
+
for (const command of commands) {
|
|
1841
|
+
const destination = path.join(commandsDir, command.fileName);
|
|
1842
|
+
if (fs.existsSync(destination)) {
|
|
1843
|
+
if (fs.lstatSync(destination).isSymbolicLink()) {
|
|
1844
|
+
throw new Error(`refusing to overwrite symlinked native-agent slash command ${destination}`);
|
|
1845
|
+
}
|
|
1846
|
+
if ((!priorOwnsManagedFiles || !priorCommands.has(command.fileName))
|
|
1847
|
+
&& !reclaimable(`commands/${command.fileName}`, destination)) {
|
|
1848
|
+
throw new Error(`refusing to overwrite unmanaged native-agent slash command ${destination}`);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
const parentDirectPermissions = client === "claude"
|
|
1853
|
+
? syncClaudeParentDirectPermissions(
|
|
1854
|
+
root,
|
|
1855
|
+
parentDirectPermissionTools(rendered),
|
|
1856
|
+
priorParentDirectPermissionGrants(prior, priorOwnsManagedFiles),
|
|
1857
|
+
)
|
|
1858
|
+
: null;
|
|
1388
1859
|
for (const agent of rendered) {
|
|
1389
|
-
atomicPrivateWrite(path.join(agentsDir, agent.fileName), agent.contents);
|
|
1860
|
+
if (agent.fileName) atomicPrivateWrite(path.join(agentsDir, agent.fileName), agent.contents);
|
|
1390
1861
|
if (client === "codex" && directProfiles) atomicPrivateWrite(path.join(root, agent.profileFileName), agent.profileContents);
|
|
1391
1862
|
}
|
|
1392
|
-
const
|
|
1863
|
+
for (const command of commands) {
|
|
1864
|
+
atomicPrivateWrite(path.join(commandsDir, command.fileName), command.contents);
|
|
1865
|
+
}
|
|
1866
|
+
const currentFiles = new Set(renderedFiles.map((agent) => agent.fileName));
|
|
1867
|
+
const currentCommands = new Set(commands.map((command) => command.fileName));
|
|
1393
1868
|
const currentProfiles = new Set(client === "codex" && directProfiles
|
|
1394
1869
|
? rendered.map((agent) => agent.profileFileName)
|
|
1395
1870
|
: []);
|
|
@@ -1422,6 +1897,15 @@ export function syncAgentProfile({
|
|
|
1422
1897
|
fs.rmSync(path.join(root, stale), { force: true });
|
|
1423
1898
|
}
|
|
1424
1899
|
}
|
|
1900
|
+
for (const stale of client === "claude" ? prior?.commands || [] : []) {
|
|
1901
|
+
if (
|
|
1902
|
+
staleRemovable(stale, `commands/${stale}`, path.join(commandsDir, stale))
|
|
1903
|
+
&& stale.endsWith(".md")
|
|
1904
|
+
&& !currentCommands.has(stale)
|
|
1905
|
+
) {
|
|
1906
|
+
fs.rmSync(path.join(commandsDir, stale), { force: true });
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1425
1909
|
if (!priorUsesDiscoveryRoot) {
|
|
1426
1910
|
for (const legacy of prior?.files || []) {
|
|
1427
1911
|
if (
|
|
@@ -1433,20 +1917,45 @@ export function syncAgentProfile({
|
|
|
1433
1917
|
}
|
|
1434
1918
|
}
|
|
1435
1919
|
}
|
|
1920
|
+
if (client === "claude") {
|
|
1921
|
+
ensureClaudeNativeInterceptHook(root, tenantId, {
|
|
1922
|
+
enabled: nativeIntercept,
|
|
1923
|
+
...(nativeIntercept ? { timeoutMs: interceptTimeoutMs } : {}),
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1436
1926
|
const contentDigests = Object.fromEntries(rendered.flatMap((agent) => [
|
|
1437
|
-
[`agents/${agent.fileName}`, contentDigest(agent.contents)],
|
|
1927
|
+
...(agent.fileName ? [[`agents/${agent.fileName}`, contentDigest(agent.contents)]] : []),
|
|
1438
1928
|
...(client === "codex" && directProfiles
|
|
1439
1929
|
? [[agent.profileFileName, contentDigest(agent.profileContents)]]
|
|
1440
1930
|
: []),
|
|
1441
|
-
]).
|
|
1931
|
+
]).concat(commands.map((command) => [
|
|
1932
|
+
`commands/${command.fileName}`,
|
|
1933
|
+
contentDigest(command.contents),
|
|
1934
|
+
])).sort(([left], [right]) => left.localeCompare(right)));
|
|
1935
|
+
const telemetryEnvironment = nativeAgentTelemetryEnvironment(environment);
|
|
1936
|
+
const benchmark = nativeBenchmarkHeaderValue(environment);
|
|
1442
1937
|
atomicPrivateWrite(manifestPath, `${JSON.stringify({
|
|
1443
1938
|
version: MANAGED_AGENT_MANIFEST_VERSION,
|
|
1444
1939
|
tenantId,
|
|
1445
1940
|
client,
|
|
1446
1941
|
directProfiles: client === "codex" && directProfiles,
|
|
1447
1942
|
directCodeMode: client === "codex" && directCodeMode,
|
|
1943
|
+
...(client === "claude" && nativeParentDirect ? { nativeParentDirect: true } : {}),
|
|
1944
|
+
...(client === "claude" && parentDirectPermissions.length > 0 ? {
|
|
1945
|
+
nativeParentDirectPermissionGrants: parentDirectPermissions,
|
|
1946
|
+
} : {}),
|
|
1947
|
+
...(client === "claude" && nativeSlashCommands ? { nativeSlashCommands: true } : {}),
|
|
1948
|
+
...(client === "claude" && nativeIntercept ? {
|
|
1949
|
+
nativeIntercept: true,
|
|
1950
|
+
nativeInterceptTimeoutMs: interceptTimeoutMs,
|
|
1951
|
+
} : {}),
|
|
1952
|
+
...(Object.keys(telemetryEnvironment).length > 0 ? {
|
|
1953
|
+
nativeAgentTelemetryEnvironment: telemetryEnvironment,
|
|
1954
|
+
} : {}),
|
|
1955
|
+
...(benchmark ? { nativeBenchmark: true } : {}),
|
|
1448
1956
|
syncedAt: new Date(now).toISOString(),
|
|
1449
1957
|
files: [...currentFiles].sort(),
|
|
1958
|
+
...(client === "claude" && nativeSlashCommands ? { commands: [...currentCommands].sort() } : {}),
|
|
1450
1959
|
profiles: [...currentProfiles].sort(),
|
|
1451
1960
|
contentDigests,
|
|
1452
1961
|
agents: rendered.map((agent) => ({
|
|
@@ -1458,7 +1967,9 @@ export function syncAgentProfile({
|
|
|
1458
1967
|
retired: agent.retired,
|
|
1459
1968
|
name: agent.name,
|
|
1460
1969
|
...(client === "claude" ? {
|
|
1461
|
-
|
|
1970
|
+
...(agent.parentLaunchDefinition
|
|
1971
|
+
? { launchMode: "parent-direct", parentLaunchDefinition: agent.parentLaunchDefinition }
|
|
1972
|
+
: { launchDefinition: agent.launchDefinition }),
|
|
1462
1973
|
} : {}),
|
|
1463
1974
|
...(client === "codex" && directProfiles ? {
|
|
1464
1975
|
profileName: agent.profileName,
|
|
@@ -1474,6 +1985,7 @@ export function syncAgentProfile({
|
|
|
1474
1985
|
count: active.length,
|
|
1475
1986
|
recoveryCount: retired.length,
|
|
1476
1987
|
files: [...currentFiles],
|
|
1988
|
+
...(client === "claude" && nativeSlashCommands ? { commands: [...currentCommands] } : {}),
|
|
1477
1989
|
profiles: [...currentProfiles],
|
|
1478
1990
|
};
|
|
1479
1991
|
}
|