@clawos-dev/clawd 0.2.292 → 0.2.293

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.
Files changed (2) hide show
  1. package/dist/cli.cjs +129 -57
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -43776,9 +43776,11 @@ init_protocol();
43776
43776
  var import_node_os2 = require("os");
43777
43777
  var FORCED_DENY_READ = ["~/"];
43778
43778
  function personaEditDeny(personaDir) {
43779
+ return `Edit(${tildePath(personaDir)}/**)`;
43780
+ }
43781
+ function tildePath(p2) {
43779
43782
  const home = (0, import_node_os2.homedir)();
43780
- const rel = personaDir.startsWith(`${home}/`) ? `~${personaDir.slice(home.length)}` : personaDir;
43781
- return `Edit(${rel}/**)`;
43783
+ return p2.startsWith(`${home}/`) ? `~${p2.slice(home.length)}` : p2;
43782
43784
  }
43783
43785
  function unionArr(a, b2) {
43784
43786
  const out = [...a ?? []];
@@ -43794,8 +43796,8 @@ function buildGuestSettingsV1() {
43794
43796
  return {
43795
43797
  permissions: {
43796
43798
  defaultMode: "dontAsk"
43797
- // persona-dir 的 Edit deny 由 composeGuestSandbox spawnCwd 注入 '~/<persona>/**' 绝对规则
43798
- // (base 不放相对 './'——会随 getCwd 漂移误拦项目,见 personaEditDeny)。
43799
+ // persona-dir 的 Edit deny 由 store.writeSandboxSettings 落盘注入 '~/<persona>/**' 绝对规则
43800
+ // (base 模板不放相对 './'——会随 getCwd 漂移误拦项目,见 personaEditDeny)。
43799
43801
  },
43800
43802
  sandbox: {
43801
43803
  enabled: true,
@@ -43808,7 +43810,7 @@ function buildGuestSettingsV1() {
43808
43810
  denyWrite: [],
43809
43811
  // **不放 '~/'**(写是白名单制,home 自动不可写);persona(cwd) 由 compose 强制 deny
43810
43812
  allowRead: [],
43811
- // userWorkDir + persona cwd 由 compose 强制注入,profile 加工具链精确路径
43813
+ // persona 读权由 store.writeSandboxSettings 落盘注入;userWorkDir 由 compose 注入;profile 加工具链精确路径
43812
43814
  allowWrite: []
43813
43815
  }
43814
43816
  }
@@ -43846,7 +43848,7 @@ function composeGuestSandbox(base, userWorkDir, spawnCwd) {
43846
43848
  s.sandbox.allowUnsandboxedCommands = false;
43847
43849
  fsv.denyRead = unionArr(fsv.denyRead, FORCED_DENY_READ);
43848
43850
  fsv.denyWrite = unionArr((fsv.denyWrite ?? []).filter((p2) => p2 !== "~/"), [spawnCwd]);
43849
- fsv.allowRead = unionArr(fsv.allowRead, [userWorkDir, spawnCwd]);
43851
+ fsv.allowRead = unionArr(fsv.allowRead, [userWorkDir]);
43850
43852
  fsv.allowWrite = unionArr(fsv.allowWrite, [userWorkDir]);
43851
43853
  return s;
43852
43854
  }
@@ -44088,6 +44090,8 @@ function buildConnectionPrompt(args) {
44088
44090
  let line = `\u4F60\u7684\u6587\u4EF6\u5DE5\u4F5C\u76EE\u5F55\u662F ${args.workDir}\uFF0C\u8FD9\u4E2A\u76EE\u5F55\u5BF9\u4F60\u53EF\u8BFB\u53EF\u5199\uFF0C\u6587\u4EF6\u7684\u8BFB\u53D6/\u521B\u5EFA/\u4FEE\u6539\u90FD\u5728\u8FD9\u91CC\u8FDB\u884C\u3002`;
44089
44091
  if (args.mode === "guest") {
44090
44092
  line += "persona \u76EE\u5F55\u5BF9\u4F60\u53EA\u8BFB\uFF0C\u4E0D\u8981\u5F80\u91CC\u5199\u3002";
44093
+ } else {
44094
+ line += "persona \u76EE\u5F55\u5B58\u653E\u7684\u662F\u4EBA\u683C\u914D\u7F6E\uFF1B\u5DE5\u4F5C\u4EA7\u7269\u3001\u4E34\u65F6\u6587\u4EF6\u3001\u4E0B\u8F7D\u5185\u5BB9\u9ED8\u8BA4\u5199\u5230\u4E0A\u9762\u7684\u5DE5\u4F5C\u76EE\u5F55\uFF0C\u4E0D\u8981\u843D\u5728 persona \u76EE\u5F55\u91CC\uFF08\u66F4\u65B0\u4EBA\u683C\u914D\u7F6E\u672C\u8EAB\u9664\u5916\uFF09\u3002";
44091
44095
  }
44092
44096
  parts.push(line);
44093
44097
  }
@@ -47736,21 +47740,40 @@ var PersonaStore = class {
47736
47740
  * 覆盖式写 persona 级 sandbox 配置(seed 给有 profile 的 persona 写 base⊕profile;owner 之后可手编)。
47737
47741
  * 这份是 guest 沙箱的 **base 真源**:manager spawn 时 composeGuestSandbox 读它 + 叠 sandbox 强制底座
47738
47742
  * (enabled/denyRead/denyWrite/userWorkDir owner 削弱不了)→ 写 per-guest 文件给 CC。
47739
- * 写入时强制注入本 persona Edit-deny('~/<persona>/**' 按实际 persona dir 算)+ 剥掉历史相对 'Edit(./**)',
47740
- * 让落盘 base 就是 source of truth(compose 不再运行时替换 permission deny)。
47743
+ * 写入时强制注入本 persona 的**基线**(Edit-deny + allowRead,见 injectPersonaBaseline),
47744
+ * 让落盘 base 就是 source of truth(compose 不再运行时替换 permission deny / 注入 persona 读权)。
47741
47745
  */
47742
47746
  writeSandboxSettings(personaId2, settings) {
47743
47747
  this.atomicWrite(
47744
47748
  this.sandboxSettingsPath(personaId2),
47745
- JSON.stringify(this.injectPersonaEditDeny(personaId2, settings), null, 2)
47749
+ JSON.stringify(this.injectPersonaBaseline(personaId2, settings), null, 2)
47746
47750
  );
47747
47751
  }
47748
- /** 把 persona Edit-deny(绝对 '~/<persona>/**')并进 settings.permissions.deny,剥掉历史相对 'Edit(./**)'。 */
47749
- injectPersonaEditDeny(personaId2, settings) {
47752
+ /**
47753
+ * persona 基线注入:Edit-deny(绝对 '~/<persona>/**')并进 permissions.deny + 剥掉历史相对
47754
+ * 'Edit(./**)';persona 目录 '~/' 路径 union 进 sandbox.filesystem.allowRead(guest 读权
47755
+ * 落盘即真源,compose 不再运行时注入——spec 2026-08-06-persona-workdir-discipline §1-§2)。
47756
+ */
47757
+ injectPersonaBaseline(personaId2, settings) {
47758
+ const dir = this.personaDir(personaId2);
47750
47759
  const s = settings;
47751
- const rule = personaEditDeny(this.personaDir(personaId2));
47752
- const prev = (s.permissions?.deny ?? []).filter((r) => r !== "Edit(./**)" && r !== rule);
47753
- return { ...s, permissions: { ...s.permissions ?? {}, deny: [...prev, rule] } };
47760
+ const rule = personaEditDeny(dir);
47761
+ const rawDeny = s.permissions && typeof s.permissions === "object" ? s.permissions.deny : void 0;
47762
+ const prevDeny = (Array.isArray(rawDeny) ? rawDeny : []).filter((r) => r !== "Edit(./**)" && r !== rule);
47763
+ const readPath = tildePath(dir);
47764
+ const sandbox = s.sandbox && typeof s.sandbox === "object" && !Array.isArray(s.sandbox) ? s.sandbox : {};
47765
+ const rawFs = sandbox.filesystem;
47766
+ const filesystem = rawFs && typeof rawFs === "object" && !Array.isArray(rawFs) ? rawFs : {};
47767
+ const rawRead = filesystem.allowRead;
47768
+ const prevRead = (Array.isArray(rawRead) ? rawRead : []).filter((p2) => p2 !== readPath);
47769
+ return {
47770
+ ...s,
47771
+ permissions: {
47772
+ ...s.permissions && typeof s.permissions === "object" ? s.permissions : {},
47773
+ deny: [...prevDeny, rule]
47774
+ },
47775
+ sandbox: { ...sandbox, filesystem: { ...filesystem, allowRead: [...prevRead, readPath] } }
47776
+ };
47754
47777
  }
47755
47778
  codexSandboxSettingsPath(personaId2) {
47756
47779
  return path12.join(this.personaDir(personaId2), ".clawd", "codex-sandbox.json");
@@ -48615,6 +48638,35 @@ function migrateCodexSandbox(args) {
48615
48638
  }
48616
48639
  if (n > 0) args.logger.info("persona.codex-sandbox.done", { migrated: n });
48617
48640
  }
48641
+ function migrateSandboxAllowRead(args) {
48642
+ let n = 0;
48643
+ for (const personaId2 of args.store.list()) {
48644
+ try {
48645
+ const readPath = tildePath(args.store.personaDirPath(personaId2));
48646
+ if (!fs12.existsSync(args.store.sandboxSettingsPath(personaId2))) {
48647
+ args.store.writeSandboxSettings(personaId2, buildGuestSettingsV1());
48648
+ n++;
48649
+ continue;
48650
+ }
48651
+ const raw = args.store.readSandboxSettings(personaId2);
48652
+ if (raw === null) {
48653
+ args.logger.warn("persona.sandbox.allow-read.corrupt-skip", { personaId: personaId2 });
48654
+ continue;
48655
+ }
48656
+ const allowRead = raw.sandbox?.filesystem?.allowRead;
48657
+ if (Array.isArray(allowRead) && allowRead.includes(readPath)) continue;
48658
+ args.store.writeSandboxSettings(personaId2, raw);
48659
+ n++;
48660
+ args.logger.info("persona.sandbox.allow-read.migrated", { personaId: personaId2 });
48661
+ } catch (err) {
48662
+ args.logger.warn("persona.sandbox.allow-read.failed", {
48663
+ personaId: personaId2,
48664
+ error: err instanceof Error ? err.message : String(err)
48665
+ });
48666
+ }
48667
+ }
48668
+ if (n > 0) args.logger.info("persona.sandbox.allow-read.done", { migrated: n });
48669
+ }
48618
48670
  var PERSONA_BUILDER_ID = "persona-builder";
48619
48671
  var PERSONA_BUILDER_TOMBSTONE_MARKER = "<!-- clawd:persona-builder-retired -->";
48620
48672
  var PERSONA_BUILDER_TOMBSTONE = `${PERSONA_BUILDER_TOMBSTONE_MARKER}
@@ -54833,7 +54885,7 @@ function assertValidFileName(fileName) {
54833
54885
  var HASH_PREFIX_LEN = 16;
54834
54886
  async function writeUploadedAttachment(args) {
54835
54887
  assertValidFileName(args.fileName);
54836
- const attachmentsRoot = import_node_path34.default.join(args.sessionDir, ".attachments");
54888
+ const attachmentsRoot = import_node_path34.default.join(args.baseDir, ".attachments");
54837
54889
  try {
54838
54890
  import_node_fs31.default.mkdirSync(attachmentsRoot, { recursive: true });
54839
54891
  } catch (err) {
@@ -54904,7 +54956,7 @@ async function writeUploadedAttachment(args) {
54904
54956
  }
54905
54957
  }
54906
54958
  const absPath = import_node_path34.default.join(hashDir, finalFileName);
54907
- const relPath = import_node_path34.default.relative(args.sessionDir, absPath);
54959
+ const relPath = absPath;
54908
54960
  args.groupFileStore.upsert(args.scope, args.sessionId, {
54909
54961
  relPath: absPath,
54910
54962
  // 存绝对路径,与现有 agent 入清单的形态一致(attachment.ts:144 双形态兼容)
@@ -55411,9 +55463,9 @@ function createHttpRouter(deps) {
55411
55463
  });
55412
55464
  return true;
55413
55465
  }
55414
- const sessionDir = up.getSessionDir(sessionId);
55466
+ const baseDir = up.getAttachmentBaseDir(sessionId);
55415
55467
  const scope = up.getSessionScope(sessionId);
55416
- if (!sessionDir || !scope) {
55468
+ if (!baseDir || !scope) {
55417
55469
  sendUploadJson(404, { code: "NOT_FOUND", message: `session ${sessionId} not found` });
55418
55470
  return true;
55419
55471
  }
@@ -55435,7 +55487,7 @@ function createHttpRouter(deps) {
55435
55487
  }
55436
55488
  try {
55437
55489
  const result = await writeUploadedAttachment({
55438
- sessionDir,
55490
+ baseDir,
55439
55491
  sessionId,
55440
55492
  scope,
55441
55493
  fileName,
@@ -55613,57 +55665,66 @@ function runAttachmentGc(args) {
55613
55665
  if (import_node_path37.default.isAbsolute(entry.relPath)) liveAbs.add(entry.relPath);
55614
55666
  }
55615
55667
  }
55668
+ const visited = /* @__PURE__ */ new Set();
55616
55669
  for (const { scope, sessionId } of args.sessionScopes) {
55617
- const sessionDir = args.getSessionCwd?.(sessionId) ?? import_node_path37.default.join(
55670
+ const legacyDir = args.getSessionCwd?.(sessionId) ?? import_node_path37.default.join(
55618
55671
  args.dataDir,
55619
55672
  "sessions",
55620
55673
  ...scopeSubPath(scope).map(safeFileName),
55621
55674
  safeFileName(sessionId)
55622
55675
  );
55623
- const attRoot = import_node_path37.default.join(sessionDir, ".attachments");
55624
- let hashDirs;
55625
- try {
55626
- hashDirs = import_node_fs33.default.readdirSync(attRoot);
55627
- } catch (err) {
55628
- if (err.code === "ENOENT") continue;
55629
- args.logger?.warn("attachment gc: readdir failed", { attRoot, err: err.message });
55630
- continue;
55631
- }
55632
- for (const hashDir of hashDirs) {
55633
- const hashDirAbs = import_node_path37.default.join(attRoot, hashDir);
55634
- let files;
55676
+ const userWorkDir = args.getUserWorkDir?.(sessionId) ?? null;
55677
+ const roots = [legacyDir, ...userWorkDir ? [userWorkDir] : []].filter((r) => {
55678
+ if (visited.has(r)) return false;
55679
+ visited.add(r);
55680
+ return true;
55681
+ });
55682
+ for (const rootDir of roots) {
55683
+ const attRoot = import_node_path37.default.join(rootDir, ".attachments");
55684
+ let hashDirs;
55635
55685
  try {
55636
- files = import_node_fs33.default.readdirSync(hashDirAbs);
55637
- } catch {
55686
+ hashDirs = import_node_fs33.default.readdirSync(attRoot);
55687
+ } catch (err) {
55688
+ if (err.code === "ENOENT") continue;
55689
+ args.logger?.warn("attachment gc: readdir failed", { attRoot, err: err.message });
55638
55690
  continue;
55639
55691
  }
55640
- for (const name of files) {
55641
- const file = import_node_path37.default.join(hashDirAbs, name);
55642
- let stat;
55692
+ for (const hashDir of hashDirs) {
55693
+ const hashDirAbs = import_node_path37.default.join(attRoot, hashDir);
55694
+ let files;
55643
55695
  try {
55644
- stat = import_node_fs33.default.statSync(file);
55696
+ files = import_node_fs33.default.readdirSync(hashDirAbs);
55645
55697
  } catch {
55646
55698
  continue;
55647
55699
  }
55648
- if (!stat.isFile()) continue;
55649
- const age = now - stat.mtimeMs;
55650
- if (age < ttlMs) continue;
55651
- if (liveAbs.has(file)) continue;
55700
+ for (const name of files) {
55701
+ const file = import_node_path37.default.join(hashDirAbs, name);
55702
+ let stat;
55703
+ try {
55704
+ stat = import_node_fs33.default.statSync(file);
55705
+ } catch {
55706
+ continue;
55707
+ }
55708
+ if (!stat.isFile()) continue;
55709
+ const age = now - stat.mtimeMs;
55710
+ if (age < ttlMs) continue;
55711
+ if (liveAbs.has(file)) continue;
55712
+ try {
55713
+ import_node_fs33.default.unlinkSync(file);
55714
+ } catch (err) {
55715
+ args.logger?.warn("attachment gc: unlink failed", { file, err: err.message });
55716
+ }
55717
+ }
55652
55718
  try {
55653
- import_node_fs33.default.unlinkSync(file);
55654
- } catch (err) {
55655
- args.logger?.warn("attachment gc: unlink failed", { file, err: err.message });
55719
+ if (import_node_fs33.default.readdirSync(hashDirAbs).length === 0) import_node_fs33.default.rmdirSync(hashDirAbs);
55720
+ } catch {
55656
55721
  }
55657
55722
  }
55658
55723
  try {
55659
- if (import_node_fs33.default.readdirSync(hashDirAbs).length === 0) import_node_fs33.default.rmdirSync(hashDirAbs);
55724
+ if (import_node_fs33.default.readdirSync(attRoot).length === 0) import_node_fs33.default.rmdirSync(attRoot);
55660
55725
  } catch {
55661
55726
  }
55662
55727
  }
55663
- try {
55664
- if (import_node_fs33.default.readdirSync(attRoot).length === 0) import_node_fs33.default.rmdirSync(attRoot);
55665
- } catch {
55666
- }
55667
55728
  }
55668
55729
  }
55669
55730
 
@@ -58789,7 +58850,7 @@ function computeMethodAccess(args) {
58789
58850
  }
58790
58851
 
58791
58852
  // src/version.ts
58792
- var version = "0.2.292".length > 0 ? "0.2.292" : "dev";
58853
+ var version = "0.2.293".length > 0 ? "0.2.293" : "dev";
58793
58854
 
58794
58855
  // src/cli-probe/probe.ts
58795
58856
  var fs55 = __toESM(require("fs"), 1);
@@ -62058,6 +62119,7 @@ async function startDaemon(config) {
62058
62119
  refreshSandboxSettings({ store: personaStore, logger });
62059
62120
  migrateAgentsMirror({ store: personaStore, logger });
62060
62121
  migrateCodexSandbox({ store: personaStore, logger });
62122
+ migrateSandboxAllowRead({ store: personaStore, logger });
62061
62123
  retirePersonaBuilder({ store: personaStore, logger });
62062
62124
  const groupFileStore = new GroupFileStore({ dataDir: config.dataDir, logger });
62063
62125
  const dispatchStore = createDispatchStore({
@@ -62487,6 +62549,12 @@ async function startDaemon(config) {
62487
62549
  });
62488
62550
  const getProjectStore = (userId) => new ProjectStore(deriveUserWorkDir(userId, usersRoot));
62489
62551
  const appBuilderStore = getProjectStore(ownerPrincipalId);
62552
+ const attachmentUserWorkDir = (sid) => {
62553
+ const scope = manager.findOwnedSessionScope(sid);
62554
+ if (scope?.kind !== "persona") return null;
62555
+ const userId = scope.mode === "owner" ? ownerPrincipalId : scope.capId;
62556
+ return deriveUserWorkDir(userId, usersRoot);
62557
+ };
62490
62558
  recoverInterruptedPublishJobs(appBuilderStore, logger).catch((err) => {
62491
62559
  logger.warn("app-builder.publish.recover-startup-failed", {
62492
62560
  error: err instanceof Error ? err.message : String(err)
@@ -62879,15 +62947,17 @@ async function startDaemon(config) {
62879
62947
  larkBotEvent: larkBotEventEndpoint,
62880
62948
  // /files HMAC verify 用 auth.json token(与 attachment.signUrl 同源)。
62881
62949
  getSignSecret: () => authFile.token,
62882
- // chatinput 附件上传(POST /attachments/upload,spec §6.1)。upload pipeline
62883
- // session.cwd 下的 .attachments/ 落盘;签名 secret /files HMAC 同源;
62884
- // tunnel 未启时返 503 TUNNEL_NOT_READY 让前端显式提示。
62950
+ // chatinput 附件上传(POST /attachments/upload,spec §6.1 + 2026-08-06 §5)。落盘根按
62951
+ // scope 切换:persona 会话 连接者 userWorkDir(与 connection-prompt 宣称的工作目录一致,
62952
+ // guest persona 目录的读权不再连带看到 owner 上传的附件);非 persona 会话 → session.cwd
62953
+ // (项目目录本就是用户地盘)。签名 secret 与 /files HMAC 同源;tunnel 未启时返 503。
62885
62954
  attachmentUpload: {
62886
62955
  groupFileStore,
62887
62956
  getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
62888
- getSessionDir: (sid) => {
62957
+ getAttachmentBaseDir: (sid) => {
62889
62958
  const f = manager.findOwnedSession(sid);
62890
- return f ? f.cwd : null;
62959
+ if (!f) return null;
62960
+ return attachmentUserWorkDir(sid) ?? f.cwd;
62891
62961
  },
62892
62962
  getHttpBaseUrl: () => currentTunnelUrl ? getHttpBaseUrl() : null
62893
62963
  },
@@ -63131,8 +63201,10 @@ ${bar}
63131
63201
  scope,
63132
63202
  sessionId
63133
63203
  })),
63134
- // 注入 cwd GC 直接定位 .attachments 目录(与 upload 落盘点一致)
63204
+ // 双根扫描(spec 2026-08-06 §5):legacy cwd 根(迁移前落盘点,30 天自然老化)+
63205
+ // userWorkDir 根(迁移后 upload 落盘点,与 getAttachmentBaseDir 同源派生)
63135
63206
  getSessionCwd: (sid) => manager.findOwnedSession(sid)?.cwd ?? null,
63207
+ getUserWorkDir: attachmentUserWorkDir,
63136
63208
  logger
63137
63209
  });
63138
63210
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawos-dev/clawd",
3
- "version": "0.2.292",
3
+ "version": "0.2.293",
4
4
  "description": "Standalone clawd daemon — Claude Code (and future Codex) session server over WebSocket",
5
5
  "type": "module",
6
6
  "license": "MIT",