impel-cli 0.20.51 → 0.20.53
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 +3 -1
- package/RELEASE_NOTES.md +27 -1
- package/docs/native-agent-host-capability-matrix.md +2 -2
- package/package.json +1 -1
- package/src/apps.js +8 -8
- package/src/cli.js +2 -0
- package/src/commands/converge.js +17 -0
- package/src/commands/launch.js +23 -1
- package/src/desktopTasks.js +26 -2
- package/src/managedApps.js +194 -0
- package/src/managedProfileVersion.js +3 -1
- package/src/tenants.js +13 -1
package/README.md
CHANGED
|
@@ -410,7 +410,9 @@ nor a tenant-derived credential.
|
|
|
410
410
|
Electron archives cannot be patched without invalidating vendor signatures.
|
|
411
411
|
It loads the authenticated canonical Impel `/tasks` application directly in
|
|
412
412
|
a sandboxed, tenant-isolated web view; a fresh profile uses the existing
|
|
413
|
-
Board default.
|
|
413
|
+
Board default. Selecting a card, list row, or fleet item opens that exact
|
|
414
|
+
tenant's ticket in the main Impel browser app; the constrained desktop
|
|
415
|
+
session remains board-only.
|
|
414
416
|
- The trusted native process obtains the selected tenant credential through a
|
|
415
417
|
private one-shot pipe, exchanges the PAT for a bounded HttpOnly web session,
|
|
416
418
|
and then loads `/tasks` without an Authorization header. The web renderer has
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Release notes
|
|
2
2
|
|
|
3
|
+
## 0.20.53 — Launch distributed managed CLIs
|
|
4
|
+
|
|
5
|
+
- Adds `impel claude` and `impel codex` fallback launch through an installed
|
|
6
|
+
tenant-managed app when a standalone CLI credential is not configured.
|
|
7
|
+
- Selects an explicit `--tenant`, configured tenant, or the sole installed
|
|
8
|
+
tenant; ambiguous installations fail closed with an actionable tenant list.
|
|
9
|
+
- Validates the fixed application root, adjacent managed manifest, runtime
|
|
10
|
+
ownership, platform, logical tenant identity, and enrollment credential
|
|
11
|
+
presence without reading or placing the credential on the command line.
|
|
12
|
+
- Keeps the existing standalone Claude and Codex launch behavior unchanged
|
|
13
|
+
whenever its separate CLI credential is configured.
|
|
14
|
+
|
|
15
|
+
## 0.20.52 — Open embedded Tasks tickets safely
|
|
16
|
+
|
|
17
|
+
- Makes cards, list rows, fleet items, and the card menu open their exact
|
|
18
|
+
tenant-scoped ticket in the main Impel browser app.
|
|
19
|
+
- Keeps the embedded desktop session restricted to the Tasks board and sends
|
|
20
|
+
only the credential-free, membership-checked issue handoff to the system
|
|
21
|
+
browser.
|
|
22
|
+
- Updates the coordinated reviewed ChatGPT/Codex pin to desktop
|
|
23
|
+
`26.810.41047`, embedded Codex `0.148.0-alpha.9`, and the currently
|
|
24
|
+
fulfillable Windows Store package `26.810.4967.0`, retaining exact artifact,
|
|
25
|
+
signature, package, publisher, executable, and runtime verification.
|
|
26
|
+
- Skips the prerelease soak because this is an immediate stable repair for the
|
|
27
|
+
embedded ticket-navigation regression and Microsoft Store fulfillment drift;
|
|
28
|
+
the full release CI and Windows vendor contract remain mandatory before tag.
|
|
29
|
+
|
|
3
30
|
## 0.20.51 — Restore Claude Tasks in the sidebar
|
|
4
31
|
|
|
5
32
|
- Restores one embedded Tasks row in managed Claude's existing sidebar,
|
|
@@ -85,7 +112,6 @@
|
|
|
85
112
|
orchestration without changing vendor pins, generated profile contracts, or
|
|
86
113
|
built bundle bytes; the full release CI, pinned managed-app launch smoke, and
|
|
87
114
|
Windows vendor contracts remain mandatory before tagging.
|
|
88
|
-
|
|
89
115
|
## 0.20.46-beta.2 — Reload reconciled apps safely
|
|
90
116
|
|
|
91
117
|
- Closes every running managed macOS app before rewriting its profile and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Native-agent host capability matrix
|
|
2
2
|
|
|
3
|
-
Validated on 2026-08-
|
|
3
|
+
Validated on 2026-08-14 with isolated temporary homes and tenant profiles. The
|
|
4
4
|
matrix is intentionally pinned: a client upgrade must be re-qualified before it
|
|
5
5
|
is treated as an eager host.
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ is treated as an eager host.
|
|
|
9
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
|
-
| ChatGPT desktop Codex | 26.
|
|
12
|
+
| ChatGPT desktop Codex | 26.810.41047 (embedded Codex 0.148.0-alpha.9) | 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. |
|
|
13
13
|
|
|
14
14
|
## Qualified contract
|
|
15
15
|
|
package/package.json
CHANGED
package/src/apps.js
CHANGED
|
@@ -105,8 +105,8 @@ export const PINNED_VENDOR_APPS = Object.freeze({
|
|
|
105
105
|
}),
|
|
106
106
|
}),
|
|
107
107
|
chatgpt: Object.freeze({
|
|
108
|
-
version: "26.
|
|
109
|
-
codexVersion: "0.
|
|
108
|
+
version: "26.810.41047",
|
|
109
|
+
codexVersion: "0.148.0-alpha.9",
|
|
110
110
|
bundleName: "ChatGPT.app",
|
|
111
111
|
windows: Object.freeze({
|
|
112
112
|
storeProductId: "9PLM9XGG6VKS",
|
|
@@ -114,20 +114,20 @@ export const PINNED_VENDOR_APPS = Object.freeze({
|
|
|
114
114
|
// The Microsoft Store manifest identifies this exact reviewed build.
|
|
115
115
|
// Keep one Windows package version so the manifest contract and local
|
|
116
116
|
// AppX validation cannot silently drift apart again.
|
|
117
|
-
packageVersion: "26.
|
|
118
|
-
codexVersion: "0.
|
|
117
|
+
packageVersion: "26.810.4967.0",
|
|
118
|
+
codexVersion: "0.148.0-alpha.9",
|
|
119
119
|
publisherId: "2p2nqsd0c76g0",
|
|
120
120
|
executable: "app\\ChatGPT.exe",
|
|
121
121
|
updateManifestUrl: "https://persistent.oaistatic.com/codex-app-prod/windows-store-update.json",
|
|
122
122
|
}),
|
|
123
123
|
downloads: Object.freeze({
|
|
124
124
|
arm64: Object.freeze({
|
|
125
|
-
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.
|
|
126
|
-
sha256: "
|
|
125
|
+
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.810.41047.zip",
|
|
126
|
+
sha256: "b8e82ade7b1f0161f1aec50c165bc97083ae5592703f8df3bff5850f57aaf287",
|
|
127
127
|
}),
|
|
128
128
|
x64: Object.freeze({
|
|
129
|
-
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.
|
|
130
|
-
sha256: "
|
|
129
|
+
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.810.41047.zip",
|
|
130
|
+
sha256: "bec467c8ab052cfd6acb2492ba95439fbbbe0e001a138b3943f55036e0c51c4e",
|
|
131
131
|
}),
|
|
132
132
|
}),
|
|
133
133
|
}),
|
package/src/cli.js
CHANGED
|
@@ -47,6 +47,8 @@ Work:
|
|
|
47
47
|
impel codex [args...] Launch Codex with an isolated Impel profile
|
|
48
48
|
impel codex --agent <id|exact-title> ... Run one fixed tenant agent without a parent hop
|
|
49
49
|
impel codex --benchmark ... Tag native-agent MCP calls as benchmark traffic
|
|
50
|
+
impel claude|codex --tenant <id> ... Select an enrolled Managed Software Center tenant
|
|
51
|
+
when the CLI has no separate Impel authentication
|
|
50
52
|
impel remote handoff|dispatch|handback Move or control provider-native sessions remotely
|
|
51
53
|
impel remote status|viewer|proxy Inspect, control, or connect to a remote run
|
|
52
54
|
impel tenant list List accessible organizations
|
package/src/commands/converge.js
CHANGED
|
@@ -15,6 +15,21 @@ import { createTenantRecoveryReportTracker } from "./tenantRecoveryReport.js";
|
|
|
15
15
|
|
|
16
16
|
const RETRYABLE_TENANT_DISCOVERY_ERROR = /could not reach|request timed out|fetch failed|ECONNRESET|ECONNABORTED|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|network error|socket/iu;
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Wait before the single discovery retry.
|
|
20
|
+
*
|
|
21
|
+
* An immediate retry cannot change the answer for the failure it most often
|
|
22
|
+
* sees: the OS caches a negative DNS result, so a second lookup microseconds
|
|
23
|
+
* later reads the same cached entry. This is long enough for a resolver or
|
|
24
|
+
* interface blip to clear and short enough that nobody watching decides the
|
|
25
|
+
* command has hung.
|
|
26
|
+
*/
|
|
27
|
+
export const TENANT_DISCOVERY_RETRY_DELAY_MS = 1_500;
|
|
28
|
+
|
|
29
|
+
function sleep(milliseconds) {
|
|
30
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
function confirmed(answer) {
|
|
19
34
|
return /^(?:y|yes)$/iu.test(String(answer || "").trim());
|
|
20
35
|
}
|
|
@@ -36,6 +51,7 @@ export async function cmdConverge(argv = [], overrides = {}) {
|
|
|
36
51
|
environment: process.env,
|
|
37
52
|
preparePlatformClis,
|
|
38
53
|
discoveryLogger: console,
|
|
54
|
+
sleep,
|
|
39
55
|
...overrides,
|
|
40
56
|
};
|
|
41
57
|
if (overrides.prepareWindowsClis && !overrides.preparePlatformClis) {
|
|
@@ -59,6 +75,7 @@ export async function cmdConverge(argv = [], overrides = {}) {
|
|
|
59
75
|
return false;
|
|
60
76
|
}
|
|
61
77
|
io.discoveryLogger.log("Tenants: discovery request failed transiently; retrying once…");
|
|
78
|
+
await io.sleep(TENANT_DISCOVERY_RETRY_DELAY_MS);
|
|
62
79
|
try {
|
|
63
80
|
listing = await io.fetchTenants(config);
|
|
64
81
|
} catch (retryError) {
|
package/src/commands/launch.js
CHANGED
|
@@ -29,6 +29,12 @@ import { IMPEL_NATIVE_BENCHMARK_ENV } from "../selfInvocation.js";
|
|
|
29
29
|
import { resolveReviewedVendorCliBinary } from "../vendorCliBinaries.js";
|
|
30
30
|
import { PINNED_VENDOR_CLI_VERSIONS } from "../vendorCliVersions.js";
|
|
31
31
|
import { RUNTIME_BRAND } from "../runtimeBrand.js";
|
|
32
|
+
import {
|
|
33
|
+
installedManagedAppCandidates,
|
|
34
|
+
parseManagedTenantArgument,
|
|
35
|
+
runManagedAppCli,
|
|
36
|
+
selectManagedAppCandidate,
|
|
37
|
+
} from "../managedApps.js";
|
|
32
38
|
import {
|
|
33
39
|
cacheProviderReadiness,
|
|
34
40
|
cachedProviderReadiness,
|
|
@@ -462,7 +468,23 @@ export async function cmdLaunch(tool, argv) {
|
|
|
462
468
|
}
|
|
463
469
|
const config = loadConfig();
|
|
464
470
|
if (!config?.pat) {
|
|
465
|
-
|
|
471
|
+
try {
|
|
472
|
+
const parsed = parseManagedTenantArgument(nativeArgv);
|
|
473
|
+
const candidate = selectManagedAppCandidate(installedManagedAppCandidates(tool), {
|
|
474
|
+
requestedTenant: parsed.tenantId,
|
|
475
|
+
configuredTenant: config?.tenantId || null,
|
|
476
|
+
});
|
|
477
|
+
if (candidate) {
|
|
478
|
+
const exitCode = await runManagedAppCli(candidate, tool, parsed.argv);
|
|
479
|
+
if (exitCode !== 0) process.exitCode = exitCode;
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
} catch (error) {
|
|
483
|
+
console.error(`impel ${tool}: ${redactSecretText(error?.message || error)}`);
|
|
484
|
+
process.exitCode = 1;
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
console.error(`impel ${tool}: not authenticated and no enrolled managed application is installed. Run \`impel auth\` or install it from Managed Software Center.`);
|
|
466
488
|
process.exitCode = 1;
|
|
467
489
|
return;
|
|
468
490
|
}
|
package/src/desktopTasks.js
CHANGED
|
@@ -455,10 +455,33 @@ if (
|
|
|
455
455
|
try { return new URL(rawUrl).origin === appOrigin; }
|
|
456
456
|
catch { return false; }
|
|
457
457
|
};
|
|
458
|
+
const issueHandoff = (rawUrl) => {
|
|
459
|
+
try {
|
|
460
|
+
const url = new URL(rawUrl);
|
|
461
|
+
const prefix = "/api/issues/open/";
|
|
462
|
+
const issueId = url.pathname.startsWith(prefix) ? url.pathname.slice(prefix.length) : "";
|
|
463
|
+
const orgIds = url.searchParams.getAll("orgId");
|
|
464
|
+
return url.origin === appOrigin
|
|
465
|
+
&& !url.username
|
|
466
|
+
&& !url.password
|
|
467
|
+
&& !url.hash
|
|
468
|
+
&& Boolean(issueId)
|
|
469
|
+
&& !issueId.includes("/")
|
|
470
|
+
&& issueId.length <= 128
|
|
471
|
+
&& [...url.searchParams.keys()].every((key) => key === "orgId")
|
|
472
|
+
&& orgIds.length === 1
|
|
473
|
+
&& /^[A-Za-z0-9_.-]{1,128}$/u.test(orgIds[0]);
|
|
474
|
+
} catch { return false; }
|
|
475
|
+
};
|
|
458
476
|
const external = (rawUrl) => {
|
|
459
477
|
try {
|
|
460
478
|
const url = new URL(rawUrl);
|
|
461
|
-
if (
|
|
479
|
+
if (
|
|
480
|
+
url.protocol === "https:"
|
|
481
|
+
&& !url.username
|
|
482
|
+
&& !url.password
|
|
483
|
+
&& (url.origin !== appOrigin || issueHandoff(url.href))
|
|
484
|
+
) {
|
|
462
485
|
shell.openExternal(url.href).catch(() => record("external-link-failed"));
|
|
463
486
|
}
|
|
464
487
|
} catch {}
|
|
@@ -636,7 +659,8 @@ if (
|
|
|
636
659
|
view.setVisible(false);
|
|
637
660
|
view.setBackgroundColor("#181818");
|
|
638
661
|
view.webContents.setWindowOpenHandler(({ url }) => {
|
|
639
|
-
if (
|
|
662
|
+
if (issueHandoff(url)) external(url);
|
|
663
|
+
else if (exactOrigin(url)) view.webContents.loadURL(url).catch(() => record("board-navigation-failed"));
|
|
640
664
|
else external(url);
|
|
641
665
|
return { action: "deny" };
|
|
642
666
|
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const TENANT = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/u;
|
|
7
|
+
const APPLICATION_BY_TOOL = Object.freeze({ claude: "claude", codex: "chatgpt" });
|
|
8
|
+
|
|
9
|
+
function regularFile(file) {
|
|
10
|
+
try {
|
|
11
|
+
const stat = fs.lstatSync(file);
|
|
12
|
+
return stat.isFile() && !stat.isSymbolicLink();
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseCandidate(manifestPath, launcher, tool, platform, home) {
|
|
19
|
+
if (!regularFile(manifestPath) || !regularFile(launcher)) return null;
|
|
20
|
+
let manifest;
|
|
21
|
+
try {
|
|
22
|
+
const raw = fs.readFileSync(manifestPath, "utf8");
|
|
23
|
+
if (Buffer.byteLength(raw, "utf8") > 512 * 1024) return null;
|
|
24
|
+
manifest = JSON.parse(raw);
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const applicationId = APPLICATION_BY_TOOL[tool];
|
|
29
|
+
const tenantId = manifest?.tenant?.id;
|
|
30
|
+
const expectedPlatform = platform === "win32" ? "windows" : "macos";
|
|
31
|
+
if (
|
|
32
|
+
manifest?.schemaVersion !== 2
|
|
33
|
+
|| manifest?.kind !== "com.useimpel.managed-application"
|
|
34
|
+
|| manifest?.runtime?.owner !== "impel-apps"
|
|
35
|
+
|| manifest?.runtime?.entrypoint !== "engine/main.mjs"
|
|
36
|
+
|| manifest?.platform !== expectedPlatform
|
|
37
|
+
|| manifest?.application?.id !== applicationId
|
|
38
|
+
|| manifest?.application?.adapter !== applicationId
|
|
39
|
+
|| !TENANT.test(tenantId || "")
|
|
40
|
+
|| String(tenantId).includes("..")
|
|
41
|
+
|| manifest?.logicalApplicationId !== `${tenantId}.${applicationId}`
|
|
42
|
+
) return null;
|
|
43
|
+
const credential = path.join(home, ".config", "impel", "apps", "credentials", `${tenantId}.json`);
|
|
44
|
+
if (!regularFile(credential)) return null;
|
|
45
|
+
return {
|
|
46
|
+
tenantId,
|
|
47
|
+
tenantDisplayName: typeof manifest.tenant.displayName === "string" ? manifest.tenant.displayName : tenantId,
|
|
48
|
+
launcher,
|
|
49
|
+
manifestPath,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function macCandidates(tool, home, applicationsRoot) {
|
|
54
|
+
let entries;
|
|
55
|
+
try {
|
|
56
|
+
entries = fs.readdirSync(applicationsRoot, { withFileTypes: true });
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
const candidates = [];
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (!entry.isDirectory() || !entry.name.endsWith(".app")) continue;
|
|
63
|
+
const root = path.join(applicationsRoot, entry.name, "Contents");
|
|
64
|
+
const candidate = parseCandidate(
|
|
65
|
+
path.join(root, "Resources", "app-manifest.json"),
|
|
66
|
+
path.join(root, "MacOS", "impel-apps-launcher"),
|
|
67
|
+
tool,
|
|
68
|
+
"darwin",
|
|
69
|
+
home,
|
|
70
|
+
);
|
|
71
|
+
if (candidate) candidates.push(candidate);
|
|
72
|
+
}
|
|
73
|
+
return candidates;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function windowsCandidates(tool, home, applicationsRoot) {
|
|
77
|
+
const candidates = [];
|
|
78
|
+
let tenants;
|
|
79
|
+
try {
|
|
80
|
+
tenants = fs.readdirSync(applicationsRoot, { withFileTypes: true });
|
|
81
|
+
} catch {
|
|
82
|
+
return candidates;
|
|
83
|
+
}
|
|
84
|
+
for (const tenant of tenants) {
|
|
85
|
+
if (!tenant.isDirectory() || !TENANT.test(tenant.name) || tenant.name.includes("..")) continue;
|
|
86
|
+
const root = path.join(applicationsRoot, tenant.name, APPLICATION_BY_TOOL[tool]);
|
|
87
|
+
const candidate = parseCandidate(
|
|
88
|
+
path.join(root, "resources", "app-manifest.json"),
|
|
89
|
+
path.join(root, "impel-apps-launcher.exe"),
|
|
90
|
+
tool,
|
|
91
|
+
"win32",
|
|
92
|
+
home,
|
|
93
|
+
);
|
|
94
|
+
if (candidate) candidates.push(candidate);
|
|
95
|
+
}
|
|
96
|
+
return candidates;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function installedManagedAppCandidates(tool, {
|
|
100
|
+
platform = process.platform,
|
|
101
|
+
home = os.homedir(),
|
|
102
|
+
applicationsRoot = platform === "win32"
|
|
103
|
+
? path.join(process.env.ProgramFiles || "C:\\Program Files", "Impel", "Apps")
|
|
104
|
+
: "/Applications",
|
|
105
|
+
} = {}) {
|
|
106
|
+
if (!APPLICATION_BY_TOOL[tool]) return [];
|
|
107
|
+
const candidates = platform === "win32"
|
|
108
|
+
? windowsCandidates(tool, home, applicationsRoot)
|
|
109
|
+
: platform === "darwin" ? macCandidates(tool, home, applicationsRoot) : [];
|
|
110
|
+
return candidates.sort((left, right) => left.tenantId.localeCompare(right.tenantId));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function parseManagedTenantArgument(argv) {
|
|
114
|
+
const passthrough = [];
|
|
115
|
+
let tenantId = null;
|
|
116
|
+
let literal = false;
|
|
117
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
118
|
+
const argument = argv[index];
|
|
119
|
+
if (literal) {
|
|
120
|
+
passthrough.push(argument);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (argument === "--") {
|
|
124
|
+
literal = true;
|
|
125
|
+
passthrough.push(argument);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (argument === "--tenant") {
|
|
129
|
+
tenantId = argv[index += 1] || null;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (argument.startsWith("--tenant=")) {
|
|
133
|
+
tenantId = argument.slice("--tenant=".length) || null;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
passthrough.push(argument);
|
|
137
|
+
}
|
|
138
|
+
if (tenantId !== null && (!TENANT.test(tenantId) || tenantId.includes(".."))) {
|
|
139
|
+
throw new Error("--tenant requires a valid managed tenant id");
|
|
140
|
+
}
|
|
141
|
+
return { tenantId, argv: passthrough };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function selectManagedAppCandidate(candidates, {
|
|
145
|
+
requestedTenant = null,
|
|
146
|
+
environment = process.env,
|
|
147
|
+
configuredTenant = null,
|
|
148
|
+
} = {}) {
|
|
149
|
+
const tenantId = requestedTenant || environment.IMPEL_MANAGED_TENANT || configuredTenant;
|
|
150
|
+
if (tenantId) {
|
|
151
|
+
const selected = candidates.find((candidate) => candidate.tenantId === tenantId);
|
|
152
|
+
if (!selected) throw new Error(`no installed managed application is available for tenant "${tenantId}"`);
|
|
153
|
+
return selected;
|
|
154
|
+
}
|
|
155
|
+
if (candidates.length === 1) return candidates[0];
|
|
156
|
+
if (candidates.length === 0) return null;
|
|
157
|
+
throw new Error(
|
|
158
|
+
`more than one managed tenant is installed (${candidates.map(({ tenantId: id }) => id).join(", ")}); pass --tenant <id>`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function childExitCode(code, signal) {
|
|
163
|
+
if (Number.isInteger(code)) return code;
|
|
164
|
+
const signalNumber = signal ? os.constants.signals[signal] : null;
|
|
165
|
+
return Number.isInteger(signalNumber) ? 128 + signalNumber : 1;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function runManagedAppCli(candidate, tool, argv, {
|
|
169
|
+
environment = process.env,
|
|
170
|
+
cwd = process.cwd(),
|
|
171
|
+
spawnProcess = spawn,
|
|
172
|
+
} = {}) {
|
|
173
|
+
return new Promise((resolve) => {
|
|
174
|
+
const child = spawnProcess(candidate.launcher, ["--managed-cli", tool, "--", ...argv], {
|
|
175
|
+
cwd,
|
|
176
|
+
env: environment,
|
|
177
|
+
stdio: "inherit",
|
|
178
|
+
windowsHide: false,
|
|
179
|
+
});
|
|
180
|
+
let settled = false;
|
|
181
|
+
child.once("error", (error) => {
|
|
182
|
+
if (settled) return;
|
|
183
|
+
settled = true;
|
|
184
|
+
console.error(`impel ${tool}: could not launch the managed application: ${error.message}`);
|
|
185
|
+
resolve(127);
|
|
186
|
+
});
|
|
187
|
+
child.once("exit", (code, signal) => {
|
|
188
|
+
if (settled) return;
|
|
189
|
+
settled = true;
|
|
190
|
+
resolve(childExitCode(code, signal));
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
@@ -4,4 +4,6 @@
|
|
|
4
4
|
// v42 restores Claude's embedded Tasks entry through its real sidebar, using
|
|
5
5
|
// the same fallback-free, tenant-bound board lifecycle already enabled for
|
|
6
6
|
// managed ChatGPT profiles.
|
|
7
|
-
|
|
7
|
+
// v43 sends tenant-checked issue handoffs from the embedded board to the main
|
|
8
|
+
// Impel browser app without expanding the capability-scoped desktop session.
|
|
9
|
+
export const CURRENT_CONFIG_VERSION = 43;
|
package/src/tenants.js
CHANGED
|
@@ -25,6 +25,10 @@ const PRODUCT_ACCESS_VALUES = new Set([
|
|
|
25
25
|
const PROVIDER_SCOPE = Object.freeze({ claude: PAT_SCOPE_CLAUDE, codex: PAT_SCOPE_CODEX });
|
|
26
26
|
const TENANT_ID_RE = /^[A-Za-z0-9_.-]{1,128}$/u;
|
|
27
27
|
const PAT_SCOPE_RE = /^[a-z0-9][a-z0-9:._-]{0,63}$/u;
|
|
28
|
+
// Node surfaces a name-resolution failure through these `getaddrinfo` codes.
|
|
29
|
+
// `EAI_AGAIN` is a transient resolver error; `ENOTFOUND` is a negative answer
|
|
30
|
+
// the OS then caches, which is why an immediate retry re-reads the same result.
|
|
31
|
+
const DNS_FAILURE_CODES = new Set(["ENOTFOUND", "EAI_AGAIN"]);
|
|
28
32
|
|
|
29
33
|
export function normalizeProductAccess(value, { allowMissing = false } = {}) {
|
|
30
34
|
if ((value === undefined || value === null || value === "") && allowMissing) return null;
|
|
@@ -136,7 +140,15 @@ export async function fetchTenants(config, fetchImpl = fetchHttp1) {
|
|
|
136
140
|
signal: controller.signal,
|
|
137
141
|
});
|
|
138
142
|
} catch (error) {
|
|
139
|
-
|
|
143
|
+
// A resolver failure is not a control-plane outage, and the raw
|
|
144
|
+
// `getaddrinfo ENOTFOUND` reads like one. Name the machine's own DNS so the
|
|
145
|
+
// remedy is looked for locally. The `could not reach` prefix is preserved
|
|
146
|
+
// because `cmdConverge` matches on it to decide the failure is retryable.
|
|
147
|
+
const message = error?.name === "AbortError"
|
|
148
|
+
? "request timed out"
|
|
149
|
+
: DNS_FAILURE_CODES.has(error?.code)
|
|
150
|
+
? `${error.code}; this machine could not resolve the hostname. Check its DNS resolver or VPN`
|
|
151
|
+
: error?.message || error;
|
|
140
152
|
throw new Error(`could not reach ${appUrl}: ${redactSecretText(message)}`);
|
|
141
153
|
} finally {
|
|
142
154
|
clearTimeout(timeout);
|