@jevitate/cli 0.12.0 → 0.14.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/dist/author-journey-cli.d.ts +9 -0
- package/dist/author-journey-cli.js +18 -1
- package/dist/author-journey-cli.js.map +1 -1
- package/dist/bin.js +25673 -24216
- package/dist/build-info.generated.js +2 -2
- package/dist/catalog-analysis.js +4 -2
- package/dist/catalog-analysis.js.map +1 -1
- package/dist/catalog-bundle-api.js +1 -3
- package/dist/catalog-bundle-api.js.map +1 -1
- package/dist/catalog-bundle-findings.js +1 -3
- package/dist/catalog-bundle-findings.js.map +1 -1
- package/dist/catalog-bundle.js +1 -4
- package/dist/catalog-bundle.js.map +1 -1
- package/dist/catalog-cli.js +48 -6
- package/dist/catalog-cli.js.map +1 -1
- package/dist/catalog-review.d.ts +8 -0
- package/dist/catalog-review.js +26 -0
- package/dist/catalog-review.js.map +1 -1
- package/dist/catalog.d.ts +42 -0
- package/dist/catalog.js +67 -4
- package/dist/catalog.js.map +1 -1
- package/dist/check-execute.js +9 -0
- package/dist/check-execute.js.map +1 -1
- package/dist/check-plan.d.ts +5 -2
- package/dist/check-plan.js +9 -5
- package/dist/check-plan.js.map +1 -1
- package/dist/check-suite.js +1 -3
- package/dist/check-suite.js.map +1 -1
- package/dist/cli-output.js +1 -3
- package/dist/cli-output.js.map +1 -1
- package/dist/cli-shared.d.ts +1 -1
- package/dist/cli-shared.js +2 -0
- package/dist/cli-shared.js.map +1 -1
- package/dist/defect-evidence.js +4 -8
- package/dist/defect-evidence.js.map +1 -1
- package/dist/environments.d.ts +1 -1
- package/dist/environments.js +1 -1
- package/dist/environments.js.map +1 -1
- package/dist/exit-codes.d.ts +0 -1
- package/dist/exit-codes.js +2 -0
- package/dist/exit-codes.js.map +1 -1
- package/dist/explore-adversarial.d.ts +10 -1
- package/dist/explore-adversarial.js +3 -0
- package/dist/explore-adversarial.js.map +1 -1
- package/dist/explore-cli/adversarial-strategy.d.ts +3 -0
- package/dist/explore-cli/adversarial-strategy.js +101 -0
- package/dist/explore-cli/adversarial-strategy.js.map +1 -0
- package/dist/explore-cli/coverage-strategy.d.ts +3 -0
- package/dist/explore-cli/coverage-strategy.js +97 -0
- package/dist/explore-cli/coverage-strategy.js.map +1 -0
- package/dist/explore-cli/feature-strategy.d.ts +3 -0
- package/dist/explore-cli/feature-strategy.js +60 -0
- package/dist/explore-cli/feature-strategy.js.map +1 -0
- package/dist/explore-cli/goal-strategy.d.ts +3 -0
- package/dist/explore-cli/goal-strategy.js +185 -0
- package/dist/explore-cli/goal-strategy.js.map +1 -0
- package/dist/explore-cli/options.d.ts +3 -0
- package/dist/explore-cli/options.js +157 -0
- package/dist/explore-cli/options.js.map +1 -0
- package/dist/explore-cli/run.d.ts +7 -0
- package/dist/explore-cli/run.js +702 -0
- package/dist/explore-cli/run.js.map +1 -0
- package/dist/explore-cli/scope-region.d.ts +17 -0
- package/dist/explore-cli/scope-region.js +24 -0
- package/dist/explore-cli/scope-region.js.map +1 -0
- package/dist/explore-cli/shared.d.ts +167 -0
- package/dist/explore-cli/shared.js +20 -0
- package/dist/explore-cli/shared.js.map +1 -0
- package/dist/explore-cli/usability-strategy.d.ts +3 -0
- package/dist/explore-cli/usability-strategy.js +137 -0
- package/dist/explore-cli/usability-strategy.js.map +1 -0
- package/dist/explore-cli.d.ts +2 -2
- package/dist/explore-cli.js +2 -1347
- package/dist/explore-cli.js.map +1 -1
- package/dist/explore-coverage.d.ts +10 -1
- package/dist/explore-coverage.js +3 -0
- package/dist/explore-coverage.js.map +1 -1
- package/dist/explore-feature.d.ts +6 -0
- package/dist/explore-feature.js +1 -0
- package/dist/explore-feature.js.map +1 -1
- package/dist/explore-goal.d.ts +6 -0
- package/dist/explore-goal.js +1 -0
- package/dist/explore-goal.js.map +1 -1
- package/dist/explore-specs.d.ts +3 -1
- package/dist/explore-specs.js +2 -1
- package/dist/explore-specs.js.map +1 -1
- package/dist/github-issue-filer.js +1 -4
- package/dist/github-issue-filer.js.map +1 -1
- package/dist/goal-run-flags.d.ts +0 -1
- package/dist/goal-run-flags.js.map +1 -1
- package/dist/inbox-cli.js +1 -3
- package/dist/inbox-cli.js.map +1 -1
- package/dist/init-skills.js +1 -4
- package/dist/init-skills.js.map +1 -1
- package/dist/journey-anchor-api.d.ts +10 -9
- package/dist/journey-anchor-api.js +12 -22
- package/dist/journey-anchor-api.js.map +1 -1
- package/dist/journey-anchor-cli.d.ts +1 -1
- package/dist/journey-anchor-cli.js +22 -8
- package/dist/journey-anchor-cli.js.map +1 -1
- package/dist/journey-cli/anchors.d.ts +4 -0
- package/dist/journey-cli/anchors.js +48 -0
- package/dist/journey-cli/anchors.js.map +1 -0
- package/dist/journey-cli/find.d.ts +4 -0
- package/dist/journey-cli/find.js +44 -0
- package/dist/journey-cli/find.js.map +1 -0
- package/dist/journey-cli/lint.d.ts +4 -0
- package/dist/journey-cli/lint.js +84 -0
- package/dist/journey-cli/lint.js.map +1 -0
- package/dist/journey-cli/list.d.ts +4 -0
- package/dist/journey-cli/list.js +48 -0
- package/dist/journey-cli/list.js.map +1 -0
- package/dist/journey-cli/migrate.d.ts +4 -0
- package/dist/journey-cli/migrate.js +101 -0
- package/dist/journey-cli/migrate.js.map +1 -0
- package/dist/journey-cli/promote.d.ts +4 -0
- package/dist/journey-cli/promote.js +181 -0
- package/dist/journey-cli/promote.js.map +1 -0
- package/dist/journey-cli/publish.d.ts +4 -0
- package/dist/journey-cli/publish.js +70 -0
- package/dist/journey-cli/publish.js.map +1 -0
- package/dist/journey-cli/review.d.ts +4 -0
- package/dist/journey-cli/review.js +108 -0
- package/dist/journey-cli/review.js.map +1 -0
- package/dist/journey-cli/run.d.ts +4 -0
- package/dist/journey-cli/run.js +244 -0
- package/dist/journey-cli/run.js.map +1 -0
- package/dist/journey-cli/shared.d.ts +9 -0
- package/dist/journey-cli/shared.js +12 -0
- package/dist/journey-cli/shared.js.map +1 -0
- package/dist/journey-cli/verify.d.ts +4 -0
- package/dist/journey-cli/verify.js +125 -0
- package/dist/journey-cli/verify.js.map +1 -0
- package/dist/journey-cli.d.ts +3 -2
- package/dist/journey-cli.js +25 -950
- package/dist/journey-cli.js.map +1 -1
- package/dist/journey-link-api.d.ts +40 -0
- package/dist/journey-link-api.js +90 -0
- package/dist/journey-link-api.js.map +1 -0
- package/dist/journey-link-cli.d.ts +7 -0
- package/dist/journey-link-cli.js +58 -0
- package/dist/journey-link-cli.js.map +1 -0
- package/dist/journey-link-hints.d.ts +8 -0
- package/dist/journey-link-hints.js +68 -0
- package/dist/journey-link-hints.js.map +1 -0
- package/dist/journey-lint-sarif.js +3 -0
- package/dist/journey-lint-sarif.js.map +1 -1
- package/dist/journey-network-checks.d.ts +9 -1
- package/dist/journey-network-checks.js +55 -12
- package/dist/journey-network-checks.js.map +1 -1
- package/dist/journey-prefix.d.ts +9 -1
- package/dist/journey-prefix.js +12 -0
- package/dist/journey-prefix.js.map +1 -1
- package/dist/journey-review/render.d.ts +43 -0
- package/dist/journey-review/render.js +461 -0
- package/dist/journey-review/render.js.map +1 -0
- package/dist/journey-review-api.d.ts +1 -0
- package/dist/journey-review-api.js +7 -0
- package/dist/journey-review-api.js.map +1 -1
- package/dist/journey-review-store.js +1 -0
- package/dist/journey-review-store.js.map +1 -1
- package/dist/journey-review.d.ts +6 -4
- package/dist/journey-review.js +18 -393
- package/dist/journey-review.js.map +1 -1
- package/dist/journey-verify.js +2 -1
- package/dist/journey-verify.js.map +1 -1
- package/dist/ledger-api.js +1 -4
- package/dist/ledger-api.js.map +1 -1
- package/dist/locator-health-api.js +1 -3
- package/dist/locator-health-api.js.map +1 -1
- package/dist/locator-health.d.ts +0 -1
- package/dist/locator-health.js +0 -3
- package/dist/locator-health.js.map +1 -1
- package/dist/log-classes.js +1 -3
- package/dist/log-classes.js.map +1 -1
- package/dist/log-sources.js +17 -1
- package/dist/log-sources.js.map +1 -1
- package/dist/mcp-api.js +16 -821
- package/dist/mcp-api.js.map +1 -1
- package/dist/mcp-cli-tools.js +48 -7
- package/dist/mcp-cli-tools.js.map +1 -1
- package/dist/mcp-tools/cli.d.ts +3 -0
- package/dist/mcp-tools/cli.js +20 -0
- package/dist/mcp-tools/cli.js.map +1 -0
- package/dist/mcp-tools/explore.d.ts +3 -0
- package/dist/mcp-tools/explore.js +141 -0
- package/dist/mcp-tools/explore.js.map +1 -0
- package/dist/mcp-tools/findings.d.ts +3 -0
- package/dist/mcp-tools/findings.js +277 -0
- package/dist/mcp-tools/findings.js.map +1 -0
- package/dist/mcp-tools/inbox.d.ts +3 -0
- package/dist/mcp-tools/inbox.js +106 -0
- package/dist/mcp-tools/inbox.js.map +1 -0
- package/dist/mcp-tools/journey.d.ts +3 -0
- package/dist/mcp-tools/journey.js +260 -0
- package/dist/mcp-tools/journey.js.map +1 -0
- package/dist/mcp-tools/shared.d.ts +39 -0
- package/dist/mcp-tools/shared.js +69 -0
- package/dist/mcp-tools/shared.js.map +1 -0
- package/dist/mission-fixtures.js +1 -8
- package/dist/mission-fixtures.js.map +1 -1
- package/dist/mission-queue-cli.js +1 -3
- package/dist/mission-queue-cli.js.map +1 -1
- package/dist/multi-run.js +1 -3
- package/dist/multi-run.js.map +1 -1
- package/dist/pre-approval.js +4 -4
- package/dist/pre-approval.js.map +1 -1
- package/dist/readiness.js +2 -2
- package/dist/readiness.js.map +1 -1
- package/dist/report-api.js +1 -4
- package/dist/report-api.js.map +1 -1
- package/dist/resource-preflight.js.map +1 -1
- package/dist/run-metadata.js +1 -3
- package/dist/run-metadata.js.map +1 -1
- package/dist/run-tags-cli.d.ts +0 -2
- package/dist/run-tags-cli.js +0 -4
- package/dist/run-tags-cli.js.map +1 -1
- package/dist/session-check.js +1 -3
- package/dist/session-check.js.map +1 -1
- package/dist/signal-triage.js +6 -2
- package/dist/signal-triage.js.map +1 -1
- package/dist/suite-explore-options.d.ts +8 -0
- package/dist/suite-explore-options.js +4 -0
- package/dist/suite-explore-options.js.map +1 -1
- package/dist/sweep-api.d.ts +9 -0
- package/dist/sweep-api.js +70 -4
- package/dist/sweep-api.js.map +1 -1
- package/dist/sweep-cli.d.ts +2 -0
- package/dist/sweep-cli.js +22 -5
- package/dist/sweep-cli.js.map +1 -1
- package/dist/transcript-file.d.ts +0 -3
- package/dist/transcript-file.js +0 -9
- package/dist/transcript-file.js.map +1 -1
- package/dist/ui-api.js +1 -4
- package/dist/ui-api.js.map +1 -1
- package/dist/ux-usability/analysis.d.ts +14 -0
- package/dist/ux-usability/analysis.js +106 -0
- package/dist/ux-usability/analysis.js.map +1 -0
- package/dist/ux-usability/done-adjudication.d.ts +15 -0
- package/dist/ux-usability/done-adjudication.js +56 -0
- package/dist/ux-usability/done-adjudication.js.map +1 -0
- package/dist/ux-usability/hang.d.ts +5 -0
- package/dist/ux-usability/hang.js +41 -0
- package/dist/ux-usability/hang.js.map +1 -0
- package/dist/ux-usability/loop.d.ts +15 -0
- package/dist/ux-usability/loop.js +104 -0
- package/dist/ux-usability/loop.js.map +1 -0
- package/dist/ux-usability/observe.d.ts +7 -0
- package/dist/ux-usability/observe.js +89 -0
- package/dist/ux-usability/observe.js.map +1 -0
- package/dist/ux-usability/result.d.ts +11 -0
- package/dist/ux-usability/result.js +113 -0
- package/dist/ux-usability/result.js.map +1 -0
- package/dist/ux-usability/setup.d.ts +76 -0
- package/dist/ux-usability/setup.js +204 -0
- package/dist/ux-usability/setup.js.map +1 -0
- package/dist/ux-usability/teardown.d.ts +3 -0
- package/dist/ux-usability/teardown.js +13 -0
- package/dist/ux-usability/teardown.js.map +1 -0
- package/dist/ux-usability.d.ts +20 -17
- package/dist/ux-usability.js +15 -589
- package/dist/ux-usability.js.map +1 -1
- package/dist/verify-fix-api.js +1 -4
- package/dist/verify-fix-api.js.map +1 -1
- package/package.json +5 -6
- package/skills/jevitate-explore/SKILL.md +9 -1
- package/skills/jevitate-getting-started/SKILL.md +1 -0
- package/skills/jevitate-promotion-ready/SKILL.md +76 -0
- package/skills/jevitate-record/SKILL.md +3 -0
- package/skills/jevitate-run-journey/SKILL.md +3 -0
- package/skills/jevitate-test-campaign/SKILL.md +5 -3
package/dist/mcp-api.js
CHANGED
|
@@ -1,43 +1,21 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
-
import { SiteGateRefusedError } from "@jevitate/runtime";
|
|
3
2
|
import { withSiteGate } from "./site-gate-cli.js";
|
|
4
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
4
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
-
import { ALLOWED_TOOLS, FORBIDDEN_TOOLS
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { ALL_CREDENTIAL_KEYS, envCredentialStore, } from "@jevitate/ai-core";
|
|
11
|
-
import { existsSync } from "node:fs";
|
|
12
|
-
import { access, readFile } from "node:fs/promises";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
import { ParamValidationError } from "@jevitate/journey";
|
|
15
|
-
import { RunTagError, JOURNEY_RUN_OUTCOMES, journeyExitCode, safeRunPolicy as defaultRunPolicy, validateRunTags } from "@jevitate/domain";
|
|
16
|
-
import { stampRunMetadata, withRunMetadata } from "./run-metadata.js";
|
|
17
|
-
import { JourneyRequiresAuthError, UnknownJourneyError, runJourneyProgrammatically } from "./journey-api.js";
|
|
5
|
+
import { ALLOWED_TOOLS, FORBIDDEN_TOOLS } from "@jevitate/mcp-facade";
|
|
6
|
+
import { envCredentialStore } from "@jevitate/ai-core";
|
|
7
|
+
import { safeRunPolicy as defaultRunPolicy } from "@jevitate/domain";
|
|
8
|
+
import { runJourneyProgrammatically } from "./journey-api.js";
|
|
18
9
|
import { runVerifyFix } from "./verify-fix-api.js";
|
|
19
|
-
import { ExtensionMismatchError } from "./browser-run-options.js";
|
|
20
|
-
import { browserRunFromFlags, emulationFromFlags } from "./cli-shared.js";
|
|
21
|
-
import { environmentFromFlags, isEnvironmentError } from "./environments.js";
|
|
22
|
-
import { buildMissionFixtures, checkSetupRefs } from "./fixture-cli.js";
|
|
23
|
-
import { FixtureSetupError, FixtureSpecError, UnboundSetupRefError } from "./mission-fixtures.js";
|
|
24
|
-
import { parseScreenshotsArg } from "./run-screenshots.js";
|
|
25
|
-
import { makeEvidenceSelfHealer } from "./self-heal-adapter.js";
|
|
26
|
-
import { JourneyHealArgsError, MCP_HEAL_NAMES, journeyHealBudget, readJourneyChangeScope, validateJourneyHeal } from "./journey-heal.js";
|
|
27
|
-
import { ChangesArgsError, ChangesInputError } from "./change-context.js";
|
|
28
|
-
import { MAX_STEP_RESPONSE_TIMEOUT_MS } from "./journey-network-checks.js";
|
|
29
|
-
import { McpArgError, argErrorBody, optBool, optEnum, optInt, optNamedSessions, optPath, optExtensions, optRecordVideo, optScreenshots, optString, optStringArray, optStringMap, optViewport } from "./mcp-args.js";
|
|
30
10
|
import { defaultMcpPathRoots } from "./mcp-paths.js";
|
|
31
|
-
import { CLI_TOOL_SPECS, cliToolInputSchema, runCliTool } from "./mcp-cli-tools.js";
|
|
32
11
|
import { currentEngineInfo } from "./engine.js";
|
|
33
12
|
import { loadTargetsFile } from "./target-config.js";
|
|
34
|
-
import {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
13
|
+
import { errorResult } from "./mcp-tools/shared.js";
|
|
14
|
+
import { findingTools } from "./mcp-tools/findings.js";
|
|
15
|
+
import { journeyTools } from "./mcp-tools/journey.js";
|
|
16
|
+
import { exploreTools } from "./mcp-tools/explore.js";
|
|
17
|
+
import { inboxTools } from "./mcp-tools/inbox.js";
|
|
18
|
+
import { cliTools } from "./mcp-tools/cli.js";
|
|
41
19
|
const ALLOWED = new Set(ALLOWED_TOOLS);
|
|
42
20
|
const FORBIDDEN = new Set(FORBIDDEN_TOOLS);
|
|
43
21
|
/**
|
|
@@ -53,52 +31,6 @@ function assertAllowlisted(name) {
|
|
|
53
31
|
throw new Error(`refusing to serve non-allowlisted tool '${name}'`);
|
|
54
32
|
}
|
|
55
33
|
}
|
|
56
|
-
/** #453: a Journey run result with the CLI's exit code for its outcome (anything else unchanged). */
|
|
57
|
-
function withJourneyExitCode(result) {
|
|
58
|
-
if (result === null || typeof result !== "object")
|
|
59
|
-
return result;
|
|
60
|
-
const outcome = result.outcome;
|
|
61
|
-
if (typeof outcome !== "string" || !JOURNEY_RUN_OUTCOMES.includes(outcome))
|
|
62
|
-
return result;
|
|
63
|
-
return { ...result, exitCode: journeyExitCode(outcome) };
|
|
64
|
-
}
|
|
65
|
-
function jsonResult(value) {
|
|
66
|
-
return { content: [{ type: "text", text: JSON.stringify(value) }] };
|
|
67
|
-
}
|
|
68
|
-
function errorResult(value) {
|
|
69
|
-
return { content: [{ type: "text", text: JSON.stringify(value) }], isError: true };
|
|
70
|
-
}
|
|
71
|
-
/** Strips any known credential VALUE from a diagnostic string so a provider
|
|
72
|
-
* SDK error (which can echo request internals verbatim) never surfaces a key
|
|
73
|
-
* through the MCP result. The message returned to the caller is generic and
|
|
74
|
-
* key-free; this is belt-and-braces on top of the gateway's outbound guard. */
|
|
75
|
-
/** #217: a persisted goal result's own ending (`result.goalOutcome`), when present. */
|
|
76
|
-
function goalOutcomeOf(file) {
|
|
77
|
-
const result = file.result;
|
|
78
|
-
return result !== null && typeof result === "object" && "goalOutcome" in result ? result.goalOutcome : undefined;
|
|
79
|
-
}
|
|
80
|
-
/** #423: a persisted result's `defectOutcome.status` (`none`/`defects`), when present. */
|
|
81
|
-
function defectStatusOf(file) {
|
|
82
|
-
const outcome = resultField(file, "defectOutcome");
|
|
83
|
-
return outcome !== null && typeof outcome === "object" && "status" in outcome ? outcome.status : undefined;
|
|
84
|
-
}
|
|
85
|
-
/** One field of a persisted result's `result` object, else undefined. */
|
|
86
|
-
function resultField(file, key) {
|
|
87
|
-
const result = file.result;
|
|
88
|
-
return result !== null && typeof result === "object" && key in result ? result[key] : undefined;
|
|
89
|
-
}
|
|
90
|
-
/** A model-backed option was asked for without usable gateways/keys (typed `setup_required`). */
|
|
91
|
-
class SetupRequired extends Error {
|
|
92
|
-
}
|
|
93
|
-
function redactCredentials(message, store) {
|
|
94
|
-
let out = message;
|
|
95
|
-
for (const key of ALL_CREDENTIAL_KEYS) {
|
|
96
|
-
const value = store.read(key);
|
|
97
|
-
if (value)
|
|
98
|
-
out = out.split(value).join("***REDACTED***");
|
|
99
|
-
}
|
|
100
|
-
return out;
|
|
101
|
-
}
|
|
102
34
|
/**
|
|
103
35
|
* Builds the served tool descriptors — EXACTLY one per `ALLOWED_TOOLS` entry.
|
|
104
36
|
* Wired to their real backing services: `find_capabilities` + `run_journey`
|
|
@@ -111,8 +43,6 @@ function redactCredentials(message, store) {
|
|
|
111
43
|
* No handler ever fabricates a success.
|
|
112
44
|
*/
|
|
113
45
|
export function buildMcpTools(deps) {
|
|
114
|
-
const findCaps = deps.findCapabilities ??
|
|
115
|
-
((query) => findCapabilities(new JourneyRegistry(new FsJourneyStore(deps.journeysDir)), query));
|
|
116
46
|
const runJourney = deps.runJourney ??
|
|
117
47
|
((id, params, storageState, o) => withSiteGate(deps.sitePolicyDbPath, (siteGate) => runJourneyProgrammatically({
|
|
118
48
|
dir: deps.journeysDir,
|
|
@@ -132,189 +62,7 @@ export function buildMcpTools(deps) {
|
|
|
132
62
|
...(o?.stepResponseTimeoutMs === undefined ? {} : { stepResponseTimeoutMs: o.stepResponseTimeoutMs }),
|
|
133
63
|
})));
|
|
134
64
|
const pathRoots = deps.pathRoots ?? defaultMcpPathRoots();
|
|
135
|
-
// queue_exploration: enqueue over the SAME promoted fs store `jevitate
|
|
136
|
-
// mission target` writes. The store is built lazily inside the closure so
|
|
137
|
-
// constructing the tool set never touches disk; a missing dir is a config
|
|
138
|
-
// error (a refusal), never a silent success.
|
|
139
|
-
const queueExploration = deps.queueExploration ??
|
|
140
|
-
(async (args) => {
|
|
141
|
-
if (!deps.missionTargetsDir || !deps.missionQueueDir) {
|
|
142
|
-
throw new Error("queue_exploration requires missionTargetsDir and missionQueueDir to be configured");
|
|
143
|
-
}
|
|
144
|
-
const targets = new MissionTargetRegistry(new FsMissionTargetStore(deps.missionTargetsDir));
|
|
145
|
-
const queue = new FsMissionQueueStore(deps.missionQueueDir);
|
|
146
|
-
return facadeQueueExploration(targets, queue, args);
|
|
147
|
-
});
|
|
148
|
-
// ai_generate_text: preflight-gated generation. Wired only when a gateway (or
|
|
149
|
-
// an explicit seam) is available — otherwise it stays not_implemented rather
|
|
150
|
-
// than fabricate an answer. The credential store is read ONLY inside the
|
|
151
|
-
// gateway/preflight; never here.
|
|
152
65
|
const credentialStore = deps.credentialStore ?? envCredentialStore();
|
|
153
|
-
const aiGenerateText = deps.aiGenerateText ??
|
|
154
|
-
(deps.generationGateway
|
|
155
|
-
? (args) => facadeAiGenerateText(credentialStore, deps.generationGateway, args)
|
|
156
|
-
: undefined);
|
|
157
|
-
// The 8 inbox tools: the store is built lazily inside this closure (mirrors
|
|
158
|
-
// `queueExploration` above) so constructing the tool set never touches
|
|
159
|
-
// disk. A missing `inboxDir` is a config error (a refusal), never a silent
|
|
160
|
-
// success — `inboxHandler` below catches it (and any store-layer throw,
|
|
161
|
-
// e.g. `getForAgent`/`get`'s fail-closed throw on corrupt/tampered data)
|
|
162
|
-
// and converts it to a typed `internal` error, never lets it fabricate a
|
|
163
|
-
// success or leak an unhandled rejection out of the handler.
|
|
164
|
-
const inboxStore = () => {
|
|
165
|
-
if (!deps.inboxDir) {
|
|
166
|
-
throw new Error("inbox tools require inboxDir to be configured");
|
|
167
|
-
}
|
|
168
|
-
return new FsInboxStore(deps.inboxDir, deps.engine ?? currentEngineInfo());
|
|
169
|
-
};
|
|
170
|
-
function inboxHandler(fn) {
|
|
171
|
-
return async (args) => {
|
|
172
|
-
try {
|
|
173
|
-
return jsonResult(await fn(inboxStore(), args));
|
|
174
|
-
}
|
|
175
|
-
catch (err) {
|
|
176
|
-
return errorResult({ error: "internal", message: err instanceof Error ? err.message : String(err) });
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
const idInputSchema = {
|
|
181
|
-
type: "object",
|
|
182
|
-
properties: { id: { type: "string" } },
|
|
183
|
-
required: ["id"],
|
|
184
|
-
};
|
|
185
|
-
const queueCommonProperties = {
|
|
186
|
-
run: { type: "string" },
|
|
187
|
-
journey: { type: "string" },
|
|
188
|
-
step: { type: "string" },
|
|
189
|
-
reason: { type: "string" },
|
|
190
|
-
agent: { type: "string" },
|
|
191
|
-
targetUrl: { type: "string" },
|
|
192
|
-
hasScreenshot: { type: "boolean" },
|
|
193
|
-
findings: {
|
|
194
|
-
type: "array",
|
|
195
|
-
items: {
|
|
196
|
-
type: "object",
|
|
197
|
-
properties: {
|
|
198
|
-
id: { type: "string" },
|
|
199
|
-
title: { type: "string" },
|
|
200
|
-
severity: { type: "string", enum: ["low", "med", "high"] },
|
|
201
|
-
evidence: { type: "string" },
|
|
202
|
-
},
|
|
203
|
-
required: ["id", "title", "severity"],
|
|
204
|
-
},
|
|
205
|
-
},
|
|
206
|
-
};
|
|
207
|
-
const queueCommonRequired = ["run", "journey", "step", "reason", "agent"];
|
|
208
|
-
// get_mission_result: reads a persisted typed mission result by ID only (the artifact stem the
|
|
209
|
-
// CLI wrote) — never a caller-supplied path. The status carries the CLI exit code, and a run that
|
|
210
|
-
// itself broke (inconclusive/crashed) is an MCP error result, so it can never read as a pass.
|
|
211
|
-
//
|
|
212
|
-
// The id is either a result stem (`explore-<stamp>`, `usability-<stamp>.recording`, …) or a
|
|
213
|
-
// `queue_exploration` missionId (#117): the latter resolves through the mission's queue record —
|
|
214
|
-
// `queued`/`running` is reported as such (not an error, not a result), `failed` as an error, and
|
|
215
|
-
// `done` reads the result its drain wrote. Both shapes are closed regexes; nothing caller-shaped
|
|
216
|
-
// ever reaches a path.
|
|
217
|
-
const resolveResultId = async (id, tool) => {
|
|
218
|
-
if (isMissionResultId(id))
|
|
219
|
-
return { resultId: id };
|
|
220
|
-
if (!isQueuedMissionId(id)) {
|
|
221
|
-
return {
|
|
222
|
-
response: errorResult({
|
|
223
|
-
error: "invalid_args",
|
|
224
|
-
message: `${tool} requires a mission result 'id' (explore-|coverage-|exploratory-|adversarial-|feature-|usability-<stamp>) or a queue_exploration missionId`,
|
|
225
|
-
}),
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
if (!deps.missionQueueDir) {
|
|
229
|
-
return { response: errorResult({ error: "not_configured", message: `${tool} requires missionQueueDir to resolve a missionId` }) };
|
|
230
|
-
}
|
|
231
|
-
let mission;
|
|
232
|
-
try {
|
|
233
|
-
mission = await new FsMissionQueueStore(deps.missionQueueDir).get(id);
|
|
234
|
-
}
|
|
235
|
-
catch {
|
|
236
|
-
return { response: errorResult({ error: "corrupt_mission", id }) };
|
|
237
|
-
}
|
|
238
|
-
if (mission === null)
|
|
239
|
-
return { response: errorResult({ error: "not_found", id }) };
|
|
240
|
-
if (mission.status === "queued" || mission.status === "running") {
|
|
241
|
-
// Not finished: neither a pass nor a failure — the agent polls again later.
|
|
242
|
-
const pending = {
|
|
243
|
-
id,
|
|
244
|
-
missionId: id,
|
|
245
|
-
status: mission.status,
|
|
246
|
-
pending: true,
|
|
247
|
-
enqueuedAtIso: mission.enqueuedAtIso,
|
|
248
|
-
...(mission.startedAtIso === undefined ? {} : { startedAtIso: mission.startedAtIso }),
|
|
249
|
-
};
|
|
250
|
-
return { response: jsonResult(pending), pending };
|
|
251
|
-
}
|
|
252
|
-
if (mission.status === "failed") {
|
|
253
|
-
return { response: errorResult({ id, missionId: id, status: "failed", isError: true, error: mission.error ?? "mission failed" }) };
|
|
254
|
-
}
|
|
255
|
-
if (!isMissionResultId(mission.resultId))
|
|
256
|
-
return { response: errorResult({ error: "corrupt_mission", id }) };
|
|
257
|
-
return { resultId: mission.resultId, missionId: id };
|
|
258
|
-
};
|
|
259
|
-
/** The dirs a result may live in: recordings, and (usability reviews) the UX reports dir. */
|
|
260
|
-
const resultPathsFor = (resultId) => {
|
|
261
|
-
const file = missionResultFileName(resultId);
|
|
262
|
-
const dirs = deps.resultDirsFor?.(resultId) ?? [deps.recordingsDir, ...(resultId.startsWith("usability-") ? [deps.uxReportsDir] : [])];
|
|
263
|
-
return dirs.filter((d) => d !== undefined).map((d) => join(d, file));
|
|
264
|
-
};
|
|
265
|
-
const getMissionResult = async (args) => {
|
|
266
|
-
if (!deps.recordingsDir) {
|
|
267
|
-
return errorResult({ error: "not_configured", message: "get_mission_result requires recordingsDir" });
|
|
268
|
-
}
|
|
269
|
-
const ref = await resolveResultId(args.id, "get_mission_result");
|
|
270
|
-
if ("response" in ref)
|
|
271
|
-
return ref.response;
|
|
272
|
-
const ids = { id: args.id, ...(ref.missionId === undefined ? {} : { missionId: ref.missionId, resultId: ref.resultId }) };
|
|
273
|
-
let raw;
|
|
274
|
-
for (const path of resultPathsFor(ref.resultId)) {
|
|
275
|
-
try {
|
|
276
|
-
raw = await readFile(path, "utf8");
|
|
277
|
-
break;
|
|
278
|
-
}
|
|
279
|
-
catch {
|
|
280
|
-
// Try the next candidate dir.
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
if (raw === undefined) {
|
|
284
|
-
return errorResult({ error: "not_found", ...ids });
|
|
285
|
-
}
|
|
286
|
-
let parsed;
|
|
287
|
-
try {
|
|
288
|
-
parsed = JSON.parse(raw);
|
|
289
|
-
}
|
|
290
|
-
catch {
|
|
291
|
-
return errorResult({ error: "corrupt_result", ...ids });
|
|
292
|
-
}
|
|
293
|
-
// #217: `missionOutcome` is canonical; a goal run's own ending rides beside it as `goalOutcome`.
|
|
294
|
-
const parsedOutcome = parsed !== null && typeof parsed === "object" && "missionOutcome" in parsed
|
|
295
|
-
? parseResultOutcome(parsed.missionOutcome, goalOutcomeOf(parsed), defectStatusOf(parsed))
|
|
296
|
-
: null;
|
|
297
|
-
if (parsedOutcome === null) {
|
|
298
|
-
return errorResult({ error: "corrupt_result", ...ids });
|
|
299
|
-
}
|
|
300
|
-
// #423: the two orthogonal verdicts beside the status — did the goal get reached (goalOutcome +
|
|
301
|
-
// goalReason) and did the app break (defectOutcome, counted by kind) — read from the structured
|
|
302
|
-
// result, never from its `reason` prose.
|
|
303
|
-
const goalReason = resultField(parsed, "goalReason");
|
|
304
|
-
const defectOutcome = resultField(parsed, "defectOutcome");
|
|
305
|
-
const status = {
|
|
306
|
-
...missionStatus(parsedOutcome.outcome),
|
|
307
|
-
...(parsedOutcome.goalOutcome === undefined ? {} : { goalOutcome: parsedOutcome.goalOutcome }),
|
|
308
|
-
...(typeof goalReason === "string" ? { goalReason } : {}),
|
|
309
|
-
...(defectOutcome !== null && typeof defectOutcome === "object" ? { defectOutcome } : {}),
|
|
310
|
-
};
|
|
311
|
-
const result = parsed.result ?? null;
|
|
312
|
-
// An adversarial run's coverage is surfaced next to the status: an `inconclusive` run says what
|
|
313
|
-
// it did and did not exercise, so an agent can tell "found nothing" from "tried nothing".
|
|
314
|
-
const coverage = result !== null && typeof result === "object" && "coverage" in result ? result.coverage : undefined;
|
|
315
|
-
const body = { ...ids, ...status, ...(coverage === undefined ? {} : { coverage }), result };
|
|
316
|
-
return status.isError ? errorResult(body) : jsonResult(body);
|
|
317
|
-
};
|
|
318
66
|
// verify_fix: replays a persisted finding by (result id, fingerprint) — never a caller-supplied
|
|
319
67
|
// recording or path (invariant #5 analogue). Passes only if the defect signal is absent; an
|
|
320
68
|
// unreachable replay is `inconclusive` and returned as an error result, never as "fixed".
|
|
@@ -338,567 +86,14 @@ export function buildMcpTools(deps) {
|
|
|
338
86
|
...(a.env === undefined ? {} : { env: a.env }),
|
|
339
87
|
...(a.environmentsFile === undefined ? {} : { environmentsFile: a.environmentsFile }),
|
|
340
88
|
}));
|
|
341
|
-
|
|
342
|
-
const verifyFixOptions = (args) => {
|
|
343
|
-
const storageState = optPath(args, "storageState", pathRoots, { session: true });
|
|
344
|
-
if (storageState !== undefined && !existsSync(storageState))
|
|
345
|
-
throw new McpArgError(`storage state not found: ${storageState}`);
|
|
346
|
-
const replays = optInt(args, "replays", 1);
|
|
347
|
-
const invariantFiles = (optStringArray(args, "invariants") ?? []).map((f, i) => optPath({ [`invariants[${i}]`]: f }, `invariants[${i}]`, pathRoots));
|
|
348
|
-
const fixtures = optPath(args, "fixtures", pathRoots);
|
|
349
|
-
const fixtureIdentity = optNamedSessions(args, "fixtureIdentity", pathRoots);
|
|
350
|
-
const allowEmulationOverride = optBool(args, "allowEmulationOverride");
|
|
351
|
-
const headed = optBool(args, "headed");
|
|
352
|
-
const slowMo = optInt(args, "slowMo", 0);
|
|
353
|
-
const recordVideo = optRecordVideo(args, pathRoots);
|
|
354
|
-
const shots = optScreenshots(args, pathRoots);
|
|
355
|
-
const viewport = optViewport(args);
|
|
356
|
-
const device = optString(args, "device");
|
|
357
|
-
const geolocation = optString(args, "geolocation");
|
|
358
|
-
const extension = optExtensions(args, pathRoots);
|
|
359
|
-
const governance = governanceArgs(args);
|
|
360
|
-
const actionDeltas = optBool(args, "actionDeltas");
|
|
361
|
-
// #484: replaying under another allow list widens what the replay may reach — explicit only (never
|
|
362
|
-
// defaulted), and the report carries allowlistOverride. An MCP caller never names origins: only
|
|
363
|
-
// `env` (an operator-configured environment) can override the stored list; `--allow` is CLI-only.
|
|
364
|
-
if (args["allow"] !== undefined)
|
|
365
|
-
throw new McpArgError("'allow' is not an MCP argument: a replay reaches only operator-configured origins — pass 'env' (an environment in environments.json), or run `jevitate verify-fix --allow` yourself");
|
|
366
|
-
const env = optString(args, "env");
|
|
367
|
-
try {
|
|
368
|
-
const browser = browserRunFromFlags({ browserArg: [], ...governance, ...(extension === undefined ? {} : { extension }), ...(headed === undefined ? {} : { headed }), ...(slowMo === undefined ? {} : { slowMo }), ...(recordVideo === undefined ? {} : { recordVideo }) });
|
|
369
|
-
const screenshots = parseScreenshotsArg(shots);
|
|
370
|
-
const emulation = emulationFromFlags({ ...(viewport === undefined ? {} : { viewport }), ...(device === undefined ? {} : { device }), ...(geolocation === undefined ? {} : { geolocation }) });
|
|
371
|
-
return {
|
|
372
|
-
...(storageState === undefined ? {} : { storageState }),
|
|
373
|
-
...(replays === undefined ? {} : { replays }),
|
|
374
|
-
...(invariantFiles.length === 0 ? {} : { invariantFiles }),
|
|
375
|
-
...(fixtures === undefined && fixtureIdentity === undefined
|
|
376
|
-
? {}
|
|
377
|
-
: { fixtureFlags: { ...(fixtures === undefined ? {} : { fixtures }), ...(fixtureIdentity === undefined ? {} : { fixtureIdentity }) } }),
|
|
378
|
-
...(allowEmulationOverride === undefined ? {} : { allowEmulationOverride }),
|
|
379
|
-
...(browser === undefined ? {} : { browser }),
|
|
380
|
-
...(screenshots === undefined ? {} : { screenshots }),
|
|
381
|
-
...(emulation === undefined ? {} : { emulation }),
|
|
382
|
-
...(actionDeltas === true ? { actionDeltas: true } : {}),
|
|
383
|
-
...(env === undefined ? {} : { env, ...(deps.environmentsFile === undefined ? {} : { environmentsFile: deps.environmentsFile }) }),
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
catch (err) {
|
|
387
|
-
throw new McpArgError(err instanceof Error ? err.message : String(err));
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
|
-
/** #426: the run tools' `tags` object — validated like `--tag` (an invalid one is invalid_args, never dropped). */
|
|
391
|
-
const tagsArg = (args) => {
|
|
392
|
-
if (args.tags === undefined)
|
|
393
|
-
return { tags: {} };
|
|
394
|
-
try {
|
|
395
|
-
return { tags: validateRunTags(args.tags, "tags") };
|
|
396
|
-
}
|
|
397
|
-
catch (err) {
|
|
398
|
-
if (!(err instanceof RunTagError))
|
|
399
|
-
throw err;
|
|
400
|
-
return { error: { error: "invalid_args", code: err.code, message: err.message } };
|
|
401
|
-
}
|
|
402
|
-
};
|
|
403
|
-
const verifyFixTool = async (args) => {
|
|
404
|
-
if (!deps.recordingsDir) {
|
|
405
|
-
return errorResult({ error: "not_configured", message: "verify_fix requires recordingsDir" });
|
|
406
|
-
}
|
|
407
|
-
if (!(isMissionResultId(args.id) || isQueuedMissionId(args.id)) ||
|
|
408
|
-
typeof args.fingerprint !== "string" ||
|
|
409
|
-
!/^[0-9a-f]{16}$/.test(args.fingerprint)) {
|
|
410
|
-
return errorResult({ error: "invalid_args", message: "verify_fix requires a mission result 'id' (or missionId) and a 16-hex 'fingerprint'" });
|
|
411
|
-
}
|
|
412
|
-
const tagged = tagsArg(args);
|
|
413
|
-
if ("error" in tagged)
|
|
414
|
-
return errorResult(tagged.error);
|
|
415
|
-
let options;
|
|
416
|
-
try {
|
|
417
|
-
options = verifyFixOptions(args);
|
|
418
|
-
}
|
|
419
|
-
catch (err) {
|
|
420
|
-
const body = argErrorBody(err);
|
|
421
|
-
if (body === undefined)
|
|
422
|
-
throw err;
|
|
423
|
-
return errorResult(body);
|
|
424
|
-
}
|
|
425
|
-
// #205: the same pre-run checks as `jevitate verify-fix` (orphan sweep, starved-host refusal).
|
|
426
|
-
const starved = await resourcePreflight({});
|
|
427
|
-
if (starved !== null)
|
|
428
|
-
return errorResult({ error: "refused", code: starved.error?.code, message: starved.error?.message });
|
|
429
|
-
const ref = await resolveResultId(args.id, "verify_fix");
|
|
430
|
-
if ("response" in ref) {
|
|
431
|
-
// A mission still queued/running has nothing to verify yet: never a pass.
|
|
432
|
-
return ref.pending === undefined ? ref.response : errorResult({ error: "not_ready", ...ref.pending });
|
|
433
|
-
}
|
|
434
|
-
// The first candidate that exists (usability results may sit in the UX reports dir).
|
|
435
|
-
const candidates = resultPathsFor(ref.resultId);
|
|
436
|
-
let resultPath = candidates[0];
|
|
437
|
-
for (const path of candidates) {
|
|
438
|
-
try {
|
|
439
|
-
await access(path);
|
|
440
|
-
resultPath = path;
|
|
441
|
-
break;
|
|
442
|
-
}
|
|
443
|
-
catch {
|
|
444
|
-
// Try the next candidate dir.
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
try {
|
|
448
|
-
const fingerprint = args.fingerprint;
|
|
449
|
-
const report = await withRunMetadata({ tags: tagged.tags }, () => verifyFixImpl({ ...options, resultPath, fingerprint }));
|
|
450
|
-
const body = stampRunMetadata({
|
|
451
|
-
id: args.id,
|
|
452
|
-
...(ref.missionId === undefined ? {} : { missionId: ref.missionId, resultId: ref.resultId }),
|
|
453
|
-
status: report.verdict,
|
|
454
|
-
...report,
|
|
455
|
-
}, { tags: tagged.tags });
|
|
456
|
-
// Neither is a pass: `inconclusive` proved nothing either way, `intermittent` (#74) means the
|
|
457
|
-
// signal fired on SOME but not all fresh-context replays — never trustworthy as "fixed".
|
|
458
|
-
return report.verdict === "inconclusive" || report.verdict === "intermittent" ? errorResult(body) : jsonResult(body);
|
|
459
|
-
}
|
|
460
|
-
catch (err) {
|
|
461
|
-
return errorResult({ error: "verify_fix_refused", message: err instanceof Error ? err.message : String(err) });
|
|
462
|
-
}
|
|
463
|
-
};
|
|
464
|
-
/**
|
|
465
|
-
* #255: `run_journey`'s CLI-parity options, resolved exactly as `journey run` resolves its flags
|
|
466
|
-
* (the same helpers: environmentFromFlags, browserRunFromFlags, parseScreenshotsArg,
|
|
467
|
-
* emulationFromFlags, buildMissionFixtures) — every refusal typed, before any browser opens.
|
|
468
|
-
*/
|
|
469
|
-
const resolveJourneyRun = async (args) => {
|
|
470
|
-
// Params keep their historical leniency for a non-object (treated as none); a non-string VALUE is refused.
|
|
471
|
-
const params = args.params && typeof args.params === "object" && !Array.isArray(args.params) ? (optStringMap(args, "params") ?? {}) : {};
|
|
472
|
-
const envName = optString(args, "env");
|
|
473
|
-
const baseUrl = optString(args, "baseUrl");
|
|
474
|
-
const environment = environmentFromFlags({ ...(envName === undefined ? {} : { env: envName }), ...(baseUrl === undefined ? {} : { baseUrl }) }, { ...(deps.environmentsFile === undefined ? {} : { environmentsFile: deps.environmentsFile }), ...(deps.targetsConfigPath === undefined ? {} : { targetsFile: deps.targetsConfigPath }) });
|
|
475
|
-
// --storage-state wins; else the environment's own session (~/.jevitate/targets.json[<origin>]).
|
|
476
|
-
const storageState = optPath(args, "storageState", pathRoots, { session: true }) ?? environment?.storageState;
|
|
477
|
-
if (args.storageState !== undefined && storageState !== undefined && !existsSync(storageState))
|
|
478
|
-
throw new McpArgError(`storage state not found: ${storageState}`);
|
|
479
|
-
const headed = optBool(args, "headed");
|
|
480
|
-
const slowMo = optInt(args, "slowMo", 0);
|
|
481
|
-
const recordVideo = optRecordVideo(args, pathRoots);
|
|
482
|
-
const extension = optExtensions(args, pathRoots);
|
|
483
|
-
const governance = governanceArgs(args);
|
|
484
|
-
let browser;
|
|
485
|
-
let screenshots;
|
|
486
|
-
let emulation;
|
|
487
|
-
try {
|
|
488
|
-
browser = browserRunFromFlags({ browserArg: [], ...governance, ...(extension === undefined ? {} : { extension }), ...(headed === undefined ? {} : { headed }), ...(slowMo === undefined ? {} : { slowMo }), ...(recordVideo === undefined ? {} : { recordVideo }) });
|
|
489
|
-
screenshots = parseScreenshotsArg(optScreenshots(args, pathRoots));
|
|
490
|
-
const viewport = optViewport(args);
|
|
491
|
-
const device = optString(args, "device");
|
|
492
|
-
const geolocation = optString(args, "geolocation");
|
|
493
|
-
emulation = emulationFromFlags({ ...(viewport === undefined ? {} : { viewport }), ...(device === undefined ? {} : { device }), ...(geolocation === undefined ? {} : { geolocation }) });
|
|
494
|
-
}
|
|
495
|
-
catch (err) {
|
|
496
|
-
if (err instanceof McpArgError || argErrorBody(err) !== undefined)
|
|
497
|
-
throw err;
|
|
498
|
-
throw new McpArgError(err instanceof Error ? err.message : String(err));
|
|
499
|
-
}
|
|
500
|
-
const fixturesFile = optPath(args, "fixtures", pathRoots);
|
|
501
|
-
const fixtureIdentity = optNamedSessions(args, "fixtureIdentity", pathRoots);
|
|
502
|
-
// The environment's hooks apply as on the CLI; they need --allow-shell-hooks, which MCP never sets.
|
|
503
|
-
const fixtureFlags = {
|
|
504
|
-
...(fixturesFile === undefined ? {} : { fixtures: fixturesFile }),
|
|
505
|
-
...(fixtureIdentity === undefined ? {} : { fixtureIdentity }),
|
|
506
|
-
...(environment?.hooks?.before === undefined ? {} : { before: environment.hooks.before }),
|
|
507
|
-
...(environment?.hooks?.after === undefined ? {} : { after: environment.hooks.after }),
|
|
508
|
-
};
|
|
509
|
-
const fixtures = (site) => {
|
|
510
|
-
const fx = buildMissionFixtures(fixtureFlags, {
|
|
511
|
-
allowlist: environment === undefined ? [site] : environment.allowedOrigins,
|
|
512
|
-
baseUrl: site,
|
|
513
|
-
...(storageState !== undefined ? { storageState } : {}),
|
|
514
|
-
...(environment?.fixtures === undefined ? {} : { targetFixtures: environment.fixtures }),
|
|
515
|
-
});
|
|
516
|
-
checkSetupRefs({ "--param": Object.values(params) }, fx);
|
|
517
|
-
return fx;
|
|
518
|
-
};
|
|
519
|
-
const selfHeal = optEnum(args, "selfHeal", ["fail-closed", "hybrid", "full"]) ?? "fail-closed";
|
|
520
|
-
const real = optBool(args, "real") ?? false;
|
|
521
|
-
const fakeAi = optBool(args, "fakeAi") ?? false;
|
|
522
|
-
// #429: the Jev provider for self-heal judgments (typesafe | openrouter), as `--jev-provider`.
|
|
523
|
-
const jevProvider = optEnum(args, "jevProvider", ["typesafe", "openrouter"]);
|
|
524
|
-
// #453 Q1: the same refusals as `journey run` — a heal needs a change context, the change and
|
|
525
|
-
// budget arguments need a heal — before any gateway, Journey lookup or browser.
|
|
526
|
-
const changes = optString(args, "changes");
|
|
527
|
-
const healMaxAttempts = optInt(args, "healMaxAttempts", 1);
|
|
528
|
-
const healMaxModelCalls = optInt(args, "healMaxModelCalls", 1);
|
|
529
|
-
const healMaxMs = optInt(args, "healMaxMs", 1);
|
|
530
|
-
const healMaxRunAttempts = optInt(args, "healMaxRunAttempts", 1);
|
|
531
|
-
const healMaxRunMs = optInt(args, "healMaxRunMs", 1);
|
|
532
|
-
const stepResponseTimeoutMs = optInt(args, "stepResponseTimeoutMs", 1);
|
|
533
|
-
if (stepResponseTimeoutMs !== undefined && stepResponseTimeoutMs > MAX_STEP_RESPONSE_TIMEOUT_MS) {
|
|
534
|
-
throw new McpArgError(`'stepResponseTimeoutMs' must be an integer from 1 to ${MAX_STEP_RESPONSE_TIMEOUT_MS}`);
|
|
535
|
-
}
|
|
536
|
-
const healRequest = {
|
|
537
|
-
selfHeal,
|
|
538
|
-
...(changes === undefined ? {} : { changes }),
|
|
539
|
-
changeNotes: optStringArray(args, "changeNote") ?? [],
|
|
540
|
-
...(healMaxAttempts === undefined ? {} : { maxAttempts: healMaxAttempts }),
|
|
541
|
-
...(healMaxModelCalls === undefined ? {} : { maxModelCalls: healMaxModelCalls }),
|
|
542
|
-
...(healMaxMs === undefined ? {} : { maxMs: healMaxMs }),
|
|
543
|
-
...(healMaxRunAttempts === undefined ? {} : { maxRunAttempts: healMaxRunAttempts }),
|
|
544
|
-
...(healMaxRunMs === undefined ? {} : { maxRunMs: healMaxRunMs }),
|
|
545
|
-
};
|
|
546
|
-
validateJourneyHeal(healRequest, MCP_HEAL_NAMES);
|
|
547
|
-
const healScope = selfHeal === "fail-closed" ? undefined : await readJourneyChangeScope(healRequest, deps.journeysDir);
|
|
548
|
-
let selfHealer;
|
|
549
|
-
let policy = defaultRunPolicy();
|
|
550
|
-
let usage;
|
|
551
|
-
if (selfHeal !== "fail-closed") {
|
|
552
|
-
if (deps.selfHealGateways === undefined)
|
|
553
|
-
throw new SetupRequired("run_journey selfHeal needs the model gateways, which this server was not given");
|
|
554
|
-
// #453: the healer is evidence-only — its model is an advisory ranker; judgment never decides a heal.
|
|
555
|
-
let gen;
|
|
556
|
-
try {
|
|
557
|
-
({ gen, usage } = await deps.selfHealGateways({ real, fakeAi, ...(jevProvider === undefined ? {} : { jevProvider }) }));
|
|
558
|
-
}
|
|
559
|
-
catch (err) {
|
|
560
|
-
throw new SetupRequired(redactCredentials(err instanceof Error ? err.message : String(err), credentialStore));
|
|
561
|
-
}
|
|
562
|
-
selfHealer = makeEvidenceSelfHealer(gen, usage === undefined ? {} : { usage });
|
|
563
|
-
policy = { ...policy, selfHeal: { mode: selfHeal, budget: journeyHealBudget(healRequest) } };
|
|
564
|
-
}
|
|
565
|
-
return {
|
|
566
|
-
params,
|
|
567
|
-
...(storageState === undefined ? {} : { storageState }),
|
|
568
|
-
options: {
|
|
569
|
-
policy,
|
|
570
|
-
fixtures,
|
|
571
|
-
...(selfHealer === undefined ? {} : { selfHealer }),
|
|
572
|
-
...(healScope === undefined ? {} : { heal: { scope: healScope } }),
|
|
573
|
-
...(environment === undefined ? {} : { environment }),
|
|
574
|
-
...(browser === undefined ? {} : { browser }),
|
|
575
|
-
...(emulation === undefined ? {} : { emulation }),
|
|
576
|
-
...(screenshots === undefined ? {} : { screenshots }),
|
|
577
|
-
...(optBool(args, "actionDeltas") === true ? { actionDeltas: true } : {}),
|
|
578
|
-
...(stepResponseTimeoutMs === undefined ? {} : { stepResponseTimeoutMs }),
|
|
579
|
-
},
|
|
580
|
-
...(usage === undefined ? {} : { usage }),
|
|
581
|
-
};
|
|
582
|
-
};
|
|
89
|
+
const ctx = { deps, pathRoots, credentialStore, runJourney, verifyFixImpl };
|
|
583
90
|
const wired = {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
"'invariants' (invariant file paths re-checking a declared-invariant defect); 'fixtures' (a fixtures file overriding the mission's saved one); 'fixtureIdentity' (#243: 'name=<storageState path>' entries re-binding a fixture step's auth.identity; default: the identities the mission recorded); 'extension' (#256: unpacked extension directories — refused unless the same build the finding was recorded with); 'actionDeltas' (#303, opt-in: the defect step's replayed delta vs the recorded one, as evidence on each attempt); 'maxBrowsers'/'maxBrowserMemory' (#205: machine-wide browser cap, browser memory ceiling in MiB); 'tags' (#426: {key: value} run metadata, returned on the result — never a secret); #484 'env' (a named environment in environments.json: replay under its CURRENT allow list instead of the one stored with the result, e.g. to prove a localhost → 127.0.0.1 config fix; explicit only, never defaulted) — the result then carries allowlistOverride (source, origins, recorded, added, removed); raw origins are CLI-only (`jevitate verify-fix --allow`), never an MCP argument. Operator-only settings (shell hooks, cmd: log sources, re-sending paid/destructive hang writes) come from targets.json, never an argument.",
|
|
589
|
-
inputSchema: {
|
|
590
|
-
type: "object",
|
|
591
|
-
properties: {
|
|
592
|
-
id: { type: "string" },
|
|
593
|
-
fingerprint: { type: "string" },
|
|
594
|
-
replays: { type: "integer", minimum: 1 },
|
|
595
|
-
actionDeltas: { type: "boolean" },
|
|
596
|
-
recordVideo: { type: ["boolean", "string"] },
|
|
597
|
-
screenshots: { type: ["boolean", "string"] },
|
|
598
|
-
headed: { type: "boolean" },
|
|
599
|
-
slowMo: { type: "integer", minimum: 0 },
|
|
600
|
-
storageState: { type: "string" },
|
|
601
|
-
viewport: { type: "object", properties: { width: { type: "integer" }, height: { type: "integer" } }, required: ["width", "height"] },
|
|
602
|
-
device: { type: "string" },
|
|
603
|
-
geolocation: { type: "string" },
|
|
604
|
-
allowEmulationOverride: { type: "boolean" },
|
|
605
|
-
invariants: { type: "array", items: { type: "string" } },
|
|
606
|
-
fixtures: { type: "string" },
|
|
607
|
-
fixtureIdentity: { type: "array", items: { type: "string" } },
|
|
608
|
-
extension: { type: "array", items: { type: "string" } },
|
|
609
|
-
maxBrowsers: { type: "integer", minimum: 1 },
|
|
610
|
-
maxBrowserMemory: { type: "integer", minimum: 1 },
|
|
611
|
-
tags: { type: "object", additionalProperties: { type: "string" } },
|
|
612
|
-
env: { type: "string" },
|
|
613
|
-
},
|
|
614
|
-
required: ["id", "fingerprint"],
|
|
615
|
-
},
|
|
616
|
-
handler: verifyFixTool,
|
|
617
|
-
},
|
|
618
|
-
get_mission_result: {
|
|
619
|
-
description: "Read a mission's TYPED result by id: a result stem (explore-|coverage-|exploratory-|adversarial-|feature-|usability-<stamp>, e.g. adversarial-2026-09-23T00-00-00-000Z) or a queue_exploration missionId. A queued mission reports status queued | running (pending: true — poll again; `jevitate mission run` drains the queue) or failed (an error: it could not run). A finished one: status is clean | defects-found | hang | intermittent | inconclusive | crashed, with the matching CLI exit code. A broken run (inconclusive/crashed) is returned as an error result — never a pass. An adversarial result carries `coverage` (target controls exercised/total, forms submitted, strategies applied vs found nothing, out-of-scope steps): a run below its coverage thresholds is inconclusive, never clean.",
|
|
620
|
-
inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
621
|
-
handler: getMissionResult,
|
|
622
|
-
},
|
|
623
|
-
find_capabilities: {
|
|
624
|
-
description: "Find promoted Journeys (capabilities) whose metadata matches a query.",
|
|
625
|
-
inputSchema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] },
|
|
626
|
-
handler: async (args) => jsonResult(await findCaps(typeof args.query === "string" ? args.query : "")),
|
|
627
|
-
},
|
|
628
|
-
run_journey: {
|
|
629
|
-
description: "Run a PUBLISHED Journey by id with string params (fail-closed policy) — `jevitate journey run`. Never accepts inline steps. " +
|
|
630
|
-
"Optional 'storageState': a PATH (on the machine running this MCP server; inside the project or ~/.jevitate, never a repo's .jevitate/) to a Playwright storageState " +
|
|
631
|
-
"JSON file, for a Journey authored behind a login (#118) — the file's contents are read only by the " +
|
|
632
|
-
"server's own browser, never returned or logged. A Journey that declares metadata.requiresAuth refuses " +
|
|
633
|
-
"with a clear error when no storageState is given. #255 (same as the CLI flags): 'env' (a named environment in .jevitate/environments.json; its session from ~/.jevitate/targets.json) and/or 'baseUrl'; " +
|
|
634
|
-
"'headed' (needs a display) and 'slowMo'; 'recordVideo' (true, or a directory) → videoPaths; 'screenshots' (true | screens | steps | mode:<dir> | <dir>) → screenshotPaths; " +
|
|
635
|
-
"'viewport' {width,height} or 'device' (mutually exclusive); 'geolocation' '<lat>,<lng>[,<accuracy m>]' (#329); 'fixtures' (a fixtures JSON path: setup before, restore after; 'fixtureIdentity' (#243) 'name=<storageState path>' entries name who a step with auth.identity authenticates as; an environment's shell hooks are never run over MCP — they refuse as the CLI does without --allow-shell-hooks); " +
|
|
636
|
-
"'extension' (#256: unpacked extension directories inside the project or ~/.jevitate; a Journey recorded with extensions needs the same build); 'selfHeal' fail-closed (default) | hybrid | full with 'real' or 'fakeAi' (a write step never self-heals; 'jevProvider' typesafe | openrouter picks the Jev key with real, #429) — #453: a heal mode needs a change context, 'changes' (a git range such as HEAD~1..HEAD, read-only) and/or 'changeNote' (strings), else it is refused; only a break that change explains is retargeted, one-for-one, and the run ends healed-pending-review (exitCode 5: a proposed revision a person accepts, never a pass); budgets 'healMaxAttempts'/'healMaxModelCalls'/'healMaxMs' (per broken step) and 'healMaxRunAttempts'/'healMaxRunMs' (per run), each >= 1; the result's 'exitCode' is the CLI's (0 ok, 5 pending review, 1 failed); 'actionDeltas' (#303, opt-in: each replayed step's action delta, compared with the recorded one → actionDeltas); 'stepResponseTimeoutMs' (#486, 1..300000, default 15000: how long a step's responseStatus check waits for a request sent but not yet answered, e.g. a cold start — a late 2xx is an advisory slowSteps finding; a step's own responseTimeoutMs overrides it); 'maxBrowsers'/'maxBrowserMemory' (#205: machine-wide browser cap, browser memory ceiling in MiB); 'tags' (#426: {key: value} run metadata, returned on the result — never a secret). A site-policy refusal is {error: throttled, retryAfter}.",
|
|
637
|
-
inputSchema: {
|
|
638
|
-
type: "object",
|
|
639
|
-
properties: {
|
|
640
|
-
id: { type: "string" },
|
|
641
|
-
params: { type: "object", additionalProperties: { type: "string" } },
|
|
642
|
-
storageState: { type: "string" },
|
|
643
|
-
env: { type: "string" },
|
|
644
|
-
baseUrl: { type: "string" },
|
|
645
|
-
headed: { type: "boolean" },
|
|
646
|
-
slowMo: { type: "integer", minimum: 0 },
|
|
647
|
-
recordVideo: { type: ["boolean", "string"] },
|
|
648
|
-
screenshots: { type: ["boolean", "string"] },
|
|
649
|
-
viewport: { type: "object", properties: { width: { type: "integer" }, height: { type: "integer" } }, required: ["width", "height"] },
|
|
650
|
-
device: { type: "string" },
|
|
651
|
-
geolocation: { type: "string" },
|
|
652
|
-
fixtures: { type: "string" },
|
|
653
|
-
fixtureIdentity: { type: "array", items: { type: "string" } },
|
|
654
|
-
selfHeal: { type: "string", enum: ["fail-closed", "hybrid", "full"] },
|
|
655
|
-
changes: { type: "string" },
|
|
656
|
-
changeNote: { type: "array", items: { type: "string" } },
|
|
657
|
-
healMaxAttempts: { type: "integer", minimum: 1 },
|
|
658
|
-
healMaxModelCalls: { type: "integer", minimum: 1 },
|
|
659
|
-
healMaxMs: { type: "integer", minimum: 1 },
|
|
660
|
-
healMaxRunAttempts: { type: "integer", minimum: 1 },
|
|
661
|
-
healMaxRunMs: { type: "integer", minimum: 1 },
|
|
662
|
-
real: { type: "boolean" },
|
|
663
|
-
fakeAi: { type: "boolean" },
|
|
664
|
-
jevProvider: { type: "string", enum: ["typesafe", "openrouter"] },
|
|
665
|
-
extension: { type: "array", items: { type: "string" } },
|
|
666
|
-
maxBrowsers: { type: "integer", minimum: 1 },
|
|
667
|
-
maxBrowserMemory: { type: "integer", minimum: 1 },
|
|
668
|
-
actionDeltas: { type: "boolean" },
|
|
669
|
-
stepResponseTimeoutMs: { type: "integer", minimum: 1, maximum: MAX_STEP_RESPONSE_TIMEOUT_MS },
|
|
670
|
-
tags: { type: "object", additionalProperties: { type: "string" } },
|
|
671
|
-
},
|
|
672
|
-
required: ["id"],
|
|
673
|
-
},
|
|
674
|
-
handler: async (args) => {
|
|
675
|
-
if (typeof args.id !== "string" || args.id.length === 0) {
|
|
676
|
-
return errorResult({ error: "invalid_args", message: "run_journey requires a non-empty string 'id'" });
|
|
677
|
-
}
|
|
678
|
-
const tagged = tagsArg(args);
|
|
679
|
-
if ("error" in tagged)
|
|
680
|
-
return errorResult(tagged.error);
|
|
681
|
-
// Invariant #5: only id + params (+ a storageState PATH and the run options below) are
|
|
682
|
-
// threaded through — any inline `steps`/`recording` in the arguments is deliberately ignored.
|
|
683
|
-
let resolved;
|
|
684
|
-
try {
|
|
685
|
-
resolved = await resolveJourneyRun(args);
|
|
686
|
-
}
|
|
687
|
-
catch (err) {
|
|
688
|
-
const body = argErrorBody(err) ?? (isEnvironmentError(err) ? { error: "invalid_args", code: err.code, message: err.message } : undefined);
|
|
689
|
-
if (body !== undefined)
|
|
690
|
-
return errorResult(body);
|
|
691
|
-
if (err instanceof JourneyHealArgsError || err instanceof ChangesArgsError || err instanceof ChangesInputError) {
|
|
692
|
-
return errorResult({ error: "invalid_args", code: err.code, message: err.message });
|
|
693
|
-
}
|
|
694
|
-
if (err instanceof SetupRequired)
|
|
695
|
-
return errorResult({ error: "setup_required", message: err.message });
|
|
696
|
-
throw err;
|
|
697
|
-
}
|
|
698
|
-
// #205: the same pre-run checks as `jevitate journey run` (orphan sweep, starved-host refusal).
|
|
699
|
-
const starved = await resourcePreflight({});
|
|
700
|
-
if (starved !== null)
|
|
701
|
-
return errorResult({ error: "refused", code: starved.error?.code, message: starved.error?.message });
|
|
702
|
-
try {
|
|
703
|
-
const id = args.id;
|
|
704
|
-
const result = await withRunMetadata({ tags: tagged.tags }, () => runJourney(id, resolved.params, resolved.storageState, resolved.options));
|
|
705
|
-
// #163: a self-healing run's model usage lands on its result, as on the CLI. #426: so do its tags.
|
|
706
|
-
const withUsage = resolved.usage === undefined || result === null || typeof result !== "object" ? result : { ...result, usage: resolved.usage.snapshot() };
|
|
707
|
-
// #453: the CLI's exit code for the outcome — 0 ok, 5 healed-pending-review (a proposed revision, never a pass), 1 otherwise.
|
|
708
|
-
return jsonResult(stampRunMetadata(withJourneyExitCode(withUsage), { tags: tagged.tags }));
|
|
709
|
-
}
|
|
710
|
-
catch (err) {
|
|
711
|
-
// A site-policy refusal (throttle, budget, quiet hours) is an answer the agent acts on — when to retry.
|
|
712
|
-
if (err instanceof SiteGateRefusedError) {
|
|
713
|
-
return errorResult({ error: "throttled", reason: err.reason, retryAfter: err.retryAfter, message: err.message });
|
|
714
|
-
}
|
|
715
|
-
if (err instanceof UnknownJourneyError)
|
|
716
|
-
return errorResult({ error: "not_found", id: args.id, message: err.message });
|
|
717
|
-
if (err instanceof JourneyRequiresAuthError)
|
|
718
|
-
return errorResult({ error: "invalid_args", code: "E_JOURNEY_REQUIRES_AUTH", message: err.message });
|
|
719
|
-
if (err instanceof ExtensionMismatchError)
|
|
720
|
-
return errorResult({ error: "invalid_args", code: err.code, message: err.message });
|
|
721
|
-
if (err instanceof ParamValidationError)
|
|
722
|
-
return errorResult({ error: "invalid_args", code: "E_INVALID_PARAMS", message: err.message });
|
|
723
|
-
if (isEnvironmentError(err) || err instanceof FixtureSpecError || err instanceof UnboundSetupRefError) {
|
|
724
|
-
return errorResult({ error: "invalid_args", code: err.code, message: err.message });
|
|
725
|
-
}
|
|
726
|
-
// Never run on unknown state: inconclusive (exit 2) — proves nothing, so an error result.
|
|
727
|
-
if (err instanceof FixtureSetupError) {
|
|
728
|
-
return errorResult({ outcome: "inconclusive", reason: err.message, failure: { kind: "configuration", message: err.message }, attribution: "configuration", exitCode: 2 });
|
|
729
|
-
}
|
|
730
|
-
throw err;
|
|
731
|
-
}
|
|
732
|
-
},
|
|
733
|
-
},
|
|
734
|
-
queue_exploration: {
|
|
735
|
-
description: "Enqueue an exploration mission against a PROMOTED target. Never runs anything — only queues; `jevitate mission run` drains the queue, and get_mission_result {id: missionId} reports its status/result. strategy: goal-based (goal|feature|route + successAssertion) | coverage | exploratory (novelty-first coverage) | adversarial (optional in-scope route glob) | feature (feature name, optional route glob); a usability review is not queueable — use run_exploration {strategy: usability}. The target's authorized origin plus its declared apiOrigins are the only reachable origins. Refuses unknown/unpromoted targets, over-ceiling budgets and invalid declared `invariants` (an optional closed spec checked around every action; probes GET/HEAD on the target origin only). Optional 'viewport' ({width,height}) or 'device' (a Playwright devices registry name, e.g. \"iPhone 13\") — mutually exclusive (#149); default: Playwright's own default viewport. An unknown device is refused before any browser opens. #255: 'recordVideo' / 'evidenceVideo' (booleans) and 'screenshots' (screens | steps) write media next to the result (listed in it) — a queued request never names a path; 'persona' names a persona in the operator's ~/.jevitate/targets.json for the target's origin (its session, never the caller's). #424: 'minEffort' {minActions, minDistinctStates} (goal-based only) — the minimum exploration before the model may conclude, capped by the budget. Same as `jevitate mission queue`.",
|
|
736
|
-
inputSchema: {
|
|
737
|
-
type: "object",
|
|
738
|
-
properties: {
|
|
739
|
-
target: { type: "string" },
|
|
740
|
-
goal: { type: "string" },
|
|
741
|
-
feature: { type: "string" },
|
|
742
|
-
route: { type: "string" },
|
|
743
|
-
successAssertion: { type: "object" },
|
|
744
|
-
strategy: { type: "string", enum: [...MISSION_STRATEGIES] },
|
|
745
|
-
budget: {
|
|
746
|
-
type: "object",
|
|
747
|
-
properties: {
|
|
748
|
-
maxActions: { type: "number" },
|
|
749
|
-
maxDecisions: { type: "number" },
|
|
750
|
-
maxCandidates: { type: "number" },
|
|
751
|
-
},
|
|
752
|
-
},
|
|
753
|
-
// App-declared invariants (#86), inline only: a closed schema (observe: dom|network|probe,
|
|
754
|
-
// invariants: require|never|always). Probes are GET/HEAD on the target's own origin; a spec
|
|
755
|
-
// that does not validate refuses the enqueue.
|
|
756
|
-
invariants: { type: "object" },
|
|
757
|
-
// Per-mission viewport/device emulation (#149) — mutually exclusive.
|
|
758
|
-
viewport: {
|
|
759
|
-
type: "object",
|
|
760
|
-
properties: { width: { type: "number" }, height: { type: "number" } },
|
|
761
|
-
required: ["width", "height"],
|
|
762
|
-
},
|
|
763
|
-
device: { type: "string" },
|
|
764
|
-
// #255: media next to the result (a mode or a boolean — never a path) and a persona NAME.
|
|
765
|
-
recordVideo: { type: "boolean" },
|
|
766
|
-
screenshots: { type: "string", enum: [...QUEUED_SCREENSHOT_MODES] },
|
|
767
|
-
evidenceVideo: { type: "boolean" },
|
|
768
|
-
persona: { type: "string" },
|
|
769
|
-
// #424 (goal-based only): the minimum exploration effort before the model may conclude.
|
|
770
|
-
minEffort: {
|
|
771
|
-
type: "object",
|
|
772
|
-
properties: { minActions: { type: "number" }, minDistinctStates: { type: "number" } },
|
|
773
|
-
},
|
|
774
|
-
},
|
|
775
|
-
required: ["target"],
|
|
776
|
-
},
|
|
777
|
-
handler: async (args) => {
|
|
778
|
-
try {
|
|
779
|
-
// All shape/budget/promoted-target validation is delegated to
|
|
780
|
-
// `enqueueMission` (fail-closed). A refusal surfaces as a structured
|
|
781
|
-
// error — NEVER a fabricated "queued" success.
|
|
782
|
-
return jsonResult(await queueExploration(args));
|
|
783
|
-
}
|
|
784
|
-
catch (err) {
|
|
785
|
-
return errorResult({
|
|
786
|
-
error: "queue_exploration_refused",
|
|
787
|
-
message: err instanceof Error ? err.message : String(err),
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
},
|
|
791
|
-
},
|
|
792
|
-
...(aiGenerateText
|
|
793
|
-
? {
|
|
794
|
-
ai_generate_text: {
|
|
795
|
-
description: "Generate a single text value for a form field via the model gateway. Credential-gated: a missing key returns a typed setup_required result; the key is NEVER sent to the model or returned.",
|
|
796
|
-
inputSchema: {
|
|
797
|
-
type: "object",
|
|
798
|
-
properties: {
|
|
799
|
-
fieldLabel: { type: "string" },
|
|
800
|
-
goal: { type: "string" },
|
|
801
|
-
visibleContext: { type: "string" },
|
|
802
|
-
history: { type: "array", items: { type: "string" } },
|
|
803
|
-
},
|
|
804
|
-
required: ["fieldLabel", "goal", "visibleContext"],
|
|
805
|
-
},
|
|
806
|
-
handler: async (args) => {
|
|
807
|
-
if (typeof args.fieldLabel !== "string" ||
|
|
808
|
-
typeof args.goal !== "string" ||
|
|
809
|
-
typeof args.visibleContext !== "string") {
|
|
810
|
-
return errorResult({
|
|
811
|
-
error: "invalid_args",
|
|
812
|
-
message: "ai_generate_text requires string 'fieldLabel', 'goal' and 'visibleContext'",
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
const history = Array.isArray(args.history) && args.history.every((h) => typeof h === "string")
|
|
816
|
-
? args.history
|
|
817
|
-
: undefined;
|
|
818
|
-
try {
|
|
819
|
-
// The facade applies `withPreflight` — a missing credential
|
|
820
|
-
// returns a typed `setup_required` precondition (the model is
|
|
821
|
-
// never called), which is a legitimate result surface, not an
|
|
822
|
-
// error, so the host can collect the key and retry.
|
|
823
|
-
return jsonResult(await aiGenerateText({
|
|
824
|
-
fieldLabel: args.fieldLabel,
|
|
825
|
-
goal: args.goal,
|
|
826
|
-
visibleContext: args.visibleContext,
|
|
827
|
-
...(history !== undefined ? { history } : {}),
|
|
828
|
-
}));
|
|
829
|
-
}
|
|
830
|
-
catch (err) {
|
|
831
|
-
// A provider/SDK error can echo request internals verbatim —
|
|
832
|
-
// redact any credential value and return a generic, key-free
|
|
833
|
-
// message. Never surface raw provider error text.
|
|
834
|
-
const raw = err instanceof Error ? err.message : String(err);
|
|
835
|
-
return errorResult({
|
|
836
|
-
error: "ai_generate_failed",
|
|
837
|
-
message: redactCredentials(raw, credentialStore),
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
},
|
|
841
|
-
},
|
|
842
|
-
}
|
|
843
|
-
: {}),
|
|
844
|
-
list_incoming: {
|
|
845
|
-
description: "List pending inbox items (HITL commands awaiting a human) as InboxSummary projections.",
|
|
846
|
-
inputSchema: { type: "object", properties: {} },
|
|
847
|
-
handler: inboxHandler((store) => facadeListIncoming(store)),
|
|
848
|
-
},
|
|
849
|
-
get_command: {
|
|
850
|
-
description: "Agent poll for one inbox item by id (burn-after-read: consumes any human-provided secret input exactly once). Returns not_found for an unknown id.",
|
|
851
|
-
inputSchema: idInputSchema,
|
|
852
|
-
handler: inboxHandler((store, args) => facadeGetCommand(store, args)),
|
|
853
|
-
},
|
|
854
|
-
get_thread: {
|
|
855
|
-
description: "Get the conversation thread (agent/human messages) for one inbox item by id. Never returns secret input.",
|
|
856
|
-
inputSchema: idInputSchema,
|
|
857
|
-
handler: inboxHandler((store, args) => facadeGetThread(store, args)),
|
|
858
|
-
},
|
|
859
|
-
queue_action: {
|
|
860
|
-
description: "Enqueue a HITL inbox item requiring a human decision — kind defaults to 'approval' ('handback'/'review' also accepted). Never resolves anything; only queues.",
|
|
861
|
-
inputSchema: {
|
|
862
|
-
type: "object",
|
|
863
|
-
properties: { ...queueCommonProperties, kind: { type: "string", enum: ["approval", "handback", "review"] } },
|
|
864
|
-
required: queueCommonRequired,
|
|
865
|
-
},
|
|
866
|
-
handler: inboxHandler((store, args) => facadeQueueAction(store, args)),
|
|
867
|
-
},
|
|
868
|
-
queue_retrieval: {
|
|
869
|
-
description: "Enqueue a HITL 'handback' inbox item requesting the human retrieve/provide something back to the agent. Never resolves anything; only queues.",
|
|
870
|
-
inputSchema: { type: "object", properties: queueCommonProperties, required: queueCommonRequired },
|
|
871
|
-
handler: inboxHandler((store, args) => facadeQueueRetrieval(store, args)),
|
|
872
|
-
},
|
|
873
|
-
approve_action: {
|
|
874
|
-
description: "SM1: an agent can NEVER approve an inbox item over MCP — always refuses with human_approval_required. Approval is only permitted from the local jevitate UI.",
|
|
875
|
-
inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
876
|
-
handler: async () => errorResult(facadeApproveAction()),
|
|
877
|
-
},
|
|
878
|
-
cancel_command: {
|
|
879
|
-
description: "SM1: an agent can NEVER cancel an inbox item over MCP — always refuses with human_approval_required. Cancellation is only permitted from the local jevitate UI.",
|
|
880
|
-
inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
881
|
-
handler: async () => errorResult(facadeCancelCommand()),
|
|
882
|
-
},
|
|
883
|
-
get_site_health: {
|
|
884
|
-
description: "Report inbox store health: ok, pending count, age of the oldest pending item, and store version.",
|
|
885
|
-
inputSchema: { type: "object", properties: {} },
|
|
886
|
-
handler: inboxHandler((store) => facadeGetSiteHealth(store)),
|
|
887
|
-
},
|
|
91
|
+
...findingTools(ctx),
|
|
92
|
+
...journeyTools(ctx),
|
|
93
|
+
...exploreTools(ctx),
|
|
94
|
+
...inboxTools(ctx),
|
|
888
95
|
};
|
|
889
|
-
|
|
890
|
-
for (const spec of CLI_TOOL_SPECS) {
|
|
891
|
-
if (wired[spec.name] !== undefined)
|
|
892
|
-
throw new Error(`MCP tool '${spec.name}' is defined twice`);
|
|
893
|
-
wired[spec.name] = {
|
|
894
|
-
description: spec.description,
|
|
895
|
-
inputSchema: cliToolInputSchema(spec),
|
|
896
|
-
handler: async (args) => {
|
|
897
|
-
const { isError, body } = await runCliTool(spec, args, { runCli: deps.runCli, roots: pathRoots, redact: (text) => redactCredentials(text, credentialStore) });
|
|
898
|
-
return isError ? errorResult(body) : jsonResult(body);
|
|
899
|
-
},
|
|
900
|
-
};
|
|
901
|
-
}
|
|
96
|
+
cliTools(ctx, wired);
|
|
902
97
|
const notImplemented = (name) => async () => errorResult({
|
|
903
98
|
error: "not_implemented",
|
|
904
99
|
tool: name,
|