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.
- package/dist/cli.js +98 -21
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/README.md +5 -2
- package/template/apps/agent-server/README.md +6 -0
- package/template/apps/agent-server/src/southwind_agent_server/app.py +23 -0
- package/template/apps/agent-server/src/southwind_agent_server/health_routes.py +9 -0
- package/template/apps/agent-server/src/southwind_agent_server/runtime_compatibility.py +89 -0
- package/template/apps/agent-server/tests/test_runtime_compatibility.py +119 -0
- package/template/apps/server/src/agent/remote-runtime-compatibility.test.ts +147 -0
- package/template/apps/server/src/agent/remote-runtime.ts +66 -4
- package/template/apps/server/src/agent/runtime-bootstrap.ts +1 -0
- package/template/apps/web/src/components/common/FormDialog.vue +1 -0
- package/template/apps/web/src/components/common/ModalLayout.webtest.ts +1 -0
- package/template/apps/web/src/components/ui/dialog/DialogContent.vue +1 -1
- package/template/apps/web/src/components/ui/dialog/DialogScrollContent.vue +1 -1
- package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +1 -0
- package/template/apps/web/src/pages/system/components/user-account/CreateDepartmentDialog.vue +182 -0
- package/template/apps/web/src/pages/system/components/user-account/CreateDepartmentDialog.webtest.ts +93 -0
- package/template/apps/web/src/pages/system/components/user-account/RolePermissionSummary.vue +59 -0
- package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +21 -71
- package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.webtest.ts +71 -0
- package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.vue +48 -27
- package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.webtest.ts +30 -1
- package/template/docs/architecture/ai-runtime.md +8 -15
- package/template/docs/platform/agent-runtime.md +5 -0
- package/template/packages/server-sdk/contracts/agent-runtime-compatibility/v1/offer.json +22 -0
- package/template/packages/server-sdk/index.ts +19 -0
- package/template/packages/server-sdk/package.json +2 -0
- package/template/packages/server-sdk/src/agent-runtime-compatibility.contract.test.ts +143 -0
- package/template/packages/server-sdk/src/agent-runtime-compatibility.public-api.test.ts +19 -0
- 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\
|
|
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
|
-
\
|
|
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 {
|
|
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
|
-
"- \
|
|
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
|
|
10978
|
-
|
|
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 {
|
|
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\
|
|
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\
|
|
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
package/template/README.md
CHANGED
|
@@ -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
|
|
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")))
|