@cantinasecurity/apex-cli 0.1.16 → 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 +33 -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 +109 -8
- 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 +287 -23
- package/dist/config.js +357 -23
- package/dist/findings.js +50 -9
- package/dist/help.js +21 -5
- package/dist/local-source-scan.js +200 -24
- package/dist/mcp.js +132 -23
- package/dist/onboarding.js +661 -0
- package/dist/redaction.js +34 -0
- package/dist/session.js +10 -1
- package/dist/shell.js +13 -6
- package/dist/telemetry.js +98 -14
- package/dist/workspace-binding.js +16 -6
- package/package.json +3 -1
- package/skills/apex-cli/SKILL.md +45 -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,13 +1,14 @@
|
|
|
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
|
-
{ command: "scan", args: ["standard", "audit", "ultra", "pr"] },
|
|
11
|
+
{ command: "scan", args: ["standard", "audit", "ultra", "lite", "pr"] },
|
|
11
12
|
{ command: "scans" },
|
|
12
13
|
{ command: "findings", args: ["comment", "feedback", "fix-review"] },
|
|
13
14
|
{ command: "export" },
|
|
@@ -193,10 +194,17 @@ 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
|
-
if (mode && !["standard", "audit", "ultra", "pr"].includes(mode)) {
|
|
199
|
-
process.stderr.write("Usage: /scan [standard|audit|ultra|pr] [pr-number]\n");
|
|
206
|
+
if (mode && !["standard", "audit", "ultra", "lite", "pr"].includes(mode)) {
|
|
207
|
+
process.stderr.write("Usage: /scan [standard|audit|ultra|lite|pr] [pr-number]\n");
|
|
200
208
|
return {};
|
|
201
209
|
}
|
|
202
210
|
const prNumber = mode === "pr" ? parsed.args[1] : null;
|
|
@@ -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,
|
|
@@ -184,7 +234,13 @@ function sanitizeCliFlags(flags) {
|
|
|
184
234
|
contentProvided: stringFlag(flags, "content") !== null,
|
|
185
235
|
commentProvided: stringFlag(flags, "comment") !== null,
|
|
186
236
|
parentCommentProvided: stringFlag(flags, "parent-comment") !== null,
|
|
187
|
-
mode: safeEnum(stringFlag(flags, "mode"), [
|
|
237
|
+
mode: safeEnum(stringFlag(flags, "mode"), [
|
|
238
|
+
"standard",
|
|
239
|
+
"audit",
|
|
240
|
+
"ultra",
|
|
241
|
+
"lite",
|
|
242
|
+
"pr",
|
|
243
|
+
]),
|
|
188
244
|
sourceMode: safeEnum(stringFlag(flags, "source-mode"), ["auto", "remote", "local"]),
|
|
189
245
|
format: safeEnum(stringFlag(flags, "format"), ["markdown", "json", "gitlab-sast"]),
|
|
190
246
|
feedbackStatus: safeEnum(stringFlag(flags, "status"), ["valid", "invalid"]),
|
|
@@ -317,6 +373,11 @@ function safeCliCommand(parsed) {
|
|
|
317
373
|
const command = parsed.command ?? "interactive-shell";
|
|
318
374
|
return KNOWN_CLI_COMMANDS.includes(command) ? command : "unknown";
|
|
319
375
|
}
|
|
376
|
+
function safeShellCommand(command) {
|
|
377
|
+
return KNOWN_SHELL_COMMANDS.includes(command)
|
|
378
|
+
? command
|
|
379
|
+
: "unknown";
|
|
380
|
+
}
|
|
320
381
|
function getInputKeys(input) {
|
|
321
382
|
return Object.entries(input)
|
|
322
383
|
.filter(([, value]) => value !== undefined)
|
|
@@ -351,6 +412,7 @@ export function sanitizeMcpInput(input = {}) {
|
|
|
351
412
|
"standard",
|
|
352
413
|
"audit",
|
|
353
414
|
"ultra",
|
|
415
|
+
"lite",
|
|
354
416
|
"pr",
|
|
355
417
|
]),
|
|
356
418
|
sourceMode: safeEnum(typeof input.sourceMode === "string" ? input.sourceMode : null, [
|
|
@@ -538,12 +600,18 @@ export function createCliTelemetryInvocation(parsed) {
|
|
|
538
600
|
}
|
|
539
601
|
export function createShellTelemetryInvocation(parsed, flags) {
|
|
540
602
|
const shellMode = parsed.command === "scan"
|
|
541
|
-
? safeEnum(parsed.args[0] ?? "default", [
|
|
603
|
+
? safeEnum(parsed.args[0] ?? "default", [
|
|
604
|
+
"standard",
|
|
605
|
+
"audit",
|
|
606
|
+
"ultra",
|
|
607
|
+
"lite",
|
|
608
|
+
"pr",
|
|
609
|
+
]) ??
|
|
542
610
|
(parsed.args[0] ? "other" : "default")
|
|
543
611
|
: null;
|
|
544
612
|
return createInvocation({
|
|
545
613
|
surface: "interactive_shell",
|
|
546
|
-
command: parsed.command,
|
|
614
|
+
command: safeShellCommand(parsed.command),
|
|
547
615
|
metadata: {
|
|
548
616
|
argCount: parsed.args.length,
|
|
549
617
|
...sanitizeCliFlags(flags),
|
|
@@ -574,6 +642,19 @@ export async function withTelemetryContext(invocation, action) {
|
|
|
574
642
|
function currentInvocation() {
|
|
575
643
|
return telemetryContext.getStore() ?? null;
|
|
576
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
|
+
}
|
|
577
658
|
function getTelemetryDisabled(config) {
|
|
578
659
|
const disabledEnv = getDisabledEnvName();
|
|
579
660
|
if (disabledEnv)
|
|
@@ -594,10 +675,7 @@ async function getTelemetryStatusInternal(options) {
|
|
|
594
675
|
installId = randomUUID();
|
|
595
676
|
volatileTelemetryInstallId = installId;
|
|
596
677
|
try {
|
|
597
|
-
await
|
|
598
|
-
...config,
|
|
599
|
-
telemetryInstallId: installId,
|
|
600
|
-
});
|
|
678
|
+
await updateConfig({ telemetryInstallId: installId });
|
|
601
679
|
}
|
|
602
680
|
catch {
|
|
603
681
|
// Telemetry-only ID creation must not break normal CLI/API behavior.
|
|
@@ -622,10 +700,7 @@ export async function getTelemetryStatus(surface = "cli", options = {}) {
|
|
|
622
700
|
}
|
|
623
701
|
export async function setTelemetryEnabled(enabled) {
|
|
624
702
|
const config = await loadConfig();
|
|
625
|
-
await
|
|
626
|
-
...config,
|
|
627
|
-
telemetryDisabled: enabled ? false : true,
|
|
628
|
-
});
|
|
703
|
+
await updateConfig({ telemetryDisabled: enabled ? false : true });
|
|
629
704
|
return getTelemetryStatus("cli", { createInstallId: enabled });
|
|
630
705
|
}
|
|
631
706
|
function runtimePayload() {
|
|
@@ -649,10 +724,19 @@ function errorPayload(error) {
|
|
|
649
724
|
}
|
|
650
725
|
const maybeError = error;
|
|
651
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;
|
|
652
736
|
return {
|
|
653
|
-
name
|
|
737
|
+
name,
|
|
654
738
|
status,
|
|
655
|
-
code
|
|
739
|
+
code,
|
|
656
740
|
category: status ? `http_${status}` : "error",
|
|
657
741
|
};
|
|
658
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,40 +15,74 @@ 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.
|
|
28
|
-
9. For
|
|
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.
|
|
29
54
|
|
|
30
55
|
Guidelines:
|
|
31
56
|
|
|
32
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.
|
|
33
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.
|
|
34
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.
|
|
35
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.
|
|
36
63
|
- Hosted API v1 requires the GitHub repository to be linked to an active Apex workspace. It never creates or mutates workspaces; use optional `workspaceId` to disambiguate matches and treat zero or ambiguous matches as HTTP `409` setup errors.
|
|
37
64
|
- Hosted scan triggers require `Idempotency-Key`. Reuse one stable key only for an identical transport/timeout/5xx retry. After correcting a definitive 4xx response, or for any new intended scan, use a new key.
|
|
38
|
-
- `apex-credits` reports standard credits plus audit
|
|
65
|
+
- `apex-credits` reports standard credits plus audit, fix review, and Lite scan entitlements when Bedrock returns those balances.
|
|
39
66
|
- `apex-scan` scans the provided `cwd` by default; pass `repoPaths` only when the user asks to scan explicit alternate roots.
|
|
40
67
|
- `apex-doctor` reports whether Apex will use remote materialization or a local snapshot upload for each selected source.
|
|
41
68
|
- Apex can scan plain local directories and dirty git worktrees without provider connections by using local snapshot uploads.
|
|
42
69
|
- Audit scans use `mode: "audit"` in user-facing instructions and request payloads. The legacy `ultra` mode remains accepted as an alias, but audit scans still require provider-backed GitHub or GitLab sources.
|
|
70
|
+
- Lite scans use `mode: "lite"`, require Lite Scan beta access, and require provider-backed GitHub or GitLab sources without local snapshot fallback.
|
|
43
71
|
- PR scans use `mode: "pr"` and require one provider-backed GitHub repository plus one or more `pullRequests`.
|
|
44
72
|
- `apex-workspace-use` accepts a workspace name, prefix, or ID.
|
|
45
73
|
- Use `sourceMode: "remote"` only when the user explicitly wants to forbid local snapshot fallbacks.
|
|
46
74
|
- Use `force: true` on `apex-scan` only when the user explicitly wants to replace or overlap an active scan.
|
|
47
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.
|
|
48
76
|
- Prefer `apex-findings` for quick inspection and `apex-export-findings` when the user needs a file artifact.
|
|
49
|
-
- 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.
|
|
50
|
-
- 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.
|
|
51
79
|
- Invalid finding feedback requires `dismissalReason`; valid feedback can include `suggestedSeverity`, including `extreme`.
|
|
52
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`.
|
|
53
81
|
- Current-repository fix reviews omit `fixPrUrls` and require Bedrock original scan repository context for the finding.
|
|
54
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.
|