@clawos-dev/clawd 0.2.446 → 0.2.448

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.cjs CHANGED
@@ -7594,6 +7594,14 @@ var init_schemas = __esm({
7594
7594
  // 快速问答临时会话标记(spec 2026-06-23-fork-quick-ask):true = 关闭抽屉即删、
7595
7595
  // 不进 sidebar 树、daemon 启动清扫孤儿。普通会话缺省。
7596
7596
  ephemeral: external_exports.boolean().optional(),
7597
+ /**
7598
+ * 这条会话属于哪张部署单(persona 上云的环境扫描会话)。
7599
+ *
7600
+ * 有值 = 它在单据页右栏里活着,**sidebar 不再列它**——同一条会话两处出现,和话题 worker
7601
+ * 会话不进「最近」是同一条规矩。不复用 `ephemeral`:那个标记会被 daemon 启动清扫**删掉**,
7602
+ * 而扫描会话要跟单据一样长期留着(人隔天回来还要翻管家当时是怎么判的、还要接着追问)。
7603
+ */
7604
+ deployId: external_exports.string().min(1).optional(),
7597
7605
  // owner-mode persona session 身份标识;UI 用它在 SessionList 过滤 + jump,
7598
7606
  // daemon 用它做 idempotent dedupe + spawn 时派生 ctx.personaMode='owner'
7599
7607
  ownerPersonaId: external_exports.string().min(1).optional(),
@@ -8796,9 +8804,13 @@ var init_methods = __esm({
8796
8804
  args: WorkspaceReadArgs
8797
8805
  },
8798
8806
  "deploy:start": {
8799
- summary: "\u5BF9\u67D0\u4E2A persona \u53D1\u8D77\u4E00\u6B21\u4E0A\u4E91\u90E8\u7F72\uFF1A\u5EFA\u5355\u636E\u5E76**\u5F02\u6B65**\u8D77\u73AF\u5883\u626B\u63CF\u3002\u7ACB\u523B\u8FD4\u56DE scanning \u6001\u5355\u636E\uFF0C\u626B\u63CF\u8981\u8DD1\u51E0\u5206\u949F\u3001\u626B\u5B8C\u7531\u540E\u53F0\u56DE\u5199\uFF1Bowner-only",
8807
+ summary: "\u6253\u5F00\u67D0\u4E2A persona \u7684\u90E8\u7F72\u5355\uFF1A**\u5DF2\u6709\u5C31\u539F\u6837\u8FD4\u56DE\u90A3\u5F20**\uFF08\u8349\u7A3F\u7EED\u586B\uFF0C\u4E0D\u91CD\u626B\uFF0C\u4E5F\u4E0D\u65B0\u5EFA\uFF09\uFF0C\u6CA1\u6709\u624D\u5EFA\u4E00\u5F20\u5E76**\u5F02\u6B65**\u8D77\u73AF\u5883\u626B\u63CF\u3002\u4E00\u4E2A persona \u540C\u65F6\u53EA\u6709\u4E00\u5F20\u5355\uFF0C\u8981\u91CD\u626B\u8D70 deploy:rescan\uFF1Bowner-only",
8800
8808
  args: DeployStartArgsSchema
8801
8809
  },
8810
+ "deploy:rescan": {
8811
+ summary: "\u5C31\u5730\u91CD\u626B\u4E00\u5F20\u5DF2\u6709\u7684\u90E8\u7F72\u5355\uFF1A\u6E05\u6389\u4E0A\u6B21\u626B\u51FA\u6765\u7684\u6E05\u5355 / \u8BC1\u636E / \u5B58\u7591\u9879\uFF0C\u72B6\u6001\u56DE scanning\uFF0C\u8D77\u4E00\u4E2A\u65B0\u7684\u626B\u63CF\u4F1A\u8BDD\u3002**\u5DF2\u586B\u7684\u51ED\u636E\u503C\u548C\u5C65\u7EA6\u9009\u62E9\u539F\u6837\u4FDD\u7559**\uFF08\u5B83\u4EEC\u6309\u5355\u636E id \u5B58\uFF0C\u4E0D\u968F\u626B\u63CF\u7ED3\u679C\u8D70\uFF09\uFF0C\u6240\u4EE5\u91CD\u626B\u4E0D\u4F1A\u8BA9\u4EBA\u767D\u586B\u4E00\u904D\uFF1Bowner-only",
8812
+ args: DeployIdArgsSchema
8813
+ },
8802
8814
  "deploy:list": {
8803
8815
  summary: "\u5217\u51FA\u5168\u90E8\u90E8\u7F72\u5355\uFF08\u65B0\u7684\u5728\u524D\uFF09\uFF1Bowner-only\u3002\u65E0\u53C2\u6570",
8804
8816
  args: NO_ARGS
@@ -48645,6 +48657,7 @@ var SessionManager = class {
48645
48657
  // persona 绑定的 model / effort 快照进 SessionFile(spec 2026-08-12)
48646
48658
  ...personaDefaultFields(personaDefaults),
48647
48659
  ...args.sourceSessionId ? { shiftFiredFromSessionId: args.sourceSessionId } : {},
48660
+ ...args.deployId ? { deployId: args.deployId } : {},
48648
48661
  createdAt: now,
48649
48662
  updatedAt: now
48650
48663
  };
@@ -54406,8 +54419,8 @@ function buildDeployHandlers(deps) {
54406
54419
  if (!deps.hasPersona(args.personaId)) {
54407
54420
  throw new ClawdError(ERROR_CODES.PERSONA_NOT_FOUND, `PERSONA_NOT_FOUND: \u6CA1\u6709\u8FD9\u4E2A persona\uFF08${args.personaId}\uFF09`);
54408
54421
  }
54409
- const inFlight = deps.receipts.list().find((r) => r.personaId === args.personaId && r.status === "scanning");
54410
- if (inFlight) return view(inFlight);
54422
+ const existing = deps.receipts.list().find((r) => r.personaId === args.personaId);
54423
+ if (existing) return view(existing);
54411
54424
  const receipt = deps.receipts.create({
54412
54425
  deployId: deps.genId(),
54413
54426
  personaId: args.personaId,
@@ -54417,6 +54430,26 @@ function buildDeployHandlers(deps) {
54417
54430
  if (scanSessionId) deps.receipts.update(receipt.deployId, { scanSessionId });
54418
54431
  return view(deps.receipts.get(receipt.deployId) ?? receipt);
54419
54432
  };
54433
+ const rescan = async (frame, _client, ctx) => {
54434
+ ensureOwner(ctx);
54435
+ const args = parseArgs2(DeployIdArgsSchema, frame, "deploy:rescan");
54436
+ const receipt = mustGet(deps, args.deployId);
54437
+ if (receipt.status === "scanning") {
54438
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, "VALIDATION_ERROR: \u8FD9\u5F20\u5355\u6B63\u5728\u626B\u63CF\u4E2D\uFF0C\u7B49\u5B83\u626B\u5B8C\u518D\u91CD\u626B");
54439
+ }
54440
+ deps.receipts.update(args.deployId, {
54441
+ status: "scanning",
54442
+ manifest: void 0,
54443
+ evidence: [],
54444
+ notes: [],
54445
+ smokeTask: void 0,
54446
+ error: void 0,
54447
+ scanSessionId: void 0
54448
+ });
54449
+ const scanSessionId = deps.startScan(args.deployId, receipt.personaId);
54450
+ if (scanSessionId) deps.receipts.update(args.deployId, { scanSessionId });
54451
+ return view(deps.receipts.get(args.deployId) ?? receipt);
54452
+ };
54420
54453
  const list = async (_frame, _client, ctx) => {
54421
54454
  ensureOwner(ctx);
54422
54455
  return { response: { type: "deploy:list", receipts: deps.receipts.list() } };
@@ -54517,6 +54550,7 @@ function buildDeployHandlers(deps) {
54517
54550
  };
54518
54551
  return {
54519
54552
  "deploy:start": start,
54553
+ "deploy:rescan": rescan,
54520
54554
  "deploy:list": list,
54521
54555
  "deploy:get": get,
54522
54556
  "deploy:patch": patch,
@@ -63379,6 +63413,7 @@ var METHOD_GRANT_MAP = {
63379
63413
  // deploy:*(persona 上云的部署单):全部 owner-only。单据里有履约决策、凭据录入口,
63380
63414
  // guest 不该看见更不该动。
63381
63415
  "deploy:start": ADMIN_ANY,
63416
+ "deploy:rescan": ADMIN_ANY,
63382
63417
  "deploy:list": ADMIN_ANY,
63383
63418
  "deploy:get": ADMIN_ANY,
63384
63419
  "deploy:patch": ADMIN_ANY,
@@ -63631,7 +63666,7 @@ function computeMethodAccess(args) {
63631
63666
  }
63632
63667
 
63633
63668
  // src/version.ts
63634
- var version = "0.2.446".length > 0 ? "0.2.446" : "dev";
63669
+ var version = "0.2.448".length > 0 ? "0.2.448" : "dev";
63635
63670
 
63636
63671
  // src/cli-probe/probe.ts
63637
63672
  var fs68 = __toESM(require("fs"), 1);
@@ -68628,7 +68663,7 @@ async function startDaemon(config) {
68628
68663
  ].join("\n");
68629
68664
  let handle;
68630
68665
  try {
68631
- handle = manager.createShiftSession({ targetPersona: BUTLER_PERSONA_ID, prompt });
68666
+ handle = manager.createShiftSession({ targetPersona: BUTLER_PERSONA_ID, prompt, deployId });
68632
68667
  } catch (err) {
68633
68668
  deployReceipts.update(deployId, { status: "scanFailed", error: `\u8D77\u626B\u63CF\u4F1A\u8BDD\u5931\u8D25\uFF1A${String(err)}` });
68634
68669
  logger.error("deploy scan session failed to start", { deployId, personaId: personaId2, error: String(err) });
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================
3
+ # 发布/清理动手前把阿里云凭证准备好(publish.sh / remove-extension.sh 共用)。
4
+ # 被 `source` 进来,再调 ensure_credentials。跟 ensure-toolchain.sh 是一对:
5
+ # 那个管「命令在不在」,这个管「命令拿什么身份跑」。
6
+ #
7
+ # 硬约束:**全程不碰用户全局 ~/.s**。clawd 发布用的是 ship 出来的共享 demo AK,
8
+ # 而 ~/.s/access.yaml 里的 default 是用户自己账号的 Serverless Devs 凭证——它是用户的
9
+ # 东西,不是我们的。做法对齐本 kit 处理 aliyun CLI 的方式(凭证只进本次进程的 env,
10
+ # 全局 ~/.aliyun/ 一个字节不碰):把 Serverless Devs 的配置家目录整个指到 deploy-kit
11
+ # 名下,access.yaml 写在那里。
12
+ # ============================================================
13
+
14
+ # s 的 profile 名。刻意不叫 default:万一哪天某个版本的 s 不认 serverless_devs_config_home,
15
+ # 隔离失效时它会退回去用户的 ~/.s/access.yaml 里找 clawd-fc —— 找不到就
16
+ # `Not found access: clawd-fc` 硬报错,而不是默默拿用户的 default 凭证把 extension
17
+ # 部署进别人的账号。调 s 时都显式带 `--access "$S_ACCESS_ALIAS"`:命令行参数覆盖 s.yaml
18
+ # 里的 access 字段,本改动前 scaffold 的老项目(s.yaml 写着 default)照样走这条。
19
+ S_ACCESS_ALIAS=clawd-fc
20
+
21
+ # 分层加载阿里云 AK 进 env:shared demo(OTA 每次启动覆盖)→ 本机 .local override(永不被覆盖)。
22
+ ensure_aliyun_env() {
23
+ local kit="${KIT_DIR:?ensure_aliyun_env 需要调用方先设 KIT_DIR}"
24
+ [ -f "$kit/.secrets/aliyun.env" ] || {
25
+ echo "❌ 缺 $kit/.secrets/aliyun.env(clawd ship 的共享 demo 凭证,OTA 自动刷新)。参考 .secrets/aliyun.env.example 填写。" >&2
26
+ return 1
27
+ }
28
+ set -a; . "$kit/.secrets/aliyun.env"; set +a
29
+ # aliyun.env.local: 本机个人 override(永远不被 OTA 覆盖,见 refreshDeployKit)。有就后 source 覆盖 env。
30
+ # 写成 if 而不是 `[ -f ] && { }`:后者在函数末尾会把「文件不存在」变成函数返回 1,调用方 set -e 直接中止。
31
+ if [ -f "$kit/.secrets/aliyun.env.local" ]; then
32
+ set -a; . "$kit/.secrets/aliyun.env.local"; set +a
33
+ fi
34
+ export ALIBABA_CLOUD_REGION_ID="${REGION:?config.env 缺 REGION}"
35
+ }
36
+
37
+ # 把 Serverless Devs 的配置家目录(access.yaml / 组件缓存 / 日志)指到 deploy-kit 名下,
38
+ # 再把当前 env 里的凭证写成 $S_ACCESS_ALIAS profile。
39
+ # s v3 的 rootHome = `$serverless_devs_config_home/.s`(默认 `$HOME/.s`),`s -v` 打印的 s-home 就是它——
40
+ # 注意差一层:env 指到 X,文件落 X/.s/,所以下面是 "$s_home/.s/access.yaml" 而不是 "$s_home/access.yaml"。
41
+ # 前置:ensure_toolchain(aliyun 可执行)+ ensure_aliyun_env(AK 进 env)。
42
+ # 放 $KIT_DIR 下而不是另起路径:跟它写的那份凭证同源,且 refreshDeployKit 只覆盖
43
+ # scripts/ contract/ .secrets/,OTA 不会动这里的组件缓存。
44
+ ensure_s_access() {
45
+ local kit="${KIT_DIR:?ensure_s_access 需要调用方先设 KIT_DIR}"
46
+ local s_home="$kit/.s-home"
47
+ local ident acc
48
+
49
+ ident="$(aliyun --mode AK sts GetCallerIdentity 2>&1)" || {
50
+ echo "❌ aliyun sts GetCallerIdentity 失败(检查 .secrets/aliyun.env 凭证):" >&2
51
+ echo "$ident" >&2
52
+ return 1
53
+ }
54
+ # 用 node 不用 python3:目标机器保证有 node(clawd 本身就是 node 跑的),不保证有 python
55
+ # —— 清理路径尤其不该多一个装不上就跑不了的依赖。
56
+ acc="$(printf '%s' "$ident" | node -e 'console.log(JSON.parse(require("fs").readFileSync(0,"utf8")).AccountId)')" || {
57
+ echo "❌ 解析 AccountId 失败,aliyun 输出非 JSON:" >&2
58
+ echo "$ident" >&2
59
+ return 1
60
+ }
61
+
62
+ export serverless_devs_config_home="$s_home"
63
+ mkdir -p "$s_home/.s"
64
+ # umask 管「新建」:SK 从落盘第一个字节起就是 0600,不留「先 644 写完再 chmod」的窗口。
65
+ # 后面的 chmod 管「文件已存在」:umask 不改已有文件的 mode,重复发布时靠它兜底。
66
+ ( umask 077; cat > "$s_home/.s/access.yaml" <<EOF
67
+ $S_ACCESS_ALIAS:
68
+ AccountID: '$acc'
69
+ AccessKeyID: $ALIBABA_CLOUD_ACCESS_KEY_ID
70
+ AccessKeySecret: $ALIBABA_CLOUD_ACCESS_KEY_SECRET
71
+ EOF
72
+ )
73
+ chmod 600 "$s_home/.s/access.yaml"
74
+ }
75
+
76
+ ensure_credentials() {
77
+ ensure_aliyun_env
78
+ ensure_s_access
79
+ }
@@ -0,0 +1,204 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
+ import { execFileSync } from 'node:child_process'
3
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, statSync, cpSync, rmSync } from 'node:fs'
4
+ import { join } from 'node:path'
5
+ import { tmpdir } from 'node:os'
6
+
7
+ // ensure-credentials.sh 的契约测试。核心不变量:**发布/清理全程不碰用户全局 ~/.s**。
8
+ // 老实现 `cat > ~/.s/access.yaml` 会把用户自己的 Serverless Devs default 凭证顶掉且不可恢复
9
+ // (所有装了 clawd 的用户点一次「发布上线」就中招),这些用例就是钉死那个回归。
10
+ //
11
+ // 真脚本 + 真 bash 跑,只把两个外部 CLI(aliyun / s)换成记录 argv 的 shim:
12
+ // 断言的是脚本自己的行为(写哪、写什么、传什么 flag),没有 mock 掉被测逻辑。
13
+
14
+ const SCRIPTS_DIR = __dirname
15
+
16
+ interface Sandbox {
17
+ root: string
18
+ kit: string
19
+ home: string
20
+ bin: string
21
+ }
22
+
23
+ let box: Sandbox
24
+
25
+ /** 用户自己的 ~/.s/access.yaml —— 全程必须一个字节都不变。 */
26
+ const USER_ACCESS_YAML = `default:
27
+ AccountID: '1999888877776666'
28
+ AccessKeyID: LTAI-USER-OWN-KEY
29
+ AccessKeySecret: user-own-secret
30
+ `
31
+
32
+ /** aliyun CLI shim:`sts GetCallerIdentity` 回真 CLI 那样的 JSON,其余子命令原样成功。 */
33
+ function writeAliyunShim(bin: string, accountId = '5566778899001122'): void {
34
+ writeFileSync(
35
+ join(bin, 'aliyun'),
36
+ `#!/usr/bin/env bash\nprintf '{"AccountId":"${accountId}","Arn":"acs:ram::${accountId}:user/clawd-fc-developer"}\\n'\n`,
37
+ { mode: 0o755 },
38
+ )
39
+ }
40
+
41
+ /** s shim:把每次调用的 argv 追加到 $S_ARGV_LOG,供断言 --access 传对了没。 */
42
+ function writeSShim(bin: string, log: string): void {
43
+ writeFileSync(
44
+ join(bin, 's'),
45
+ `#!/usr/bin/env bash\nprintf '%s\\n' "$*" >> "${log}"\n`,
46
+ { mode: 0o755 },
47
+ )
48
+ }
49
+
50
+ beforeEach(() => {
51
+ const root = mkdtempSync(join(tmpdir(), 'ensure-creds-'))
52
+ const kit = join(root, 'deploy-kit')
53
+ const home = join(root, 'home')
54
+ const bin = join(root, 'bin')
55
+ mkdirSync(join(kit, '.secrets'), { recursive: true })
56
+ mkdirSync(join(home, '.s'), { recursive: true })
57
+ mkdirSync(bin, { recursive: true })
58
+
59
+ // 真脚本进沙箱 kit(.secrets 用假凭证,绝不碰 repo 里 ship 的那份)
60
+ cpSync(SCRIPTS_DIR, join(kit, 'scripts'), { recursive: true })
61
+ writeFileSync(
62
+ join(kit, '.secrets', 'aliyun.env'),
63
+ 'ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI-SHARED-DEMO\nALIBABA_CLOUD_ACCESS_KEY_SECRET=shared-demo-secret\n',
64
+ )
65
+ writeFileSync(join(home, '.s', 'access.yaml'), USER_ACCESS_YAML)
66
+ writeAliyunShim(bin)
67
+
68
+ box = { root, kit, home, bin }
69
+ })
70
+
71
+ afterEach(() => {
72
+ rmSync(box.root, { recursive: true, force: true })
73
+ })
74
+
75
+ /** 在沙箱里 source ensure-credentials.sh 并跑一段 bash,返回 stdout。 */
76
+ function runInKit(body: string): string {
77
+ const script = `
78
+ set -euo pipefail
79
+ KIT_DIR="${box.kit}"
80
+ REGION="\${REGION:-cn-hangzhou}"
81
+ source "$KIT_DIR/scripts/ensure-credentials.sh"
82
+ ${body}
83
+ `
84
+ return execFileSync('bash', ['-c', script], {
85
+ env: { ...process.env, HOME: box.home, PATH: `${box.bin}:${process.env.PATH}` },
86
+ encoding: 'utf8',
87
+ })
88
+ }
89
+
90
+ function userAccessYaml(): string {
91
+ return readFileSync(join(box.home, '.s', 'access.yaml'), 'utf8')
92
+ }
93
+
94
+ function kitAccessYaml(): string {
95
+ return readFileSync(join(box.kit, '.s-home', '.s', 'access.yaml'), 'utf8')
96
+ }
97
+
98
+ describe('ensure_credentials', () => {
99
+ it('不碰用户全局 ~/.s/access.yaml', () => {
100
+ runInKit('ensure_credentials')
101
+ expect(userAccessYaml()).toBe(USER_ACCESS_YAML)
102
+ })
103
+
104
+ it('凭证写进 kit 私有 s-home', () => {
105
+ runInKit('ensure_credentials')
106
+ const yaml = kitAccessYaml()
107
+ expect(yaml).toContain("AccountID: '5566778899001122'")
108
+ expect(yaml).toContain('AccessKeyID: LTAI-SHARED-DEMO')
109
+ expect(yaml).toContain('AccessKeySecret: shared-demo-secret')
110
+ })
111
+
112
+ it('profile 名是 clawd-fc 而不是 default', () => {
113
+ // 隔离万一失效(某版本 s 不认 serverless_devs_config_home)时,s 会去用户
114
+ // ~/.s/access.yaml 找 clawd-fc —— 找不到就硬报错,而不是默默拿用户的 default
115
+ // 凭证把 extension 部署进别人的账号。
116
+ runInKit('ensure_credentials')
117
+ expect(kitAccessYaml()).toMatch(/^clawd-fc:$/m)
118
+ expect(kitAccessYaml()).not.toMatch(/^default:$/m)
119
+ })
120
+
121
+ it('把 Serverless Devs 的家目录指向 kit 私有目录', () => {
122
+ const out = runInKit('ensure_credentials; echo "S_HOME=$serverless_devs_config_home"')
123
+ expect(out).toContain(`S_HOME=${box.kit}/.s-home`)
124
+ })
125
+
126
+ it('access.yaml 权限 600(含 SK,不给同机其它用户读)', () => {
127
+ runInKit('ensure_credentials')
128
+ const mode = statSync(join(box.kit, '.s-home', '.s', 'access.yaml')).mode & 0o777
129
+ expect(mode.toString(8)).toBe('600')
130
+ })
131
+
132
+ it('aliyun.env.local 覆盖 shared 凭证', () => {
133
+ writeFileSync(
134
+ join(box.kit, '.secrets', 'aliyun.env.local'),
135
+ 'ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI-MY-OWN\nALIBABA_CLOUD_ACCESS_KEY_SECRET=my-own-secret\n',
136
+ )
137
+ runInKit('ensure_credentials')
138
+ expect(kitAccessYaml()).toContain('AccessKeyID: LTAI-MY-OWN')
139
+ expect(kitAccessYaml()).toContain('AccessKeySecret: my-own-secret')
140
+ })
141
+
142
+ it('缺共享凭证文件时 fail-loud', () => {
143
+ rmSync(join(box.kit, '.secrets', 'aliyun.env'))
144
+ expect(() => runInKit('ensure_credentials')).toThrow()
145
+ expect(existsSync(join(box.kit, '.s-home', '.s', 'access.yaml'))).toBe(false)
146
+ })
147
+ })
148
+
149
+ describe('publish.sh', () => {
150
+ it('用 clawd-fc profile 调 s deploy,不碰用户全局 ~/.s', () => {
151
+ const log = join(box.root, 's-argv.log')
152
+ writeSShim(box.bin, log)
153
+ // 自定义域名查询回空 → publish.sh 在 verify 段 exit 1。deploy 之后的事,
154
+ // 不影响本用例要断言的凭证行为,省掉 curl shim。
155
+ writeFileSync(
156
+ join(box.bin, 'aliyun'),
157
+ `#!/usr/bin/env bash\ncase " $* " in\n *" sts "*) printf '{"AccountId":"5566778899001122"}\\n' ;;\n *) printf '{"customDomains":[]}\\n' ;;\nesac\n`,
158
+ { mode: 0o755 },
159
+ )
160
+
161
+ const ext = join(box.root, 'proj')
162
+ const persona = join(box.root, 'persona')
163
+ mkdirSync(join(ext, 'server'), { recursive: true })
164
+ mkdirSync(join(persona, 'extension-kit', 'contract'), { recursive: true })
165
+ mkdirSync(join(box.kit, 'contract'), { recursive: true })
166
+ writeFileSync(join(box.kit, 'contract', 'bootstrap'), '#!/usr/bin/env bash\n')
167
+ writeFileSync(join(ext, 'ext.conf'), 'APP_NAME=demo\nSLUG=ab12\nCODE_DIR=./server\n')
168
+ writeFileSync(join(persona, 'extension-kit', 'config.env'), 'REGION=cn-hangzhou\n')
169
+ writeFileSync(join(persona, 'extension-kit', 'contract', 's.yaml.tmpl'), 'name: __DEPLOY_NAME__-app\naccess: clawd-fc\n')
170
+
171
+ expect(() =>
172
+ execFileSync('bash', [join(box.kit, 'scripts', 'publish.sh'), ext, persona], {
173
+ env: { ...process.env, HOME: box.home, PATH: `${box.bin}:${process.env.PATH}` },
174
+ encoding: 'utf8',
175
+ stdio: 'pipe',
176
+ }),
177
+ ).toThrow() // verify 段无域名 → exit 1,deploy 已经跑过了
178
+
179
+ expect(readFileSync(log, 'utf8')).toContain('deploy -y --access clawd-fc')
180
+ expect(userAccessYaml()).toBe(USER_ACCESS_YAML)
181
+ })
182
+ })
183
+
184
+ describe('remove-extension.sh', () => {
185
+ it('用 clawd-fc profile 调 s remove,不碰用户全局 ~/.s', () => {
186
+ const log = join(box.root, 's-argv.log')
187
+ writeSShim(box.bin, log)
188
+
189
+ const ext = join(box.root, 'proj')
190
+ const persona = join(box.root, 'persona')
191
+ mkdirSync(ext, { recursive: true })
192
+ mkdirSync(join(persona, 'extension-kit'), { recursive: true })
193
+ writeFileSync(join(ext, 'ext.conf'), 'APP_NAME=demo\nSLUG=ab12\n')
194
+ writeFileSync(join(persona, 'extension-kit', 'config.env'), 'REGION=cn-hangzhou\n')
195
+
196
+ execFileSync('bash', [join(box.kit, 'scripts', 'remove-extension.sh'), ext, persona], {
197
+ env: { ...process.env, HOME: box.home, PATH: `${box.bin}:${process.env.PATH}` },
198
+ encoding: 'utf8',
199
+ })
200
+
201
+ expect(readFileSync(log, 'utf8')).toContain('remove -y --access clawd-fc')
202
+ expect(userAccessYaml()).toBe(USER_ACCESS_YAML)
203
+ })
204
+ })
@@ -39,21 +39,8 @@ DEPLOY_NAME="${APP_NAME}-${SLUG}"
39
39
  # 2) 工具链 + 阿里云凭证(分层:shared demo + 可选 local override)
40
40
  source "$KIT_DIR/scripts/ensure-toolchain.sh"
41
41
  ensure_toolchain
42
- [ -f "$KIT_DIR/.secrets/aliyun.env" ] || { echo "❌ 缺 $KIT_DIR/.secrets/aliyun.env(clawd ship 的共享 demo 凭证,OTA 自动刷新)。参考 .secrets/aliyun.env.example 填写。" >&2; exit 1; }
43
- set -a; . "$KIT_DIR/.secrets/aliyun.env"; set +a
44
- # aliyun.env.local: 本机个人 override(永远不被 OTA 覆盖,见 refreshDeployKit)。有就后 source 覆盖 env。
45
- [ -f "$KIT_DIR/.secrets/aliyun.env.local" ] && { set -a; . "$KIT_DIR/.secrets/aliyun.env.local"; set +a; }
46
- export ALIBABA_CLOUD_REGION_ID="${REGION:?config.env 缺 REGION}"
47
- _ident="$(aliyun --mode AK sts GetCallerIdentity 2>&1)" || { echo "❌ aliyun sts GetCallerIdentity 失败(检查 .secrets/aliyun.env 凭证):" >&2; echo "$_ident" >&2; exit 1; }
48
- ACC="$(printf '%s' "$_ident" | python3 -c 'import sys,json;print(json.load(sys.stdin)["AccountId"])')" || { echo "❌ 解析 AccountId 失败,aliyun 输出非 JSON:" >&2; echo "$_ident" >&2; exit 1; }
49
- mkdir -p ~/.s
50
- cat > ~/.s/access.yaml <<EOF
51
- default:
52
- AccountID: '$ACC'
53
- AccessKeyID: $ALIBABA_CLOUD_ACCESS_KEY_ID
54
- AccessKeySecret: $ALIBABA_CLOUD_ACCESS_KEY_SECRET
55
- EOF
56
- chmod 600 ~/.s/access.yaml
42
+ source "$KIT_DIR/scripts/ensure-credentials.sh"
43
+ ensure_credentials
57
44
 
58
45
  # 3) persona hook(可选):校验/导出 persona 特化运行时变量(如 SESSION_SECRET / APP_BASE_URL / persona 密钥)
59
46
  if [ -f "$PERSONA_KIT/hooks/pre-deploy.sh" ]; then
@@ -94,7 +81,7 @@ sed "${sed_args[@]}" "$TMPL" > "$EXT_DIR/s.yaml"
94
81
  # 7) deploy + verify
95
82
  echo "::stage::deploy"
96
83
  echo "==> s deploy ($DEPLOY_NAME)"
97
- ( cd "$EXT_DIR" && s deploy -y )
84
+ ( cd "$EXT_DIR" && s deploy -y --access "$S_ACCESS_ALIAS" )
98
85
 
99
86
  echo "::stage::verify"
100
87
  # 精确匹配 s.yaml.tmpl 里写死的 domainName 模式: <DEPLOY_NAME>.app.clawos.chat
@@ -25,14 +25,13 @@ TABLE_PREFIX="${APP_NAME}_${SLUG:+${SLUG}_}"
25
25
  source "$KIT_DIR/scripts/ensure-toolchain.sh"
26
26
  ensure_toolchain
27
27
 
28
- # 阿里云凭证(分层:shared demo + 可选 local override)
29
- [ -f "$KIT_DIR/.secrets/aliyun.env" ] || { echo "❌ 缺 $KIT_DIR/.secrets/aliyun.env(clawd ship 的共享 demo 凭证,OTA 自动刷新)。" >&2; exit 1; }
30
- set -a; . "$KIT_DIR/.secrets/aliyun.env"; set +a
31
- [ -f "$KIT_DIR/.secrets/aliyun.env.local" ] && { set -a; . "$KIT_DIR/.secrets/aliyun.env.local"; set +a; }
28
+ # 阿里云凭证(分层:shared demo + 可选 local override)+ deploy-kit 私有的 s profile
29
+ source "$KIT_DIR/scripts/ensure-credentials.sh"
30
+ ensure_credentials
32
31
 
33
32
  # 1) 删 FC 侧(函数 + 触发器 + 自定义域名)
34
33
  echo "==> 删 FC 资源: $APP_NAME${SLUG:+ (deploy=${APP_NAME}-${SLUG})}"
35
- ( cd "$EXT_DIR" && s remove -y )
34
+ ( cd "$EXT_DIR" && s remove -y --access "$S_ACCESS_ALIAS" )
36
35
 
37
36
  # 2) 列出要删的 Supabase 表(脚本不直接调 DB,改打印 DROP 让 agent 用 supabase MCP 跑)。
38
37
  # bash 调不到 MCP(MCP 是 LLM 通道),pg 直连要密码 agent 拿不到 —— 最干净:打印 DROP,
@@ -22682,6 +22682,14 @@ var SessionFileSchema = external_exports.object({
22682
22682
  // 快速问答临时会话标记(spec 2026-06-23-fork-quick-ask):true = 关闭抽屉即删、
22683
22683
  // 不进 sidebar 树、daemon 启动清扫孤儿。普通会话缺省。
22684
22684
  ephemeral: external_exports.boolean().optional(),
22685
+ /**
22686
+ * 这条会话属于哪张部署单(persona 上云的环境扫描会话)。
22687
+ *
22688
+ * 有值 = 它在单据页右栏里活着,**sidebar 不再列它**——同一条会话两处出现,和话题 worker
22689
+ * 会话不进「最近」是同一条规矩。不复用 `ephemeral`:那个标记会被 daemon 启动清扫**删掉**,
22690
+ * 而扫描会话要跟单据一样长期留着(人隔天回来还要翻管家当时是怎么判的、还要接着追问)。
22691
+ */
22692
+ deployId: external_exports.string().min(1).optional(),
22685
22693
  // owner-mode persona session 身份标识;UI 用它在 SessionList 过滤 + jump,
22686
22694
  // daemon 用它做 idempotent dedupe + spawn 时派生 ctx.personaMode='owner'
22687
22695
  ownerPersonaId: external_exports.string().min(1).optional(),
@@ -23840,9 +23848,13 @@ var METHOD_DOCS = {
23840
23848
  args: WorkspaceReadArgs
23841
23849
  },
23842
23850
  "deploy:start": {
23843
- summary: "\u5BF9\u67D0\u4E2A persona \u53D1\u8D77\u4E00\u6B21\u4E0A\u4E91\u90E8\u7F72\uFF1A\u5EFA\u5355\u636E\u5E76**\u5F02\u6B65**\u8D77\u73AF\u5883\u626B\u63CF\u3002\u7ACB\u523B\u8FD4\u56DE scanning \u6001\u5355\u636E\uFF0C\u626B\u63CF\u8981\u8DD1\u51E0\u5206\u949F\u3001\u626B\u5B8C\u7531\u540E\u53F0\u56DE\u5199\uFF1Bowner-only",
23851
+ summary: "\u6253\u5F00\u67D0\u4E2A persona \u7684\u90E8\u7F72\u5355\uFF1A**\u5DF2\u6709\u5C31\u539F\u6837\u8FD4\u56DE\u90A3\u5F20**\uFF08\u8349\u7A3F\u7EED\u586B\uFF0C\u4E0D\u91CD\u626B\uFF0C\u4E5F\u4E0D\u65B0\u5EFA\uFF09\uFF0C\u6CA1\u6709\u624D\u5EFA\u4E00\u5F20\u5E76**\u5F02\u6B65**\u8D77\u73AF\u5883\u626B\u63CF\u3002\u4E00\u4E2A persona \u540C\u65F6\u53EA\u6709\u4E00\u5F20\u5355\uFF0C\u8981\u91CD\u626B\u8D70 deploy:rescan\uFF1Bowner-only",
23844
23852
  args: DeployStartArgsSchema
23845
23853
  },
23854
+ "deploy:rescan": {
23855
+ summary: "\u5C31\u5730\u91CD\u626B\u4E00\u5F20\u5DF2\u6709\u7684\u90E8\u7F72\u5355\uFF1A\u6E05\u6389\u4E0A\u6B21\u626B\u51FA\u6765\u7684\u6E05\u5355 / \u8BC1\u636E / \u5B58\u7591\u9879\uFF0C\u72B6\u6001\u56DE scanning\uFF0C\u8D77\u4E00\u4E2A\u65B0\u7684\u626B\u63CF\u4F1A\u8BDD\u3002**\u5DF2\u586B\u7684\u51ED\u636E\u503C\u548C\u5C65\u7EA6\u9009\u62E9\u539F\u6837\u4FDD\u7559**\uFF08\u5B83\u4EEC\u6309\u5355\u636E id \u5B58\uFF0C\u4E0D\u968F\u626B\u63CF\u7ED3\u679C\u8D70\uFF09\uFF0C\u6240\u4EE5\u91CD\u626B\u4E0D\u4F1A\u8BA9\u4EBA\u767D\u586B\u4E00\u904D\uFF1Bowner-only",
23856
+ args: DeployIdArgsSchema
23857
+ },
23846
23858
  "deploy:list": {
23847
23859
  summary: "\u5217\u51FA\u5168\u90E8\u90E8\u7F72\u5355\uFF08\u65B0\u7684\u5728\u524D\uFF09\uFF1Bowner-only\u3002\u65E0\u53C2\u6570",
23848
23860
  args: NO_ARGS
@@ -173,16 +173,20 @@ AK/SK 分两层,都放在共享 deploy-kit 下(`$HOME/.clawd/deploy-kit/.sec
173
173
  - **`aliyun.env`** — clawd ship 的**共享 demo 凭证**(归属 RAM 用户 `clawd-fc-developer`,权限收敛:`AliyunFCFullAccess` + `STSAssumeRole` + `Log` + `OSS` + `YundunCert-RO`)。**每次 daemon 启动 refreshDeployKit 会用 defaults 里的最新版覆盖本文件(OTA 即升级)**——用户不用手动 rotate。
174
174
  - **`aliyun.env.local`**(可选)— 本机个人 override,永远不被 OTA 覆盖。publish.sh / remove-extension.sh 先 source `aliyun.env`(shared)、再 source `aliyun.env.local`(后者的值覆盖前者)。用户想用自己的 AK(如不想让 demo AK 部署到自己账号)就复制 `aliyun.env.local.example` → `aliyun.env.local` 填自己的值。
175
175
 
176
- 手动跑 CLI 时在当前 shell 加载:
176
+ 手动跑 CLI 时在当前 shell 加载(`ensure-credentials.sh` 就是 publish.sh / remove-extension.sh 用的那套,别另起炉灶):
177
177
 
178
178
  ```bash
179
- set -a
180
- . "$HOME/.clawd/deploy-kit/.secrets/aliyun.env"
181
- [ -f "$HOME/.clawd/deploy-kit/.secrets/aliyun.env.local" ] && . "$HOME/.clawd/deploy-kit/.secrets/aliyun.env.local"
182
- set +a
179
+ export KIT_DIR="$HOME/.clawd/deploy-kit"
180
+ source "$HOME/.clawd/personas/persona-app-builder/extension-kit/config.env" # REGION 等平台变量
181
+ source "$KIT_DIR/scripts/ensure-toolchain.sh" && ensure_toolchain # 缺 aliyun/s 则装
182
+ source "$KIT_DIR/scripts/ensure-credentials.sh" && ensure_credentials
183
183
  ```
184
184
 
185
- 之后 `aliyun` / `s` / `ossutil` 会自动读这两个环境变量。Serverless Devs 还需 `~/.s/access.yaml` 含 `AccountID`(用 `aliyun sts GetCallerIdentity` 取),SK 走 heredoc 写入、不进命令行。
185
+ ⚠️ **上面这几行和后续的 `aliyun` / `s` 命令必须在同一个 Bash 调用里跑完**——你的 Bash tool 每次调用是独立进程,shell 变量不跨调用留存。分两次跑 = 第二次丢了 `serverless_devs_config_home`,`s` 会回头去读 `~/.s` 然后报 `Not found access: clawd-fc`。**这时正确做法是把 source 那几行和 `s` 命令拼进同一条命令重跑,绝不是去写 `~/.s/access.yaml`。**
186
+
187
+ 之后 `aliyun` / `ossutil` 直接可用(AK 已进 env)。**Serverless Devs(`s`)要显式带 profile**:凭证写在 deploy-kit 私有的 `.s-home/` 里、profile 名 `clawd-fc`,所以每次都是 `s deploy -y --access "$S_ACCESS_ALIAS"` / `s remove -y --access "$S_ACCESS_ALIAS"`。
188
+
189
+ **红线:绝不写用户全局 `~/.s/access.yaml`**。那里的 `default` 是用户自己账号的凭证,clawd 用的是共享 demo AK,盖上去用户的就没了、不可恢复。
186
190
 
187
191
  - **红线**:绝不 `echo` / 打印 / 写进日志或提交记录里暴露 SK;绝不把 `.secrets/` 拷进被部署的项目代码(已被 `.gitignore` 挡住)。**shared aliyun.env 为空**(demo 凭证意外没 ship)先停下提醒老板;**用户想用自己的 AK** 就引导他建 `aliyun.env.local`,别去改 shared 那份(OTA 会盖回)。
188
192
 
@@ -7,7 +7,9 @@
7
7
  # 保证多用户/多副本同名时不撞 FC 函数 + 不撞域名
8
8
  edition: 3.0.0
9
9
  name: __DEPLOY_NAME__-app
10
- access: default
10
+ # clawd 自己的 Serverless Devs profile,写在 deploy-kit 私有 s-home 里(见 scripts/ensure-credentials.sh)。
11
+ # 刻意不用 default —— 那是用户自己账号的 profile,不该被 clawd 读也不该被 clawd 写。
12
+ access: clawd-fc
11
13
 
12
14
  vars:
13
15
  region: __REGION__
@@ -2,7 +2,9 @@
2
2
  # 框架无关:command 固定走 ./bootstrap,入口由 FC_ENTRY 环境变量决定
3
3
  edition: 3.0.0
4
4
  name: __APP_NAME__-app
5
- access: default
5
+ # clawd 自己的 Serverless Devs profile,写在 deploy-kit 私有 s-home 里(见 scripts/ensure-credentials.sh)。
6
+ # 刻意不用 default —— 那是用户自己账号的 profile,不该被 clawd 读也不该被 clawd 写。
7
+ access: clawd-fc
6
8
 
7
9
  vars:
8
10
  region: __REGION__