@clawos-dev/clawd 0.2.552 → 0.2.554

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
@@ -5989,13 +5989,13 @@ function sharedConnectorSecrets(m3) {
5989
5989
  function requiredSecretEnvNames(m3) {
5990
5990
  return [.../* @__PURE__ */ new Set([...m3.secrets.flatMap((s2) => s2.env), ...sharedConnectorSecrets(m3).flatMap((s2) => s2.env)])];
5991
5991
  }
5992
- var EnvCliSchema, EnvSecretSchema, EnvAssetSchema, ConnectorNameSchema, CONNECTOR_KINDS, CONNECTOR_RUNNERS, CONNECTOR_PROTOCOLS, CREDENTIAL_SOURCES, CONNECTOR_AUTHS, ConnectorFieldSchema, EnvConnectorSchema, MANIFEST_BODY, EnvManifestBodySchema, EnvironmentManifestSchema, EnvEvidenceSchema;
5992
+ var EnvPackageSchema, EnvSecretSchema, ConnectorNameSchema, CONNECTOR_KINDS, CONNECTOR_RUNNERS, CONNECTOR_PROTOCOLS, CREDENTIAL_SOURCES, CONNECTOR_AUTHS, ConnectorFieldSchema, EnvConnectorSchema, normalizeLegacyManifest, MANIFEST_BODY, EnvManifestBodySchema, EnvironmentManifestSchema, EnvEvidenceSchema;
5993
5993
  var init_environment_schemas = __esm({
5994
5994
  "../protocol/src/environment-schemas.ts"() {
5995
5995
  "use strict";
5996
5996
  init_zod();
5997
- EnvCliSchema = external_exports.object({
5998
- // 可执行文件名,装配后要能在 PATH 里直接调到('aliyun' / 's' / 'pnpm' / 'python3')
5997
+ EnvPackageSchema = external_exports.object({
5998
+ // 装完后在 PATH 里能直接调到的名字('aliyun' / 's' / 'pnpm' / 'python3');npm 包报 bin 名,包名进 from
5999
5999
  name: external_exports.string().min(1),
6000
6000
  /**
6001
6001
  * 从哪装,约定 `<kind>:<ref>`:`npm:@serverless-devs/s` / `github:aliyun/aliyun-cli` /
@@ -6028,9 +6028,6 @@ var init_environment_schemas = __esm({
6028
6028
  // supabase 是 url/anon/service-role/jwt 四个),所以是数组不是单值。
6029
6029
  env: external_exports.array(external_exports.string().min(1)).min(1)
6030
6030
  });
6031
- EnvAssetSchema = external_exports.object({
6032
- name: external_exports.string().min(1)
6033
- });
6034
6031
  ConnectorNameSchema = external_exports.string().regex(/^[a-z0-9-]+$/, "\u8FDE\u63A5\u5668\u540D\u53EA\u80FD\u662F\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u77ED\u6A2A\u7EBF");
6035
6032
  CONNECTOR_KINDS = ["proxy", "env"];
6036
6033
  CONNECTOR_RUNNERS = ["mcp", "cli"];
@@ -6049,10 +6046,23 @@ var init_environment_schemas = __esm({
6049
6046
  fields: external_exports.array(ConnectorFieldSchema).default([]),
6050
6047
  http: external_exports.object({ url: external_exports.string().min(1), headers: external_exports.record(external_exports.string(), external_exports.string()) }).optional()
6051
6048
  });
6049
+ normalizeLegacyManifest = (v3) => {
6050
+ if (!v3 || typeof v3 !== "object") return v3;
6051
+ let o2 = v3;
6052
+ if ("clis" in o2 && Array.isArray(o2.clis)) {
6053
+ const { clis, ...rest } = o2;
6054
+ o2 = "packages" in rest ? rest : { ...rest, packages: clis };
6055
+ }
6056
+ if ("assets" in o2 && Array.isArray(o2.assets) && o2.assets.length === 0) {
6057
+ const { assets: _dropped, ...rest } = o2;
6058
+ o2 = rest;
6059
+ }
6060
+ return o2;
6061
+ };
6052
6062
  MANIFEST_BODY = {
6053
- clis: external_exports.array(EnvCliSchema),
6063
+ // 构镜像时要装进去的所有东西(工具 / 运行时 / 包)。底座已有的构建器会跳过;persona 目录整个进镜像,不在这里列
6064
+ packages: external_exports.array(EnvPackageSchema),
6054
6065
  secrets: external_exports.array(EnvSecretSchema),
6055
- assets: external_exports.array(EnvAssetSchema),
6056
6066
  /**
6057
6067
  * 装了哪些连接器(design 2026-09-11-mcp-connector §3.1)。一条一个对象,形状见 `EnvConnectorSchema`。
6058
6068
  *
@@ -6064,14 +6074,14 @@ var init_environment_schemas = __esm({
6064
6074
  connectors: external_exports.array(EnvConnectorSchema).default([])
6065
6075
  };
6066
6076
  EnvManifestBodySchema = external_exports.object(MANIFEST_BODY);
6067
- EnvironmentManifestSchema = external_exports.object({
6077
+ EnvironmentManifestSchema = external_exports.preprocess(normalizeLegacyManifest, external_exports.object({
6068
6078
  // 格式版本。manifest 要跨进程(daemon 扫 → 镜像构建器读)也跨时间(部署记录留档),
6069
6079
  // 加字段兼容但改语义不兼容,留一个显式版本位。
6070
6080
  version: external_exports.literal(1),
6071
6081
  ...MANIFEST_BODY
6072
- }).strict();
6082
+ }).strict());
6073
6083
  EnvEvidenceSchema = external_exports.object({
6074
- // 这条证据支持的是哪一项:`${kind}:${name}`,如 'cli:aliyun' / 'secret:aliyun-ak'
6084
+ // 这条证据支持的是哪一项:`${kind}:${name}`,如 'package:aliyun' / 'secret:aliyun-ak' / 'connector:feishu'
6075
6085
  target: external_exports.string().min(1),
6076
6086
  // 一句人话:为什么需要它 + 从哪看出来的。单据默认显示这句。
6077
6087
  reason: external_exports.string().min(1),
@@ -6087,10 +6097,13 @@ var init_environment_schemas = __esm({
6087
6097
  });
6088
6098
 
6089
6099
  // ../protocol/src/deploy-schemas.ts
6090
- function isScanFresh(scan, repo2) {
6100
+ function contentHeadOf(repo2, released) {
6101
+ return released && repo2.head === released.head ? released.contentHead : repo2.head;
6102
+ }
6103
+ function isScanFresh(scan, repo2, released) {
6091
6104
  if (!scan || !repo2) return false;
6092
6105
  if (scan.status !== "done") return false;
6093
- return scan.head === repo2.head && !repo2.dirty;
6106
+ return scan.head === contentHeadOf(repo2, released) && !repo2.dirty;
6094
6107
  }
6095
6108
  function cloudEngineOk(tool) {
6096
6109
  return (tool || "claude") === "codex";
@@ -6098,7 +6111,7 @@ function cloudEngineOk(tool) {
6098
6111
  function repoSlug(url) {
6099
6112
  return url.trim().replace(/^git@[^:]+:/, "").replace(/^https?:\/\/[^/]+\//, "").replace(/\.git$/, "");
6100
6113
  }
6101
- var DeployFileSchema, SCAN_BASE_KINDS, ScanBaseKindSchema, DeployScanStatusSchema, DeployScanSchema, DeployRepoSchema, DeploySecretPresenceSchema, DEPLOY_STAGE_STATUSES, DeployStageStatusSchema, DEPLOY_STAGE_KEYS, DeployStageKeySchema, DeployJobSchema, DeployRunSchema, DeployStateSchema, DeployReleaseResultSchema, DeployPersonaArgsSchema, DeployReleaseArgsSchema, DeployPatchArgsSchema, DeployPutSecretArgsSchema, DeployRerunJobArgsSchema, DeployReportFindingArgsSchema, DeployFinishScanArgsSchema, PERSONA_REPO_ORG;
6114
+ var DeployFileSchema, SCAN_BASE_KINDS, ScanBaseKindSchema, DeployScanStatusSchema, DeployScanSchema, DeployReleasedSchema, DeployScanFileSchema, DeployRepoSchema, DeploySecretPresenceSchema, DEPLOY_STAGE_STATUSES, DeployStageStatusSchema, DEPLOY_STAGE_KEYS, DeployStageKeySchema, DeployJobSchema, DeployRunSchema, DeployStateSchema, DeployReleaseResultSchema, DeployPersonaArgsSchema, DeployScanArgsSchema, DeployReleaseArgsSchema, DeployPatchArgsSchema, DeployPutSecretArgsSchema, DeployRerunJobArgsSchema, DeployReportFindingArgsSchema, DeployFinishScanArgsSchema, PERSONA_REPO_ORG;
6102
6115
  var init_deploy_schemas = __esm({
6103
6116
  "../protocol/src/deploy-schemas.ts"() {
6104
6117
  "use strict";
@@ -6113,33 +6126,30 @@ var init_deploy_schemas = __esm({
6113
6126
  ScanBaseKindSchema = external_exports.enum(SCAN_BASE_KINDS);
6114
6127
  DeployScanStatusSchema = external_exports.enum(["scanning", "done", "failed"]);
6115
6128
  DeployScanSchema = external_exports.object({
6129
+ /** 历史里指名用(发布 / patch / putSecret 都按它找)。daemon 生成;存量单条记录迁过来的叫 `legacy` */
6130
+ id: external_exports.string().min(1),
6131
+ /**
6132
+ * 这次扫描的清单——原来在 `deploy.json` 里的那份,现在每次扫描各存一份。
6133
+ * 扫描期由 `deploy:reportFinding` 逐条长出来,扫完人在页面上改(`deploy:patch`)也改这份;
6134
+ * 发布时选中的那条被写成 `deploy.json` 进仓库。
6135
+ */
6136
+ manifest: EnvironmentManifestSchema,
6116
6137
  /** 干这次扫描的 butler 会话,「看它怎么干的」入口 */
6117
6138
  scanSessionId: external_exports.string().optional(),
6118
6139
  status: DeployScanStatusSchema,
6119
6140
  evidence: external_exports.array(EnvEvidenceSchema).default([]),
6120
6141
  notes: external_exports.array(external_exports.string()).default([]),
6121
- /** 扫描时被告知的共享资产名单(判「扫描器后来长了新本事」用) */
6122
- scannedAssets: external_exports.array(external_exports.string()).default([]),
6123
6142
  /**
6124
- * 这次扫描是**对哪个 commit 的断言**。`deploy:release` 在起扫描之前会先把 persona 目录
6125
- * 提交(只提交不推,推是发布末尾那一下的事),所以这个值一定是仓库里真实存在的一版。
6143
+ * 这次扫描是**对哪个内容版本的断言**:起扫描前先把 persona 目录提交到本地(只提交不推,推是发布的事),
6144
+ * 落存那一下的 HEAD 就是它——发布 commit 不算内容版本,见 `contentHeadOf`。所以这个值一定是仓库里真实存在的一版,
6145
+ * 同一内容上扫几次 `head` 都相同。**从此不动**:发布不再把它挪成发布 commit(线上是哪版记在 `DeployReleased` 里)。
6126
6146
  *
6127
- * 有了它,「清单过没过时」才是个可判定的等式(`head === repo.head`),
6147
+ * 有了它,「清单过没过时」才是个可判定的等式(`head === contentHeadOf(repo, released)`),
6128
6148
  * 而不是从 `dirty` / `ahead` 里猜——那两个字段判不出「清单是好几版之前扫的」。
6129
6149
  *
6130
6150
  * 缺席 = 旧版 clawd 扫的记录。**一律当过时**:没有第三种情况,重扫一次就有了。
6131
6151
  */
6132
6152
  head: external_exports.string().min(1).optional(),
6133
- /**
6134
- * 上次**发布**推的那个 commit ——「线上正跑的是哪一版」在本机唯一的答案,
6135
- * 也就是软扫的基准。只有 `deploy:release` 写它,扫描不动。
6136
- *
6137
- * 为什么不能用 `head` 顶替:`head` 被扫描和发布轮流覆盖(扫描前落存一版、发布后又挪一版),
6138
- * 它回答的是「清单绑在哪个 commit 上」,回答不了「上次发布是哪一版」。
6139
- *
6140
- * 缺席 = 这个 persona 还从没发布过,没有线上版本可比,软扫退硬扫。
6141
- */
6142
- releasedHead: external_exports.string().min(1).optional(),
6143
6153
  /**
6144
6154
  * 这次**实际**怎么扫的。档位由 daemon 定(有线上版本可比 + 有跑完的清单 → 软扫,否则硬扫),
6145
6155
  * 页面要显示的是真正发生的那个。
@@ -6148,12 +6158,23 @@ var init_deploy_schemas = __esm({
6148
6158
  * 定成必填会让 `readScan` 当场抛、上云页面直接打不开。而老记录确实都是从零扫的。
6149
6159
  */
6150
6160
  baseKind: ScanBaseKindSchema.default("hard"),
6151
- /** 软扫比的那个 commit(= `releasedHead`)。硬扫时缺席 */
6161
+ /** 软扫比的那个 commit(= 当时 `DeployReleased.head`)。硬扫时缺席 */
6152
6162
  base: external_exports.string().min(1).optional(),
6153
6163
  startedAt: external_exports.number(),
6154
6164
  finishedAt: external_exports.number().optional(),
6155
6165
  error: external_exports.string().optional()
6156
6166
  }).strict();
6167
+ DeployReleasedSchema = external_exports.object({
6168
+ head: external_exports.string().min(1),
6169
+ contentHead: external_exports.string().min(1),
6170
+ scanId: external_exports.string().min(1),
6171
+ at: external_exports.number()
6172
+ }).strict();
6173
+ DeployScanFileSchema = external_exports.object({
6174
+ version: external_exports.literal(2),
6175
+ scans: external_exports.array(DeployScanSchema),
6176
+ released: DeployReleasedSchema.optional()
6177
+ }).strict();
6157
6178
  DeployRepoSchema = external_exports.object({
6158
6179
  url: external_exports.string().min(1),
6159
6180
  branch: external_exports.string().min(1),
@@ -6202,8 +6223,10 @@ var init_deploy_schemas = __esm({
6202
6223
  /** 本机 GitHub 身份(机器级,githubAuth:* 的同一份;上云页顶上那张卡用)。老 daemon 不给(可选只为兼容),UI 缺席时按未知处理 */
6203
6224
  github: GithubAuthStatusSchema.optional(),
6204
6225
  repo: DeployRepoSchema.optional(),
6205
- file: DeployFileSchema.optional(),
6206
- scan: DeployScanSchema.optional(),
6226
+ /** 扫描历史,升序,最后一条最新。没扫过就是空数组 */
6227
+ scans: external_exports.array(DeployScanSchema).default([]),
6228
+ /** 线上正跑的是哪一版;从没发布过就缺席 */
6229
+ released: DeployReleasedSchema.optional(),
6207
6230
  /** 没仓库时缺席(查不了) */
6208
6231
  secrets: external_exports.array(DeploySecretPresenceSchema).optional(),
6209
6232
  /**
@@ -6212,27 +6235,28 @@ var init_deploy_schemas = __esm({
6212
6235
  */
6213
6236
  run: DeployRunSchema.optional(),
6214
6237
  /**
6215
- * `.clawd/deploy.json` 读不出来(人手改坏了)时的人话。有它就没有 `file`。
6216
- * 页面据此给一条能走的路(重新扫描会整份重写),而不是白屏。
6238
+ * `.clawd/deploy-scan.json` 读不出来(坏 JSON / 不合形状)时的人话。有它 `scans` 为空。
6239
+ * 页面据此给一条能走的路(重新扫描会重建这个文件),而不是白屏。
6217
6240
  */
6218
- fileError: external_exports.string().optional()
6241
+ historyError: external_exports.string().optional()
6242
+ }).strict();
6243
+ DeployReleaseResultSchema = external_exports.object({
6244
+ sha: external_exports.string().min(1),
6245
+ actionsUrl: external_exports.string().min(1)
6219
6246
  }).strict();
6220
- DeployReleaseResultSchema = external_exports.discriminatedUnion("outcome", [
6221
- external_exports.object({
6222
- outcome: external_exports.literal("pushed"),
6223
- sha: external_exports.string().min(1),
6224
- actionsUrl: external_exports.string().min(1)
6225
- }).strict(),
6226
- external_exports.object({ outcome: external_exports.literal("scanning") }).strict()
6227
- ]);
6228
6247
  DeployPersonaArgsSchema = external_exports.object({
6229
6248
  personaId: external_exports.string().min(1)
6230
6249
  }).strict();
6231
- DeployReleaseArgsSchema = DeployPersonaArgsSchema.extend({
6250
+ DeployScanArgsSchema = DeployPersonaArgsSchema.extend({
6232
6251
  scanBase: ScanBaseKindSchema.optional()
6233
6252
  }).strict();
6253
+ DeployReleaseArgsSchema = DeployPersonaArgsSchema.extend({
6254
+ scanId: external_exports.string().min(1)
6255
+ }).strict();
6234
6256
  DeployPatchArgsSchema = external_exports.object({
6235
6257
  personaId: external_exports.string().min(1),
6258
+ /** 改的是历史里哪次扫描的清单。扫描期 butler 删条目也走这条(scanId 在它的任务提示里) */
6259
+ scanId: external_exports.string().min(1),
6236
6260
  patch: external_exports.object({
6237
6261
  // 人工补依赖;整份替换 manifest
6238
6262
  manifest: EnvironmentManifestSchema.optional()
@@ -6240,6 +6264,8 @@ var init_deploy_schemas = __esm({
6240
6264
  }).strict();
6241
6265
  DeployPutSecretArgsSchema = external_exports.object({
6242
6266
  personaId: external_exports.string().min(1),
6267
+ /** 按哪次扫描的清单校验凭据名 / env 名。值本身是仓库级的(GitHub Secrets),跟扫描无关 */
6268
+ scanId: external_exports.string().min(1),
6243
6269
  // 对应 manifest.secrets[].name
6244
6270
  name: external_exports.string().min(1),
6245
6271
  // 环境变量名 → 值。key 必须是该 secret 声明过的 env 之一,handler 校验。
@@ -6252,7 +6278,7 @@ var init_deploy_schemas = __esm({
6252
6278
  }).strict();
6253
6279
  DeployReportFindingArgsSchema = external_exports.object({
6254
6280
  personaId: external_exports.string().min(1),
6255
- kind: external_exports.enum(["cli", "secret", "asset", "connector"]),
6281
+ kind: external_exports.enum(["package", "secret", "connector"]),
6256
6282
  // 该条目在 manifest 对应数组里的内容(形状随 kind 变,handler 按 kind 收窄校验)
6257
6283
  // connector 的 item 是 butler 报的 CLI 连接器({ name, fields? }),daemon 补齐 kind / runner / id 后落进清单;
6258
6284
  // MCP 连接器不经这里——由 daemon 从 .mcp.json 机械列出(design 2026-09-11-mcp-connector §3.2)
@@ -7578,7 +7604,7 @@ var init_persona_schemas = __esm({
7578
7604
  "../protocol/src/persona-schemas.ts"() {
7579
7605
  "use strict";
7580
7606
  init_zod();
7581
- PERSONA_SIZES = ["small", "medium", "large"];
7607
+ PERSONA_SIZES = ["tiny", "small", "medium", "large"];
7582
7608
  PersonaSizeSchema = external_exports.enum(PERSONA_SIZES);
7583
7609
  PersonaFileSchema = external_exports.object({
7584
7610
  personaId: external_exports.string(),
@@ -7596,7 +7622,7 @@ var init_persona_schemas = __esm({
7596
7622
  // 与 model 平行:persona 新建 session 的默认 tool,可平滑切换。
7597
7623
  tool: external_exports.string().optional(),
7598
7624
  /**
7599
- * 云上任务 Pod 的规格档(design 2026-09-18-persona-size):`small` / `medium` / `large`,没写按 medium。
7625
+ * 云上任务 Pod 的规格档(design 2026-09-18-persona-size):`tiny` / `small` / `medium` / `large`,没写按 medium。
7600
7626
  * 只有发布上云的流水线读它(cloud-host/pipeline/deploy-file.mjs 收窄值域,不认识的拒发);本机跑不看。
7601
7627
  * 协议层同 model / effort 保持 string(文件层向前兼容);改它的入口是上云页标题行的规格下拉
7602
7628
  * (design 2026-09-20-persona-size-ui),`persona:update` 那头按 PersonaSizeSchema 收窄。
@@ -7713,7 +7739,7 @@ var init_persona_schemas = __esm({
7713
7739
  model: external_exports.string().optional(),
7714
7740
  effort: external_exports.string().optional(),
7715
7741
  tool: external_exports.string().optional(),
7716
- // 云上任务 Pod 的规格档:只收三档之一(PersonaFile 层是 string,边界在这里收窄)
7742
+ // 云上任务 Pod 的规格档:只收四档之一(PersonaFile 层是 string,边界在这里收窄)
7717
7743
  size: PersonaSizeSchema.optional(),
7718
7744
  personality: external_exports.string().optional(),
7719
7745
  // 人格补充层正文(见 PersonaInfoResponseSchema.personalityLocal)。与 personality 各写
@@ -8987,15 +9013,19 @@ var init_methods = __esm({
8987
9013
  },
8988
9014
  // ---- deploy:*(persona 上云:persona 目录 = git 仓库工作树,发布 = 推送触发仓库流水线)----
8989
9015
  "deploy:get": {
8990
- summary: "\u4E0A\u4E91\u9875\u9762\u8981\u7684\u5168\u90E8\uFF1A\u4ED3\u5E93\u7ED1\u5B9A\u72B6\u6001\uFF08git \u73B0\u7B97\uFF09\u3001\u90E8\u7F72\u6587\u4EF6\u3001\u626B\u63CF\u8BB0\u5F55\u3001\u4ED3\u5E93 Secrets \u91CC\u5404\u53D8\u91CF\u540D\u586B\u8FC7\u6CA1\u6709\uFF08**\u53EA\u6709\u540D\u5B57\u548C\u65F6\u95F4\uFF0C\u6CA1\u6709\u503C**\uFF09\uFF1Bowner-only",
9016
+ summary: "\u4E0A\u4E91\u9875\u9762\u8981\u7684\u5168\u90E8\uFF1A\u4ED3\u5E93\u7ED1\u5B9A\u72B6\u6001\uFF08git \u73B0\u7B97\uFF09\u3001\u626B\u63CF\u5386\u53F2\uFF08\u6BCF\u6761\u5E26\u6E05\u5355\u5FEB\u7167\uFF09\u3001\u7EBF\u4E0A\u6B63\u8DD1\u7684\u662F\u54EA\u4E00\u7248\u3001\u4ED3\u5E93 Secrets \u91CC\u5404\u53D8\u91CF\u540D\u586B\u8FC7\u6CA1\u6709\uFF08**\u53EA\u6709\u540D\u5B57\u548C\u65F6\u95F4\uFF0C\u6CA1\u6709\u503C**\uFF09\uFF1Bowner-only",
8991
9017
  args: DeployPersonaArgsSchema
8992
9018
  },
9019
+ "deploy:scan": {
9020
+ summary: "\u8D77\u4E00\u6B21\u626B\u63CF\u3002**\u6CA1\u4ED3\u5E93\u5148\u5EFA\u4ED3**\uFF08GitHub REST \u5EFA\u5728\u7EC4\u7EC7\u4E0B\u3001\u79C1\u6709\uFF0Cgit \u63A8\u9996\u4E2A\u63D0\u4EA4\uFF1Btoken \u6765\u81EA\u672C\u673A git \u7684\u8EAB\u4EFD\uFF0C\u6CA1\u6388\u6743\u5148\u62D2\uFF09\u3002\u628A persona \u76EE\u5F55\u63D0\u4EA4\u5230\u672C\u5730\uFF08\u4E0D\u63A8\uFF09\u3001\u5728\u626B\u63CF\u5386\u53F2\u91CC\u8FFD\u52A0\u4E00\u6761\u3001\u8D77 persona-butler \u7684\u626B\u63CF\u4F1A\u8BDD\uFF1B\u6B63\u5728\u626B\u7684\u8BDD\u539F\u6837\u8FD4\u56DE\u4E0D\u8D77\u7B2C\u4E8C\u4E2A\u4F1A\u8BDD\u3002\u5E26 scanBase\uFF08hard / soft\uFF09\u6309\u8BE5\u6863\u4F4D\u626B\uFF08soft \u4E0D\u6210\u7ACB\u81EA\u52A8\u9000 hard\uFF09\uFF0C\u7F3A\u5E2D daemon \u81EA\u52A8\u5B9A\uFF1Bowner-only",
9021
+ args: DeployScanArgsSchema
9022
+ },
8993
9023
  "deploy:patch": {
8994
- summary: "\u7F16\u8F91\u90E8\u7F72\u6587\u4EF6\uFF1A\u624B\u5DE5\u8865\u4F9D\u8D56\u3002**\u4E0D\u80FD\u4ECE\u8FD9\u91CC\u4F20\u51ED\u636E\u7684\u503C**\uFF08\u503C\u8D70 deploy:putSecret\uFF09\uFF1Bowner-only",
9024
+ summary: "\u7F16\u8F91\u5386\u53F2\u91CC\u67D0\u6B21\u626B\u63CF\u7684\u6E05\u5355\uFF08scanId \u6307\u540D\uFF09\uFF1A\u624B\u5DE5\u8865 / \u5220\u4F9D\u8D56\uFF0C\u626B\u63CF\u671F butler \u5220\u6761\u76EE\u4E5F\u8D70\u8FD9\u6761\u3002**\u4E0D\u80FD\u4ECE\u8FD9\u91CC\u4F20\u51ED\u636E\u7684\u503C**\uFF08\u503C\u8D70 deploy:putSecret\uFF09\uFF1Bowner-only",
8995
9025
  args: DeployPatchArgsSchema
8996
9026
  },
8997
9027
  "deploy:putSecret": {
8998
- summary: "\u628A\u4E00\u6761\u51ED\u636E\u7684\u503C\u5199\u8FDB\u4ED3\u5E93\u7684 GitHub Secrets\uFF08GitHub REST\uFF0C\u503C\u7528\u4ED3\u5E93\u516C\u94A5\u5C01\u7BB1\u540E\u4E0A\u4F20\uFF09\u3002**\u8FD9\u662F\u503C\u552F\u4E00\u7684\u5165\u53E3**\uFF1A\u5165\u53C2\u4E0D\u8FDB\u65E5\u5FD7\u3001\u54CD\u5E94\u4E0D\u56DE\u663E\u503C\u3001\u672C\u673A\u4E0D\u843D\u76D8\u3002env \u540D\u5FC5\u987B\u662F\u8BE5\u51ED\u636E\u5728\u6E05\u5355\u91CC\u58F0\u660E\u8FC7\u7684\uFF1Bowner-only",
9028
+ summary: "\u628A\u4E00\u6761\u51ED\u636E\u7684\u503C\u5199\u8FDB\u4ED3\u5E93\u7684 GitHub Secrets\uFF08GitHub REST\uFF0C\u503C\u7528\u4ED3\u5E93\u516C\u94A5\u5C01\u7BB1\u540E\u4E0A\u4F20\uFF09\u3002**\u8FD9\u662F\u503C\u552F\u4E00\u7684\u5165\u53E3**\uFF1A\u5165\u53C2\u4E0D\u8FDB\u65E5\u5FD7\u3001\u54CD\u5E94\u4E0D\u56DE\u663E\u503C\u3001\u672C\u673A\u4E0D\u843D\u76D8\u3002env \u540D\u5FC5\u987B\u662F scanId \u90A3\u6B21\u6E05\u5355\u91CC\u8BE5\u51ED\u636E\u58F0\u660E\u8FC7\u7684\uFF1Bowner-only",
8999
9029
  args: DeployPutSecretArgsSchema
9000
9030
  },
9001
9031
  "deploy:reportFinding": {
@@ -9007,11 +9037,11 @@ var init_methods = __esm({
9007
9037
  args: DeployFinishScanArgsSchema
9008
9038
  },
9009
9039
  "deploy:release": {
9010
- summary: "\u53D1\u5E03\u2014\u2014\u4E0A\u4E91\u552F\u4E00\u7684\u52A8\u4F5C\uFF0C\u626B\u63CF\u662F\u5B83\u7684\u7B2C\u4E00\u6BB5\u3002**\u6CA1\u4ED3\u5E93\u5148\u5EFA\u4ED3**\uFF08GitHub REST \u5EFA\u5728\u7EC4\u7EC7\u4E0B\u3001\u79C1\u6709\uFF0Cgit \u63A8\u9996\u4E2A\u63D0\u4EA4\uFF1Btoken \u6765\u81EA\u672C\u673A git \u7684\u8EAB\u4EFD\uFF0C\u6CA1\u6388\u6743\u5148\u62D2\uFF09\u3002\u6E05\u5355\u8FC7\u65F6\uFF08persona \u6539\u8FC7 / \u6CA1\u626B\u8FC7 / \u4E0A\u6B21\u626B\u5931\u8D25\uFF09\uFF1A\u5148\u628A persona \u76EE\u5F55\u63D0\u4EA4\u5230\u672C\u5730\uFF08\u4E0D\u63A8\uFF09\uFF0C\u8D77 persona-butler \u7684\u626B\u63CF\u4F1A\u8BDD\uFF0C\u56DE outcome=scanning\u2014\u2014\u7ED3\u679C\u5199\u8FDB .clawd/deploy.json\uFF08\u6E05\u5355\uFF0C\u8FDB\u4ED3\u5E93\uFF09\u4E0E .clawd/deploy-scan.json\uFF08\u8BC1\u636E\uFF0C\u4E0D\u8FDB\u4ED3\u5E93\uFF09\uFF0C\u6B63\u5728\u626B\u7684\u8BDD\u539F\u6837\u8FD4\u56DE\u4E0D\u8D77\u7B2C\u4E8C\u4E2A\u4F1A\u8BDD\u3002\u6E05\u5355\u65B0\u9C9C\uFF1A\u8FC7\u95F8\uFF08\u6BCF\u4E2A\u53D8\u91CF\u540D\u90FD\u5728 Secrets \u91CC\u3001\u8FDE\u63A5\u5668\u6765\u6E90\u70B9\u8FC7\u3001\u9009\u4E86\u6A21\u578B\uFF09\u2192 \u91CD\u5199 workflow \u6A21\u677F \u2192 git add -A \u2192 \u7A7A\u63D0\u4EA4\u4E5F\u5141\u8BB8 \u2192 push\uFF0C\u56DE outcome=pushed \u5E26 commit \u4E0E Actions \u5730\u5740\uFF1B\u4ED3\u5E93\u91CC\u7684\u6D41\u6C34\u7EBF\u63A5\u7740\u6784\u955C\u50CF\u3001\u8BD5\u8DD1\u3001\u767B\u8BB0\u3002\u5E26 scanBase\uFF08hard / soft\uFF09= \u8FD9\u6B21\u4E00\u5B9A\u5148\u626B\u3001\u6309\u8BE5\u6863\u4F4D\u626B\uFF08soft \u4E0D\u6210\u7ACB\u81EA\u52A8\u9000 hard\uFF09\uFF0C\u6E05\u5355\u65B0\u9C9C\u4E5F\u56DE scanning\uFF1Bowner-only",
9040
+ summary: "\u628A\u5386\u53F2\u91CC scanId \u90A3\u6B21\u626B\u63CF\u7684\u6E05\u5355\u53D1\u4E0A\u53BB\u3002\u90A3\u6B21\u5FC5\u987B\u626B\u5B8C\u3001\u4E14\u5BF9\u5E94\u5F53\u524D\u5185\u5BB9\u7248\u672C\uFF08persona \u4E4B\u540E\u6CA1\u6539\u8FC7\uFF09\uFF0C\u5426\u5219\u62D2\u2014\u2014\u5148 deploy:scan\u3002\u8FC7\u95F8\uFF08\u6BCF\u4E2A\u53D8\u91CF\u540D\u90FD\u5728 Secrets \u91CC\u3001\u8FDE\u63A5\u5668\u6765\u6E90\u70B9\u8FC7\u3001\u9009\u4E86\u6A21\u578B\uFF09\u2192 \u628A\u90A3\u4EFD\u6E05\u5355\u5199\u6210 .clawd/deploy.json \u2192 \u91CD\u5199 workflow \u6A21\u677F \u2192 git add -A \u2192 \u7A7A\u63D0\u4EA4\u4E5F\u5141\u8BB8 \u2192 push\uFF0C\u56DE commit \u4E0E Actions \u5730\u5740\uFF1B\u4ED3\u5E93\u91CC\u7684\u6D41\u6C34\u7EBF\u63A5\u7740\u6784\u955C\u50CF\u3001\u8BD5\u8DD1\u3001\u767B\u8BB0\u3002\u540C\u4E00\u7248\u53EF\u4EE5\u53CD\u590D\u53D1\uFF08\u7528\u540C\u4E00\u4EFD\u6E05\u5355\u518D\u8D70\u4E00\u904D\uFF09\uFF1Bowner-only",
9011
9041
  args: DeployReleaseArgsSchema
9012
9042
  },
9013
9043
  "deploy:rerunJob": {
9014
- summary: "\u539F\u6837\u91CD\u8DD1\u6700\u65B0\u90A3\u6761\u6D41\u6C34\u7EBF\u91CC\u5931\u8D25\u7684\u67D0\u4E00\u6BB5\uFF08GitHub REST rerun job\uFF09\u3002**\u540C\u4E00\u4E2A commit\u3001\u540C\u4E00\u4EFD\u8F93\u5165**\uFF0C\u4E0D\u4EA7\u751F\u65B0\u63D0\u4EA4\u2014\u2014\u6539\u4E86 persona \u5185\u5BB9\u6216\u51ED\u636E\u8981\u8D70 deploy:release\u3002jobId \u5FC5\u987B\u5C5E\u4E8E\u6700\u65B0\u90A3\u6761 run\uFF1Bowner-only",
9044
+ summary: "\u539F\u6837\u91CD\u8DD1\u6700\u65B0\u90A3\u6761\u6D41\u6C34\u7EBF\u91CC\u5931\u8D25\u7684\u67D0\u4E00\u6BB5\uFF08GitHub REST rerun job\uFF09\u3002**\u540C\u4E00\u4E2A commit\u3001\u540C\u4E00\u4EFD\u8F93\u5165**\uFF0C\u4E0D\u4EA7\u751F\u65B0\u63D0\u4EA4\u2014\u2014\u6539\u4E86 persona \u5185\u5BB9\u8981 deploy:scan \u518D deploy:release\u3002jobId \u5FC5\u987B\u5C5E\u4E8E\u6700\u65B0\u90A3\u6761 run\uFF1Bowner-only",
9015
9045
  args: DeployRerunJobArgsSchema
9016
9046
  },
9017
9047
  // ---- githubAuth:*(本机 GitHub 身份,机器级;spec 2026-09-15-github-auth-unify-design §4.3)----
@@ -9718,11 +9748,12 @@ function isCodexFamily(tool) {
9718
9748
  function isRpcAgentTool(tool) {
9719
9749
  return isCodexFamily(tool) || tool === PI_TOOL_ID;
9720
9750
  }
9721
- var CODEX_CLOUD_TOOL_ID, PI_TOOL_ID;
9751
+ var CLOUD_TOOL_SUFFIX, CODEX_CLOUD_TOOL_ID, PI_TOOL_ID;
9722
9752
  var init_tool_family = __esm({
9723
9753
  "../protocol/src/tool-family.ts"() {
9724
9754
  "use strict";
9725
- CODEX_CLOUD_TOOL_ID = "codex-cloud";
9755
+ CLOUD_TOOL_SUFFIX = "-cloud";
9756
+ CODEX_CLOUD_TOOL_ID = `codex${CLOUD_TOOL_SUFFIX}`;
9726
9757
  PI_TOOL_ID = "pi";
9727
9758
  }
9728
9759
  });
@@ -32801,7 +32832,7 @@ var require_websocket = __commonJS({
32801
32832
  var http2 = require("http");
32802
32833
  var net2 = require("net");
32803
32834
  var tls = require("tls");
32804
- var { randomBytes, createHash: createHash2 } = require("crypto");
32835
+ var { randomBytes: randomBytes2, createHash: createHash2 } = require("crypto");
32805
32836
  var { Duplex, Readable: Readable3 } = require("stream");
32806
32837
  var { URL: URL2 } = require("url");
32807
32838
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -33331,7 +33362,7 @@ var require_websocket = __commonJS({
33331
33362
  }
33332
33363
  }
33333
33364
  const defaultPort = isSecure ? 443 : 80;
33334
- const key = randomBytes(16).toString("base64");
33365
+ const key = randomBytes2(16).toString("base64");
33335
33366
  const request = isSecure ? https.request : http2.request;
33336
33367
  const protocolSet = /* @__PURE__ */ new Set();
33337
33368
  let perMessageDeflate;
@@ -44494,7 +44525,7 @@ Env (advanced):
44494
44525
  var import_node_path78 = __toESM(require("path"), 1);
44495
44526
  var import_node_fs60 = __toESM(require("fs"), 1);
44496
44527
  var import_node_os28 = __toESM(require("os"), 1);
44497
- var import_node_crypto22 = require("crypto");
44528
+ var import_node_crypto23 = require("crypto");
44498
44529
  var import_node_url5 = require("url");
44499
44530
 
44500
44531
  // src/logger.ts
@@ -51856,6 +51887,7 @@ function driveCodexThread(opts) {
51856
51887
  ...translateNotification(method, params)
51857
51888
  ] : [];
51858
51889
  if (events.length) sink.pushEvents(events);
51890
+ if (route === "foreground" && method === "turn/completed") opts.onTurnCompleted?.();
51859
51891
  },
51860
51892
  onServerRequest: (id, method, params) => {
51861
51893
  const p3 = params ?? {};
@@ -55304,6 +55336,7 @@ function redactEvidence(evidence) {
55304
55336
  // src/deploy/deploy-file.ts
55305
55337
  var import_promises4 = __toESM(require("fs/promises"), 1);
55306
55338
  var import_node_path33 = __toESM(require("path"), 1);
55339
+ var import_node_crypto8 = require("crypto");
55307
55340
  init_protocol();
55308
55341
 
55309
55342
  // src/deploy/connectors.ts
@@ -55493,26 +55526,79 @@ async function readDeployFile(dir) {
55493
55526
  async function writeDeployFile(dir, file) {
55494
55527
  await writeJson(import_node_path33.default.join(dir, DEPLOY_FILE), DeployFileSchema.parse(file));
55495
55528
  }
55496
- async function readScan(dir) {
55497
- const raw = await readJson(import_node_path33.default.join(dir, SCAN_FILE));
55529
+ async function readDeployFileRaw(dir) {
55530
+ try {
55531
+ return await import_promises4.default.readFile(import_node_path33.default.join(dir, DEPLOY_FILE), "utf8");
55532
+ } catch (err) {
55533
+ if (err.code === "ENOENT") return void 0;
55534
+ throw err;
55535
+ }
55536
+ }
55537
+ async function restoreDeployFileRaw(dir, raw) {
55538
+ const file = import_node_path33.default.join(dir, DEPLOY_FILE);
55539
+ if (raw === void 0) await import_promises4.default.rm(file, { force: true });
55540
+ else await import_promises4.default.writeFile(file, raw);
55541
+ }
55542
+ var emptyScanFile = () => ({ version: 2, scans: [] });
55543
+ var LEGACY_SCAN_ID = "legacy";
55544
+ var newScanId = () => (0, import_node_crypto8.randomBytes)(4).toString("hex");
55545
+ var z1 = DeployScanSchema.omit({ id: true, manifest: true }).extend({ releasedHead: DeployScanSchema.shape.head }).strict();
55546
+ function migrateV1(v1, manifest) {
55547
+ const { releasedHead, ...rest } = v1;
55548
+ const entry = { ...rest, id: LEGACY_SCAN_ID, manifest: manifest ?? emptyDeployFile().manifest };
55549
+ return {
55550
+ version: 2,
55551
+ scans: [entry],
55552
+ ...releasedHead ? { released: { head: releasedHead, contentHead: releasedHead, scanId: LEGACY_SCAN_ID, at: v1.head === releasedHead ? v1.finishedAt ?? v1.startedAt : 0 } } : {}
55553
+ };
55554
+ }
55555
+ function normalizeLegacyScan(raw) {
55556
+ const fix = (o2) => {
55557
+ if (!o2 || typeof o2 !== "object") return o2;
55558
+ const { scannedAssets: _dropped, ...rest } = o2;
55559
+ if (Array.isArray(rest.evidence)) {
55560
+ rest.evidence = rest.evidence.map((e) => {
55561
+ if (!e || typeof e !== "object" || typeof e.target !== "string") return e;
55562
+ const target = e.target;
55563
+ return target.startsWith("cli:") ? { ...e, target: `package:${target.slice("cli:".length)}` } : e;
55564
+ });
55565
+ }
55566
+ return rest;
55567
+ };
55568
+ if (raw && typeof raw === "object" && Array.isArray(raw.scans)) {
55569
+ return { ...raw, scans: raw.scans.map(fix) };
55570
+ }
55571
+ return fix(raw);
55572
+ }
55573
+ async function readScanFile(dir) {
55574
+ const raw = normalizeLegacyScan(await readJson(import_node_path33.default.join(dir, SCAN_FILE)));
55498
55575
  if (raw === void 0) return void 0;
55499
- const parsed = DeployScanSchema.safeParse(raw);
55500
- if (!parsed.success) {
55501
- const first = parsed.error.issues[0];
55502
- throw new Error(`${SCAN_FILE} \u4E0D\u5408\u5F62\u72B6\uFF1A${first ? `${first.path.join(".")} ${first.message}` : ""}`);
55576
+ const badShape = (issues) => {
55577
+ const first = issues[0];
55578
+ return new Error(`${SCAN_FILE} \u4E0D\u5408\u5F62\u72B6\uFF1A${first ? `${first.path.join(".")} ${first.message}` : ""}`);
55579
+ };
55580
+ if (typeof raw === "object" && raw !== null && !("version" in raw)) {
55581
+ const v1 = z1.safeParse(raw);
55582
+ if (!v1.success) throw badShape(v1.error.issues);
55583
+ return migrateV1(v1.data, (await readDeployFile(dir).catch(() => void 0))?.manifest);
55503
55584
  }
55585
+ const parsed = DeployScanFileSchema.safeParse(raw);
55586
+ if (!parsed.success) throw badShape(parsed.error.issues);
55504
55587
  return parsed.data;
55505
55588
  }
55506
- async function writeScan(dir, scan) {
55507
- await writeJson(import_node_path33.default.join(dir, SCAN_FILE), DeployScanSchema.parse(scan));
55589
+ async function writeScanFile(dir, file) {
55590
+ await writeJson(import_node_path33.default.join(dir, SCAN_FILE), DeployScanFileSchema.parse(file));
55591
+ }
55592
+ function replaceScan(file, entry) {
55593
+ const idx = file.scans.findIndex((s2) => s2.id === entry.id);
55594
+ return { ...file, scans: idx === -1 ? [...file.scans, entry] : file.scans.map((s2, i2) => i2 === idx ? entry : s2) };
55508
55595
  }
55509
55596
  function emptyDeployFile() {
55510
- return { version: 1, manifest: { version: 1, clis: [], secrets: [], assets: [], connectors: [] } };
55597
+ return { version: 1, manifest: { version: 1, packages: [], secrets: [], connectors: [] } };
55511
55598
  }
55512
55599
  var FINDING_SCHEMAS = {
55513
- cli: EnvCliSchema,
55600
+ package: EnvPackageSchema,
55514
55601
  secret: EnvSecretSchema,
55515
- asset: EnvAssetSchema,
55516
55602
  // butler 报的是 CLI 连接器的「名字 + 它读的环境变量名」,kind / runner / id 由 daemon 补
55517
55603
  //(handlers/deploy.ts 的 cliConnectorFromFinding)。MCP 连接器不经上报口,由 scanMcpConnectors 机械列出。
55518
55604
  connector: CliConnectorFindingSchema
@@ -55525,26 +55611,26 @@ function parseFinding(kind, item) {
55525
55611
  }
55526
55612
  return parsed.data;
55527
55613
  }
55528
- function upsertFinding(file, kind, item) {
55614
+ function upsertFinding(manifest, kind, item) {
55529
55615
  if (kind === "connector") {
55530
55616
  const incoming = item;
55531
- const have = file.manifest.connectors;
55617
+ const have = manifest.connectors;
55532
55618
  const idx2 = have.findIndex((c2) => c2.name === incoming.name);
55533
- if (idx2 !== -1 && have[idx2].runner === "mcp" && incoming.runner === "cli") return file;
55619
+ if (idx2 !== -1 && have[idx2].runner === "mcp" && incoming.runner === "cli") return manifest;
55534
55620
  const merged = mergeConnector(idx2 === -1 ? void 0 : have[idx2], incoming);
55535
55621
  const connectors = idx2 === -1 ? [...have, merged] : have.map((c2, i2) => i2 === idx2 ? merged : c2);
55536
- return { ...file, manifest: { ...file.manifest, connectors } };
55622
+ return { ...manifest, connectors };
55537
55623
  }
55538
- const key = kind === "cli" ? "clis" : kind === "secret" ? "secrets" : "assets";
55539
- const list = [...file.manifest[key]];
55624
+ const key = kind === "package" ? "packages" : "secrets";
55625
+ const list = [...manifest[key]];
55540
55626
  const idx = list.findIndex((x2) => x2.name === item.name);
55541
55627
  if (idx === -1) list.push(item);
55542
55628
  else list[idx] = item;
55543
- return { ...file, manifest: { ...file.manifest, [key]: list } };
55629
+ return { ...manifest, [key]: list };
55544
55630
  }
55545
- function requiredEnvNames(file) {
55546
- if (!file) return [];
55547
- return requiredSecretEnvNames(file.manifest);
55631
+ function requiredEnvNames(manifest) {
55632
+ if (!manifest) return [];
55633
+ return requiredSecretEnvNames(manifest);
55548
55634
  }
55549
55635
 
55550
55636
  // src/deploy/repo.ts
@@ -59305,26 +59391,26 @@ var L2 = [
59305
59391
  " rm -rf persona/.git",
59306
59392
  " node clawos/cloud-host/provision/build-image.mjs \\",
59307
59393
  " --manifest persona/.clawd/deploy.json \\",
59308
- ' --from "$REGISTRY/$IMAGE_NAME:base" \\',
59394
+ ' --from "$REGISTRY/$IMAGE_NAME:base-${{ steps.plan.outputs.engine }}" \\',
59309
59395
  " --persona-dir persona \\",
59310
59396
  // persona 在镜像里落 /clawd/personas/<id>/(与本机同构),id 是 gateway 登记的名字;同 smoke / register 用的那个输出
59311
59397
  " --persona-id '${{ steps.plan.outputs.persona_id }}' \\",
59312
59398
  ' --tag "$REGISTRY/$IMAGE_NAME:${{ steps.plan.outputs.tag }}"',
59313
59399
  "",
59314
- " - name: \u955C\u50CF\u65AD\u8A00\uFF08\u6E05\u5355\u91CC\u7684 CLI \u90FD\u5728\u3001bundle \u91CC persona \u5728\uFF09",
59315
- " # /opt/versions.sh \u6BCF\u884C\u90FD\u4EE5 `|| echo` \u6536\u5C3E\u3001\u6C38\u8FDC exit 0\uFF0C\u53EA\u80FD\u9A8C\u8F93\u51FA\uFF1A\u6BB5\u6570\u8981\u7B49\u4E8E\u6E05\u5355\u91CC\u7684",
59316
- " # CLI \u6570\uFF0C\u6BCF\u6BB5\u4E0B\u9762\u8981\u6709\u7248\u672C\u53F7\u3002\u955C\u50CF ENTRYPOINT \u662F sh -c\uFF0C\u547D\u4EE4\u8981\u4F5C\u4E3A\u4E00\u4E2A\u5B57\u7B26\u4E32\u4F20\u3002",
59400
+ " - name: \u955C\u50CF\u65AD\u8A00\uFF08\u6E05\u5355\u91CC\u8981\u88C5\u7684\u90FD\u5728\u3001bundle \u91CC persona \u5728\uFF09",
59401
+ " # /opt/versions.sh \u6BCF\u884C\u90FD\u4EE5 `|| echo` \u6536\u5C3E\u3001\u6C38\u8FDC exit 0\uFF0C\u53EA\u80FD\u9A8C\u8F93\u51FA\uFF1A\u6BB5\u6570\u8981\u7B49\u4E8E\u6E05\u5355 packages \u7684",
59402
+ " # \u6761\u6570\uFF0C\u6BCF\u6BB5\u4E0B\u9762\u8981\u6709\u7248\u672C\u53F7\u3002\u955C\u50CF ENTRYPOINT \u662F sh -c\uFF0C\u547D\u4EE4\u8981\u4F5C\u4E3A\u4E00\u4E2A\u5B57\u7B26\u4E32\u4F20\u3002",
59317
59403
  " run: |",
59318
59404
  ' IMG="$REGISTRY/$IMAGE_NAME:${{ steps.plan.outputs.tag }}"',
59319
59405
  ` OUT=$(docker run --rm "$IMG" '/opt/versions.sh')`,
59320
59406
  ' echo "$OUT"',
59321
- ` WANT=$(node -e 'const m=JSON.parse(require("fs").readFileSync("persona/.clawd/deploy.json","utf8"));console.log(((m.manifest??m).clis??[]).length)')`,
59407
+ ` WANT=$(node -e 'const m=JSON.parse(require("fs").readFileSync("persona/.clawd/deploy.json","utf8"));const b=m.manifest??m;console.log((b.packages??b.clis??[]).length)')`,
59322
59408
  ` echo "$OUT" | awk -v want="$WANT" '`,
59323
59409
  ' /^--- / { name=$2; if ((getline v) <= 0) v=""; n++',
59324
59410
  ' if (v !~ /[0-9]+\\.[0-9]+/) { printf " \u2717 %s \u6CA1\u62A5\u51FA\u7248\u672C\u53F7\uFF1A%s\\n", name, v > "/dev/stderr"; bad++ } }',
59325
- ' END { if (n != want) { printf " \u2717 \u955C\u50CF\u91CC\u53EA\u6709 %d \u4E2A CLI \u6BB5\uFF0C\u6E05\u5355\u8981\u6C42 %d \u4E2A\\n", n, want > "/dev/stderr"; bad++ }',
59411
+ ' END { if (n != want) { printf " \u2717 \u955C\u50CF\u91CC\u53EA\u6709 %d \u6BB5\u7248\u672C\u8F93\u51FA\uFF0C\u6E05\u5355\u8981\u88C5 %d \u4E2A\\n", n, want > "/dev/stderr"; bad++ }',
59326
59412
  " exit bad ? 1 : 0 }'",
59327
- ' echo " \u2713 $WANT \u4E2A CLI \u90FD\u5728\u4E14\u62A5\u5F97\u51FA\u7248\u672C"',
59413
+ ' echo " \u2713 \u6E05\u5355\u91CC\u8981\u88C5\u7684 $WANT \u4E2A\u90FD\u5728\u4E14\u62A5\u5F97\u51FA\u7248\u672C"',
59328
59414
  ` docker run --rm "$IMG" 'test -d /clawd/personas/\${{ steps.plan.outputs.persona_id }}' \\`,
59329
59415
  ' || { echo " \u2717 /clawd/personas/${{ steps.plan.outputs.persona_id }} \u4E0D\u5728" >&2; exit 1; }',
59330
59416
  ' echo " \u2713 persona \u76EE\u5F55\u5728"',
@@ -59357,6 +59443,7 @@ var L2 = [
59357
59443
  " --model '${{ steps.plan.outputs.model }}' \\",
59358
59444
  " --effort '${{ steps.plan.outputs.effort }}' \\",
59359
59445
  " --size '${{ steps.plan.outputs.size }}' \\",
59446
+ " --engine '${{ steps.plan.outputs.engine }}' \\",
59360
59447
  " --connectors '${{ steps.plan.outputs.connectors }}' \\",
59361
59448
  " --run-number '${{ github.run_number }}'",
59362
59449
  "",
@@ -59381,6 +59468,7 @@ var L2 = [
59381
59468
  " --model '${{ steps.plan.outputs.model }}' \\",
59382
59469
  " --effort '${{ steps.plan.outputs.effort }}' \\",
59383
59470
  " --size '${{ steps.plan.outputs.size }}' \\",
59471
+ " --engine '${{ steps.plan.outputs.engine }}' \\",
59384
59472
  " --connectors '${{ steps.plan.outputs.connectors }}'",
59385
59473
  ""
59386
59474
  ];
@@ -59596,7 +59684,7 @@ async function release(exec, api, args) {
59596
59684
  await mustGit(exec, args.dir, [...identity, "commit", "--allow-empty", "-m", `clawd: \u53D1\u5E03 ${stamp}`], "\u63D0\u4EA4");
59597
59685
  await gitPush(exec, args.dir, args.auth);
59598
59686
  const sha = await mustGit(exec, args.dir, ["rev-parse", "HEAD"], "\u8BFB\u63D0\u4EA4");
59599
- return { outcome: "pushed", sha, actionsUrl: `https://github.com/${repoSlug(info.url)}/actions` };
59687
+ return { sha, actionsUrl: `https://github.com/${repoSlug(info.url)}/actions` };
59600
59688
  }
59601
59689
 
59602
59690
  // src/deploy/scan-base.ts
@@ -59675,17 +59763,6 @@ async function rerunJob(api, slug, jobId) {
59675
59763
 
59676
59764
  // src/deploy/scan-task.ts
59677
59765
  var BUTLER_PERSONA_ID = "persona-clawd-butler";
59678
- var SCANNABLE_ASSETS = [];
59679
- function staleScannerAssets(scan, known = SCANNABLE_ASSETS) {
59680
- const knew = new Set(scan.scannedAssets ?? []);
59681
- return known.filter((a2) => !knew.has(a2.name));
59682
- }
59683
- var STALE_ASSETS_NOTE_PREFIX = "\u3010\u4E0A\u4E91\u626B\u63CF\u3011";
59684
- function describeStaleAssets(stale) {
59685
- if (stale.length === 0) return void 0;
59686
- const plain = (s2) => s2.replace(/\*\*/g, "").replace(/\s+/g, " ").trim();
59687
- return `${STALE_ASSETS_NOTE_PREFIX}\u8FD9\u6B21\u626B\u63CF\u7684\u65F6\u5019\uFF0C\u626B\u63CF\u5668\u8FD8\u4E0D\u77E5\u9053\u6709${stale.map((a2) => `\u300C${a2.name}\u300D`).join("\u3001")}\u8FD9${stale.length > 1 ? "\u51E0" : ""}\u9879\u5171\u4EAB\u8D44\u4EA7\uFF0C\u6240\u4EE5\u6E05\u5355\u91CC\u53EF\u80FD\u6F0F\u4E86\u58F0\u660E\u3002${stale.map((a2) => ` ${a2.name} \u662F\uFF1A${plain(a2.what)}`).join("")} \u7528\u5F97\u4E0A\u7684\u8BDD\uFF0C\u5728\u6E05\u5355\u91CC\u8865\u4E00\u6761 asset\u3002`;
59688
- }
59689
59766
  function settleEndedScan(outcome, current, now) {
59690
59767
  if (current?.status !== "scanning") return void 0;
59691
59768
  return {
@@ -59697,13 +59774,11 @@ function settleEndedScan(outcome, current, now) {
59697
59774
  }
59698
59775
  function reconcileInterruptedScans(deps) {
59699
59776
  const flipped = [];
59700
- for (const { personaId: personaId2, scan } of deps.scans) {
59701
- if (scan?.status !== "scanning") continue;
59777
+ for (const { personaId: personaId2, file } of deps.files) {
59778
+ if (!file || !file.scans.some((s2) => s2.status === "scanning")) continue;
59702
59779
  deps.write(personaId2, {
59703
- ...scan,
59704
- status: "failed",
59705
- finishedAt: deps.now,
59706
- error: "daemon \u91CD\u542F\uFF0C\u626B\u63CF\u4F1A\u8BDD\u5DF2\u4E2D\u65AD\u2014\u2014\u518D\u70B9\u4E00\u6B21\u300C\u53D1\u5E03\u300D\u4F1A\u91CD\u65B0\u626B\u63CF"
59780
+ ...file,
59781
+ scans: file.scans.map((s2) => s2.status === "scanning" ? { ...s2, status: "failed", finishedAt: deps.now, error: "daemon \u91CD\u542F\uFF0C\u626B\u63CF\u4F1A\u8BDD\u5DF2\u4E2D\u65AD\u2014\u2014\u518D\u70B9\u4E00\u6B21\u300C\u91CD\u65B0\u626B\u63CF\u300D" } : s2)
59707
59782
  });
59708
59783
  flipped.push(personaId2);
59709
59784
  }
@@ -59733,21 +59808,33 @@ function isLiveScan(live, scan) {
59733
59808
  return live.sessionId === void 0 || live.sessionId === scan.scanSessionId;
59734
59809
  }
59735
59810
  function buildDeployHandlers(deps) {
59736
- const scannableAssets = deps.scannableAssets ?? SCANNABLE_ASSETS;
59737
59811
  const liveScans = /* @__PURE__ */ new Map();
59738
- const settleStale = async (dir, scan) => {
59739
- const settled = { ...scan, status: "failed", finishedAt: deps.now(), error: "\u626B\u63CF\u8BB0\u5F55\u505C\u5728 scanning\uFF0C\u4F46\u6CA1\u6709\u6B63\u5728\u8DD1\u7684\u626B\u63CF\u4F1A\u8BDD" };
59740
- await writeScan(dir, settled);
59812
+ const readHistory = async (dir) => await readScanFile(dir) ?? emptyScanFile();
59813
+ const settleStale = async (personaId2, dir, file) => {
59814
+ const live = liveScans.get(personaId2);
59815
+ const stale = file.scans.filter((s2) => s2.status === "scanning" && !isLiveScan(live, s2));
59816
+ if (stale.length === 0) return file;
59817
+ const settled = {
59818
+ ...file,
59819
+ scans: file.scans.map((s2) => stale.includes(s2) ? { ...s2, status: "failed", finishedAt: deps.now(), error: "\u626B\u63CF\u8BB0\u5F55\u505C\u5728 scanning\uFF0C\u4F46\u6CA1\u6709\u6B63\u5728\u8DD1\u7684\u626B\u63CF\u4F1A\u8BDD" } : s2)
59820
+ };
59821
+ await writeScanFile(dir, settled);
59741
59822
  return settled;
59742
59823
  };
59743
- const mustBeLiveScan = (personaId2, scan, method) => {
59744
- if (scan?.status !== "scanning") {
59745
- throw invalid(`${method} \u53EA\u5728\u626B\u63CF\u671F\u53EF\u7528\uFF08\u5F53\u524D ${scan?.status ?? "\u6CA1\u6709\u626B\u63CF\u8BB0\u5F55"}\uFF09`);
59746
- }
59747
- if (!isLiveScan(liveScans.get(personaId2), scan)) {
59748
- throw invalid(`${method} \u53EA\u5728\u626B\u63CF\u671F\u53EF\u7528\uFF08\u8BB0\u5F55\u505C\u5728 scanning\uFF0C\u4F46\u6CA1\u6709\u6B63\u5728\u8DD1\u7684\u626B\u63CF\u4F1A\u8BDD\uFF09`);
59749
- }
59750
- return scan;
59824
+ const mustBeLiveScan = (personaId2, file, method) => {
59825
+ const scanning = file.scans.filter((s2) => s2.status === "scanning");
59826
+ if (scanning.length === 0) {
59827
+ const last = file.scans[file.scans.length - 1];
59828
+ throw invalid(`${method} \u53EA\u5728\u626B\u63CF\u671F\u53EF\u7528\uFF08\u5F53\u524D ${last?.status ?? "\u6CA1\u6709\u626B\u63CF\u8BB0\u5F55"}\uFF09`);
59829
+ }
59830
+ const live = scanning.find((s2) => isLiveScan(liveScans.get(personaId2), s2));
59831
+ if (!live) throw invalid(`${method} \u53EA\u5728\u626B\u63CF\u671F\u53EF\u7528\uFF08\u8BB0\u5F55\u505C\u5728 scanning\uFF0C\u4F46\u6CA1\u6709\u6B63\u5728\u8DD1\u7684\u626B\u63CF\u4F1A\u8BDD\uFF09`);
59832
+ return live;
59833
+ };
59834
+ const mustScan = (file, scanId) => {
59835
+ const entry = file.scans.find((s2) => s2.id === scanId);
59836
+ if (!entry) throw invalid(`\u626B\u63CF\u5386\u53F2\u91CC\u6CA1\u6709 ${scanId} \u8FD9\u4E00\u6761\uFF0C\u5237\u65B0\u770B\u770B`);
59837
+ return entry;
59751
59838
  };
59752
59839
  const cacheMs = deps.secretsCacheMs ?? SECRETS_CACHE_MS;
59753
59840
  const secretsCache = /* @__PURE__ */ new Map();
@@ -59777,20 +59864,15 @@ function buildDeployHandlers(deps) {
59777
59864
  const github = await deps.githubIdentity.status();
59778
59865
  const auth = deps.githubIdentity.cached();
59779
59866
  const api = auth ? deps.githubApi(auth.token) : void 0;
59780
- let file;
59781
- let fileError;
59867
+ let file = emptyScanFile();
59868
+ let historyError;
59782
59869
  try {
59783
- file = await readDeployFile(dir);
59870
+ file = await readHistory(dir);
59784
59871
  } catch (err) {
59785
- fileError = err instanceof Error ? err.message : String(err);
59786
- }
59787
- let scan = await readScan(dir);
59788
- if (scan?.status === "scanning" && !isLiveScan(liveScans.get(personaId2), scan)) scan = await settleStale(dir, scan);
59789
- if (scan && scan.status === "done") {
59790
- const stale = describeStaleAssets(staleScannerAssets(scan, scannableAssets));
59791
- const kept = scan.notes.filter((n2) => !n2.startsWith(STALE_ASSETS_NOTE_PREFIX));
59792
- scan = { ...scan, notes: stale ? [...kept, stale] : kept };
59872
+ historyError = err instanceof Error ? err.message : String(err);
59793
59873
  }
59874
+ file = await settleStale(personaId2, dir, file);
59875
+ const { scans } = file;
59794
59876
  let secrets;
59795
59877
  let run;
59796
59878
  if (repo2 && api && github.status === "authorized") {
@@ -59811,72 +59893,56 @@ function buildDeployHandlers(deps) {
59811
59893
  personaId: personaId2,
59812
59894
  github,
59813
59895
  ...repo2 ? { repo: repo2 } : {},
59814
- ...file ? { file } : {},
59815
- ...scan ? { scan } : {},
59896
+ scans,
59897
+ ...file.released ? { released: file.released } : {},
59816
59898
  ...secrets ? { secrets } : {},
59817
59899
  ...run ? { run } : {},
59818
- ...fileError ? { fileError } : {}
59900
+ ...historyError ? { historyError } : {}
59819
59901
  };
59820
59902
  }
59821
59903
  const view = async (personaId2) => ({
59822
59904
  response: { type: "deploy:state", ...await stateOf(personaId2) }
59823
59905
  });
59824
- async function beginScan(personaId2, dir, current, head, want) {
59906
+ async function beginScan(personaId2, dir, file, contentHead, want) {
59825
59907
  liveScans.set(personaId2, {});
59826
- const prevManifest = await readDeployFile(dir).catch(() => void 0);
59908
+ const releasedEntry = file.released ? file.scans.find((s2) => s2.id === file.released.scanId) : void 0;
59827
59909
  const scanBase = pickScanBase({
59828
- // **文件在 ≠ 清单能用**:硬扫一开头就写了一份空清单,那一轮没跑完(OTA 重启 / butler 崩,
59829
- // 见 scan-task.ts 的收尾判定)的话文件在、内容是空的。所以判据连上「上一轮扫完了」。
59830
- hasUsableManifest: prevManifest !== void 0 && current?.status === "done",
59831
- releasedHead: await hasCommit(deps.exec, dir, current?.releasedHead) ? current?.releasedHead : void 0,
59910
+ // **有记录 ≠ 清单能用**:被发那条要是没跑完(OTA 重启 / butler 崩,见 scan-task.ts 的收尾判定)清单是半截的。
59911
+ hasUsableManifest: releasedEntry?.status === "done",
59912
+ releasedHead: await hasCommit(deps.exec, dir, file.released?.head) ? file.released?.head : void 0,
59832
59913
  ...want ? { want } : {}
59833
59914
  });
59834
- const startedAt = deps.now();
59835
- const base = {
59836
- evidence: scanBase.kind === "hard" ? [] : current?.evidence ?? [],
59837
- // notes 反过来不沿用:它是「这次扫描要老板处理的事」,沿用会把上次的事项一直复读
59838
- notes: [],
59839
- scannedAssets: [],
59840
- startedAt,
59841
- head,
59842
- // 发布记的那个 commit 跟扫描无关,原样带过去,别让重扫把软扫的基准弄丢。
59843
- //
59844
- // **哪怕上面 hasCommit 刚判定它已经不在仓库里,也照样留着**:探测失败也包括 git 没跑起来
59845
- // 这种一过性情况,据此把它抹掉就等于「一次 git 抽风 → 永久失去软扫,直到下次发布」,
59846
- // 比留一个死 sha 糟得多。留着的代价只是 UI 上「从零重扫」多显示一阵子(那期间两个按钮
59847
- // 都退硬扫、行为一致不出错),下次发布就自愈。
59848
- ...current?.releasedHead ? { releasedHead: current.releasedHead } : {},
59849
- baseKind: scanBase.kind,
59850
- ...scanBase.base ? { base: scanBase.base } : {}
59851
- };
59852
- await writeScan(dir, { status: "scanning", ...base });
59853
- if (scanBase.kind === "hard") await writeDeployFile(dir, emptyDeployFile());
59915
+ const lastDone = [...file.scans].reverse().find((s2) => s2.status === "done");
59916
+ const seed = scanBase.kind === "soft" && releasedEntry ? releasedEntry.manifest : { ...emptyDeployFile().manifest, connectors: (lastDone?.manifest.connectors ?? []).filter((c2) => c2.runner === "mcp") };
59854
59917
  const mcp = scanMcpConnectors(dir);
59855
- const current0 = await readDeployFile(dir) ?? emptyDeployFile();
59856
- const carried = scanBase.kind === "hard" ? (prevManifest?.manifest.connectors ?? []).filter((c2) => c2.runner === "mcp") : current0.manifest.connectors;
59857
- await writeDeployFile(dir, {
59858
- ...current0,
59859
- // 文件读不出来(坏 JSON)时保持原样:当成「零个 MCP」会把清单里的连接器清光,而发布闸对零连接器是放行的
59860
- manifest: { ...current0.manifest, connectors: mcp.readable ? mergeScannedConnectors(carried, mcp.connectors) : carried }
59861
- });
59862
59918
  const mcpTargets = new Set(mcp.evidence.map((e) => e.target));
59863
- const withMcp = {
59864
- ...base,
59865
- evidence: [...base.evidence.filter((e) => !mcpTargets.has(e.target)), ...mcp.evidence.map(redactEvidence)],
59866
- notes: mcp.warnings.map((w3) => `${MCP_SCAN_NOTE_PREFIX}${w3}`)
59919
+ const carriedEvidence = scanBase.kind === "soft" && releasedEntry ? releasedEntry.evidence : [];
59920
+ const entry = {
59921
+ id: newScanId(),
59922
+ status: "scanning",
59923
+ manifest: { ...seed, connectors: mcp.readable ? mergeScannedConnectors(seed.connectors, mcp.connectors) : seed.connectors },
59924
+ evidence: [...carriedEvidence.filter((e) => !mcpTargets.has(e.target)), ...mcp.evidence.map(redactEvidence)],
59925
+ // notes 不沿用:它是「这次扫描要老板处理的事」,沿用会把上次的事项一直复读
59926
+ notes: mcp.warnings.map((w3) => `${MCP_SCAN_NOTE_PREFIX}${w3}`),
59927
+ startedAt: deps.now(),
59928
+ head: contentHead,
59929
+ baseKind: scanBase.kind,
59930
+ ...scanBase.base ? { base: scanBase.base } : {}
59867
59931
  };
59868
- const started = deps.startScan(personaId2, scanBase);
59932
+ await writeScanFile(dir, replaceScan(file, entry));
59933
+ const started = deps.startScan(personaId2, scanBase, entry.id);
59869
59934
  if ("sessionId" in started) {
59870
59935
  liveScans.set(personaId2, { sessionId: started.sessionId });
59871
- await writeScan(dir, { status: "scanning", scanSessionId: started.sessionId, ...withMcp });
59936
+ await writeScanFile(dir, replaceScan(await readHistory(dir), { ...entry, scanSessionId: started.sessionId }));
59872
59937
  const mine = () => liveScans.get(personaId2)?.sessionId === started.sessionId;
59873
59938
  void started.ended.then(async (outcome) => {
59874
59939
  if (mine()) liveScans.delete(personaId2);
59875
- const current2 = await readScan(dir).catch(() => void 0);
59876
- if (liveScans.has(personaId2) || current2?.scanSessionId !== started.sessionId) return;
59877
- const settled = settleEndedScan(outcome, current2, deps.now());
59940
+ const current = await readHistory(dir).catch(() => void 0);
59941
+ const own = current?.scans.find((s2) => s2.id === entry.id);
59942
+ if (!current || !own || liveScans.has(personaId2) || own.scanSessionId !== started.sessionId) return;
59943
+ const settled = settleEndedScan(outcome, own, deps.now());
59878
59944
  if (!settled) return;
59879
- await writeScan(dir, settled);
59945
+ await writeScanFile(dir, replaceScan(current, settled));
59880
59946
  deps.warn?.("deploy scan ended without finishScan", { personaId: personaId2, why: settled.error });
59881
59947
  }).catch((err) => {
59882
59948
  if (mine()) liveScans.delete(personaId2);
@@ -59884,12 +59950,12 @@ function buildDeployHandlers(deps) {
59884
59950
  });
59885
59951
  } else {
59886
59952
  liveScans.delete(personaId2);
59887
- await writeScan(dir, {
59953
+ await writeScanFile(dir, replaceScan(await readHistory(dir), {
59954
+ ...entry,
59888
59955
  status: "failed",
59889
- ...withMcp,
59890
59956
  finishedAt: deps.now(),
59891
59957
  error: `\u8D77\u626B\u63CF\u4F1A\u8BDD\u5931\u8D25\uFF1A${started.error}`
59892
- });
59958
+ }));
59893
59959
  }
59894
59960
  }
59895
59961
  const get = async (frame, _client, ctx) => {
@@ -59902,20 +59968,21 @@ function buildDeployHandlers(deps) {
59902
59968
  ensureOwner(ctx);
59903
59969
  const args = parseArgs2(DeployPatchArgsSchema, frame, "deploy:patch");
59904
59970
  const dir = mustPersona(args.personaId);
59905
- const file = await readDeployFile(dir) ?? emptyDeployFile();
59906
- const next = {
59907
- ...file,
59908
- ...args.patch.manifest ? { manifest: args.patch.manifest } : {}
59909
- };
59910
- await writeDeployFile(dir, next);
59971
+ const file = await readHistory(dir);
59972
+ const entry = mustScan(file, args.scanId);
59973
+ if (entry.status === "failed") throw invalid("\u8FD9\u6B21\u626B\u63CF\u6CA1\u8DD1\u5B8C\uFF0C\u6E05\u5355\u4E0D\u5B8C\u6574\uFF0C\u6539\u5B83\u6CA1\u610F\u4E49\u2014\u2014\u91CD\u65B0\u626B\u63CF");
59974
+ if (entry.status === "scanning" && mustBeLiveScan(args.personaId, file, "deploy:patch").id !== entry.id) {
59975
+ throw invalid("deploy:patch \u6307\u540D\u7684\u4E0D\u662F\u6B63\u5728\u8DD1\u7684\u90A3\u6B21\u626B\u63CF");
59976
+ }
59977
+ if (args.patch.manifest) await writeScanFile(dir, replaceScan(file, { ...entry, manifest: args.patch.manifest }));
59911
59978
  return view(args.personaId);
59912
59979
  };
59913
59980
  const putSecret = async (frame, _client, ctx) => {
59914
59981
  ensureOwner(ctx);
59915
59982
  const args = parseArgs2(DeployPutSecretArgsSchema, frame, "deploy:putSecret");
59916
59983
  const dir = mustPersona(args.personaId);
59917
- const file = await readDeployFile(dir);
59918
- const declared = file ? [...file.manifest.secrets, ...sharedConnectorSecrets(file.manifest)].find((s2) => s2.name === args.name) : void 0;
59984
+ const { manifest } = mustScan(await readHistory(dir), args.scanId);
59985
+ const declared = [...manifest.secrets, ...sharedConnectorSecrets(manifest)].find((s2) => s2.name === args.name);
59919
59986
  if (!declared) throw invalid(`\u6E05\u5355\u91CC\u6CA1\u6709\u53EB\u300C${args.name}\u300D\u7684\u51ED\u636E`);
59920
59987
  const allowed = new Set(declared.env);
59921
59988
  for (const key of Object.keys(args.values)) {
@@ -59936,7 +60003,8 @@ function buildDeployHandlers(deps) {
59936
60003
  ensureOwner(ctx);
59937
60004
  const args = parseArgs2(DeployReportFindingArgsSchema, frame, "deploy:reportFinding");
59938
60005
  const dir = mustPersona(args.personaId);
59939
- const scan = mustBeLiveScan(args.personaId, await readScan(dir), "deploy:reportFinding");
60006
+ const file = await readHistory(dir);
60007
+ const scan = mustBeLiveScan(args.personaId, file, "deploy:reportFinding");
59940
60008
  let item;
59941
60009
  try {
59942
60010
  item = parseFinding(args.kind, args.item);
@@ -59944,37 +60012,34 @@ function buildDeployHandlers(deps) {
59944
60012
  throw invalid(err instanceof Error ? err.message : String(err));
59945
60013
  }
59946
60014
  if (args.kind === "connector") item = cliConnectorFromFinding(item);
59947
- const file = await readDeployFile(dir) ?? emptyDeployFile();
59948
- await writeDeployFile(dir, upsertFinding(file, args.kind, item));
59949
60015
  const target = `${args.kind}:${item.name}`;
59950
- await writeScan(dir, {
60016
+ await writeScanFile(dir, replaceScan(file, {
59951
60017
  ...scan,
60018
+ manifest: upsertFinding(scan.manifest, args.kind, item),
59952
60019
  evidence: [...scan.evidence.filter((e) => e.target !== target), redactEvidence({ target, ...args.evidence })]
59953
- });
60020
+ }));
59954
60021
  return view(args.personaId);
59955
60022
  };
59956
60023
  const finishScan = async (frame, _client, ctx) => {
59957
60024
  ensureOwner(ctx);
59958
60025
  const args = parseArgs2(DeployFinishScanArgsSchema, frame, "deploy:finishScan");
59959
60026
  const dir = mustPersona(args.personaId);
59960
- const scan = mustBeLiveScan(args.personaId, await readScan(dir), "deploy:finishScan");
59961
- await writeScan(dir, {
60027
+ const file = await readHistory(dir);
60028
+ const scan = mustBeLiveScan(args.personaId, file, "deploy:finishScan");
60029
+ await writeScanFile(dir, replaceScan(file, {
59962
60030
  ...scan,
59963
60031
  status: "done",
59964
60032
  finishedAt: deps.now(),
59965
60033
  // 存疑项也是 agent 的自由文本,跟证据同一道涂抹。daemon 自己扫 .mcp.json 时发现的问题
59966
60034
  //(带 MCP_SCAN_NOTE_PREFIX)留着——butler 不知道那些,它的那份不该把它们冲掉
59967
60035
  notes: [...scan.notes.filter((n2) => n2.startsWith(MCP_SCAN_NOTE_PREFIX)), ...args.notes.map(redactSecretValues)],
59968
- // 记下这次扫描**被告知有哪些共享资产**。以后资产表长出新条目时,只有扫的时候还不知道
59969
- // 它的那些记录会被提示一次(见 staleScannerAssets)
59970
- scannedAssets: scannableAssets.map((a2) => a2.name),
59971
60036
  error: void 0
59972
- });
60037
+ }));
59973
60038
  return view(args.personaId);
59974
60039
  };
59975
- const releaseHandler = async (frame, _client, ctx) => {
60040
+ const scanHandler = async (frame, _client, ctx) => {
59976
60041
  ensureOwner(ctx);
59977
- const { personaId: personaId2, scanBase } = parseArgs2(DeployReleaseArgsSchema, frame, "deploy:release");
60042
+ const { personaId: personaId2, scanBase } = parseArgs2(DeployScanArgsSchema, frame, "deploy:scan");
59978
60043
  const dir = mustPersona(personaId2);
59979
60044
  const wrong = wrongEngine(deps.personaEngine(personaId2)?.tool);
59980
60045
  if (wrong) throw invalid(wrong);
@@ -59990,33 +60055,43 @@ function buildDeployHandlers(deps) {
59990
60055
  repo2 = await repoInfo(deps.exec, dir);
59991
60056
  if (!repo2) throw new ClawdError(ERROR_CODES.INTERNAL, "\u4ED3\u5E93\u5EFA\u597D\u4E86\u4F46\u8BFB\u4E0D\u5230 origin");
59992
60057
  }
60058
+ if (repo2.branch !== "main") throw invalid(`\u5F53\u524D\u5728\u5206\u652F ${repo2.branch}\uFF0C\u4ED3\u5E93\u91CC\u7684\u6D41\u6C34\u7EBF\u53EA\u8BA4 main\uFF1B\u5207\u56DE main \u518D\u626B\u63CF`);
60059
+ const live = liveScans.get(personaId2);
60060
+ if (live && live.sessionId === void 0) return view(personaId2);
60061
+ let file = await readHistory(dir).catch(() => emptyScanFile());
60062
+ if (file.scans.some((s2) => s2.status === "scanning" && isLiveScan(live, s2))) return view(personaId2);
60063
+ file = await settleStale(personaId2, dir, file);
60064
+ let head;
60065
+ try {
60066
+ head = await commitAll(deps.exec, { dir, message: `clawd: \u626B\u63CF\u524D\u843D\u5B58 ${new Date(deps.now()).toISOString()}` });
60067
+ } catch (err) {
60068
+ throw toClawdError(err);
60069
+ }
60070
+ runCache.delete(personaId2);
60071
+ try {
60072
+ await beginScan(personaId2, dir, file, contentHeadOf({ head }, file.released), scanBase);
60073
+ } catch (err) {
60074
+ liveScans.delete(personaId2);
60075
+ throw err;
60076
+ }
60077
+ return view(personaId2);
60078
+ };
60079
+ const releaseHandler = async (frame, _client, ctx) => {
60080
+ ensureOwner(ctx);
60081
+ const { personaId: personaId2, scanId } = parseArgs2(DeployReleaseArgsSchema, frame, "deploy:release");
60082
+ const dir = mustPersona(personaId2);
60083
+ const wrong = wrongEngine(deps.personaEngine(personaId2)?.tool);
60084
+ if (wrong) throw invalid(wrong);
60085
+ const { auth, api } = await requireGithub();
60086
+ const repo2 = await repoInfo(deps.exec, dir);
60087
+ if (!repo2) throw invalid("\u8FD8\u6CA1\u6709\u4ED3\u5E93\u2014\u2014\u5148\u70B9\u300C\u5F00\u59CB\u626B\u63CF\u300D\uFF08\u4F1A\u5EFA\u4ED3\uFF09");
59993
60088
  if (repo2.branch !== "main") throw invalid(`\u5F53\u524D\u5728\u5206\u652F ${repo2.branch}\uFF0C\u4ED3\u5E93\u91CC\u7684\u6D41\u6C34\u7EBF\u53EA\u8BA4 main\uFF1B\u5207\u56DE main \u518D\u53D1\u5E03`);
59994
- const starting = liveScans.get(personaId2);
59995
- if (starting && starting.sessionId === void 0) return { response: { type: "deploy:release", outcome: "scanning" } };
59996
- let current = await readScan(dir);
59997
- if (current?.status === "scanning") {
59998
- if (isLiveScan(liveScans.get(personaId2), current)) return { response: { type: "deploy:release", outcome: "scanning" } };
59999
- current = await settleStale(dir, current);
60000
- }
60001
- const fileUsable = await readDeployFile(dir).then((f2) => f2 !== void 0).catch(() => false);
60002
- if (scanBase !== void 0 || !fileUsable || !isScanFresh(current, repo2)) {
60003
- let head;
60004
- try {
60005
- head = await commitAll(deps.exec, { dir, message: `clawd: \u626B\u63CF\u524D\u843D\u5B58 ${new Date(deps.now()).toISOString()}` });
60006
- } catch (err) {
60007
- throw toClawdError(err);
60008
- }
60009
- runCache.delete(personaId2);
60010
- try {
60011
- await beginScan(personaId2, dir, current, head, scanBase);
60012
- } catch (err) {
60013
- liveScans.delete(personaId2);
60014
- throw err;
60015
- }
60016
- return { response: { type: "deploy:release", outcome: "scanning" } };
60089
+ const file = await readHistory(dir);
60090
+ const entry = mustScan(file, scanId);
60091
+ if (entry.status !== "done") throw invalid(entry.status === "scanning" ? "\u8FD9\u6B21\u626B\u63CF\u8FD8\u5728\u8DD1\uFF0C\u7B49\u5B83\u626B\u5B8C" : "\u8FD9\u6B21\u626B\u63CF\u6CA1\u8DD1\u5B8C\uFF0C\u6E05\u5355\u4E0D\u5B8C\u6574\u2014\u2014\u91CD\u65B0\u626B\u63CF");
60092
+ if (!isScanFresh(entry, repo2, file.released)) {
60093
+ throw invalid(repo2.dirty ? "\u6709\u672A\u63D0\u4EA4\u7684\u6539\u52A8\uFF0C\u6E05\u5355\u662F\u6539\u52A8\u524D\u626B\u7684\u2014\u2014\u91CD\u65B0\u626B\u63CF" : "\u8FD9\u6B21\u626B\u63CF\u5BF9\u5E94\u7684\u4E0D\u662F\u5F53\u524D\u8FD9\u4E00\u7248 persona\u2014\u2014\u91CD\u65B0\u626B\u63CF");
60017
60094
  }
60018
- const file = await readDeployFile(dir);
60019
- if (!file) throw invalid("\u8FD8\u6CA1\u626B\u63CF\u51FA\u6E05\u5355");
60020
60095
  const blockers = [];
60021
60096
  let present;
60022
60097
  try {
@@ -60024,18 +60099,28 @@ function buildDeployHandlers(deps) {
60024
60099
  } catch (err) {
60025
60100
  throw toClawdError(err);
60026
60101
  }
60027
- const missing = requiredEnvNames(file).filter((e) => !present.has(e));
60102
+ const missing = requiredEnvNames(entry.manifest).filter((e) => !present.has(e));
60028
60103
  if (missing.length > 0) blockers.push(`${missing.join(" / ")} \u8FD8\u6CA1\u586B`);
60029
- for (const c2 of file.manifest.connectors) blockers.push(...connectorBlockers(c2));
60104
+ for (const c2 of entry.manifest.connectors) blockers.push(...connectorBlockers(c2));
60030
60105
  if (!deps.personaEngine(personaId2)?.model) {
60031
60106
  blockers.push("\u8FD8\u6CA1\u9009\u6A21\u578B\uFF08\u5207\u5F15\u64CE\u65F6\u4F1A\u6E05\u7A7A\uFF09\u2014\u2014\u53BB persona \u8BBE\u7F6E\u91CC\u9009\u4E00\u4E2A");
60032
60107
  }
60033
60108
  if (blockers.length > 0) throw invalid(`\u8FD8\u5DEE\uFF1A${blockers.join("\uFF1B")}`);
60109
+ const before = await readDeployFileRaw(dir);
60034
60110
  try {
60035
- const result = await release(deps.exec, api, { dir, now: deps.now, auth });
60111
+ await writeDeployFile(dir, { version: 1, manifest: entry.manifest });
60112
+ let result;
60113
+ try {
60114
+ result = await release(deps.exec, api, { dir, now: deps.now, auth });
60115
+ } catch (err) {
60116
+ if ((await repoInfo(deps.exec, dir))?.head === repo2.head) await restoreDeployFileRaw(dir, before);
60117
+ throw err;
60118
+ }
60036
60119
  runCache.delete(personaId2);
60037
- const scanned = await readScan(dir);
60038
- if (scanned) await writeScan(dir, { ...scanned, head: result.sha, releasedHead: result.sha });
60120
+ await writeScanFile(dir, {
60121
+ ...await readHistory(dir),
60122
+ released: { head: result.sha, contentHead: entry.head, scanId: entry.id, at: deps.now() }
60123
+ });
60039
60124
  return { response: { type: "deploy:release", ...result } };
60040
60125
  } catch (err) {
60041
60126
  throw toClawdError(err);
@@ -60046,7 +60131,7 @@ function buildDeployHandlers(deps) {
60046
60131
  const { personaId: personaId2, jobId } = parseArgs2(DeployRerunJobArgsSchema, frame, "deploy:rerunJob");
60047
60132
  const dir = mustPersona(personaId2);
60048
60133
  const repo2 = await repoInfo(deps.exec, dir);
60049
- if (!repo2) throw invalid("\u8FD8\u6CA1\u6709\u4ED3\u5E93\uFF0C\u5148\u70B9\u4E00\u6B21\u300C\u53D1\u5E03\u5230\u4E91\u4E0A\u300D\u5EFA\u4ED3");
60134
+ if (!repo2) throw invalid("\u8FD8\u6CA1\u6709\u4ED3\u5E93\u2014\u2014\u5148\u70B9\u300C\u5F00\u59CB\u626B\u63CF\u300D\uFF08\u4F1A\u5EFA\u4ED3\uFF09");
60050
60135
  const slug = repoSlug(repo2.url);
60051
60136
  const { api } = await requireGithub();
60052
60137
  const run = await latestRun(api, slug);
@@ -60064,6 +60149,7 @@ function buildDeployHandlers(deps) {
60064
60149
  };
60065
60150
  return {
60066
60151
  "deploy:get": get,
60152
+ "deploy:scan": scanHandler,
60067
60153
  "deploy:patch": patch,
60068
60154
  "deploy:putSecret": putSecret,
60069
60155
  "deploy:reportFinding": reportFinding,
@@ -61102,7 +61188,7 @@ ${formatLarkSpeakerReminder(speaker)}`
61102
61188
  }
61103
61189
 
61104
61190
  // src/lark-bot-connector/envelope.ts
61105
- var import_node_crypto8 = require("crypto");
61191
+ var import_node_crypto9 = require("crypto");
61106
61192
  var LARK_ENVELOPE_PREFIX = "clawdlbe1";
61107
61193
  function verifyLarkEventEnvelope(args) {
61108
61194
  const { token, publicKeyPem } = args;
@@ -61121,7 +61207,7 @@ function verifyLarkEventEnvelope(args) {
61121
61207
  const data = Buffer.from(`${prefix}.${payloadB64}`, "utf8");
61122
61208
  let sigOk = false;
61123
61209
  try {
61124
- sigOk = (0, import_node_crypto8.verify)(null, data, publicKeyPem, Buffer.from(sigB64, "base64url"));
61210
+ sigOk = (0, import_node_crypto9.verify)(null, data, publicKeyPem, Buffer.from(sigB64, "base64url"));
61125
61211
  } catch {
61126
61212
  sigOk = false;
61127
61213
  }
@@ -62488,6 +62574,7 @@ var GATEWAY_ROUTES = {
62488
62574
  healthz: () => "/healthz",
62489
62575
  session: (personaId2) => `/p/${encodeURIComponent(personaId2)}/session`,
62490
62576
  sessionSend: (personaId2, sessionId) => `/p/${encodeURIComponent(personaId2)}/session/${encodeURIComponent(sessionId)}`,
62577
+ sessionMirror: (personaId2, sessionId) => `/p/${encodeURIComponent(personaId2)}/session/${encodeURIComponent(sessionId)}/mirror`,
62491
62578
  // 调用方自己那块盘的用量(design 2026-09-19-cloud-volume-usage §3.3):同样按人、不挂 persona
62492
62579
  volume: () => "/me/volume"
62493
62580
  };
@@ -62504,7 +62591,7 @@ function cloudPersonaToFile(p3) {
62504
62591
  return {
62505
62592
  personaId: p3.personaId + CLOUD_PERSONA_ID_SUFFIX,
62506
62593
  label: p3.personaId,
62507
- tool: CODEX_CLOUD_TOOL_ID,
62594
+ tool: p3.engine + CLOUD_TOOL_SUFFIX,
62508
62595
  ...p3.model ? { model: p3.model } : {},
62509
62596
  ...p3.effort ? { effort: p3.effort } : {},
62510
62597
  public: false,
@@ -62552,9 +62639,14 @@ var CloudGatewayClient = class {
62552
62639
  for (const raw of rows) {
62553
62640
  const d2 = raw;
62554
62641
  if (d2.kind !== "official" || typeof d2.personaId !== "string") continue;
62642
+ if (typeof d2.engine !== "string" || !d2.engine) {
62643
+ this.deps.logger?.warn("cloud persona row has no engine, skipped", { personaId: d2.personaId });
62644
+ continue;
62645
+ }
62555
62646
  out.push({
62556
62647
  personaId: d2.personaId,
62557
62648
  gatewayUrl,
62649
+ engine: d2.engine,
62558
62650
  ...typeof d2.image === "string" ? { image: d2.image } : {},
62559
62651
  ...typeof d2.model === "string" && d2.model ? { model: d2.model } : {},
62560
62652
  ...typeof d2.effort === "string" && d2.effort ? { effort: d2.effort } : {},
@@ -62577,14 +62669,17 @@ var CloudGatewayClient = class {
62577
62669
  /**
62578
62670
  * 会话通道。返回的行流在 Pod 就绪前就能 write:写入先攒着,收到 `ready` 控制行后按序 POST 出去——
62579
62671
  * 编排那头一构造就发 initialize,不用知道 Pod 还在起。
62672
+ * `toolSessionId`:调用方预分配的引擎会话 id(pi 的 --session-id;codex 的 thread id 是协议内的、不传),进 open 的 body `engineSessionId`
62673
+ * (契约 2026-09-18 §3.2)。
62580
62674
  */
62581
- async openSession(personaId2) {
62675
+ async openSession(personaId2, opts = {}) {
62582
62676
  const { apiKey, gatewayUrl } = await this.credentials();
62583
62677
  return openHttpLineStream({
62584
62678
  fetchImpl: this.fetchImpl,
62585
62679
  gatewayUrl,
62586
62680
  apiKey,
62587
62681
  personaId: personaId2,
62682
+ ...opts.toolSessionId ? { engineSessionId: opts.toolSessionId } : {},
62588
62683
  logger: this.deps.logger
62589
62684
  });
62590
62685
  }
@@ -62629,7 +62724,7 @@ function parseControlLine(line) {
62629
62724
  }
62630
62725
  }
62631
62726
  function openHttpLineStream(opts) {
62632
- const { fetchImpl, gatewayUrl, apiKey, personaId: personaId2, logger } = opts;
62727
+ const { fetchImpl, gatewayUrl, apiKey, personaId: personaId2, engineSessionId, logger } = opts;
62633
62728
  const headers = { authorization: `Bearer ${apiKey}`, "content-type": "application/json" };
62634
62729
  const ac = new AbortController();
62635
62730
  const lineCbs = [];
@@ -62686,7 +62781,7 @@ function openHttpLineStream(opts) {
62686
62781
  res = await fetchImpl(gatewayUrl + GATEWAY_ROUTES.session(personaId2), {
62687
62782
  method: "POST",
62688
62783
  headers,
62689
- body: "{}",
62784
+ body: JSON.stringify(engineSessionId ? { engineSessionId } : {}),
62690
62785
  signal: ac.signal
62691
62786
  });
62692
62787
  } catch (e) {
@@ -62782,6 +62877,16 @@ function openHttpLineStream(opts) {
62782
62877
  close: () => {
62783
62878
  ac.abort();
62784
62879
  finish();
62880
+ },
62881
+ mirror: async () => {
62882
+ if (closed || !sessionId) return;
62883
+ const url = gatewayUrl + GATEWAY_ROUTES.sessionMirror(personaId2, sessionId);
62884
+ try {
62885
+ const res = await fetchImpl(url, { method: "POST", headers, body: "" });
62886
+ if (!res.ok) logger?.warn("cloud session mirror failed", { status: res.status });
62887
+ } catch (e) {
62888
+ if (!closed) logger?.warn("cloud session mirror error", { message: e.message });
62889
+ }
62785
62890
  }
62786
62891
  };
62787
62892
  }
@@ -62928,6 +63033,7 @@ function createCloudCodexSession(ctx, sink, deps) {
62928
63033
  paths.catch(() => {
62929
63034
  });
62930
63035
  stream.onClose(() => rejectPaths(new Error("\u4E91\u4E0A\u901A\u9053\u5728 ready \u4E4B\u524D\u5173\u4E86\uFF0C\u62FF\u4E0D\u5230 Pod \u8DEF\u5F84")));
63036
+ let channel = null;
62931
63037
  const driver = driveCodexThread({
62932
63038
  ctx,
62933
63039
  sink,
@@ -62937,7 +63043,8 @@ function createCloudCodexSession(ctx, sink, deps) {
62937
63043
  imageInput: cloudImageInput(deps),
62938
63044
  // skills 在 Pod 里由 persona 目录自带,本机路径对它没意义
62939
63045
  // 「让对端走人」= 断开下行流,gateway 收到就删 Job;信号对 Pod 没有意义
62940
- kill: () => stream.close()
63046
+ kill: () => stream.close(),
63047
+ onTurnCompleted: () => void channel?.mirror()
62941
63048
  });
62942
63049
  if (!personaId2) {
62943
63050
  queueMicrotask(() => {
@@ -62947,7 +63054,8 @@ function createCloudCodexSession(ctx, sink, deps) {
62947
63054
  return driver.session;
62948
63055
  }
62949
63056
  void deps.client.openSession(personaId2).then(
62950
- (channel) => {
63057
+ (opened) => {
63058
+ channel = opened;
62951
63059
  channel.onControl((c2) => {
62952
63060
  if (c2.kind === "ready") {
62953
63061
  resolvePaths({ personaDir: c2.personaDir, workDir: c2.workDir });
@@ -63110,7 +63218,7 @@ var PiRpcClient = class {
63110
63218
 
63111
63219
  // src/tools/pi-session.ts
63112
63220
  var import_node_child_process10 = require("child_process");
63113
- var import_node_crypto9 = require("crypto");
63221
+ var import_node_crypto10 = require("crypto");
63114
63222
  var import_node_fs35 = __toESM(require("fs"), 1);
63115
63223
  var import_node_path39 = __toESM(require("path"), 1);
63116
63224
 
@@ -63288,7 +63396,7 @@ ${dirs.map((d2) => `- ${d2}`).join("\n")}`;
63288
63396
  }
63289
63397
  function createPiSession(ctx, sink, deps = {}) {
63290
63398
  const cmd = process.env.PI_BIN ?? "pi";
63291
- const sessionId = ctx.toolSessionId ?? (0, import_node_crypto9.randomUUID)();
63399
+ const sessionId = ctx.toolSessionId ?? (0, import_node_crypto10.randomUUID)();
63292
63400
  const skillsDir = import_node_path39.default.join(ctx.cwd, ".claude", "skills");
63293
63401
  const exists = deps.skillsDirExists ?? ((d2) => import_node_fs35.default.existsSync(d2));
63294
63402
  const args = buildPiArgs(ctx, {
@@ -64735,7 +64843,7 @@ var wrapper_default = import_websocket.default;
64735
64843
 
64736
64844
  // src/transport/local-ws-server.ts
64737
64845
  var import_node_http = __toESM(require("http"), 1);
64738
- var import_node_crypto10 = require("crypto");
64846
+ var import_node_crypto11 = require("crypto");
64739
64847
  var LocalWsServer = class {
64740
64848
  constructor(opts) {
64741
64849
  this.opts = opts;
@@ -64952,7 +65060,7 @@ var LocalWsServer = class {
64952
65060
  }
64953
65061
  }
64954
65062
  onConnection(socket, remoteAddress) {
64955
- const id = (0, import_node_crypto10.randomUUID)();
65063
+ const id = (0, import_node_crypto11.randomUUID)();
64956
65064
  const subscribedSessions = /* @__PURE__ */ new Map();
64957
65065
  const handle = {
64958
65066
  id,
@@ -65917,7 +66025,7 @@ var import_node_path49 = __toESM(require("path"), 1);
65917
66025
  // src/attachment/upload.ts
65918
66026
  var import_node_fs43 = __toESM(require("fs"), 1);
65919
66027
  var import_node_path47 = __toESM(require("path"), 1);
65920
- var import_node_crypto11 = __toESM(require("crypto"), 1);
66028
+ var import_node_crypto12 = __toESM(require("crypto"), 1);
65921
66029
  var import_promises7 = require("stream/promises");
65922
66030
  var UploadError = class extends Error {
65923
66031
  constructor(code, message) {
@@ -65941,11 +66049,11 @@ async function writeUploadedAttachment(args) {
65941
66049
  } catch (err) {
65942
66050
  throw new UploadError("STORAGE_ERROR", `mkdir failed: ${err.message}`);
65943
66051
  }
65944
- const hasher = import_node_crypto11.default.createHash("sha256");
66052
+ const hasher = import_node_crypto12.default.createHash("sha256");
65945
66053
  let actualSize = 0;
65946
66054
  const tmpPath = import_node_path47.default.join(
65947
66055
  attachmentsRoot,
65948
- `.upload-${process.pid}-${Date.now()}-${import_node_crypto11.default.randomBytes(4).toString("hex")}`
66056
+ `.upload-${process.pid}-${Date.now()}-${import_node_crypto12.default.randomBytes(4).toString("hex")}`
65949
66057
  );
65950
66058
  try {
65951
66059
  await (0, import_promises7.pipeline)(
@@ -66789,7 +66897,7 @@ function runAttachmentGc(args) {
66789
66897
  // src/attachment/group.ts
66790
66898
  var import_node_fs46 = __toESM(require("fs"), 1);
66791
66899
  var import_node_path51 = __toESM(require("path"), 1);
66792
- var import_node_crypto12 = __toESM(require("crypto"), 1);
66900
+ var import_node_crypto13 = __toESM(require("crypto"), 1);
66793
66901
  init_protocol();
66794
66902
  var GroupFileStore = class {
66795
66903
  dataDir;
@@ -66878,7 +66986,7 @@ var GroupFileStore = class {
66878
66986
  entries[idx] = next;
66879
66987
  } else {
66880
66988
  next = {
66881
- id: `gf-${import_node_crypto12.default.randomBytes(6).toString("base64url")}`,
66989
+ id: `gf-${import_node_crypto13.default.randomBytes(6).toString("base64url")}`,
66882
66990
  relPath: input.relPath,
66883
66991
  from: input.from,
66884
66992
  label: input.label,
@@ -67024,7 +67132,7 @@ function readSpawnedByDesktopFromEnv(env = process.env) {
67024
67132
  // src/tunnel/tunnel-manager.ts
67025
67133
  var import_node_fs51 = __toESM(require("fs"), 1);
67026
67134
  var import_node_path56 = __toESM(require("path"), 1);
67027
- var import_node_crypto13 = __toESM(require("crypto"), 1);
67135
+ var import_node_crypto14 = __toESM(require("crypto"), 1);
67028
67136
  var import_node_child_process15 = require("child_process");
67029
67137
 
67030
67138
  // src/tunnel/tunnel-store.ts
@@ -67523,7 +67631,7 @@ var TunnelManager = class {
67523
67631
  override: this.deps.frpcBinaryOverride ?? void 0
67524
67632
  });
67525
67633
  const tomlPath = import_node_path56.default.join(this.deps.dataDir, "frpc.toml");
67526
- const proxyName = `clawd-${t2.subdomain}-${localPort}-${import_node_crypto13.default.randomBytes(3).toString("hex")}`;
67634
+ const proxyName = `clawd-${t2.subdomain}-${localPort}-${import_node_crypto14.default.randomBytes(3).toString("hex")}`;
67527
67635
  const toml = buildFrpcToml({
67528
67636
  serverAddr: t2.frpsHost,
67529
67637
  serverPort: t2.frpsPort,
@@ -67624,7 +67732,7 @@ async function waitForFrpcReady(proc, timeoutMs) {
67624
67732
  // src/tunnel/device-key.ts
67625
67733
  var import_node_os18 = __toESM(require("os"), 1);
67626
67734
  var import_node_path57 = __toESM(require("path"), 1);
67627
- var import_node_crypto14 = __toESM(require("crypto"), 1);
67735
+ var import_node_crypto15 = __toESM(require("crypto"), 1);
67628
67736
  var DERIVE_SALT = "clawd-tunnel-device-v1";
67629
67737
  function deriveStableDeviceKey(opts = {}) {
67630
67738
  const hostname = opts.hostname ?? import_node_os18.default.hostname();
@@ -67634,13 +67742,13 @@ function deriveStableDeviceKey(opts = {}) {
67634
67742
  const normalizedDataDir = opts.dataDir ? import_node_path57.default.resolve(opts.dataDir) : null;
67635
67743
  const isDefaultDir = normalizedDataDir == null || normalizedDataDir === defaultDataDir;
67636
67744
  const input = isDefaultDir ? `${hostname}::${uid}` : `${hostname}::${uid}::${normalizedDataDir}`;
67637
- return import_node_crypto14.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
67745
+ return import_node_crypto15.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
67638
67746
  }
67639
67747
 
67640
67748
  // src/auth-store.ts
67641
67749
  var import_node_fs52 = __toESM(require("fs"), 1);
67642
67750
  var import_node_path58 = __toESM(require("path"), 1);
67643
- var import_node_crypto15 = __toESM(require("crypto"), 1);
67751
+ var import_node_crypto16 = __toESM(require("crypto"), 1);
67644
67752
  var AUTH_FILE_NAME = "auth.json";
67645
67753
  function authFilePath(dataDir) {
67646
67754
  return import_node_path58.default.join(dataDir, AUTH_FILE_NAME);
@@ -67677,10 +67785,10 @@ function loadOrCreateTunnelDeviceKey(opts) {
67677
67785
  return tunnelDeviceKey;
67678
67786
  }
67679
67787
  function defaultGenerateToken() {
67680
- return import_node_crypto15.default.randomBytes(32).toString("base64url");
67788
+ return import_node_crypto16.default.randomBytes(32).toString("base64url");
67681
67789
  }
67682
67790
  function defaultGenerateOwnerPrincipalId() {
67683
- return `owner-${import_node_crypto15.default.randomUUID()}`;
67791
+ return `owner-${import_node_crypto16.default.randomUUID()}`;
67684
67792
  }
67685
67793
  function readAuthFile(file) {
67686
67794
  try {
@@ -67812,7 +67920,7 @@ var OwnerIdentityStore = class {
67812
67920
  };
67813
67921
 
67814
67922
  // src/feishu-auth/login-flow.ts
67815
- var import_node_crypto16 = __toESM(require("crypto"), 1);
67923
+ var import_node_crypto17 = __toESM(require("crypto"), 1);
67816
67924
  var STATE_TTL_MS = 5 * 60 * 1e3;
67817
67925
  var LoginFlow = class {
67818
67926
  constructor(deps) {
@@ -67821,7 +67929,7 @@ var LoginFlow = class {
67821
67929
  deps;
67822
67930
  pendingStates = /* @__PURE__ */ new Map();
67823
67931
  start() {
67824
- const state = import_node_crypto16.default.randomBytes(16).toString("base64url");
67932
+ const state = import_node_crypto17.default.randomBytes(16).toString("base64url");
67825
67933
  const now = (this.deps.now ?? Date.now)();
67826
67934
  this.pendingStates.set(state, now);
67827
67935
  this.gcExpired(now);
@@ -68352,7 +68460,7 @@ init_protocol();
68352
68460
  var import_node_fs55 = __toESM(require("fs"), 1);
68353
68461
  var import_node_os20 = __toESM(require("os"), 1);
68354
68462
  var import_node_path61 = __toESM(require("path"), 1);
68355
- var import_node_crypto17 = require("crypto");
68463
+ var import_node_crypto18 = require("crypto");
68356
68464
  init_claude_history();
68357
68465
  function readJsonlEntries(file) {
68358
68466
  const raw = import_node_fs55.default.readFileSync(file, "utf8");
@@ -68389,7 +68497,7 @@ function forkSession(input) {
68389
68497
  throw new Error(`fork: messageUuid not found in transcript: ${input.messageUuid}`);
68390
68498
  }
68391
68499
  const sliced = mainEntries.slice(0, targetIdx + 1);
68392
- const forkedToolSessionId = (0, import_node_crypto17.randomUUID)();
68500
+ const forkedToolSessionId = (0, import_node_crypto18.randomUUID)();
68393
68501
  let parentUuid = null;
68394
68502
  const forkedLines = [];
68395
68503
  for (const entry of sliced) {
@@ -70715,6 +70823,7 @@ var METHOD_GRANT_MAP = {
70715
70823
  // deploy:*(persona 上云):全部 owner-only。里面有凭据录入口、建仓和推送,
70716
70824
  // guest 不该看见更不该动。
70717
70825
  "deploy:get": ADMIN_ANY,
70826
+ "deploy:scan": ADMIN_ANY,
70718
70827
  "deploy:patch": ADMIN_ANY,
70719
70828
  "deploy:putSecret": ADMIN_ANY,
70720
70829
  "deploy:reportFinding": ADMIN_ANY,
@@ -70928,6 +71037,7 @@ var METHOD_GRANT_MAP = {
70928
71037
  "device:connections": ADMIN_ANY
70929
71038
  };
70930
71039
  var HUMAN_ONLY_METHODS = [
71040
+ "deploy:scan",
70931
71041
  "deploy:release",
70932
71042
  "deploy:putSecret",
70933
71043
  "deploy:rerunJob",
@@ -70975,7 +71085,7 @@ function computeMethodAccess(args) {
70975
71085
  }
70976
71086
 
70977
71087
  // src/version.ts
70978
- var version = "0.2.552".length > 0 ? "0.2.552" : "dev";
71088
+ var version = "0.2.554".length > 0 ? "0.2.554" : "dev";
70979
71089
 
70980
71090
  // src/cli-probe/probe.ts
70981
71091
  var fs78 = __toESM(require("fs"), 1);
@@ -71372,7 +71482,7 @@ init_protocol();
71372
71482
  // src/extension/bundle-zip.ts
71373
71483
  var import_promises11 = __toESM(require("fs/promises"), 1);
71374
71484
  var import_node_path67 = __toESM(require("path"), 1);
71375
- var import_node_crypto18 = __toESM(require("crypto"), 1);
71485
+ var import_node_crypto19 = __toESM(require("crypto"), 1);
71376
71486
  var import_jszip2 = __toESM(require_lib3(), 1);
71377
71487
  async function bundleExtensionDir(dir) {
71378
71488
  const entries = await listFilesSorted(dir);
@@ -71387,7 +71497,7 @@ async function bundleExtensionDir(dir) {
71387
71497
  compression: "DEFLATE",
71388
71498
  compressionOptions: { level: 6 }
71389
71499
  });
71390
- const sha256 = import_node_crypto18.default.createHash("sha256").update(buffer).digest("hex");
71500
+ const sha256 = import_node_crypto19.default.createHash("sha256").update(buffer).digest("hex");
71391
71501
  return { buffer, sha256 };
71392
71502
  }
71393
71503
  var FIXED_DATE = /* @__PURE__ */ new Date("2020-01-01T00:00:00.000Z");
@@ -71455,7 +71565,7 @@ function computePublishCheck(args) {
71455
71565
  var import_promises12 = __toESM(require("fs/promises"), 1);
71456
71566
  var import_node_path69 = __toESM(require("path"), 1);
71457
71567
  var import_node_os25 = __toESM(require("os"), 1);
71458
- var import_node_crypto19 = __toESM(require("crypto"), 1);
71568
+ var import_node_crypto20 = __toESM(require("crypto"), 1);
71459
71569
  var import_jszip3 = __toESM(require_lib3(), 1);
71460
71570
 
71461
71571
  // src/extension/paths.ts
@@ -71484,7 +71594,7 @@ var InstallError = class extends Error {
71484
71594
  };
71485
71595
  async function installFromChannel(args, deps) {
71486
71596
  const { channelRef, snapshotHash, bundleZip } = args;
71487
- const computed = import_node_crypto19.default.createHash("sha256").update(bundleZip).digest("hex");
71597
+ const computed = import_node_crypto20.default.createHash("sha256").update(bundleZip).digest("hex");
71488
71598
  if (computed !== snapshotHash) {
71489
71599
  throw new InstallError(
71490
71600
  "HASH_MISMATCH",
@@ -71576,7 +71686,7 @@ async function installFromChannel(args, deps) {
71576
71686
  var import_promises13 = __toESM(require("fs/promises"), 1);
71577
71687
  var import_node_path70 = __toESM(require("path"), 1);
71578
71688
  var import_node_os26 = __toESM(require("os"), 1);
71579
- var import_node_crypto20 = __toESM(require("crypto"), 1);
71689
+ var import_node_crypto21 = __toESM(require("crypto"), 1);
71580
71690
  var import_jszip4 = __toESM(require_lib3(), 1);
71581
71691
  var UpdateError = class extends Error {
71582
71692
  constructor(code, message) {
@@ -71614,7 +71724,7 @@ async function updateFromChannel(args, deps) {
71614
71724
  if (e instanceof UpdateError) throw e;
71615
71725
  throw e;
71616
71726
  }
71617
- const computed = import_node_crypto20.default.createHash("sha256").update(bundleZip).digest("hex");
71727
+ const computed = import_node_crypto21.default.createHash("sha256").update(bundleZip).digest("hex");
71618
71728
  if (computed !== snapshotHash) {
71619
71729
  throw new UpdateError(
71620
71730
  "HASH_MISMATCH",
@@ -72743,7 +72853,7 @@ async function uninstall(deps) {
72743
72853
  }
72744
72854
 
72745
72855
  // src/handlers/index.ts
72746
- var import_node_crypto21 = require("crypto");
72856
+ var import_node_crypto22 = require("crypto");
72747
72857
 
72748
72858
  // src/handlers/peer-exec.ts
72749
72859
  init_protocol();
@@ -72898,7 +73008,7 @@ function buildMethodHandlers(deps) {
72898
73008
  const c2 = deps.contactStore.get(deviceId);
72899
73009
  return c2 ? { deviceId: c2.deviceId, remoteUrl: c2.remoteUrl, connectToken: c2.connectToken } : null;
72900
73010
  },
72901
- genId: () => (0, import_node_crypto21.randomUUID)(),
73011
+ genId: () => (0, import_node_crypto22.randomUUID)(),
72902
73012
  now: () => Date.now(),
72903
73013
  forwardInboxPostToPeer,
72904
73014
  logger: deps.logger
@@ -73627,7 +73737,7 @@ async function startDaemon(config) {
73627
73737
  const channelStore = createChannelStore({
73628
73738
  filePath: import_node_path78.default.join(config.dataDir, "channels.json"),
73629
73739
  now: () => Date.now(),
73630
- genId: () => `ch-${(0, import_node_crypto22.randomUUID)().slice(0, 8)}`,
73740
+ genId: () => `ch-${(0, import_node_crypto23.randomUUID)().slice(0, 8)}`,
73631
73741
  onFlushError: (err) => logger.error("channel store flush failed", {
73632
73742
  reason: err instanceof Error ? err.message : String(err)
73633
73743
  })
@@ -73636,7 +73746,7 @@ async function startDaemon(config) {
73636
73746
  const topicStore = createTopicStore({
73637
73747
  filePath: import_node_path78.default.join(config.dataDir, "topics.json"),
73638
73748
  now: () => Date.now(),
73639
- genId: () => `tp-${(0, import_node_crypto22.randomUUID)().slice(0, 8)}`,
73749
+ genId: () => `tp-${(0, import_node_crypto23.randomUUID)().slice(0, 8)}`,
73640
73750
  onFlushError: (err) => logger.error("topic store flush failed", {
73641
73751
  reason: err instanceof Error ? err.message : String(err)
73642
73752
  })
@@ -73644,7 +73754,7 @@ async function startDaemon(config) {
73644
73754
  await topicStore.load();
73645
73755
  let deliverPendingRef;
73646
73756
  const personaDispatchManager = new PersonaDispatchManager({
73647
- genId: () => (0, import_node_crypto22.randomUUID)(),
73757
+ genId: () => (0, import_node_crypto23.randomUUID)(),
73648
73758
  store: dispatchStore,
73649
73759
  now: () => Date.now(),
73650
73760
  onCompleted: (sid) => deliverPendingRef?.(sid),
@@ -74609,15 +74719,15 @@ async function startDaemon(config) {
74609
74719
  });
74610
74720
  Object.assign(handlers, shiftHandlers);
74611
74721
  {
74612
- const scans = await Promise.all(personaStore.list().map(async (personaId2) => ({
74722
+ const files = await Promise.all(personaStore.list().map(async (personaId2) => ({
74613
74723
  personaId: personaId2,
74614
- scan: await readScan(personaStore.personaDirPath(personaId2)).catch(() => void 0)
74724
+ file: await readScanFile(personaStore.personaDirPath(personaId2)).catch(() => void 0)
74615
74725
  })));
74616
74726
  const interrupted = reconcileInterruptedScans({
74617
- scans,
74727
+ files,
74618
74728
  now: Date.now(),
74619
- write: (personaId2, scan) => {
74620
- void writeScan(personaStore.personaDirPath(personaId2), scan).catch((err) => {
74729
+ write: (personaId2, file) => {
74730
+ void writeScanFile(personaStore.personaDirPath(personaId2), file).catch((err) => {
74621
74731
  logger.warn("deploy scan reconcile write failed", { personaId: personaId2, error: String(err) });
74622
74732
  });
74623
74733
  }
@@ -74638,10 +74748,7 @@ async function startDaemon(config) {
74638
74748
  org: PERSONA_REPO_ORG,
74639
74749
  now: () => Date.now(),
74640
74750
  warn: (msg, meta) => logger.warn(msg, meta),
74641
- startScan: (personaId2, scanBase) => {
74642
- const assetLines = SCANNABLE_ASSETS.map(
74643
- (a2) => ` - ${a2.name}${a2.path ? `\uFF08${a2.path(config.dataDir)}\uFF09` : ""}\uFF1A${a2.what}`
74644
- ).join("\n");
74751
+ startScan: (personaId2, scanBase, scanId) => {
74645
74752
  const baseLines = scanBase.base ? [
74646
74753
  `\u672C\u6B21\u662F**${SCAN_BASE_LABEL[scanBase.kind]}**\uFF1A\u53EA\u770B\u8DDF\u7EBF\u4E0A\u6B63\u8DD1\u90A3\u7248\u7684\u5DEE\u522B\uFF0C`,
74647
74754
  `\u90A3\u4E00\u7248\u662F commit ${scanBase.base}\u3002`,
@@ -74651,14 +74758,12 @@ async function startDaemon(config) {
74651
74758
  `\u672C\u6B21\u662F**${SCAN_BASE_LABEL[scanBase.kind]}**\uFF1A\u4ECE\u96F6\u628A persona \u6574\u4E2A\u8BFB\u4E00\u904D\uFF0C\u6E05\u5355\u662F\u7A7A\u7684\u3002`
74652
74759
  ];
74653
74760
  const prompt = [
74654
- `\u626B\u63CF persona\u300C${personaId2}\u300D\u7684\u90E8\u7F72\u73AF\u5883\uFF0CpersonaId=${personaId2}\u3002`,
74761
+ `\u626B\u63CF persona\u300C${personaId2}\u300D\u7684\u90E8\u7F72\u73AF\u5883\uFF0CpersonaId=${personaId2}\uFF0CscanId=${scanId}\u3002`,
74655
74762
  "",
74656
74763
  `persona \u76EE\u5F55\uFF1A${personaStore.personaDirPath(personaId2)}`,
74764
+ `\u8FD9\u6B21\u626B\u63CF\u5728\u5386\u53F2\u91CC\u7684 id \u662F ${scanId}\uFF08\u6536\u5C3E\u524D\u8981\u5220\u6E05\u5355\u6761\u76EE\u65F6 deploy:patch \u7528\u5B83\u6307\u540D\uFF09\u3002`,
74657
74765
  ...baseLines,
74658
74766
  "",
74659
- "\u53EF\u4EE5\u58F0\u660E\u7684 persona \u76EE\u5F55\u4E4B\u5916\u7684\u5171\u4EAB\u8D44\u4EA7\uFF08\u7528\u5230\u54EA\u6761\u5C31\u62A5\u54EA\u6761\uFF09\uFF1A",
74660
- assetLines || " \uFF08\u65E0\uFF09",
74661
- "",
74662
74767
  "\u6309 scan-persona-for-deploy skill \u5E72\u6D3B\uFF1A\u8FB9\u67E5\u8FB9\u7528 deploy:reportFinding \u4E0A\u62A5\uFF0C",
74663
74768
  "\u5168\u90E8\u67E5\u5B8C\u8C03 deploy:finishScan \u6536\u5C3E\u3002"
74664
74769
  ].join("\n");