@guanlin4924/devflow-pi 0.1.94 → 0.1.96
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/README.md +34 -4
- package/node_modules/@playwright/test/LICENSE +202 -0
- package/node_modules/@playwright/test/NOTICE +5 -0
- package/node_modules/@playwright/test/README.md +318 -0
- package/node_modules/@playwright/test/cli.js +19 -0
- package/node_modules/@playwright/test/index.d.ts +18 -0
- package/node_modules/@playwright/test/index.js +17 -0
- package/node_modules/@playwright/test/index.mjs +18 -0
- package/node_modules/@playwright/test/package.json +35 -0
- package/node_modules/@playwright/test/reporter.d.ts +17 -0
- package/node_modules/@playwright/test/reporter.js +17 -0
- package/node_modules/@playwright/test/reporter.mjs +17 -0
- package/node_modules/playwright/LICENSE +202 -0
- package/node_modules/playwright/NOTICE +5 -0
- package/node_modules/playwright/README.md +318 -0
- package/node_modules/playwright/ThirdPartyNotices.txt +14 -0
- package/node_modules/playwright/cli.js +19 -0
- package/node_modules/playwright/index.d.ts +17 -0
- package/node_modules/playwright/index.js +17 -0
- package/node_modules/playwright/index.mjs +18 -0
- package/node_modules/playwright/jsx-runtime.js +42 -0
- package/node_modules/playwright/jsx-runtime.mjs +21 -0
- package/node_modules/playwright/lib/agents/agentParser.js +89 -0
- package/node_modules/playwright/lib/agents/copilot-setup-steps.yml +34 -0
- package/node_modules/playwright/lib/agents/generateAgents.js +395 -0
- package/node_modules/playwright/lib/agents/playwright-test-coverage.prompt.md +31 -0
- package/node_modules/playwright/lib/agents/playwright-test-generate.prompt.md +8 -0
- package/node_modules/playwright/lib/agents/playwright-test-generator.agent.md +88 -0
- package/node_modules/playwright/lib/agents/playwright-test-heal.prompt.md +6 -0
- package/node_modules/playwright/lib/agents/playwright-test-healer.agent.md +56 -0
- package/node_modules/playwright/lib/agents/playwright-test-plan.prompt.md +9 -0
- package/node_modules/playwright/lib/agents/playwright-test-planner.agent.md +74 -0
- package/node_modules/playwright/lib/cli/reportActions.js +78 -0
- package/node_modules/playwright/lib/cli/testActions.js +214 -0
- package/node_modules/playwright/lib/common/index.js +2923 -0
- package/node_modules/playwright/lib/common/index.js.txt +34 -0
- package/node_modules/playwright/lib/errorContext.js +129 -0
- package/node_modules/playwright/lib/globals.js +58 -0
- package/node_modules/playwright/lib/index.js +815 -0
- package/node_modules/playwright/lib/isomorphic.js +260 -0
- package/node_modules/playwright/lib/isomorphic.js.txt +9 -0
- package/node_modules/playwright/lib/loader/loaderProcessEntry.js +34 -0
- package/node_modules/playwright/lib/loader/loaderProcessEntry.js.txt +9 -0
- package/node_modules/playwright/lib/matchers/expect.js +13490 -0
- package/node_modules/playwright/lib/matchers/expect.js.LICENSE +640 -0
- package/node_modules/playwright/lib/matchers/expect.js.txt +67 -0
- package/node_modules/playwright/lib/mcp/test/browserBackend.js +135 -0
- package/node_modules/playwright/lib/mcp/test/generatorTools.js +122 -0
- package/node_modules/playwright/lib/mcp/test/plannerTools.js +150 -0
- package/node_modules/playwright/lib/mcp/test/seed.js +82 -0
- package/node_modules/playwright/lib/mcp/test/streams.js +44 -0
- package/node_modules/playwright/lib/mcp/test/testBackend.js +99 -0
- package/node_modules/playwright/lib/mcp/test/testContext.js +306 -0
- package/node_modules/playwright/lib/mcp/test/testTool.js +30 -0
- package/node_modules/playwright/lib/mcp/test/testTools.js +98 -0
- package/node_modules/playwright/lib/package.js +47 -0
- package/node_modules/playwright/lib/program.js +241 -0
- package/node_modules/playwright/lib/runner/index.js +8527 -0
- package/node_modules/playwright/lib/runner/index.js.txt +61 -0
- package/node_modules/playwright/lib/transform/babelBundle.js +72274 -0
- package/node_modules/playwright/lib/transform/babelBundle.js.LICENSE +2384 -0
- package/node_modules/playwright/lib/transform/babelBundle.js.txt +325 -0
- package/node_modules/playwright/lib/transform/esmLoader.js +8054 -0
- package/node_modules/playwright/lib/transform/esmLoader.js.LICENSE +355 -0
- package/node_modules/playwright/lib/transform/esmLoader.js.txt +60 -0
- package/node_modules/playwright/lib/util.js +373 -0
- package/node_modules/playwright/lib/worker/workerProcessEntry.js +1861 -0
- package/node_modules/playwright/lib/worker/workerProcessEntry.js.txt +19 -0
- package/node_modules/playwright/package.json +55 -0
- package/node_modules/playwright/test.d.ts +18 -0
- package/node_modules/playwright/test.js +24 -0
- package/node_modules/playwright/test.mjs +35 -0
- package/node_modules/playwright/types/test.d.ts +11074 -0
- package/node_modules/playwright/types/testReporter.d.ts +950 -0
- package/node_modules/playwright-core/LICENSE +202 -0
- package/node_modules/playwright-core/NOTICE +5 -0
- package/node_modules/playwright-core/README.md +3 -0
- package/node_modules/playwright-core/ThirdPartyNotices.txt +13 -0
- package/node_modules/playwright-core/bin/install_media_pack.ps1 +5 -0
- package/node_modules/playwright-core/bin/install_webkit_wsl.ps1 +33 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh +42 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh +13 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 +24 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh +42 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh +12 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 +24 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh +48 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh +11 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 +23 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh +48 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh +11 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 +23 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh +48 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh +11 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 +24 -0
- package/node_modules/playwright-core/browsers.json +52 -0
- package/node_modules/playwright-core/cli.js +21 -0
- package/node_modules/playwright-core/index.d.ts +17 -0
- package/node_modules/playwright-core/index.js +17 -0
- package/node_modules/playwright-core/index.mjs +28 -0
- package/node_modules/playwright-core/lib/bootstrap.js +88 -0
- package/node_modules/playwright-core/lib/coreBundle.js +75813 -0
- package/node_modules/playwright-core/lib/entry/cliDaemon.js +5 -0
- package/node_modules/playwright-core/lib/entry/dashboardApp.js +3 -0
- package/node_modules/playwright-core/lib/entry/mcp.js +10 -0
- package/node_modules/playwright-core/lib/entry/oopBrowserDownload.js +3 -0
- package/node_modules/playwright-core/lib/package.js +50 -0
- package/node_modules/playwright-core/lib/server/chromium/appIcon.png +0 -0
- package/node_modules/playwright-core/lib/server/electron/loader.js +116 -0
- package/node_modules/playwright-core/lib/serverRegistry.js +1939 -0
- package/node_modules/playwright-core/lib/serverRegistry.js.LICENSE +65 -0
- package/node_modules/playwright-core/lib/tools/cli-client/channelSessions.js +141 -0
- package/node_modules/playwright-core/lib/tools/cli-client/cli.js +6 -0
- package/node_modules/playwright-core/lib/tools/cli-client/help.json +721 -0
- package/node_modules/playwright-core/lib/tools/cli-client/minimist.js +128 -0
- package/node_modules/playwright-core/lib/tools/cli-client/output.js +351 -0
- package/node_modules/playwright-core/lib/tools/cli-client/program.js +418 -0
- package/node_modules/playwright-core/lib/tools/cli-client/registry.js +180 -0
- package/node_modules/playwright-core/lib/tools/cli-client/session.js +259 -0
- package/node_modules/playwright-core/lib/tools/dashboard/appIcon.png +0 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/SKILL.md +425 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/element-attributes.md +23 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/playwright-tests.md +39 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/request-mocking.md +87 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/running-code.md +241 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/session-management.md +225 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/storage-state.md +275 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/test-generation.md +433 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/tracing.md +139 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/video-recording.md +143 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/SKILL.md +136 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +144 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/migration.md +89 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/react.md +69 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/typing.md +173 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/vue.md +77 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-trace/SKILL.md +174 -0
- package/node_modules/playwright-core/lib/tools/utils/extension.js +112 -0
- package/node_modules/playwright-core/lib/tools/utils/socketConnection.js +108 -0
- package/node_modules/playwright-core/lib/utilsBundle.js +85357 -0
- package/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +1890 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/codeMirrorModule--QdMvsKi.css +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/codeMirrorModule-CQ8RZGBm.js +32 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/codicon-DCmgc-ay.ttf +0 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-1bWoP6pv.svg +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-beta-k3eOH_eK.svg +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-nightly-Cp5nfeDT.svg +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-AwgwcYie.css +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-hRvO4yU2.js +12 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/safari-na3_-uQk.svg +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/index.html +29 -0
- package/node_modules/playwright-core/lib/vite/dashboard/playwright-logo.svg +24 -0
- package/node_modules/playwright-core/lib/vite/htmlReport/index.html +16 -0
- package/node_modules/playwright-core/lib/vite/htmlReport/report.css +2 -0
- package/node_modules/playwright-core/lib/vite/htmlReport/report.js +32 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-CVQWJZAA.js +32 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/codicon-DCmgc-ay.ttf +0 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-BZpYJZ-P.css +1 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-hrlLqLtq.js +129 -0
- package/node_modules/playwright-core/lib/vite/recorder/index.html +29 -0
- package/node_modules/playwright-core/lib/vite/recorder/playwright-logo.svg +9 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-BbkfBe3n.js +32 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Ds6CBOo0.js +182 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-DywYcAf8.js +7 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/codicon.DCmgc-ay.ttf +0 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.Bqk9acqE.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.B4cLoZK3.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.html +44 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/manifest.webmanifest +16 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/playwright-logo.svg +9 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html +10 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +4 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CU5KtEkS.js +5 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CyMfwkXJ.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +18 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
- package/node_modules/playwright-core/lib/webp_codec.LICENSE +173 -0
- package/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
- package/node_modules/playwright-core/lib/xdg-open +1267 -0
- package/node_modules/playwright-core/package.json +34 -0
- package/node_modules/playwright-core/types/protocol.d.ts +24645 -0
- package/node_modules/playwright-core/types/structs.d.ts +52 -0
- package/node_modules/playwright-core/types/types.d.ts +26632 -0
- package/package.json +13 -2
- package/plugins/devflow-core/extensions/devflow-core.ts +432 -7
- package/plugins/devflow-core/runtime/hooks/hook_common.py +514 -0
- package/plugins/devflow-core/runtime/hooks/index_gate.py +47 -16
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +488 -71
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +51 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/approval_pins.py +433 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/approval_store.py +137 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_coverage.py +87 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_draft.py +53 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_v1.py +318 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/environment_profile.py +134 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +1 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_surface.py +138 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/playwright_adapter.py +515 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/tc_hash.py +132 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/test_command_policy.py +102 -8
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +13 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +13 -34
- package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +3 -1
- package/plugins/devflow-core/skills/workflow-pipeline/references/pipeline-flow.md +68 -140
- package/plugins/devflow-core/skills/workflow-project-context/scripts/validate_initialization_database.py +1 -1
- package/plugins/devflow-implementation/skills/workflow-execute-phase/references/execute-phase.md +3 -1
- package/plugins/devflow-intel/extensions/devflow-intel.ts +1 -1
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py +23 -4
- package/plugins/devflow-quality/runtime/playwright/devflow-test-dsl-v1.schema.json +872 -0
- package/plugins/devflow-quality/runtime/playwright/playwright.config.mjs +46 -0
- package/plugins/devflow-quality/runtime/playwright/reporter.mjs +311 -0
- package/plugins/devflow-quality/runtime/playwright/runner.spec.mjs +1308 -0
- package/plugins/devflow-quality/runtime/playwright/runtime-manifest.json +11 -0
- package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +3 -0
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/SKILL.md +71 -58
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/api-mainflow-rules.md +20 -15
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/codebase-discovery.md +3 -1
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/intermediate-format.md +115 -192
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/mainflow-selection.md +27 -23
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/project-dsl-contract.md +78 -153
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/ui-mainflow-rules.md +36 -83
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/compile_dsl.py +497 -556
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/export_legacy_testagent.py +647 -0
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/normalize_case_payloads.py +10 -3
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/validate_case_payloads.py +165 -275
- package/plugins/devflow-quality/skills/workflow-human-acceptance/SKILL.md +63 -0
- package/plugins/devflow-quality/skills/workflow-release-closeout/SKILL.md +4 -4
- package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +17 -9
- package/src/approval-store.ts +340 -0
- package/src/cli.ts +10 -0
- package/src/constants.ts +1 -1
- package/src/doctor.ts +268 -2
- package/src/initialization-store.ts +48 -1
- package/src/mcp/auth.ts +103 -0
- package/src/mcp/core.ts +102 -0
- package/src/mcp/server.ts +193 -0
- package/src/mcp/types.ts +5 -0
- package/src/paths.ts +12 -1
- package/src/project-context.ts +24 -1
- package/templates/omp/hook-config.json +6 -0
package/src/doctor.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import { access, lstat, readFile } from "node:fs/promises";
|
|
1
|
+
import { access, lstat, readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
|
|
4
5
|
import { DEVFLOW_PLUGIN_NAMES, OMP_RUNTIME_VERSION, PRODUCT_VERSION } from "./constants.ts";
|
|
5
6
|
import { devflowAgentDir, devflowHome, ompSdkManifestPath, pluginRoots, projectSessionDir } from "./paths.ts";
|
|
6
7
|
import type { ProjectContext } from "./project-context.ts";
|
|
7
8
|
|
|
9
|
+
const RUNTIME_FILES = ["runner.spec.mjs", "playwright.config.mjs", "reporter.mjs", "devflow-test-dsl-v1.schema.json"] as const;
|
|
10
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
11
|
+
const PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
12
|
+
const PROFILE_FIELDS = new Set(["profile", "base_url", "api_base_url", "navigation_origins", "api_origins", "subresources", "secret_env", "trace", "screenshots"]);
|
|
13
|
+
const RESERVED_ENV = new Set([
|
|
14
|
+
"DEVFLOW_DSL_FILE", "DEVFLOW_DSL_SHA256", "DEVFLOW_TC_ID", "DEVFLOW_TC_SHA256", "DEVFLOW_EXECUTION_ID",
|
|
15
|
+
"DEVFLOW_EXECUTION_NONCE", "DEVFLOW_RESULT_FILE", "DEVFLOW_ARTIFACT_DIR", "DEVFLOW_ENVIRONMENT_FILE",
|
|
16
|
+
"PATH", "HOME", "TMPDIR", "LANG", "PLAYWRIGHT_BROWSERS_PATH", "SHELL", "USER", "PWD",
|
|
17
|
+
]);
|
|
18
|
+
const RESERVED_ENV_PREFIXES = ["NODE_", "LD_", "DYLD_", "PLAYWRIGHT_", "PYTHON", "BUN_", "NPM_", "DEVFLOW_HOME", "DEVFLOW_KIT", "DEVFLOW_SOFT"];
|
|
19
|
+
|
|
8
20
|
export interface DoctorCheck {
|
|
9
21
|
id: string;
|
|
10
22
|
ok: boolean;
|
|
@@ -30,11 +42,200 @@ function bunVersionOk(version: string): boolean {
|
|
|
30
42
|
return major > 1 || (major === 1 && (minor > 3 || (minor === 3 && patch >= 14)));
|
|
31
43
|
}
|
|
32
44
|
|
|
33
|
-
|
|
45
|
+
function majorVersion(version: string): number {
|
|
46
|
+
return Number((version.match(/v?(\d+)/) || [])[1] || 0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ProbeResult {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
timedOut: boolean;
|
|
52
|
+
exitCode: number | null;
|
|
53
|
+
stdout: string;
|
|
54
|
+
stderr: string;
|
|
55
|
+
/** Human-readable reason when ok is false. */
|
|
56
|
+
error?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ProbeOptions {
|
|
60
|
+
timeoutMs: number;
|
|
61
|
+
cwd?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Runs one external command; must never throw and must always settle. */
|
|
65
|
+
export type ProbeRunner = (argv: string[], options: ProbeOptions) => Promise<ProbeResult>;
|
|
66
|
+
|
|
67
|
+
export interface DoctorOptions {
|
|
68
|
+
probe?: ProbeRunner;
|
|
69
|
+
/** Hard limits. A probe that exceeds them is reported as a failed check, never a pass. */
|
|
70
|
+
timeouts?: { browserLaunchMs?: number; versionMs?: number };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const DEFAULT_BROWSER_LAUNCH_TIMEOUT_MS = 20_000;
|
|
74
|
+
const DEFAULT_VERSION_TIMEOUT_MS = 10_000;
|
|
75
|
+
const PIPE_DRAIN_MS = 500;
|
|
76
|
+
|
|
77
|
+
function settleWithin<T>(promise: Promise<T>, ms: number, fallback: T): Promise<T> {
|
|
78
|
+
return new Promise(resolve => {
|
|
79
|
+
const timer = setTimeout(() => resolve(fallback), ms);
|
|
80
|
+
promise.then(value => { clearTimeout(timer); resolve(value); }, () => { clearTimeout(timer); resolve(fallback); });
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Async spawn with a hard timeout: the child is SIGKILLed on expiry and the result is marked failed. */
|
|
85
|
+
export const spawnProbe: ProbeRunner = async (argv, options) => {
|
|
86
|
+
let child: ReturnType<typeof Bun.spawn>;
|
|
87
|
+
try {
|
|
88
|
+
child = Bun.spawn(argv, { stdout: "pipe", stderr: "pipe", stdin: "ignore", cwd: options.cwd });
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return { ok: false, timedOut: false, exitCode: null, stdout: "", stderr: "", error: error instanceof Error ? error.message : String(error) };
|
|
91
|
+
}
|
|
92
|
+
let timedOut = false;
|
|
93
|
+
const timer = setTimeout(() => {
|
|
94
|
+
timedOut = true;
|
|
95
|
+
try { child.kill("SIGKILL"); } catch { /* already gone */ }
|
|
96
|
+
}, options.timeoutMs);
|
|
97
|
+
const stdoutText = new Response(child.stdout as ReadableStream).text();
|
|
98
|
+
const stderrText = new Response(child.stderr as ReadableStream).text();
|
|
99
|
+
try {
|
|
100
|
+
const exitCode = await child.exited;
|
|
101
|
+
// A grandchild may keep the pipes open after the kill; never wait on them indefinitely.
|
|
102
|
+
const [stdout, stderr] = await Promise.all([
|
|
103
|
+
settleWithin(stdoutText, timedOut ? PIPE_DRAIN_MS : 5_000, ""),
|
|
104
|
+
settleWithin(stderrText, timedOut ? PIPE_DRAIN_MS : 5_000, ""),
|
|
105
|
+
]);
|
|
106
|
+
if (timedOut) {
|
|
107
|
+
return { ok: false, timedOut: true, exitCode: null, stdout: stdout.trim(), stderr: stderr.trim(), error: `timed out after ${options.timeoutMs} ms and was killed` };
|
|
108
|
+
}
|
|
109
|
+
return { ok: exitCode === 0, timedOut: false, exitCode, stdout: stdout.trim(), stderr: stderr.trim(), error: exitCode === 0 ? undefined : `exit code ${exitCode}` };
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return { ok: false, timedOut, exitCode: null, stdout: "", stderr: "", error: error instanceof Error ? error.message : String(error) };
|
|
112
|
+
} finally {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** Wraps a probe so that even a misbehaving runner cannot block the caller past its deadline. */
|
|
118
|
+
async function guardedProbe(probe: ProbeRunner, argv: string[], options: ProbeOptions): Promise<ProbeResult> {
|
|
119
|
+
const deadline = options.timeoutMs + Math.min(2_000, Math.max(50, options.timeoutMs));
|
|
120
|
+
const fallback: ProbeResult = { ok: false, timedOut: true, exitCode: null, stdout: "", stderr: "", error: `timed out after ${options.timeoutMs} ms` };
|
|
121
|
+
try {
|
|
122
|
+
const result = await settleWithin(probe(argv, options), deadline, fallback);
|
|
123
|
+
// A timed-out probe can never count as a pass, whatever the runner claims.
|
|
124
|
+
return result.timedOut ? { ...result, ok: false } : result;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return { ...fallback, timedOut: false, error: error instanceof Error ? error.message : String(error) };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function probeFailure(result: ProbeResult): string {
|
|
131
|
+
const detail = (result.stderr || result.stdout).split("\n")[0]?.trim();
|
|
132
|
+
return [result.error, detail].filter(Boolean).join(": ") || "probe failed";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function verifyTestRuntime(runtimeRoot: string): Promise<{ ok: boolean; message: string; playwrightVersion?: string }> {
|
|
136
|
+
const manifestPath = path.join(runtimeRoot, "runtime-manifest.json");
|
|
137
|
+
try {
|
|
138
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Record<string, unknown>;
|
|
139
|
+
const files = manifest.files && typeof manifest.files === "object" ? manifest.files as Record<string, unknown> : {};
|
|
140
|
+
const errors: string[] = [];
|
|
141
|
+
if (manifest.schema_version !== 1) errors.push("schema_version must be 1");
|
|
142
|
+
if (typeof manifest.runtime_version !== "string" || !manifest.runtime_version) errors.push("runtime_version is missing");
|
|
143
|
+
if (typeof manifest.playwright_version !== "string" || !/^\d+\.\d+\.\d+$/.test(manifest.playwright_version)) errors.push("playwright_version is invalid");
|
|
144
|
+
for (const name of RUNTIME_FILES) {
|
|
145
|
+
const expected = files[name];
|
|
146
|
+
const target = path.join(runtimeRoot, name);
|
|
147
|
+
const stat = await lstat(target).catch(() => null);
|
|
148
|
+
if (typeof expected !== "string" || !SHA256.test(expected)) errors.push(`manifest does not pin ${name}`);
|
|
149
|
+
else if (!stat?.isFile() || stat.isSymbolicLink()) errors.push(`runtime file is missing or unsafe: ${name}`);
|
|
150
|
+
else if (createHash("sha256").update(await readFile(target)).digest("hex") !== expected) errors.push(`runtime file hash mismatch: ${name}`);
|
|
151
|
+
}
|
|
152
|
+
if (!errors.length) {
|
|
153
|
+
const reporter = await readFile(path.join(runtimeRoot, "reporter.mjs"), "utf8");
|
|
154
|
+
const embedded = /RUNTIME_VERSION\s*=\s*["']([^"']+)["']/.exec(reporter)?.[1];
|
|
155
|
+
if (embedded !== manifest.runtime_version) errors.push(`runtime_version ${String(manifest.runtime_version)} does not match reporter ${String(embedded)}`);
|
|
156
|
+
}
|
|
157
|
+
return { ok: !errors.length, message: errors.length ? errors.join("; ") : manifestPath, playwrightVersion: typeof manifest.playwright_version === "string" ? manifest.playwright_version : undefined };
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return { ok: false, message: `invalid manifest: ${error instanceof Error ? error.message : String(error)}` };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function playwrightVersionMatches(installed: string, manifest: string | undefined): boolean {
|
|
164
|
+
return /^\d+\.\d+\.\d+$/.test(installed) && installed === manifest;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function exactOrigin(value: unknown): string | null {
|
|
168
|
+
if (typeof value !== "string") return null;
|
|
169
|
+
try {
|
|
170
|
+
const url = new URL(value);
|
|
171
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) return null;
|
|
172
|
+
return `${url.protocol}//${url.host}` === value ? value : null;
|
|
173
|
+
} catch { return null; }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function validateEnvironmentProfile(file: string): Promise<string[]> {
|
|
177
|
+
try {
|
|
178
|
+
const stat = await lstat(file);
|
|
179
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return ["profile must be a regular non-symlink file"];
|
|
180
|
+
const profile = JSON.parse(await readFile(file, "utf8")) as Record<string, unknown>;
|
|
181
|
+
const name = path.basename(file, ".json");
|
|
182
|
+
const errors: string[] = [];
|
|
183
|
+
if (!profile || typeof profile !== "object" || Array.isArray(profile)) return ["profile root must be an object"];
|
|
184
|
+
if (!PROFILE_NAME.test(name)) errors.push(`invalid environment profile name: ${name}`);
|
|
185
|
+
for (const key of Object.keys(profile)) if (!PROFILE_FIELDS.has(key)) errors.push(`profile field is not allowed: ${key}`);
|
|
186
|
+
if (profile.profile !== name) errors.push("profile field must equal file name");
|
|
187
|
+
for (const key of ["navigation_origins", "api_origins"] as const) {
|
|
188
|
+
const values = key === "api_origins" && !(key in profile) ? [] : profile[key];
|
|
189
|
+
if (!Array.isArray(values) || (key === "navigation_origins" && !values.length)) errors.push(`${key} must be ${key === "navigation_origins" ? "a non-empty " : "an "}array`);
|
|
190
|
+
else {
|
|
191
|
+
if (values.some(value => !exactOrigin(value))) errors.push(`${key} contains an invalid origin`);
|
|
192
|
+
if (new Set(values).size !== values.length) errors.push(`${key} contains duplicate origins`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const navigation = Array.isArray(profile.navigation_origins) ? profile.navigation_origins : [];
|
|
196
|
+
const api = Array.isArray(profile.api_origins) ? profile.api_origins : [];
|
|
197
|
+
for (const [key, allowed] of [["base_url", navigation], ["api_base_url", api]] as const) {
|
|
198
|
+
if (key === "api_base_url" && !(key in profile)) continue;
|
|
199
|
+
if (typeof profile[key] !== "string") errors.push(`${key} must be an absolute http(s) URL`);
|
|
200
|
+
else {
|
|
201
|
+
try { if (!allowed.includes(new URL(profile[key]).origin)) errors.push(`${key} origin is not declared`); }
|
|
202
|
+
catch { errors.push(`${key} must be an absolute http(s) URL`); }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const subresources = profile.subresources ?? { policy: "same-site-plus-declared", allowed_origins: [] };
|
|
206
|
+
if (!subresources || typeof subresources !== "object" || Array.isArray(subresources)) errors.push("subresources must be an object");
|
|
207
|
+
else {
|
|
208
|
+
const value = subresources as Record<string, unknown>;
|
|
209
|
+
if (Object.keys(value).some(key => !["policy", "allowed_origins"].includes(key))) errors.push("subresources contains an unsupported field");
|
|
210
|
+
if (!["same-site-plus-declared", "declared-only"].includes(String(value.policy))) errors.push("subresources.policy is invalid");
|
|
211
|
+
const allowed = value.allowed_origins;
|
|
212
|
+
if (!Array.isArray(allowed) || allowed.some(origin => !exactOrigin(origin))) errors.push("subresources.allowed_origins contains an invalid origin");
|
|
213
|
+
}
|
|
214
|
+
const secrets = profile.secret_env ?? {};
|
|
215
|
+
if (!secrets || typeof secrets !== "object" || Array.isArray(secrets)) errors.push("secret_env must be an object");
|
|
216
|
+
else for (const [key, value] of Object.entries(secrets)) {
|
|
217
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(key)) errors.push(`secret_env key is invalid: ${key}`);
|
|
218
|
+
if (typeof value !== "string" || !/^[A-Z_][A-Z0-9_]{0,127}$/.test(value)) errors.push(`secret_env.${key} must name an upper-case environment variable`);
|
|
219
|
+
else if (RESERVED_ENV.has(value) || RESERVED_ENV_PREFIXES.some(prefix => value.startsWith(prefix))) errors.push(`secret_env.${key} uses a reserved environment variable`);
|
|
220
|
+
}
|
|
221
|
+
for (const key of ["trace", "screenshots"] as const) if (key in profile && !["on_failure", "off"].includes(String(profile[key]))) errors.push(`${key} is invalid`);
|
|
222
|
+
return errors;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
return [`invalid profile: ${error instanceof Error ? error.message : String(error)}`];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function runHostDoctor(project: ProjectContext, options: DoctorOptions = {}): Promise<HostDoctorReport> {
|
|
34
229
|
const checks: DoctorCheck[] = [];
|
|
35
230
|
const add = (id: string, ok: boolean, message: string) => checks.push({ id, ok, message });
|
|
231
|
+
const probe = options.probe ?? spawnProbe;
|
|
232
|
+
const versionMs = options.timeouts?.versionMs ?? DEFAULT_VERSION_TIMEOUT_MS;
|
|
233
|
+
const browserMs = options.timeouts?.browserLaunchMs ?? DEFAULT_BROWSER_LAUNCH_TIMEOUT_MS;
|
|
36
234
|
add("branch-runtime", true, `devflow-pi ${PRODUCT_VERSION}; OMP SDK ${OMP_RUNTIME_VERSION}`);
|
|
37
235
|
add("bun", bunVersionOk(Bun.version), bunVersionOk(Bun.version) ? Bun.version : `${Bun.version}; requires >=1.3.14`);
|
|
236
|
+
const node = await guardedProbe(probe, ["node", "--version"], { timeoutMs: versionMs });
|
|
237
|
+
const nodeOk = node.ok && majorVersion(node.stdout) >= 20;
|
|
238
|
+
add("node", nodeOk, node.ok ? `${node.stdout}; requires >=20` : `missing or unusable: ${probeFailure(node)}`);
|
|
38
239
|
add("project-root", path.isAbsolute(project.root), project.root);
|
|
39
240
|
add("project-id", Boolean(project.manifest.projectId), project.manifest.projectId);
|
|
40
241
|
const planningStat = await lstat(project.planningRoot).catch(() => null);
|
|
@@ -45,6 +246,62 @@ export async function runHostDoctor(project: ProjectContext): Promise<HostDoctor
|
|
|
45
246
|
const root = roots[index];
|
|
46
247
|
add(`plugin:${plugin}`, await exists(path.join(root, "skills")), root);
|
|
47
248
|
}
|
|
249
|
+
const qualityRoot = roots[DEVFLOW_PLUGIN_NAMES.indexOf("devflow-quality")];
|
|
250
|
+
// Never fall back to a relative path: verifyTestRuntime("") would read ./runtime-manifest.json from the cwd.
|
|
251
|
+
const runtimeRoot = qualityRoot ? path.join(qualityRoot, "runtime", "playwright") : null;
|
|
252
|
+
const runtime = runtimeRoot
|
|
253
|
+
? await verifyTestRuntime(runtimeRoot)
|
|
254
|
+
: { ok: false, message: "devflow-quality plugin root is unknown", playwrightVersion: undefined };
|
|
255
|
+
add("test-runtime-manifest", runtime.ok, runtime.message);
|
|
256
|
+
let installedPlaywright: ProbeResult | null = null;
|
|
257
|
+
if (runtime.ok && runtimeRoot) {
|
|
258
|
+
installedPlaywright = await guardedProbe(
|
|
259
|
+
probe,
|
|
260
|
+
["node", "-e", "process.stdout.write(require(require.resolve('@playwright/test/package.json',{paths:[process.argv[1]]})).version)", runtimeRoot],
|
|
261
|
+
{ timeoutMs: versionMs },
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
const versionOk = Boolean(installedPlaywright?.ok && playwrightVersionMatches(installedPlaywright.stdout, runtime.playwrightVersion));
|
|
265
|
+
add("test-runtime-playwright-version", versionOk,
|
|
266
|
+
!installedPlaywright ? "skipped: Runtime manifest is invalid"
|
|
267
|
+
: !installedPlaywright.ok ? `Playwright version probe failed: ${probeFailure(installedPlaywright)}`
|
|
268
|
+
: `${installedPlaywright.stdout}; manifest ${runtime.playwrightVersion}${versionOk ? "" : " (mismatch)"}`);
|
|
269
|
+
let chromiumOk = false;
|
|
270
|
+
let chromiumMessage = "not checked: Node >=20 and a valid Runtime manifest are required";
|
|
271
|
+
if (nodeOk && runtime.ok && runtimeRoot) {
|
|
272
|
+
const launch = await guardedProbe(
|
|
273
|
+
probe,
|
|
274
|
+
["node", "-e", `const {chromium}=require(require.resolve('@playwright/test',{paths:[process.argv[1]]}));chromium.launch({timeout:${Math.max(1000, browserMs - 2000)}}).then(b=>b.close()).then(()=>process.exit(0),e=>{console.error(e.message);process.exit(1)})`, runtimeRoot],
|
|
275
|
+
{ timeoutMs: browserMs },
|
|
276
|
+
);
|
|
277
|
+
chromiumOk = launch.ok && !launch.timedOut;
|
|
278
|
+
chromiumMessage = chromiumOk ? "launchable"
|
|
279
|
+
: launch.timedOut ? `Chromium launch timed out after ${browserMs} ms and was killed; the browser is not launchable`
|
|
280
|
+
: `Chromium is not launchable: ${probeFailure(launch)}`;
|
|
281
|
+
}
|
|
282
|
+
add("test-runtime-chromium", chromiumOk, chromiumMessage);
|
|
283
|
+
const environmentRoot = path.join(project.planningRoot, "test-environments");
|
|
284
|
+
const environmentStat = await lstat(environmentRoot).catch(() => null);
|
|
285
|
+
let profiles: string[] = [];
|
|
286
|
+
if (environmentStat?.isDirectory() && !environmentStat.isSymbolicLink()) {
|
|
287
|
+
profiles = (await readdir(environmentRoot)).filter(name => name.endsWith(".json") && !name.endsWith(".example.json"));
|
|
288
|
+
}
|
|
289
|
+
const profileErrors: string[] = [];
|
|
290
|
+
for (const profile of profiles) for (const error of await validateEnvironmentProfile(path.join(environmentRoot, profile))) profileErrors.push(`${profile}: ${error}`);
|
|
291
|
+
add("test-environments", profiles.length > 0 && !profileErrors.length,
|
|
292
|
+
!profiles.length ? `${environmentRoot}: configure a profile from local.example.json` : profileErrors.length ? profileErrors.join("; ") : `${environmentRoot}: ${profiles.join(", ")}`);
|
|
293
|
+
const runsRoot = path.join(project.root, ".devflow", "runs");
|
|
294
|
+
const runsStat = await lstat(runsRoot).catch(() => null);
|
|
295
|
+
if (!runsStat?.isDirectory() || runsStat.isSymbolicLink()) {
|
|
296
|
+
add("test-runs-root", false, `${runsRoot}: missing, not a directory, or a symlink`);
|
|
297
|
+
} else {
|
|
298
|
+
// Run directories hold traces, screenshots and result files and must never enter source control.
|
|
299
|
+
const ignored = await guardedProbe(probe, ["git", "-C", project.root, "check-ignore", "-q", "--no-index", ".devflow/runs/doctor-probe/x"], { timeoutMs: versionMs });
|
|
300
|
+
if (ignored.ok) add("test-runs-root", true, `${runsRoot} (git-ignored)`);
|
|
301
|
+
else if (ignored.exitCode === 1) add("test-runs-root", false, `${runsRoot} is not git-ignored; add .devflow/runs/ to .gitignore (run artifacts may contain secrets)`);
|
|
302
|
+
else if (ignored.exitCode === 128 && /not a git repository/i.test(ignored.stderr)) add("test-runs-root", true, `${runsRoot} (project is not a git repository; ignore rule not applicable)`);
|
|
303
|
+
else add("test-runs-root", false, `cannot verify that ${runsRoot} is git-ignored: ${probeFailure(ignored)}`);
|
|
304
|
+
}
|
|
48
305
|
const sdkManifest = ompSdkManifestPath();
|
|
49
306
|
let installedVersion = "missing";
|
|
50
307
|
if (await exists(sdkManifest)) {
|
|
@@ -59,6 +316,15 @@ export async function runHostDoctor(project: ProjectContext): Promise<HostDoctor
|
|
|
59
316
|
add("agent-dir", true, devflowAgentDir());
|
|
60
317
|
add("session-isolation", true, projectSessionDir(project.manifest.projectId));
|
|
61
318
|
add("devflow-home", true, devflowHome());
|
|
319
|
+
const mcpConfig = path.join(devflowHome(), "mcp.json");
|
|
320
|
+
if (!(await exists(mcpConfig))) add("mcp", true, "MCP 未配置");
|
|
321
|
+
else {
|
|
322
|
+
try {
|
|
323
|
+
const config = JSON.parse(await readFile(mcpConfig, "utf8")) as Record<string, unknown>;
|
|
324
|
+
const valid = config.schema_version === 1 && config.clients !== null && typeof config.clients === "object" && !Array.isArray(config.clients);
|
|
325
|
+
add("mcp", valid, valid ? `${mcpConfig} (schema_version 1)` : `${mcpConfig}: schema_version must be 1 and clients must be an object`);
|
|
326
|
+
} catch (error) { add("mcp", false, `${mcpConfig}: invalid JSON: ${error instanceof Error ? error.message : String(error)}`); }
|
|
327
|
+
}
|
|
62
328
|
return { ok: checks.every(check => check.ok), checks };
|
|
63
329
|
}
|
|
64
330
|
|
|
@@ -7,7 +7,7 @@ import { Database } from "bun:sqlite";
|
|
|
7
7
|
import { projectControlDatabasePath, projectDataDir } from "./paths.ts";
|
|
8
8
|
import { DEVFLOW_SCAN_EXCLUDES, PLANNING_DIRECTORY } from "./project-layout.ts";
|
|
9
9
|
|
|
10
|
-
export const INITIALIZATION_SCHEMA_VERSION =
|
|
10
|
+
export const INITIALIZATION_SCHEMA_VERSION = 3;
|
|
11
11
|
export const FACT_KINDS = [
|
|
12
12
|
"asset", "technology", "api", "page", "data_model", "integration",
|
|
13
13
|
"rule", "toolchain", "evidence", "capability",
|
|
@@ -199,6 +199,48 @@ WHEN EXISTS(SELECT 1 FROM initialization_runs r WHERE r.id=OLD.run_id AND r.stat
|
|
|
199
199
|
BEGIN SELECT RAISE(ABORT, 'published facts are immutable'); END;
|
|
200
200
|
`;
|
|
201
201
|
|
|
202
|
+
// Human approval records (schema v3). The column contract is shared with the
|
|
203
|
+
// Python reader side; the triggers below make the records append-only except
|
|
204
|
+
// for the Core-owned proposal lifecycle and approval supersession.
|
|
205
|
+
export const APPROVAL_SCHEMA = `
|
|
206
|
+
CREATE TABLE IF NOT EXISTS approval_proposals(
|
|
207
|
+
id TEXT PRIMARY KEY, project_id TEXT NOT NULL, feature TEXT NOT NULL,
|
|
208
|
+
kind TEXT NOT NULL CHECK(kind IN ('execution_plan','environment_profile','mode_override','human_result')),
|
|
209
|
+
subject_key TEXT NOT NULL, payload_json TEXT NOT NULL, pins_json TEXT NOT NULL,
|
|
210
|
+
state_revision INTEGER NOT NULL, status TEXT NOT NULL CHECK(status IN ('proposed','consumed','rejected','expired')),
|
|
211
|
+
created_at TEXT NOT NULL, resolved_at TEXT);
|
|
212
|
+
CREATE TABLE IF NOT EXISTS approvals(
|
|
213
|
+
id TEXT PRIMARY KEY, proposal_id TEXT NOT NULL UNIQUE REFERENCES approval_proposals(id),
|
|
214
|
+
project_id TEXT NOT NULL, feature TEXT NOT NULL, kind TEXT NOT NULL, subject_key TEXT NOT NULL,
|
|
215
|
+
pins_json TEXT NOT NULL, decision_json TEXT NOT NULL, approved_via TEXT NOT NULL CHECK(approved_via='runtime_ui_confirm'),
|
|
216
|
+
approved_at TEXT NOT NULL, superseded_at TEXT);
|
|
217
|
+
CREATE INDEX IF NOT EXISTS idx_approval_proposals_subject ON approval_proposals(project_id, feature, kind, subject_key, status);
|
|
218
|
+
CREATE INDEX IF NOT EXISTS idx_approvals_subject ON approvals(project_id, feature, kind, subject_key, superseded_at);
|
|
219
|
+
CREATE TRIGGER IF NOT EXISTS approval_proposal_lifecycle BEFORE UPDATE ON approval_proposals
|
|
220
|
+
WHEN OLD.status!='proposed' OR NEW.status='proposed' OR NEW.resolved_at IS NULL
|
|
221
|
+
OR NEW.id!=OLD.id OR NEW.project_id!=OLD.project_id OR NEW.feature!=OLD.feature OR NEW.kind!=OLD.kind
|
|
222
|
+
OR NEW.subject_key!=OLD.subject_key OR NEW.payload_json!=OLD.payload_json OR NEW.pins_json!=OLD.pins_json
|
|
223
|
+
OR NEW.state_revision!=OLD.state_revision OR NEW.created_at!=OLD.created_at
|
|
224
|
+
BEGIN SELECT RAISE(ABORT, 'approval proposals only transition once from proposed'); END;
|
|
225
|
+
CREATE TRIGGER IF NOT EXISTS approval_proposal_delete BEFORE DELETE ON approval_proposals
|
|
226
|
+
BEGIN SELECT RAISE(ABORT, 'approval proposals are append-only'); END;
|
|
227
|
+
CREATE TRIGGER IF NOT EXISTS approval_requires_open_proposal BEFORE INSERT ON approvals
|
|
228
|
+
WHEN NOT EXISTS(
|
|
229
|
+
SELECT 1 FROM approval_proposals p
|
|
230
|
+
WHERE p.id=NEW.proposal_id AND p.status='proposed' AND p.project_id=NEW.project_id AND p.feature=NEW.feature
|
|
231
|
+
AND p.kind=NEW.kind AND p.subject_key=NEW.subject_key AND p.pins_json=NEW.pins_json
|
|
232
|
+
)
|
|
233
|
+
BEGIN SELECT RAISE(ABORT, 'approval does not match an open proposal'); END;
|
|
234
|
+
CREATE TRIGGER IF NOT EXISTS approval_supersede_only BEFORE UPDATE ON approvals
|
|
235
|
+
WHEN OLD.superseded_at IS NOT NULL OR NEW.superseded_at IS NULL
|
|
236
|
+
OR NEW.id!=OLD.id OR NEW.proposal_id!=OLD.proposal_id OR NEW.project_id!=OLD.project_id OR NEW.feature!=OLD.feature
|
|
237
|
+
OR NEW.kind!=OLD.kind OR NEW.subject_key!=OLD.subject_key OR NEW.pins_json!=OLD.pins_json
|
|
238
|
+
OR NEW.decision_json!=OLD.decision_json OR NEW.approved_via!=OLD.approved_via OR NEW.approved_at!=OLD.approved_at
|
|
239
|
+
BEGIN SELECT RAISE(ABORT, 'approvals are immutable except supersession'); END;
|
|
240
|
+
CREATE TRIGGER IF NOT EXISTS approval_delete BEFORE DELETE ON approvals
|
|
241
|
+
BEGIN SELECT RAISE(ABORT, 'approvals are append-only'); END;
|
|
242
|
+
`;
|
|
243
|
+
|
|
202
244
|
function now(): string { return new Date().toISOString(); }
|
|
203
245
|
function canonicalJson(value: unknown): string {
|
|
204
246
|
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
@@ -268,6 +310,7 @@ export class InitializationStore {
|
|
|
268
310
|
}
|
|
269
311
|
const migrate = db.transaction(() => {
|
|
270
312
|
db.exec(SCHEMA);
|
|
313
|
+
db.exec(APPROVAL_SCHEMA);
|
|
271
314
|
const ensureColumn = (table: string, column: string, definition: string) => {
|
|
272
315
|
const columns = db.query(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
|
|
273
316
|
if (!columns.some(item => item.name === column)) db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
@@ -287,6 +330,10 @@ export class InitializationStore {
|
|
|
287
330
|
db.exec("CREATE INDEX IF NOT EXISTS idx_document_jobs_status ON document_jobs(status, updated_at)");
|
|
288
331
|
version = 2;
|
|
289
332
|
}
|
|
333
|
+
if (version < 3) {
|
|
334
|
+
// v3 only adds the approval tables created above; existing rows are untouched.
|
|
335
|
+
version = 3;
|
|
336
|
+
}
|
|
290
337
|
db.query("UPDATE schema_meta SET version=?").run(version);
|
|
291
338
|
}
|
|
292
339
|
const verified = db.query("SELECT version FROM schema_meta LIMIT 1").get() as { version: number };
|
package/src/mcp/auth.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { normalizeDevflowHome } from "../paths.ts";
|
|
6
|
+
import type { McpToolName } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
export const FEATURE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
9
|
+
export const TOOLS = ["validate_feature", "run_feature", "get_feature_report", "dsl_draft_validate"] as const;
|
|
10
|
+
export const MAX_DSL_BYTES = 2 * 1024 * 1024;
|
|
11
|
+
const PERMISSIONS = new Set(["validate", "report", "run", "dsl_draft"]);
|
|
12
|
+
|
|
13
|
+
export type ProjectGrant = { features: string[] | "*"; permissions: string[]; environment_profiles: string[] };
|
|
14
|
+
export type McpConfig = { schema_version: 1; clients: Record<string, { projects: Record<string, ProjectGrant> }> };
|
|
15
|
+
|
|
16
|
+
export class McpError extends Error {
|
|
17
|
+
constructor(public readonly code: string, message: string) { super(message); }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function invalidConfig(message: string): never {
|
|
21
|
+
throw new McpError("mcp_permission_denied", `invalid mcp.json: ${message}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function stringArray(value: unknown, label: string): string[] {
|
|
25
|
+
if (!Array.isArray(value) || value.some(item => typeof item !== "string")) invalidConfig(`${label} must be an array of strings`);
|
|
26
|
+
return value as string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function validateGrant(value: unknown, label: string): ProjectGrant {
|
|
30
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) invalidConfig(`${label} must be an object`);
|
|
31
|
+
const grant = value as Record<string, unknown>;
|
|
32
|
+
const features = grant.features === "*" ? "*" : stringArray(grant.features, `${label}.features`);
|
|
33
|
+
const permissions = stringArray(grant.permissions, `${label}.permissions`);
|
|
34
|
+
if (permissions.some(permission => !PERMISSIONS.has(permission))) invalidConfig(`${label}.permissions contains an unsupported permission`);
|
|
35
|
+
const environmentProfiles = grant.environment_profiles === undefined ? [] : stringArray(grant.environment_profiles, `${label}.environment_profiles`);
|
|
36
|
+
return { features, permissions, environment_profiles: environmentProfiles };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validateConfig(value: unknown): McpConfig {
|
|
40
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) invalidConfig("root must be an object");
|
|
41
|
+
const root = value as Record<string, unknown>;
|
|
42
|
+
if (root.schema_version !== 1) invalidConfig("schema_version must be 1");
|
|
43
|
+
if (!root.clients || typeof root.clients !== "object" || Array.isArray(root.clients)) invalidConfig("clients must be an object");
|
|
44
|
+
const clients: McpConfig["clients"] = {};
|
|
45
|
+
for (const [clientName, clientValue] of Object.entries(root.clients as Record<string, unknown>)) {
|
|
46
|
+
if (!clientValue || typeof clientValue !== "object" || Array.isArray(clientValue)) invalidConfig(`clients.${clientName} must be an object`);
|
|
47
|
+
const projectsValue = (clientValue as Record<string, unknown>).projects;
|
|
48
|
+
if (!projectsValue || typeof projectsValue !== "object" || Array.isArray(projectsValue)) invalidConfig(`clients.${clientName}.projects must be an object`);
|
|
49
|
+
const projects: Record<string, ProjectGrant> = {};
|
|
50
|
+
for (const [project, grant] of Object.entries(projectsValue as Record<string, unknown>)) {
|
|
51
|
+
if (!path.isAbsolute(project)) invalidConfig(`project key must be absolute: ${project}`);
|
|
52
|
+
projects[project] = validateGrant(grant, `clients.${clientName}.projects.${project}`);
|
|
53
|
+
}
|
|
54
|
+
clients[clientName] = { projects };
|
|
55
|
+
}
|
|
56
|
+
return { schema_version: 1, clients };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function loadAuthorization(client: string, home = normalizeDevflowHome(process.env.DEVFLOW_HOME)): Promise<McpConfig> {
|
|
60
|
+
const file = path.join(home, "mcp.json");
|
|
61
|
+
let value: unknown;
|
|
62
|
+
try {
|
|
63
|
+
value = JSON.parse(await readFile(file, "utf8"));
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new McpError("mcp_permission_denied", `MCP authorization is unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
66
|
+
}
|
|
67
|
+
const config = validateConfig(value);
|
|
68
|
+
if (!Object.prototype.hasOwnProperty.call(config.clients, client)) throw new McpError("mcp_permission_denied", `unknown MCP client: ${client}`);
|
|
69
|
+
return config;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function permissionFor(tool: McpToolName): string {
|
|
73
|
+
if (tool === "dsl_draft_validate") return "dsl_draft";
|
|
74
|
+
if (tool === "get_feature_report") return "report";
|
|
75
|
+
if (tool === "validate_feature") return "validate";
|
|
76
|
+
return "run";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function authorize(config: McpConfig, client: string, tool: McpToolName, project: unknown, feature?: unknown): Promise<{ project: string; grant: ProjectGrant }> {
|
|
80
|
+
if (typeof project !== "string" || !path.isAbsolute(project)) throw new McpError("mcp_input_invalid", "project must be an absolute path");
|
|
81
|
+
let resolved: string;
|
|
82
|
+
try { resolved = await realpath(project); } catch { throw new McpError("mcp_project_not_allowed", "project does not exist"); }
|
|
83
|
+
const grants = config.clients[client]?.projects ?? {};
|
|
84
|
+
let grant: ProjectGrant | undefined;
|
|
85
|
+
for (const [key, candidate] of Object.entries(grants)) {
|
|
86
|
+
try {
|
|
87
|
+
if (await realpath(key) === resolved) { grant = candidate; break; }
|
|
88
|
+
} catch {
|
|
89
|
+
// A missing configured project cannot authorize a request.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!grant) throw new McpError("mcp_project_not_allowed", "project is not authorized");
|
|
93
|
+
if (tool !== "dsl_draft_validate") {
|
|
94
|
+
if (typeof feature !== "string" || !FEATURE_RE.test(feature)) throw new McpError("mcp_input_invalid", "feature is invalid");
|
|
95
|
+
if (grant.features !== "*" && !grant.features.includes(feature)) throw new McpError("mcp_feature_not_allowed", "feature is not authorized");
|
|
96
|
+
}
|
|
97
|
+
if (!grant.permissions.includes(permissionFor(tool))) throw new McpError("mcp_permission_denied", "permission is not authorized");
|
|
98
|
+
return { project: resolved, grant };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function requestHash(value: unknown): string {
|
|
102
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
103
|
+
}
|
package/src/mcp/core.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { packageRoot } from "../paths.ts";
|
|
4
|
+
import type { CoreRunner, CoreResult } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export const runnerScript = path.join(packageRoot(), "plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py");
|
|
7
|
+
export const dslScript = path.join(packageRoot(), "plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_draft.py");
|
|
8
|
+
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 7_200_000;
|
|
10
|
+
const TERMINATION_GRACE_MS = 5_000;
|
|
11
|
+
const PROGRESS_INTERVAL_MS = 10_000;
|
|
12
|
+
|
|
13
|
+
function terminateProcessGroup(pid: number | undefined, signal: "SIGTERM" | "SIGKILL"): void {
|
|
14
|
+
if (pid === undefined) return;
|
|
15
|
+
try { process.kill(-pid, signal); } catch { /* process group may already be gone */ }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function settleText(value: Promise<string>, timeoutMs: number): Promise<string> {
|
|
19
|
+
return Promise.race([
|
|
20
|
+
value,
|
|
21
|
+
new Promise<string>(resolve => setTimeout(() => resolve(""), timeoutMs)),
|
|
22
|
+
]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const runPython: CoreRunner = async (script, args, cwd, stdin, timeoutMs = DEFAULT_TIMEOUT_MS, onProgress) => {
|
|
26
|
+
// -E/-s ignore PYTHON* variables and user site-packages. Not -I: isolated mode also drops the
|
|
27
|
+
// script directory from sys.path, and the Core scripts import their sibling modules.
|
|
28
|
+
const child = Bun.spawn(["python3", "-E", "-s", script, ...args], {
|
|
29
|
+
cwd,
|
|
30
|
+
detached: true,
|
|
31
|
+
stdin: "pipe",
|
|
32
|
+
stdout: "pipe",
|
|
33
|
+
stderr: "pipe",
|
|
34
|
+
});
|
|
35
|
+
const stdoutPromise = new Response(child.stdout).text();
|
|
36
|
+
const stderrPromise = new Response(child.stderr).text();
|
|
37
|
+
if (stdin !== undefined) child.stdin.write(stdin);
|
|
38
|
+
child.stdin.end();
|
|
39
|
+
const progressTimer = onProgress === undefined ? undefined : setInterval(() => {
|
|
40
|
+
void Promise.resolve(onProgress()).catch(() => undefined);
|
|
41
|
+
}, PROGRESS_INTERVAL_MS);
|
|
42
|
+
|
|
43
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
44
|
+
let timedOut = false;
|
|
45
|
+
const timeoutPromise = new Promise<"timeout">(resolve => {
|
|
46
|
+
timeoutHandle = setTimeout(() => resolve("timeout"), timeoutMs);
|
|
47
|
+
});
|
|
48
|
+
const exitPromise = child.exited.then(exitCode => ({ exitCode }));
|
|
49
|
+
const winner = await Promise.race([exitPromise, timeoutPromise]);
|
|
50
|
+
if (winner === "timeout") {
|
|
51
|
+
timedOut = true;
|
|
52
|
+
terminateProcessGroup(child.pid, "SIGTERM");
|
|
53
|
+
try { child.kill("SIGTERM"); } catch { /* already exited */ }
|
|
54
|
+
await new Promise(resolve => setTimeout(resolve, TERMINATION_GRACE_MS));
|
|
55
|
+
terminateProcessGroup(child.pid, "SIGKILL");
|
|
56
|
+
try { child.kill("SIGKILL"); } catch { /* already exited */ }
|
|
57
|
+
if (progressTimer !== undefined) clearInterval(progressTimer);
|
|
58
|
+
clearTimeout(timeoutHandle);
|
|
59
|
+
return {
|
|
60
|
+
exitCode: null,
|
|
61
|
+
stdout: await settleText(stdoutPromise, 100),
|
|
62
|
+
stderr: await settleText(stderrPromise, 100),
|
|
63
|
+
timedOut,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (progressTimer !== undefined) clearInterval(progressTimer);
|
|
68
|
+
clearTimeout(timeoutHandle);
|
|
69
|
+
return {
|
|
70
|
+
exitCode: winner.exitCode,
|
|
71
|
+
stdout: await stdoutPromise,
|
|
72
|
+
stderr: await stderrPromise,
|
|
73
|
+
timedOut,
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export async function invokeCore(
|
|
78
|
+
runner: CoreRunner,
|
|
79
|
+
action: string,
|
|
80
|
+
project: string,
|
|
81
|
+
feature: string,
|
|
82
|
+
timeoutMs?: number,
|
|
83
|
+
onProgress?: () => void | Promise<void>,
|
|
84
|
+
): Promise<CoreResult> {
|
|
85
|
+
const result = await runner(
|
|
86
|
+
runnerScript,
|
|
87
|
+
[action, "--feature", feature, "--json", ...(action === "run" ? ["--write"] : [])],
|
|
88
|
+
project,
|
|
89
|
+
undefined,
|
|
90
|
+
timeoutMs,
|
|
91
|
+
onProgress,
|
|
92
|
+
);
|
|
93
|
+
if (result.timedOut) return { ok: false, reason: "mcp_run_timeout", error_code: "mcp_run_timeout" };
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(result.stdout) as CoreResult;
|
|
96
|
+
} catch {
|
|
97
|
+
// Keep the tail of the diagnostic (where Python tracebacks end), without project paths.
|
|
98
|
+
const detail = (result.stderr || result.stdout || `Core exited ${result.exitCode}`).slice(-1000)
|
|
99
|
+
.replace(/(?:[A-Za-z]:)?[^\s,;()"']+[\/][^\s,;()"']+/g, "<path>");
|
|
100
|
+
return { ok: false, reason: "core_failed", error_code: "core_failed", errors: [detail] };
|
|
101
|
+
}
|
|
102
|
+
}
|