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
@@ -1,7 +1,11 @@
1
1
  <script setup lang="ts">
2
2
  import { onMounted, shallowRef, watch } from "vue";
3
3
  import { PlusIcon } from "@lucide/vue";
4
- import { PASSWORD_POLICY, type PasswordPolicy } from "@southwind-ai/shared";
4
+ import {
5
+ PASSWORD_POLICY,
6
+ type PasswordPolicy,
7
+ type PermissionDefinition,
8
+ } from "@southwind-ai/shared";
5
9
  import { Button } from "@/components/ui/button";
6
10
  import { Input } from "@/components/ui/input";
7
11
  import {
@@ -11,13 +15,9 @@ import {
11
15
  SelectTrigger,
12
16
  SelectValue,
13
17
  } from "@/components/ui/select";
14
- import {
15
- createSystemResource,
16
- listSystemResource,
17
- quickCreateDepartment,
18
- type SystemRecord,
19
- } from "@/services/system-api";
18
+ import { listSystemResource, type SystemRecord } from "@/services/system-api";
20
19
  import { loadPasswordPolicy } from "@/services/auth-api";
20
+ import CreateDepartmentDialog from "./CreateDepartmentDialog.vue";
21
21
  import UserRoleSelector from "./UserRoleSelector.vue";
22
22
 
23
23
  interface DepartmentOption {
@@ -30,6 +30,7 @@ interface DepartmentOption {
30
30
  const props = defineProps<{
31
31
  item?: SystemRecord;
32
32
  defaults: Record<string, unknown>;
33
+ permissions: PermissionDefinition[];
33
34
  departmentCreateMode?: "create" | "manage";
34
35
  }>();
35
36
 
@@ -40,12 +41,8 @@ const emit = defineEmits<{
40
41
  const form = shallowRef<Record<string, unknown>>({});
41
42
  const departments = shallowRef<DepartmentOption[]>([]);
42
43
  const loading = shallowRef(false);
43
- const creating = shallowRef(false);
44
44
  const error = shallowRef("");
45
- const quickOpen = shallowRef(false);
46
- const quickName = shallowRef("");
47
- const quickCode = shallowRef("");
48
- const quickParentId = shallowRef<string | undefined>();
45
+ const departmentDialogOpen = shallowRef(false);
49
46
  const governedTouched = shallowRef<Record<string, boolean>>({});
50
47
  const passwordPolicy = shallowRef<PasswordPolicy>(PASSWORD_POLICY);
51
48
 
@@ -114,32 +111,9 @@ function selectedRoleIds(): string[] {
114
111
  : [];
115
112
  }
116
113
 
117
- async function createDepartment() {
118
- if (!props.departmentCreateMode || creating.value) return;
119
- creating.value = true;
120
- error.value = "";
121
- try {
122
- const payload = {
123
- code: quickCode.value,
124
- name: quickName.value,
125
- parentId: quickParentId.value,
126
- order: 100,
127
- };
128
- const created =
129
- props.departmentCreateMode === "create"
130
- ? await quickCreateDepartment(payload)
131
- : await createSystemResource("departments", payload);
132
- await loadDepartments();
133
- update("departmentId", created.id);
134
- quickOpen.value = false;
135
- quickName.value = "";
136
- quickCode.value = "";
137
- quickParentId.value = undefined;
138
- } catch (currentError) {
139
- error.value = readError(currentError);
140
- } finally {
141
- creating.value = false;
142
- }
114
+ async function selectCreatedDepartment(department: DepartmentOption) {
115
+ await loadDepartments();
116
+ update("departmentId", department.id);
143
117
  }
144
118
 
145
119
  function submit() {
@@ -218,7 +192,7 @@ defineExpose({ submit });
218
192
  type="button"
219
193
  size="sm"
220
194
  variant="ghost"
221
- @click="quickOpen = !quickOpen"
195
+ @click="departmentDialogOpen = true"
222
196
  >
223
197
  <PlusIcon data-icon="inline-start" />新建部门
224
198
  </Button>
@@ -246,6 +220,7 @@ defineExpose({ submit });
246
220
  </div>
247
221
  <UserRoleSelector
248
222
  :model-value="selectedRoleIds()"
223
+ :permissions="permissions"
249
224
  @update:model-value="update('roleIds', $event)"
250
225
  />
251
226
  <div class="grid gap-4 sm:grid-cols-2">
@@ -283,38 +258,13 @@ defineExpose({ submit });
283
258
  </span>
284
259
  </label>
285
260
  </div>
286
- <section
287
- v-if="quickOpen"
288
- class="space-y-3 rounded-lg border bg-muted/20 p-4"
289
- >
290
- <p class="text-sm font-medium">新建并选中部门</p>
291
- <div class="grid gap-3 sm:grid-cols-2">
292
- <Input v-model="quickName" placeholder="部门名称" />
293
- <Input v-model="quickCode" placeholder="部门编码,例如 sales" />
294
- </div>
295
- <Select v-model="quickParentId">
296
- <SelectTrigger class="w-full" aria-label="上级部门">
297
- <SelectValue placeholder="请选择上级部门" />
298
- </SelectTrigger>
299
- <SelectContent>
300
- <SelectItem
301
- v-for="department in departments"
302
- :key="department.id"
303
- :value="department.id"
304
- >
305
- {{ department.name }}
306
- </SelectItem>
307
- </SelectContent>
308
- </Select>
309
- <Button
310
- type="button"
311
- size="sm"
312
- :disabled="creating || !quickParentId"
313
- @click="createDepartment"
314
- >
315
- {{ creating ? "创建中…" : "创建并选中" }}
316
- </Button>
317
- </section>
318
261
  <p v-if="error" class="text-sm text-destructive">{{ error }}</p>
262
+ <CreateDepartmentDialog
263
+ v-if="departmentCreateMode"
264
+ v-model:open="departmentDialogOpen"
265
+ :mode="departmentCreateMode"
266
+ :departments="departments"
267
+ @created="selectCreatedDepartment"
268
+ />
319
269
  </div>
320
270
  </template>
@@ -0,0 +1,71 @@
1
+ import { defineComponent } from "vue";
2
+ import { afterEach, describe, expect, test, vi } from "vitest";
3
+ import { flushPromises, mount } from "@vue/test-utils";
4
+ import UserAccountForm from "./UserAccountForm.vue";
5
+
6
+ const api = vi.hoisted(() => ({
7
+ list: vi.fn(),
8
+ loadPasswordPolicy: vi.fn(),
9
+ }));
10
+
11
+ vi.mock("@/services/system-api", async () => {
12
+ const actual = await vi.importActual<typeof import("@/services/system-api")>(
13
+ "@/services/system-api",
14
+ );
15
+ return { ...actual, listSystemResource: api.list };
16
+ });
17
+
18
+ vi.mock("@/services/auth-api", () => ({
19
+ loadPasswordPolicy: api.loadPasswordPolicy,
20
+ }));
21
+
22
+ afterEach(() => vi.clearAllMocks());
23
+
24
+ describe("UserAccountForm", () => {
25
+ test("新建部门使用独立 Modal,不在用户表单底部追加内容", async () => {
26
+ api.list.mockResolvedValue({
27
+ items: [{ id: "dept_root", code: "root", name: "总部" }],
28
+ total: 1,
29
+ page: 1,
30
+ pageSize: 100,
31
+ });
32
+ api.loadPasswordPolicy.mockResolvedValue({
33
+ minLength: 12,
34
+ maxLength: 128,
35
+ requireUppercase: true,
36
+ requireLowercase: true,
37
+ requireNumber: true,
38
+ requireSpecial: false,
39
+ });
40
+ const dialogStub = defineComponent({
41
+ name: "CreateDepartmentDialog",
42
+ props: { open: Boolean },
43
+ emits: ["update:open", "created"],
44
+ template: '<div data-testid="department-dialog" />',
45
+ });
46
+ const wrapper = mount(UserAccountForm, {
47
+ props: {
48
+ defaults: { roleIds: [], status: "active" },
49
+ permissions: [],
50
+ departmentCreateMode: "create",
51
+ },
52
+ global: {
53
+ stubs: {
54
+ CreateDepartmentDialog: dialogStub,
55
+ UserRoleSelector: { template: "<div />" },
56
+ },
57
+ },
58
+ });
59
+ await flushPromises();
60
+
61
+ const dialog = wrapper.getComponent(dialogStub);
62
+ expect(dialog.props("open")).toBe(false);
63
+ const openButton = wrapper
64
+ .findAll("button")
65
+ .find((button) => button.text() === "新建部门");
66
+ await openButton?.trigger("click");
67
+
68
+ expect(dialog.props("open")).toBe(true);
69
+ expect(wrapper.text()).not.toContain("新建并选中部门");
70
+ });
71
+ });
@@ -1,16 +1,22 @@
1
1
  <script setup lang="ts">
2
+ import type { PermissionDefinition } from "@southwind-ai/shared";
2
3
  import { onMounted, shallowRef } from "vue";
3
4
  import { Checkbox } from "@/components/ui/checkbox";
4
5
  import { listSystemResource } from "@/services/system-api";
6
+ import RolePermissionSummary from "./RolePermissionSummary.vue";
5
7
 
6
8
  interface RoleOption {
7
9
  id: string;
8
10
  code: string;
9
11
  name: string;
10
12
  dataScope: string;
13
+ permissionKeys: string[];
11
14
  }
12
15
 
13
- const props = defineProps<{ modelValue: string[] }>();
16
+ const props = defineProps<{
17
+ modelValue: string[];
18
+ permissions: PermissionDefinition[];
19
+ }>();
14
20
  const emit = defineEmits<{ "update:modelValue": [value: string[]] }>();
15
21
  const roles = shallowRef<RoleOption[]>([]);
16
22
  const loading = shallowRef(false);
@@ -37,6 +43,11 @@ async function loadRoles() {
37
43
  name: item.name,
38
44
  dataScope:
39
45
  typeof item.dataScope === "string" ? item.dataScope : "self",
46
+ permissionKeys: Array.isArray(item.permissionKeys)
47
+ ? item.permissionKeys.filter(
48
+ (key): key is string => typeof key === "string",
49
+ )
50
+ : [],
40
51
  },
41
52
  ]
42
53
  : [],
@@ -78,34 +89,44 @@ function dataScopeLabel(scope: string): string {
78
89
  <label class="text-sm font-medium">账号角色</label>
79
90
  <span class="text-xs text-muted-foreground">至少选择一个</span>
80
91
  </div>
81
- <div
82
- class="grid gap-2 rounded-lg border p-3 sm:grid-cols-2"
83
- aria-label="账号角色"
84
- >
85
- <p v-if="loading" class="text-sm text-muted-foreground sm:col-span-2">
86
- 正在加载角色…
87
- </p>
88
- <p v-else-if="error" class="text-sm text-destructive sm:col-span-2">
92
+ <div class="grid gap-3 rounded-lg border p-3" aria-label="账号角色">
93
+ <p v-if="loading" class="text-sm text-muted-foreground">正在加载角色…</p>
94
+ <p v-else-if="error" class="text-sm text-destructive">
89
95
  {{ error }}
90
96
  </p>
91
- <label
92
- v-for="role in roles"
93
- v-else
94
- :key="role.id"
95
- class="flex cursor-pointer items-start gap-3 rounded-md p-2 hover:bg-muted/60"
96
- >
97
- <Checkbox
98
- :model-value="modelValue.includes(role.id)"
99
- :aria-label="role.name"
100
- @update:model-value="toggle(role.id, $event)"
101
- />
102
- <span class="grid gap-0.5 text-sm">
103
- <span class="font-medium">{{ role.name }}</span>
104
- <span class="text-xs text-muted-foreground">
105
- {{ role.code }} · {{ dataScopeLabel(role.dataScope) }}
106
- </span>
107
- </span>
108
- </label>
97
+ <template v-else>
98
+ <article
99
+ v-for="role in roles"
100
+ :key="role.id"
101
+ class="rounded-lg border p-3 transition-colors"
102
+ :class="
103
+ modelValue.includes(role.id)
104
+ ? 'border-primary/50 bg-primary/5'
105
+ : 'hover:bg-muted/40'
106
+ "
107
+ >
108
+ <label class="flex cursor-pointer items-start gap-3">
109
+ <Checkbox
110
+ :model-value="modelValue.includes(role.id)"
111
+ :aria-label="role.name"
112
+ @update:model-value="toggle(role.id, $event)"
113
+ />
114
+ <span class="grid gap-0.5 text-sm">
115
+ <span class="font-medium">{{ role.name }}</span>
116
+ <span class="text-xs text-muted-foreground">
117
+ {{ role.code }} · {{ dataScopeLabel(role.dataScope) }}
118
+ </span>
119
+ </span>
120
+ </label>
121
+ <RolePermissionSummary
122
+ :permission-keys="role.permissionKeys"
123
+ :permissions="permissions"
124
+ />
125
+ </article>
126
+ <p v-if="!roles.length" class="text-sm text-muted-foreground">
127
+ 暂无可选角色。
128
+ </p>
129
+ </template>
109
130
  </div>
110
131
  </div>
111
132
  </template>
@@ -22,13 +22,38 @@ describe("UserRoleSelector", () => {
22
22
  code: "department-manager",
23
23
  name: "部门负责人",
24
24
  dataScope: "department-and-children",
25
+ permissionKeys: ["system.user.read", "system.user.manage"],
25
26
  },
26
27
  ],
27
28
  total: 1,
28
29
  page: 1,
29
30
  pageSize: 100,
30
31
  });
31
- const wrapper = mount(UserRoleSelector, { props: { modelValue: [] } });
32
+ const wrapper = mount(UserRoleSelector, {
33
+ props: {
34
+ modelValue: [],
35
+ permissions: [
36
+ {
37
+ key: "system.user.read",
38
+ label: "查看用户",
39
+ module: "system",
40
+ moduleLabel: "系统管理",
41
+ resource: "user",
42
+ resourceLabel: "用户账号",
43
+ action: "read",
44
+ },
45
+ {
46
+ key: "system.user.manage",
47
+ label: "管理用户",
48
+ module: "system",
49
+ moduleLabel: "系统管理",
50
+ resource: "user",
51
+ resourceLabel: "用户账号",
52
+ action: "manage",
53
+ },
54
+ ],
55
+ },
56
+ });
32
57
  await flushPromises();
33
58
 
34
59
  expect(api.list).toHaveBeenCalledWith(
@@ -37,6 +62,10 @@ describe("UserRoleSelector", () => {
37
62
  );
38
63
  expect(wrapper.text()).toContain("部门负责人");
39
64
  expect(wrapper.text()).toContain("本部门及下级");
65
+ expect(wrapper.text()).toContain("查看 2 项功能权限");
66
+ await wrapper.get("summary").trigger("click");
67
+ expect(wrapper.text()).toContain("用户账号");
68
+ expect(wrapper.text()).toContain("查看用户、管理用户");
40
69
  await wrapper.get('[role="checkbox"]').trigger("click");
41
70
  expect(wrapper.emitted("update:modelValue")?.at(-1)).toEqual([
42
71
  ["role_manager"],
@@ -1,20 +1,13 @@
1
1
  # Windy AI Runtime 架构
2
2
 
3
- 更新时间:2026-08-27。
4
-
5
- 状态:架构已确认,文本 Agent、Document AI、批量 Speech/ASR 与受控 Vision Artifact
6
- 切片已实现。当前包含 Module AI Operation
7
- 注册、Guarded Run facade、短时 Execution Grant、受限 Tool Host、OpenAI-compatible
8
- Provider、canonical HTTP/SSE、取消、cursor 重放和单副本有界 SQLite Store。未配置
9
- Provider 时仍安全拒绝。真实业务 Run 由 Windy Direct OpenAI-compatible Engine 执行,
10
- 并直接产生 canonical signal。持久 Session/Workflow、LiteLLM 多 Provider 路由和
11
- PostgreSQL 多副本 Store 尚未实现。Document AI 已提供独立
12
- Server、授权 ArtifactRef、结构化页级结果、本地/内网 Adapter 契约,以及搜索型 PDF
13
- 等派生文件的受治理 Output Artifact producer 协议;项目 composition 尚未接线,不能
14
- 视为生产可用。Speech 已提供批量转写、归一化、VAD/时间戳契约、稳定分段和结果
15
- Artifact,但未提供实时 ASR、TTS 或 Dictation。不能据此宣称完整 AI Runtime 已交付。
16
- Vision 已提供 `agent.vision` 的多图片受控输入、像素限制、逐次发送审计和 JSON Schema
17
- 终态校验;视频帧输入与实时视觉不在当前切片。
3
+ 更新时间:2026-09-02。
4
+
5
+ 状态:文本 Agent、Document、Embedding、批量 Speech/ASR、受控 Vision Artifact、PostgreSQL
6
+ Run Store、持久 Session/Memory 与 Workflow/委派切片均已实现。WAI-1 已为 Windy Server 到
7
+ Agent Server 增加 additive 的 V1 兼容 Offer、release/capability digest 和 Run 创建 pin;详见
8
+ [Remote Agent Runtime 兼容契约](./agent-runtime-compatibility.md)。当前 Provider 仍是单一 Direct
9
+ OpenAI-compatible Adapter;LiteLLM 多 Provider 路由、完整 RAG、外部业务 Host callback 认证与
10
+ Sharp S-Word 接线均未实现。未配置任一可选 Runtime 时继续安全拒绝对应 Operation,不阻止普通业务。
18
11
 
19
12
  ## 目标
20
13
 
@@ -18,6 +18,11 @@ POST /api/agent/runs/:runId/cancel
18
18
  创建 Run 必须携带幂等键;SSE 支持 cursor 重放和 heartbeat。Engine 切换不改变 Run DTO、
19
19
  canonical Event、唯一终态、取消或 Tool 授权语义。
20
20
 
21
+ Windy Server 与 Agent Server 之间已经提供 additive 的 V1 兼容 Offer、release/capability digest
22
+ 和 Run 创建请求头 pin。当前同部署 Adapter 使用滚动兼容的 `prefer-v1`;任何外部业务 Host Adapter
23
+ 必须使用 `require-v1`,且在独立 callback 认证 profile 落地前不得投入生产。完整职责与失败语义见
24
+ [Remote Agent Runtime 兼容契约](../architecture/agent-runtime-compatibility.md)。
25
+
21
26
  ## 能力与治理边界
22
27
 
23
28
  Direct Engine 已覆盖:
@@ -0,0 +1,22 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "runtimeKind": "agent",
4
+ "engineId": "windy.direct.openai-compatible.v1",
5
+ "protocolVersions": [1],
6
+ "protocolFeatures": [
7
+ "canonical-sse-replay.v1",
8
+ "execution-grant-forwarding.v1",
9
+ "operation-release-attestation.v1",
10
+ "run-cancel.v1",
11
+ "run-lifecycle.v1",
12
+ "runtime-reporting.v1",
13
+ "tool-callback.v1"
14
+ ],
15
+ "toolCallback": {
16
+ "contractVersion": 1,
17
+ "authentication": "windy-internal-bearer-v1"
18
+ },
19
+ "releaseDigest": "sha256:410c49040a4e4de40dd2fed60877b2b08e9bbcb23198f10a7cbb3cf7d1135ce0",
20
+ "capabilitiesDigest": "sha256:bcdd3822f9077ac6d1cd42f8a53dbdb46d9a5bc908821e92bd6fb28e2ec6ab65",
21
+ "offerDigest": "sha256:1375cd3a1511f2d5fdde3cac123d9d6bc603f80ad746758508d0a3da67ed8fad"
22
+ }
@@ -1,4 +1,23 @@
1
1
  export type { ModuleActor } from "./src/actor.js";
2
+ export {
3
+ AGENT_RUNTIME_COMPATIBILITY_PATH_V1,
4
+ AGENT_RUNTIME_COMPATIBILITY_SCHEMA_VERSION,
5
+ AGENT_RUNTIME_INTERNAL_CALLBACK_AUTH_V1,
6
+ AGENT_RUNTIME_KIND_V1,
7
+ AGENT_RUNTIME_OFFER_DIGEST_HEADER_V1,
8
+ AGENT_RUNTIME_PROTOCOL_FEATURES_V1,
9
+ AGENT_RUNTIME_PROTOCOL_HEADER_V1,
10
+ AGENT_RUNTIME_PROTOCOL_VERSION_V1,
11
+ AGENT_RUNTIME_TOOL_CALLBACK_CONTRACT_VERSION_V1,
12
+ createAgentRuntimeCompatibilityOfferV1,
13
+ localAgentRuntimeCompatibilityRequirementsV1,
14
+ negotiateAgentRuntimeCompatibilityV1,
15
+ parseAgentRuntimeCompatibilityOfferV1,
16
+ type AgentRuntimeCompatibilityDigestV1,
17
+ type AgentRuntimeCompatibilityNegotiationV1,
18
+ type AgentRuntimeCompatibilityOfferV1,
19
+ type AgentRuntimeCompatibilityRequirementsV1,
20
+ } from "./src/agent-runtime-compatibility.js";
2
21
  export { pageAgentClientRunCapability } from "./src/client-agent-session.js";
3
22
  export type {
4
23
  ModuleClientAgentHostAuthAdapter,
@@ -26,6 +26,7 @@
26
26
  "src/ai-runtime-readiness-validation.ts",
27
27
  "src/ai-runtime-readiness-validation-values.ts",
28
28
  "src/ai-runtime-readiness-values.ts",
29
+ "src/agent-runtime-compatibility.ts",
29
30
  "src/agent-session-memory-port.ts",
30
31
  "src/agent-session-provider.ts",
31
32
  "src/agent-source-invalidation-port.ts",
@@ -71,6 +72,7 @@
71
72
  },
72
73
  "exports": {
73
74
  ".": "./index.ts",
75
+ "./contracts/agent-runtime-compatibility/v1/*": "./contracts/agent-runtime-compatibility/v1/*",
74
76
  "./contracts/ai-runtime-readiness/v1/*": "./contracts/ai-runtime-readiness/v1/*"
75
77
  },
76
78
  "publishConfig": {
@@ -0,0 +1,143 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { describe, expect, test } from "bun:test";
3
+ import {
4
+ AGENT_RUNTIME_PROTOCOL_FEATURES_V1,
5
+ type AgentRuntimeCompatibilityOfferV1,
6
+ createAgentRuntimeCompatibilityOfferV1,
7
+ localAgentRuntimeCompatibilityRequirementsV1,
8
+ negotiateAgentRuntimeCompatibilityV1,
9
+ parseAgentRuntimeCompatibilityOfferV1,
10
+ } from "./agent-runtime-compatibility.js";
11
+
12
+ const fixture = await loadFixture();
13
+
14
+ describe("Remote Agent Runtime compatibility V1", () => {
15
+ test("公共 builder 与跨语言 fixture 字节语义一致", () => {
16
+ expect(
17
+ createAgentRuntimeCompatibilityOfferV1(
18
+ "windy.direct.openai-compatible.v1",
19
+ ),
20
+ ).toEqual(fixture);
21
+ expect(parseAgentRuntimeCompatibilityOfferV1(fixture)).toEqual(fixture);
22
+ });
23
+
24
+ test("协商最高共同版本并要求完整协议能力与 callback 认证", () => {
25
+ const result = negotiateAgentRuntimeCompatibilityV1(
26
+ fixture,
27
+ localAgentRuntimeCompatibilityRequirementsV1(),
28
+ );
29
+
30
+ expect(result).toEqual({ ok: true, protocolVersion: 1, offer: fixture });
31
+ });
32
+
33
+ test("未知 schema、缺失能力和 callback 认证漂移均失败关闭", () => {
34
+ expect(
35
+ negotiateAgentRuntimeCompatibilityV1(
36
+ { ...fixture, schemaVersion: 2 },
37
+ localAgentRuntimeCompatibilityRequirementsV1(),
38
+ ),
39
+ ).toEqual({ ok: false, code: "CONTRACT_UNSUPPORTED" });
40
+ expect(
41
+ negotiateAgentRuntimeCompatibilityV1(fixture, {
42
+ ...localAgentRuntimeCompatibilityRequirementsV1(),
43
+ requiredProtocolFeatures: [
44
+ ...AGENT_RUNTIME_PROTOCOL_FEATURES_V1,
45
+ "future-required.v1",
46
+ ],
47
+ }),
48
+ ).toEqual({
49
+ ok: false,
50
+ code: "CAPABILITY_MISSING",
51
+ subject: "future-required.v1",
52
+ });
53
+ expect(
54
+ negotiateAgentRuntimeCompatibilityV1(fixture, {
55
+ ...localAgentRuntimeCompatibilityRequirementsV1(),
56
+ requiredToolCallbackAuthentication: "signed-callback-v1",
57
+ }),
58
+ ).toEqual({
59
+ ok: false,
60
+ code: "CALLBACK_AUTH_UNSUPPORTED",
61
+ subject: "signed-callback-v1",
62
+ });
63
+ expect(
64
+ negotiateAgentRuntimeCompatibilityV1(fixture, {
65
+ ...localAgentRuntimeCompatibilityRequirementsV1(),
66
+ protocolVersions: [2],
67
+ }),
68
+ ).toEqual({ ok: false, code: "CONTRACT_UNSUPPORTED" });
69
+ expect(
70
+ negotiateAgentRuntimeCompatibilityV1(fixture, {
71
+ ...localAgentRuntimeCompatibilityRequirementsV1(),
72
+ expectedReleaseDigest: `sha256:${"0".repeat(64)}`,
73
+ }),
74
+ ).toEqual({ ok: false, code: "RELEASE_DIGEST_MISMATCH" });
75
+ });
76
+
77
+ test("任一受摘要保护字段漂移都不能沿用旧 Offer", () => {
78
+ expect(
79
+ parseAgentRuntimeCompatibilityOfferV1({
80
+ ...fixture,
81
+ protocolFeatures: [...fixture.protocolFeatures, "tampered.v1"],
82
+ }),
83
+ ).toBeNull();
84
+ expect(
85
+ parseAgentRuntimeCompatibilityOfferV1({
86
+ ...fixture,
87
+ engineId: "windy.other-engine.v1",
88
+ }),
89
+ ).toBeNull();
90
+ });
91
+
92
+ test("重复或超界集合与非稳定 identity 被拒绝", () => {
93
+ expect(
94
+ parseAgentRuntimeCompatibilityOfferV1({
95
+ ...fixture,
96
+ protocolVersions: [1, 1],
97
+ }),
98
+ ).toBeNull();
99
+ expect(
100
+ parseAgentRuntimeCompatibilityOfferV1({
101
+ ...fixture,
102
+ protocolFeatures: Array.from(
103
+ { length: 65 },
104
+ (_, index) => `feature-${index}.v1`,
105
+ ),
106
+ }),
107
+ ).toBeNull();
108
+ expect(() =>
109
+ createAgentRuntimeCompatibilityOfferV1("https://provider.invalid"),
110
+ ).toThrow("engineId invalid");
111
+ });
112
+
113
+ test("恶意 getter 或循环对象只形成安全的契约拒绝", () => {
114
+ const throwing = Object.defineProperty({}, "schemaVersion", {
115
+ get() {
116
+ throw new Error("secret getter body");
117
+ },
118
+ });
119
+ const cyclic: Record<string, unknown> = { ...fixture };
120
+ cyclic.toolCallback = cyclic;
121
+
122
+ expect(parseAgentRuntimeCompatibilityOfferV1(throwing)).toBeNull();
123
+ expect(parseAgentRuntimeCompatibilityOfferV1(cyclic)).toBeNull();
124
+ expect(
125
+ negotiateAgentRuntimeCompatibilityV1(
126
+ throwing,
127
+ localAgentRuntimeCompatibilityRequirementsV1(),
128
+ ),
129
+ ).toEqual({ ok: false, code: "CONTRACT_INVALID" });
130
+ });
131
+ });
132
+
133
+ async function loadFixture(): Promise<AgentRuntimeCompatibilityOfferV1> {
134
+ return JSON.parse(
135
+ await readFile(
136
+ new URL(
137
+ "../contracts/agent-runtime-compatibility/v1/offer.json",
138
+ import.meta.url,
139
+ ),
140
+ "utf8",
141
+ ),
142
+ ) as AgentRuntimeCompatibilityOfferV1;
143
+ }
@@ -0,0 +1,19 @@
1
+ import { expect, test } from "bun:test";
2
+ import {
3
+ createAgentRuntimeCompatibilityOfferV1,
4
+ localAgentRuntimeCompatibilityRequirementsV1,
5
+ negotiateAgentRuntimeCompatibilityV1,
6
+ } from "../index.js";
7
+
8
+ test("@southwind-ai/server-sdk 根入口公开 Remote Agent Runtime V1 协商", () => {
9
+ const offer = createAgentRuntimeCompatibilityOfferV1(
10
+ "windy.direct.openai-compatible.v1",
11
+ );
12
+
13
+ expect(
14
+ negotiateAgentRuntimeCompatibilityV1(
15
+ offer,
16
+ localAgentRuntimeCompatibilityRequirementsV1(),
17
+ ),
18
+ ).toMatchObject({ ok: true, protocolVersion: 1 });
19
+ });