create-windy 0.6.17 → 0.6.19

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.
Files changed (32) hide show
  1. package/dist/cli.js +98 -21
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/README.md +5 -2
  5. package/template/apps/agent-server/README.md +6 -0
  6. package/template/apps/agent-server/src/southwind_agent_server/app.py +23 -0
  7. package/template/apps/agent-server/src/southwind_agent_server/health_routes.py +9 -0
  8. package/template/apps/agent-server/src/southwind_agent_server/runtime_compatibility.py +89 -0
  9. package/template/apps/agent-server/tests/test_runtime_compatibility.py +119 -0
  10. package/template/apps/server/src/agent/remote-runtime-compatibility.test.ts +147 -0
  11. package/template/apps/server/src/agent/remote-runtime.ts +66 -4
  12. package/template/apps/server/src/agent/runtime-bootstrap.ts +1 -0
  13. package/template/apps/web/src/components/common/FormDialog.vue +1 -0
  14. package/template/apps/web/src/components/common/ModalLayout.webtest.ts +1 -0
  15. package/template/apps/web/src/components/ui/dialog/DialogContent.vue +1 -1
  16. package/template/apps/web/src/components/ui/dialog/DialogScrollContent.vue +1 -1
  17. package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +1 -0
  18. package/template/apps/web/src/pages/system/components/user-account/CreateDepartmentDialog.vue +182 -0
  19. package/template/apps/web/src/pages/system/components/user-account/CreateDepartmentDialog.webtest.ts +93 -0
  20. package/template/apps/web/src/pages/system/components/user-account/RolePermissionSummary.vue +59 -0
  21. package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +21 -71
  22. package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.webtest.ts +71 -0
  23. package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.vue +48 -27
  24. package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.webtest.ts +30 -1
  25. package/template/docs/architecture/ai-runtime.md +8 -15
  26. package/template/docs/platform/agent-runtime.md +5 -0
  27. package/template/packages/server-sdk/contracts/agent-runtime-compatibility/v1/offer.json +22 -0
  28. package/template/packages/server-sdk/index.ts +19 -0
  29. package/template/packages/server-sdk/package.json +2 -0
  30. package/template/packages/server-sdk/src/agent-runtime-compatibility.contract.test.ts +143 -0
  31. package/template/packages/server-sdk/src/agent-runtime-compatibility.public-api.test.ts +19 -0
  32. package/template/packages/server-sdk/src/agent-runtime-compatibility.ts +284 -0
package/dist/cli.js CHANGED
@@ -3921,6 +3921,8 @@ bun run build
3921
3921
  bun run windy doctor
3922
3922
  bun run windy update --check
3923
3923
  bun run windy update --dry-run
3924
+ bun run windy update
3925
+ # \u5982\u9700\u5728\u672C\u6B21\u66F4\u65B0\u4E2D\u663E\u5F0F\u6267\u884C\u9879\u76EE\u8D28\u91CF\u811A\u672C\uFF1Abun run windy update --verify
3924
3926
  bun run windy module list
3925
3927
  ${moduleActivationCommands}
