impel-cli 0.20.16 → 0.20.18
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 +8 -4
- package/RELEASE_NOTES.md +17 -0
- package/package.json +1 -1
- package/scripts/profile-native-codex.mjs +35 -8
- package/src/apps.js +10 -2
- package/src/codexSecurity.js +18 -0
- package/src/sessionCollector.js +2 -1
- package/src/sessionHooks.js +4 -0
package/README.md
CHANGED
|
@@ -378,10 +378,14 @@ control-plane HTTP bearer credential, and fixes tenant selection in the
|
|
|
378
378
|
transport header. Generated Claude/Codex MCP entries contain neither the PAT
|
|
379
379
|
nor a tenant-derived credential.
|
|
380
380
|
|
|
381
|
-
- The pinned Impel Claude Desktop
|
|
382
|
-
|
|
383
|
-
-
|
|
384
|
-
|
|
381
|
+
- The pinned Impel Claude Desktop renders the standard MCP Apps task board when
|
|
382
|
+
its host supports that protocol.
|
|
383
|
+
- The exact pinned Impel Codex desktop profile enables its reviewed
|
|
384
|
+
`enable_mcp_apps` contract so `show_tasks` can render the same board. The
|
|
385
|
+
vendor-pin test fails closed if the embedded Codex binary stops accepting it.
|
|
386
|
+
- Claude Code and Codex CLI profiles remain headless and receive complete task
|
|
387
|
+
tools plus text results; the desktop-only renderer flag is never written to
|
|
388
|
+
those profiles.
|
|
385
389
|
- Profiles preserve unrelated MCP servers. If a user-authored server already
|
|
386
390
|
owns the `impel-tasks` name, setup/update fails with remediation instead of
|
|
387
391
|
overwriting it.
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Release notes
|
|
2
2
|
|
|
3
|
+
## 0.20.18 — Recoverable Codex profiling cohorts
|
|
4
|
+
|
|
5
|
+
- Widens the bounded retry window for transient current-tenant reads while
|
|
6
|
+
still rejecting an exact tenant mismatch before a Codex attempt starts.
|
|
7
|
+
- Stops scheduling new work after an attempt-infrastructure failure, waits for
|
|
8
|
+
active workers to settle, and retains an explicitly incomplete private
|
|
9
|
+
aggregate so valid samples are not silently lost.
|
|
10
|
+
|
|
11
|
+
## 0.20.17 — Managed desktop task views
|
|
12
|
+
|
|
13
|
+
- Enables the reviewed MCP Apps renderer only in exact managed Codex Desktop
|
|
14
|
+
profiles, while keeping Codex CLI profiles on their text and tool fallback.
|
|
15
|
+
- Captures Claude permission, notification, and task lifecycle events through
|
|
16
|
+
the existing asynchronous, tenant-bound session hooks.
|
|
17
|
+
- Preserves the additional lifecycle context needed to correlate task updates
|
|
18
|
+
without expanding the oversized-hook payload boundary.
|
|
19
|
+
|
|
3
20
|
## 0.20.16 — Correlated Codex transport timing
|
|
4
21
|
|
|
5
22
|
- Derives native-agent MCP duration from correlated, timestamped Codex rollout
|
package/package.json
CHANGED
|
@@ -14,6 +14,7 @@ const PROFILE_SCHEMA = "impel.native-codex-profile.v1";
|
|
|
14
14
|
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
15
15
|
const DEFAULT_GRACE_MS = 5_000;
|
|
16
16
|
const MAX_CAPTURE_BYTES = 256 * 1024 * 1024;
|
|
17
|
+
const TENANT_PREFLIGHT_RETRY_DELAYS_MS = [250, 750, 1_500];
|
|
17
18
|
|
|
18
19
|
function privateDirectory(directory) {
|
|
19
20
|
if (fs.existsSync(directory) && fs.lstatSync(directory).isSymbolicLink()) {
|
|
@@ -211,16 +212,17 @@ function runCaptured(command, args, { environment = process.env, timeoutMs = 60_
|
|
|
211
212
|
}
|
|
212
213
|
|
|
213
214
|
async function currentTenant(impelBinary, environment) {
|
|
214
|
-
for (let attempt = 0; attempt
|
|
215
|
+
for (let attempt = 0; attempt <= TENANT_PREFLIGHT_RETRY_DELAYS_MS.length; attempt += 1) {
|
|
215
216
|
const invocation = impelInvocation(impelBinary, ["tenant", "current"], environment);
|
|
216
217
|
const result = await runCaptured(invocation.command, invocation.args, { environment });
|
|
217
218
|
const lines = result.stdout.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
|
|
218
219
|
if (result.code === 0 && lines.length === 1 && /^[A-Za-z0-9_.:-]{1,160}$/u.test(lines[0])) {
|
|
219
220
|
return lines[0];
|
|
220
221
|
}
|
|
221
|
-
|
|
222
|
+
const retryDelay = TENANT_PREFLIGHT_RETRY_DELAYS_MS[attempt];
|
|
223
|
+
if (retryDelay !== undefined) await delay(retryDelay);
|
|
222
224
|
}
|
|
223
|
-
throw new Error("could not establish one exact current Impel tenant after
|
|
225
|
+
throw new Error("could not establish one exact current Impel tenant after bounded attempts");
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
export async function assertExpectedTenant({ expectedTenant, impelBinary = "impel", environment = process.env }) {
|
|
@@ -495,19 +497,38 @@ async function profileAttempt(options, sessionDir, index) {
|
|
|
495
497
|
return record;
|
|
496
498
|
}
|
|
497
499
|
|
|
498
|
-
|
|
500
|
+
function profilerFailureClass(error) {
|
|
501
|
+
if (/could not establish one exact current Impel tenant/u.test(error?.message || "")) {
|
|
502
|
+
return "tenant-preflight-unavailable";
|
|
503
|
+
}
|
|
504
|
+
if (/selected tenant .* does not match expected tenant/u.test(error?.message || "")) {
|
|
505
|
+
return "tenant-mismatch";
|
|
506
|
+
}
|
|
507
|
+
return "attempt-infrastructure";
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export async function runWorkers(options, sessionDir, profileAttemptImpl = profileAttempt) {
|
|
499
511
|
const results = new Array(options.attempts);
|
|
500
512
|
let next = 0;
|
|
513
|
+
let abort = null;
|
|
501
514
|
const worker = async () => {
|
|
502
515
|
for (;;) {
|
|
516
|
+
if (abort) return;
|
|
503
517
|
const index = next;
|
|
504
518
|
next += 1;
|
|
505
519
|
if (index >= options.attempts) return;
|
|
506
|
-
|
|
520
|
+
try {
|
|
521
|
+
results[index] = await profileAttemptImpl(options, sessionDir, index);
|
|
522
|
+
} catch (error) {
|
|
523
|
+
abort ||= {
|
|
524
|
+
failureClass: profilerFailureClass(error),
|
|
525
|
+
attemptId: `${options.cohort}-${String(index + 1 + options.attemptOffset).padStart(3, "0")}`,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
507
528
|
}
|
|
508
529
|
};
|
|
509
530
|
await Promise.all(Array.from({ length: options.concurrency }, () => worker()));
|
|
510
|
-
return results;
|
|
531
|
+
return { attempts: results.filter(Boolean), abort };
|
|
511
532
|
}
|
|
512
533
|
|
|
513
534
|
async function restoreTenant(originalTenant, options) {
|
|
@@ -553,7 +574,7 @@ async function main(argv) {
|
|
|
553
574
|
privateDirectory(sessionDir);
|
|
554
575
|
let restored = false;
|
|
555
576
|
try {
|
|
556
|
-
const
|
|
577
|
+
const result = await runWorkers(options, sessionDir);
|
|
557
578
|
const aggregate = {
|
|
558
579
|
schema: PROFILE_SCHEMA,
|
|
559
580
|
sessionId,
|
|
@@ -564,12 +585,18 @@ async function main(argv) {
|
|
|
564
585
|
cohort: options.cohort,
|
|
565
586
|
cliVersion: options.hostBuild,
|
|
566
587
|
promptSha256: options.promptSha256,
|
|
567
|
-
attempts,
|
|
588
|
+
complete: result.abort === null && result.attempts.length === options.attempts,
|
|
589
|
+
expectedAttempts: options.attempts,
|
|
590
|
+
abort: result.abort,
|
|
591
|
+
attempts: result.attempts,
|
|
568
592
|
};
|
|
569
593
|
const aggregatePath = path.join(sessionDir, "aggregate.json");
|
|
570
594
|
privateWrite(aggregatePath, `${JSON.stringify(aggregate, null, 2)}\n`);
|
|
571
595
|
process.stderr.write(`Private Codex profile written under ${sessionDir}\n`);
|
|
572
596
|
process.stdout.write(`${JSON.stringify(aggregate)}\n`);
|
|
597
|
+
if (!aggregate.complete) {
|
|
598
|
+
throw new Error(`cohort aborted (${aggregate.abort?.failureClass || "incomplete"}); partial aggregate retained`);
|
|
599
|
+
}
|
|
573
600
|
} finally {
|
|
574
601
|
restored = await restoreTenant(originalTenant, options);
|
|
575
602
|
if (!restored) process.stderr.write("profile-native-codex: could not verify restoration of the original tenant\n");
|
package/src/apps.js
CHANGED
|
@@ -5,6 +5,7 @@ import crypto from "node:crypto";
|
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import {
|
|
8
|
+
enableManagedCodexDesktopMcpApps,
|
|
8
9
|
hardenManagedCodexToml,
|
|
9
10
|
secureAllManagedCodexHomes,
|
|
10
11
|
secureManagedCodexHome,
|
|
@@ -281,7 +282,9 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
|
281
282
|
// expose direct-answer native agents through their one-shot local MCP binding.
|
|
282
283
|
// 30: generate integrity-tracked top-level Codex profiles and direct code-mode
|
|
283
284
|
// namespaces for fixed native-agent bindings.
|
|
284
|
-
|
|
285
|
+
// 31: enable the reviewed MCP Apps renderer in pinned Codex Desktop profiles
|
|
286
|
+
// and install Claude task, notification, and permission lifecycle hooks.
|
|
287
|
+
export const CURRENT_CONFIG_VERSION = 31;
|
|
285
288
|
|
|
286
289
|
// Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
|
|
287
290
|
// helper rebranding, and signing. A vendored bundle is rebuilt only when this
|
|
@@ -1400,7 +1403,12 @@ function writeChatGPTConfig(
|
|
|
1400
1403
|
CHATGPT_CONFIG_END,
|
|
1401
1404
|
].join("\n");
|
|
1402
1405
|
const mergedToml = mergeManagedChatGPTToml(preservedToml, managedToml);
|
|
1403
|
-
|
|
1406
|
+
const hardenedToml = hardenManagedCodexToml(mergedToml, configPath);
|
|
1407
|
+
writeAtomic(
|
|
1408
|
+
configPath,
|
|
1409
|
+
enableManagedCodexDesktopMcpApps(hardenedToml, configPath),
|
|
1410
|
+
0o600,
|
|
1411
|
+
);
|
|
1404
1412
|
secureManagedCodexHome(paths.chatgpt.codexHome);
|
|
1405
1413
|
}
|
|
1406
1414
|
|
package/src/codexSecurity.js
CHANGED
|
@@ -148,6 +148,24 @@ export function hardenManagedCodexToml(toml, configPath = "managed Codex config"
|
|
|
148
148
|
return applyManagedCodexSandboxPolicy(withoutCredentialSnapshots, configPath);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Enable the standard MCP Apps renderer only in the exact pinned desktop
|
|
153
|
+
* profile. CLI profiles deliberately stay headless, and vendor-pin contract
|
|
154
|
+
* tests must prove that the embedded Codex binary still recognizes this flag.
|
|
155
|
+
*/
|
|
156
|
+
export function enableManagedCodexDesktopMcpApps(
|
|
157
|
+
toml,
|
|
158
|
+
configPath = "managed Codex desktop config",
|
|
159
|
+
) {
|
|
160
|
+
return upsertManagedScalar(
|
|
161
|
+
toml,
|
|
162
|
+
"features",
|
|
163
|
+
"enable_mcp_apps",
|
|
164
|
+
"true",
|
|
165
|
+
configPath,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
151
169
|
function assertSafeManagedPath(target, expectedType) {
|
|
152
170
|
const stat = lstatOrNull(target);
|
|
153
171
|
if (!stat) return null;
|
package/src/sessionCollector.js
CHANGED
|
@@ -236,7 +236,8 @@ function compactLedgerPayload(input) {
|
|
|
236
236
|
const keys = [
|
|
237
237
|
"session_id", "hook_event_name", "cwd", "permission_mode", "model", "source", "reason",
|
|
238
238
|
"turn_id", "prompt_id", "agent_id", "agent_type", "tool_name", "tool_use_id", "error",
|
|
239
|
-
"error_details", "last_assistant_message",
|
|
239
|
+
"error_details", "last_assistant_message", "notification_type", "message", "title",
|
|
240
|
+
"task_id", "task_subject", "task_description", "teammate_name", "tool_input",
|
|
240
241
|
];
|
|
241
242
|
const payload = {};
|
|
242
243
|
for (const key of keys) {
|
package/src/sessionHooks.js
CHANGED
|
@@ -12,10 +12,14 @@ const CODEX_TRUST_END = `# <<< ${RUNTIME_BRAND.product.id} managed session hook
|
|
|
12
12
|
export const CLAUDE_SESSION_EVENTS = Object.freeze([
|
|
13
13
|
"SessionStart",
|
|
14
14
|
"UserPromptSubmit",
|
|
15
|
+
"PermissionRequest",
|
|
15
16
|
"PostToolUse",
|
|
16
17
|
"PostToolUseFailure",
|
|
18
|
+
"Notification",
|
|
17
19
|
"SubagentStart",
|
|
18
20
|
"SubagentStop",
|
|
21
|
+
"TaskCreated",
|
|
22
|
+
"TaskCompleted",
|
|
19
23
|
"PreCompact",
|
|
20
24
|
"PostCompact",
|
|
21
25
|
"Stop",
|