@cantinasecurity/apex-cli 0.1.17 → 0.1.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/.claude/skills/apex-cli/SKILL.md +30 -11
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.claude.json +1 -1
- package/.mcp.codex.json +1 -1
- package/MARKETPLACE.md +1 -1
- package/README.md +102 -5
- package/dist/apex.js +15 -1
- package/dist/api-client.js +255 -35
- package/dist/args.js +8 -1
- package/dist/auth.js +171 -54
- package/dist/claim-proof.js +65 -0
- package/dist/commands.js +253 -19
- package/dist/config.js +357 -23
- package/dist/findings.js +50 -9
- package/dist/help.js +14 -1
- package/dist/local-source-scan.js +200 -24
- package/dist/mcp.js +128 -19
- package/dist/onboarding.js +661 -0
- package/dist/redaction.js +34 -0
- package/dist/session.js +10 -1
- package/dist/shell.js +10 -3
- package/dist/telemetry.js +83 -12
- package/dist/workspace-binding.js +16 -6
- package/package.json +3 -1
- package/skills/apex-cli/SKILL.md +43 -11
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const APEX_SECRET_PATTERN = /(apex_(?:ct|at|rt|dc)_)[A-Za-z0-9._~+/=-]+/gi;
|
|
2
|
+
const ONBOARDING_CODE_PATTERN = /SPBT-[A-Z0-9]{2,24}-[A-Z0-9]{8}/gi;
|
|
3
|
+
// Hex-only boundaries avoid redacting a 64-character window from a longer hex
|
|
4
|
+
// value while still catching fingerprints next to punctuation or prose.
|
|
5
|
+
const ONBOARDING_FINGERPRINT_PATTERN = /(?<![A-Fa-f0-9])[A-Fa-f0-9]{64}(?![A-Fa-f0-9])/g;
|
|
6
|
+
export function redactSensitiveText(value) {
|
|
7
|
+
return value
|
|
8
|
+
.replace(APEX_SECRET_PATTERN, (_match, prefix) => `${prefix}[REDACTED]`)
|
|
9
|
+
.replace(ONBOARDING_CODE_PATTERN, "SPBT-[REDACTED]")
|
|
10
|
+
.replace(ONBOARDING_FINGERPRINT_PATTERN, "[REDACTED]");
|
|
11
|
+
}
|
|
12
|
+
export function containsSensitiveText(value) {
|
|
13
|
+
return redactSensitiveText(value) !== value;
|
|
14
|
+
}
|
|
15
|
+
/** Redacts every string key and value in an API error body without mutating it. */
|
|
16
|
+
export function redactSensitiveValue(value) {
|
|
17
|
+
const seen = new WeakSet();
|
|
18
|
+
const visit = (current) => {
|
|
19
|
+
if (typeof current === "string")
|
|
20
|
+
return redactSensitiveText(current);
|
|
21
|
+
if (!current || typeof current !== "object")
|
|
22
|
+
return current;
|
|
23
|
+
if (seen.has(current))
|
|
24
|
+
return "[CIRCULAR]";
|
|
25
|
+
seen.add(current);
|
|
26
|
+
if (Array.isArray(current))
|
|
27
|
+
return current.map(visit);
|
|
28
|
+
return Object.fromEntries(Object.entries(current).map(([key, nested]) => [
|
|
29
|
+
redactSensitiveText(key),
|
|
30
|
+
visit(nested),
|
|
31
|
+
]));
|
|
32
|
+
};
|
|
33
|
+
return visit(value);
|
|
34
|
+
}
|
package/dist/session.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { canPromptInteractively, getFlagList, getFlagString, isJsonMode, } from "./args.js";
|
|
3
|
-
import { login } from "./auth.js";
|
|
3
|
+
import { getMe, login } from "./auth.js";
|
|
4
4
|
import { ApiError } from "./api-client.js";
|
|
5
|
+
import { isClaimProofAvailable } from "./claim-proof.js";
|
|
5
6
|
import { openInBrowser } from "./browser.js";
|
|
6
7
|
import { loadConfig } from "./config.js";
|
|
7
8
|
import { chooseOne, confirm, promptText, waitForEnter } from "./prompt.js";
|
|
@@ -132,6 +133,14 @@ function ensurePlannedSources(selection, resolve) {
|
|
|
132
133
|
};
|
|
133
134
|
}
|
|
134
135
|
export async function ensureAuthenticated(client, flags) {
|
|
136
|
+
const existing = await getMe(client);
|
|
137
|
+
if (existing) {
|
|
138
|
+
return existing;
|
|
139
|
+
}
|
|
140
|
+
const credentials = await client.getCredentials();
|
|
141
|
+
if (isClaimProofAvailable(credentials)) {
|
|
142
|
+
throw new Error("Your guest trial session is no longer available, but its claim proof is stored on this machine. Run `apex claim` to keep the trial results.");
|
|
143
|
+
}
|
|
135
144
|
return login(client, {
|
|
136
145
|
noOpen: flags["no-open"] === true,
|
|
137
146
|
quiet: isJsonMode(flags),
|
package/dist/shell.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { SHELL_HELP_TEXT } from "./help.js";
|
|
2
|
-
import { commandCancelScan, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingFeedback, commandFindingFixReview, commandFindings, commandLogout, commandScan, commandScans, commandStatus, commandTelemetry, commandUpdate, commandWorkspace, commandWorkspaceUse, commandWorkspaces, initializeInteractiveSession, openCurrentApexView, } from "./commands.js";
|
|
2
|
+
import { commandCancelScan, commandClaim, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingFeedback, commandFindingFixReview, commandFindings, commandLogout, commandScan, commandScans, commandStatus, commandTelemetry, commandUpdate, commandWorkspace, commandWorkspaceUse, commandWorkspaces, initializeInteractiveSession, openCurrentApexView, } from "./commands.js";
|
|
3
3
|
import { withFlag } from "./args.js";
|
|
4
4
|
import { printCompanyDetails, printInteractiveSessionSummary, printSourceList, printWorkspaceDetails, } from "./output.js";
|
|
5
5
|
import { readLine } from "./prompt.js";
|
|
6
6
|
import { formatApiError } from "./api-client.js";
|
|
7
7
|
import { createShellTelemetryInvocation, emitInvocationCompleted, emitInvocationStarted, withTelemetryContext, } from "./telemetry.js";
|
|
8
8
|
const SHELL_COMPLETIONS = [
|
|
9
|
+
{ command: "claim" },
|
|
9
10
|
{ command: "credits" },
|
|
10
11
|
{ command: "scan", args: ["standard", "audit", "ultra", "lite", "pr"] },
|
|
11
12
|
{ command: "scans" },
|
|
@@ -193,6 +194,13 @@ async function runShellCommand(client, cwd, parsed, shellFlags, session) {
|
|
|
193
194
|
case "credits":
|
|
194
195
|
await commandCredits(client, cwd, shellFlags);
|
|
195
196
|
return {};
|
|
197
|
+
case "claim":
|
|
198
|
+
if (parsed.args.length > 0) {
|
|
199
|
+
process.stderr.write("Usage: /claim\n");
|
|
200
|
+
return {};
|
|
201
|
+
}
|
|
202
|
+
await commandClaim(client, cwd, shellFlags);
|
|
203
|
+
return false;
|
|
196
204
|
case "scan": {
|
|
197
205
|
const mode = parsed.args[0];
|
|
198
206
|
if (mode && !["standard", "audit", "ultra", "lite", "pr"].includes(mode)) {
|
|
@@ -329,8 +337,7 @@ async function runShellCommand(client, cwd, parsed, shellFlags, session) {
|
|
|
329
337
|
await commandUpdate(shellFlags);
|
|
330
338
|
return false;
|
|
331
339
|
case "logout":
|
|
332
|
-
await commandLogout(client, shellFlags);
|
|
333
|
-
return false;
|
|
340
|
+
return (await commandLogout(client, shellFlags)).loggedOut ? false : {};
|
|
334
341
|
case "repos":
|
|
335
342
|
printSourceList(session.sources);
|
|
336
343
|
return {};
|
package/dist/telemetry.js
CHANGED
|
@@ -2,7 +2,8 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { loadConfig,
|
|
5
|
+
import { loadConfig, updateConfig } from "./config.js";
|
|
6
|
+
import { containsSensitiveText } from "./redaction.js";
|
|
6
7
|
import { APEX_CLI_VERSION } from "./version.js";
|
|
7
8
|
const TELEMETRY_ENDPOINT_PATH = "/api/cli/v1/telemetry/events";
|
|
8
9
|
const TELEMETRY_SESSION_ID = randomUUID();
|
|
@@ -29,6 +30,7 @@ const KNOWN_CLI_FLAG_NAMES = [
|
|
|
29
30
|
"parent-comment",
|
|
30
31
|
"pr",
|
|
31
32
|
"pr-path",
|
|
33
|
+
"replace",
|
|
32
34
|
"repo",
|
|
33
35
|
"review-target",
|
|
34
36
|
"scan",
|
|
@@ -41,6 +43,7 @@ const KNOWN_CLI_FLAG_NAMES = [
|
|
|
41
43
|
const KNOWN_CLI_COMMANDS = [
|
|
42
44
|
"cancel-scan",
|
|
43
45
|
"connect",
|
|
46
|
+
"claim",
|
|
44
47
|
"credits",
|
|
45
48
|
"doctor",
|
|
46
49
|
"export",
|
|
@@ -59,6 +62,50 @@ const KNOWN_CLI_COMMANDS = [
|
|
|
59
62
|
"update",
|
|
60
63
|
"workspace",
|
|
61
64
|
"workspaces",
|
|
65
|
+
"redeem",
|
|
66
|
+
];
|
|
67
|
+
const KNOWN_SHELL_COMMANDS = [
|
|
68
|
+
"cancel",
|
|
69
|
+
"cancel-scan",
|
|
70
|
+
"claim",
|
|
71
|
+
"clear",
|
|
72
|
+
"company",
|
|
73
|
+
"connect",
|
|
74
|
+
"credits",
|
|
75
|
+
"doctor",
|
|
76
|
+
"exit",
|
|
77
|
+
"export",
|
|
78
|
+
"findings",
|
|
79
|
+
"help",
|
|
80
|
+
"logout",
|
|
81
|
+
"open",
|
|
82
|
+
"quit",
|
|
83
|
+
"repos",
|
|
84
|
+
"scan",
|
|
85
|
+
"scans",
|
|
86
|
+
"status",
|
|
87
|
+
"telemetry",
|
|
88
|
+
"update",
|
|
89
|
+
"workspace",
|
|
90
|
+
"workspaces",
|
|
91
|
+
];
|
|
92
|
+
const ONBOARDING_TELEMETRY_OUTCOMES = [
|
|
93
|
+
"redeemed",
|
|
94
|
+
"idempotent",
|
|
95
|
+
"already_authenticated",
|
|
96
|
+
"invalid",
|
|
97
|
+
"expired",
|
|
98
|
+
"exhausted",
|
|
99
|
+
"rate_limited",
|
|
100
|
+
"capacity",
|
|
101
|
+
"active_guest_session",
|
|
102
|
+
"claimed",
|
|
103
|
+
"email_unverified",
|
|
104
|
+
"claim_expired",
|
|
105
|
+
"already_claimed",
|
|
106
|
+
"scan_in_progress",
|
|
107
|
+
"fingerprint_mismatch",
|
|
108
|
+
"error",
|
|
62
109
|
];
|
|
63
110
|
let cachedParentProcesses = null;
|
|
64
111
|
let volatileTelemetryInstallId = null;
|
|
@@ -83,6 +130,8 @@ function safeToken(value, fallback = "unknown") {
|
|
|
83
130
|
const trimmed = value?.trim();
|
|
84
131
|
if (!trimmed)
|
|
85
132
|
return fallback;
|
|
133
|
+
if (containsSensitiveText(trimmed))
|
|
134
|
+
return fallback;
|
|
86
135
|
const normalized = trimmed
|
|
87
136
|
.slice(0, 80)
|
|
88
137
|
.replace(/[^A-Za-z0-9_.:-]+/g, "_")
|
|
@@ -173,6 +222,7 @@ function sanitizeCliFlags(flags) {
|
|
|
173
222
|
unknownFlagCount: unknownFlagCount(flags),
|
|
174
223
|
json: boolFlag(flags, "json"),
|
|
175
224
|
force: boolFlag(flags, "force"),
|
|
225
|
+
replace: boolFlag(flags, "replace"),
|
|
176
226
|
nonInteractive: boolFlag(flags, "non-interactive"),
|
|
177
227
|
noOpen: boolFlag(flags, "no-open"),
|
|
178
228
|
companyProvided: stringFlag(flags, "company") !== null,
|
|
@@ -323,6 +373,11 @@ function safeCliCommand(parsed) {
|
|
|
323
373
|
const command = parsed.command ?? "interactive-shell";
|
|
324
374
|
return KNOWN_CLI_COMMANDS.includes(command) ? command : "unknown";
|
|
325
375
|
}
|
|
376
|
+
function safeShellCommand(command) {
|
|
377
|
+
return KNOWN_SHELL_COMMANDS.includes(command)
|
|
378
|
+
? command
|
|
379
|
+
: "unknown";
|
|
380
|
+
}
|
|
326
381
|
function getInputKeys(input) {
|
|
327
382
|
return Object.entries(input)
|
|
328
383
|
.filter(([, value]) => value !== undefined)
|
|
@@ -556,7 +611,7 @@ export function createShellTelemetryInvocation(parsed, flags) {
|
|
|
556
611
|
: null;
|
|
557
612
|
return createInvocation({
|
|
558
613
|
surface: "interactive_shell",
|
|
559
|
-
command: parsed.command,
|
|
614
|
+
command: safeShellCommand(parsed.command),
|
|
560
615
|
metadata: {
|
|
561
616
|
argCount: parsed.args.length,
|
|
562
617
|
...sanitizeCliFlags(flags),
|
|
@@ -587,6 +642,19 @@ export async function withTelemetryContext(invocation, action) {
|
|
|
587
642
|
function currentInvocation() {
|
|
588
643
|
return telemetryContext.getStore() ?? null;
|
|
589
644
|
}
|
|
645
|
+
export function setTelemetryAuthKind(authKind) {
|
|
646
|
+
const invocation = currentInvocation();
|
|
647
|
+
if (!invocation)
|
|
648
|
+
return;
|
|
649
|
+
invocation.metadata.authKind = authKind;
|
|
650
|
+
}
|
|
651
|
+
export function setTelemetryOnboardingOutcome(outcome) {
|
|
652
|
+
const invocation = currentInvocation();
|
|
653
|
+
if (!invocation)
|
|
654
|
+
return;
|
|
655
|
+
invocation.metadata.onboardingOutcome =
|
|
656
|
+
safeEnum(outcome, ONBOARDING_TELEMETRY_OUTCOMES) ?? "error";
|
|
657
|
+
}
|
|
590
658
|
function getTelemetryDisabled(config) {
|
|
591
659
|
const disabledEnv = getDisabledEnvName();
|
|
592
660
|
if (disabledEnv)
|
|
@@ -607,10 +675,7 @@ async function getTelemetryStatusInternal(options) {
|
|
|
607
675
|
installId = randomUUID();
|
|
608
676
|
volatileTelemetryInstallId = installId;
|
|
609
677
|
try {
|
|
610
|
-
await
|
|
611
|
-
...config,
|
|
612
|
-
telemetryInstallId: installId,
|
|
613
|
-
});
|
|
678
|
+
await updateConfig({ telemetryInstallId: installId });
|
|
614
679
|
}
|
|
615
680
|
catch {
|
|
616
681
|
// Telemetry-only ID creation must not break normal CLI/API behavior.
|
|
@@ -635,10 +700,7 @@ export async function getTelemetryStatus(surface = "cli", options = {}) {
|
|
|
635
700
|
}
|
|
636
701
|
export async function setTelemetryEnabled(enabled) {
|
|
637
702
|
const config = await loadConfig();
|
|
638
|
-
await
|
|
639
|
-
...config,
|
|
640
|
-
telemetryDisabled: enabled ? false : true,
|
|
641
|
-
});
|
|
703
|
+
await updateConfig({ telemetryDisabled: enabled ? false : true });
|
|
642
704
|
return getTelemetryStatus("cli", { createInstallId: enabled });
|
|
643
705
|
}
|
|
644
706
|
function runtimePayload() {
|
|
@@ -662,10 +724,19 @@ function errorPayload(error) {
|
|
|
662
724
|
}
|
|
663
725
|
const maybeError = error;
|
|
664
726
|
const status = typeof maybeError.status === "number" ? maybeError.status : null;
|
|
727
|
+
const name = typeof maybeError.name === "string" &&
|
|
728
|
+
["AbortError", "ApiError", "Error", "TypeError"].includes(maybeError.name)
|
|
729
|
+
? maybeError.name
|
|
730
|
+
: "Error";
|
|
731
|
+
const code = typeof maybeError.code === "string" &&
|
|
732
|
+
!containsSensitiveText(maybeError.code) &&
|
|
733
|
+
/^[A-Z][A-Z0-9_]{1,63}$/.test(maybeError.code)
|
|
734
|
+
? maybeError.code
|
|
735
|
+
: null;
|
|
665
736
|
return {
|
|
666
|
-
name
|
|
737
|
+
name,
|
|
667
738
|
status,
|
|
668
|
-
code
|
|
739
|
+
code,
|
|
669
740
|
category: status ? `http_${status}` : "error",
|
|
670
741
|
};
|
|
671
742
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
function getWorkspaceDir(cwd) {
|
|
4
5
|
return path.join(cwd, ".apex");
|
|
@@ -18,12 +19,21 @@ export async function loadWorkspaceBinding(cwd) {
|
|
|
18
19
|
export async function saveWorkspaceBinding(cwd, binding) {
|
|
19
20
|
const workspaceDir = getWorkspaceDir(cwd);
|
|
20
21
|
const bindingPath = getWorkspaceBindingPath(cwd);
|
|
22
|
+
const temporaryPath = path.join(workspaceDir, `.workspace.json.${process.pid}.${randomUUID()}.tmp`);
|
|
21
23
|
await mkdir(workspaceDir, { recursive: true, mode: 0o700 });
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
try {
|
|
25
|
+
await writeFile(temporaryPath, `${JSON.stringify(binding, null, 2)}\n`, {
|
|
26
|
+
encoding: "utf8",
|
|
27
|
+
mode: 0o600,
|
|
28
|
+
});
|
|
29
|
+
await chmod(temporaryPath, 0o600);
|
|
30
|
+
await rename(temporaryPath, bindingPath);
|
|
31
|
+
await chmod(bindingPath, 0o600);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
27
37
|
}
|
|
28
38
|
export async function clearWorkspaceBinding(cwd) {
|
|
29
39
|
await rm(getWorkspaceBindingPath(cwd), { force: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cantinasecurity/apex-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Standalone CLI and MCP server for Apex.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -29,11 +29,13 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
31
31
|
"ignore": "^7.0.5",
|
|
32
|
+
"proper-lockfile": "^4.1.2",
|
|
32
33
|
"tar": "^7.4.3",
|
|
33
34
|
"zod": "^4.3.6"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@types/node": "^22.18.6",
|
|
38
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
37
39
|
"tsx": "^4.21.0",
|
|
38
40
|
"typescript": "^5.9.3",
|
|
39
41
|
"vitest": "^2.1.9"
|
package/skills/apex-cli/SKILL.md
CHANGED
|
@@ -15,22 +15,48 @@ For serverless, cron, or other HTTP-only integrations, use the hosted Apex REST
|
|
|
15
15
|
|
|
16
16
|
For on-prem or other non-default Apex hosts, make sure the MCP server runs with `APEX_BASE_URL` set to the external HTTPS Bedrock origin before auth or provider connection flows.
|
|
17
17
|
|
|
18
|
+
Trial-code exception: if the user supplied a Cantina trial code, do not call
|
|
19
|
+
`apex-auth-start` or open a login URL. Run `apex redeem <code> --json` in a
|
|
20
|
+
terminal from the repository root, then use the normal MCP scan tools with the
|
|
21
|
+
shared guest credentials. Only when the first attempt has an ambiguous
|
|
22
|
+
transport/connection failure or interrupted/missing response with no typed
|
|
23
|
+
server error may you retry that exact same command once on the same machine,
|
|
24
|
+
HOME, and repository root. Never retry a typed server rejection or make more
|
|
25
|
+
than one recovery retry. Treat the code as a secret: pass it only to
|
|
26
|
+
`apex redeem`; never echo it, write it to a file, or include it in an MCP call
|
|
27
|
+
or tool result. Trial redemption, claim, and revocation are hosted-only; stop
|
|
28
|
+
before passing the code when `APEX_BASE_URL` has any origin other than
|
|
29
|
+
`https://ai.cantina.xyz`. Stop if the redeem outcome is not `redeemed` or
|
|
30
|
+
`idempotent`.
|
|
31
|
+
|
|
18
32
|
Workflow:
|
|
19
33
|
|
|
20
34
|
1. Start with `apex-auth-status`.
|
|
21
|
-
2.
|
|
22
|
-
3.
|
|
23
|
-
4.
|
|
24
|
-
5.
|
|
25
|
-
6.
|
|
26
|
-
7.
|
|
27
|
-
8. For
|
|
28
|
-
9.
|
|
29
|
-
10.
|
|
35
|
+
2. Always call `apex-trial-status` next, even when account authentication succeeded. When `claimAvailable` is true, do not call `apex-auth-start`; tell the user to run `apex claim` in a terminal first so the trial workspace, scans, and findings are kept. When the claim deadline has passed and `claimAvailable` is false, continue normal account authentication instead of steering the user to an expired claim.
|
|
36
|
+
3. Only when Apex is unauthenticated, no unexpired claim proof is available, and no trial code was supplied, call `apex-auth-start`, ask the user to approve the device-login URL, then call `apex-auth-wait` with both the returned `deviceCode` and opaque `credentialGeneration`. If the wait is superseded, keep the current credential state and re-check `apex-auth-status` before starting another login.
|
|
37
|
+
4. For repository work, pass `cwd` explicitly.
|
|
38
|
+
5. Call `apex-doctor` before starting a scan if workspace binding or source planning might be unclear.
|
|
39
|
+
6. If the directory is not bound to the right workspace, call `apex-workspaces` and `apex-workspace-use`.
|
|
40
|
+
7. Use `apex-scan`, `apex-status`, `apex-scans`, `apex-findings`, and `apex-export-findings` for the scan lifecycle.
|
|
41
|
+
8. For Lite scans, call `apex-scan` with `mode: "lite"` only when the company has Lite Scan beta access and the sources are provider-backed.
|
|
42
|
+
9. For PR scans, call `apex-scan` with `mode: "pr"` and `pullRequests`.
|
|
43
|
+
10. Use `apex-finding-comment` and `apex-finding-feedback` when the user wants to leave review notes or valid/invalid feedback on a finding.
|
|
44
|
+
11. For fix reviews, call `apex-finding-fix-review`. Pass `fixPrUrls` for a PR-based review, or omit them to review the current repository state when Bedrock has original scan repository context.
|
|
45
|
+
|
|
46
|
+
If the Apex MCP server is not configured, fall back to the local CLI:
|
|
47
|
+
|
|
48
|
+
- Prefer scripted commands over the interactive shell.
|
|
49
|
+
- A trial redeem with an ambiguous transport or response-loss failure may be retried exactly once with the same code, machine, HOME, and repository root. Never retry a typed server rejection or use `--replace` for response-loss recovery.
|
|
50
|
+
- After a successful scan JSON response, inspect `localPersistence.workspaceBindingSaved`. If it is `false`, the server scan already started: capture and report `binding.lastScanId`, then STOP; do not run status or findings until local repository permissions are repaired.
|
|
51
|
+
- `apex-trial-status` is MCP-only; do not invoke it as a CLI command. In a CLI-only flow, use the `claimDeadline` returned by `apex redeem <code> --json` without repeating or persisting the secret code.
|
|
52
|
+
- After a guest scan, tell the user to run `apex claim` on the same machine before that returned claim deadline. If the deadline is unavailable or uncertain, do not guess or start a replacement login; run `apex claim` and let the server validate the proof.
|
|
53
|
+
- Claim currently requires a Cantina account that does not already own a personal Apex tenant. If the user already completed Cantina Apex onboarding, explain that CLI linking is not available yet and tell them to contact Cantina support before the claim deadline.
|
|
30
54
|
|
|
31
55
|
Guidelines:
|
|
32
56
|
|
|
33
57
|
- Do not rely on interactive CLI prompts. The MCP tools are intentionally non-interactive.
|
|
58
|
+
- The MCP-only `apex-trial-status` tool may report local guest and claim deadlines, but it must never return a trial code or claim token. `claimAvailable` becomes false at a known passed deadline; missing or invalid local deadline metadata remains conservative and server-authoritative. There is intentionally no MCP redemption or claim tool.
|
|
59
|
+
- When MCP remains available after a guest scan, remind the user to run `apex claim` on the same machine while `claimAvailable` is true; after a known passed deadline, continue with normal account login.
|
|
34
60
|
- Service-key management has no MCP tools. Never ask a user to paste a service key into a prompt, and never place one in an MCP call, tool result, source file, or log.
|
|
35
61
|
- If a user explicitly needs a new service key, tell them to run `apex login` if needed, then run `apex service-key create --name <name> --scope <scope[,scope]>` directly in their own terminal. Use only the required scopes: `scans:create` to trigger, `scans:read` to poll status, and `findings:read` to retrieve findings. A P1-only adapter can use `--scope scans:read,findings:read`; omitting `--scope` grants all three. The CLI automatically selects the company when only one is available and prompts when there are several; use `--company <id-or-handle>` to skip that prompt or resolve a non-interactive command. Service-key commands never initiate login, and the secret is shown once.
|
|
36
62
|
- For a headless HTTP adapter, read the service key from a secret-backed environment variable, send it as a bearer token to `/api/apex/v1`, and redact it from errors and logs. Do not shell out to `apex` or launch MCP from the adapter.
|
|
@@ -48,9 +74,15 @@ Guidelines:
|
|
|
48
74
|
- Use `force: true` on `apex-scan` only when the user explicitly wants to replace or overlap an active scan.
|
|
49
75
|
- When checking a scan that is not the workspace binding's latest scan, pass `scanId` to `apex-status`; use `apex-scans` first if you need to discover scan IDs.
|
|
50
76
|
- Prefer `apex-findings` for quick inspection and `apex-export-findings` when the user needs a file artifact.
|
|
51
|
-
- Finding comments, feedback, and fix review scan starts use the same Apex device-login credentials as read tools. If a write tool reports missing auth, re-run `apex-auth-status` and complete `apex-auth-start` / `apex-auth-wait` instead of asking for browser cookies or auth tokens.
|
|
52
|
-
- Anonymous telemetry records only sanitized command/tool metadata such as command names, enum modes, counts, durations, success/failure categories, CLI version, and client integration. It must not include raw repository paths, scan IDs, finding IDs, comments, file paths, PR URLs, or tokens.
|
|
77
|
+
- Finding comments, feedback, and fix review scan starts use the same Apex device-login credentials as read tools. If a write tool reports missing auth, re-run `apex-auth-status`, check `apex-trial-status`, and complete `apex-auth-start` / `apex-auth-wait` only when no unexpired local claim proof is available, instead of asking for browser cookies or auth tokens.
|
|
78
|
+
- Anonymous telemetry records only sanitized command/tool metadata such as command names, enum modes, counts, durations, success/failure categories, CLI version, and client integration. It must not include raw repository paths, scan IDs, finding IDs, comments, file paths, PR URLs, trial codes, claim tokens, onboarding fingerprints, or bearer tokens.
|
|
53
79
|
- Invalid finding feedback requires `dismissalReason`; valid feedback can include `suggestedSeverity`, including `extreme`.
|
|
54
80
|
- PR-based fix reviews can pass `fixPrUrls` directly to `apex-finding-fix-review`; agents that also need to record fixed feedback can first call `apex-finding-feedback` with `status: "valid"`, `labels: ["fixed"]`, and `fixPrUrls`.
|
|
55
81
|
- Current-repository fix reviews omit `fixPrUrls` and require Bedrock original scan repository context for the finding.
|
|
56
82
|
- Finding identifiers such as `KERN2-25` resolve against the selected or latest scan for the current workspace binding. Pass an explicit scan when needed, or use the finding UUID directly.
|
|
83
|
+
|
|
84
|
+
Example trial flow: redeem a user-supplied code with `apex redeem <code> --json`
|
|
85
|
+
in a terminal, use MCP for scan/status/findings after redemption, then check the
|
|
86
|
+
MCP-only `apex-trial-status` tool and remind the user to run `apex claim`
|
|
87
|
+
locally while `claimAvailable` remains true. In a CLI-only flow, use the
|
|
88
|
+
redeem response's `claimDeadline` instead.
|