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 +1 -1
- package/package.json +1 -1
- package/resources/custom-service-sdk/auth_test.go +56 -0
- package/resources/custom-service-sdk/manifest.json +10 -5
- package/resources/custom-service-sdk/platform.go +14 -29
- package/resources/custom-service-sdk/sdk.go +2 -2
- package/resources/skill/SKILL.md +1 -1
- package/resources/skill/manifest.json +1 -1
- package/resources/skill/references/custom-services.md +3 -3
- package/resources/skill/references/modules.md +1 -1
- package/src/mcp/protocol.js +20 -0
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`
|
|
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.
|
|
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": "
|
|
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": "
|
|
34
|
-
"bytes":
|
|
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": "
|
|
69
|
-
"bytes":
|
|
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{
|
|
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
|
-
|
|
328
|
-
|
|
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
|
-
|
|
334
|
-
|
|
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
|
-
|
|
348
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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},
|
package/resources/skill/SKILL.md
CHANGED
|
@@ -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.*`
|
|
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
|
## 完成条件
|
|
@@ -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.*`
|
|
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
|
-
|
|
|
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
|
|
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
|
|
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`。
|
package/src/mcp/protocol.js
CHANGED
|
@@ -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
|
};
|