@debugg-ai/debugg-ai-mcp 6.0.1 → 6.0.3

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.
@@ -11,7 +11,7 @@
11
11
  * and reports back what happened.
12
12
  */
13
13
  import { config } from '../config/index.js';
14
- import { toUserFacingOutcome, noVerdictReason } from '../services/verdictAdapter.js';
14
+ import { adaptVerdict } from '../services/verdictAdapter.js';
15
15
  import { Logger } from '../utils/logger.js';
16
16
  import { handleExternalServiceError } from '../utils/errors.js';
17
17
  import { DebuggAIServerClient } from '../services/index.js';
@@ -290,24 +290,24 @@ export async function triggerCrawlHandler(input, context, rawProgressCallback) {
290
290
  targetUrl: ctx.originalUrl,
291
291
  durationMs: finalExecution.durationMs ?? duration,
292
292
  };
293
- // Map onto the user-facing enum instead of relaying raw. This path never
294
- // went through adaptVerdict, so it would otherwise emit whatever the
295
- // backend sent — including retired non-answers like 'inconclusive'.
296
- const rawOutcome = finalExecution.state?.outcome;
297
- if (finalExecution.verdict?.skipped === true && !finalExecution.verdict.outcome) {
293
+ // THE run verdict (platform-4cvj.72): the backend's recorded `verdict`, through
294
+ // the same adaptVerdict path check_app_in_browser uses — never the raw
295
+ // `state.outcome` (the run's internal record), which can say pass for a run
296
+ // the verdict calls error. No verdict yet (still running) -> outcome null.
297
+ const relay = adaptVerdict(finalExecution);
298
+ responsePayload.outcome = relay.outcome;
299
+ if (relay.skipped) {
298
300
  // platform-98fv.16: the run never attempted a test — no verdict, the
299
- // backend's recorded reason verbatim. Never the raw run outcome.
300
- responsePayload.outcome = null;
301
+ // backend's recorded reason verbatim.
301
302
  responsePayload.skipped = true;
302
- const skipReason = finalExecution.verdict.reason;
303
- if (typeof skipReason === 'string' && skipReason.trim() !== '')
304
- responsePayload.skipReason = skipReason;
303
+ if (relay.skipReason)
304
+ responsePayload.skipReason = relay.skipReason;
305
305
  }
306
- else if (rawOutcome !== undefined && rawOutcome !== null) {
307
- responsePayload.outcome = toUserFacingOutcome(rawOutcome);
308
- if (responsePayload.outcome === 'error' && !finalExecution.errorMessage) {
309
- responsePayload.reason = noVerdictReason(rawOutcome);
310
- }
306
+ else if (relay.outcome === 'error' && relay.reason) {
307
+ const backendReason = typeof finalExecution.verdict?.reason === 'string'
308
+ && finalExecution.verdict.reason.trim() !== '';
309
+ if (backendReason || !finalExecution.errorMessage)
310
+ responsePayload.reason = relay.reason;
311
311
  }
312
312
  if (finalExecution.errorMessage)
313
313
  responsePayload.errorMessage = finalExecution.errorMessage;
@@ -41,6 +41,22 @@ function pickAuthorizedCredentialHosts(e) {
41
41
  ? { authorizedCredentialHosts: e.authorizedCredentialHosts }
42
42
  : {};
43
43
  }
44
+ /**
45
+ * The outcome a suite's run SHOWS (platform-4cvj.72): the run's recorded verdict
46
+ * when the backend sent one, else its raw `outcome`, both through the user-facing
47
+ * allowlist. A run with neither keeps its null.
48
+ */
49
+ export function suiteRunOutcome(run) {
50
+ const verdict = run?.verdict;
51
+ const fromVerdict = verdict && typeof verdict === 'object' ? verdict.outcome : undefined;
52
+ if (typeof fromVerdict === 'string' && fromVerdict.trim() !== '') {
53
+ return toUserFacingOutcome(fromVerdict);
54
+ }
55
+ const raw = run?.outcome;
56
+ if (typeof raw === 'string' && raw.trim() !== '')
57
+ return toUserFacingOutcome(raw);
58
+ return raw ?? null;
59
+ }
44
60
  export class DebuggAIServerClient {
45
61
  userApiKey;
46
62
  tx;
@@ -604,11 +620,11 @@ export class DebuggAIServerClient {
604
620
  lastRun: lastRun ? {
605
621
  uuid: lastRun.uuid,
606
622
  status: lastRun.status,
607
- // the user-facing verdict allowlist, same as every other surface;
608
- // a run with no outcome yet keeps its null
609
- outcome: typeof lastRun.outcome === 'string' && lastRun.outcome.trim() !== ''
610
- ? toUserFacingOutcome(lastRun.outcome)
611
- : lastRun.outcome ?? null,
623
+ // platform-4cvj.72: THE run verdict (`verdict.outcome`, what the run
624
+ // page shows) wins; the raw recorded `outcome` is only read for a run
625
+ // the backend sent no verdict for. Same user-facing allowlist as every
626
+ // other surface; a run with no outcome yet keeps its null.
627
+ outcome: suiteRunOutcome(lastRun),
612
628
  executionTime: lastRun.executionTime ?? lastRun.execution_time ?? null,
613
629
  timestamp: lastRun.timestamp,
614
630
  } : null,
@@ -3,6 +3,7 @@
3
3
  * 4-step integration: find template → execute → poll → result
4
4
  */
5
5
  import { Telemetry, TelemetryEvents } from '../utils/telemetry.js';
6
+ import { redactRunSecrets } from '../utils/redactRunSecrets.js';
6
7
  const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled']);
7
8
  // Exponential backoff polling: short executions (10-15s crawls) detect terminal
8
9
  // status quickly via the early polls; long executions (60-150s browser runs)
@@ -75,7 +76,9 @@ export const createWorkflowsService = (tx) => {
75
76
  if (!response) {
76
77
  throw new Error(`Execution not found: ${executionUuid}`);
77
78
  }
78
- return response;
79
+ // platform-98fv.25: every caller (executions tool, check_app polling) reads
80
+ // the execution through here, so this is where the relay drops run secrets.
81
+ return redactRunSecrets(response);
79
82
  },
80
83
  async listExecutions(filters) {
81
84
  const { makePageInfo } = await import('../utils/pagination.js');
@@ -0,0 +1,40 @@
1
+ /**
2
+ * platform-98fv.25: the relay never passes a run secret on.
3
+ *
4
+ * The backend used to persist the password a check_app_in_browser run was given
5
+ * and serve it back in `contextData.env.password`, `state.env.password` and every
6
+ * node payload. The backend now seals it at rest and redacts it on read; this is
7
+ * the relay's own copy of that rule, so an older or misbehaving backend still
8
+ * cannot leak through the MCP.
9
+ *
10
+ * Deliberately narrow and exact (mirrors sentinal `run_secrets.redact_run_secrets`):
11
+ * - any non-empty string under a key named exactly `password` / `passwd`;
12
+ * - every string value of a `projectSecrets` / `project_secrets` mapping;
13
+ * - any string carrying the backend's sealed-value prefix.
14
+ * Identifiers such as `browserSessionId` / `resolvedCredentialId` are untouched.
15
+ */
16
+ export const REDACTED = '[REDACTED]';
17
+ const SEALED_PREFIX = 'sealed:v1:';
18
+ const SECRET_KEYS = new Set(['password', 'passwd']);
19
+ const SECRET_MAPPING_KEYS = new Set(['projectSecrets', 'project_secrets']);
20
+ function isSealed(v) {
21
+ return typeof v === 'string' && v.startsWith(SEALED_PREFIX);
22
+ }
23
+ export function redactRunSecrets(value, inMapping = false) {
24
+ if (Array.isArray(value)) {
25
+ return value.map((v) => (isSealed(v) ? REDACTED : redactRunSecrets(v)));
26
+ }
27
+ if (value && typeof value === 'object') {
28
+ const out = {};
29
+ for (const [k, v] of Object.entries(value)) {
30
+ if (typeof v === 'string' && v && (inMapping || SECRET_KEYS.has(k) || isSealed(v))) {
31
+ out[k] = REDACTED;
32
+ }
33
+ else {
34
+ out[k] = redactRunSecrets(v, SECRET_MAPPING_KEYS.has(k));
35
+ }
36
+ }
37
+ return out;
38
+ }
39
+ return value;
40
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@debugg-ai/debugg-ai-mcp",
3
- "version": "6.0.1",
3
+ "version": "6.0.3",
4
4
  "description": "Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.",
5
5
  "type": "module",
6
6
  "caddy": "2.11.3",