create-windy 0.9.1 → 0.10.1
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/cli.js +1691 -1151
- package/package.json +2 -2
- package/template/.agents/skills/windy-business-module/SKILL.md +33 -1
- package/template/.windy-template.json +6 -2
- package/template/AGENTS.md +4 -0
- package/template/README.md +2 -0
- package/template/apps/agent-server/Dockerfile +1 -1
- package/template/apps/agent-server/README.md +1 -1
- package/template/apps/agent-server/native/EXTENSIONS.md +2 -2
- package/template/apps/agent-server/native/parallel-harness.ts +2 -0
- package/template/apps/agent-server/native/storage-lifecycle.test.ts +2 -1
- package/template/apps/agent-server/package.json +42 -41
- package/template/apps/agent-server/plugins/downloads/README.md +1 -1
- package/template/apps/agent-server/plugins/drop-overlay/README.md +1 -1
- package/template/apps/agent-server/plugins/settings/README.md +1 -1
- package/template/apps/agent-server/plugins/skill-library/client-chunks.test.ts +4 -1
- package/template/apps/agent-server/plugins/theme/README.md +5 -1
- package/template/apps/agent-server/plugins/theme/source-tokens.ts +0 -1
- package/template/apps/agent-server/plugins/theme/tokens.json +0 -4
- package/template/apps/agent-server/plugins/tool-catalog/README.md +6 -3
- package/template/apps/agent-server/plugins/tool-catalog/discovery.mjs +13 -10
- package/template/apps/agent-server/plugins/tool-catalog/discovery.test.ts +68 -6
- package/template/apps/agent-server/runtime/compatibility.ts +7 -1
- package/template/apps/agent-server/runtime/dsh-engine.ts +2 -0
- package/template/apps/agent-server/runtime/questions-workflow.nodecheck.ts +2 -0
- package/template/apps/server/Dockerfile +2 -1
- package/template/apps/server/package.json +11 -11
- package/template/apps/server/scripts/validate-module-composition-once.ts +2 -0
- package/template/apps/server/scripts/validate-module-composition.ts +8 -4
- package/template/apps/server/src/admin/routes.test.ts +29 -1
- package/template/apps/server/src/admin/routes.ts +20 -2
- package/template/apps/server/src/agent/client-session/completion.ts +1 -1
- package/template/apps/server/src/agent/client-session/manifest-admission.ts +1 -1
- package/template/apps/server/src/agent/client-session/request.ts +1 -1
- package/template/apps/server/src/agent/context-version-routes.ts +37 -13
- package/template/apps/server/src/agent/run-facade.ts +1 -1
- package/template/apps/server/src/agent/run-input-verification.ts +1 -1
- package/template/apps/server/src/agent/run-routes.ts +1 -14
- package/template/apps/server/src/agent/run-start-preparation.ts +1 -1
- package/template/apps/server/src/agent/runtime-bootstrap-input.ts +2 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +49 -157
- package/template/apps/server/src/agent/runtime-disabled-composition.ts +2 -1
- package/template/apps/server/src/agent/session-invalidation/authorization-revocation.ts +54 -0
- package/template/apps/server/src/agent/session-runtime-composition.ts +1 -51
- package/template/apps/server/src/agent/unavailable-routes.ts +13 -2
- package/template/apps/server/src/agent-workflow/runtime-composition.ts +3 -0
- package/template/apps/server/src/agent-workflow/service.ts +17 -1
- package/template/apps/server/src/ai-artifact/media-resource-route.test.ts +134 -0
- package/template/apps/server/src/ai-artifact/routes.ts +1 -1
- package/template/apps/server/src/ai-artifact/runtime.ts +12 -0
- package/template/apps/server/src/ai-runtime-readiness/operation-port.test-support.ts +1 -1
- package/template/apps/server/src/ai-runtime-readiness/operation-port.ts +8 -0
- package/template/apps/server/src/ai-runtime-security/internal-token.ts +14 -0
- package/template/apps/server/src/app/antivirus-composition.test.ts +63 -0
- package/template/apps/server/src/app/antivirus-composition.ts +29 -0
- package/template/apps/server/src/app/dsh-workbench-socket-audit.test.ts +2 -1
- package/template/apps/server/src/configuration/definition.ts +15 -11
- package/template/apps/server/src/document/facade-support.ts +2 -2
- package/template/apps/server/src/document/facade.ts +2 -2
- package/template/apps/server/src/document/output-artifact-composition.ts +9 -5
- package/template/apps/server/src/document/output-artifact.test-support.ts +1 -1
- package/template/apps/server/src/embedding/facade.ts +1 -1
- package/template/apps/server/src/embedding/input.ts +1 -1
- package/template/apps/server/src/embedding/routes.ts +1 -1
- package/template/apps/server/src/index.ts +60 -60
- package/template/apps/server/src/license/routes.ts +4 -1
- package/template/apps/server/src/license/runtime-business-access.test.ts +261 -0
- package/template/apps/server/src/media-runtime/composition.ts +157 -0
- package/template/apps/server/src/media-runtime/media-only-composition.ts +44 -0
- package/template/apps/server/src/media-runtime/module-ai-port-factory.test.ts +69 -0
- package/template/apps/server/src/media-runtime/module-ai-port-factory.ts +72 -0
- package/template/apps/server/src/{agent → media-runtime}/runtime-readiness-sources.ts +14 -6
- package/template/apps/server/src/media-runtime/shared-composition.ts +104 -0
- package/template/apps/server/src/module-composition.test.ts +22 -0
- package/template/apps/server/src/module-composition.ts +48 -0
- package/template/apps/server/src/module-host/ai-operation-port.ts +97 -30
- package/template/apps/server/src/module-host/ai-operation-speech-normalize.test.ts +1 -1
- package/template/apps/server/src/module-host/http-runtime.ts +9 -3
- package/template/apps/server/src/module-host/static-route-authorization.test.ts +6 -3
- package/template/apps/server/src/module-host/storage-authorization-binding.test.ts +4 -0
- package/template/apps/server/src/module-runtime-validation.ts +2 -0
- package/template/apps/server/src/observability/diagnostic-index.ts +26 -1
- package/template/apps/server/src/observability/diagnostic-repository.ts +40 -11
- package/template/apps/server/src/platform-http-runtime.ts +1 -1
- package/template/apps/server/src/runtime-feature.ts +5 -1
- package/template/apps/server/src/runtime.ts +3 -0
- package/template/apps/server/src/settings/drizzle-watermark-repository.ts +35 -4
- package/template/apps/server/src/settings/watermark-repository.ts +14 -0
- package/template/apps/server/src/settings/watermark-routes.test.ts +84 -2
- package/template/apps/server/src/settings/watermark-routes.ts +109 -0
- package/template/apps/server/src/settings/watermark-transfer.ts +79 -0
- package/template/apps/server/src/speech/facade-validation.ts +1 -1
- package/template/apps/server/src/speech/facade.ts +2 -2
- package/template/apps/server/src/startup-composition-validation.test.ts +32 -0
- package/template/apps/server/src/startup-finalization.ts +2 -0
- package/template/apps/server/src/startup-runtime.ts +77 -0
- package/template/apps/server/src/system/ai-runtime-build.ts +3 -0
- package/template/apps/server/src/system/ai-runtime-configuration.test.ts +36 -0
- package/template/apps/server/src/system/ai-runtime-configuration.ts +58 -0
- package/template/apps/server/src/system/diagnostic-routes.test.ts +45 -0
- package/template/apps/server/src/system/diagnostic-routes.ts +35 -0
- package/template/apps/server/src/system/environment-policy-reader.test.ts +74 -0
- package/template/apps/server/src/system/environment-policy-reader.ts +62 -0
- package/template/apps/server/src/system/feature-environment-assessment.test.ts +148 -0
- package/template/apps/server/src/system/feature-environment-assessment.ts +179 -0
- package/template/apps/server/src/system/host-diagnostics.test.ts +155 -0
- package/template/apps/server/src/system/host-diagnostics.ts +225 -0
- package/template/apps/server/src/system/menu-registry.test.ts +26 -0
- package/template/apps/server/src/system/menu-registry.ts +9 -7
- package/template/apps/server/src/system/operations.test.ts +5 -0
- package/template/apps/server/src/system/operations.ts +36 -12
- package/template/apps/server/src/system/role-authorization-postgres.integration.test.ts +181 -0
- package/template/apps/web/Dockerfile +7 -3
- package/template/apps/web/dev/dev-port.test.ts +6 -6
- package/template/apps/web/dev/dev-port.ts +1 -1
- package/template/apps/web/dev/start.ts +6 -1
- package/template/apps/web/index.html +1 -0
- package/template/apps/web/package.json +16 -16
- package/template/apps/web/runtime-server.test.ts +22 -1
- package/template/apps/web/runtime-server.ts +6 -0
- package/template/apps/web/src/App.vue +6 -3
- package/template/apps/web/src/assets/windy.svg +4 -0
- package/template/apps/web/src/components/auth/LoginForm.vue +2 -2
- package/template/apps/web/src/components/auth/PasswordChangeForm.vue +14 -3
- package/template/apps/web/src/components/common/FormDialog.vue +1 -1
- package/template/apps/web/src/components/license/LicenseFlow.webtest.ts +3 -0
- package/template/apps/web/src/components/license/LicenseMachineCodeQr.vue +99 -0
- package/template/apps/web/src/components/license/LicenseMachineCodeQr.webtest.ts +83 -0
- package/template/apps/web/src/components/license/LicensePairingCard.vue +1 -1
- package/template/apps/web/src/components/license/LicenseStatusCard.vue +14 -2
- package/template/apps/web/src/composables/useLoginFlow.ts +2 -2
- package/template/apps/web/src/composables/usePlatformSettings.ts +1 -1
- package/template/apps/web/src/composables/useWatermarkSettings.ts +1 -0
- package/template/apps/web/src/layout/AppSidebar.vue +5 -9
- package/template/apps/web/src/layout/AppSidebar.webtest.ts +4 -1
- package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +67 -6
- package/template/apps/web/src/pages/auth/LoginPage.vue +3 -12
- package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +30 -0
- package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +25 -14
- package/template/apps/web/src/pages/configuration/components/TimeZoneSelect.vue +55 -0
- package/template/apps/web/src/pages/profile/PersonalSettingsPage.vue +64 -11
- package/template/apps/web/src/pages/profile/PersonalSettingsPage.webtest.ts +21 -1
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +7 -18
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +9 -4
- package/template/apps/web/src/pages/settings/GlobalSettingsPage.vue +51 -6
- package/template/apps/web/src/pages/settings/GlobalSettingsPage.webtest.ts +19 -3
- package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +1 -0
- package/template/apps/web/src/pages/settings/WatermarkYamlTransfer.vue +211 -0
- package/template/apps/web/src/pages/system/DiagnosticLookupPage.vue +18 -3
- package/template/apps/web/src/pages/system/DiagnosticLookupPage.webtest.ts +4 -1
- package/template/apps/web/src/pages/system/SystemOperationsPage.vue +23 -0
- package/template/apps/web/src/pages/system/components/DiagnosticLogList.vue +120 -0
- package/template/apps/web/src/pages/system/components/OperationEnvironmentPanel.vue +237 -0
- package/template/apps/web/src/pages/system/components/OperationFeatureAssessmentPanel.vue +131 -0
- package/template/apps/web/src/pages/system/components/PagedSelectionList.vue +31 -24
- package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +2 -1
- package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +11 -2
- package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +1 -0
- package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +6 -2
- package/template/apps/web/src/pages/system/components/agent-skills/AgentSkillImportDialog.vue +18 -9
- package/template/apps/web/src/pages/system/components/agent-workbench/SessionReplayTimeline.vue +18 -9
- package/template/apps/web/src/pages/system/components/agent-workbench/TestRunComposer.vue +66 -34
- package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.vue +48 -33
- package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.webtest.ts +9 -2
- package/template/apps/web/src/pages/system/components/agent-workbench/ToolRuleDetail.vue +23 -16
- package/template/apps/web/src/pages/system/components/dictionary-bulk/DictionaryBulkJobDetail.vue +10 -7
- package/template/apps/web/src/pages/system/components/user-account/RolePermissionSummary.vue +81 -57
- package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.vue +6 -4
- package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.webtest.ts +20 -4
- package/template/apps/web/src/pages/system/resource-config.ts +9 -0
- package/template/apps/web/src/pages/work-orders/WorkOrderListPage.vue +1 -1
- package/template/apps/web/src/router/disabled-feature-loading.webtest.ts +78 -0
- package/template/apps/web/src/router/index.ts +3 -6
- package/template/apps/web/src/router/manifest-routes.ts +2 -3
- package/template/apps/web/src/router/platform-admin-pages.ts +21 -62
- package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -4
- package/template/apps/web/src/services/diagnostics-api.ts +13 -1
- package/template/apps/web/src/services/operations-api.ts +4 -0
- package/template/apps/web/src/services/platform-settings-api.ts +35 -1
- package/template/apps/web/vite.config.ts +9 -3
- package/template/deploy/environment/windy.environment.example.yaml +53 -0
- package/template/docker-compose.yml +4 -2
- package/template/docs/architecture/agent-admin-workbench-implementation.md +1 -1
- package/template/docs/architecture/agent-admin-workbench-plan.md +1 -1
- package/template/docs/architecture/agent-workflow.md +8 -0
- package/template/docs/architecture/durable-jobs.md +17 -0
- package/template/docs/architecture/embedded-dsh-integration.md +1 -1
- package/template/docs/development/dsh-plugins.md +1 -1
- package/template/docs/development/performance-engineering.md +24 -0
- package/template/docs/operations/admin-settings-yaml.md +29 -0
- package/template/docs/operations/dsh-agent-migration.md +2 -2
- package/template/docs/operations/environment-capabilities.md +185 -0
- package/template/docs/platform/module-configuration.md +18 -3
- package/template/docs/platform/platform-authentication.md +1 -1
- package/template/package.json +10 -8
- package/template/packages/config/package.json +2 -2
- package/template/packages/config/src/definition.ts +14 -4
- package/template/packages/config/src/environment-loader.ts +27 -3
- package/template/packages/config/src/platform-ai-runtime.test.ts +94 -0
- package/template/packages/config/src/platform.ts +5 -2
- package/template/packages/database/drizzle/0065_durable_job_lease_snapshot_lookup.sql +1 -0
- package/template/packages/database/drizzle/0066_agent_source_sequence_bigint.sql +3 -0
- package/template/packages/database/drizzle/0067_media_runtime_access.sql +46 -0
- package/template/packages/database/drizzle/meta/0065_snapshot.json +15370 -0
- package/template/packages/database/drizzle/meta/0066_snapshot.json +15370 -0
- package/template/packages/database/drizzle/meta/0067_snapshot.json +15370 -0
- package/template/packages/database/drizzle/meta/_journal.json +21 -0
- package/template/packages/database/package.json +3 -3
- package/template/packages/database/src/schema/agent-session.ts +4 -3
- package/template/packages/database/src/schema/durable-jobs.ts +4 -0
- package/template/packages/database/src/schema/index.ts +3 -1
- package/template/packages/database/src/schema-durable-recovery.test.ts +21 -1
- package/template/packages/database/src/schema-source-sequence.test.ts +24 -0
- package/template/packages/example-work-order/package.json +2 -2
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/admin-ui-boundary.test.ts +33 -0
- package/template/packages/modules/src/composition.ts +11 -28
- package/template/packages/modules/src/manifest.ts +2 -0
- package/template/packages/modules/src/menu-composition.ts +24 -0
- package/template/packages/modules/src/system-admin-routes.ts +2 -2
- package/template/packages/modules/src/system-admin-ui-catalog.ts +6 -1
- package/template/packages/modules/src/system-ai-api-bindings.ts +0 -16
- package/template/packages/modules/src/system-api-control-plane.ts +9 -1
- package/template/packages/modules/src/system-api-permissions.ts +7 -0
- package/template/packages/modules/src/system-api-watermark.ts +21 -0
- package/template/packages/modules/src/system-audit-actions.ts +3 -1
- package/template/packages/modules/src/system-features.ts +21 -3
- package/template/packages/modules/src/system-media-api-bindings.ts +97 -0
- package/template/packages/modules/src/system-module-audit-selection.ts +2 -0
- package/template/packages/modules/src/system-module-catalog.test.ts +13 -6
- package/template/packages/modules/src/system-module-catalog.ts +6 -241
- package/template/packages/modules/src/system-module-definitions.ts +260 -0
- package/template/packages/modules/src/system-modules-access.test.ts +2 -0
- package/template/packages/modules/src/system-modules.test.ts +7 -1
- package/template/packages/modules/src/system-modules.ts +25 -13
- package/template/packages/modules/src/system-permissions.ts +12 -0
- package/template/packages/server-sdk/package.json +1 -1
- package/template/packages/shared/index.ts +3 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/environment-assessment.ts +68 -0
- package/template/packages/shared/src/environment-policy.ts +205 -0
- package/template/packages/shared/src/license-console-capabilities.ts +7 -0
- package/template/packages/shared/src/license.ts +1 -0
- package/template/packages/shared/src/module-configuration.ts +2 -1
- package/template/scripts/operations/collect-device-info.py +230 -0
- package/template/apps/server/src/agent/module-ai-port-factory.ts +0 -41
- /package/template/apps/server/src/{agent → ai-runtime-security}/execution-grants.ts +0 -0
- /package/template/apps/server/src/{agent → ai-runtime-security}/idempotency-key.test.ts +0 -0
- /package/template/apps/server/src/{agent → ai-runtime-security}/idempotency-key.ts +0 -0
- /package/template/apps/server/src/{agent → ai-runtime-security}/provider-input-governance.test.ts +0 -0
- /package/template/apps/server/src/{agent → ai-runtime-security}/provider-input-governance.ts +0 -0
- /package/template/apps/server/src/{agent → media-runtime}/required-environment.ts +0 -0
- /package/template/apps/server/src/{agent → media-runtime}/runtime-readiness-sources.test.ts +0 -0
|
@@ -2,6 +2,38 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { validateCompositionBeforeStartup } from "./startup-composition-validation.js";
|
|
3
3
|
|
|
4
4
|
describe("完整 Server Composition 启动前门禁", () => {
|
|
5
|
+
test("生产启动的真实装配子进程隔离审计存储且不改变父进程环境", () => {
|
|
6
|
+
const moduleUrl = new URL(
|
|
7
|
+
"./startup-composition-validation.ts",
|
|
8
|
+
import.meta.url,
|
|
9
|
+
);
|
|
10
|
+
const result = Bun.spawnSync({
|
|
11
|
+
cmd: [
|
|
12
|
+
process.execPath,
|
|
13
|
+
"--eval",
|
|
14
|
+
`
|
|
15
|
+
const { validateCompositionBeforeStartup } = await import(${JSON.stringify(moduleUrl.href)});
|
|
16
|
+
validateCompositionBeforeStartup(["bun", "src/index.ts"]);
|
|
17
|
+
if (process.env.NODE_ENV !== "production") throw new Error("parent environment changed");
|
|
18
|
+
if (process.env.DATABASE_URL !== "postgres://unreachable.invalid/validation-must-not-connect")
|
|
19
|
+
throw new Error("parent database configuration changed");
|
|
20
|
+
`,
|
|
21
|
+
],
|
|
22
|
+
env: {
|
|
23
|
+
...process.env,
|
|
24
|
+
NODE_ENV: "production",
|
|
25
|
+
DATABASE_URL:
|
|
26
|
+
"postgres://unreachable.invalid/validation-must-not-connect",
|
|
27
|
+
},
|
|
28
|
+
stdout: "pipe",
|
|
29
|
+
stderr: "pipe",
|
|
30
|
+
});
|
|
31
|
+
expect(new TextDecoder().decode(result.stderr)).not.toContain(
|
|
32
|
+
"AuditWriteUnavailableError",
|
|
33
|
+
);
|
|
34
|
+
expect(result.exitCode).toBe(0);
|
|
35
|
+
}, 15000);
|
|
36
|
+
|
|
5
37
|
test("正常启动先执行无数据库验证子进程", () => {
|
|
6
38
|
let databaseUrlSeen = "not-called";
|
|
7
39
|
validateCompositionBeforeStartup(
|
|
@@ -11,6 +11,7 @@ import { installPlatformShutdown } from "./startup-shutdown.js";
|
|
|
11
11
|
export function finalizePlatformStartup(input: {
|
|
12
12
|
app: Parameters<typeof listenPlatformServer>[0];
|
|
13
13
|
modules: ModuleManifest[];
|
|
14
|
+
disabledFeatureKeys?: ReadonlySet<string>;
|
|
14
15
|
hosts: InitializedModuleHosts;
|
|
15
16
|
tasks?: readonly { key: string }[];
|
|
16
17
|
tools?: ReadonlyMap<string, unknown>;
|
|
@@ -24,6 +25,7 @@ export function finalizePlatformStartup(input: {
|
|
|
24
25
|
}): void {
|
|
25
26
|
validateInstalledRuntime({
|
|
26
27
|
modules: input.modules,
|
|
28
|
+
disabledFeatureKeys: input.disabledFeatureKeys,
|
|
27
29
|
routes: input.app.routes,
|
|
28
30
|
tasks: input.tasks ?? [],
|
|
29
31
|
tools: input.tools ?? new Map(),
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { bootstrapServerModules } from "./module-bootstrap.js";
|
|
2
|
+
import { createComposedAuthRuntime } from "./auth/composition.js";
|
|
3
|
+
import { createServerRuntime } from "./runtime.js";
|
|
4
|
+
import { createHttpObservability } from "./platform-http-runtime.js";
|
|
5
|
+
import { resolveAiRuntimeConfiguration } from "./system/ai-runtime-configuration.js";
|
|
6
|
+
import { createAuthorizationInvalidationComposition } from "./system/authorization-invalidation-composition.js";
|
|
7
|
+
// @windy-module system.license begin
|
|
8
|
+
import { createLicenseRuntimeActivation } from "./license/activation.js";
|
|
9
|
+
// @windy-module system.license end
|
|
10
|
+
|
|
11
|
+
type ModuleBootstrap = Awaited<ReturnType<typeof bootstrapServerModules>>;
|
|
12
|
+
interface StartupRuntimeInput {
|
|
13
|
+
platformConfig: Parameters<typeof bootstrapServerModules>[0];
|
|
14
|
+
persistence: Parameters<typeof bootstrapServerModules>[1];
|
|
15
|
+
logger: Parameters<typeof createHttpObservability>[2];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Bind runtime policy and audit before constructing services or registering routes. */
|
|
19
|
+
export async function createStartupRuntime(
|
|
20
|
+
{ platformConfig, persistence, logger }: StartupRuntimeInput,
|
|
21
|
+
{
|
|
22
|
+
foundationSnapshot,
|
|
23
|
+
systemRepositories,
|
|
24
|
+
}: Pick<ModuleBootstrap, "foundationSnapshot" | "systemRepositories">,
|
|
25
|
+
) {
|
|
26
|
+
const features = foundationSnapshot.features;
|
|
27
|
+
// @windy-module system.license begin
|
|
28
|
+
const licenseActivation = createLicenseRuntimeActivation(
|
|
29
|
+
process.env,
|
|
30
|
+
foundationSnapshot.platform.licenseEnabled,
|
|
31
|
+
persistence?.licenseResolver,
|
|
32
|
+
);
|
|
33
|
+
// @windy-module system.license end
|
|
34
|
+
const { aiRuntimeSwitches, disabledAiFeatures } =
|
|
35
|
+
await resolveAiRuntimeConfiguration(platformConfig.enableAI, features);
|
|
36
|
+
const runtime = createServerRuntime(process.env, {
|
|
37
|
+
actorResolver: persistence?.actorResolver,
|
|
38
|
+
// @windy-module system.license begin
|
|
39
|
+
licenseResolver: licenseActivation.resolver,
|
|
40
|
+
licenseEnforced: licenseActivation.enforced,
|
|
41
|
+
// @windy-module system.license end
|
|
42
|
+
featureResolver: persistence ? systemRepositories.features : undefined,
|
|
43
|
+
featureCatalog: features,
|
|
44
|
+
disabledFeatureKeys: disabledAiFeatures,
|
|
45
|
+
disabledModuleNames: new Set(foundationSnapshot.disabledModuleNames),
|
|
46
|
+
// @windy-module system.audit begin
|
|
47
|
+
auditReader: persistence?.auditReader,
|
|
48
|
+
auditWriter: persistence?.auditWriter,
|
|
49
|
+
// @windy-module system.audit end
|
|
50
|
+
});
|
|
51
|
+
const observability = createHttpObservability(runtime, persistence, logger);
|
|
52
|
+
const authorizationInvalidation = createAuthorizationInvalidationComposition({
|
|
53
|
+
platformConfig,
|
|
54
|
+
persistence,
|
|
55
|
+
repositories: systemRepositories,
|
|
56
|
+
});
|
|
57
|
+
// @windy-module system.license begin
|
|
58
|
+
licenseActivation.runtime.setAuditSink(runtime.auditSink);
|
|
59
|
+
// @windy-module system.license end
|
|
60
|
+
const auth = createComposedAuthRuntime({
|
|
61
|
+
repositories: systemRepositories,
|
|
62
|
+
platformConfig,
|
|
63
|
+
persistence,
|
|
64
|
+
runtime,
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
...auth,
|
|
68
|
+
runtime,
|
|
69
|
+
observability,
|
|
70
|
+
authorizationInvalidation,
|
|
71
|
+
aiRuntimeSwitches,
|
|
72
|
+
disabledAiFeatures,
|
|
73
|
+
// @windy-module system.license begin
|
|
74
|
+
licenseActivation,
|
|
75
|
+
// @windy-module system.license end
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
featureKey,
|
|
4
|
+
parseEnvironmentPolicy,
|
|
5
|
+
type FeatureFlagDefinition,
|
|
6
|
+
} from "@southwind-ai/shared";
|
|
7
|
+
import { applyAiRuntimePolicy } from "./ai-runtime-configuration.js";
|
|
8
|
+
|
|
9
|
+
const document: FeatureFlagDefinition = {
|
|
10
|
+
key: featureKey("system.document"),
|
|
11
|
+
module: "system.media-runtime",
|
|
12
|
+
label: "OCR",
|
|
13
|
+
enabled: true,
|
|
14
|
+
visible: "visible",
|
|
15
|
+
stage: "stable",
|
|
16
|
+
};
|
|
17
|
+
const profile = parseEnvironmentPolicy({
|
|
18
|
+
schemaVersion: 1,
|
|
19
|
+
build: { modules: ["system.media-runtime"] },
|
|
20
|
+
runtime: { agent: false, document: true, speech: false, embedding: false },
|
|
21
|
+
features: [],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("YAML 显式启用 OCR 时拒绝关闭总开关的冲突配置", () => {
|
|
25
|
+
expect(() => applyAiRuntimePolicy(false, [document], profile)).toThrow(
|
|
26
|
+
"PLATFORM_ENABLE_AI_RUNTIME",
|
|
27
|
+
);
|
|
28
|
+
const enabled = applyAiRuntimePolicy(true, [document], profile);
|
|
29
|
+
expect(enabled.aiRuntimeSwitches.document).toBe(true);
|
|
30
|
+
expect(enabled.disabledAiFeatures.has("system.document")).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("缺省预设时保留旧总开关行为", () => {
|
|
34
|
+
const disabled = applyAiRuntimePolicy(false, [document], undefined);
|
|
35
|
+
expect(disabled.disabledAiFeatures.has("system.document")).toBe(true);
|
|
36
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { configureProjectAntivirus } from "../app/antivirus-composition.js";
|
|
2
|
+
import { excludedAiRuntimes } from "./ai-runtime-build.js";
|
|
3
|
+
import {
|
|
4
|
+
AI_RUNTIME_KINDS,
|
|
5
|
+
type FeatureFlagDefinition,
|
|
6
|
+
type EnvironmentPolicy,
|
|
7
|
+
} from "@southwind-ai/shared";
|
|
8
|
+
import { createEnvironmentPolicyReader } from "./environment-policy-reader.js";
|
|
9
|
+
|
|
10
|
+
export async function resolveAiRuntimeConfiguration(
|
|
11
|
+
enableAI: boolean,
|
|
12
|
+
features: readonly FeatureFlagDefinition[],
|
|
13
|
+
) {
|
|
14
|
+
const environmentPolicy = await createEnvironmentPolicyReader()();
|
|
15
|
+
if (environmentPolicy.status === "invalid")
|
|
16
|
+
throw new Error(environmentPolicy.message);
|
|
17
|
+
const policy =
|
|
18
|
+
environmentPolicy.status === "available"
|
|
19
|
+
? environmentPolicy.policy
|
|
20
|
+
: undefined;
|
|
21
|
+
const settings = applyAiRuntimePolicy(enableAI, features, policy);
|
|
22
|
+
await configureProjectAntivirus(policy?.antivirus);
|
|
23
|
+
return settings;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function applyAiRuntimePolicy(
|
|
27
|
+
enableAI: boolean,
|
|
28
|
+
features: readonly FeatureFlagDefinition[],
|
|
29
|
+
policy: EnvironmentPolicy | undefined,
|
|
30
|
+
) {
|
|
31
|
+
const aiRuntimeSwitches = { ...policy?.runtime };
|
|
32
|
+
for (const kind of AI_RUNTIME_KINDS) {
|
|
33
|
+
const omitted =
|
|
34
|
+
excludedAiRuntimes.includes(kind) ||
|
|
35
|
+
!features.some(({ key }) => key === `system.${kind}`);
|
|
36
|
+
if (omitted && aiRuntimeSwitches[kind] === true) {
|
|
37
|
+
throw new Error(`环境预设启用了未随包交付的运行时:${kind}`);
|
|
38
|
+
}
|
|
39
|
+
if (!enableAI && aiRuntimeSwitches[kind] === true) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`环境预设启用了 ${kind},但 PLATFORM_ENABLE_AI_RUNTIME 已关闭`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (omitted || policy?.build.runtimes?.[kind] === false)
|
|
45
|
+
aiRuntimeSwitches[kind] = false;
|
|
46
|
+
}
|
|
47
|
+
const disabledAiFeatures = new Set(
|
|
48
|
+
AI_RUNTIME_KINDS.filter(
|
|
49
|
+
(kind) => !enableAI || aiRuntimeSwitches?.[kind] === false,
|
|
50
|
+
).map((kind) => `system.${kind}`),
|
|
51
|
+
);
|
|
52
|
+
if (
|
|
53
|
+
!enableAI ||
|
|
54
|
+
AI_RUNTIME_KINDS.every((kind) => aiRuntimeSwitches?.[kind] === false)
|
|
55
|
+
)
|
|
56
|
+
disabledAiFeatures.add("system.ai-artifact");
|
|
57
|
+
return { aiRuntimeSwitches, disabledAiFeatures };
|
|
58
|
+
}
|
|
@@ -67,6 +67,51 @@ describe("diagnostic lookup", () => {
|
|
|
67
67
|
expect.objectContaining({ action: "diagnostic.read" }),
|
|
68
68
|
);
|
|
69
69
|
|
|
70
|
+
const list = (query: Record<string, string>, guardContext = admin) =>
|
|
71
|
+
app.invoke("GET", "/system/diagnostics", {
|
|
72
|
+
guardContext,
|
|
73
|
+
params: {},
|
|
74
|
+
query,
|
|
75
|
+
}) as Promise<Response>;
|
|
76
|
+
expect((await list({}, anonymous)).status).toBe(403);
|
|
77
|
+
expect((await list({ pageSize: "0" })).status).toBe(400);
|
|
78
|
+
const older = diagnosticRecord(
|
|
79
|
+
{
|
|
80
|
+
diagnosticId: newDiagnosticId(),
|
|
81
|
+
requestId: "older",
|
|
82
|
+
service: "server",
|
|
83
|
+
stage: "http",
|
|
84
|
+
method: "GET",
|
|
85
|
+
route: "/api/resources",
|
|
86
|
+
statusCode: 503,
|
|
87
|
+
errorCode: "RUNTIME_UNAVAILABLE",
|
|
88
|
+
},
|
|
89
|
+
new Date(Date.now() - 1_000),
|
|
90
|
+
);
|
|
91
|
+
await index.record(older);
|
|
92
|
+
await index.record({
|
|
93
|
+
...older,
|
|
94
|
+
diagnosticId: newDiagnosticId(),
|
|
95
|
+
expiresAt: older.occurredAt,
|
|
96
|
+
});
|
|
97
|
+
const firstPage = await list({ page: "1", pageSize: "1" });
|
|
98
|
+
expect(firstPage.headers.get("cache-control")).toBe("no-store");
|
|
99
|
+
expect(await firstPage.json()).toMatchObject({
|
|
100
|
+
total: 2,
|
|
101
|
+
items: [{ diagnosticId: id }],
|
|
102
|
+
});
|
|
103
|
+
const secondPage = await list({ page: "2", pageSize: "1" });
|
|
104
|
+
expect(await secondPage.json()).toMatchObject({
|
|
105
|
+
total: 2,
|
|
106
|
+
items: [{ requestId: "older" }],
|
|
107
|
+
});
|
|
108
|
+
expect(runtime.auditSink.list()).toContainEqual(
|
|
109
|
+
expect.objectContaining({
|
|
110
|
+
action: "diagnostic.read",
|
|
111
|
+
metadata: expect.objectContaining({ page: 2, pageSize: 1 }),
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
|
|
70
115
|
const expired = (await app.invoke("GET", route, {
|
|
71
116
|
guardContext: admin,
|
|
72
117
|
params: { diagnosticId: "diag-00000000-0000-7000-8000-000000000000" },
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
validDiagnosticId,
|
|
6
6
|
type DiagnosticIndex,
|
|
7
7
|
} from "../observability/diagnostic-index.js";
|
|
8
|
+
import { parsePagination } from "../pagination.js";
|
|
8
9
|
import { findFeature } from "../route-guards.js";
|
|
9
10
|
import { resourceEvent, routeContext } from "./route-helpers.js";
|
|
10
11
|
import { withPermission } from "./resource-route-support.js";
|
|
@@ -18,6 +19,40 @@ export function registerDiagnosticRoutes(
|
|
|
18
19
|
) {
|
|
19
20
|
const feature = findFeature(features, "system.diagnostics");
|
|
20
21
|
if (!feature) throw new Error("错误诊断缺少 system.diagnostics Feature");
|
|
22
|
+
app.get("/system/diagnostics", (raw) => {
|
|
23
|
+
const { query, guardContext } = routeContext(raw);
|
|
24
|
+
return withPermission(
|
|
25
|
+
runtime,
|
|
26
|
+
guardContext,
|
|
27
|
+
"GET",
|
|
28
|
+
"diagnostics",
|
|
29
|
+
"read",
|
|
30
|
+
feature,
|
|
31
|
+
async () => {
|
|
32
|
+
const pagination = parsePagination(query);
|
|
33
|
+
if (pagination instanceof Response) return pagination;
|
|
34
|
+
try {
|
|
35
|
+
await runtime.auditSink.recordCriticalEvent(
|
|
36
|
+
resourceEvent(
|
|
37
|
+
guardContext,
|
|
38
|
+
"diagnostic.read",
|
|
39
|
+
"diagnostic-events",
|
|
40
|
+
undefined,
|
|
41
|
+
{
|
|
42
|
+
page: pagination.page,
|
|
43
|
+
pageSize: pagination.pageSize,
|
|
44
|
+
},
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
return Response.json(await index.list(pagination), {
|
|
48
|
+
headers: { "cache-control": "no-store" },
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
return failure("DIAGNOSTIC_INDEX_UNAVAILABLE", 503);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
});
|
|
21
56
|
app.get("/system/diagnostics/:diagnosticId", (raw) => {
|
|
22
57
|
const { params, guardContext } = routeContext(raw);
|
|
23
58
|
return withPermission(
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { parseEnvironmentPolicy } from "@southwind-ai/shared";
|
|
2
|
+
import { expect, test } from "bun:test";
|
|
3
|
+
import { mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { createEnvironmentPolicyReader } from "./environment-policy-reader.js";
|
|
7
|
+
|
|
8
|
+
test("bounded policy reader rejects invalid, oversized and symlink files without exposing contents", async () => {
|
|
9
|
+
const directory = await mkdtemp(join(tmpdir(), "windy-policy-"));
|
|
10
|
+
try {
|
|
11
|
+
const file = join(directory, "policy.yaml");
|
|
12
|
+
await writeFile(file, "private-secret-input");
|
|
13
|
+
const result = await createEnvironmentPolicyReader(file)();
|
|
14
|
+
expect(result.status).toBe("invalid");
|
|
15
|
+
expect(JSON.stringify(result)).not.toContain("private-secret-input");
|
|
16
|
+
await writeFile(file, " ".repeat(128 * 1024 + 1));
|
|
17
|
+
expect((await createEnvironmentPolicyReader(file)()).status).toBe(
|
|
18
|
+
"invalid",
|
|
19
|
+
);
|
|
20
|
+
await writeFile(
|
|
21
|
+
file,
|
|
22
|
+
"schemaVersion: 1\nbuild: { modules: [] }\nfeatures: []\n",
|
|
23
|
+
);
|
|
24
|
+
expect((await createEnvironmentPolicyReader(file)()).status).toBe(
|
|
25
|
+
"available",
|
|
26
|
+
);
|
|
27
|
+
const link = join(directory, "link");
|
|
28
|
+
await symlink(file, link);
|
|
29
|
+
expect((await createEnvironmentPolicyReader(link)()).status).toBe(
|
|
30
|
+
"invalid",
|
|
31
|
+
);
|
|
32
|
+
expect((await createEnvironmentPolicyReader(directory)()).status).toBe(
|
|
33
|
+
"invalid",
|
|
34
|
+
);
|
|
35
|
+
} finally {
|
|
36
|
+
await rm(directory, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test.skipIf(process.platform === "win32")(
|
|
41
|
+
"FIFO policy paths fail without waiting for a writer",
|
|
42
|
+
async () => {
|
|
43
|
+
const directory = await mkdtemp(join(tmpdir(), "windy-policy-fifo-"));
|
|
44
|
+
try {
|
|
45
|
+
const file = join(directory, "fifo");
|
|
46
|
+
const command = Bun.spawn(["mkfifo", file]);
|
|
47
|
+
expect(await command.exited).toBe(0);
|
|
48
|
+
expect((await createEnvironmentPolicyReader(file)()).status).toBe(
|
|
49
|
+
"invalid",
|
|
50
|
+
);
|
|
51
|
+
} finally {
|
|
52
|
+
await rm(directory, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
2000,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
test("运行时开关保留显式关闭,拒绝未知能力和非布尔值", () => {
|
|
59
|
+
const base = { schemaVersion: 1, build: { modules: [] }, features: [] };
|
|
60
|
+
expect(parseEnvironmentPolicy(base).runtime).toBeUndefined();
|
|
61
|
+
const runtime = {
|
|
62
|
+
agent: false,
|
|
63
|
+
document: true,
|
|
64
|
+
speech: false,
|
|
65
|
+
embedding: false,
|
|
66
|
+
};
|
|
67
|
+
expect(parseEnvironmentPolicy({ ...base, runtime }).runtime).toEqual(runtime);
|
|
68
|
+
expect(() =>
|
|
69
|
+
parseEnvironmentPolicy({ ...base, runtime: { agent: "false" } }),
|
|
70
|
+
).toThrow();
|
|
71
|
+
expect(() =>
|
|
72
|
+
parseEnvironmentPolicy({ ...base, runtime: { typo: true } }),
|
|
73
|
+
).toThrow();
|
|
74
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { open } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
parseEnvironmentPolicy,
|
|
5
|
+
type EnvironmentPolicy,
|
|
6
|
+
} from "@southwind-ai/shared";
|
|
7
|
+
|
|
8
|
+
/** Never return YAML contents or secrets in diagnostics. Reads are bounded even if a file grows. */
|
|
9
|
+
export function createEnvironmentPolicyReader(
|
|
10
|
+
path = process.env.WINDY_ENVIRONMENT_PROFILE_FILE,
|
|
11
|
+
) {
|
|
12
|
+
let cached: { at: number; result: EnvironmentPolicyRead } | undefined;
|
|
13
|
+
return async (): Promise<EnvironmentPolicyRead> => {
|
|
14
|
+
if (!path)
|
|
15
|
+
return {
|
|
16
|
+
status: "not-configured",
|
|
17
|
+
message: "未配置环境预设;不自动推断功能依赖。",
|
|
18
|
+
};
|
|
19
|
+
if (cached && Date.now() - cached.at < 5000) return cached.result;
|
|
20
|
+
let result: EnvironmentPolicyRead;
|
|
21
|
+
try {
|
|
22
|
+
const handle = await open(
|
|
23
|
+
path,
|
|
24
|
+
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
|
|
25
|
+
);
|
|
26
|
+
try {
|
|
27
|
+
const metadata = await handle.stat();
|
|
28
|
+
if (!metadata.isFile() || metadata.size > 128 * 1024)
|
|
29
|
+
throw new Error("invalid file");
|
|
30
|
+
const buffer = Buffer.alloc(128 * 1024 + 1);
|
|
31
|
+
let bytesRead = 0;
|
|
32
|
+
while (bytesRead < buffer.length) {
|
|
33
|
+
const chunk = await handle.read(
|
|
34
|
+
buffer,
|
|
35
|
+
bytesRead,
|
|
36
|
+
buffer.length - bytesRead,
|
|
37
|
+
bytesRead,
|
|
38
|
+
);
|
|
39
|
+
if (chunk.bytesRead === 0) break;
|
|
40
|
+
bytesRead += chunk.bytesRead;
|
|
41
|
+
}
|
|
42
|
+
if (bytesRead > 128 * 1024) throw new Error("oversized");
|
|
43
|
+
const policy = parseEnvironmentPolicy(
|
|
44
|
+
Bun.YAML.parse(buffer.subarray(0, bytesRead).toString("utf8")),
|
|
45
|
+
);
|
|
46
|
+
result = { status: "available", policy };
|
|
47
|
+
} finally {
|
|
48
|
+
await handle.close();
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
result = {
|
|
52
|
+
status: "invalid",
|
|
53
|
+
message: "环境预设不可读、超出大小限制或格式无效;未改变任何功能。",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
cached = { at: Date.now(), result };
|
|
57
|
+
return result;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export type EnvironmentPolicyRead =
|
|
61
|
+
| { status: "available"; policy: EnvironmentPolicy }
|
|
62
|
+
| { status: "not-configured" | "invalid"; message: string };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { featureKey, type FeatureFlagDefinition } from "@southwind-ai/shared";
|
|
3
|
+
import type { EnvironmentSnapshot } from "../../../../packages/shared/src/environment-assessment.js";
|
|
4
|
+
import { parseEnvironmentPolicy } from "../../../../packages/shared/src/environment-policy.js";
|
|
5
|
+
import {
|
|
6
|
+
assessFeatureEnvironment,
|
|
7
|
+
type FeatureAssessmentInput,
|
|
8
|
+
} from "./feature-environment-assessment.js";
|
|
9
|
+
|
|
10
|
+
const feature: FeatureFlagDefinition = {
|
|
11
|
+
key: featureKey("example.ocr"),
|
|
12
|
+
label: "CPU OCR",
|
|
13
|
+
module: "example",
|
|
14
|
+
enabled: true,
|
|
15
|
+
visible: "visible",
|
|
16
|
+
stage: "stable",
|
|
17
|
+
};
|
|
18
|
+
function input(): FeatureAssessmentInput {
|
|
19
|
+
return {
|
|
20
|
+
definitions: [feature],
|
|
21
|
+
resolveEnabled: async () => true,
|
|
22
|
+
environment: {
|
|
23
|
+
generatedAt: new Date().toISOString(),
|
|
24
|
+
process: {
|
|
25
|
+
scope: "process",
|
|
26
|
+
platform: "linux",
|
|
27
|
+
architecture: "arm64",
|
|
28
|
+
nodeVersion: "test",
|
|
29
|
+
containerized: true,
|
|
30
|
+
logicalCpuCount: 2,
|
|
31
|
+
memoryTotalBytes: 1024,
|
|
32
|
+
},
|
|
33
|
+
host: { status: "not-configured", message: "unknown" },
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function policy(
|
|
38
|
+
execution: "cpu" | "local-gpu" | "remote" = "cpu",
|
|
39
|
+
extra: Record<string, unknown> = {},
|
|
40
|
+
) {
|
|
41
|
+
return {
|
|
42
|
+
status: "available" as const,
|
|
43
|
+
policy: parseEnvironmentPolicy({
|
|
44
|
+
schemaVersion: 1,
|
|
45
|
+
build: { modules: ["system"] },
|
|
46
|
+
features: [{ key: "example.ocr", title: "OCR", execution, ...extra }],
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
test("missing host GPU evidence is unknown; CPU OCR is not disabled by missing GPU", async () => {
|
|
51
|
+
expect(
|
|
52
|
+
(await assessFeatureEnvironment(policy("local-gpu"), input())).features[0]
|
|
53
|
+
?.status,
|
|
54
|
+
).toBe("unknown");
|
|
55
|
+
expect(
|
|
56
|
+
(await assessFeatureEnvironment(policy("cpu"), input())).features[0]
|
|
57
|
+
?.status,
|
|
58
|
+
).toBe("available");
|
|
59
|
+
});
|
|
60
|
+
test("software support and missing configuration are distinct from hardware", async () => {
|
|
61
|
+
expect(
|
|
62
|
+
(
|
|
63
|
+
await assessFeatureEnvironment(
|
|
64
|
+
policy("cpu", { supportedArchitectures: ["x64"] }),
|
|
65
|
+
input(),
|
|
66
|
+
)
|
|
67
|
+
).features[0]?.status,
|
|
68
|
+
).toBe("software");
|
|
69
|
+
const result = await assessFeatureEnvironment(
|
|
70
|
+
policy("remote", { requiredEnvironmentKeys: ["MODEL_TOKEN"] }),
|
|
71
|
+
input(),
|
|
72
|
+
);
|
|
73
|
+
expect(result.features[0]?.status).toBe("configuration");
|
|
74
|
+
const configured = {
|
|
75
|
+
...input(),
|
|
76
|
+
environmentValues: { MODEL_TOKEN: "do-not-leak" },
|
|
77
|
+
};
|
|
78
|
+
expect(
|
|
79
|
+
JSON.stringify(
|
|
80
|
+
await assessFeatureEnvironment(
|
|
81
|
+
policy("remote", { requiredEnvironmentKeys: ["MODEL_TOKEN"] }),
|
|
82
|
+
configured,
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
).not.toContain("do-not-leak");
|
|
86
|
+
});
|
|
87
|
+
test("remote execution does not require local GPU or local model memory", async () => {
|
|
88
|
+
const result = await assessFeatureEnvironment(
|
|
89
|
+
policy("remote", { minMemoryGiB: 64 }),
|
|
90
|
+
input(),
|
|
91
|
+
);
|
|
92
|
+
expect(result.features[0]?.status).toBe("available");
|
|
93
|
+
});
|
|
94
|
+
test("valid insufficient host memory is hardware; stale evidence cannot reject hardware", async () => {
|
|
95
|
+
const value = input();
|
|
96
|
+
value.environment.host = {
|
|
97
|
+
status: "available",
|
|
98
|
+
message: "fixture",
|
|
99
|
+
report: {
|
|
100
|
+
collectedAt: new Date().toISOString(),
|
|
101
|
+
facts: { memoryTotalBytes: 1024 ** 3 },
|
|
102
|
+
} as NonNullable<EnvironmentSnapshot["host"]["report"]>,
|
|
103
|
+
};
|
|
104
|
+
expect(
|
|
105
|
+
(await assessFeatureEnvironment(policy("cpu", { minMemoryGiB: 8 }), value))
|
|
106
|
+
.features[0]?.status,
|
|
107
|
+
).toBe("hardware");
|
|
108
|
+
value.environment.host.status = "stale";
|
|
109
|
+
expect(
|
|
110
|
+
(await assessFeatureEnvironment(policy("cpu", { minMemoryGiB: 8 }), value))
|
|
111
|
+
.features[0]?.status,
|
|
112
|
+
).toBe("unknown");
|
|
113
|
+
});
|
|
114
|
+
test("package absence, explicit disable and resolver failure retain distinct states", async () => {
|
|
115
|
+
expect(
|
|
116
|
+
(await assessFeatureEnvironment(policy(), { ...input(), definitions: [] }))
|
|
117
|
+
.features[0]?.status,
|
|
118
|
+
).toBe("not-packaged");
|
|
119
|
+
expect(
|
|
120
|
+
(
|
|
121
|
+
await assessFeatureEnvironment(policy(), {
|
|
122
|
+
...input(),
|
|
123
|
+
resolveEnabled: async () => false,
|
|
124
|
+
})
|
|
125
|
+
).features[0]?.status,
|
|
126
|
+
).toBe("disabled");
|
|
127
|
+
expect(
|
|
128
|
+
(
|
|
129
|
+
await assessFeatureEnvironment(policy(), {
|
|
130
|
+
...input(),
|
|
131
|
+
resolveEnabled: async () => {
|
|
132
|
+
throw new Error("private");
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
).features[0]?.status,
|
|
136
|
+
).toBe("unknown");
|
|
137
|
+
});
|
|
138
|
+
test("unknown policy fields, duplicate feature keys and invalid booleans are rejected", () => {
|
|
139
|
+
expect(() => policy("cpu", { recommendedEnabled: "false" })).toThrow();
|
|
140
|
+
expect(() => policy("cpu", { secretlyEnable: true })).toThrow();
|
|
141
|
+
const parsed = policy().policy;
|
|
142
|
+
expect(() =>
|
|
143
|
+
parseEnvironmentPolicy({
|
|
144
|
+
...parsed,
|
|
145
|
+
features: [...parsed.features, ...parsed.features],
|
|
146
|
+
}),
|
|
147
|
+
).toThrow();
|
|
148
|
+
});
|