impel-cli 0.20.37 → 0.20.38
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 +24 -12
- package/RELEASE_NOTES.md +12 -0
- package/docs/native-agent-host-capability-matrix.md +12 -1
- package/package.json +1 -1
- package/src/agents.js +210 -18
- package/src/cli.js +1 -0
- package/src/commands/launch.js +177 -4
- package/src/selfInvocation.js +12 -2
package/README.md
CHANGED
|
@@ -180,24 +180,35 @@ impel claude --model opus
|
|
|
180
180
|
impel codex exec "review this repository"
|
|
181
181
|
```
|
|
182
182
|
|
|
183
|
-
To run one explicitly selected tenant agent in a single
|
|
183
|
+
To run one explicitly selected tenant agent in a single native thread, select
|
|
184
184
|
it by exact agent ID (preferred) or unique exact title:
|
|
185
185
|
|
|
186
186
|
```sh
|
|
187
|
+
impel claude --agent research-agent --print "answer this question"
|
|
187
188
|
impel codex --agent research-agent exec "answer this question"
|
|
188
189
|
```
|
|
189
190
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
191
|
+
Both managed paths verify the selected tenant manifest and generated adapter
|
|
192
|
+
bytes, resolve the canonical native agent name, and omit the normal parent
|
|
193
|
+
specialist instructions. Claude Code receives the explicit 2.1.220 opt-in and
|
|
194
|
+
session permission mode required to run only that adapter's fixed MCP
|
|
195
|
+
allowlist. Impel re-declares the verified adapter under a random per-launch
|
|
196
|
+
name and loads only the isolated user settings source, so project, user, plugin,
|
|
197
|
+
and fixed organization agent names cannot replace it. User-authored Claude
|
|
198
|
+
agents that are absent from the Impel manifest keep Claude's ordinary
|
|
199
|
+
permission behavior. Model, prompt, agent-definition, tool, MCP, settings,
|
|
200
|
+
plugin, worktree, and permission overrides are rejected for a verified managed
|
|
201
|
+
Claude selection.
|
|
202
|
+
|
|
203
|
+
Codex loads a generated, tenant-bound profile with the ultra-fast Codex Spark
|
|
204
|
+
model at low reasoning for the thin adapter turns, a read-only local sandbox,
|
|
205
|
+
the exact fixed MCP binding, and only that binding's answer/run, resume, and
|
|
206
|
+
recovery tools. It omits unrelated local repository, app, collaboration,
|
|
207
|
+
permission, environment, and skill-catalog prompt context; the selected Eve
|
|
208
|
+
agent still performs the substantive work. Model, profile,
|
|
209
|
+
developer-instruction, MCP, approval, sandbox, and feature overrides are
|
|
210
|
+
rejected for its managed path. Ordinary `impel claude`, user-authored Claude
|
|
211
|
+
agent, and `impel codex` passthrough are unchanged.
|
|
201
212
|
|
|
202
213
|
## Remote Fargate sessions
|
|
203
214
|
|
|
@@ -518,6 +529,7 @@ must remain explicit and destructive-state aware.
|
|
|
518
529
|
impel setup
|
|
519
530
|
impel update
|
|
520
531
|
impel claude
|
|
532
|
+
impel claude --agent <id|exact-title> ...
|
|
521
533
|
impel codex
|
|
522
534
|
impel codex --agent <id|exact-title> ...
|
|
523
535
|
impel tenant list|current|use
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Release notes
|
|
2
2
|
|
|
3
|
+
## 0.20.38 — Restore managed Claude agent permissions
|
|
4
|
+
|
|
5
|
+
- Verifies an exact synchronized Claude agent and its generated file digest
|
|
6
|
+
before enabling Claude Code 2.1.220's required bypass-permission opt-in and
|
|
7
|
+
session mode for that fixed MCP-only adapter.
|
|
8
|
+
- Resolves exact agent IDs and titles to a byte-verified definition, declares
|
|
9
|
+
it under a random per-launch name with user-only setting sources, omits the
|
|
10
|
+
parent delegation prompt, and rejects model, prompt, tool, MCP, settings,
|
|
11
|
+
plugin, worktree, and permission overrides on the managed path.
|
|
12
|
+
- Leaves ordinary Claude launches, user-authored Claude agents, and every
|
|
13
|
+
Codex launch path unchanged.
|
|
14
|
+
|
|
3
15
|
## 0.20.37 — Resume ambiguous native answers
|
|
4
16
|
|
|
5
17
|
- Retries Next's typed ambiguous direct-answer response with the exact persisted
|
|
@@ -6,7 +6,7 @@ is treated as an eager host.
|
|
|
6
6
|
|
|
7
7
|
| Surface | Pinned build | Exact selected tools | Selected path | Host behavior |
|
|
8
8
|
| --- | --- | --- | --- | --- |
|
|
9
|
-
| Claude Code | 2.1.220 | Yes, agent frontmatter `tools` | Eager | The bound run/answer, resume, and recovery tools are direct child tools.
|
|
9
|
+
| Claude Code | 2.1.220 | Yes, agent frontmatter `tools` | Eager; optional single-thread selection | The bound run/answer, resume, and recovery tools are direct child tools. `impel claude --agent` verifies the exact managed adapter before enabling 2.1.220's bypass-permission opt-in and mode. |
|
|
10
10
|
| Claude Desktop | 1.26832.0 (embedded Code 2.1.222) | Yes, agent frontmatter `tools` | Eager | Same direct-tool contract as Claude Code. |
|
|
11
11
|
| Codex CLI through `impel codex` | 0.147.0 | Yes, with `features.code_mode.direct_only_tool_namespaces = ["mcp__impel_agent"]` | Eager; optional single-thread profile | Existing `@Agent` children call the fixed MCP namespace directly. `impel codex --agent` also removes the parent spawn/wait/relay thread. |
|
|
12
12
|
| ChatGPT desktop Codex | 26.803.41515 (embedded Codex 0.147.0-alpha.6.5) | Yes, with the same exact native namespace | Eager child, compatible parent | Keeps the app's supported `@Agent` parent/child route, but the child no longer needs code cells or code-mode waits. |
|
|
@@ -16,6 +16,17 @@ is treated as an eager host.
|
|
|
16
16
|
- Eager profiles expose only the exact fixed binding's answer/run, resume, and
|
|
17
17
|
recovery tools. `IMPEL_NATIVE_EAGER_TRANSPORT=0` independently restores the
|
|
18
18
|
legacy host-visible tool configuration without deleting profiles or runs.
|
|
19
|
+
- A top-level Claude selection receives `--allow-dangerously-skip-permissions`
|
|
20
|
+
plus `--permission-mode bypassPermissions` only after its manifest record,
|
|
21
|
+
canonical definition, fixed MCP binding, and generated file bytes have been
|
|
22
|
+
verified. User-authored agents keep native Claude behavior.
|
|
23
|
+
- The verified definition is passed through `--agents` under a random
|
|
24
|
+
per-launch name while `--setting-sources user` excludes project/local
|
|
25
|
+
definitions. This outranks user and plugin names and makes collisions with a
|
|
26
|
+
fixed organization-managed agent name impractical.
|
|
27
|
+
- Verified Claude selections reject model, prompt, agent-definition, tool,
|
|
28
|
+
MCP, settings, plugin, worktree, and permission-policy overrides before
|
|
29
|
+
native launch.
|
|
19
30
|
- Every local composite keeps one upstream MCP session for its process lifetime,
|
|
20
31
|
validates the exact binding once, and persists the authoritative run handle
|
|
21
32
|
before ambiguous retries.
|
package/package.json
CHANGED
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 = 20;
|
|
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"]),
|
|
@@ -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);
|
|
@@ -871,9 +902,11 @@ export function renderManagedAgents(client, tenantId, agents, invocation = null,
|
|
|
871
902
|
// collision-safe, filesystem-safe identifiers, so use them as filenames.
|
|
872
903
|
const fileStem = client === "claude" ? name : fileStems[index];
|
|
873
904
|
const boundInvocation = boundNativeAgentInvocation(normalizedTenant, agent, invocation);
|
|
874
|
-
const
|
|
905
|
+
const claudeRendered = client === "claude"
|
|
875
906
|
? renderClaudeAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation })
|
|
876
|
-
:
|
|
907
|
+
: null;
|
|
908
|
+
const contents = claudeRendered?.contents
|
|
909
|
+
?? renderCodexAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation, directCodeMode });
|
|
877
910
|
const profileName = client === "codex" ? fileStem : null;
|
|
878
911
|
return {
|
|
879
912
|
agentId: agent.agentId,
|
|
@@ -885,6 +918,7 @@ export function renderManagedAgents(client, tenantId, agents, invocation = null,
|
|
|
885
918
|
name,
|
|
886
919
|
fileName: `${fileStem}${extension}`,
|
|
887
920
|
contents,
|
|
921
|
+
...(claudeRendered ? { launchDefinition: claudeRendered.launchDefinition } : {}),
|
|
888
922
|
...(profileName ? {
|
|
889
923
|
profileName,
|
|
890
924
|
profileFileName: `${profileName}.config.toml`,
|
|
@@ -939,9 +973,11 @@ function renderRetiredManagedAgents(client, tenantId, bindings, active, invocati
|
|
|
939
973
|
policyFingerprint: binding.policyFingerprint,
|
|
940
974
|
mode: "recovery",
|
|
941
975
|
});
|
|
942
|
-
const
|
|
976
|
+
const claudeRendered = client === "claude"
|
|
943
977
|
? renderClaudeAgent({ tenantId, agent, name, invocation: boundInvocation, recoveryOnly: true })
|
|
944
|
-
:
|
|
978
|
+
: null;
|
|
979
|
+
const contents = claudeRendered?.contents
|
|
980
|
+
?? renderCodexAgent({ tenantId, agent, name, invocation: boundInvocation, recoveryOnly: true, directCodeMode });
|
|
945
981
|
const profileName = client === "codex" ? path.basename(fileName, extension) : null;
|
|
946
982
|
return {
|
|
947
983
|
agentId: binding.agentId,
|
|
@@ -953,6 +989,7 @@ function renderRetiredManagedAgents(client, tenantId, bindings, active, invocati
|
|
|
953
989
|
name,
|
|
954
990
|
fileName,
|
|
955
991
|
contents,
|
|
992
|
+
...(claudeRendered ? { launchDefinition: claudeRendered.launchDefinition } : {}),
|
|
956
993
|
...(profileName ? {
|
|
957
994
|
profileName,
|
|
958
995
|
profileFileName: `${profileName}.config.toml`,
|
|
@@ -999,7 +1036,8 @@ function managedManifestPath(root) {
|
|
|
999
1036
|
}
|
|
1000
1037
|
|
|
1001
1038
|
function contentDigest(contents) {
|
|
1002
|
-
|
|
1039
|
+
const bytes = Buffer.isBuffer(contents) ? contents : Buffer.from(contents, "utf8");
|
|
1040
|
+
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
1003
1041
|
}
|
|
1004
1042
|
|
|
1005
1043
|
function managedArtifactIsCurrent(root, relativePath, expectedDigest) {
|
|
@@ -1009,8 +1047,9 @@ function managedArtifactIsCurrent(root, relativePath, expectedDigest) {
|
|
|
1009
1047
|
if (!/^[a-f0-9]{64}$/u.test(expectedDigest || "")) return false;
|
|
1010
1048
|
const artifactPath = path.join(root, ...segments);
|
|
1011
1049
|
try {
|
|
1012
|
-
|
|
1013
|
-
|
|
1050
|
+
const stat = fs.lstatSync(artifactPath);
|
|
1051
|
+
if (stat.isSymbolicLink() || !stat.isFile()) return false;
|
|
1052
|
+
return contentDigest(fs.readFileSync(artifactPath)) === expectedDigest;
|
|
1014
1053
|
} catch {
|
|
1015
1054
|
return false;
|
|
1016
1055
|
}
|
|
@@ -1033,6 +1072,156 @@ export function readManagedAgentManifest(root) {
|
|
|
1033
1072
|
return readManifest(managedManifestPath(root));
|
|
1034
1073
|
}
|
|
1035
1074
|
|
|
1075
|
+
function exactObjectKeys(value, keys) {
|
|
1076
|
+
return value
|
|
1077
|
+
&& typeof value === "object"
|
|
1078
|
+
&& !Array.isArray(value)
|
|
1079
|
+
&& JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...keys].sort());
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function validateClaudeLaunchDefinition(record, tenantId) {
|
|
1083
|
+
const definition = record.launchDefinition;
|
|
1084
|
+
if (!exactObjectKeys(definition, [
|
|
1085
|
+
"description", "prompt", "model", "permissionMode", "tools", "mcpServers",
|
|
1086
|
+
])
|
|
1087
|
+
|| typeof definition.description !== "string"
|
|
1088
|
+
|| !definition.description
|
|
1089
|
+
|| definition.description.length > 900
|
|
1090
|
+
|| typeof definition.prompt !== "string"
|
|
1091
|
+
|| !definition.prompt
|
|
1092
|
+
|| Buffer.byteLength(definition.prompt, "utf8") > 256 * 1024
|
|
1093
|
+
|| definition.model !== "haiku"
|
|
1094
|
+
|| definition.permissionMode !== "bypassPermissions"
|
|
1095
|
+
|| !Array.isArray(definition.tools)
|
|
1096
|
+
|| definition.tools.length < 2
|
|
1097
|
+
|| definition.tools.length > 3
|
|
1098
|
+
|| new Set(definition.tools).size !== definition.tools.length
|
|
1099
|
+
|| !definition.tools.every((tool) => [
|
|
1100
|
+
NATIVE_AGENT_ANSWER_TOOL,
|
|
1101
|
+
NATIVE_AGENT_RUN_TOOL,
|
|
1102
|
+
NATIVE_AGENT_RESUME_TOOL,
|
|
1103
|
+
NATIVE_AGENT_RECOVER_TOOL,
|
|
1104
|
+
].map((name) => nativeToolName(name)).includes(tool))
|
|
1105
|
+
|| !Array.isArray(definition.mcpServers)
|
|
1106
|
+
|| definition.mcpServers.length !== 1
|
|
1107
|
+
|| !exactObjectKeys(definition.mcpServers[0], [MANAGED_AGENT_MCP_SERVER])) {
|
|
1108
|
+
throw new Error("the managed Claude launch definition is invalid; run `impel agents sync claude`");
|
|
1109
|
+
}
|
|
1110
|
+
const server = definition.mcpServers[0][MANAGED_AGENT_MCP_SERVER];
|
|
1111
|
+
const allowedEnvironment = new Set([IMPEL_MANAGED_MCP_ENV, ...IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES]);
|
|
1112
|
+
if (!exactObjectKeys(server, ["type", "command", "args", "env"])
|
|
1113
|
+
|| server.type !== "stdio"
|
|
1114
|
+
|| typeof server.command !== "string"
|
|
1115
|
+
|| !server.command
|
|
1116
|
+
|| !Array.isArray(server.args)
|
|
1117
|
+
|| !server.args.every((argument) => typeof argument === "string")
|
|
1118
|
+
|| !server.env
|
|
1119
|
+
|| typeof server.env !== "object"
|
|
1120
|
+
|| Array.isArray(server.env)
|
|
1121
|
+
|| server.env[IMPEL_MANAGED_MCP_ENV] !== "1"
|
|
1122
|
+
|| !Object.entries(server.env).every(([key, value]) =>
|
|
1123
|
+
allowedEnvironment.has(key)
|
|
1124
|
+
&& typeof value === "string"
|
|
1125
|
+
&& value.length <= 4096
|
|
1126
|
+
&& redactCredentialText(value) === value
|
|
1127
|
+
)) {
|
|
1128
|
+
throw new Error("the managed Claude MCP definition is invalid; run `impel agents sync claude`");
|
|
1129
|
+
}
|
|
1130
|
+
const answerOnly = server.args.at(-1) === "--answer-only";
|
|
1131
|
+
const trustedInvocation = impelNativeAgentMcpInvocation({
|
|
1132
|
+
tenantId,
|
|
1133
|
+
agentId: record.agentId,
|
|
1134
|
+
scopeParam: record.scopeParam,
|
|
1135
|
+
policyFingerprint: record.policyFingerprint,
|
|
1136
|
+
mode: answerOnly ? "answer" : "durable",
|
|
1137
|
+
});
|
|
1138
|
+
const expectedTools = (answerOnly
|
|
1139
|
+
? [NATIVE_AGENT_ANSWER_TOOL, NATIVE_AGENT_RESUME_TOOL]
|
|
1140
|
+
: NATIVE_AGENT_TOOL_NAMES
|
|
1141
|
+
).map((name) => nativeToolName(name));
|
|
1142
|
+
if (server.command !== trustedInvocation.command
|
|
1143
|
+
|| JSON.stringify(server.args) !== JSON.stringify(trustedInvocation.args)
|
|
1144
|
+
|| JSON.stringify(definition.tools) !== JSON.stringify(expectedTools)) {
|
|
1145
|
+
throw new Error("the managed Claude binding is invalid; run `impel agents sync claude`");
|
|
1146
|
+
}
|
|
1147
|
+
return definition;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// A selector absent from the Impel manifest remains Claude-native. A matching
|
|
1151
|
+
// record is integrity checked, then re-declared under a random per-launch name
|
|
1152
|
+
// through Claude's higher-priority --agents source. This prevents project,
|
|
1153
|
+
// user, plugin, and fixed managed-settings names from replacing the adapter.
|
|
1154
|
+
export function resolveManagedClaudeAgent(root, tenantId, selector) {
|
|
1155
|
+
const normalizedTenant = normalizeTenantId(tenantId);
|
|
1156
|
+
if (typeof selector !== "string" || !selector || selector.length > 512) return null;
|
|
1157
|
+
const manifest = readManagedAgentManifest(root);
|
|
1158
|
+
if (!manifest
|
|
1159
|
+
|| manifest.version !== MANAGED_AGENT_MANIFEST_VERSION
|
|
1160
|
+
|| manifest.client !== "claude"
|
|
1161
|
+
|| manifest.tenantId !== normalizedTenant
|
|
1162
|
+
|| !Array.isArray(manifest.agents)) {
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
const records = manifest.agents.filter((record) => record
|
|
1166
|
+
&& typeof record === "object"
|
|
1167
|
+
&& typeof record.name === "string");
|
|
1168
|
+
const active = records.filter((record) => !record.retired);
|
|
1169
|
+
let matches = active.filter((record) => record.agentId === selector);
|
|
1170
|
+
if (matches.length === 0) matches = active.filter((record) => record.title === selector);
|
|
1171
|
+
if (matches.length === 0) matches = active.filter((record) => record.name === selector);
|
|
1172
|
+
if (matches.length === 0 && records.some((record) => record.retired && [
|
|
1173
|
+
record.agentId,
|
|
1174
|
+
record.title,
|
|
1175
|
+
record.name,
|
|
1176
|
+
].includes(selector))) {
|
|
1177
|
+
throw new Error(`managed Claude agent ${JSON.stringify(selector)} is retired and cannot start new work`);
|
|
1178
|
+
}
|
|
1179
|
+
if (matches.length === 0) return null;
|
|
1180
|
+
if (matches.length !== 1) {
|
|
1181
|
+
throw new Error(
|
|
1182
|
+
`managed Claude agent ${JSON.stringify(selector)} is ambiguous for tenant ${JSON.stringify(normalizedTenant)}`,
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
const [record] = matches;
|
|
1186
|
+
if (typeof record.agentId !== "string"
|
|
1187
|
+
|| !SAFE_AGENT_ID_RE.test(record.agentId)
|
|
1188
|
+
|| typeof record.scopeParam !== "string"
|
|
1189
|
+
|| !SAFE_SCOPE_PARAM_RE.test(record.scopeParam)
|
|
1190
|
+
|| !/^[a-f0-9]{64}$/u.test(record.policyFingerprint || "")) {
|
|
1191
|
+
throw new Error("the managed Claude binding metadata is invalid; run `impel agents sync claude`");
|
|
1192
|
+
}
|
|
1193
|
+
const fileName = `${record.name}.md`;
|
|
1194
|
+
if (path.basename(fileName) !== fileName || !manifest.files.includes(fileName)) {
|
|
1195
|
+
throw new Error("the managed Claude agent mapping is invalid; run `impel agents sync claude`");
|
|
1196
|
+
}
|
|
1197
|
+
const relativePath = `agents/${fileName}`;
|
|
1198
|
+
if (!managedArtifactIsCurrent(root, relativePath, manifest.contentDigests?.[relativePath])) {
|
|
1199
|
+
throw new Error("the managed Claude agent failed its integrity check; run `impel agents sync claude`");
|
|
1200
|
+
}
|
|
1201
|
+
const launchDefinition = validateClaudeLaunchDefinition(record, normalizedTenant);
|
|
1202
|
+
if (contentDigest(renderClaudeAgentDefinition(record.name, launchDefinition))
|
|
1203
|
+
!== manifest.contentDigests?.[relativePath]) {
|
|
1204
|
+
throw new Error("the managed Claude launch definition does not match its agent file; run `impel agents sync claude`");
|
|
1205
|
+
}
|
|
1206
|
+
const suffix = crypto.randomBytes(12).toString("hex");
|
|
1207
|
+
const prefix = `impel-${record.name}`.slice(0, 63 - suffix.length - 1).replace(/-+$/u, "");
|
|
1208
|
+
const launchName = `${prefix}-${suffix}`;
|
|
1209
|
+
const launchAgentsJson = JSON.stringify({ [launchName]: launchDefinition });
|
|
1210
|
+
if (Buffer.byteLength(launchAgentsJson, "utf8") > MAX_CLAUDE_INLINE_AGENT_JSON_BYTES) {
|
|
1211
|
+
throw new Error(
|
|
1212
|
+
`managed Claude agent ${JSON.stringify(selector)} is too large for a safe cross-platform direct launch; shorten its synchronized catalog policy`,
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
return {
|
|
1216
|
+
...record,
|
|
1217
|
+
fileName,
|
|
1218
|
+
path: path.join(root, "agents", fileName),
|
|
1219
|
+
launchName,
|
|
1220
|
+
launchDefinition,
|
|
1221
|
+
launchAgentsJson,
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1036
1225
|
export function resolveManagedCodexAgentProfile(root, tenantId, selector) {
|
|
1037
1226
|
const normalizedTenant = normalizeTenantId(tenantId);
|
|
1038
1227
|
const normalizedSelector = boundedString(selector, "agent selector", { max: 512 });
|
|
@@ -1236,6 +1425,9 @@ export function syncAgentProfile({
|
|
|
1236
1425
|
policyFingerprint: agent.policyFingerprint,
|
|
1237
1426
|
retired: agent.retired,
|
|
1238
1427
|
name: agent.name,
|
|
1428
|
+
...(client === "claude" ? {
|
|
1429
|
+
launchDefinition: agent.launchDefinition,
|
|
1430
|
+
} : {}),
|
|
1239
1431
|
...(client === "codex" && directProfiles ? {
|
|
1240
1432
|
profileName: agent.profileName,
|
|
1241
1433
|
profileFileName: agent.profileFileName,
|
package/src/cli.js
CHANGED
|
@@ -34,6 +34,7 @@ Get started:
|
|
|
34
34
|
|
|
35
35
|
Work:
|
|
36
36
|
impel claude [args...] Launch Claude Code with an isolated Impel profile
|
|
37
|
+
impel claude --agent <id|exact-title> ... Run one fixed tenant agent without a parent hop
|
|
37
38
|
impel codex [args...] Launch Codex with an isolated Impel profile
|
|
38
39
|
impel codex --agent <id|exact-title> ... Run one fixed tenant agent without a parent hop
|
|
39
40
|
impel remote handoff|dispatch|handback Move or control provider-native sessions remotely
|
package/src/commands/launch.js
CHANGED
|
@@ -6,7 +6,11 @@ import {
|
|
|
6
6
|
ensureImpelClaudeProfile,
|
|
7
7
|
ensureImpelCodexProfile,
|
|
8
8
|
} from "../cliProfiles.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
resolveManagedClaudeAgent,
|
|
11
|
+
resolveManagedCodexAgentProfile,
|
|
12
|
+
syncAgentProfilesSafe,
|
|
13
|
+
} from "../agents.js";
|
|
10
14
|
import {
|
|
11
15
|
crossAppModelsEnabled,
|
|
12
16
|
loadConfig,
|
|
@@ -77,10 +81,43 @@ const IMPEL_CODEX_RUNTIME_OVERRIDES = [
|
|
|
77
81
|
];
|
|
78
82
|
|
|
79
83
|
const IMPEL_CODEX_UNRESTRICTED_ARGUMENT = "--dangerously-bypass-approvals-and-sandbox";
|
|
84
|
+
const IMPEL_CLAUDE_MANAGED_AGENT_ARGUMENTS = [
|
|
85
|
+
"--allow-dangerously-skip-permissions",
|
|
86
|
+
"--permission-mode",
|
|
87
|
+
"bypassPermissions",
|
|
88
|
+
// Pinned Claude 2.1.220 treats agents, skills, commands, and settings as
|
|
89
|
+
// source-scoped. Load only the isolated user profile so project/local agent
|
|
90
|
+
// definitions cannot replace the integrity-verified adapter after launch.
|
|
91
|
+
"--setting-sources",
|
|
92
|
+
"user",
|
|
93
|
+
];
|
|
80
94
|
|
|
81
|
-
export function impelLaunchArguments(tool, argv, {
|
|
95
|
+
export function impelLaunchArguments(tool, argv, {
|
|
96
|
+
claudeManagedAgent = null,
|
|
97
|
+
codexAgentProfile = null,
|
|
98
|
+
} = {}) {
|
|
82
99
|
if (!RUNTIME_BRAND.features.agents) return [...argv];
|
|
83
100
|
if (tool === "claude") {
|
|
101
|
+
if (claudeManagedAgent) {
|
|
102
|
+
const { launchName, launchAgentsJson } = claudeManagedAgent;
|
|
103
|
+
if (typeof launchName !== "string"
|
|
104
|
+
|| !launchName
|
|
105
|
+
|| typeof launchAgentsJson !== "string"
|
|
106
|
+
|| !launchAgentsJson) {
|
|
107
|
+
throw new Error("managed Claude launch configuration is invalid");
|
|
108
|
+
}
|
|
109
|
+
if (Buffer.byteLength(launchAgentsJson, "utf8") > 12 * 1024) {
|
|
110
|
+
throw new Error("managed Claude launch configuration is too large");
|
|
111
|
+
}
|
|
112
|
+
return [
|
|
113
|
+
...IMPEL_CLAUDE_MANAGED_AGENT_ARGUMENTS,
|
|
114
|
+
"--agents",
|
|
115
|
+
launchAgentsJson,
|
|
116
|
+
"--agent",
|
|
117
|
+
launchName,
|
|
118
|
+
...argv,
|
|
119
|
+
];
|
|
120
|
+
}
|
|
84
121
|
return ["--append-system-prompt", IMPEL_CLAUDE_PARENT_DELEGATION_INSTRUCTIONS, ...argv];
|
|
85
122
|
}
|
|
86
123
|
if (tool === "codex") {
|
|
@@ -99,6 +136,113 @@ export function impelLaunchArguments(tool, argv, { codexAgentProfile = null } =
|
|
|
99
136
|
throw new Error(`unsupported CLI launcher: ${tool}`);
|
|
100
137
|
}
|
|
101
138
|
|
|
139
|
+
const CLAUDE_MANAGED_AGENT_LOCKED_OPTIONS = new Set([
|
|
140
|
+
"--add-dir",
|
|
141
|
+
"--agents",
|
|
142
|
+
"--allow-tools",
|
|
143
|
+
"--allow-dangerously-skip-permissions",
|
|
144
|
+
"--allowedTools",
|
|
145
|
+
"--allowed-tools",
|
|
146
|
+
"--append-subagent-system-prompt",
|
|
147
|
+
"--append-system-prompt",
|
|
148
|
+
"--append-system-prompt-file",
|
|
149
|
+
"--bare",
|
|
150
|
+
"--brief",
|
|
151
|
+
"--chrome",
|
|
152
|
+
"--dangerously-skip-permissions",
|
|
153
|
+
"--disable-slash-commands",
|
|
154
|
+
"--disallowedTools",
|
|
155
|
+
"--disallowed-tools",
|
|
156
|
+
"--effort",
|
|
157
|
+
"--exclude-dynamic-system-prompt-sections",
|
|
158
|
+
"--fallback-model",
|
|
159
|
+
"--file",
|
|
160
|
+
"--ide",
|
|
161
|
+
"--managed-settings",
|
|
162
|
+
"--mcp-config",
|
|
163
|
+
"--model",
|
|
164
|
+
"--no-chrome",
|
|
165
|
+
"--permission-mode",
|
|
166
|
+
"--permission-prompt-tool",
|
|
167
|
+
"--plugin-dir",
|
|
168
|
+
"--plugin-dir-no-mcp",
|
|
169
|
+
"--plugin-url",
|
|
170
|
+
"--safe-mode",
|
|
171
|
+
"--setting-sources",
|
|
172
|
+
"--settings",
|
|
173
|
+
"--strict-mcp-config",
|
|
174
|
+
"--system-prompt",
|
|
175
|
+
"--system-prompt-file",
|
|
176
|
+
"--tmux",
|
|
177
|
+
"--tools",
|
|
178
|
+
"--worktree",
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
function claudeManagedAgentLockedOption(argument) {
|
|
182
|
+
if (argument === "-w" || (argument.startsWith("-w") && argument.length > 2)) return "-w";
|
|
183
|
+
if (CLAUDE_MANAGED_AGENT_LOCKED_OPTIONS.has(argument)) return argument;
|
|
184
|
+
return [...CLAUDE_MANAGED_AGENT_LOCKED_OPTIONS].find((option) =>
|
|
185
|
+
argument.startsWith(`${option}=`)
|
|
186
|
+
) || null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function parseClaudeAgentLaunchArguments(argv) {
|
|
190
|
+
const passthrough = [];
|
|
191
|
+
const selectors = [];
|
|
192
|
+
let literal = false;
|
|
193
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
194
|
+
const argument = argv[index];
|
|
195
|
+
if (literal) {
|
|
196
|
+
passthrough.push(argument);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (argument === "--") {
|
|
200
|
+
literal = true;
|
|
201
|
+
passthrough.push(argument);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (argument === "--agent") {
|
|
205
|
+
const value = argv[index += 1];
|
|
206
|
+
selectors.push(typeof value === "string" && value && !value.startsWith("-") ? value : null);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (argument.startsWith("--agent=")) {
|
|
210
|
+
const value = argument.slice("--agent=".length);
|
|
211
|
+
selectors.push(value && !value.startsWith("-") ? value : null);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
passthrough.push(argument);
|
|
215
|
+
}
|
|
216
|
+
const selector = selectors.length === 1 ? selectors[0] : null;
|
|
217
|
+
return {
|
|
218
|
+
selector,
|
|
219
|
+
argv: selector === null ? [...argv] : passthrough,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function assertManagedClaudeAgentLaunchArguments(argv) {
|
|
224
|
+
let literal = false;
|
|
225
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
226
|
+
const argument = argv[index];
|
|
227
|
+
if (literal) continue;
|
|
228
|
+
if (argument === "--") {
|
|
229
|
+
literal = true;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (argument === "--agent") {
|
|
233
|
+
index += 1;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (argument.startsWith("--agent=")) continue;
|
|
237
|
+
const locked = claudeManagedAgentLockedOption(argument);
|
|
238
|
+
if (locked) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`${locked} cannot be used with a verified managed Claude agent; Impel locks model, instructions, tools, MCP, settings, and permission policy`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
102
246
|
const CODEX_AGENT_LOCKED_OPTIONS = new Set([
|
|
103
247
|
"-c",
|
|
104
248
|
"--config",
|
|
@@ -264,8 +408,11 @@ function runNativeCli(tool, argv, environment) {
|
|
|
264
408
|
|
|
265
409
|
export async function cmdLaunch(tool, argv) {
|
|
266
410
|
let nativeArgv = [...argv];
|
|
411
|
+
let claudeAgentSelector = null;
|
|
267
412
|
let codexAgentSelector = null;
|
|
268
|
-
if (tool === "
|
|
413
|
+
if (tool === "claude" && RUNTIME_BRAND.features.agents) {
|
|
414
|
+
({ selector: claudeAgentSelector, argv: nativeArgv } = parseClaudeAgentLaunchArguments(argv));
|
|
415
|
+
} else if (tool === "codex") {
|
|
269
416
|
try {
|
|
270
417
|
({ selector: codexAgentSelector, argv: nativeArgv } = parseCodexAgentLaunchArguments(argv));
|
|
271
418
|
} catch (error) {
|
|
@@ -356,6 +503,29 @@ export async function cmdLaunch(tool, argv) {
|
|
|
356
503
|
});
|
|
357
504
|
}
|
|
358
505
|
|
|
506
|
+
let claudeManagedAgent = null;
|
|
507
|
+
if (RUNTIME_BRAND.features.agents && claudeAgentSelector !== null) {
|
|
508
|
+
try {
|
|
509
|
+
claudeManagedAgent = resolveManagedClaudeAgent(
|
|
510
|
+
agentProfile.root,
|
|
511
|
+
tenantId,
|
|
512
|
+
claudeAgentSelector,
|
|
513
|
+
);
|
|
514
|
+
if (claudeManagedAgent) {
|
|
515
|
+
assertManagedClaudeAgentLaunchArguments(nativeArgv);
|
|
516
|
+
} else {
|
|
517
|
+
// `--agent` is also Claude's native user-agent selector. If the exact
|
|
518
|
+
// value is not in the integrity-tracked Impel manifest, preserve the
|
|
519
|
+
// user's original argv and ordinary permission behavior byte-for-byte.
|
|
520
|
+
nativeArgv = [...argv];
|
|
521
|
+
}
|
|
522
|
+
} catch (error) {
|
|
523
|
+
console.error(`impel claude: ${redactSecretText(error?.message || error)}`);
|
|
524
|
+
process.exitCode = 1;
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
359
529
|
let codexAgentProfile = null;
|
|
360
530
|
if (codexAgentSelector !== null) {
|
|
361
531
|
try {
|
|
@@ -377,7 +547,10 @@ export async function cmdLaunch(tool, argv) {
|
|
|
377
547
|
const { env: launchEnvironment } = withGitEnvironment(environment, { baseEnvironment: environment });
|
|
378
548
|
const exitCode = await runNativeCli(
|
|
379
549
|
tool,
|
|
380
|
-
impelLaunchArguments(tool, nativeArgv, {
|
|
550
|
+
impelLaunchArguments(tool, nativeArgv, {
|
|
551
|
+
claudeManagedAgent,
|
|
552
|
+
codexAgentProfile,
|
|
553
|
+
}),
|
|
381
554
|
launchEnvironment,
|
|
382
555
|
);
|
|
383
556
|
if (exitCode !== 0) process.exitCode = exitCode;
|
package/src/selfInvocation.js
CHANGED
|
@@ -5,8 +5,18 @@ import { environmentValue } from "./nativeProcess.js";
|
|
|
5
5
|
import { brandedEnvironmentName, RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
6
6
|
|
|
7
7
|
/** The running package's own bin script, used directly for live child spawns. */
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
const PACKAGE_CLI_ENTRYPOINT = fileURLToPath(new URL("../bin/impel.js", import.meta.url));
|
|
9
|
+
const extensionEntrypoint = process.env.IMPEL_CLI_EXTENSION_ENTRYPOINT;
|
|
10
|
+
// The public Impel package never delegates its trusted MCP command prefix to
|
|
11
|
+
// inherited process state. First-party extension wrappers install a validated
|
|
12
|
+
// non-Impel runtime brand before import, must actually be executing the named
|
|
13
|
+
// entrypoint, and then prove that same path in createImpelCliExtension.
|
|
14
|
+
export const IMPEL_CLI_ENTRYPOINT = RUNTIME_BRAND.cli.packageName === "impel-cli"
|
|
15
|
+
|| typeof extensionEntrypoint !== "string"
|
|
16
|
+
|| !path.isAbsolute(extensionEntrypoint)
|
|
17
|
+
|| path.resolve(process.argv[1] || "") !== path.resolve(extensionEntrypoint)
|
|
18
|
+
? PACKAGE_CLI_ENTRYPOINT
|
|
19
|
+
: extensionEntrypoint;
|
|
10
20
|
|
|
11
21
|
// Matches a real package install (npm/pnpm/volta all place the package under
|
|
12
22
|
// node_modules/impel-cli; an unsupported project-local install matches too,
|