@vm0/cli 9.113.3 → 9.114.0
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/{chunk-3M2HM4GT.js → chunk-YGGTGOHH.js} +45 -13
- package/{chunk-3M2HM4GT.js.map → chunk-YGGTGOHH.js.map} +1 -1
- package/index.js +10 -10
- package/package.json +1 -1
- package/zero.js +389 -61
- package/zero.js.map +1 -1
package/index.js
CHANGED
|
@@ -70,7 +70,7 @@ import {
|
|
|
70
70
|
source_default,
|
|
71
71
|
volumeConfigSchema,
|
|
72
72
|
withErrorHandler
|
|
73
|
-
} from "./chunk-
|
|
73
|
+
} from "./chunk-YGGTGOHH.js";
|
|
74
74
|
|
|
75
75
|
// src/index.ts
|
|
76
76
|
init_esm_shims();
|
|
@@ -463,7 +463,7 @@ function getConfigPath() {
|
|
|
463
463
|
return join(homedir(), ".vm0", "config.json");
|
|
464
464
|
}
|
|
465
465
|
var infoCommand = new Command().name("info").description("Display environment and debug information").action(async () => {
|
|
466
|
-
console.log(source_default.bold(`VM0 CLI v${"9.
|
|
466
|
+
console.log(source_default.bold(`VM0 CLI v${"9.114.0"}`));
|
|
467
467
|
console.log();
|
|
468
468
|
const config = await loadConfig();
|
|
469
469
|
const hasEnvToken = !!process.env.VM0_TOKEN;
|
|
@@ -4492,7 +4492,7 @@ var composeCommand = new Command().name("compose").description("Create or update
|
|
|
4492
4492
|
options.autoUpdate = false;
|
|
4493
4493
|
}
|
|
4494
4494
|
if (options.autoUpdate !== false) {
|
|
4495
|
-
await startSilentUpgrade("9.
|
|
4495
|
+
await startSilentUpgrade("9.114.0");
|
|
4496
4496
|
}
|
|
4497
4497
|
try {
|
|
4498
4498
|
let result;
|
|
@@ -4572,7 +4572,7 @@ var mainRunCommand = new Command().name("run").description("Run an agent").argum
|
|
|
4572
4572
|
withErrorHandler(
|
|
4573
4573
|
async (identifier, prompt, options) => {
|
|
4574
4574
|
if (options.autoUpdate !== false) {
|
|
4575
|
-
await startSilentUpgrade("9.
|
|
4575
|
+
await startSilentUpgrade("9.114.0");
|
|
4576
4576
|
}
|
|
4577
4577
|
const { name, version } = parseIdentifier(identifier);
|
|
4578
4578
|
let composeId;
|
|
@@ -6313,7 +6313,7 @@ var cookAction = new Command().name("cook").description("Quick start: prepare, c
|
|
|
6313
6313
|
withErrorHandler(
|
|
6314
6314
|
async (prompt, options) => {
|
|
6315
6315
|
if (options.autoUpdate !== false) {
|
|
6316
|
-
const shouldExit = await checkAndUpgrade("9.
|
|
6316
|
+
const shouldExit = await checkAndUpgrade("9.114.0", prompt);
|
|
6317
6317
|
if (shouldExit) {
|
|
6318
6318
|
process.exit(0);
|
|
6319
6319
|
}
|
|
@@ -7080,13 +7080,13 @@ var upgradeCommand = new Command().name("upgrade").description("Upgrade vm0 CLI
|
|
|
7080
7080
|
if (latestVersion === null) {
|
|
7081
7081
|
throw new Error("Could not check for updates. Please try again later.");
|
|
7082
7082
|
}
|
|
7083
|
-
if (latestVersion === "9.
|
|
7084
|
-
console.log(source_default.green(`\u2713 Already up to date (${"9.
|
|
7083
|
+
if (latestVersion === "9.114.0") {
|
|
7084
|
+
console.log(source_default.green(`\u2713 Already up to date (${"9.114.0"})`));
|
|
7085
7085
|
return;
|
|
7086
7086
|
}
|
|
7087
7087
|
console.log(
|
|
7088
7088
|
source_default.yellow(
|
|
7089
|
-
`Current version: ${"9.
|
|
7089
|
+
`Current version: ${"9.114.0"} -> Latest version: ${latestVersion}`
|
|
7090
7090
|
)
|
|
7091
7091
|
);
|
|
7092
7092
|
console.log();
|
|
@@ -7113,7 +7113,7 @@ var upgradeCommand = new Command().name("upgrade").description("Upgrade vm0 CLI
|
|
|
7113
7113
|
const success = await performUpgrade(packageManager);
|
|
7114
7114
|
if (success) {
|
|
7115
7115
|
console.log(
|
|
7116
|
-
source_default.green(`\u2713 Upgraded from ${"9.
|
|
7116
|
+
source_default.green(`\u2713 Upgraded from ${"9.114.0"} to ${latestVersion}`)
|
|
7117
7117
|
);
|
|
7118
7118
|
return;
|
|
7119
7119
|
}
|
|
@@ -7180,7 +7180,7 @@ var whoamiCommand = new Command().name("whoami").description("Show current ident
|
|
|
7180
7180
|
|
|
7181
7181
|
// src/index.ts
|
|
7182
7182
|
var program = new Command();
|
|
7183
|
-
program.name("vm0").description("VM0 CLI - Build and run agents with natural language").version("9.
|
|
7183
|
+
program.name("vm0").description("VM0 CLI - Build and run agents with natural language").version("9.114.0");
|
|
7184
7184
|
program.addCommand(authCommand);
|
|
7185
7185
|
program.addCommand(infoCommand);
|
|
7186
7186
|
program.addCommand(composeCommand);
|
package/package.json
CHANGED
package/zero.js
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
deployZeroSchedule,
|
|
39
39
|
disableZeroSchedule,
|
|
40
40
|
enableZeroSchedule,
|
|
41
|
+
extractSecretNamesFromApis,
|
|
41
42
|
findMatchingPermissions,
|
|
42
43
|
getActiveOrg,
|
|
43
44
|
getApiUrl,
|
|
@@ -45,6 +46,7 @@ import {
|
|
|
45
46
|
getBaseUrl,
|
|
46
47
|
getComputerUseHost,
|
|
47
48
|
getConnectorDerivedNames,
|
|
49
|
+
getConnectorEnvironmentMapping,
|
|
48
50
|
getConnectorFirewall,
|
|
49
51
|
getConnectorTypeForSecretName,
|
|
50
52
|
getCustomModelPlaceholder,
|
|
@@ -67,6 +69,7 @@ import {
|
|
|
67
69
|
getZeroOrgMembers,
|
|
68
70
|
getZeroRun,
|
|
69
71
|
getZeroRunAgentEvents,
|
|
72
|
+
getZeroRunContext,
|
|
70
73
|
getZeroUserPreferences,
|
|
71
74
|
hasAuthMethods,
|
|
72
75
|
hasModelSelection,
|
|
@@ -127,7 +130,7 @@ import {
|
|
|
127
130
|
upsertZeroOrgModelProvider,
|
|
128
131
|
withErrorHandler,
|
|
129
132
|
zeroAgentCustomSkillNameSchema
|
|
130
|
-
} from "./chunk-
|
|
133
|
+
} from "./chunk-YGGTGOHH.js";
|
|
131
134
|
|
|
132
135
|
// src/zero.ts
|
|
133
136
|
init_esm_shims();
|
|
@@ -2183,7 +2186,7 @@ var zeroConnectorCommand = new Command().name("connector").description("Check or
|
|
|
2183
2186
|
// src/commands/zero/doctor/index.ts
|
|
2184
2187
|
init_esm_shims();
|
|
2185
2188
|
|
|
2186
|
-
// src/commands/zero/doctor/
|
|
2189
|
+
// src/commands/zero/doctor/check-connector.ts
|
|
2187
2190
|
init_esm_shims();
|
|
2188
2191
|
|
|
2189
2192
|
// src/commands/zero/doctor/platform-url.ts
|
|
@@ -2206,80 +2209,403 @@ async function getPlatformOrigin() {
|
|
|
2206
2209
|
return toPlatformUrl(apiUrl).origin;
|
|
2207
2210
|
}
|
|
2208
2211
|
|
|
2209
|
-
// src/commands/zero/doctor/
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2212
|
+
// src/commands/zero/doctor/check-connector.ts
|
|
2213
|
+
function resolveConnectorFromUrl(url) {
|
|
2214
|
+
const allTypes = Object.keys(CONNECTOR_TYPES);
|
|
2215
|
+
const normalized = url.endsWith("/") ? url.slice(0, -1) : url;
|
|
2216
|
+
let bestMatch = null;
|
|
2217
|
+
for (const type of allTypes) {
|
|
2218
|
+
if (!isFirewallConnectorType(type)) continue;
|
|
2219
|
+
const config = getConnectorFirewall(type);
|
|
2220
|
+
for (const api of config.apis) {
|
|
2221
|
+
const base = api.base.endsWith("/") ? api.base.slice(0, -1) : api.base;
|
|
2222
|
+
if (normalized === base || normalized.startsWith(base + "/")) {
|
|
2223
|
+
if (!bestMatch || base.length > bestMatch.base.length) {
|
|
2224
|
+
bestMatch = { connectorType: type, base, config };
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
if (!bestMatch) return null;
|
|
2230
|
+
const mapping = getConnectorEnvironmentMapping(
|
|
2231
|
+
bestMatch.connectorType
|
|
2232
|
+
);
|
|
2233
|
+
const envName = Object.keys(mapping)[0];
|
|
2234
|
+
if (!envName) return null;
|
|
2235
|
+
const relativePath = normalized === bestMatch.base ? "/" : normalized.slice(bestMatch.base.length);
|
|
2236
|
+
return {
|
|
2237
|
+
connectorType: bestMatch.connectorType,
|
|
2238
|
+
envName,
|
|
2239
|
+
matchedBase: bestMatch.base,
|
|
2240
|
+
relativePath
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
function checkEnvVariable(ctx) {
|
|
2244
|
+
console.log("## Step 1: Sandbox environment variable");
|
|
2245
|
+
console.log("");
|
|
2246
|
+
const envPresent = Boolean(process.env[ctx.envName]);
|
|
2247
|
+
console.log(
|
|
2248
|
+
`Checking process.env.${ctx.envName}: ${envPresent ? "present" : "not present"}`
|
|
2249
|
+
);
|
|
2250
|
+
if (envPresent) {
|
|
2251
|
+
console.log(
|
|
2252
|
+
"A placeholder value is present in the sandbox environment. This value is not the real credential \u2014 it is a stand-in that gets replaced at the network boundary when requests are sent to registered base URLs."
|
|
2253
|
+
);
|
|
2254
|
+
} else {
|
|
2255
|
+
console.log(
|
|
2256
|
+
"No value found for this environment variable. Note: credential replacement at the network boundary is independent of this variable \u2014 the proxy injects auth headers based on the destination URL, not the presence of this env var."
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2259
|
+
console.log("");
|
|
2260
|
+
return envPresent;
|
|
2261
|
+
}
|
|
2262
|
+
async function checkConnectorStatus(ctx) {
|
|
2263
|
+
console.log("## Step 2: Connector configuration");
|
|
2264
|
+
console.log("");
|
|
2265
|
+
console.log(
|
|
2266
|
+
"A Connector holds the real credentials (OAuth tokens or API keys) for an external service. These credentials are never injected into the sandbox. Instead, when the sandbox sends an HTTP request to a base URL registered by the Connector, the network boundary intercepts the request and replaces the auth headers with real credentials. For this to work, three conditions must be met:"
|
|
2267
|
+
);
|
|
2268
|
+
console.log("");
|
|
2269
|
+
const [connector, enabledTypes] = await Promise.all([
|
|
2270
|
+
getZeroConnector(ctx.connectorType),
|
|
2271
|
+
ctx.agentId ? getZeroAgentUserConnectors(ctx.agentId) : Promise.resolve(null)
|
|
2272
|
+
]);
|
|
2273
|
+
const isConnected = connector !== null;
|
|
2274
|
+
const isExpired = connector?.needsReconnect === true;
|
|
2275
|
+
const hasPermission = enabledTypes !== null && enabledTypes.includes(ctx.connectorType);
|
|
2276
|
+
console.log(
|
|
2277
|
+
`### 2a: Connector status (user must configure via OAuth login or API key)`
|
|
2278
|
+
);
|
|
2279
|
+
console.log("");
|
|
2280
|
+
if (!isConnected) {
|
|
2281
|
+
const connectUrl = ctx.agentId ? `${ctx.platformOrigin}/connectors/${ctx.connectorType}/connect?agentId=${ctx.agentId}` : `${ctx.platformOrigin}/connectors/${ctx.connectorType}/connect`;
|
|
2282
|
+
console.log(`The ${ctx.label} connector is not connected.`);
|
|
2283
|
+
console.log(`Connect it at: [Connect ${ctx.label}](${connectUrl})`);
|
|
2284
|
+
} else if (isExpired) {
|
|
2285
|
+
const url = `${ctx.platformOrigin}/connectors`;
|
|
2286
|
+
console.log(
|
|
2287
|
+
`The ${ctx.label} connector is connected but has expired and needs to be reconnected.`
|
|
2288
|
+
);
|
|
2289
|
+
console.log(`Reconnect it at: [Reconnect ${ctx.label}](${url})`);
|
|
2290
|
+
} else {
|
|
2291
|
+
console.log(`The ${ctx.label} connector is connected and active.`);
|
|
2292
|
+
}
|
|
2293
|
+
console.log("");
|
|
2294
|
+
console.log(
|
|
2295
|
+
`### 2b: Agent authorization (user must authorize agent to use this connector)`
|
|
2296
|
+
);
|
|
2297
|
+
console.log("");
|
|
2298
|
+
if (!ctx.agentId) {
|
|
2299
|
+
console.log("ZERO_AGENT_ID is not set \u2014 cannot check agent authorization.");
|
|
2300
|
+
} else if (!hasPermission) {
|
|
2301
|
+
const url = `${ctx.platformOrigin}/connectors/${ctx.connectorType}/authorize?agentId=${ctx.agentId}`;
|
|
2302
|
+
console.log(
|
|
2303
|
+
`The ${ctx.label} connector is not authorized for this agent (${ctx.agentId}).`
|
|
2304
|
+
);
|
|
2305
|
+
console.log(`Authorize it at: [Authorize ${ctx.label}](${url})`);
|
|
2306
|
+
} else {
|
|
2307
|
+
console.log(`The ${ctx.label} connector is authorized for this agent.`);
|
|
2308
|
+
}
|
|
2309
|
+
console.log("");
|
|
2310
|
+
return { isConnected, isExpired, hasPermission };
|
|
2311
|
+
}
|
|
2312
|
+
async function checkConnectorDomains(ctx) {
|
|
2313
|
+
console.log(
|
|
2314
|
+
`### 2c: Registered base URLs (credential replacement only applies to URLs matching these prefixes)`
|
|
2315
|
+
);
|
|
2316
|
+
console.log("");
|
|
2317
|
+
const payload = decodeZeroTokenPayload();
|
|
2318
|
+
const runId = payload?.runId;
|
|
2319
|
+
if (!runId) {
|
|
2320
|
+
console.log(
|
|
2321
|
+
"Cannot determine run ID from ZERO_TOKEN \u2014 skipping base URL check."
|
|
2322
|
+
);
|
|
2323
|
+
console.log("");
|
|
2324
|
+
return null;
|
|
2325
|
+
}
|
|
2326
|
+
const runContext = await getZeroRunContext(runId);
|
|
2327
|
+
printConnectorDomains(ctx, runContext);
|
|
2328
|
+
console.log("");
|
|
2329
|
+
return runContext.networkPolicies;
|
|
2330
|
+
}
|
|
2331
|
+
function printConnectorDomains(ctx, runContext) {
|
|
2332
|
+
const matchingEntry = runContext.firewalls.find((fw) => {
|
|
2333
|
+
return fw.ref === ctx.connectorType;
|
|
2334
|
+
});
|
|
2335
|
+
if (!matchingEntry) {
|
|
2336
|
+
console.log(
|
|
2337
|
+
`No configuration found for the ${ctx.label} connector in this run.`
|
|
2338
|
+
);
|
|
2339
|
+
console.log(
|
|
2340
|
+
"This means no base URLs are registered for credential replacement for this connector."
|
|
2341
|
+
);
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
console.log(
|
|
2345
|
+
`The ${ctx.label} connector is configured for this run with the following base URLs:`
|
|
2346
|
+
);
|
|
2347
|
+
for (const api of matchingEntry.apis) {
|
|
2348
|
+
console.log(` - ${api.base}`);
|
|
2349
|
+
}
|
|
2350
|
+
console.log("");
|
|
2351
|
+
console.log(
|
|
2352
|
+
"When the sandbox sends an HTTP request matching one of these URL prefixes, the network boundary intercepts the request and injects real credentials into the auth headers."
|
|
2353
|
+
);
|
|
2354
|
+
if (isFirewallConnectorType(ctx.connectorType)) {
|
|
2355
|
+
const firewallConfig = getConnectorFirewall(ctx.connectorType);
|
|
2356
|
+
const secretNames = extractSecretNamesFromApis(firewallConfig.apis);
|
|
2357
|
+
if (secretNames.length > 0) {
|
|
2358
|
+
console.log(`Credentials resolved from: ${secretNames.join(", ")}`);
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
function checkPermissionPolicy(connectorType, label, permissionName, networkPolicies) {
|
|
2363
|
+
console.log("## Step 3: Permission policy check");
|
|
2364
|
+
console.log("");
|
|
2365
|
+
console.log(
|
|
2366
|
+
`Checking permission: "${permissionName}" for the ${label} connector.`
|
|
2367
|
+
);
|
|
2368
|
+
console.log(
|
|
2369
|
+
`Beyond credential replacement, the ${label} connector enforces permission policies on each API path. A request either matches a named permission or falls through to the unknown-endpoint policy.`
|
|
2370
|
+
);
|
|
2371
|
+
console.log("");
|
|
2372
|
+
if (!networkPolicies) {
|
|
2373
|
+
console.log(
|
|
2374
|
+
"Network policies are not available for this run \u2014 cannot check permission status."
|
|
2375
|
+
);
|
|
2376
|
+
console.log("");
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
const connectorPolicies = networkPolicies[connectorType];
|
|
2380
|
+
if (!connectorPolicies) {
|
|
2381
|
+
console.log(
|
|
2382
|
+
`No policy entry found for the ${label} connector in this run's network policies.`
|
|
2383
|
+
);
|
|
2384
|
+
console.log(
|
|
2385
|
+
"When a connector has no policy entry, all requests are fully permissive (allowed)."
|
|
2386
|
+
);
|
|
2387
|
+
console.log("");
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
console.log(`Permission policies for the ${label} connector:`);
|
|
2391
|
+
console.log(` allow list: [${connectorPolicies.allow.join(", ")}]`);
|
|
2392
|
+
console.log(` deny list: [${connectorPolicies.deny.join(", ")}]`);
|
|
2393
|
+
console.log(` unknown endpoint policy: ${connectorPolicies.unknownPolicy}`);
|
|
2394
|
+
console.log("");
|
|
2395
|
+
const isInAllow = connectorPolicies.allow.includes(permissionName);
|
|
2396
|
+
const isInDeny = connectorPolicies.deny.includes(permissionName);
|
|
2397
|
+
if (isInAllow) {
|
|
2398
|
+
console.log(
|
|
2399
|
+
`Result: "${permissionName}" is in the allow list. Requests matching this permission are allowed.`
|
|
2400
|
+
);
|
|
2401
|
+
} else if (isInDeny) {
|
|
2402
|
+
console.log(
|
|
2403
|
+
`Result: "${permissionName}" is in the deny list. Requests matching this permission are denied.`
|
|
2404
|
+
);
|
|
2405
|
+
} else {
|
|
2406
|
+
console.log(
|
|
2407
|
+
`Result: "${permissionName}" is not in any permission list. It will be handled by the unknown endpoint policy: ${connectorPolicies.unknownPolicy}.`
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2410
|
+
console.log("");
|
|
2411
|
+
}
|
|
2412
|
+
function resolvePermissionFromUrl(connectorType, label, method, relativePath, matchedBase, networkPolicies) {
|
|
2413
|
+
console.log("## Step 3: Permission policy check (auto-detected from URL)");
|
|
2414
|
+
console.log("");
|
|
2415
|
+
console.log(
|
|
2416
|
+
`Matching ${method} ${relativePath} (relative to base URL ${matchedBase}) against the ${label} connector's permission rules.`
|
|
2417
|
+
);
|
|
2418
|
+
console.log("");
|
|
2419
|
+
if (!isFirewallConnectorType(connectorType)) {
|
|
2420
|
+
console.log(
|
|
2421
|
+
`The ${label} connector does not have permission rules defined.`
|
|
2422
|
+
);
|
|
2423
|
+
console.log("");
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
const config = getConnectorFirewall(connectorType);
|
|
2427
|
+
const matchedPermissions = findMatchingPermissions(
|
|
2428
|
+
method,
|
|
2429
|
+
relativePath,
|
|
2430
|
+
config
|
|
2431
|
+
);
|
|
2432
|
+
if (matchedPermissions.length === 0) {
|
|
2433
|
+
console.log(
|
|
2434
|
+
`No named permission matches ${method} ${relativePath}. This request falls through to the unknown-endpoint policy.`
|
|
2435
|
+
);
|
|
2436
|
+
} else {
|
|
2437
|
+
console.log(`Matched permissions: [${matchedPermissions.join(", ")}]`);
|
|
2438
|
+
}
|
|
2439
|
+
console.log("");
|
|
2440
|
+
if (!networkPolicies) {
|
|
2441
|
+
console.log(
|
|
2442
|
+
"Network policies are not available for this run \u2014 cannot check allow/deny status."
|
|
2443
|
+
);
|
|
2444
|
+
console.log("");
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
const connectorPolicies = networkPolicies[connectorType];
|
|
2448
|
+
if (!connectorPolicies) {
|
|
2449
|
+
console.log(
|
|
2450
|
+
`No policy entry found for the ${label} connector. All requests are fully permissive (allowed).`
|
|
2451
|
+
);
|
|
2452
|
+
console.log("");
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
console.log(`Permission policies for the ${label} connector:`);
|
|
2456
|
+
console.log(` allow list: [${connectorPolicies.allow.join(", ")}]`);
|
|
2457
|
+
console.log(` deny list: [${connectorPolicies.deny.join(", ")}]`);
|
|
2458
|
+
console.log(` unknown endpoint policy: ${connectorPolicies.unknownPolicy}`);
|
|
2459
|
+
console.log("");
|
|
2460
|
+
if (matchedPermissions.length === 0) {
|
|
2461
|
+
console.log(
|
|
2462
|
+
`Result: No permission matched. The unknown endpoint policy applies: ${connectorPolicies.unknownPolicy}.`
|
|
2463
|
+
);
|
|
2464
|
+
} else {
|
|
2465
|
+
for (const perm of matchedPermissions) {
|
|
2466
|
+
const isInAllow = connectorPolicies.allow.includes(perm);
|
|
2467
|
+
const isInDeny = connectorPolicies.deny.includes(perm);
|
|
2468
|
+
if (isInAllow) {
|
|
2469
|
+
console.log(`Result: "${perm}" is in the allow list \u2014 allowed.`);
|
|
2470
|
+
} else if (isInDeny) {
|
|
2471
|
+
console.log(`Result: "${perm}" is in the deny list \u2014 denied.`);
|
|
2472
|
+
} else {
|
|
2473
|
+
console.log(
|
|
2474
|
+
`Result: "${perm}" is not in any list \u2014 falls through to unknown endpoint policy: ${connectorPolicies.unknownPolicy}.`
|
|
2475
|
+
);
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
console.log("");
|
|
2480
|
+
}
|
|
2481
|
+
var checkConnectorCommand = new Command().name("check-connector").description(
|
|
2482
|
+
"Diagnose connector health: environment variable, connector configuration, and permission policies"
|
|
2483
|
+
).addOption(
|
|
2484
|
+
new Option(
|
|
2485
|
+
"--env-name <ENV_NAME>",
|
|
2486
|
+
"The environment variable name to check (e.g. GITHUB_TOKEN)"
|
|
2487
|
+
)
|
|
2488
|
+
).addOption(
|
|
2489
|
+
new Option(
|
|
2490
|
+
"--url <URL>",
|
|
2491
|
+
"A full URL to diagnose \u2014 auto-detects the connector, env var, and permission (e.g. https://api.github.com/repos/owner/repo)"
|
|
2492
|
+
)
|
|
2493
|
+
).addOption(
|
|
2494
|
+
new Option(
|
|
2495
|
+
"--method <METHOD>",
|
|
2496
|
+
"HTTP method to use when matching permissions with --url (default: GET)"
|
|
2497
|
+
).default("GET")
|
|
2498
|
+
).addOption(
|
|
2499
|
+
new Option(
|
|
2500
|
+
"--check-permission <name>",
|
|
2501
|
+
"Check whether a specific permission is allowed or denied (e.g. contents:read)"
|
|
2502
|
+
)
|
|
2503
|
+
).addHelpText(
|
|
2213
2504
|
"after",
|
|
2214
2505
|
`
|
|
2215
2506
|
Examples:
|
|
2216
|
-
zero doctor
|
|
2217
|
-
zero doctor
|
|
2218
|
-
zero doctor
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2507
|
+
zero doctor check-connector --env-name GITHUB_TOKEN
|
|
2508
|
+
zero doctor check-connector --url https://api.github.com/repos/owner/repo
|
|
2509
|
+
zero doctor check-connector --url https://slack.com/api/chat.postMessage --method POST
|
|
2510
|
+
zero doctor check-connector --env-name SLACK_TOKEN --check-permission chat:write
|
|
2511
|
+
|
|
2512
|
+
How connectors work:
|
|
2513
|
+
A Connector holds the real credentials for an external service. These credentials
|
|
2514
|
+
are never injected into the sandbox. Instead, when the sandbox sends an HTTP
|
|
2515
|
+
request to a base URL registered by the Connector, the network boundary intercepts
|
|
2516
|
+
the request and replaces the auth headers with real credentials.
|
|
2517
|
+
|
|
2518
|
+
This command checks each part of that pipeline and reports what it finds.`
|
|
2223
2519
|
).action(
|
|
2224
|
-
withErrorHandler(async (
|
|
2225
|
-
|
|
2226
|
-
if (!connectorType) {
|
|
2520
|
+
withErrorHandler(async (opts) => {
|
|
2521
|
+
if (!opts.envName && !opts.url) {
|
|
2227
2522
|
throw new Error(
|
|
2228
|
-
|
|
2523
|
+
"Either --env-name or --url is required. Use --help for usage."
|
|
2229
2524
|
);
|
|
2230
2525
|
}
|
|
2526
|
+
let envName;
|
|
2527
|
+
let connectorType;
|
|
2528
|
+
let urlLookup = null;
|
|
2529
|
+
if (opts.url) {
|
|
2530
|
+
urlLookup = resolveConnectorFromUrl(opts.url);
|
|
2531
|
+
if (!urlLookup) {
|
|
2532
|
+
throw new Error(
|
|
2533
|
+
`No connector found for URL: ${opts.url} \u2014 no registered base URL matches this URL`
|
|
2534
|
+
);
|
|
2535
|
+
}
|
|
2536
|
+
connectorType = urlLookup.connectorType;
|
|
2537
|
+
envName = opts.envName ?? urlLookup.envName;
|
|
2538
|
+
console.log(
|
|
2539
|
+
`URL ${opts.url} matches the ${CONNECTOR_TYPES[connectorType].label} connector (type: ${connectorType}).`
|
|
2540
|
+
);
|
|
2541
|
+
console.log(` Matched base URL: ${urlLookup.matchedBase}`);
|
|
2542
|
+
console.log(` Relative path: ${urlLookup.relativePath}`);
|
|
2543
|
+
console.log(` Environment var: ${envName}`);
|
|
2544
|
+
} else {
|
|
2545
|
+
connectorType = getConnectorTypeForSecretName(
|
|
2546
|
+
envName = opts.envName
|
|
2547
|
+
);
|
|
2548
|
+
if (!connectorType) {
|
|
2549
|
+
throw new Error(
|
|
2550
|
+
`Unknown environment variable: ${envName} \u2014 not managed by any connector`
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2553
|
+
console.log(
|
|
2554
|
+
`${envName} is managed by the ${CONNECTOR_TYPES[connectorType].label} connector (type: ${connectorType}).`
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2557
|
+
console.log("");
|
|
2231
2558
|
const { label } = CONNECTOR_TYPES[connectorType];
|
|
2232
2559
|
const apiUrl = await getApiUrl();
|
|
2233
2560
|
const platformUrl = toPlatformUrl(apiUrl);
|
|
2234
|
-
const
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
}) : Promise.resolve(null)
|
|
2246
|
-
]);
|
|
2247
|
-
const isConnected = connector !== null;
|
|
2248
|
-
const hasPermission = enabledTypes !== null && enabledTypes.includes(connectorType);
|
|
2249
|
-
const rediagnoseHint = `Important: if ${tokenName} is still missing after the user takes action, run \`zero doctor missing-token ${tokenName}\` again to re-diagnose instead of assuming the status.`;
|
|
2250
|
-
if (!isConnected) {
|
|
2251
|
-
const connectUrl = agentId ? `${platformUrl.origin}/connectors/${connectorType}/connect?agentId=${agentId}` : `${platformUrl.origin}/connectors/${connectorType}/connect`;
|
|
2561
|
+
const ctx = {
|
|
2562
|
+
envName,
|
|
2563
|
+
connectorType,
|
|
2564
|
+
label,
|
|
2565
|
+
platformOrigin: platformUrl.origin,
|
|
2566
|
+
agentId: process.env.ZERO_AGENT_ID || void 0
|
|
2567
|
+
};
|
|
2568
|
+
checkEnvVariable(ctx);
|
|
2569
|
+
const { isConnected, isExpired, hasPermission } = await checkConnectorStatus(ctx);
|
|
2570
|
+
const networkPolicies = await checkConnectorDomains(ctx);
|
|
2571
|
+
if (isConnected && !isExpired && hasPermission) {
|
|
2252
2572
|
console.log(
|
|
2253
|
-
`The ${label} connector is
|
|
2254
|
-
${rediagnoseHint}`
|
|
2573
|
+
`Steps 1-2 summary: The ${label} connector is connected, active, and authorized. Outbound requests to the registered base URLs will have credentials injected at the network boundary.`
|
|
2255
2574
|
);
|
|
2256
|
-
return;
|
|
2257
2575
|
}
|
|
2258
|
-
|
|
2259
|
-
if (
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2576
|
+
console.log("");
|
|
2577
|
+
if (urlLookup) {
|
|
2578
|
+
resolvePermissionFromUrl(
|
|
2579
|
+
connectorType,
|
|
2580
|
+
label,
|
|
2581
|
+
opts.method,
|
|
2582
|
+
urlLookup.relativePath,
|
|
2583
|
+
urlLookup.matchedBase,
|
|
2584
|
+
networkPolicies
|
|
2263
2585
|
);
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2586
|
+
} else if (opts.checkPermission) {
|
|
2587
|
+
checkPermissionPolicy(
|
|
2588
|
+
connectorType,
|
|
2589
|
+
label,
|
|
2590
|
+
opts.checkPermission,
|
|
2591
|
+
networkPolicies
|
|
2269
2592
|
);
|
|
2270
2593
|
}
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2594
|
+
const args = [];
|
|
2595
|
+
if (opts.url) {
|
|
2596
|
+
args.push(`--url ${opts.url}`);
|
|
2597
|
+
if (opts.method !== "GET") {
|
|
2598
|
+
args.push(`--method ${opts.method}`);
|
|
2274
2599
|
}
|
|
2275
|
-
console.log(rediagnoseHint);
|
|
2276
2600
|
} else {
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
${
|
|
2281
|
-
);
|
|
2601
|
+
args.push(`--env-name ${envName}`);
|
|
2602
|
+
}
|
|
2603
|
+
if (opts.checkPermission) {
|
|
2604
|
+
args.push(`--check-permission ${opts.checkPermission}`);
|
|
2282
2605
|
}
|
|
2606
|
+
console.log(
|
|
2607
|
+
`To re-diagnose after changes, run: zero doctor check-connector ${args.join(" ")}`
|
|
2608
|
+
);
|
|
2283
2609
|
})
|
|
2284
2610
|
);
|
|
2285
2611
|
|
|
@@ -2509,11 +2835,13 @@ Notes:
|
|
|
2509
2835
|
);
|
|
2510
2836
|
|
|
2511
2837
|
// src/commands/zero/doctor/index.ts
|
|
2512
|
-
var zeroDoctorCommand = new Command().name("doctor").description("Diagnose runtime issues (
|
|
2838
|
+
var zeroDoctorCommand = new Command().name("doctor").description("Diagnose runtime issues (connector health, permission denials)").addCommand(checkConnectorCommand).addCommand(permissionDenyCommand).addCommand(permissionChangeCommand).addHelpText(
|
|
2513
2839
|
"after",
|
|
2514
2840
|
`
|
|
2515
2841
|
Examples:
|
|
2516
|
-
|
|
2842
|
+
Check a connector? zero doctor check-connector --env-name GITHUB_TOKEN
|
|
2843
|
+
Check a URL? zero doctor check-connector --url https://api.github.com/repos/owner/repo
|
|
2844
|
+
Check with permission? zero doctor check-connector --env-name SLACK_TOKEN --check-permission chat:write
|
|
2517
2845
|
Permission denied? zero doctor permission-deny github --method GET --path /repos/owner/repo
|
|
2518
2846
|
Change a permission? zero doctor permission-change github --permission contents:read --enable
|
|
2519
2847
|
|
|
@@ -6011,11 +6339,11 @@ function registerZeroCommands(prog, commands) {
|
|
|
6011
6339
|
var program = new Command();
|
|
6012
6340
|
program.name("zero").description(
|
|
6013
6341
|
"Zero CLI \u2014 interact with the zero platform from inside the sandbox"
|
|
6014
|
-
).version("9.
|
|
6342
|
+
).version("9.114.0").addHelpText(
|
|
6015
6343
|
"after",
|
|
6016
6344
|
`
|
|
6017
6345
|
Examples:
|
|
6018
|
-
|
|
6346
|
+
Check a connector? zero doctor check-connector --env-name <ENV_NAME>
|
|
6019
6347
|
Send a Slack message? zero slack message send --help
|
|
6020
6348
|
Set up a schedule? zero schedule setup --help
|
|
6021
6349
|
Update yourself? zero agent --help
|