draftgo-cli 4.0.22 → 4.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -134,7 +134,7 @@ draftgo test custom-services <id> --source draft --handler route:POST:/path --in
134
134
 
135
135
  `commit custom-services` 会自动完成提交、验证和发布,并输出明确的 published 提示;`validate`、`test`、`publish` 仍可作为单独的兼容入口。`test` 只运行 cloud draft,不调用线上版本;发布后如需再试运行,先修改并提交形成新草稿。默认拒绝外部副作用;外部调用用 `--side-effect-policy mock` 模拟,只有明确需要真实副作用时才组合 `--side-effect-policy live --test-write`。数据库等写操作也必须显式加 `--test-write`。
136
136
 
137
- `--source` 支持 `draft`(默认)、`auto`、`published`。Selector 支持 `route:METHOD:/path`、`event:name`、`scheduled:name` 或 handler 名;`--input`、`--headers`、`--user` 都读取 UTF-8 JSON object。普通 SDK 调用继承已验证的 `platform` 或 `space` 上下文;可信服务可对单次操作显式使用 `ctx.Admin.*`,该提升会审计且不会泄漏到后续调用。余额扣款、权益、支付和订阅使用 `ctx.Billing` / `ctx.Admin.Billing`,精确签名查看 checkout 后只读的 `.draftgo-sdk/billing.go`。完整字段与规则见[自定义服务方法指南](resources/skill/references/custom-services.md)。
137
+ `--source` 支持 `draft`(默认)、`auto`、`published`。Selector 支持 `route:METHOD:/path`、`event:name`、`scheduled:name` 或 handler 名;`--input`、`--headers`、`--user` 都读取 UTF-8 JSON object。普通 SDK 调用继承已验证的 `platform` 或 `space` 上下文;自定义服务可对单次操作显式使用 `ctx.Admin.*`,该次调用按系统内置 `*:*:all` 全权限执行、进入审计且不会泄漏到后续调用。余额扣款、权益、支付和订阅使用 `ctx.Billing` / `ctx.Admin.Billing`,精确签名查看 checkout 后只读的 `.draftgo-sdk/billing.go`。完整字段与规则见[自定义服务方法指南](resources/skill/references/custom-services.md)。
138
138
 
139
139
  ### MCP/API
140
140
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "draftgo-cli",
3
- "version": "4.0.22",
3
+ "version": "4.0.23",
4
4
  "description": "Install and manage the DraftGo skill across AI coding agents (Claude Code, Codex, Cursor, Windsurf, Antigravity, Copilot, Gemini, Kiro, Pi).",
