@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 +40 -5
- package/dist/deploy-kit/scripts/ensure-credentials.sh +79 -0
- package/dist/deploy-kit/scripts/ensure-credentials.spec.ts +204 -0
- package/dist/deploy-kit/scripts/publish.sh +3 -16
- package/dist/deploy-kit/scripts/remove-extension.sh +4 -5
- package/dist/dispatch/mcp-server.cjs +13 -1
- package/dist/persona-defaults/persona-app-builder/CLAUDE.md +10 -6
- package/dist/persona-defaults/persona-app-builder/extension-kit/contract/s.yaml.tmpl +3 -1
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/contract/s.yaml.tmpl +3 -1
- package/dist/share-md-viewer-error.html +1 -1
- package/dist/share-md-viewer.html +1 -1
- package/dist/share-ui/assets/{guest-DgGpoVf5.css → guest-7QsWRy9Q.css} +1 -1
- package/dist/share-ui/assets/{guest-ChNRpL1J.js → guest-CilettnR.js} +3 -3
- package/dist/share-ui/guest.html +2 -2
- package/package.json +1 -1
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: "\
|
|
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
|
|
54410
|
-
if (
|
|
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.
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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: "\
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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__
|