impel-cli 0.20.37 → 0.20.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -12
- package/RELEASE_NOTES.md +21 -0
- package/bin/impel.js +10 -1
- package/docs/native-agent-host-capability-matrix.md +16 -4
- package/package.json +1 -1
- package/scripts/capture-release-fixtures.mjs +56 -0
- package/scripts/clean-machine/README.md +51 -0
- package/scripts/clean-machine/run-clean-windows.sh +115 -0
- package/scripts/clean-machine/userdata.ps1.template +102 -0
- package/scripts/regen-managed-artifacts.mjs +475 -0
- package/scripts/verify-vendor-pins.mjs +207 -0
- package/src/agents.js +256 -32
- package/src/apps.js +233 -2
- package/src/cli.js +1 -0
- package/src/commands/apps.js +25 -2
- package/src/commands/launch.js +177 -4
- package/src/commands/mcp.js +1 -1
- package/src/commands/sessions.js +44 -11
- package/src/commands/token.js +23 -3
- package/src/macSetup.js +9 -8
- package/src/nativeAgentTransport.js +13 -4
- package/src/selfInvocation.js +12 -2
- package/src/vendorCliBinaries.js +36 -7
- package/src/verbatimRelay.js +9 -1
package/src/agents.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "./config.js";
|
|
18
18
|
import {
|
|
19
19
|
CODEX_NATIVE_AGENT_ATTACHMENT_WINDOW_MS,
|
|
20
|
+
IMPEL_MANAGED_MCP_ENV,
|
|
20
21
|
IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV,
|
|
21
22
|
IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES,
|
|
22
23
|
IMPEL_NATIVE_AGENT_MCP_TARGET,
|
|
@@ -55,7 +56,7 @@ export const NATIVE_AGENT_RESUME_TOOL = "resume_native_agent_run";
|
|
|
55
56
|
export const NATIVE_AGENT_RECOVER_TOOL = "recover_native_agent_runs";
|
|
56
57
|
export const NATIVE_AGENT_CONTINUATION_SCHEMA = "impel.native-agent-continuation.v1";
|
|
57
58
|
export const MANAGED_AGENT_MCP_SERVER = "impel_agent";
|
|
58
|
-
export const MANAGED_AGENT_MANIFEST_VERSION =
|
|
59
|
+
export const MANAGED_AGENT_MANIFEST_VERSION = 22;
|
|
59
60
|
|
|
60
61
|
// The host model only selects the fixed MCP tool and faithfully returns its
|
|
61
62
|
// result. Spark minimizes those transport-only turns while the selected Eve
|
|
@@ -82,6 +83,7 @@ const TERMINAL_NATIVE_AGENT_STATUSES = new Set(["succeeded", "failed", "cancelle
|
|
|
82
83
|
const SAFE_AGENT_ID_RE = /^[A-Za-z0-9_.:-]{1,160}$/u;
|
|
83
84
|
const SAFE_SCOPE_PARAM_RE = /^[A-Za-z0-9_.:-]{1,160}$/u;
|
|
84
85
|
const MAX_CATALOG_ITEMS = 500;
|
|
86
|
+
const MAX_CLAUDE_INLINE_AGENT_JSON_BYTES = 12 * 1024;
|
|
85
87
|
const RETRYABLE_AGENT_CATALOG_ERROR = /request timed out|fetch failed|ECONNRESET|ETIMEDOUT|EAI_AGAIN|network error/iu;
|
|
86
88
|
const RESERVED_AGENT_NAMES = Object.freeze({
|
|
87
89
|
claude: new Set(["explore", "general-purpose", "plan"]),
|
|
@@ -621,7 +623,7 @@ function claudeAdapterInstructions(tenantId, agent) {
|
|
|
621
623
|
`Confirm that the request fits the synchronized capabilities ${JSON.stringify(agent.capabilities)} and none of the exclusions ${JSON.stringify(agent.exclusions)}.${contextRequirement}`,
|
|
622
624
|
sideEffectInstruction,
|
|
623
625
|
`Call ${nativeToolName(NATIVE_AGENT_ANSWER_TOOL)} exactly once with question set to the complete assigned task, optional context set to one string containing all supplied context, and contextKeys naming the fields present in that string.`,
|
|
624
|
-
`If the bounded 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. Never change its schema
|
|
626
|
+
`If the bounded 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. Never change its schema, add handle metadata, or fabricate a continuation from any other field. If the answer call is cancelled or fails before returning any continuation, call ${nativeToolName(NATIVE_AGENT_ANSWER_TOOL)} again with the same question; once a continuation has been returned, never call it again for this request.`,
|
|
625
627
|
completionGuidance,
|
|
626
628
|
].join(" ");
|
|
627
629
|
}
|
|
@@ -671,7 +673,7 @@ function codexAdapterInstructions(tenantId, agent) {
|
|
|
671
673
|
`Confirm that the assigned request fits the cataloged capabilities ${JSON.stringify(agent.capabilities)} and none of the exclusions ${JSON.stringify(agent.exclusions)} before answering.${contextRequirement}`,
|
|
672
674
|
sideEffectInstruction,
|
|
673
675
|
`Call ${nativeToolName(NATIVE_AGENT_ANSWER_TOOL)} exactly once with question set to the complete assigned task, optional context set to one string containing all supplied context, and contextKeys naming the fields present in that string.`,
|
|
674
|
-
`If the bounded 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. Never change its schema
|
|
676
|
+
`If the bounded 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. Never change its schema, add handle metadata, or fabricate a continuation from any other field. If the answer call is cancelled or fails before returning any continuation, call ${nativeToolName(NATIVE_AGENT_ANSWER_TOOL)} again with the same question; once a continuation has been returned, never call it again for this request.`,
|
|
675
677
|
completionGuidance,
|
|
676
678
|
].join("\n\n");
|
|
677
679
|
}
|
|
@@ -711,44 +713,73 @@ function customAgentDescription(tenantId, agent) {
|
|
|
711
713
|
).slice(0, 900);
|
|
712
714
|
}
|
|
713
715
|
|
|
714
|
-
function
|
|
716
|
+
function claudeAgentDefinition({ tenantId, agent, invocation, recoveryOnly = false }) {
|
|
715
717
|
const description = recoveryOnly
|
|
716
718
|
? `Recovery-only access to pending runs for retired Impel binding ${agent.agentId} in tenant ${tenantId}.`
|
|
717
719
|
: customAgentDescription(tenantId, agent);
|
|
718
720
|
const toolNames = recoveryOnly ? NATIVE_AGENT_RECOVERY_TOOL_NAMES : nativeAgentToolNames(agent);
|
|
719
721
|
const eager = eagerNativeAgentTransportEnabled();
|
|
722
|
+
return {
|
|
723
|
+
description,
|
|
724
|
+
prompt: recoveryOnly ? retiredAdapterInstructions(tenantId, agent) : claudeAdapterInstructions(tenantId, agent),
|
|
725
|
+
model: "haiku",
|
|
726
|
+
...(eager ? {
|
|
727
|
+
permissionMode: "bypassPermissions",
|
|
728
|
+
tools: toolNames.map((tool) => nativeToolName(tool)),
|
|
729
|
+
} : {}),
|
|
730
|
+
mcpServers: [{
|
|
731
|
+
[MANAGED_AGENT_MCP_SERVER]: {
|
|
732
|
+
type: "stdio",
|
|
733
|
+
command: invocation.command,
|
|
734
|
+
args: [...invocation.args],
|
|
735
|
+
env: { ...(invocation.env || {}) },
|
|
736
|
+
},
|
|
737
|
+
}],
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function renderClaudeAgentDefinition(name, definition) {
|
|
742
|
+
const server = definition.mcpServers[0][MANAGED_AGENT_MCP_SERVER];
|
|
720
743
|
const lines = [
|
|
721
744
|
"---",
|
|
722
745
|
`name: ${JSON.stringify(name)}`,
|
|
723
|
-
`description: ${JSON.stringify(description)}`,
|
|
724
|
-
|
|
725
|
-
...(
|
|
746
|
+
`description: ${JSON.stringify(definition.description)}`,
|
|
747
|
+
`model: ${definition.model}`,
|
|
748
|
+
...(definition.permissionMode
|
|
726
749
|
? [
|
|
727
750
|
// This agent receives only the exact fixed-binding MCP allowlist below,
|
|
728
751
|
// so bypassing prompts cannot grant filesystem, shell, connector, or
|
|
729
752
|
// arbitrary MCP access. It lets non-interactive/dontAsk hosts execute
|
|
730
753
|
// the selected managed adapter instead of auto-denying its sole call.
|
|
731
|
-
|
|
754
|
+
`permissionMode: ${definition.permissionMode}`,
|
|
732
755
|
"tools:",
|
|
733
|
-
...
|
|
756
|
+
...definition.tools.map((tool) => ` - ${JSON.stringify(tool)}`),
|
|
734
757
|
]
|
|
735
758
|
: []),
|
|
736
759
|
"mcpServers:",
|
|
737
760
|
` - ${MANAGED_AGENT_MCP_SERVER}:`,
|
|
738
761
|
" type: stdio",
|
|
739
|
-
` command: ${JSON.stringify(
|
|
762
|
+
` command: ${JSON.stringify(server.command)}`,
|
|
740
763
|
" args:",
|
|
741
|
-
...
|
|
764
|
+
...server.args.map((argument) => ` - ${JSON.stringify(argument)}`),
|
|
742
765
|
" env:",
|
|
743
|
-
...Object.entries(
|
|
766
|
+
...Object.entries(server.env).map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`),
|
|
744
767
|
"---",
|
|
745
768
|
"",
|
|
746
|
-
|
|
769
|
+
definition.prompt,
|
|
747
770
|
"",
|
|
748
771
|
];
|
|
749
772
|
return lines.join("\n");
|
|
750
773
|
}
|
|
751
774
|
|
|
775
|
+
function renderClaudeAgent({ tenantId, agent, name, invocation, recoveryOnly = false }) {
|
|
776
|
+
const definition = claudeAgentDefinition({ tenantId, agent, invocation, recoveryOnly });
|
|
777
|
+
return {
|
|
778
|
+
contents: renderClaudeAgentDefinition(name, definition),
|
|
779
|
+
launchDefinition: definition,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
752
783
|
function codexInvocationEnvironment(invocation, { durableProfile = false } = {}) {
|
|
753
784
|
const entries = Object.entries(invocation.env || {});
|
|
754
785
|
const transient = new Set(IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES);
|
|
@@ -796,6 +827,13 @@ function renderCodexConfiguration({
|
|
|
796
827
|
...(directCodeMode ? [
|
|
797
828
|
"[features]",
|
|
798
829
|
"multi_agent = false",
|
|
830
|
+
// Fixed-binding adapters cannot use host plugins. Disable both local and
|
|
831
|
+
// remote plugin paths so profile layering does not load the ordinary
|
|
832
|
+
// tenant Codex plugin catalog or make unrelated catalog requests.
|
|
833
|
+
"plugins = false",
|
|
834
|
+
"remote_plugin = false",
|
|
835
|
+
"plugin_sharing = false",
|
|
836
|
+
"recommended_plugins = false",
|
|
799
837
|
"",
|
|
800
838
|
"[features.code_mode]",
|
|
801
839
|
"enabled = true",
|
|
@@ -871,9 +909,11 @@ export function renderManagedAgents(client, tenantId, agents, invocation = null,
|
|
|
871
909
|
// collision-safe, filesystem-safe identifiers, so use them as filenames.
|
|
872
910
|
const fileStem = client === "claude" ? name : fileStems[index];
|
|
873
911
|
const boundInvocation = boundNativeAgentInvocation(normalizedTenant, agent, invocation);
|
|
874
|
-
const
|
|
912
|
+
const claudeRendered = client === "claude"
|
|
875
913
|
? renderClaudeAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation })
|
|
876
|
-
:
|
|
914
|
+
: null;
|
|
915
|
+
const contents = claudeRendered?.contents
|
|
916
|
+
?? renderCodexAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation, directCodeMode });
|
|
877
917
|
const profileName = client === "codex" ? fileStem : null;
|
|
878
918
|
return {
|
|
879
919
|
agentId: agent.agentId,
|
|
@@ -885,6 +925,7 @@ export function renderManagedAgents(client, tenantId, agents, invocation = null,
|
|
|
885
925
|
name,
|
|
886
926
|
fileName: `${fileStem}${extension}`,
|
|
887
927
|
contents,
|
|
928
|
+
...(claudeRendered ? { launchDefinition: claudeRendered.launchDefinition } : {}),
|
|
888
929
|
...(profileName ? {
|
|
889
930
|
profileName,
|
|
890
931
|
profileFileName: `${profileName}.config.toml`,
|
|
@@ -939,9 +980,11 @@ function renderRetiredManagedAgents(client, tenantId, bindings, active, invocati
|
|
|
939
980
|
policyFingerprint: binding.policyFingerprint,
|
|
940
981
|
mode: "recovery",
|
|
941
982
|
});
|
|
942
|
-
const
|
|
983
|
+
const claudeRendered = client === "claude"
|
|
943
984
|
? renderClaudeAgent({ tenantId, agent, name, invocation: boundInvocation, recoveryOnly: true })
|
|
944
|
-
:
|
|
985
|
+
: null;
|
|
986
|
+
const contents = claudeRendered?.contents
|
|
987
|
+
?? renderCodexAgent({ tenantId, agent, name, invocation: boundInvocation, recoveryOnly: true, directCodeMode });
|
|
945
988
|
const profileName = client === "codex" ? path.basename(fileName, extension) : null;
|
|
946
989
|
return {
|
|
947
990
|
agentId: binding.agentId,
|
|
@@ -953,6 +996,7 @@ function renderRetiredManagedAgents(client, tenantId, bindings, active, invocati
|
|
|
953
996
|
name,
|
|
954
997
|
fileName,
|
|
955
998
|
contents,
|
|
999
|
+
...(claudeRendered ? { launchDefinition: claudeRendered.launchDefinition } : {}),
|
|
956
1000
|
...(profileName ? {
|
|
957
1001
|
profileName,
|
|
958
1002
|
profileFileName: `${profileName}.config.toml`,
|
|
@@ -999,7 +1043,8 @@ function managedManifestPath(root) {
|
|
|
999
1043
|
}
|
|
1000
1044
|
|
|
1001
1045
|
function contentDigest(contents) {
|
|
1002
|
-
|
|
1046
|
+
const bytes = Buffer.isBuffer(contents) ? contents : Buffer.from(contents, "utf8");
|
|
1047
|
+
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
1003
1048
|
}
|
|
1004
1049
|
|
|
1005
1050
|
function managedArtifactIsCurrent(root, relativePath, expectedDigest) {
|
|
@@ -1009,8 +1054,9 @@ function managedArtifactIsCurrent(root, relativePath, expectedDigest) {
|
|
|
1009
1054
|
if (!/^[a-f0-9]{64}$/u.test(expectedDigest || "")) return false;
|
|
1010
1055
|
const artifactPath = path.join(root, ...segments);
|
|
1011
1056
|
try {
|
|
1012
|
-
|
|
1013
|
-
|
|
1057
|
+
const stat = fs.lstatSync(artifactPath);
|
|
1058
|
+
if (stat.isSymbolicLink() || !stat.isFile()) return false;
|
|
1059
|
+
return contentDigest(fs.readFileSync(artifactPath)) === expectedDigest;
|
|
1014
1060
|
} catch {
|
|
1015
1061
|
return false;
|
|
1016
1062
|
}
|
|
@@ -1033,6 +1079,156 @@ export function readManagedAgentManifest(root) {
|
|
|
1033
1079
|
return readManifest(managedManifestPath(root));
|
|
1034
1080
|
}
|
|
1035
1081
|
|
|
1082
|
+
function exactObjectKeys(value, keys) {
|
|
1083
|
+
return value
|
|
1084
|
+
&& typeof value === "object"
|
|
1085
|
+
&& !Array.isArray(value)
|
|
1086
|
+
&& JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...keys].sort());
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function validateClaudeLaunchDefinition(record, tenantId) {
|
|
1090
|
+
const definition = record.launchDefinition;
|
|
1091
|
+
if (!exactObjectKeys(definition, [
|
|
1092
|
+
"description", "prompt", "model", "permissionMode", "tools", "mcpServers",
|
|
1093
|
+
])
|
|
1094
|
+
|| typeof definition.description !== "string"
|
|
1095
|
+
|| !definition.description
|
|
1096
|
+
|| definition.description.length > 900
|
|
1097
|
+
|| typeof definition.prompt !== "string"
|
|
1098
|
+
|| !definition.prompt
|
|
1099
|
+
|| Buffer.byteLength(definition.prompt, "utf8") > 256 * 1024
|
|
1100
|
+
|| definition.model !== "haiku"
|
|
1101
|
+
|| definition.permissionMode !== "bypassPermissions"
|
|
1102
|
+
|| !Array.isArray(definition.tools)
|
|
1103
|
+
|| definition.tools.length < 2
|
|
1104
|
+
|| definition.tools.length > 3
|
|
1105
|
+
|| new Set(definition.tools).size !== definition.tools.length
|
|
1106
|
+
|| !definition.tools.every((tool) => [
|
|
1107
|
+
NATIVE_AGENT_ANSWER_TOOL,
|
|
1108
|
+
NATIVE_AGENT_RUN_TOOL,
|
|
1109
|
+
NATIVE_AGENT_RESUME_TOOL,
|
|
1110
|
+
NATIVE_AGENT_RECOVER_TOOL,
|
|
1111
|
+
].map((name) => nativeToolName(name)).includes(tool))
|
|
1112
|
+
|| !Array.isArray(definition.mcpServers)
|
|
1113
|
+
|| definition.mcpServers.length !== 1
|
|
1114
|
+
|| !exactObjectKeys(definition.mcpServers[0], [MANAGED_AGENT_MCP_SERVER])) {
|
|
1115
|
+
throw new Error("the managed Claude launch definition is invalid; run `impel agents sync claude`");
|
|
1116
|
+
}
|
|
1117
|
+
const server = definition.mcpServers[0][MANAGED_AGENT_MCP_SERVER];
|
|
1118
|
+
const allowedEnvironment = new Set([IMPEL_MANAGED_MCP_ENV, ...IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES]);
|
|
1119
|
+
if (!exactObjectKeys(server, ["type", "command", "args", "env"])
|
|
1120
|
+
|| server.type !== "stdio"
|
|
1121
|
+
|| typeof server.command !== "string"
|
|
1122
|
+
|| !server.command
|
|
1123
|
+
|| !Array.isArray(server.args)
|
|
1124
|
+
|| !server.args.every((argument) => typeof argument === "string")
|
|
1125
|
+
|| !server.env
|
|
1126
|
+
|| typeof server.env !== "object"
|
|
1127
|
+
|| Array.isArray(server.env)
|
|
1128
|
+
|| server.env[IMPEL_MANAGED_MCP_ENV] !== "1"
|
|
1129
|
+
|| !Object.entries(server.env).every(([key, value]) =>
|
|
1130
|
+
allowedEnvironment.has(key)
|
|
1131
|
+
&& typeof value === "string"
|
|
1132
|
+
&& value.length <= 4096
|
|
1133
|
+
&& redactCredentialText(value) === value
|
|
1134
|
+
)) {
|
|
1135
|
+
throw new Error("the managed Claude MCP definition is invalid; run `impel agents sync claude`");
|
|
1136
|
+
}
|
|
1137
|
+
const answerOnly = server.args.at(-1) === "--answer-only";
|
|
1138
|
+
const trustedInvocation = impelNativeAgentMcpInvocation({
|
|
1139
|
+
tenantId,
|
|
1140
|
+
agentId: record.agentId,
|
|
1141
|
+
scopeParam: record.scopeParam,
|
|
1142
|
+
policyFingerprint: record.policyFingerprint,
|
|
1143
|
+
mode: answerOnly ? "answer" : "durable",
|
|
1144
|
+
});
|
|
1145
|
+
const expectedTools = (answerOnly
|
|
1146
|
+
? [NATIVE_AGENT_ANSWER_TOOL, NATIVE_AGENT_RESUME_TOOL]
|
|
1147
|
+
: NATIVE_AGENT_TOOL_NAMES
|
|
1148
|
+
).map((name) => nativeToolName(name));
|
|
1149
|
+
if (server.command !== trustedInvocation.command
|
|
1150
|
+
|| JSON.stringify(server.args) !== JSON.stringify(trustedInvocation.args)
|
|
1151
|
+
|| JSON.stringify(definition.tools) !== JSON.stringify(expectedTools)) {
|
|
1152
|
+
throw new Error("the managed Claude binding is invalid; run `impel agents sync claude`");
|
|
1153
|
+
}
|
|
1154
|
+
return definition;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// A selector absent from the Impel manifest remains Claude-native. A matching
|
|
1158
|
+
// record is integrity checked, then re-declared under a random per-launch name
|
|
1159
|
+
// through Claude's higher-priority --agents source. This prevents project,
|
|
1160
|
+
// user, plugin, and fixed managed-settings names from replacing the adapter.
|
|
1161
|
+
export function resolveManagedClaudeAgent(root, tenantId, selector) {
|
|
1162
|
+
const normalizedTenant = normalizeTenantId(tenantId);
|
|
1163
|
+
if (typeof selector !== "string" || !selector || selector.length > 512) return null;
|
|
1164
|
+
const manifest = readManagedAgentManifest(root);
|
|
1165
|
+
if (!manifest
|
|
1166
|
+
|| manifest.version !== MANAGED_AGENT_MANIFEST_VERSION
|
|
1167
|
+
|| manifest.client !== "claude"
|
|
1168
|
+
|| manifest.tenantId !== normalizedTenant
|
|
1169
|
+
|| !Array.isArray(manifest.agents)) {
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
const records = manifest.agents.filter((record) => record
|
|
1173
|
+
&& typeof record === "object"
|
|
1174
|
+
&& typeof record.name === "string");
|
|
1175
|
+
const active = records.filter((record) => !record.retired);
|
|
1176
|
+
let matches = active.filter((record) => record.agentId === selector);
|
|
1177
|
+
if (matches.length === 0) matches = active.filter((record) => record.title === selector);
|
|
1178
|
+
if (matches.length === 0) matches = active.filter((record) => record.name === selector);
|
|
1179
|
+
if (matches.length === 0 && records.some((record) => record.retired && [
|
|
1180
|
+
record.agentId,
|
|
1181
|
+
record.title,
|
|
1182
|
+
record.name,
|
|
1183
|
+
].includes(selector))) {
|
|
1184
|
+
throw new Error(`managed Claude agent ${JSON.stringify(selector)} is retired and cannot start new work`);
|
|
1185
|
+
}
|
|
1186
|
+
if (matches.length === 0) return null;
|
|
1187
|
+
if (matches.length !== 1) {
|
|
1188
|
+
throw new Error(
|
|
1189
|
+
`managed Claude agent ${JSON.stringify(selector)} is ambiguous for tenant ${JSON.stringify(normalizedTenant)}`,
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
const [record] = matches;
|
|
1193
|
+
if (typeof record.agentId !== "string"
|
|
1194
|
+
|| !SAFE_AGENT_ID_RE.test(record.agentId)
|
|
1195
|
+
|| typeof record.scopeParam !== "string"
|
|
1196
|
+
|| !SAFE_SCOPE_PARAM_RE.test(record.scopeParam)
|
|
1197
|
+
|| !/^[a-f0-9]{64}$/u.test(record.policyFingerprint || "")) {
|
|
1198
|
+
throw new Error("the managed Claude binding metadata is invalid; run `impel agents sync claude`");
|
|
1199
|
+
}
|
|
1200
|
+
const fileName = `${record.name}.md`;
|
|
1201
|
+
if (path.basename(fileName) !== fileName || !manifest.files.includes(fileName)) {
|
|
1202
|
+
throw new Error("the managed Claude agent mapping is invalid; run `impel agents sync claude`");
|
|
1203
|
+
}
|
|
1204
|
+
const relativePath = `agents/${fileName}`;
|
|
1205
|
+
if (!managedArtifactIsCurrent(root, relativePath, manifest.contentDigests?.[relativePath])) {
|
|
1206
|
+
throw new Error("the managed Claude agent failed its integrity check; run `impel agents sync claude`");
|
|
1207
|
+
}
|
|
1208
|
+
const launchDefinition = validateClaudeLaunchDefinition(record, normalizedTenant);
|
|
1209
|
+
if (contentDigest(renderClaudeAgentDefinition(record.name, launchDefinition))
|
|
1210
|
+
!== manifest.contentDigests?.[relativePath]) {
|
|
1211
|
+
throw new Error("the managed Claude launch definition does not match its agent file; run `impel agents sync claude`");
|
|
1212
|
+
}
|
|
1213
|
+
const suffix = crypto.randomBytes(12).toString("hex");
|
|
1214
|
+
const prefix = `impel-${record.name}`.slice(0, 63 - suffix.length - 1).replace(/-+$/u, "");
|
|
1215
|
+
const launchName = `${prefix}-${suffix}`;
|
|
1216
|
+
const launchAgentsJson = JSON.stringify({ [launchName]: launchDefinition });
|
|
1217
|
+
if (Buffer.byteLength(launchAgentsJson, "utf8") > MAX_CLAUDE_INLINE_AGENT_JSON_BYTES) {
|
|
1218
|
+
throw new Error(
|
|
1219
|
+
`managed Claude agent ${JSON.stringify(selector)} is too large for a safe cross-platform direct launch; shorten its synchronized catalog policy`,
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
return {
|
|
1223
|
+
...record,
|
|
1224
|
+
fileName,
|
|
1225
|
+
path: path.join(root, "agents", fileName),
|
|
1226
|
+
launchName,
|
|
1227
|
+
launchDefinition,
|
|
1228
|
+
launchAgentsJson,
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1036
1232
|
export function resolveManagedCodexAgentProfile(root, tenantId, selector) {
|
|
1037
1233
|
const normalizedTenant = normalizeTenantId(tenantId);
|
|
1038
1234
|
const normalizedSelector = boundedString(selector, "agent selector", { max: 512 });
|
|
@@ -1144,9 +1340,23 @@ export function syncAgentProfile({
|
|
|
1144
1340
|
const priorUsesDiscoveryRoot = Number.isInteger(prior?.version)
|
|
1145
1341
|
&& prior.version >= 2
|
|
1146
1342
|
&& prior.version <= MANAGED_AGENT_MANIFEST_VERSION;
|
|
1147
|
-
|
|
1148
|
-
|
|
1343
|
+
// Ownership requires the client and tenant to match on every leg; a manifest
|
|
1344
|
+
// from another scope must neither authorize overwrites nor deletions.
|
|
1345
|
+
const priorOwnsManagedFiles = priorUsesDiscoveryRoot
|
|
1346
|
+
&& prior?.client === client
|
|
1149
1347
|
&& prior?.tenantId === tenantId;
|
|
1348
|
+
// A colliding artifact whose bytes match the recorded digest of a
|
|
1349
|
+
// non-owning manifest is provably impel-generated (e.g. after a tenant
|
|
1350
|
+
// scope-key drift), so reclaiming it cannot lose operator-authored content.
|
|
1351
|
+
const reclaimable = (digestKey, destination) => {
|
|
1352
|
+
const recorded = prior?.contentDigests?.[digestKey];
|
|
1353
|
+
if (typeof recorded !== "string" || !recorded) return false;
|
|
1354
|
+
try {
|
|
1355
|
+
return contentDigest(fs.readFileSync(destination)) === recorded;
|
|
1356
|
+
} catch {
|
|
1357
|
+
return false;
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1150
1360
|
|
|
1151
1361
|
// Native clients discover standalone definitions directly under `agents/`.
|
|
1152
1362
|
// Preflight every destination before writing so an unmanaged file with the
|
|
@@ -1154,12 +1364,13 @@ export function syncAgentProfile({
|
|
|
1154
1364
|
for (const agent of rendered) {
|
|
1155
1365
|
const destination = path.join(agentsDir, agent.fileName);
|
|
1156
1366
|
if (fs.existsSync(destination)) {
|
|
1157
|
-
if (!priorUsesDiscoveryRoot || !priorFiles.has(agent.fileName)) {
|
|
1158
|
-
throw new Error(`refusing to overwrite unmanaged native-agent file ${destination}`);
|
|
1159
|
-
}
|
|
1160
1367
|
if (fs.lstatSync(destination).isSymbolicLink()) {
|
|
1161
1368
|
throw new Error(`refusing to overwrite symlinked native-agent file ${destination}`);
|
|
1162
1369
|
}
|
|
1370
|
+
if ((!priorOwnsManagedFiles || !priorFiles.has(agent.fileName))
|
|
1371
|
+
&& !reclaimable(`agents/${agent.fileName}`, destination)) {
|
|
1372
|
+
throw new Error(`refusing to overwrite unmanaged native-agent file ${destination}`);
|
|
1373
|
+
}
|
|
1163
1374
|
}
|
|
1164
1375
|
if (client === "codex" && directProfiles) {
|
|
1165
1376
|
const profileDestination = path.join(root, agent.profileFileName);
|
|
@@ -1167,7 +1378,8 @@ export function syncAgentProfile({
|
|
|
1167
1378
|
if (fs.lstatSync(profileDestination).isSymbolicLink()) {
|
|
1168
1379
|
throw new Error(`refusing to overwrite symlinked Codex profile ${profileDestination}`);
|
|
1169
1380
|
}
|
|
1170
|
-
if (!
|
|
1381
|
+
if ((!priorOwnsManagedFiles || !priorProfiles.has(agent.profileFileName))
|
|
1382
|
+
&& !reclaimable(agent.profileFileName, profileDestination)) {
|
|
1171
1383
|
throw new Error(`refusing to overwrite unmanaged Codex profile ${profileDestination}`);
|
|
1172
1384
|
}
|
|
1173
1385
|
}
|
|
@@ -1181,20 +1393,29 @@ export function syncAgentProfile({
|
|
|
1181
1393
|
const currentProfiles = new Set(client === "codex" && directProfiles
|
|
1182
1394
|
? rendered.map((agent) => agent.profileFileName)
|
|
1183
1395
|
: []);
|
|
1396
|
+
// Stale-file cleanup in the shared discovery root: an owning manifest's
|
|
1397
|
+
// listing is authoritative, while a non-owning manifest (another tenant or
|
|
1398
|
+
// a drifted scope) may only remove files it digest-proves impel wrote —
|
|
1399
|
+
// native-profile roots are shared across tenants, so skipping cleanup
|
|
1400
|
+
// entirely would orphan the previous tenant's agents forever, and deleting
|
|
1401
|
+
// unproven listings could destroy another owner's live files. Legacy
|
|
1402
|
+
// nested manifests only ever clean impel's private managed directory below.
|
|
1403
|
+
const staleRemovable = (stale, digestKey, destination) =>
|
|
1404
|
+
typeof stale === "string"
|
|
1405
|
+
&& path.basename(stale) === stale
|
|
1406
|
+
&& (priorOwnsManagedFiles || reclaimable(digestKey, destination));
|
|
1184
1407
|
for (const stale of prior?.files || []) {
|
|
1185
1408
|
if (
|
|
1186
|
-
|
|
1187
|
-
&& path.basename(stale) === stale
|
|
1409
|
+
staleRemovable(stale, `agents/${stale}`, path.join(agentsDir, stale))
|
|
1188
1410
|
&& !currentFiles.has(stale)
|
|
1189
1411
|
&& (stale.endsWith(".md") || stale.endsWith(".toml"))
|
|
1190
1412
|
) {
|
|
1191
|
-
fs.rmSync(path.join(
|
|
1413
|
+
fs.rmSync(path.join(agentsDir, stale), { force: true });
|
|
1192
1414
|
}
|
|
1193
1415
|
}
|
|
1194
|
-
for (const stale of
|
|
1416
|
+
for (const stale of client === "codex" ? prior?.profiles || [] : []) {
|
|
1195
1417
|
if (
|
|
1196
|
-
|
|
1197
|
-
&& path.basename(stale) === stale
|
|
1418
|
+
staleRemovable(stale, stale, path.join(root, stale))
|
|
1198
1419
|
&& stale.endsWith(".config.toml")
|
|
1199
1420
|
&& !currentProfiles.has(stale)
|
|
1200
1421
|
) {
|
|
@@ -1236,6 +1457,9 @@ export function syncAgentProfile({
|
|
|
1236
1457
|
policyFingerprint: agent.policyFingerprint,
|
|
1237
1458
|
retired: agent.retired,
|
|
1238
1459
|
name: agent.name,
|
|
1460
|
+
...(client === "claude" ? {
|
|
1461
|
+
launchDefinition: agent.launchDefinition,
|
|
1462
|
+
} : {}),
|
|
1239
1463
|
...(client === "codex" && directProfiles ? {
|
|
1240
1464
|
profileName: agent.profileName,
|
|
1241
1465
|
profileFileName: agent.profileFileName,
|