5
5
  "bin": {
6
6
  "draftgo": "bin/draftgo.js"
@@ -0,0 +1,56 @@
1
+ package sdk
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "testing"
7
+ )
8
+
9
+ type authRecordingClient struct {
10
+ operation string
11
+ args map[string]any
12
+ err error
13
+ }
14
+
15
+ func (client *authRecordingClient) Call(_ context.Context, operation string, args any) (any, error) {
16
+ client.operation = operation
17
+ client.args, _ = args.(map[string]any)
18
+ return true, client.err
19
+ }
20
+
21
+ func TestAuthAssertionsUseHostAuthorization(t *testing.T) {
22
+ client := &authRecordingClient{}
23
+ draftgo := NewContext(context.Background(), nil, map[string]any{
24
+ "id": float64(7), "is_admin": false, "role_code": "member",
25
+ }, nil, client)
26
+
27
+ if err := draftgo.Auth.RequireAdmin(); err != nil {
28
+ t.Fatal(err)
29
+ }
30
+ if client.operation != "auth.require_admin" {
31
+ t.Fatalf("operation = %q", client.operation)
32
+ }
33
+ if err := draftgo.Auth.RequireRole(" workspace_owner "); err != nil {
34
+ t.Fatal(err)
35
+ }
36
+ if client.operation != "auth.require_role" || client.args["role"] != "workspace_owner" {
37
+ t.Fatalf("operation=%q args=%#v", client.operation, client.args)
38
+ }
39
+
40
+ client.err = errors.New("denied by host")
41
+ if err := draftgo.Auth.RequireAdmin(); err == nil || err.Error() != "denied by host" {
42
+ t.Fatalf("host denial was not returned: %v", err)
43
+ }
44
+ }
45
+
46
+ func TestAdminAuthCallCarriesScopedElevation(t *testing.T) {
47
+ client := &authRecordingClient{}
48
+ draftgo := NewContext(context.Background(), nil, nil, nil, client)
49
+
50
+ if err := draftgo.Admin.Auth.RequireAdmin(); err != nil {
51
+ t.Fatal(err)
52
+ }
53
+ if client.operation != "auth.require_admin" || client.args["__draftgo_admin"] != true {
54
+ t.Fatalf("operation=%q args=%#v", client.operation, client.args)
55
+ }
56
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": 1,
3
3
  "module": "draftgo/sdk",
4
- "fingerprint": "c750610f8d4b1652da259ea436d47d9856104261c9af1b0e3753c073d36d4693",
4
+ "fingerprint": "94d0b5064c3d5c3d742ddf2856ed6e37196ff81bd16696aa88cd8267c3f57889",
5
5
  "files": [
6
6
  {
7
7
  "path": "ai.go",
@@ -13,6 +13,11 @@
13
13
  "sha256": "d5a906ea3d9d0451f116d2acac54bfb83669e5e471d1d5ada7c0e1fdabef13b6",
14
14
  "bytes": 6531
15
15
  },
16
+ {
17
+ "path": "auth_test.go",
18
+ "sha256": "f0a02db739c8c5fa9d2856c59bd7ec823194cae50c1445f7b1340ce3dc0a616f",
19
+ "bytes": 1604
20
+ },
16
21
  {
17
22
  "path": "billing.go",
18
23
  "sha256": "9ce87c80885aa8b8996710622ec0f14cc7a74d3ca78e5ed4168b6c135994d583",
@@ -30,8 +35,8 @@
30
35
  },
31
36
  {
32
37
  "path": "platform.go",
33
- "sha256": "ec0d8621a8bb677578001d029fe191af58df9d275204eced5e8061b339ad085b",
34
- "bytes": 12373
38
+ "sha256": "deb0ab4a91dbb9a1f07974982ffb218d89f719b3691cf5e9a955abcc86a6d2ba",
39
+ "bytes": 11923
35
40
  },
36
41
  {
37
42
  "path": "platform_logger_test.go",
@@ -65,8 +70,8 @@
65
70
  },
66
71
  {
67
72
  "path": "sdk.go",
68
- "sha256": "95ffcc335416ac842e06ea51b75e6739a04ddd5573a946e4175d6c5f8c110a67",
69
- "bytes": 8167
73
+ "sha256": "b67db621642c389a2000e52b1640bacccf23a6d94f1fdcc1989e6a4244821924",
74
+ "bytes": 8224
70
75
  }
71
76
  ]
72
77
  }
@@ -6,6 +6,7 @@ import (
6
6
  "errors"
7
7
  "fmt"
8
8
  "net/http"
9
+ "strings"
9
10
  "sync"
10
11
  "time"
11
12
  )
@@ -320,41 +321,25 @@ func asInt(value any) int {
320
321
  }
321
322
  }
322
323
 
323
- type authClient struct{ user map[string]any }
324
+ type authClient struct {
325
+ platformClient
326
+ user map[string]any
327
+ }
324
328
 
325
329
  func (a authClient) CurrentUser() map[string]any { return a.user }
326
330
  func (a authClient) RequireLogin() error {
327
- if a.user == nil {
328
- return errors.New("require_login failed: user is not authenticated")
329
- }
330
- return nil
331
+ _, err := a.call(a.context, "auth.require_login", map[string]any{})
332
+ return err
331
333
  }
332
334
  func (a authClient) RequireAdmin() error {
333
- if err := a.RequireLogin(); err != nil {
334
- return err
335
- }
336
- if a.user["is_admin"] == true || a.user["role_code"] == "admin" {
337
- return nil
338
- }
339
- for _, role := range asSlice(a.user["roles"]) {
340
- if item, ok := role.(map[string]any); ok && item["code"] == "admin" {
341
- return nil
342
- }
343
- }
344
- return errors.New("require_admin failed: user is not an administrator")
335
+ _, err := a.call(a.context, "auth.require_admin", map[string]any{})
336
+ return err
345
337
  }