3926
3928
  ${includeOxc ? `bun run lint
@@ -3931,11 +3933,12 @@ ${includeOxc ? "\u63D0\u4EA4\u65F6 Husky \u4F1A\u901A\u8FC7 lint-staged \u4E0E O
3931
3933
 
3932
3934
  \u66F4\u65B0\u524D\u65E0\u9700\u63D0\u4EA4\u3001\u6682\u5B58\u6216\u6E05\u7406\u4E1A\u52A1\u6539\u52A8\u3002\`--dry-run\` \u53EA\u751F\u6210\u8BA1\u5212\uFF0C\u4E0D\u5199\u9879\u76EE\uFF1B\u65E0\u5173 WIP
3933
3935
  \u4FDD\u6301\u539F\u6837\uFF0C\u547D\u4E2D\u540C\u4E00\u8DEF\u5F84\u65F6\u6309\u51BB\u7ED3\u5FEB\u7167\u4E0E\u4E09\u65B9\u5408\u5E76 fail closed\u3002\u6B63\u5F0F\u66F4\u65B0\u5148\u5907\u4EFD\uFF0C\u518D\u505A
3934
- package \u5B57\u6BB5\u7EA7\u5408\u5E76\u3001\u5171\u4EAB\u811A\u624B\u67B6\u4E09\u65B9\u5408\u5E76\u4E0E\u5B8C\u6574\u9A8C\u8BC1\u3002\u6574\u4E2A \`apps/web/src\` \u548C
3936
+ package \u5B57\u6BB5\u7EA7\u5408\u5E76\u3001\u5171\u4EAB\u811A\u624B\u67B6\u4E09\u65B9\u5408\u5E76\u4E0E\u5B89\u5168\u6536\u53E3\uFF1B\u9ED8\u8BA4\u4E0D\u6267\u884C\u9879\u76EE lint\u3001typecheck\u3001
3937
+ test \u6216 build\uFF0C\u4F20\u5165 \`--verify\` \u624D\u663E\u5F0F\u6267\u884C\u5E76\u8BB0\u5F55\u7ED3\u679C\u3002\u6574\u4E2A \`apps/web/src\` \u548C
3935
3938
  \`apps/server/src/app\` \u9ED8\u8BA4\u4FDD\u6301\u9879\u76EE\u7248\u672C\uFF1B\u5DF2\u767B\u8BB0\u7248\u672C recipe \u53EA\u4FEE\u6539\u5176\u58F0\u660E\u7684\u7A84\u951A\u70B9\u3002
3936
3939
  \u4EFB\u4F55\u51B2\u7A81\u90FD\u4F1A\u5728\u5199\u5165\u524D\u4E2D\u6B62\uFF0C\u6A21\u5757\u542F\u505C\u5728\u4EA4\u4E92
3937
3940
  \u7EC8\u7AEF\u786E\u8BA4\uFF1BCI\u3001Agent \u6216\u5176\u5B83\u975E TTY \u73AF\u5883\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 \`--yes\`\u3002
3938
- \u65E0\u5E76\u53D1\u53D8\u5316\u7684\u9A8C\u8BC1\u5931\u8D25\u4F1A\u81EA\u52A8\u56DE\u6EDA\uFF1B\u4E2D\u65AD\u6062\u590D\u4F7F\u7528
3941
+ \u5B89\u5168\u68C0\u67E5\u5931\u8D25\u4F1A\u81EA\u52A8\u56DE\u6EDA\uFF1B\u9879\u76EE\u8D28\u91CF\u811A\u672C\u5931\u8D25\u672C\u8EAB\u4E0D\u56DE\u6EDA\u5DF2\u5B89\u5168\u5E94\u7528\u7684\u66F4\u65B0\u3002\u4E2D\u65AD\u6062\u590D\u4F7F\u7528
3939
3942
  \`bun run windy doctor --repair\`\u3002\u8BE5\u547D\u4EE4\u5728\u63D0\u4EA4\u70B9\u524D\u4FDD\u7559\u5E76\u53D1\u7F16\u8F91\u5E76\u56DE\u6EDA\u5176\u4F59\u8DEF\u5F84\uFF0C
3940
3943
  \u5728 committed \u63D0\u4EA4\u70B9\u540E\u53EA\u5B8C\u6210\u4E8B\u52A1\u72B6\u6001\u4E0E\u5907\u4EFD\u6E05\u7406\uFF0C\u4E0D\u56DE\u6EDA\u9879\u76EE\u6587\u4EF6\u3002
3941
3944
 
@@ -8487,6 +8490,16 @@ var recipes = [
8487
8490
  id: "starter-0.6.16-to-0.6.17",
8488
8491
  from: "0.6.16",
8489
8492
  to: "0.6.17"
8493
+ },
8494
+ {
8495
+ id: "starter-0.6.17-to-0.6.18",
8496
+ from: "0.6.17",
8497
+ to: "0.6.18"
8498
+ },
8499
+ {
8500
+ id: "starter-0.6.18-to-0.6.19",
8501
+ from: "0.6.18",
8502
+ to: "0.6.19"
8490
8503
  }
8491
8504
  ];
8492
8505
  function resolveRecipeChain(sourceVersion, targetVersion) {
@@ -8532,8 +8545,7 @@ function parseVersion2(version) {
8532
8545
  }
8533
8546
 
8534
8547
  // src/update/updater.ts
8535
- import { readFile as readFile24, rm as rm8 } from "fs/promises";
8536
- import { join as join29 } from "path";
8548
+ import { rm as rm8 } from "fs/promises";
8537
8549
 
8538
8550
  // src/update/artifacts.ts
8539
8551
  import { mkdtemp, readFile as readFile15 } from "fs/promises";
@@ -10854,12 +10866,48 @@ function safeTemporaryPath(root, path2) {
10854
10866
  return target;
10855
10867
  }
10856
10868
 
10869
+ // src/update/project-quality-verification.ts
10870
+ import { readFile as readFile24 } from "fs/promises";
10871
+ import { join as join29 } from "path";
10872
+ var qualityScriptNames = ["lint", "typecheck", "test", "build"];
10873
+ function skippedProjectQualityVerification() {
10874
+ return {
10875
+ status: "skipped",
10876
+ steps: [
10877
+ {
10878
+ name: "\u9879\u76EE\u8D28\u91CF\u9A8C\u8BC1\uFF08lint/typecheck/test/build\uFF09",
10879
+ status: "skipped"
10880
+ }
10881
+ ]
10882
+ };
10883
+ }
10884
+ async function runProjectQualityVerification(projectDirectory, execute) {
10885
+ const packageJson = JSON.parse(await readFile24(join29(projectDirectory, "package.json"), "utf8"));
10886
+ const steps = [];
10887
+ for (const name of qualityScriptNames) {
10888
+ const stepName = `bun run ${name}`;
10889
+ if (!packageJson.scripts?.[name]) {
10890
+ steps.push({ name: stepName, status: "skipped" });
10891
+ continue;
10892
+ }
10893
+ try {
10894
+ await execute("bun", ["run", name], projectDirectory);
10895
+ steps.push({ name: stepName, status: "passed" });
10896
+ } catch {
10897
+ steps.push({ name: stepName, status: "failed" });
10898
+ return { status: "failed", steps };
10899
+ }
10900
+ }
10901
+ return { status: "passed", steps };
10902
+ }
10903
+
10857
10904
  // src/update/updater-report.ts
10858
- async function writeUpdateReport(result2, steps, relativePath2, expected) {
10905
+ async function writeUpdateReport(result2, steps, projectQualityVerification, relativePath2, expected) {
10859
10906
  const lines = [
10860
10907
  `# Windy \u66F4\u65B0\u62A5\u544A ${result2.plan.id}`,
10861
10908
  "",
10862
- "- \u9A8C\u8BC1\u72B6\u6001\uFF1A\u5DF2\u901A\u8FC7\uFF1B\u4E8B\u52A1\u7ED3\u679C\u4EE5 update-state \u6700\u7EC8\u63D0\u4EA4\u4E3A\u51C6",
10909
+ "- \u5B89\u5168\u63D0\u4EA4\u72B6\u6001\uFF1A\u5DF2\u901A\u8FC7\uFF1B\u4E8B\u52A1\u7ED3\u679C\u4EE5 update-state \u6700\u7EC8\u63D0\u4EA4\u4E3A\u51C6",
10910
+ `- \u9879\u76EE\u8D28\u91CF\u9A8C\u8BC1\uFF1A${qualityStatusLabel(projectQualityVerification)}`,
10863
10911
  `- \u6765\u6E90\u7248\u672C\uFF1A${result2.plan.sourceVersion}`,
10864
10912
  `- \u76EE\u6807\u7248\u672C\uFF1A${result2.plan.targetVersion}`,
10865
10913
  `- Recipe\uFF1A${result2.plan.recipes.join("\u3001") || "\u65E0"}`,
@@ -10878,10 +10926,14 @@ async function writeUpdateReport(result2, steps, relativePath2, expected) {
10878
10926
  ...result2.plan.manualCleanup.map(({ message }) => `- ${message}`)
10879
10927
  ] : [],
10880
10928
  "",
10881
- "## \u9A8C\u8BC1",
10929
+ "## \u5B89\u5168\u68C0\u67E5\u4E0E\u53EF\u9009\u9A8C\u8BC1",
10882
10930
  "",
10883
10931
  ...steps.map(({ name, status }) => `- ${name}\uFF1A${status}`),
10884
10932
  "",
10933
+ ...projectQualityVerification === "skipped" ? [
10934
+ "\u672C\u6B21\u66F4\u65B0\u672A\u6267\u884C\u9879\u76EE\u7EA7 lint/typecheck/test/build\u3002\u9700\u8981\u5B8C\u6574\u8D28\u91CF\u9A8C\u8BC1\u65F6\uFF0C\u8BF7\u5728\u9879\u76EE\u6839\u76EE\u5F55\u6309\u5B9E\u9645\u5B58\u5728\u7684\u811A\u672C\u663E\u5F0F\u8FD0\u884C\u8FD9\u4E9B\u547D\u4EE4\u3002",
10935
+ ""
10936
+ ] : [],
10885
10937
  "\u4E8B\u52A1\u63D0\u4EA4\u540E\u81EA\u52A8\u6E05\u7406\u5907\u4EFD\uFF1B\u82E5\u63D0\u793A\u6E05\u7406\u5F85\u5B8C\u6210\uFF0C\u8BF7\u8FD0\u884C `bun run windy doctor --repair`\u3002",
10886
10938
  ""
10887
10939
  ];