346
338
  func (a authClient) RequireRole(role string) error {
347
- if err := a.RequireLogin(); err != nil {
348
- return err
349
- }
350
- if a.user["role_code"] == role {
351
- return nil
339
+ role = strings.TrimSpace(role)
340
+ if role == "" {
341
+ return errors.New("require_role failed: role is required")
352
342
  }
353
- for _, raw := range asSlice(a.user["roles"]) {
354
- if item, ok := raw.(map[string]any); ok && item["code"] == role {
355
- return nil
356
- }
357
- }
358
- return fmt.Errorf("require_role failed: missing role %q", role)
343
+ _, err := a.call(a.context, "auth.require_role", map[string]any{"role": role})
344
+ return err
359
345
  }
360
- func asSlice(value any) []any { items, _ := value.([]any); return items }
@@ -151,11 +151,11 @@ func NewContextWithResource(draftgo context.Context, input, user map[string]any,
151
151
  return &Context{
152
152
  Input: input, User: user, Headers: headers, context: draftgo,
153
153
  Platform: resource.ScopeType == ScopePlatform, WorkspaceID: resource.WorkspaceID, SpaceID: resource.SpaceID, Principal: principal,
154
- DB: dbClient{platform}, Users: usersClient{platform}, Auth: authClient{user: user}, Notify: notifierClient{platform},
154
+ DB: dbClient{platform}, Users: usersClient{platform}, Auth: authClient{platformClient: platform, user: user}, Notify: notifierClient{platform},
155
155
  HTTP: httpClient{platform}, Cache: cacheClient{platform}, Config: configClient{platform}, AIHub: aiHubClient{platform},
156
156
  Knowledge: knowledgeClient{platform}, Memory: memoryClient{platform}, Files: fileStoreClient{platform}, Scope: scopeClient{platform}, Billing: billingClient{platform},
157
157
  Admin: Admin{
158
- DB: dbClient{adminPlatform}, Users: usersClient{adminPlatform}, Auth: authClient{user: user}, Notify: notifierClient{adminPlatform},
158
+ DB: dbClient{adminPlatform}, Users: usersClient{adminPlatform}, Auth: authClient{platformClient: adminPlatform, user: user}, Notify: notifierClient{adminPlatform},
159
159
  HTTP: httpClient{adminPlatform}, Cache: cacheClient{adminPlatform}, Config: configClient{adminPlatform}, AIHub: aiHubClient{adminPlatform},
160
160
  Knowledge: knowledgeClient{adminPlatform}, Memory: memoryClient{adminPlatform},
161
161
  Files: fileStoreClient{adminPlatform}, Scope: scopeClient{adminPlatform}, Billing: billingClient{adminPlatform},
@@ -47,7 +47,7 @@ description: Use this skill to inspect, develop, debug, or deliver a DraftGo app
47
47
  - 同一文件或 DraftGo 资源全程只能由一个 Agent 修改;不同资源、operation 和 owner 不冲突的单元可以并发,不设置客户端并发上限;同一资源的依赖步骤保持串行。主 Agent 负责 owner 分配、汇总、验证、交付和 worklog 状态。
48
48
  - 用户 API Key 只保存在 `.draftgo/config.json`,不得进入宿主配置、命令参数、Skill、日志、manifest 或错误消息。宿主 MCP 配置只运行 `draftgo mcp serve`。
49
49
  - 页面优先使用可信本地资源;禁止境外 CDN。净化不可信 HTML,关键状态不能只靠颜色或动效表达。页面通过 `window.parent.App` 使用认证、权限、反馈和主题;详细规则见前端与运行时 Reference。
50
- - 普通自定义服务 SDK 调用继承调用者、服务归属和 AccessGrant;`ctx.Admin.*` 是可信服务显式选择的单次管理提升,允许跨用户/空间操作且必须进入审计,不会泄漏到后续调用。高风险 MCP operation 仅在用户意图明确且影响范围已核对时设置 `confirm=true`。
50
+ - 普通自定义服务 SDK 调用继承调用者、服务归属和 AccessGrant;`ctx.Admin.*` 是仅供自定义服务显式选择的单次系统内部授权,按 `*:*:all` 全权限执行并进入审计,不会泄漏到后续调用。高风险 MCP operation 仅在用户意图明确且影响范围已核对时设置 `confirm=true`。
51
51
  - `.draftgo/tmp/` 可清理;先运行 `draftgo clean --dry-run`,再在需要时使用 `--yes`。用户可查验的证据放入已注册的 `.draftgo/artifacts/`。
52
52
 
53
53
  ## 完成条件
@@ -2,7 +2,7 @@
2
2
  "schema_version": "1.0",
3
3
  "id": "draftgo",
4
4
  "name": "DraftGo 开发助手",
5
- "version": "4.0.22",
5
+ "version": "4.0.23",
6
6
  "entry": "SKILL.md",
7
7
  "description": "以 Skill/reference 任务路由、MCP 实时发现、长正文 checkout/commit、统一验证和完成日志为边界的 DraftGo 工作流。",
8
8
  "license": "MIT",
@@ -62,7 +62,7 @@ func health(ctx *sdk.Context) (any, error) {
62
62
 
63
63
  输入字段为 `method`、`headers`、`body`、`query_params`、`path_params`。身份由 `ctx.Auth.CurrentUser()` 获取。普通 `ctx.DB`、`ctx.Users`、`ctx.Billing` 等调用继承请求调用者权限;管理员创建服务、持有服务凭据或 `RequireAdmin` 都不会自动提升 SDK 调用。
64
64
 
65
- `ctx.Admin.*` 是可信服务显式选择的单次管理提升:保持服务主体与已验证执行上下文,但可执行平台级或跨用户/空间管理操作;每次调用都会进入审计,提升不会泄漏到后续普通调用。服务代码仍拿不到服务凭据、数据库连接或管理员凭据。公开 Route 不应无条件调用 Admin;先完成业务鉴权、参数校验和幂等设计,返回值仍需自行脱敏。
65
+ `ctx.Admin.*` 是自定义服务显式选择的单次系统内部授权:该次 RPC 按 `*:*:all` 全权限执行,可跨用户/空间,不检查调用者或服务主体 AccessGrant;服务主体仅保留用于 ownership 与审计。每次调用都会进入审计,内部授权不会泄漏到后续普通调用。服务代码仍拿不到服务凭据、数据库连接或管理员凭据。公开 Route 不应无条件调用 Admin;先完成业务鉴权、参数校验和幂等设计,返回值仍需自行脱敏。
66
66
 
67
67
  ### Event
68
68
 
@@ -126,7 +126,7 @@ response, err := ctx.HTTP.Get(ctx.Context(), "https://api.example.com/health", n
126
126
  | 场景 | 方法 |
127
127
  |---|---|
128
128
  | 当前调用者操作服务归属内账务 | `ctx.Billing.*` |
129
- | 可信服务执行需要管理员权限的账务管理 | `ctx.Admin.Billing.*`;目标仍由服务端资源归属决定 |
129
+ | 自定义服务执行系统级账务管理 | `ctx.Admin.Billing.*`;单次调用按系统全权限执行并审计 |
130
130
  | 金额已确定且应立即扣除 | `DebitAccount` |
131
131
  | 最终金额不确定或业务可能失败 | `HoldFunds` -> `SettleHold`;失败时 `ReleaseHold` |
132
132
  | 更正已入账流水 | `ReverseJournal`,不要用反向充值伪造冲正 |
@@ -179,7 +179,7 @@ typed helper 未覆盖的新端点可使用 `ctx.AIHub.Request`,但 path 只
179
179
  - Role 不带作用域;服务必须通过覆盖持久化 ResourceOwnership 的有效 AccessGrant 授权。工作区成员关系不能单独授权。
180
180
  - platform Grant 可跨空间但只能使用显式权限;space Grant 不能跨根。请求中的范围不能覆盖服务已保存的归属。
181
181
  - 普通服务调用访问动态 DB 时仍受目标 DB 的 DataRange(`none` / `own` / `all`)限制;`ctx.Admin.*` 只应用于源码明确选择的单次可信管理操作,不能从请求参数隐式开启。
182
- - Route 仍受服务 `permission` `config.route_security` 控制。
182
+ - Route 不接受服务级 `permission`;入口由宿主认证、服务 ownership 和调用者 `scripts:execute` AccessGrant 控制,业务级公开/登录规则在 handler 内显式实现。
183
183
  - 定时任务和无可解析用户的事件默认使用服务主体及其持久化空间;需要平台级或跨归属管理时必须在对应单次调用显式使用 `ctx.Admin.*`。
184
184
  - 服务 principal 由平台运行时注入;不要在 `service.json`、源码、测试参数或日志中保存/模拟服务身份凭据。Route/Event 取调用者与服务 Grant 的权限交集,Scheduled 使用服务持久化 ownership,而不是假造用户 API Key。
185
185
  - `config.timeout`、`max_concurrency`、`queue_timeout_ms` 控制执行;Route 饱和返回 429。
@@ -88,7 +88,7 @@ AI 新建或扩展受保护模块时,按以下最小闭环逐项确认;缺
88
88
  - `Event`:响应平台事件,如 `db.created` / `db.updated` / `user.registered`;用 `app.On(event, handler)` 注册,可为同一事件注册多个 handler。
89
89
  - `Scheduled`:用 `app.Schedule("分 时 日 月 周", handler)` 注册 cron;同一服务可声明多个定时 handler。
90
90
  - 新 Go 服务的 `Register` 是唯一触发器事实来源;旧 `triggers` 字段不参与注册。
91
- - 管理面由角色 RBAC 的 `scripts:*` 动作控制;Route 调用面继续由每个服务的 `permission` / `route_security` 独立控制,不要把两层权限混为一谈。
91
+ - 管理面由角色 RBAC 的 `scripts:*` 动作控制;Route 不再读取服务级 `permission`,由宿主认证、持久化 ownership `scripts:execute` AccessGrant 统一控制。
92
92
  - Go 服务在独立子进程中构建/运行;只有可信角色才能获得 `scripts:create` / `scripts:update`。
93
93
  - 自定义服务适合服务端加工、鉴权后聚合、第三方回调、定时任务和事件响应;普通 CRUD 管理界面优先用“页面 + 动态 DB”,不要把所有业务后台都塞进 route 脚本。
94
94
  - 脚本内读动态 DB 用 `ctx.DB.Query(type, sdk.QueryOptions{...})`;返回 `sdk.QueryResult`,分页和筛选见 `references/custom-services.md`。
@@ -44,6 +44,24 @@ function jsonRpcMessages(value) {
44
44
  return value && typeof value === 'object' ? [value] : [];
45
45
  }
46
46
 
47
+ function routingHeaders(message) {
48
+ const messages = jsonRpcMessages(message);
49
+ if (messages.length !== 1 || typeof messages[0].method !== 'string' || !messages[0].method) return {};
50
+ const request = messages[0];
51
+ const headers = { 'Mcp-Method': request.method };
52
+ const params = request.params && typeof request.params === 'object' && !Array.isArray(request.params)
53
+ ? request.params
54
+ : {};
55
+ if (request.method === 'tools/call' && typeof params.name === 'string' && params.name) {
56
+ headers['Mcp-Name'] = params.name;
57
+ } else if (request.method === 'prompts/get' && typeof params.name === 'string' && params.name) {
58
+ headers['Mcp-Name'] = params.name;
59
+ } else if (request.method === 'resources/read' && typeof params.uri === 'string' && params.uri) {
60
+ headers['Mcp-Name'] = params.uri;
61
+ }
62
+ return headers;
63
+ }
64
+
47
65
  async function parseEventStream(body, onMessage) {
48
66
  if (!body) return [];
49
67
  const reader = body.getReader();
@@ -113,6 +131,7 @@ async function postJsonRpc(config, message, options = {}) {
113
131
  Accept: 'application/json, text/event-stream',
114
132
  'Content-Type': 'application/json',
115
133
  'MCP-Protocol-Version': options.protocolVersion || DEFAULT_PROTOCOL_VERSION,
134
+ ...routingHeaders(message),
116
135
  };
117
136
  if (options.sessionId) headers['Mcp-Session-Id'] = options.sessionId;
118
137
 
@@ -169,6 +188,7 @@ module.exports = {
169
188
  McpHttpError,
170
189
  redactText,
171
190
  endpointFor,
191
+ routingHeaders,
172
192
  postJsonRpc,
173
193
  parseEventStream,
174
194
  };