@@ -10890,6 +10942,13 @@ async function writeUpdateReport(result2, steps, relativePath2, expected) {
10890
10942
  await writeOwnedTransactionFile(result2.plan.projectDirectory, relativePath2, content, expected);
10891
10943
  return expectationForContent(relativePath2, content);
10892
10944
  }
10945
+ function qualityStatusLabel(status) {
10946
+ if (status === "passed")
10947
+ return "\u5DF2\u901A\u8FC7\uFF08\u663E\u5F0F --verify\uFF09";
10948
+ if (status === "failed")
10949
+ return "\u672A\u901A\u8FC7\uFF1B\u5347\u7EA7\u4ECD\u6309\u5B89\u5168\u68C0\u67E5\u7ED3\u679C\u63D0\u4EA4";
10950
+ return "\u5DF2\u8DF3\u8FC7\uFF08\u9ED8\u8BA4\u4E0D\u6267\u884C lint/typecheck/test/build\uFF09";
10951
+ }
10893
10952
 
10894
10953
  // src/update/updater.ts
10895
10954
  class DefaultStarterUpdater {
@@ -10898,12 +10957,14 @@ class DefaultStarterUpdater {
10898
10957
  #beforeTransactionFinalize;
10899
10958
  #finalizeTransaction;
10900
10959
  #skipInstall;
10960
+ #verifyProject;
10901
10961
  constructor(dependencies = {}) {
10902
10962
  this.#artifacts = dependencies.artifacts ?? new PublishedArtifactProvider;
10903
10963
  this.#execute = dependencies.execute ?? runCommand;
10904
10964
  this.#beforeTransactionFinalize = dependencies.beforeTransactionFinalize;
10905
10965
  this.#finalizeTransaction = dependencies.finalizeTransaction ?? finalizeOwnedTransaction;
10906
10966
  this.#skipInstall = dependencies.skipInstall ?? false;
10967
+ this.#verifyProject = dependencies.verifyProject ?? false;
10907
10968
  }
10908
10969
  async plan(input) {
10909
10970
  return createUpdatePlan(input, this.#artifacts);
@@ -10974,18 +11035,11 @@ class DefaultStarterUpdater {
10974
11035
  const plannedPaths = new Set(result2.plan.files.map(({ path: path2 }) => path2));
10975
11036
  replaceExpectations(result2, verificationSnapshot.filter(({ path: path2 }) => !metadataPaths2.has(path2) && !plannedPaths.has(path2)));
10976
11037
  verificationExpectationsFrozen = true;
10977
- const packageJson = JSON.parse(await readFile24(join29(result2.plan.projectDirectory, "package.json"), "utf8"));
10978
- for (const name of ["lint", "typecheck", "test", "build"]) {
10979
- if (!packageJson.scripts?.[name]) {
10980
- steps.push({ name: `bun run ${name}`, status: "skipped" });
10981
- continue;
10982
- }
10983
- await this.#execute("bun", ["run", name], result2.plan.projectDirectory);
10984
- steps.push({ name: `bun run ${name}`, status: "passed" });
10985
- }
11038
+ const projectQualityVerification = this.#verifyProject ? await runProjectQualityVerification(result2.plan.projectDirectory, this.#execute) : skippedProjectQualityVerification();
11039
+ steps.push(...projectQualityVerification.steps);
10986
11040
  const metadataExpectations = await finalize(result2, stabilized);
10987
11041
  const reportPath = `.windy/reports/${result2.plan.id}.md`;
10988
- const reportExpectation = await writeUpdateReport(result2, steps, reportPath, expectedForPath(result2, reportPath));
11042
+ const reportExpectation = await writeUpdateReport(result2, steps, projectQualityVerification.status, reportPath, expectedForPath(result2, reportPath));
10989
11043
  replaceExpectations(result2, [...metadataExpectations, reportExpectation]);
10990
11044
  await rm8(result2.plan.temporaryRoot, { recursive: true, force: true });
10991
11045
  await this.#beforeTransactionFinalize?.({ result: result2, reportPath });
@@ -10995,7 +11049,13 @@ class DefaultStarterUpdater {
10995
11049
  expectations: result2.finalizationExpectations,
10996
11050
  workspaceMembershipExpectations: result2.plan.finalWorkspaceMembershipExpectations ?? result2.plan.workspaceMembershipExpectations
10997
11051
  });
10998
- return { updateId: result2.plan.id, status: "passed", steps, reportPath };
11052
+ return {
11053
+ updateId: result2.plan.id,
11054
+ status: "passed",
11055
+ projectQualityVerification: projectQualityVerification.status,
11056
+ steps,
11057
+ reportPath
11058
+ };
10999
11059
  } catch (error) {
11000
11060
  await rm8(result2.plan.temporaryRoot, { recursive: true, force: true });
11001
11061
  if (error instanceof TransactionCommittedCleanupPendingError) {
@@ -11008,7 +11068,7 @@ class DefaultStarterUpdater {
11008
11068
  if (verificationExpectationsFrozen) {
11009
11069
  const conflicts = await retainOwnedTransactionExpectationConflicts(result2.plan.projectDirectory, result2.transactionOwnerToken, result2.finalizationExpectations);
11010
11070
  if (conflicts.length) {
11011
- throw new Error(`\u66F4\u65B0\u9A8C\u8BC1\u671F\u95F4\u68C0\u6D4B\u5230\u5E76\u53D1\u4FEE\u6539\uFF0C\u4E8B\u52A1\u73B0\u573A\u5DF2\u4FDD\u7559\uFF1A${conflicts.join("\u3001")}`, { cause: error });
11071
+ throw new Error(`\u66F4\u65B0\u5B89\u5168\u6536\u53E3\u671F\u95F4\u68C0\u6D4B\u5230\u5E76\u53D1\u4FEE\u6539\uFF0C\u4E8B\u52A1\u73B0\u573A\u5DF2\u4FDD\u7559\uFF1A${conflicts.join("\u3001")}`, { cause: error });
11012
11072
  }
11013
11073
  }
11014
11074
  await restoreUpdateState(result2.plan.projectDirectory, {
@@ -11020,7 +11080,7 @@ class DefaultStarterUpdater {
11020
11080
  phase: "ready",
11021
11081
  manifestHash: result2.expectedManifestHash
11022
11082
  });
11023
- throw new Error(`\u66F4\u65B0\u9A8C\u8BC1\u5931\u8D25\uFF0C\u9879\u76EE\u5DF2\u56DE\u6EDA\u5230 ${result2.plan.sourceVersion}\uFF1A${error instanceof Error ? error.message : String(error)}`, { cause: error });
11083
+ throw new Error(`\u66F4\u65B0\u5B89\u5168\u68C0\u67E5\u5931\u8D25\uFF0C\u9879\u76EE\u5DF2\u56DE\u6EDA\u5230 ${result2.plan.sourceVersion}\uFF1A${error instanceof Error ? error.message : String(error)}`, { cause: error });
11024
11084
  }
11025
11085
  }
11026
11086
  }
@@ -11666,6 +11726,20 @@ function actionLabel(action) {
11666
11726
  return action === "add" ? "\u5B89\u88C5" : action === "update" ? "\u66F4\u65B0" : "\u5378\u8F7D";
11667
11727
  }
11668
11728
 
11729
+ // src/update/cli-quality-output.ts
11730
+ function printProjectQualityVerification(status) {
11731
+ if (status === "passed") {
11732
+ console.log("\u9879\u76EE\u8D28\u91CF\u9A8C\u8BC1\uFF1A\u5DF2\u901A\u8FC7\uFF08\u663E\u5F0F --verify\uFF09");
11733
+ return;
11734
+ }
11735
+ if (status === "failed") {
11736
+ console.error("\u9879\u76EE\u8D28\u91CF\u9A8C\u8BC1\uFF1A\u672A\u901A\u8FC7\uFF1B\u5347\u7EA7\u5DF2\u5B89\u5168\u63D0\u4EA4\uFF0C\u8BF7\u67E5\u770B\u4E0A\u65B9\u5931\u8D25\u547D\u4EE4\u548C\u66F4\u65B0\u62A5\u544A");
11737
+ process.exitCode = 1;
11738
+ return;
11739
+ }
11740
+ console.log("\u9879\u76EE\u8D28\u91CF\u9A8C\u8BC1\uFF1A\u5DF2\u8DF3\u8FC7\uFF1B\u53EF\u6309\u9700\u663E\u5F0F\u8FD0\u884C lint\u3001typecheck\u3001test \u548C build");
11741
+ }
11742
+
11669
11743
  // src/update/cli.ts
11670
11744
  async function runLifecycleCommand(args, dependencies = {}) {
11671
11745
  const command = args[0];
@@ -11740,6 +11814,7 @@ var updateHelpText = `\u7528\u6CD5\uFF1Acreate-windy update [\u9009\u9879]
11740
11814
  --to <\u7248\u672C> \u66F4\u65B0\u5230\u6307\u5B9A\u7248\u672C
11741
11815
  --cwd <\u76EE\u5F55> \u6307\u5B9A\u9879\u76EE\u76EE\u5F55
11742
11816
  --skip-install \u66F4\u65B0\u65F6\u4E0D\u6267\u884C bun install
11817
+ --verify \u663E\u5F0F\u6267\u884C\u9879\u76EE lint/typecheck/test/build
11743
11818
  --force \u5F3A\u5236\u91C7\u7528\u5E73\u53F0\u57FA\u7EBF\u53C2\u4E0E\u4E09\u65B9\u5408\u5E76
11744
11819
  -h, --help \u663E\u793A update \u5E2E\u52A9\u5E76\u9000\u51FA
11745
11820
  `;
@@ -11772,7 +11847,8 @@ async function runUpdate(args, projectDirectory) {
11772
11847
  return;
11773
11848
  }
11774
11849
  const updater = new DefaultStarterUpdater({
11775
- skipInstall: args.includes("--skip-install")
11850
+ skipInstall: args.includes("--skip-install"),
11851
+ verifyProject: args.includes("--verify")
11776
11852
  });
11777
11853
  const plan = await updater.plan({
11778
11854
  projectDirectory,
@@ -11795,6 +11871,7 @@ async function runUpdate(args, projectDirectory) {
11795
11871
  const environmentRepair = await repairLocalEnvironment(projectDirectory);
11796
11872
  console.log(`
11797
11873
  \u66F4\u65B0\u5B8C\u6210\uFF1A${plan.sourceVersion} \u2192 ${plan.targetVersion}`);
11874
+ printProjectQualityVerification(report.projectQualityVerification);
11798
11875
  console.log(`\u62A5\u544A\uFF1A${report.reportPath}`);
11799
11876
  printEnvironmentRepair(environmentRepair);
11800
11877
  if (environmentRepair.missingKeys.length || environmentRepair.duplicateKeys.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-windy",
3
- "version": "0.6.17",
3
+ "version": "0.6.19",
4
4
  "description": "创建单组织、单租户、私有部署优先的 Windy 企业 Dashboard Starter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "templateCommit": "4b392b4ca9083489e1a2134c55f3eb1c8ca638ad",
4
- "generatorVersion": "0.6.17",
3
+ "templateCommit": "f2e5ced778480dc66e9dd03dd79023b9eda766a1",
4
+ "generatorVersion": "0.6.19",
5
5
  "modules": [
6
6
  {
7
7
  "name": "system",
@@ -160,6 +160,8 @@ bun run build
160
160
  bun run windy doctor
161
161
  bun run windy update --check
162
162
  bun run windy update --dry-run
163
+ bun run windy update
164
+ # 如需在本次更新中显式执行项目质量脚本:bun run windy update --verify
163
165
  bun run windy module list
164
166
  # 当前组合没有可安全 runtime disable 的已安装可选模块
165
167
  bun run lint
@@ -170,11 +172,12 @@ bun run format
170
172
 
171
173
  更新前无需提交、暂存或清理业务改动。`--dry-run` 只生成计划,不写项目;无关 WIP
172
174
  保持原样,命中同一路径时按冻结快照与三方合并 fail closed。正式更新先备份,再做
173
- package 字段级合并、共享脚手架三方合并与完整验证。整个 `apps/web/src` 和
175
+ package 字段级合并、共享脚手架三方合并与安全收口;默认不执行项目 lint、typecheck、
176
+ test 或 build,传入 `--verify` 才显式执行并记录结果。整个 `apps/web/src` 和
174
177
  `apps/server/src/app` 默认保持项目版本;已登记版本 recipe 只修改其声明的窄锚点。
175
178
  任何冲突都会在写入前中止,模块启停在交互
176
179
  终端确认;CI、Agent 或其它非 TTY 环境必须显式传入 `--yes`。
177
- 无并发变化的验证失败会自动回滚;中断恢复使用
180
+ 安全检查失败会自动回滚;项目质量脚本失败本身不回滚已安全应用的更新。中断恢复使用
178
181
  `bun run windy doctor --repair`。该命令在提交点前保留并发编辑并回滚其余路径,
179
182
  在 committed 提交点后只完成事务状态与备份清理,不回滚项目文件。
180
183
 
@@ -15,6 +15,7 @@ uv run mypy
15
15
 
16
16
  HTTP 接口:
17
17
 
18
+ - `GET /api/agent/runtime/compatibility`(内部认证、V1 协议 Offer)
18
19
  - `POST /api/agent/runs`
19
20
  - `GET /api/agent/runs/:runId/events`
20
21
  - `GET /api/agent/runs/:runId/result`
@@ -24,6 +25,11 @@ Run 创建使用 `Idempotency-Key`。事件以 SSE 传输,支持 `Last-Event-I
24
25
  断线重放,并周期发送 comment heartbeat。Provider 或 Engine 异常只映射为 canonical
25
26
  安全错误,不把原始异常正文暴露给调用方。
26
27
 
28
+ 兼容 Offer 返回 Engine identity、支持的协议版本、传输能力、callback auth profile 及三层摘要。
29
+ 新 Windy Server 会在 Run 请求头固定所选版本和 Offer digest;新 Agent Server 仍接受完全不带 pin
30
+ 的旧调用方,但拒绝半套或不匹配 pin。它只用于同一受控部署,不能把内部 token 发给外部业务系统。
31
+ 跨系统限制见 [Remote Agent Runtime 兼容契约](../../docs/architecture/agent-runtime-compatibility.md)。
32
+
27
33
  配置以下仅服务端可见的环境变量后启用 Direct Engine:
28
34
 
29
35
  ```bash
@@ -34,6 +34,12 @@ from .http_helpers import (
34
34
  )
35
35
  from .report_routes import create_report_router
36
36
  from .report_store import AgentRuntimeReportStore
37
+ from .runtime_compatibility import (
38
+ OFFER_DIGEST_HEADER,
39
+ PROTOCOL_HEADER,
40
+ RuntimeCompatibilityError,
41
+ validate_runtime_compatibility_pin,
42
+ )
37
43
  from .runtime_factory import (
38
44
  default_engine,
39
45
  default_report_store,
@@ -191,11 +197,28 @@ def create_app(
191
197
  request: StartRunRequest,
192
198
  idempotency_key: Annotated[str, Header(alias="Idempotency-Key")],
193
199
  authorization: Annotated[str | None, Header()] = None,
200
+ runtime_protocol: Annotated[str | None, Header(alias=PROTOCOL_HEADER)] = None,
201
+ runtime_offer_digest: Annotated[
202
+ str | None, Header(alias=OFFER_DIGEST_HEADER)
203
+ ] = None,
194
204
  ) -> StartRunResponse | JSONResponse:
195
205
  if not authorized(authorization, internal_token):
196
206
  return safe_error(403, "FORBIDDEN", "内部调用认证失败", False)
197
207
  if not idempotency_key.strip():
198
208
  return safe_error(400, "INVALID_INPUT", "幂等键不能为空", False)
209
+ try:
210
+ validate_runtime_compatibility_pin(
211
+ runtime_protocol,
212
+ runtime_offer_digest,
213
+ runtime_engine.engine_id,
214
+ )
215
+ except RuntimeCompatibilityError:
216
+ return safe_error(
217
+ 409,
218
+ "RUNTIME_UNAVAILABLE",
219
+ "Agent Runtime compatibility pin 不匹配",
220
+ False,
221
+ )
199
222
  try:
200
223
  await require_vision_readiness(runtime_engine, request)
201
224
  except SafeEngineError as error:
@@ -7,6 +7,7 @@ from fastapi import FastAPI, Header
7
7
  from fastapi.responses import JSONResponse
8
8
 
9
9
  from .http_helpers import authorized, safe_error
10
+ from .runtime_compatibility import COMPATIBILITY_PATH, runtime_compatibility_offer
10
11
  from .service import AgentRunService
11
12
  from .vlm_readiness import unavailable_vlm_snapshot
12
13
 
@@ -82,3 +83,11 @@ def install_health_routes(
82
83
  "Agent Runtime Model coordinate 不可用",
83
84
  True,
84
85
  )
86
+
87
+ @application.get(COMPATIBILITY_PATH, response_model=None)
88
+ async def runtime_compatibility(
89
+ authorization: Annotated[str | None, Header()] = None,
90
+ ) -> dict[str, object] | JSONResponse:
91
+ if not authorized(authorization, internal_token):
92
+ return safe_error(403, "FORBIDDEN", "内部调用认证失败", False)
93
+ return runtime_compatibility_offer(runtime.engine.engine_id)
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import re
6
+
7
+ SCHEMA_VERSION = 1
8
+ PROTOCOL_VERSION = 1
9
+ RUNTIME_KIND = "agent"
10
+ COMPATIBILITY_PATH = "/api/agent/runtime/compatibility"
11
+ PROTOCOL_HEADER = "X-Windy-Agent-Runtime-Protocol"
12
+ OFFER_DIGEST_HEADER = "X-Windy-Agent-Runtime-Offer-Digest"
13
+ TOOL_CALLBACK_CONTRACT_VERSION = 1
14
+ TOOL_CALLBACK_AUTHENTICATION = "windy-internal-bearer-v1"
15
+ PROTOCOL_FEATURES = (
16
+ "canonical-sse-replay.v1",
17
+ "execution-grant-forwarding.v1",
18
+ "operation-release-attestation.v1",
19
+ "run-cancel.v1",
20
+ "run-lifecycle.v1",
21
+ "runtime-reporting.v1",
22
+ "tool-callback.v1",
23
+ )
24
+
25
+ _STABLE_VALUE = re.compile(r"^[a-z0-9][a-z0-9._-]{0,159}$")
26
+
27
+
28
+ class RuntimeCompatibilityError(ValueError):
29
+ pass
30
+
31
+
32
+ def runtime_compatibility_offer(engine_id: str) -> dict[str, object]:
33
+ if not _STABLE_VALUE.fullmatch(engine_id):
34
+ raise RuntimeCompatibilityError("Agent Runtime engineId invalid")
35
+ protocol_versions = [PROTOCOL_VERSION]
36
+ protocol_features = sorted(PROTOCOL_FEATURES)
37
+ tool_callback: dict[str, object] = {
38
+ "contractVersion": TOOL_CALLBACK_CONTRACT_VERSION,
39
+ "authentication": TOOL_CALLBACK_AUTHENTICATION,
40
+ }
41
+ release_digest = _digest(
42
+ {
43
+ "runtimeKind": RUNTIME_KIND,
44
+ "engineId": engine_id,
45
+ "protocolVersions": protocol_versions,
46
+ }
47
+ )
48
+ capabilities_digest = _digest(
49
+ {
50
+ "protocolFeatures": protocol_features,
51
+ "toolCallback": tool_callback,
52
+ }
53
+ )
54
+ body: dict[str, object] = {
55
+ "schemaVersion": SCHEMA_VERSION,
56
+ "runtimeKind": RUNTIME_KIND,
57
+ "engineId": engine_id,
58
+ "protocolVersions": protocol_versions,
59
+ "protocolFeatures": protocol_features,
60
+ "toolCallback": tool_callback,
61
+ "releaseDigest": release_digest,
62
+ "capabilitiesDigest": capabilities_digest,
63
+ }
64
+ return {**body, "offerDigest": _digest(body)}
65
+
66
+
67
+ def validate_runtime_compatibility_pin(
68
+ protocol_header: str | None,
69
+ offer_digest_header: str | None,
70
+ engine_id: str,
71
+ ) -> None:
72
+ if protocol_header is None and offer_digest_header is None:
73
+ return
74
+ if protocol_header is None or offer_digest_header is None:
75
+ raise RuntimeCompatibilityError("Agent Runtime compatibility pin incomplete")
76
+ if protocol_header != str(PROTOCOL_VERSION):
77
+ raise RuntimeCompatibilityError("Agent Runtime protocol unsupported")
78
+ expected = runtime_compatibility_offer(engine_id)["offerDigest"]
79
+ if offer_digest_header != expected:
80
+ raise RuntimeCompatibilityError("Agent Runtime offer digest mismatch")
81
+
82
+
83
+ def _digest(value: object) -> str:
84
+ encoded = json.dumps(
85
+ value,
86
+ ensure_ascii=False,
87
+ separators=(",", ":"),
88
+ ).encode()
89
+ return f"sha256:{hashlib.sha256(encoded).hexdigest()}"
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import AsyncIterator
5
+ from pathlib import Path
6
+ from typing import cast
7
+
8
+ import httpx
9
+ import pytest
10
+
11
+ from southwind_agent_server.app import create_app
12
+ from southwind_agent_server.deterministic import DeterministicEngineAdapter
13
+ from southwind_agent_server.runtime_compatibility import (
14
+ runtime_compatibility_offer,
15
+ )
16
+
17
+ TOKEN = "runtime-compatibility-test-token"
18
+
19
+
20
+ @pytest.fixture
21
+ async def client() -> AsyncIterator[httpx.AsyncClient]:
22
+ app = create_app(DeterministicEngineAdapter(), internal_token=TOKEN)
23
+ async with httpx.AsyncClient(
24
+ transport=httpx.ASGITransport(app=app),
25
+ base_url="http://agent.test",
26
+ ) as value:
27
+ yield value
28
+
29
+
30
+ async def test_compatibility_offer_is_authenticated_and_matches_public_fixture(
31
+ client: httpx.AsyncClient,
32
+ ) -> None:
33
+ denied = await client.get("/api/agent/runtime/compatibility")
34
+ allowed = await client.get(
35
+ "/api/agent/runtime/compatibility",
36
+ headers={"Authorization": f"Bearer {TOKEN}"},
37
+ )
38
+
39
+ assert denied.status_code == 403
40
+ assert allowed.status_code == 200
41
+ assert allowed.json() == public_fixture()
42
+
43
+
44
+ async def test_start_accepts_legacy_without_pin(
45
+ client: httpx.AsyncClient,
46
+ ) -> None:
47
+ legacy = await start(client, "legacy-pin")
48
+
49
+ assert legacy.status_code == 200
50
+
51
+
52
+ async def test_start_accepts_complete_matching_pin(
53
+ client: httpx.AsyncClient,
54
+ ) -> None:
55
+ offer = runtime_compatibility_offer("windy.direct.openai-compatible.v1")
56
+ pinned = await start(
57
+ client,
58
+ "current-pin",
59
+ {
60
+ "X-Windy-Agent-Runtime-Protocol": "1",
61
+ "X-Windy-Agent-Runtime-Offer-Digest": str(offer["offerDigest"]),
62
+ },
63
+ )
64
+
65
+ assert pinned.status_code == 200
66
+
67
+
68
+ @pytest.mark.parametrize(
69
+ "headers",
70
+ [
71
+ {"X-Windy-Agent-Runtime-Protocol": "1"},
72
+ {"X-Windy-Agent-Runtime-Offer-Digest": "sha256:" + "0" * 64},
73
+ {
74
+ "X-Windy-Agent-Runtime-Protocol": "2",
75
+ "X-Windy-Agent-Runtime-Offer-Digest": "sha256:" + "0" * 64,
76
+ },
77
+ {
78
+ "X-Windy-Agent-Runtime-Protocol": "1",
79
+ "X-Windy-Agent-Runtime-Offer-Digest": "sha256:" + "0" * 64,
80
+ },
81
+ ],
82
+ )
83
+ async def test_start_rejects_partial_or_incompatible_pin_before_admission(
84
+ client: httpx.AsyncClient,
85
+ headers: dict[str, str],
86
+ ) -> None:
87
+ response = await start(client, "rejected-pin", headers)
88
+
89
+ assert response.status_code == 409
90
+ assert response.json() == {
91
+ "code": "RUNTIME_UNAVAILABLE",
92
+ "message": "Agent Runtime compatibility pin 不匹配",
93
+ "requestId": response.json()["requestId"],
94
+ "retryable": False,
95
+ }
96
+
97
+
98
+ async def start(
99
+ client: httpx.AsyncClient,
100
+ key: str,
101
+ headers: dict[str, str] | None = None,
102
+ ) -> httpx.Response:
103
+ return await client.post(
104
+ "/api/agent/runs",
105
+ headers={
106
+ "Authorization": f"Bearer {TOKEN}",
107
+ "Idempotency-Key": key,
108
+ **(headers or {}),
109
+ },
110
+ json={"operationKey": "test.echo", "input": {"text": key}},
111
+ )
112
+
113
+
114
+ def public_fixture() -> dict[str, object]:
115
+ path = (
116
+ Path(__file__).parents[3]
117
+ / "packages/server-sdk/contracts/agent-runtime-compatibility/v1/offer.json"
118
+ )
119
+ return cast(dict[str, object], json.loads(path.read_text(encoding="utf8")))