@clawos-dev/clawd 0.2.70 → 0.2.71-beta.124.b09b0a0

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 +993 -280
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -114,6 +114,13 @@ var init_methods = __esm({
114
114
  "attachment.groupList",
115
115
  // v2:跨 session 聚合(本期 UI 不调,保留槽位用于 HTTP ACL 内部判定 / 未来 "All files" tab)
116
116
  "attachment.groupListPersona",
117
+ // ---- capability:* (capability platform 鉴权底座) ----
118
+ // owner 颁发 / 列出 / 撤销给 guest 的 capability。三者均需 admin 权限(METHOD_GRANT_MAP
119
+ // 在 daemon 端固定为 `{ resource: '*', action: 'admin' }`,owner 自动满足)。
120
+ // 颁发后 daemon 推 'capability:tokenIssued' 帧;撤销推 'capability:revoked' 帧。
121
+ "capability:issue",
122
+ "capability:list",
123
+ "capability:revoke",
117
124
  "info",
118
125
  "ping"
119
126
  ];
@@ -605,8 +612,8 @@ var init_parseUtil = __esm({
605
612
  init_errors2();
606
613
  init_en();
607
614
  makeIssue = (params) => {
608
- const { data, path: path32, errorMaps, issueData } = params;
609
- const fullPath = [...path32, ...issueData.path || []];
615
+ const { data, path: path35, errorMaps, issueData } = params;
616
+ const fullPath = [...path35, ...issueData.path || []];
610
617
  const fullIssue = {
611
618
  ...issueData,
612
619
  path: fullPath
@@ -917,11 +924,11 @@ var init_types = __esm({
917
924
  init_parseUtil();
918
925
  init_util();
919
926
  ParseInputLazyPath = class {
920
- constructor(parent, value, path32, key) {
927
+ constructor(parent, value, path35, key) {
921
928
  this._cachedPath = [];
922
929
  this.parent = parent;
923
930
  this.data = value;
924
- this._path = path32;
931
+ this._path = path35;
925
932
  this._key = key;
926
933
  }
927
934
  get path() {
@@ -4331,11 +4338,9 @@ var init_attachment_schemas = __esm({
4331
4338
  stale: external_exports.boolean().optional()
4332
4339
  });
4333
4340
  AttachmentSignUrlArgs = external_exports.object({
4334
- /** 群文件所属的 session */
4335
- sessionId: external_exports.string().min(1),
4336
- /** 相对 session.cwd 的路径;允许传绝对路径,daemon 端会归一化(必须在 cwd 内) */
4337
- relPath: external_exports.string().min(1),
4338
- /** TTL 秒数;缺省 24h;null 走永久 URL(不带 exp 字段) */
4341
+ /** 要分享的绝对路径;签名只关心 absPath,不区分 persona/session */
4342
+ absPath: external_exports.string().min(1),
4343
+ /** TTL 秒数;缺省 24h;'never' null 不带 exp 字段(永久有效) */
4339
4344
  ttlSeconds: external_exports.number().int().positive().nullable().optional()
4340
4345
  });
4341
4346
  AttachmentSignUrlResponseSchema = external_exports.object({
@@ -4470,7 +4475,7 @@ var init_persona_schemas = __esm({
4470
4475
  });
4471
4476
 
4472
4477
  // ../protocol/src/schemas.ts
4473
- var SessionStatusSchema, UsageSchema, ContextUsageSchema, sessionMetaShape, SessionMetaSchema, ModelInfoSchema, ModeInfoSchema, ConfigFieldSchemaSchema, CapabilitiesGetArgs, CapabilitiesResponseSchema, AllowRuleSchema, SessionFileSchema, ParsedEventBase, HistoryUserMetaSchema, SubagentToolStatsSchema, StructuredPatchHunkSchema, ToolResultExtraSchema, MemoryEntrySchema, AskQuestionOptionSchema, AskQuestionItemSchema, ParsedEventSchema, SessionCreateArgs, SessionIdArgs, SessionUpdateArgs, SessionSendArgs, SessionRewindArgs, SessionRewindResponseSchema, SessionRewindDiffArgs, RewindDiffHunkSchema, RewindDiffFileSchema, SessionRewindDiffResponseSchema, SessionRewindableMessageIdsArgs, SessionRewindableMessageIdsResponseSchema, SessionResumeArgs, SessionForkArgs, SessionForkResponseSchema, SessionObserveArgs, SessionEventsArgs, PermissionRespondArgs, HistoryListArgs, HistoryReadArgs, HistorySubagentsArgs, HistorySubagentReadArgs, WorkspaceListArgs, WorkspaceReadArgs, SkillsListArgs, SkillEntrySchema, AgentEntrySchema, AgentsListArgs, AgentsListResponseSchema, SessionSubscribeArgs, SessionPinArgs, SessionReorderPinsArgs, GitRootArgs, GitRootResponseSchema, GitBranchArgs, GitBranchResponseSchema, GitBranchesArgs, GitBranchesResponseSchema, HistoryRecentDirsArgs, RecentDirEntrySchema, HistoryRecentDirsResponseSchema, SessionQuestionFrameSchema, SessionQuestionClearedFrameSchema, AnswerQuestionArgs, AnswerQuestionResponseSchema, CancelQuestionArgs, CancelQuestionResponseSchema, AuthRequestFrameSchema, AuthOkFrameSchema, TunnelExitedEventSchema, TunnelUnavailableEventSchema, InfoRunningSessionSchema, InfoResponseSchema;
4478
+ var SessionStatusSchema, UsageSchema, ContextUsageSchema, sessionMetaShape, SessionMetaSchema, ModelInfoSchema, ModeInfoSchema, ConfigFieldSchemaSchema, CapabilitiesGetArgs, CapabilitiesResponseSchema, AllowRuleSchema, SessionFileSchema, ParsedEventBase, HistoryUserMetaSchema, SubagentToolStatsSchema, StructuredPatchHunkSchema, ToolResultExtraSchema, MemoryEntrySchema, AskQuestionOptionSchema, AskQuestionItemSchema, ParsedEventSchema, SessionCreateArgs, SessionIdArgs, SessionUpdateArgs, SessionSendArgs, SessionRewindArgs, SessionRewindResponseSchema, SessionRewindDiffArgs, RewindDiffHunkSchema, RewindDiffFileSchema, SessionRewindDiffResponseSchema, SessionRewindableMessageIdsArgs, SessionRewindableMessageIdsResponseSchema, SessionResumeArgs, SessionForkArgs, SessionForkResponseSchema, SessionObserveArgs, SessionEventsArgs, PermissionRespondArgs, HistoryListArgs, HistoryReadArgs, HistorySubagentsArgs, HistorySubagentReadArgs, WorkspaceListArgs, WorkspaceReadArgs, SkillsListArgs, SkillEntrySchema, AgentEntrySchema, AgentsListArgs, AgentsListResponseSchema, SessionSubscribeArgs, SessionPinArgs, SessionReorderPinsArgs, GitRootArgs, GitRootResponseSchema, GitBranchArgs, GitBranchResponseSchema, GitBranchesArgs, GitBranchesResponseSchema, HistoryRecentDirsArgs, RecentDirEntrySchema, HistoryRecentDirsResponseSchema, SessionQuestionFrameSchema, SessionQuestionClearedFrameSchema, AnswerQuestionArgs, AnswerQuestionResponseSchema, CancelQuestionArgs, CancelQuestionResponseSchema, AuthRequestFrameSchema, AuthOkFrameSchema, TunnelExitedEventSchema, InfoRunningSessionSchema, InfoResponseSchema;
4474
4479
  var init_schemas = __esm({
4475
4480
  "../protocol/src/schemas.ts"() {
4476
4481
  "use strict";
@@ -4994,11 +4999,6 @@ var init_schemas = __esm({
4994
4999
  subdomain: external_exports.string().nullable(),
4995
5000
  url: external_exports.string().nullable()
4996
5001
  });
4997
- TunnelUnavailableEventSchema = external_exports.object({
4998
- type: external_exports.literal("tunnel:unavailable"),
4999
- reason: external_exports.string().min(1),
5000
- failedAt: external_exports.string().min(1)
5001
- });
5002
5002
  InfoRunningSessionSchema = external_exports.object({
5003
5003
  sessionId: external_exports.string().min(1),
5004
5004
  status: SessionStatusSchema,
@@ -5049,6 +5049,69 @@ var init_persona_mode = __esm({
5049
5049
  }
5050
5050
  });
5051
5051
 
5052
+ // ../protocol/src/principal.ts
5053
+ var PrincipalKindSchema, PrincipalSchema, OWNER_PRINCIPAL;
5054
+ var init_principal = __esm({
5055
+ "../protocol/src/principal.ts"() {
5056
+ "use strict";
5057
+ init_zod();
5058
+ PrincipalKindSchema = external_exports.enum(["owner", "guest"]);
5059
+ PrincipalSchema = external_exports.object({
5060
+ id: external_exports.string().min(1),
5061
+ kind: PrincipalKindSchema,
5062
+ displayName: external_exports.string()
5063
+ }).strict();
5064
+ OWNER_PRINCIPAL = {
5065
+ id: "owner",
5066
+ kind: "owner",
5067
+ displayName: "owner"
5068
+ };
5069
+ }
5070
+ });
5071
+
5072
+ // ../protocol/src/capability.ts
5073
+ function stripSecretHash(cap) {
5074
+ const { secretHash: _hash, ...wire } = cap;
5075
+ return wire;
5076
+ }
5077
+ var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema;
5078
+ var init_capability = __esm({
5079
+ "../protocol/src/capability.ts"() {
5080
+ "use strict";
5081
+ init_zod();
5082
+ ResourceSchema = external_exports.discriminatedUnion("type", [
5083
+ external_exports.object({ type: external_exports.literal("persona"), id: external_exports.string().min(1) }).strict(),
5084
+ external_exports.object({ type: external_exports.literal("chat"), id: external_exports.string().min(1) }).strict(),
5085
+ external_exports.object({ type: external_exports.literal("*") }).strict()
5086
+ ]);
5087
+ ActionSchema = external_exports.enum(["read", "send", "admin"]);
5088
+ GrantSchema = external_exports.object({
5089
+ resource: ResourceSchema,
5090
+ actions: external_exports.array(ActionSchema).min(1)
5091
+ }).strict();
5092
+ CapabilitySchema = external_exports.object({
5093
+ id: external_exports.string().min(1),
5094
+ // sha256(token) hex 64
5095
+ secretHash: external_exports.string().regex(/^[a-f0-9]{64}$/),
5096
+ displayName: external_exports.string(),
5097
+ // 空数组合法 = 纯访客(只能 DM)
5098
+ grants: external_exports.array(GrantSchema),
5099
+ issuedAt: external_exports.number().int().nonnegative(),
5100
+ expiresAt: external_exports.number().int().positive().optional(),
5101
+ maxUses: external_exports.number().int().positive().optional(),
5102
+ usedCount: external_exports.number().int().nonnegative(),
5103
+ revokedAt: external_exports.number().int().positive().optional()
5104
+ }).strict();
5105
+ CapabilityWireSchema = CapabilitySchema.omit({ secretHash: true });
5106
+ CapabilityErrorCodeSchema = external_exports.enum([
5107
+ "TOKEN_INVALID",
5108
+ "TOKEN_REVOKED",
5109
+ "TOKEN_EXPIRED",
5110
+ "TOKEN_EXHAUSTED"
5111
+ ]);
5112
+ }
5113
+ });
5114
+
5052
5115
  // ../protocol/src/runtime.ts
5053
5116
  var init_runtime = __esm({
5054
5117
  "../protocol/src/runtime.ts"() {
@@ -5061,6 +5124,8 @@ var init_runtime = __esm({
5061
5124
  init_persona_schemas();
5062
5125
  init_persona_mode();
5063
5126
  init_attachment_schemas();
5127
+ init_principal();
5128
+ init_capability();
5064
5129
  }
5065
5130
  });
5066
5131
 
@@ -5335,8 +5400,8 @@ var require_req = __commonJS({
5335
5400
  if (req.originalUrl) {
5336
5401
  _req.url = req.originalUrl;
5337
5402
  } else {
5338
- const path32 = req.path;
5339
- _req.url = typeof path32 === "string" ? path32 : req.url ? req.url.path || req.url : void 0;
5403
+ const path35 = req.path;
5404
+ _req.url = typeof path35 === "string" ? path35 : req.url ? req.url.path || req.url : void 0;
5340
5405
  }
5341
5406
  if (req.query) {
5342
5407
  _req.query = req.query;
@@ -5501,14 +5566,14 @@ var require_redact = __commonJS({
5501
5566
  }
5502
5567
  return obj;
5503
5568
  }
5504
- function parsePath(path32) {
5569
+ function parsePath(path35) {
5505
5570
  const parts = [];
5506
5571
  let current = "";
5507
5572
  let inBrackets = false;
5508
5573
  let inQuotes = false;
5509
5574
  let quoteChar = "";
5510
- for (let i = 0; i < path32.length; i++) {
5511
- const char = path32[i];
5575
+ for (let i = 0; i < path35.length; i++) {
5576
+ const char = path35[i];
5512
5577
  if (!inBrackets && char === ".") {
5513
5578
  if (current) {
5514
5579
  parts.push(current);
@@ -5639,10 +5704,10 @@ var require_redact = __commonJS({
5639
5704
  return current;
5640
5705
  }
5641
5706
  function redactPaths(obj, paths, censor, remove = false) {
5642
- for (const path32 of paths) {
5643
- const parts = parsePath(path32);
5707
+ for (const path35 of paths) {
5708
+ const parts = parsePath(path35);
5644
5709
  if (parts.includes("*")) {
5645
- redactWildcardPath(obj, parts, censor, path32, remove);
5710
+ redactWildcardPath(obj, parts, censor, path35, remove);
5646
5711
  } else {
5647
5712
  if (remove) {
5648
5713
  removeKey(obj, parts);
@@ -5727,8 +5792,8 @@ var require_redact = __commonJS({
5727
5792
  }
5728
5793
  } else {
5729
5794
  if (afterWildcard.includes("*")) {
5730
- const wrappedCensor = typeof censor === "function" ? (value, path32) => {
5731
- const fullPath = [...pathArray.slice(0, pathLength), ...path32];
5795
+ const wrappedCensor = typeof censor === "function" ? (value, path35) => {
5796
+ const fullPath = [...pathArray.slice(0, pathLength), ...path35];
5732
5797
  return censor(value, fullPath);
5733
5798
  } : censor;
5734
5799
  redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
@@ -5763,8 +5828,8 @@ var require_redact = __commonJS({
5763
5828
  return null;
5764
5829
  }
5765
5830
  const pathStructure = /* @__PURE__ */ new Map();
5766
- for (const path32 of pathsToClone) {
5767
- const parts = parsePath(path32);
5831
+ for (const path35 of pathsToClone) {
5832
+ const parts = parsePath(path35);
5768
5833
  let current = pathStructure;
5769
5834
  for (let i = 0; i < parts.length; i++) {
5770
5835
  const part = parts[i];
@@ -5816,24 +5881,24 @@ var require_redact = __commonJS({
5816
5881
  }
5817
5882
  return cloneSelectively(obj, pathStructure);
5818
5883
  }
5819
- function validatePath(path32) {
5820
- if (typeof path32 !== "string") {
5884
+ function validatePath(path35) {
5885
+ if (typeof path35 !== "string") {
5821
5886
  throw new Error("Paths must be (non-empty) strings");
5822
5887
  }
5823
- if (path32 === "") {
5888
+ if (path35 === "") {
5824
5889
  throw new Error("Invalid redaction path ()");
5825
5890
  }
5826
- if (path32.includes("..")) {
5827
- throw new Error(`Invalid redaction path (${path32})`);
5891
+ if (path35.includes("..")) {
5892
+ throw new Error(`Invalid redaction path (${path35})`);
5828
5893
  }
5829
- if (path32.includes(",")) {
5830
- throw new Error(`Invalid redaction path (${path32})`);
5894
+ if (path35.includes(",")) {
5895
+ throw new Error(`Invalid redaction path (${path35})`);
5831
5896
  }
5832
5897
  let bracketCount = 0;
5833
5898
  let inQuotes = false;
5834
5899
  let quoteChar = "";
5835
- for (let i = 0; i < path32.length; i++) {
5836
- const char = path32[i];
5900
+ for (let i = 0; i < path35.length; i++) {
5901
+ const char = path35[i];
5837
5902
  if ((char === '"' || char === "'") && bracketCount > 0) {
5838
5903
  if (!inQuotes) {
5839
5904
  inQuotes = true;
@@ -5847,20 +5912,20 @@ var require_redact = __commonJS({
5847
5912
  } else if (char === "]" && !inQuotes) {
5848
5913
  bracketCount--;
5849
5914
  if (bracketCount < 0) {
5850
- throw new Error(`Invalid redaction path (${path32})`);
5915
+ throw new Error(`Invalid redaction path (${path35})`);
5851
5916
  }
5852
5917
  }
5853
5918
  }
5854
5919
  if (bracketCount !== 0) {
5855
- throw new Error(`Invalid redaction path (${path32})`);
5920
+ throw new Error(`Invalid redaction path (${path35})`);
5856
5921
  }
5857
5922
  }
5858
5923
  function validatePaths(paths) {
5859
5924
  if (!Array.isArray(paths)) {
5860
5925
  throw new TypeError("paths must be an array");
5861
5926
  }
5862
- for (const path32 of paths) {
5863
- validatePath(path32);
5927
+ for (const path35 of paths) {
5928
+ validatePath(path35);
5864
5929
  }
5865
5930
  }
5866
5931
  function slowRedact(options = {}) {
@@ -6028,8 +6093,8 @@ var require_redaction = __commonJS({
6028
6093
  if (shape[k2] === null) {
6029
6094
  o[k2] = (value) => topCensor(value, [k2]);
6030
6095
  } else {
6031
- const wrappedCensor = typeof censor === "function" ? (value, path32) => {
6032
- return censor(value, [k2, ...path32]);
6096
+ const wrappedCensor = typeof censor === "function" ? (value, path35) => {
6097
+ return censor(value, [k2, ...path35]);
6033
6098
  } : censor;
6034
6099
  o[k2] = Redact({
6035
6100
  paths: shape[k2],
@@ -6247,10 +6312,10 @@ var require_atomic_sleep = __commonJS({
6247
6312
  var require_sonic_boom = __commonJS({
6248
6313
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
6249
6314
  "use strict";
6250
- var fs28 = require("fs");
6315
+ var fs31 = require("fs");
6251
6316
  var EventEmitter2 = require("events");
6252
6317
  var inherits = require("util").inherits;
6253
- var path32 = require("path");
6318
+ var path35 = require("path");
6254
6319
  var sleep = require_atomic_sleep();
6255
6320
  var assert = require("assert");
6256
6321
  var BUSY_WRITE_TIMEOUT = 100;
@@ -6304,20 +6369,20 @@ var require_sonic_boom = __commonJS({
6304
6369
  const mode = sonic.mode;
6305
6370
  if (sonic.sync) {
6306
6371
  try {
6307
- if (sonic.mkdir) fs28.mkdirSync(path32.dirname(file), { recursive: true });
6308
- const fd = fs28.openSync(file, flags, mode);
6372
+ if (sonic.mkdir) fs31.mkdirSync(path35.dirname(file), { recursive: true });
6373
+ const fd = fs31.openSync(file, flags, mode);
6309
6374
  fileOpened(null, fd);
6310
6375
  } catch (err) {
6311
6376
  fileOpened(err);
6312
6377
  throw err;
6313
6378
  }
6314
6379
  } else if (sonic.mkdir) {
6315
- fs28.mkdir(path32.dirname(file), { recursive: true }, (err) => {
6380
+ fs31.mkdir(path35.dirname(file), { recursive: true }, (err) => {
6316
6381
  if (err) return fileOpened(err);
6317
- fs28.open(file, flags, mode, fileOpened);
6382
+ fs31.open(file, flags, mode, fileOpened);
6318
6383
  });
6319
6384
  } else {
6320
- fs28.open(file, flags, mode, fileOpened);
6385
+ fs31.open(file, flags, mode, fileOpened);
6321
6386
  }
6322
6387
  }
6323
6388
  function SonicBoom(opts) {
@@ -6358,8 +6423,8 @@ var require_sonic_boom = __commonJS({
6358
6423
  this.flush = flushBuffer;
6359
6424
  this.flushSync = flushBufferSync;
6360
6425
  this._actualWrite = actualWriteBuffer;
6361
- fsWriteSync = () => fs28.writeSync(this.fd, this._writingBuf);
6362
- fsWrite = () => fs28.write(this.fd, this._writingBuf, this.release);
6426
+ fsWriteSync = () => fs31.writeSync(this.fd, this._writingBuf);
6427
+ fsWrite = () => fs31.write(this.fd, this._writingBuf, this.release);
6363
6428
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
6364
6429
  this._writingBuf = "";
6365
6430
  this.write = write;
@@ -6368,15 +6433,15 @@ var require_sonic_boom = __commonJS({
6368
6433
  this._actualWrite = actualWrite;
6369
6434
  fsWriteSync = () => {
6370
6435
  if (Buffer.isBuffer(this._writingBuf)) {
6371
- return fs28.writeSync(this.fd, this._writingBuf);
6436
+ return fs31.writeSync(this.fd, this._writingBuf);
6372
6437
  }
6373
- return fs28.writeSync(this.fd, this._writingBuf, "utf8");
6438
+ return fs31.writeSync(this.fd, this._writingBuf, "utf8");
6374
6439
  };
6375
6440
  fsWrite = () => {
6376
6441
  if (Buffer.isBuffer(this._writingBuf)) {
6377
- return fs28.write(this.fd, this._writingBuf, this.release);
6442
+ return fs31.write(this.fd, this._writingBuf, this.release);
6378
6443
  }
6379
- return fs28.write(this.fd, this._writingBuf, "utf8", this.release);
6444
+ return fs31.write(this.fd, this._writingBuf, "utf8", this.release);
6380
6445
  };
6381
6446
  } else {
6382
6447
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -6433,7 +6498,7 @@ var require_sonic_boom = __commonJS({
6433
6498
  }
6434
6499
  }
6435
6500
  if (this._fsync) {
6436
- fs28.fsyncSync(this.fd);
6501
+ fs31.fsyncSync(this.fd);
6437
6502
  }
6438
6503
  const len = this._len;
6439
6504
  if (this._reopening) {
@@ -6547,7 +6612,7 @@ var require_sonic_boom = __commonJS({
6547
6612
  const onDrain = () => {
6548
6613
  if (!this._fsync) {
6549
6614
  try {
6550
- fs28.fsync(this.fd, (err) => {
6615
+ fs31.fsync(this.fd, (err) => {
6551
6616
  this._flushPending = false;
6552
6617
  cb(err);
6553
6618
  });
@@ -6649,7 +6714,7 @@ var require_sonic_boom = __commonJS({
6649
6714
  const fd = this.fd;
6650
6715
  this.once("ready", () => {
6651
6716
  if (fd !== this.fd) {
6652
- fs28.close(fd, (err) => {
6717
+ fs31.close(fd, (err) => {
6653
6718
  if (err) {
6654
6719
  return this.emit("error", err);
6655
6720
  }
@@ -6698,7 +6763,7 @@ var require_sonic_boom = __commonJS({
6698
6763
  buf = this._bufs[0];
6699
6764
  }
6700
6765
  try {
6701
- const n = Buffer.isBuffer(buf) ? fs28.writeSync(this.fd, buf) : fs28.writeSync(this.fd, buf, "utf8");
6766
+ const n = Buffer.isBuffer(buf) ? fs31.writeSync(this.fd, buf) : fs31.writeSync(this.fd, buf, "utf8");
6702
6767
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
6703
6768
  buf = releasedBufObj.writingBuf;
6704
6769
  this._len = releasedBufObj.len;
@@ -6714,7 +6779,7 @@ var require_sonic_boom = __commonJS({
6714
6779
  }
6715
6780
  }
6716
6781
  try {
6717
- fs28.fsyncSync(this.fd);
6782
+ fs31.fsyncSync(this.fd);
6718
6783
  } catch {
6719
6784
  }
6720
6785
  }
@@ -6735,7 +6800,7 @@ var require_sonic_boom = __commonJS({
6735
6800
  buf = mergeBuf(this._bufs[0], this._lens[0]);
6736
6801
  }
6737
6802
  try {
6738
- const n = fs28.writeSync(this.fd, buf);
6803
+ const n = fs31.writeSync(this.fd, buf);
6739
6804
  buf = buf.subarray(n);
6740
6805
  this._len = Math.max(this._len - n, 0);
6741
6806
  if (buf.length <= 0) {
@@ -6763,13 +6828,13 @@ var require_sonic_boom = __commonJS({
6763
6828
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
6764
6829
  if (this.sync) {
6765
6830
  try {
6766
- const written = Buffer.isBuffer(this._writingBuf) ? fs28.writeSync(this.fd, this._writingBuf) : fs28.writeSync(this.fd, this._writingBuf, "utf8");
6831
+ const written = Buffer.isBuffer(this._writingBuf) ? fs31.writeSync(this.fd, this._writingBuf) : fs31.writeSync(this.fd, this._writingBuf, "utf8");
6767
6832
  release(null, written);
6768
6833
  } catch (err) {
6769
6834
  release(err);
6770
6835
  }
6771
6836
  } else {
6772
- fs28.write(this.fd, this._writingBuf, release);
6837
+ fs31.write(this.fd, this._writingBuf, release);
6773
6838
  }
6774
6839
  }
6775
6840
  function actualWriteBuffer() {
@@ -6778,7 +6843,7 @@ var require_sonic_boom = __commonJS({
6778
6843
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
6779
6844
  if (this.sync) {
6780
6845
  try {
6781
- const written = fs28.writeSync(this.fd, this._writingBuf);
6846
+ const written = fs31.writeSync(this.fd, this._writingBuf);
6782
6847
  release(null, written);
6783
6848
  } catch (err) {
6784
6849
  release(err);
@@ -6787,7 +6852,7 @@ var require_sonic_boom = __commonJS({
6787
6852
  if (kCopyBuffer) {
6788
6853
  this._writingBuf = Buffer.from(this._writingBuf);
6789
6854
  }
6790
- fs28.write(this.fd, this._writingBuf, release);
6855
+ fs31.write(this.fd, this._writingBuf, release);
6791
6856
  }
6792
6857
  }
6793
6858
  function actualClose(sonic) {
@@ -6803,12 +6868,12 @@ var require_sonic_boom = __commonJS({
6803
6868
  sonic._lens = [];
6804
6869
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
6805
6870
  try {
6806
- fs28.fsync(sonic.fd, closeWrapped);
6871
+ fs31.fsync(sonic.fd, closeWrapped);
6807
6872
  } catch {
6808
6873
  }
6809
6874
  function closeWrapped() {
6810
6875
  if (sonic.fd !== 1 && sonic.fd !== 2) {
6811
- fs28.close(sonic.fd, done);
6876
+ fs31.close(sonic.fd, done);
6812
6877
  } else {
6813
6878
  done();
6814
6879
  }
@@ -7065,7 +7130,7 @@ var require_thread_stream = __commonJS({
7065
7130
  var { version: version2 } = require_package();
7066
7131
  var { EventEmitter: EventEmitter2 } = require("events");
7067
7132
  var { Worker } = require("worker_threads");
7068
- var { join: join4 } = require("path");
7133
+ var { join: join8 } = require("path");
7069
7134
  var { pathToFileURL } = require("url");
7070
7135
  var { wait } = require_wait();
7071
7136
  var {
@@ -7101,7 +7166,7 @@ var require_thread_stream = __commonJS({
7101
7166
  function createWorker(stream, opts) {
7102
7167
  const { filename, workerData } = opts;
7103
7168
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
7104
- const toExecute = bundlerOverrides["thread-stream-worker"] || join4(__dirname, "lib", "worker.js");
7169
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join8(__dirname, "lib", "worker.js");
7105
7170
  const worker = new Worker(toExecute, {
7106
7171
  ...opts.workerOpts,
7107
7172
  trackUnmanagedFds: false,
@@ -7487,7 +7552,7 @@ var require_transport = __commonJS({
7487
7552
  "use strict";
7488
7553
  var { createRequire } = require("module");
7489
7554
  var getCallers = require_caller();
7490
- var { join: join4, isAbsolute, sep } = require("path");
7555
+ var { join: join8, isAbsolute, sep: sep2 } = require("path");
7491
7556
  var sleep = require_atomic_sleep();
7492
7557
  var onExit = require_on_exit_leak_free();
7493
7558
  var ThreadStream = require_thread_stream();
@@ -7550,7 +7615,7 @@ var require_transport = __commonJS({
7550
7615
  throw new Error("only one of target or targets can be specified");
7551
7616
  }
7552
7617
  if (targets) {
7553
- target = bundlerOverrides["pino-worker"] || join4(__dirname, "worker.js");
7618
+ target = bundlerOverrides["pino-worker"] || join8(__dirname, "worker.js");
7554
7619
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
7555
7620
  return {
7556
7621
  ...dest,
@@ -7568,7 +7633,7 @@ var require_transport = __commonJS({
7568
7633
  });
7569
7634
  });
7570
7635
  } else if (pipeline2) {
7571
- target = bundlerOverrides["pino-worker"] || join4(__dirname, "worker.js");
7636
+ target = bundlerOverrides["pino-worker"] || join8(__dirname, "worker.js");
7572
7637
  options.pipelines = [pipeline2.map((dest) => {
7573
7638
  return {
7574
7639
  ...dest,
@@ -7590,12 +7655,12 @@ var require_transport = __commonJS({
7590
7655
  return origin;
7591
7656
  }
7592
7657
  if (origin === "pino/file") {
7593
- return join4(__dirname, "..", "file.js");
7658
+ return join8(__dirname, "..", "file.js");
7594
7659
  }
7595
7660
  let fixTarget2;
7596
7661
  for (const filePath of callers) {
7597
7662
  try {
7598
- const context = filePath === "node:repl" ? process.cwd() + sep : filePath;
7663
+ const context = filePath === "node:repl" ? process.cwd() + sep2 : filePath;
7599
7664
  fixTarget2 = createRequire(context).resolve(origin);
7600
7665
  break;
7601
7666
  } catch (err) {
@@ -8580,7 +8645,7 @@ var require_safe_stable_stringify = __commonJS({
8580
8645
  return circularValue;
8581
8646
  }
8582
8647
  let res = "";
8583
- let join4 = ",";
8648
+ let join8 = ",";
8584
8649
  const originalIndentation = indentation;
8585
8650
  if (Array.isArray(value)) {
8586
8651
  if (value.length === 0) {
@@ -8594,7 +8659,7 @@ var require_safe_stable_stringify = __commonJS({
8594
8659
  indentation += spacer;
8595
8660
  res += `
8596
8661
  ${indentation}`;
8597
- join4 = `,
8662
+ join8 = `,
8598
8663
  ${indentation}`;
8599
8664
  }
8600
8665
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -8602,13 +8667,13 @@ ${indentation}`;
8602
8667
  for (; i < maximumValuesToStringify - 1; i++) {
8603
8668
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
8604
8669
  res += tmp2 !== void 0 ? tmp2 : "null";
8605
- res += join4;
8670
+ res += join8;
8606
8671
  }
8607
8672
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
8608
8673
  res += tmp !== void 0 ? tmp : "null";
8609
8674
  if (value.length - 1 > maximumBreadth) {
8610
8675
  const removedKeys = value.length - maximumBreadth - 1;
8611
- res += `${join4}"... ${getItemCount(removedKeys)} not stringified"`;
8676
+ res += `${join8}"... ${getItemCount(removedKeys)} not stringified"`;
8612
8677
  }
8613
8678
  if (spacer !== "") {
8614
8679
  res += `
@@ -8629,7 +8694,7 @@ ${originalIndentation}`;
8629
8694
  let separator = "";
8630
8695
  if (spacer !== "") {
8631
8696
  indentation += spacer;
8632
- join4 = `,
8697
+ join8 = `,
8633
8698
  ${indentation}`;
8634
8699
  whitespace = " ";
8635
8700
  }
@@ -8643,13 +8708,13 @@ ${indentation}`;
8643
8708
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
8644
8709
  if (tmp !== void 0) {
8645
8710
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
8646
- separator = join4;
8711
+ separator = join8;
8647
8712
  }
8648
8713
  }
8649
8714
  if (keyLength > maximumBreadth) {
8650
8715
  const removedKeys = keyLength - maximumBreadth;
8651
8716
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
8652
- separator = join4;
8717
+ separator = join8;
8653
8718
  }
8654
8719
  if (spacer !== "" && separator.length > 1) {
8655
8720
  res = `
@@ -8690,7 +8755,7 @@ ${originalIndentation}`;
8690
8755
  }
8691
8756
  const originalIndentation = indentation;
8692
8757
  let res = "";
8693
- let join4 = ",";
8758
+ let join8 = ",";
8694
8759
  if (Array.isArray(value)) {
8695
8760
  if (value.length === 0) {
8696
8761
  return "[]";
@@ -8703,7 +8768,7 @@ ${originalIndentation}`;
8703
8768
  indentation += spacer;
8704
8769
  res += `
8705
8770
  ${indentation}`;
8706
- join4 = `,
8771
+ join8 = `,
8707
8772
  ${indentation}`;
8708
8773
  }
8709
8774
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -8711,13 +8776,13 @@ ${indentation}`;
8711
8776
  for (; i < maximumValuesToStringify - 1; i++) {
8712
8777
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
8713
8778
  res += tmp2 !== void 0 ? tmp2 : "null";
8714
- res += join4;
8779
+ res += join8;
8715
8780
  }
8716
8781
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
8717
8782
  res += tmp !== void 0 ? tmp : "null";
8718
8783
  if (value.length - 1 > maximumBreadth) {
8719
8784
  const removedKeys = value.length - maximumBreadth - 1;
8720
- res += `${join4}"... ${getItemCount(removedKeys)} not stringified"`;
8785
+ res += `${join8}"... ${getItemCount(removedKeys)} not stringified"`;
8721
8786
  }
8722
8787
  if (spacer !== "") {
8723
8788
  res += `
@@ -8730,7 +8795,7 @@ ${originalIndentation}`;
8730
8795
  let whitespace = "";
8731
8796
  if (spacer !== "") {
8732
8797
  indentation += spacer;
8733
- join4 = `,
8798
+ join8 = `,
8734
8799
  ${indentation}`;
8735
8800
  whitespace = " ";
8736
8801
  }
@@ -8739,7 +8804,7 @@ ${indentation}`;
8739
8804
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
8740
8805
  if (tmp !== void 0) {
8741
8806
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
8742
- separator = join4;
8807
+ separator = join8;
8743
8808
  }
8744
8809
  }
8745
8810
  if (spacer !== "" && separator.length > 1) {
@@ -8797,20 +8862,20 @@ ${originalIndentation}`;
8797
8862
  indentation += spacer;
8798
8863
  let res2 = `
8799
8864
  ${indentation}`;
8800
- const join5 = `,
8865
+ const join9 = `,
8801
8866
  ${indentation}`;
8802
8867
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
8803
8868
  let i = 0;
8804
8869
  for (; i < maximumValuesToStringify - 1; i++) {
8805
8870
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
8806
8871
  res2 += tmp2 !== void 0 ? tmp2 : "null";
8807
- res2 += join5;
8872
+ res2 += join9;
8808
8873
  }
8809
8874
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
8810
8875
  res2 += tmp !== void 0 ? tmp : "null";
8811
8876
  if (value.length - 1 > maximumBreadth) {
8812
8877
  const removedKeys = value.length - maximumBreadth - 1;
8813
- res2 += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
8878
+ res2 += `${join9}"... ${getItemCount(removedKeys)} not stringified"`;
8814
8879
  }
8815
8880
  res2 += `
8816
8881
  ${originalIndentation}`;
@@ -8826,16 +8891,16 @@ ${originalIndentation}`;
8826
8891
  return '"[Object]"';
8827
8892
  }
8828
8893
  indentation += spacer;
8829
- const join4 = `,
8894
+ const join8 = `,
8830
8895
  ${indentation}`;
8831
8896
  let res = "";
8832
8897
  let separator = "";
8833
8898
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
8834
8899
  if (isTypedArrayWithEntries(value)) {
8835
- res += stringifyTypedArray(value, join4, maximumBreadth);
8900
+ res += stringifyTypedArray(value, join8, maximumBreadth);
8836
8901
  keys = keys.slice(value.length);
8837
8902
  maximumPropertiesToStringify -= value.length;
8838
- separator = join4;
8903
+ separator = join8;
8839
8904
  }
8840
8905
  if (deterministic) {
8841
8906
  keys = sort(keys, comparator);
@@ -8846,13 +8911,13 @@ ${indentation}`;
8846
8911
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
8847
8912
  if (tmp !== void 0) {
8848
8913
  res += `${separator}${strEscape(key2)}: ${tmp}`;
8849
- separator = join4;
8914
+ separator = join8;
8850
8915
  }
8851
8916
  }
8852
8917
  if (keyLength > maximumBreadth) {
8853
8918
  const removedKeys = keyLength - maximumBreadth;
8854
8919
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
8855
- separator = join4;
8920
+ separator = join8;
8856
8921
  }
8857
8922
  if (separator !== "") {
8858
8923
  res = `
@@ -9943,11 +10008,11 @@ var init_lib = __esm({
9943
10008
  }
9944
10009
  }
9945
10010
  },
9946
- addToPath: function addToPath(path32, added, removed, oldPosInc, options) {
9947
- var last = path32.lastComponent;
10011
+ addToPath: function addToPath(path35, added, removed, oldPosInc, options) {
10012
+ var last = path35.lastComponent;
9948
10013
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
9949
10014
  return {
9950
- oldPos: path32.oldPos + oldPosInc,
10015
+ oldPos: path35.oldPos + oldPosInc,
9951
10016
  lastComponent: {
9952
10017
  count: last.count + 1,
9953
10018
  added,
@@ -9957,7 +10022,7 @@ var init_lib = __esm({
9957
10022
  };
9958
10023
  } else {
9959
10024
  return {
9960
- oldPos: path32.oldPos + oldPosInc,
10025
+ oldPos: path35.oldPos + oldPosInc,
9961
10026
  lastComponent: {
9962
10027
  count: 1,
9963
10028
  added,
@@ -10015,7 +10080,7 @@ var init_lib = __esm({
10015
10080
  tokenize: function tokenize(value) {
10016
10081
  return Array.from(value);
10017
10082
  },
10018
- join: function join3(chars) {
10083
+ join: function join4(chars) {
10019
10084
  return chars.join("");
10020
10085
  },
10021
10086
  postProcess: function postProcess(changeObjects) {
@@ -10388,10 +10453,10 @@ function attachmentToHistoryMessage(o, ts) {
10388
10453
  const memories = raw.map((m2) => {
10389
10454
  if (!m2 || typeof m2 !== "object") return null;
10390
10455
  const rec = m2;
10391
- const path32 = typeof rec.path === "string" ? rec.path : null;
10456
+ const path35 = typeof rec.path === "string" ? rec.path : null;
10392
10457
  const content = typeof rec.content === "string" ? rec.content : null;
10393
- if (!path32 || content == null) return null;
10394
- const entry = { path: path32, content };
10458
+ if (!path35 || content == null) return null;
10459
+ const entry = { path: path35, content };
10395
10460
  if (typeof rec.mtimeMs === "number") entry.mtimeMs = rec.mtimeMs;
10396
10461
  return entry;
10397
10462
  }).filter((m2) => m2 !== null);
@@ -10851,6 +10916,27 @@ var init_claude_history = __esm({
10851
10916
  }
10852
10917
  });
10853
10918
 
10919
+ // src/tools/sandbox.ts
10920
+ function shouldSandbox(cwd, personaRoot) {
10921
+ if (!personaRoot) return false;
10922
+ const sep2 = personaRoot.endsWith(path12.sep) ? "" : path12.sep;
10923
+ return cwd.startsWith(personaRoot + sep2) && cwd !== personaRoot;
10924
+ }
10925
+ function inferSandboxSettingsPath(cwd, personaRoot) {
10926
+ if (!shouldSandbox(cwd, personaRoot)) return null;
10927
+ const rel = path12.relative(personaRoot, cwd);
10928
+ const personaId = rel.split(path12.sep)[0];
10929
+ if (!personaId) return null;
10930
+ return path12.join(personaRoot, personaId, ".clawd", "sandbox-settings.json");
10931
+ }
10932
+ var path12;
10933
+ var init_sandbox = __esm({
10934
+ "src/tools/sandbox.ts"() {
10935
+ "use strict";
10936
+ path12 = __toESM(require("path"), 1);
10937
+ }
10938
+ });
10939
+
10854
10940
  // src/tools/claude.ts
10855
10941
  function macOSDesktopCandidates(home) {
10856
10942
  return [
@@ -10915,7 +11001,8 @@ function buildSpawnArgs(ctx) {
10915
11001
  throw new Error(`unexpected personaMode: ${String(_exhaustive)}`);
10916
11002
  }
10917
11003
  }
10918
- if (ctx.extraSettings) args.push("--settings", ctx.extraSettings);
11004
+ const sandboxSettings = ctx.extraSettings ?? inferSandboxSettingsPath(ctx.cwd, ctx.personaRoot);
11005
+ if (sandboxSettings) args.push("--settings", sandboxSettings);
10919
11006
  if (ctx.extraSystemPrompt) args.push("--append-system-prompt", ctx.extraSystemPrompt);
10920
11007
  if (ctx.effort) args.push("--effort", ctx.effort);
10921
11008
  if (ctx.toolSessionId) args.push("--resume", ctx.toolSessionId);
@@ -11195,10 +11282,10 @@ function parseAttachment(obj) {
11195
11282
  const memories = raw.map((m2) => {
11196
11283
  if (!m2 || typeof m2 !== "object") return null;
11197
11284
  const rec = m2;
11198
- const path32 = typeof rec.path === "string" ? rec.path : null;
11285
+ const path35 = typeof rec.path === "string" ? rec.path : null;
11199
11286
  const content = typeof rec.content === "string" ? rec.content : null;
11200
- if (!path32 || content == null) return null;
11201
- const out = { path: path32, content };
11287
+ if (!path35 || content == null) return null;
11288
+ const out = { path: path35, content };
11202
11289
  if (typeof rec.mtimeMs === "number") out.mtimeMs = rec.mtimeMs;
11203
11290
  return out;
11204
11291
  }).filter((m2) => m2 !== null);
@@ -11314,6 +11401,7 @@ var init_claude = __esm({
11314
11401
  init_protocol();
11315
11402
  init_claude_history();
11316
11403
  init_tool_result_extra();
11404
+ init_sandbox();
11317
11405
  ATTACHMENT_SILENT_SUBTYPES2 = /* @__PURE__ */ new Set([
11318
11406
  "hook_additional_context",
11319
11407
  "hook_success",
@@ -18696,7 +18784,7 @@ var require_websocket = __commonJS({
18696
18784
  var http2 = require("http");
18697
18785
  var net = require("net");
18698
18786
  var tls = require("tls");
18699
- var { randomBytes, createHash } = require("crypto");
18787
+ var { randomBytes: randomBytes2, createHash: createHash3 } = require("crypto");
18700
18788
  var { Duplex, Readable: Readable3 } = require("stream");
18701
18789
  var { URL: URL2 } = require("url");
18702
18790
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -19226,7 +19314,7 @@ var require_websocket = __commonJS({
19226
19314
  }
19227
19315
  }
19228
19316
  const defaultPort = isSecure ? 443 : 80;
19229
- const key = randomBytes(16).toString("base64");
19317
+ const key = randomBytes2(16).toString("base64");
19230
19318
  const request = isSecure ? https.request : http2.request;
19231
19319
  const protocolSet = /* @__PURE__ */ new Set();
19232
19320
  let perMessageDeflate;
@@ -19356,7 +19444,7 @@ var require_websocket = __commonJS({
19356
19444
  abortHandshake(websocket, socket, "Invalid Upgrade header");
19357
19445
  return;
19358
19446
  }
19359
- const digest = createHash("sha1").update(key + GUID).digest("base64");
19447
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
19360
19448
  if (res.headers["sec-websocket-accept"] !== digest) {
19361
19449
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
19362
19450
  return;
@@ -19723,7 +19811,7 @@ var require_websocket_server = __commonJS({
19723
19811
  var EventEmitter2 = require("events");
19724
19812
  var http2 = require("http");
19725
19813
  var { Duplex } = require("stream");
19726
- var { createHash } = require("crypto");
19814
+ var { createHash: createHash3 } = require("crypto");
19727
19815
  var extension2 = require_extension();
19728
19816
  var PerMessageDeflate2 = require_permessage_deflate();
19729
19817
  var subprotocol2 = require_subprotocol();
@@ -20024,7 +20112,7 @@ var require_websocket_server = __commonJS({
20024
20112
  );
20025
20113
  }
20026
20114
  if (this._state > RUNNING) return abortHandshake(socket, 503);
20027
- const digest = createHash("sha1").update(key + GUID).digest("base64");
20115
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
20028
20116
  const headers = [
20029
20117
  "HTTP/1.1 101 Switching Protocols",
20030
20118
  "Upgrade: websocket",
@@ -20112,7 +20200,7 @@ var require_websocket_server = __commonJS({
20112
20200
  // src/run-case/recorder.ts
20113
20201
  function startRunCaseRecorder(opts) {
20114
20202
  const now = opts.now ?? Date.now;
20115
- const dir = import_node_path28.default.dirname(opts.recordPath);
20203
+ const dir = import_node_path27.default.dirname(opts.recordPath);
20116
20204
  let stream = null;
20117
20205
  let closing = false;
20118
20206
  let closedSettled = false;
@@ -20152,12 +20240,12 @@ function startRunCaseRecorder(opts) {
20152
20240
  };
20153
20241
  return { tap, close, closed };
20154
20242
  }
20155
- var import_node_fs25, import_node_path28;
20243
+ var import_node_fs25, import_node_path27;
20156
20244
  var init_recorder = __esm({
20157
20245
  "src/run-case/recorder.ts"() {
20158
20246
  "use strict";
20159
20247
  import_node_fs25 = __toESM(require("fs"), 1);
20160
- import_node_path28 = __toESM(require("path"), 1);
20248
+ import_node_path27 = __toESM(require("path"), 1);
20161
20249
  }
20162
20250
  });
20163
20251
 
@@ -20200,7 +20288,7 @@ var init_wire = __esm({
20200
20288
  // src/run-case/controller.ts
20201
20289
  async function runController(opts) {
20202
20290
  const now = opts.now ?? Date.now;
20203
- const cwd = opts.cwd ?? (0, import_node_fs26.mkdtempSync)(import_node_path29.default.join(import_node_os14.default.tmpdir(), "clawd-runcase-"));
20291
+ const cwd = opts.cwd ?? (0, import_node_fs26.mkdtempSync)(import_node_path28.default.join(import_node_os14.default.tmpdir(), "clawd-runcase-"));
20204
20292
  const ownsCwd = opts.cwd === void 0;
20205
20293
  const recorder = startRunCaseRecorder({ recordPath: opts.record, now });
20206
20294
  const spawnCtx = { cwd };
@@ -20367,13 +20455,13 @@ async function runController(opts) {
20367
20455
  }
20368
20456
  return exitCode ?? 0;
20369
20457
  }
20370
- var import_node_fs26, import_node_os14, import_node_path29;
20458
+ var import_node_fs26, import_node_os14, import_node_path28;
20371
20459
  var init_controller = __esm({
20372
20460
  "src/run-case/controller.ts"() {
20373
20461
  "use strict";
20374
20462
  import_node_fs26 = require("fs");
20375
20463
  import_node_os14 = __toESM(require("os"), 1);
20376
- import_node_path29 = __toESM(require("path"), 1);
20464
+ import_node_path28 = __toESM(require("path"), 1);
20377
20465
  init_claude();
20378
20466
  init_stdout_splitter();
20379
20467
  init_permission_stdio();
@@ -20605,7 +20693,7 @@ Env (advanced):
20605
20693
  `;
20606
20694
 
20607
20695
  // src/index.ts
20608
- var import_node_path27 = __toESM(require("path"), 1);
20696
+ var import_node_path26 = __toESM(require("path"), 1);
20609
20697
  var import_node_fs24 = __toESM(require("fs"), 1);
20610
20698
 
20611
20699
  // src/logger.ts
@@ -20643,6 +20731,9 @@ function createLogger(opts = {}) {
20643
20731
  return wrap(base);
20644
20732
  }
20645
20733
 
20734
+ // src/session/store-factory.ts
20735
+ var path5 = __toESM(require("path"), 1);
20736
+
20646
20737
  // src/session/store.ts
20647
20738
  var import_node_fs3 = __toESM(require("fs"), 1);
20648
20739
  var import_node_path4 = __toESM(require("path"), 1);
@@ -20680,12 +20771,16 @@ function safeFileName(sessionId) {
20680
20771
  var SessionStore = class {
20681
20772
  root;
20682
20773
  constructor(opts) {
20683
- const scope = opts.scope ?? { kind: "default" };
20684
- this.root = import_node_path4.default.join(
20685
- opts.dataDir,
20686
- "sessions",
20687
- ...scopeSubPath(scope).map(safeFileName)
20688
- );
20774
+ if ("root" in opts) {
20775
+ this.root = opts.root;
20776
+ } else {
20777
+ const scope = opts.scope ?? { kind: "default" };
20778
+ this.root = import_node_path4.default.join(
20779
+ opts.dataDir,
20780
+ "sessions",
20781
+ ...scopeSubPath(scope).map(safeFileName)
20782
+ );
20783
+ }
20689
20784
  }
20690
20785
  filePath(sessionId) {
20691
20786
  return import_node_path4.default.join(this.root, `${safeFileName(sessionId)}.json`);
@@ -20750,6 +20845,66 @@ var SessionStore = class {
20750
20845
  }
20751
20846
  };
20752
20847
 
20848
+ // src/session/store-factory.ts
20849
+ var SessionStoreFactory = class {
20850
+ dataDir;
20851
+ bareStore = null;
20852
+ vmOwnerStores = /* @__PURE__ */ new Map();
20853
+ // vmGuest 索引 key = `${pid}::${capId}`
20854
+ vmGuestStores = /* @__PURE__ */ new Map();
20855
+ constructor(opts) {
20856
+ this.dataDir = opts.dataDir;
20857
+ }
20858
+ // ---- root path 派生(暴露给 migration / revoke cascade 等外部消费方) ----
20859
+ bareRoot() {
20860
+ return path5.join(this.dataDir, "sessions");
20861
+ }
20862
+ vmOwnerRoot(personaId) {
20863
+ return path5.join(
20864
+ this.dataDir,
20865
+ "personas",
20866
+ safeFileName(personaId),
20867
+ ".clawd",
20868
+ "sessions",
20869
+ "owner"
20870
+ );
20871
+ }
20872
+ vmGuestRoot(personaId, capabilityId) {
20873
+ return path5.join(
20874
+ this.dataDir,
20875
+ "personas",
20876
+ safeFileName(personaId),
20877
+ ".clawd",
20878
+ "sessions",
20879
+ "guests",
20880
+ safeFileName(capabilityId)
20881
+ );
20882
+ }
20883
+ // ---- SessionStore 工厂(缓存) ----
20884
+ forBare() {
20885
+ if (!this.bareStore) {
20886
+ this.bareStore = new SessionStore({ root: this.bareRoot() });
20887
+ }
20888
+ return this.bareStore;
20889
+ }
20890
+ forVmOwner(personaId) {
20891
+ const key = personaId;
20892
+ const cached = this.vmOwnerStores.get(key);
20893
+ if (cached) return cached;
20894
+ const st = new SessionStore({ root: this.vmOwnerRoot(personaId) });
20895
+ this.vmOwnerStores.set(key, st);
20896
+ return st;
20897
+ }
20898
+ forVmGuest(personaId, capabilityId) {
20899
+ const key = `${personaId}::${capabilityId}`;
20900
+ const cached = this.vmGuestStores.get(key);
20901
+ if (cached) return cached;
20902
+ const st = new SessionStore({ root: this.vmGuestRoot(personaId, capabilityId) });
20903
+ this.vmGuestStores.set(key, st);
20904
+ return st;
20905
+ }
20906
+ };
20907
+
20753
20908
  // src/session/manager.ts
20754
20909
  var import_node_fs5 = __toESM(require("fs"), 1);
20755
20910
  var import_node_path6 = __toESM(require("path"), 1);
@@ -20909,7 +21064,10 @@ function buildSpawnContext(state, deps) {
20909
21064
  toolSessionId: file.toolSessionId,
20910
21065
  model: file.model,
20911
21066
  permissionMode: file.permissionMode,
20912
- effort: file.effort
21067
+ effort: file.effort,
21068
+ // Phase 2 capability platform (plan §3): personaRoot 透传给 buildSpawnArgs;
21069
+ // 内部 shouldSandbox(cwd, personaRoot) 决定是否注入 --settings sandbox-settings.
21070
+ personaRoot: deps.personaRoot
20913
21071
  };
20914
21072
  const meta = state.subSessionMeta;
20915
21073
  if (meta?.personaMode) {
@@ -21636,7 +21794,8 @@ var SessionRunner = class {
21636
21794
  now: this.hooks.now ?? Date.now,
21637
21795
  resolveContextWindow: this.hooks.resolveContextWindow,
21638
21796
  genUuid: this.hooks.genUuid ?? v4_default,
21639
- ownerDisplayName: this.hooks.ownerDisplayName
21797
+ ownerDisplayName: this.hooks.ownerDisplayName,
21798
+ personaRoot: this.hooks.personaRoot
21640
21799
  };
21641
21800
  const { state, effects } = reduceSession(this.state, inputMsg, deps);
21642
21801
  this.state = state;
@@ -22060,9 +22219,20 @@ var SessionManager = class {
22060
22219
  attachObserver(observer) {
22061
22220
  this.attachedObserver = observer;
22062
22221
  }
22063
- // 按 scope 拿对应的 SessionStore。default scope 复用 deps.store(保证与
22064
- // bootstrap 注入的 store 一致);persona scope 第一次访问时按需创建并缓存。
22222
+ // 按 scope 拿对应的 SessionStore.
22223
+ // Phase 2 (capability platform plan §1) 路由切到 SessionStoreFactory:
22224
+ // default → factory.forBare() <dataDir>/sessions/
22225
+ // persona/<pid>/owner → factory.forVmOwner(pid) <dataDir>/personas/<pid>/.clawd/sessions/owner/
22226
+ // persona/<pid>/listener → throw (listener 角色 #698 已下线, schema 字段保留但运行时不该出现)
22227
+ // 缺省 (无 factory 注入): 回退 dataDir+scope 老路径 (向后兼容旧 spec).
22065
22228
  storeFor(scope) {
22229
+ if (this.deps.storeFactory) {
22230
+ if (scope.kind === "default") return this.deps.storeFactory.forBare();
22231
+ if (scope.mode === "owner") return this.deps.storeFactory.forVmOwner(scope.personaId);
22232
+ throw new Error(
22233
+ `SessionManager: listener scope is deprecated (#698); use forVmGuest in Phase 3+ instead`
22234
+ );
22235
+ }
22066
22236
  if (scope.kind === "default") return this.deps.store;
22067
22237
  const key = scopeKey(scope);
22068
22238
  const cached = this.storesByScope.get(key);
@@ -22090,11 +22260,13 @@ var SessionManager = class {
22090
22260
  scopeForFile(file) {
22091
22261
  return file.ownerPersonaId ? { kind: "persona", personaId: file.ownerPersonaId, mode: "owner" } : { kind: "default" };
22092
22262
  }
22093
- // <dataDir>/sessions/ 列出所有 persona 命名空间(不含 'default')。
22094
- // 用于 findOwnedSession / listAllOwned scope 查询。
22263
+ // 扫所有 persona 命名空间. 用于 findOwnedSession / listAllOwned scope 查询.
22264
+ // Phase 2 capability platform: 新布局下 persona 资产在 <dataDir>/personas/<pid>/,
22265
+ // 直接 readdir 这个目录拿所有 personaId. 老布局 (无 storeFactory) fallback 扫
22266
+ // <dataDir>/sessions/ 列子目录 (排除 'default').
22095
22267
  listPersonaIdsOnDisk() {
22096
22268
  if (!this.deps.dataDir) return [];
22097
- const root = import_node_path6.default.join(this.deps.dataDir, "sessions");
22269
+ const root = this.deps.storeFactory ? import_node_path6.default.join(this.deps.dataDir, "personas") : import_node_path6.default.join(this.deps.dataDir, "sessions");
22098
22270
  let entries;
22099
22271
  try {
22100
22272
  entries = import_node_fs5.default.readdirSync(root, { withFileTypes: true });
@@ -22106,19 +22278,9 @@ var SessionManager = class {
22106
22278
  return entries.filter((e) => e.isDirectory() && e.name !== "default").map((e) => e.name);
22107
22279
  }
22108
22280
  // owner / default 两个分类下按 sessionId 找文件——前端只传 sessionId 不带 scope,
22109
- // SessionManager 在这里定位。三层快慢路径:
22110
- // 1) active runner(用户当前在用的 session):runner.state.file 是 SessionFile 内存权威副本,
22111
- // 直接返回,零磁盘 I/O。覆盖 attachment / file-sharing 等"用户操作 → 紧接着 RPC"的
22112
- // 绝大多数场景
22113
- // 2) default scope 磁盘:inactive default session 命中
22114
- // 3) 所有 persona owner 目录扫盘:inactive persona owner session 命中
22281
+ // SessionManager 在这里扫盘定位(default 直接试,未命中再轮询所有 persona owner 目录)。
22115
22282
  // listener sub-session 不走这条——transport 始终明确传 (personaId, sessionId)。
22116
- //
22117
- // 公开方法:attachment / file-sharing handler 通过 deps 闭包消费,不应直接持 SessionStore
22118
- // (持 default-only store 是 file-sharing v1 的预存 bug 根因——见 fix #703)
22119
22283
  findOwnedSession(sessionId) {
22120
- const runner = this.runners.get(sessionId);
22121
- if (runner) return runner.getState().file;
22122
22284
  const dflt = this.deps.store.read(sessionId);
22123
22285
  if (dflt) return dflt;
22124
22286
  for (const personaId of this.listPersonaIdsOnDisk()) {
@@ -22128,13 +22290,6 @@ var SessionManager = class {
22128
22290
  }
22129
22291
  return null;
22130
22292
  }
22131
- // findOwnedSession + scopeForFile 收口。把"sessionId → SessionScope"的派生
22132
- // 集中到 manager(scope 单一来源),调用方拿 scope 不再自己拼 object 也不依赖
22133
- // ownerPersonaId 字段语义。给 attachment handler 通过 deps.getSessionScope 闭包消费。
22134
- findOwnedSessionScope(sessionId) {
22135
- const file = this.findOwnedSession(sessionId);
22136
- return file ? this.scopeForFile(file) : null;
22137
- }
22138
22293
  // 合并 default + 所有 persona owner 的 SessionFile —— 桌面 App 主 session 列表入口。
22139
22294
  // 同样不含 listener sub-session(listener 走 persona:listSubSessions RPC 单独入口)。
22140
22295
  listAllOwned() {
@@ -22186,6 +22341,9 @@ var SessionManager = class {
22186
22341
  dataDir: this.deps.dataDir,
22187
22342
  personaStore: this.deps.personaStore,
22188
22343
  ownerDisplayName: this.deps.ownerDisplayName,
22344
+ // Phase 2 capability platform (plan §3): 透传 personaRoot, buildSpawnArgs 据
22345
+ // cwd 是否在 personaRoot 下自动决定是否注入 --settings sandbox-settings.json.
22346
+ personaRoot: this.deps.personaRoot,
22189
22347
  // file-sharing (spec §6 PR 3):闭包 scope + sessionId,runner 只暴露 tool/relPath/cwd
22190
22348
  onFileEdit: attachmentGroup ? (input) => attachmentGroup.onFileEdit({
22191
22349
  scope,
@@ -23198,7 +23356,7 @@ var SessionManager = class {
23198
23356
 
23199
23357
  // src/persona/store.ts
23200
23358
  var fs6 = __toESM(require("fs"), 1);
23201
- var path7 = __toESM(require("path"), 1);
23359
+ var path8 = __toESM(require("path"), 1);
23202
23360
  init_protocol();
23203
23361
  var DEFAULT_SETTINGS = {
23204
23362
  permissions: {
@@ -23227,13 +23385,13 @@ var PersonaStore = class {
23227
23385
  }
23228
23386
  root;
23229
23387
  personaDir(personaId) {
23230
- return path7.join(this.root, safeFileName(personaId));
23388
+ return path8.join(this.root, safeFileName(personaId));
23231
23389
  }
23232
23390
  metaPath(personaId) {
23233
- return path7.join(this.personaDir(personaId), ".clawd", "persona.json");
23391
+ return path8.join(this.personaDir(personaId), ".clawd", "persona.json");
23234
23392
  }
23235
23393
  claudeMdPath(personaId) {
23236
- return path7.join(this.personaDir(personaId), "CLAUDE.md");
23394
+ return path8.join(this.personaDir(personaId), "CLAUDE.md");
23237
23395
  }
23238
23396
  /**
23239
23397
  * Sandbox settings 落盘路径 —— 故意放在 `.clawd/` 而不是 `.claude/`,让 CC 的
@@ -23243,11 +23401,11 @@ var PersonaStore = class {
23243
23401
  * 加载 persona 人格,只有 listener 多一层 OS sandbox。
23244
23402
  */
23245
23403
  sandboxSettingsPath(personaId) {
23246
- return path7.join(this.personaDir(personaId), ".clawd", "sandbox-settings.json");
23404
+ return path8.join(this.personaDir(personaId), ".clawd", "sandbox-settings.json");
23247
23405
  }
23248
23406
  write(persona, personality) {
23249
23407
  const dir = this.personaDir(persona.personaId);
23250
- fs6.mkdirSync(path7.join(dir, ".clawd"), { recursive: true });
23408
+ fs6.mkdirSync(path8.join(dir, ".clawd"), { recursive: true });
23251
23409
  this.atomicWrite(this.claudeMdPath(persona.personaId), personality);
23252
23410
  this.atomicWrite(
23253
23411
  this.sandboxSettingsPath(persona.personaId),
@@ -23290,12 +23448,12 @@ var PersonaStore = class {
23290
23448
  }
23291
23449
  /** Persona 私有 skills 目录路径:<personaDir>/.claude/skills */
23292
23450
  skillsDir(personaId) {
23293
- return path7.join(this.personaDir(personaId), ".claude", "skills");
23451
+ return path8.join(this.personaDir(personaId), ".claude", "skills");
23294
23452
  }
23295
23453
  list() {
23296
23454
  if (!fs6.existsSync(this.root)) return [];
23297
23455
  return fs6.readdirSync(this.root).filter((name) => {
23298
- return fs6.existsSync(path7.join(this.root, name, ".clawd", "persona.json"));
23456
+ return fs6.existsSync(path8.join(this.root, name, ".clawd", "persona.json"));
23299
23457
  });
23300
23458
  }
23301
23459
  remove(personaId) {
@@ -23694,7 +23852,17 @@ var PersonaManager = class {
23694
23852
  }
23695
23853
  /**
23696
23854
  * 删除 persona。
23697
- * PersonaStore.remove(personaId) 已级联删除整个 <personaRoot>/<personaId>/ 目录。
23855
+ * PersonaStore.remove(personaId) 已级联删除整个 <personaRoot>/<personaId>/ 目录,
23856
+ * Phase 2 capability platform 新布局下含 .clawd/sessions/owner/ +
23857
+ * .clawd/sessions/guests/<capId>/, rmSync recursive force 一锅清.
23858
+ *
23859
+ * ⚠️ TODO (Phase 3+ design gap, reviewer P1 flagged):
23860
+ * ~/.clawd/capabilities.json 里若有 cap.grants 含本 personaId, grant 会变成
23861
+ * 悬空引用 (guest 仍能持 token 接入, 但调依赖该 persona 的 RPC 会 fail).
23862
+ * 选项: (A) 这里扫所有 cap 过滤掉该 personaId 的 grant; (B) revoke 所有 grant
23863
+ * 含该 personaId 的 cap. 当前因 cleanupGuestSessionsForCapability 用 rmSync
23864
+ * force=true 吞 ENOENT 不会 crash, 但 UI CapabilityManagerDrawer 会展示
23865
+ * "无效的 persona" grant. Phase 3 加 cross-reference 矩阵后正式实现.
23698
23866
  */
23699
23867
  delete(personaId) {
23700
23868
  this.deps.store.remove(personaId);
@@ -23759,7 +23927,7 @@ var PersonaManager = class {
23759
23927
 
23760
23928
  // src/persona/seed.ts
23761
23929
  var fs8 = __toESM(require("fs"), 1);
23762
- var path9 = __toESM(require("path"), 1);
23930
+ var path10 = __toESM(require("path"), 1);
23763
23931
  var import_node_url = require("url");
23764
23932
  var import_meta = {};
23765
23933
  var DEFAULT_PERSONAS = [
@@ -23795,14 +23963,14 @@ var DEFAULT_PERSONAS = [
23795
23963
  function findDefaultsRoot() {
23796
23964
  const candidates = [];
23797
23965
  try {
23798
- const here = path9.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
23799
- candidates.push(path9.resolve(here, "defaults"));
23800
- candidates.push(path9.resolve(here, "persona-defaults"));
23966
+ const here = path10.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
23967
+ candidates.push(path10.resolve(here, "defaults"));
23968
+ candidates.push(path10.resolve(here, "persona-defaults"));
23801
23969
  } catch {
23802
23970
  }
23803
23971
  if (process.argv[1]) {
23804
- const argvDir = path9.dirname(process.argv[1]);
23805
- candidates.push(path9.resolve(argvDir, "persona-defaults"));
23972
+ const argvDir = path10.dirname(process.argv[1]);
23973
+ candidates.push(path10.resolve(argvDir, "persona-defaults"));
23806
23974
  }
23807
23975
  for (const c of candidates) {
23808
23976
  try {
@@ -23819,7 +23987,7 @@ function seedDefaultPersonas(args) {
23819
23987
  args.logger.info("persona.seed.skip", { personaId: entry.personaId, reason: "exists" });
23820
23988
  continue;
23821
23989
  }
23822
- const bundleDir = path9.join(args.defaultsRoot, entry.personaId);
23990
+ const bundleDir = path10.join(args.defaultsRoot, entry.personaId);
23823
23991
  if (!fs8.existsSync(bundleDir)) {
23824
23992
  args.logger.warn("persona.seed.skip", {
23825
23993
  personaId: entry.personaId,
@@ -23828,7 +23996,7 @@ function seedDefaultPersonas(args) {
23828
23996
  });
23829
23997
  continue;
23830
23998
  }
23831
- const claudeMdPath = path9.join(bundleDir, "CLAUDE.md");
23999
+ const claudeMdPath = path10.join(bundleDir, "CLAUDE.md");
23832
24000
  if (!fs8.existsSync(claudeMdPath)) {
23833
24001
  args.logger.warn("persona.seed.skip", {
23834
24002
  personaId: entry.personaId,
@@ -23857,8 +24025,8 @@ function seedDefaultPersonas(args) {
23857
24025
  function copyBundleExtras(srcDir, dstDir) {
23858
24026
  for (const entry of fs8.readdirSync(srcDir, { withFileTypes: true })) {
23859
24027
  if (entry.name === "CLAUDE.md" || entry.name === ".clawd") continue;
23860
- const srcPath = path9.join(srcDir, entry.name);
23861
- const dstPath = path9.join(dstDir, entry.name);
24028
+ const srcPath = path10.join(srcDir, entry.name);
24029
+ const dstPath = path10.join(dstDir, entry.name);
23862
24030
  if (entry.isDirectory()) {
23863
24031
  fs8.cpSync(srcPath, dstPath, { recursive: true, dereference: true });
23864
24032
  } else if (entry.isFile()) {
@@ -25240,6 +25408,9 @@ var LocalWsServer = class {
25240
25408
  httpServer = null;
25241
25409
  frameHandler = null;
25242
25410
  clients = /* @__PURE__ */ new Map();
25411
+ // Task 1.7 capability platform:capId → Set<clientId>,撤销时 O(1) 找到该 cap 的
25412
+ // 所有活跃连接做关闭级联(Task 1.10)。同一 cap 可能多端同时连(多设备 / 多 tab)。
25413
+ capabilityIdToClients = /* @__PURE__ */ new Map();
25243
25414
  logger;
25244
25415
  pingIntervalMs;
25245
25416
  async start() {
@@ -25330,6 +25501,17 @@ var LocalWsServer = class {
25330
25501
  this.safeSend(c.ws, frame);
25331
25502
  }
25332
25503
  }
25504
+ // Task 1.9 capability platform:仅广播给 owner 连接(跳过 guest ws)。
25505
+ // 用于 capability:tokenIssued / capability:revoked 等 owner-only push frame——
25506
+ // guest 没必要也无法消费这类管理帧。noAuth localhost ws 没有 ctx, 视作 owner.
25507
+ broadcastToOwners(frame) {
25508
+ const gate = this.opts.authGate;
25509
+ for (const c of this.clients.values()) {
25510
+ if (gate && !gate.isAuthed(c.handle.id)) continue;
25511
+ if (c.ctx && c.ctx.principal.kind !== "owner") continue;
25512
+ this.safeSend(c.ws, frame);
25513
+ }
25514
+ }
25333
25515
  firstSubscriber(sessionId) {
25334
25516
  for (const c of this.clients.values()) {
25335
25517
  if (c.handle.subscribedSessions.has(sessionId)) return c.handle;
@@ -25351,6 +25533,40 @@ var LocalWsServer = class {
25351
25533
  if (!c) return;
25352
25534
  this.safeSend(c.ws, frame);
25353
25535
  }
25536
+ // Task 1.7 capability platform:AuthGate.onAuthed 回调里调本方法,把 ConnectionContext
25537
+ // 绑到 client;guest 连接同时进 capId 索引(Task 1.10 撤销级联用)。
25538
+ attachClientContext(clientId, ctx) {
25539
+ const c = this.clients.get(clientId);
25540
+ if (!c) return;
25541
+ c.ctx = ctx;
25542
+ if (ctx.capabilityId) {
25543
+ let set = this.capabilityIdToClients.get(ctx.capabilityId);
25544
+ if (!set) {
25545
+ set = /* @__PURE__ */ new Set();
25546
+ this.capabilityIdToClients.set(ctx.capabilityId, set);
25547
+ }
25548
+ set.add(clientId);
25549
+ }
25550
+ }
25551
+ // 测试 / Task 1.8 dispatcher 用:拿 client 当前 ConnectionContext(null = 未鉴权 / noAuth)
25552
+ getClientContext(clientId) {
25553
+ return this.clients.get(clientId)?.ctx ?? null;
25554
+ }
25555
+ // Task 1.10 撤销级联:关闭某 capability 的所有活跃 ws 连接。close code 4401
25556
+ // 让客户端识别 'capability revoked' 而非普通断连。
25557
+ closeConnectionsByCapability(capabilityId, code = 4401, reason = "TOKEN_REVOKED") {
25558
+ const set = this.capabilityIdToClients.get(capabilityId);
25559
+ if (!set) return;
25560
+ const ids = [...set];
25561
+ for (const id of ids) {
25562
+ const c = this.clients.get(id);
25563
+ if (!c) continue;
25564
+ try {
25565
+ c.ws.close(code, reason);
25566
+ } catch {
25567
+ }
25568
+ }
25569
+ }
25354
25570
  // URL path 路由:'/' → 主连接路径;其他 → close 4404
25355
25571
  routeConnection(socket, req) {
25356
25572
  const remoteAddress = req?.socket?.remoteAddress;
@@ -25385,7 +25601,7 @@ var LocalWsServer = class {
25385
25601
  } catch {
25386
25602
  }
25387
25603
  }, this.pingIntervalMs);
25388
- this.clients.set(id, { handle, ws: socket, pingTimer });
25604
+ this.clients.set(id, { handle, ws: socket, pingTimer, ctx: null });
25389
25605
  this.logger?.info("client connected", { clientId: id, total: this.clients.size, remoteAddress });
25390
25606
  const authGate = this.opts.authGate;
25391
25607
  let authed = true;
@@ -25409,6 +25625,12 @@ var LocalWsServer = class {
25409
25625
  socket.on("close", () => {
25410
25626
  const c = this.clients.get(id);
25411
25627
  if (c?.pingTimer) clearInterval(c.pingTimer);
25628
+ const capId = c?.ctx?.capabilityId;
25629
+ if (capId) {
25630
+ const set = this.capabilityIdToClients.get(capId);
25631
+ set?.delete(id);
25632
+ if (set && set.size === 0) this.capabilityIdToClients.delete(capId);
25633
+ }
25412
25634
  this.clients.delete(id);
25413
25635
  authGate?.unregister(id);
25414
25636
  this.logger?.info("client disconnected", { clientId: id, remaining: this.clients.size });
@@ -25557,15 +25779,27 @@ var AuthGate = class {
25557
25779
  this.markFailed(handle.id);
25558
25780
  return frame.type === "auth" ? "consumed" : "reject";
25559
25781
  }
25560
- if (!this.opts.expectedToken) {
25561
- this.opts.closeConnection(handle, 1008, "auth not configured");
25562
- this.markFailed(handle.id);
25563
- return "consumed";
25564
- }
25565
- if (!constantTimeEqual(parsed.data.token, this.opts.expectedToken)) {
25566
- this.opts.closeConnection(handle, 1008, "auth failed");
25567
- this.markFailed(handle.id);
25568
- return "consumed";
25782
+ let ctx = null;
25783
+ if (this.opts.authenticate) {
25784
+ const r = this.opts.authenticate(parsed.data.token);
25785
+ if (!r.ok) {
25786
+ this.opts.closeConnection(handle, 4401, r.code);
25787
+ this.markFailed(handle.id);
25788
+ return "consumed";
25789
+ }
25790
+ ctx = r.context;
25791
+ } else {
25792
+ if (!this.opts.expectedToken) {
25793
+ this.opts.closeConnection(handle, 1008, "auth not configured");
25794
+ this.markFailed(handle.id);
25795
+ return "consumed";
25796
+ }
25797
+ if (!constantTimeEqual(parsed.data.token, this.opts.expectedToken)) {
25798
+ this.opts.closeConnection(handle, 1008, "auth failed");
25799
+ this.markFailed(handle.id);
25800
+ return "consumed";
25801
+ }
25802
+ ctx = this.opts.buildOwnerContext?.() ?? null;
25569
25803
  }
25570
25804
  if (st.timer != null) {
25571
25805
  const clear = this.opts.clearTimer ?? ((h) => clearTimeout(h));
@@ -25573,6 +25807,7 @@ var AuthGate = class {
25573
25807
  }
25574
25808
  st.authed = true;
25575
25809
  st.timer = null;
25810
+ if (ctx && this.opts.onAuthed) this.opts.onAuthed(handle, ctx);
25576
25811
  this.opts.sendOk(handle, { type: "auth:ok" });
25577
25812
  return "consumed";
25578
25813
  }
@@ -25648,6 +25883,328 @@ function constantTimeEqual2(a, b2) {
25648
25883
  return diff2 === 0;
25649
25884
  }
25650
25885
 
25886
+ // ../protocol/src/index.ts
25887
+ init_runtime();
25888
+
25889
+ // src/transport/connection-context.ts
25890
+ function ownerContext() {
25891
+ return {
25892
+ principal: OWNER_PRINCIPAL,
25893
+ grants: [{ resource: { type: "*" }, actions: ["admin"] }]
25894
+ };
25895
+ }
25896
+ function guestContext(cap) {
25897
+ return {
25898
+ principal: { id: cap.id, kind: "guest", displayName: cap.displayName },
25899
+ grants: cap.grants,
25900
+ capabilityId: cap.id
25901
+ };
25902
+ }
25903
+ function authenticate(token, deps) {
25904
+ if (!token) return { ok: false, code: "NO_TOKEN" };
25905
+ if (deps.isOwnerToken(token)) return { ok: true, context: ownerContext() };
25906
+ if (!deps.capabilityRegistry) return { ok: false, code: "BAD_TOKEN" };
25907
+ const v2 = deps.capabilityRegistry.verifyToken(token);
25908
+ if (v2.ok) return { ok: true, context: guestContext(v2.capability) };
25909
+ if (v2.code === "TOKEN_INVALID") return { ok: false, code: "BAD_TOKEN" };
25910
+ return { ok: false, code: v2.code };
25911
+ }
25912
+
25913
+ // src/permission/capability-store.ts
25914
+ var fs15 = __toESM(require("fs"), 1);
25915
+ var path18 = __toESM(require("path"), 1);
25916
+ var CAPABILITIES_FILE_NAME = "capabilities.json";
25917
+ var FILE_VERSION = 1;
25918
+ var CapabilityStore = class {
25919
+ constructor(dataDir) {
25920
+ this.dataDir = dataDir;
25921
+ fs15.mkdirSync(dataDir, { recursive: true });
25922
+ this.cache = this.readFromDisk();
25923
+ }
25924
+ dataDir;
25925
+ cache;
25926
+ list() {
25927
+ return [...this.cache];
25928
+ }
25929
+ upsert(cap) {
25930
+ const idx = this.cache.findIndex((c) => c.id === cap.id);
25931
+ if (idx >= 0) {
25932
+ this.cache[idx] = cap;
25933
+ } else {
25934
+ this.cache.push(cap);
25935
+ }
25936
+ this.flush();
25937
+ }
25938
+ remove(id) {
25939
+ const next = this.cache.filter((c) => c.id !== id);
25940
+ if (next.length === this.cache.length) return;
25941
+ this.cache = next;
25942
+ this.flush();
25943
+ }
25944
+ filePath() {
25945
+ return path18.join(this.dataDir, CAPABILITIES_FILE_NAME);
25946
+ }
25947
+ readFromDisk() {
25948
+ const file = this.filePath();
25949
+ let raw;
25950
+ try {
25951
+ raw = fs15.readFileSync(file, "utf8");
25952
+ } catch (err) {
25953
+ if (err?.code === "ENOENT") return [];
25954
+ return [];
25955
+ }
25956
+ if (!raw.trim()) return [];
25957
+ let parsed;
25958
+ try {
25959
+ parsed = JSON.parse(raw);
25960
+ } catch {
25961
+ return [];
25962
+ }
25963
+ if (!parsed || typeof parsed !== "object") return [];
25964
+ const arr = parsed.capabilities;
25965
+ if (!Array.isArray(arr)) return [];
25966
+ const out = [];
25967
+ for (const item of arr) {
25968
+ const r = CapabilitySchema.safeParse(item);
25969
+ if (r.success) out.push(r.data);
25970
+ }
25971
+ return out;
25972
+ }
25973
+ flush() {
25974
+ const content = { version: FILE_VERSION, capabilities: this.cache };
25975
+ this.atomicWrite(this.filePath(), JSON.stringify(content, null, 2));
25976
+ }
25977
+ atomicWrite(file, content) {
25978
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
25979
+ fs15.writeFileSync(tmp, content, { mode: 384 });
25980
+ fs15.renameSync(tmp, file);
25981
+ try {
25982
+ fs15.chmodSync(file, 384);
25983
+ } catch {
25984
+ }
25985
+ }
25986
+ };
25987
+
25988
+ // src/permission/capability-registry.ts
25989
+ var crypto4 = __toESM(require("crypto"), 1);
25990
+ var CapabilityRegistry = class {
25991
+ constructor(store, now = () => Date.now()) {
25992
+ this.store = store;
25993
+ this.now = now;
25994
+ for (const cap of store.list()) {
25995
+ this.bySecretHash.set(cap.secretHash, cap);
25996
+ }
25997
+ }
25998
+ store;
25999
+ now;
26000
+ // sha256(token) → Capability
26001
+ bySecretHash = /* @__PURE__ */ new Map();
26002
+ list() {
26003
+ return this.store.list();
26004
+ }
26005
+ verifyToken(token) {
26006
+ if (!token) return { ok: false, code: "TOKEN_INVALID" };
26007
+ const hash = sha256Hex(token);
26008
+ const cap = this.bySecretHash.get(hash);
26009
+ if (!cap) return { ok: false, code: "TOKEN_INVALID" };
26010
+ if (cap.revokedAt) return { ok: false, code: "TOKEN_REVOKED" };
26011
+ if (cap.expiresAt && this.now() >= cap.expiresAt) {
26012
+ return { ok: false, code: "TOKEN_EXPIRED" };
26013
+ }
26014
+ if (cap.maxUses && cap.usedCount >= cap.maxUses) {
26015
+ return { ok: false, code: "TOKEN_EXHAUSTED" };
26016
+ }
26017
+ const updated = { ...cap, usedCount: cap.usedCount + 1 };
26018
+ this.bySecretHash.set(hash, updated);
26019
+ this.store.upsert(updated);
26020
+ return { ok: true, capability: updated };
26021
+ }
26022
+ upsertCapability(cap) {
26023
+ this.bySecretHash.set(cap.secretHash, cap);
26024
+ this.store.upsert(cap);
26025
+ }
26026
+ markRevoked(id, revokedAt) {
26027
+ const current = this.store.list().find((c) => c.id === id);
26028
+ if (!current) return null;
26029
+ if (current.revokedAt) return current;
26030
+ const updated = { ...current, revokedAt };
26031
+ this.bySecretHash.set(updated.secretHash, updated);
26032
+ this.store.upsert(updated);
26033
+ return updated;
26034
+ }
26035
+ findById(id) {
26036
+ return this.store.list().find((c) => c.id === id) ?? null;
26037
+ }
26038
+ };
26039
+ function sha256Hex(s) {
26040
+ return crypto4.createHash("sha256").update(s).digest("hex");
26041
+ }
26042
+
26043
+ // src/permission/capability-manager.ts
26044
+ var crypto5 = __toESM(require("crypto"), 1);
26045
+ var CapabilityManager = class {
26046
+ constructor(registry2, hooks = {}) {
26047
+ this.registry = registry2;
26048
+ this.hooks = hooks;
26049
+ }
26050
+ registry;
26051
+ hooks;
26052
+ list() {
26053
+ return this.registry.list();
26054
+ }
26055
+ issue(args) {
26056
+ if (!args.displayName.trim()) {
26057
+ throw new Error("CapabilityManager.issue: displayName must be non-empty");
26058
+ }
26059
+ const token = (this.hooks.generateToken ?? defaultGenerateToken)();
26060
+ const id = (this.hooks.generateId ?? defaultGenerateId)();
26061
+ const now = (this.hooks.now ?? Date.now)();
26062
+ const cap = {
26063
+ id,
26064
+ secretHash: sha256Hex2(token),
26065
+ displayName: args.displayName,
26066
+ grants: args.grants,
26067
+ issuedAt: now,
26068
+ usedCount: 0,
26069
+ ...args.expiresAt !== void 0 ? { expiresAt: args.expiresAt } : {},
26070
+ ...args.maxUses !== void 0 ? { maxUses: args.maxUses } : {}
26071
+ };
26072
+ this.registry.upsertCapability(cap);
26073
+ this.hooks.onIssued?.(cap, token);
26074
+ return { token, capability: cap };
26075
+ }
26076
+ revoke(id) {
26077
+ const existing = this.registry.findById(id);
26078
+ if (!existing) return null;
26079
+ const now = (this.hooks.now ?? Date.now)();
26080
+ if (existing.revokedAt) {
26081
+ return { revokedAt: existing.revokedAt, capability: existing };
26082
+ }
26083
+ const updated = this.registry.markRevoked(id, now);
26084
+ if (!updated) return null;
26085
+ this.hooks.onRevoked?.(updated);
26086
+ return { revokedAt: now, capability: updated };
26087
+ }
26088
+ };
26089
+ function defaultGenerateToken() {
26090
+ return crypto5.randomBytes(24).toString("base64url");
26091
+ }
26092
+ function defaultGenerateId() {
26093
+ return "cap_" + crypto5.randomBytes(6).toString("base64url");
26094
+ }
26095
+ function sha256Hex2(s) {
26096
+ return crypto5.createHash("sha256").update(s).digest("hex");
26097
+ }
26098
+
26099
+ // src/permission/cleanup.ts
26100
+ var fs16 = __toESM(require("fs"), 1);
26101
+ function cleanupGuestSessionsForCapability(cap, factory) {
26102
+ const removed = [];
26103
+ for (const g2 of cap.grants) {
26104
+ if (g2.resource.type !== "persona") continue;
26105
+ const dir = factory.vmGuestRoot(g2.resource.id, cap.id);
26106
+ try {
26107
+ fs16.rmSync(dir, { recursive: true, force: true });
26108
+ removed.push(dir);
26109
+ } catch {
26110
+ }
26111
+ }
26112
+ return { removed };
26113
+ }
26114
+
26115
+ // src/migrations/2026-05-20-flatten-sessions.ts
26116
+ var fs17 = __toESM(require("fs"), 1);
26117
+ var path19 = __toESM(require("path"), 1);
26118
+ var MIGRATION_FLAG_NAME = ".migration.v1.done";
26119
+ function migrateFlattenSessions(opts) {
26120
+ const dataDir = opts.dataDir;
26121
+ const now = opts.now ?? Date.now;
26122
+ const sessionsDir = path19.join(dataDir, "sessions");
26123
+ const flagPath = path19.join(sessionsDir, MIGRATION_FLAG_NAME);
26124
+ if (existsSync3(flagPath)) {
26125
+ return { skipped: true, flagWritten: false, movedBare: 0, movedVmOwner: 0, archivedListener: 0 };
26126
+ }
26127
+ let movedBare = 0;
26128
+ let movedVmOwner = 0;
26129
+ let archivedListener = 0;
26130
+ const defaultDir = path19.join(sessionsDir, "default");
26131
+ if (existsSync3(defaultDir)) {
26132
+ for (const entry of readdirSafe(defaultDir)) {
26133
+ if (!entry.endsWith(".json")) continue;
26134
+ const src = path19.join(defaultDir, entry);
26135
+ const dst = path19.join(sessionsDir, entry);
26136
+ fs17.renameSync(src, dst);
26137
+ movedBare += 1;
26138
+ }
26139
+ rmdirIfEmpty(defaultDir);
26140
+ }
26141
+ for (const pid of readdirSafe(sessionsDir)) {
26142
+ const personaDir = path19.join(sessionsDir, pid);
26143
+ if (!isDir(personaDir)) continue;
26144
+ if (pid === "default") continue;
26145
+ const ownerSrc = path19.join(personaDir, "owner");
26146
+ if (existsSync3(ownerSrc) && isDir(ownerSrc)) {
26147
+ const ownerDst = path19.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
26148
+ fs17.mkdirSync(ownerDst, { recursive: true });
26149
+ for (const file of readdirSafe(ownerSrc)) {
26150
+ if (!file.endsWith(".json")) continue;
26151
+ fs17.renameSync(path19.join(ownerSrc, file), path19.join(ownerDst, file));
26152
+ movedVmOwner += 1;
26153
+ }
26154
+ rmdirIfEmpty(ownerSrc);
26155
+ }
26156
+ const listenerSrc = path19.join(personaDir, "listener");
26157
+ if (existsSync3(listenerSrc) && isDir(listenerSrc)) {
26158
+ const archiveDst = path19.join(dataDir, ".legacy", `listener-${pid}`);
26159
+ fs17.mkdirSync(archiveDst, { recursive: true });
26160
+ for (const file of readdirSafe(listenerSrc)) {
26161
+ if (!file.endsWith(".json")) continue;
26162
+ fs17.renameSync(path19.join(listenerSrc, file), path19.join(archiveDst, file));
26163
+ archivedListener += 1;
26164
+ }
26165
+ rmdirIfEmpty(listenerSrc);
26166
+ }
26167
+ rmdirIfEmpty(personaDir);
26168
+ }
26169
+ fs17.mkdirSync(sessionsDir, { recursive: true });
26170
+ fs17.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
26171
+ return {
26172
+ skipped: false,
26173
+ flagWritten: true,
26174
+ movedBare,
26175
+ movedVmOwner,
26176
+ archivedListener
26177
+ };
26178
+ }
26179
+ function existsSync3(p2) {
26180
+ try {
26181
+ fs17.statSync(p2);
26182
+ return true;
26183
+ } catch {
26184
+ return false;
26185
+ }
26186
+ }
26187
+ function isDir(p2) {
26188
+ try {
26189
+ return fs17.statSync(p2).isDirectory();
26190
+ } catch {
26191
+ return false;
26192
+ }
26193
+ }
26194
+ function readdirSafe(p2) {
26195
+ try {
26196
+ return fs17.readdirSync(p2);
26197
+ } catch {
26198
+ return [];
26199
+ }
26200
+ }
26201
+ function rmdirIfEmpty(p2) {
26202
+ try {
26203
+ fs17.rmdirSync(p2);
26204
+ } catch {
26205
+ }
26206
+ }
26207
+
25651
26208
  // src/transport/http-router.ts
25652
26209
  var import_node_fs14 = __toESM(require("fs"), 1);
25653
26210
  var import_node_path16 = __toESM(require("path"), 1);
@@ -26869,24 +27426,14 @@ var AUTH_FILE_NAME = "auth.json";
26869
27426
  function authFilePath(dataDir) {
26870
27427
  return import_node_path22.default.join(dataDir, AUTH_FILE_NAME);
26871
27428
  }
26872
- function loadOrCreateAuthFile(opts) {
27429
+ function loadOrCreateAuthToken(opts) {
26873
27430
  const file = authFilePath(opts.dataDir);
26874
- const generate = opts.generate ?? defaultGenerate;
26875
- const now = opts.now ?? (() => /* @__PURE__ */ new Date());
26876
27431
  const existing = readAuthFile(file);
26877
- if (existing && existing.token && existing.signSecret) {
26878
- return {
26879
- token: existing.token,
26880
- signSecret: existing.signSecret,
26881
- createdAt: existing.createdAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
26882
- };
26883
- }
26884
- const token = existing?.token || generate();
26885
- const signSecret = existing?.signSecret || generate();
26886
- const createdAt = existing?.createdAt || now().toISOString();
26887
- const next = { token, signSecret, createdAt };
26888
- writeAuthFile(file, next);
26889
- return next;
27432
+ if (existing && existing.token) return existing.token;
27433
+ const token = (opts.generate ?? defaultGenerate)();
27434
+ const now = (opts.now ?? (() => /* @__PURE__ */ new Date()))();
27435
+ writeAuthFile(file, { token, createdAt: now.toISOString() });
27436
+ return token;
26890
27437
  }
26891
27438
  function defaultGenerate() {
26892
27439
  return import_node_crypto8.default.randomBytes(32).toString("base64url");
@@ -26895,14 +27442,13 @@ function readAuthFile(file) {
26895
27442
  try {
26896
27443
  const raw = import_node_fs20.default.readFileSync(file, "utf8");
26897
27444
  const parsed = JSON.parse(raw);
26898
- if (typeof parsed?.token !== "string" || parsed.token.length === 0) {
26899
- return null;
27445
+ if (typeof parsed?.token === "string" && parsed.token.length > 0) {
27446
+ return {
27447
+ token: parsed.token,
27448
+ createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : (/* @__PURE__ */ new Date(0)).toISOString()
27449
+ };
26900
27450
  }
26901
- return {
26902
- token: parsed.token,
26903
- signSecret: typeof parsed.signSecret === "string" && parsed.signSecret.length > 0 ? parsed.signSecret : void 0,
26904
- createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : void 0
26905
- };
27451
+ return null;
26906
27452
  } catch (err) {
26907
27453
  const code = err?.code;
26908
27454
  if (code === "ENOENT") return null;
@@ -27446,6 +27992,65 @@ function buildCapabilitiesHandlers(deps) {
27446
27992
  };
27447
27993
  }
27448
27994
 
27995
+ // src/handlers/capability.ts
27996
+ init_zod();
27997
+ init_protocol();
27998
+ var IssueArgsSchema = external_exports.object({
27999
+ displayName: external_exports.string().min(1),
28000
+ grants: external_exports.array(GrantSchema),
28001
+ expiresAt: external_exports.number().int().positive().optional(),
28002
+ maxUses: external_exports.number().int().positive().optional()
28003
+ }).strict();
28004
+ var RevokeArgsSchema = external_exports.object({
28005
+ capabilityId: external_exports.string().min(1)
28006
+ }).strict();
28007
+ function buildCapabilityHandlers(deps) {
28008
+ const { manager } = deps;
28009
+ const issue = async (frame) => {
28010
+ const { type: _type, requestId: _requestId, ...rest } = frame;
28011
+ const args = IssueArgsSchema.parse(rest);
28012
+ const { token, capability } = manager.issue(args);
28013
+ return {
28014
+ response: {
28015
+ type: "capability:issued",
28016
+ token,
28017
+ capability: stripSecretHash(capability)
28018
+ }
28019
+ };
28020
+ };
28021
+ const list = async () => {
28022
+ return {
28023
+ response: {
28024
+ type: "capability:list",
28025
+ capabilities: manager.list().map(stripSecretHash)
28026
+ }
28027
+ };
28028
+ };
28029
+ const revoke = async (frame) => {
28030
+ const { type: _type, requestId: _requestId, ...rest } = frame;
28031
+ const args = RevokeArgsSchema.parse(rest);
28032
+ const result = manager.revoke(args.capabilityId);
28033
+ if (!result) {
28034
+ throw new ClawdError(
28035
+ ERROR_CODES.VALIDATION_ERROR,
28036
+ `capability not found: ${args.capabilityId}`
28037
+ );
28038
+ }
28039
+ return {
28040
+ response: {
28041
+ type: "capability:revoked",
28042
+ capabilityId: args.capabilityId,
28043
+ revokedAt: result.revokedAt
28044
+ }
28045
+ };
28046
+ };
28047
+ return {
28048
+ "capability:issue": issue,
28049
+ "capability:list": list,
28050
+ "capability:revoke": revoke
28051
+ };
28052
+ }
28053
+
27449
28054
  // src/handlers/meta.ts
27450
28055
  var import_node_os13 = __toESM(require("os"), 1);
27451
28056
  init_protocol();
@@ -27571,7 +28176,6 @@ function buildPersonaHandlers(deps) {
27571
28176
  }
27572
28177
 
27573
28178
  // src/handlers/attachment.ts
27574
- var import_node_path26 = __toESM(require("path"), 1);
27575
28179
  init_protocol();
27576
28180
  init_protocol();
27577
28181
  var DEFAULT_TTL_SECONDS = 24 * 3600;
@@ -27596,41 +28200,8 @@ function buildAttachmentHandlers(deps) {
27596
28200
  "httpBaseUrl unavailable (daemon HTTP not ready)"
27597
28201
  );
27598
28202
  }
27599
- if (!deps.sessionStore || !deps.getSessionScope || !deps.groupFileStore) {
27600
- throw new ClawdError(
27601
- ERROR_CODES.METHOD_NOT_IMPLEMENTED,
27602
- "signUrl requires session/group stores"
27603
- );
27604
- }
27605
- const sessionFile = deps.sessionStore.read(args.sessionId);
27606
- if (!sessionFile) {
27607
- throw new ClawdError(
27608
- ERROR_CODES.VALIDATION_ERROR,
27609
- `session ${args.sessionId} not found`
27610
- );
27611
- }
27612
- const scope = deps.getSessionScope(args.sessionId);
27613
- if (!scope) {
27614
- throw new ClawdError(
27615
- ERROR_CODES.VALIDATION_ERROR,
27616
- `session ${args.sessionId} scope unresolved`
27617
- );
27618
- }
27619
- const cwdAbs = import_node_path26.default.resolve(sessionFile.cwd);
27620
- const candidateAbs = import_node_path26.default.isAbsolute(args.relPath) ? import_node_path26.default.resolve(args.relPath) : import_node_path26.default.resolve(cwdAbs, args.relPath);
27621
- const entries = deps.groupFileStore.list(scope, args.sessionId);
27622
- const entry = entries.find((e) => {
27623
- const storedAbs = import_node_path26.default.isAbsolute(e.relPath) ? import_node_path26.default.resolve(e.relPath) : import_node_path26.default.resolve(cwdAbs, e.relPath);
27624
- return storedAbs === candidateAbs && !e.stale;
27625
- });
27626
- if (!entry) {
27627
- throw new ClawdError(
27628
- ERROR_CODES.VALIDATION_ERROR,
27629
- `relPath not in session group files or stale: ${args.relPath}`
27630
- );
27631
- }
27632
28203
  const ttl = args.ttlSeconds === null ? null : args.ttlSeconds ?? DEFAULT_TTL_SECONDS;
27633
- const parts = signUrlParts(secret, candidateAbs, ttl);
28204
+ const parts = signUrlParts(secret, args.absPath, ttl);
27634
28205
  const url = buildSignedFileUrl(httpBaseUrl, parts);
27635
28206
  return {
27636
28207
  response: {
@@ -27731,15 +28302,112 @@ function buildMethodHandlers(deps) {
27731
28302
  personaManager: deps.personaManager,
27732
28303
  personaRegistry: deps.personaRegistry
27733
28304
  }),
28305
+ ...buildCapabilityHandlers({ manager: deps.capabilityManager }),
27734
28306
  ...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {}
27735
28307
  };
27736
28308
  }
27737
28309
 
28310
+ // src/handlers/method-grants.ts
28311
+ var ADMIN_ANY = {
28312
+ kind: "fixed",
28313
+ resource: { type: "*" },
28314
+ action: "admin"
28315
+ };
28316
+ var METHOD_GRANT_MAP = {
28317
+ // ---- public(meta-only,guest 也能调) ----
28318
+ "info": { kind: "public" },
28319
+ "ping": { kind: "public" },
28320
+ // ---- capability platform(admin-only,本 PR 新增) ----
28321
+ "capability:issue": ADMIN_ANY,
28322
+ "capability:list": ADMIN_ANY,
28323
+ "capability:revoke": ADMIN_ANY,
28324
+ // ---- 业务方法:Phase 1 全 admin-only(owner 自动通过;guest 无法调用) ----
28325
+ "session:create": ADMIN_ANY,
28326
+ "session:list": ADMIN_ANY,
28327
+ "session:get": ADMIN_ANY,
28328
+ "session:update": ADMIN_ANY,
28329
+ "session:delete": ADMIN_ANY,
28330
+ "session:send": ADMIN_ANY,
28331
+ "session:stop": ADMIN_ANY,
28332
+ "session:interrupt": ADMIN_ANY,
28333
+ "session:rewind": ADMIN_ANY,
28334
+ "session:rewind-diff": ADMIN_ANY,
28335
+ "session:rewindable-message-ids": ADMIN_ANY,
28336
+ "session:fork": ADMIN_ANY,
28337
+ "session:new": ADMIN_ANY,
28338
+ "session:resume": ADMIN_ANY,
28339
+ "session:observe": ADMIN_ANY,
28340
+ "session:events": ADMIN_ANY,
28341
+ "session:subscribe": ADMIN_ANY,
28342
+ "session:unsubscribe": ADMIN_ANY,
28343
+ "session:pin": ADMIN_ANY,
28344
+ "session:reorderPins": ADMIN_ANY,
28345
+ "permission:respond": ADMIN_ANY,
28346
+ "session:answerQuestion": ADMIN_ANY,
28347
+ "session:cancelQuestion": ADMIN_ANY,
28348
+ "history:projects": ADMIN_ANY,
28349
+ "history:list": ADMIN_ANY,
28350
+ "history:read": ADMIN_ANY,
28351
+ "history:subagents": ADMIN_ANY,
28352
+ "history:subagent-read": ADMIN_ANY,
28353
+ "history:recentDirs": ADMIN_ANY,
28354
+ "workspace:list": ADMIN_ANY,
28355
+ "workspace:read": ADMIN_ANY,
28356
+ "skills:list": ADMIN_ANY,
28357
+ "agents:list": ADMIN_ANY,
28358
+ "git:root": ADMIN_ANY,
28359
+ "git:branch": ADMIN_ANY,
28360
+ "git:branches": ADMIN_ANY,
28361
+ "capabilities:get": ADMIN_ANY,
28362
+ "persona:create": ADMIN_ANY,
28363
+ "persona:list": ADMIN_ANY,
28364
+ "persona:get": ADMIN_ANY,
28365
+ "persona:update": ADMIN_ANY,
28366
+ "persona:delete": ADMIN_ANY,
28367
+ "persona:issueToken": ADMIN_ANY,
28368
+ "persona:revokeToken": ADMIN_ANY,
28369
+ "session:pty:input": ADMIN_ANY,
28370
+ "session:pty:resize": ADMIN_ANY,
28371
+ // file-sharing attachment.*:handler 内部已有 requireOwner(HTTP 路径同),dispatcher 这里
28372
+ // 用 admin-only 兜底(双保险,wire-level 也拦)
28373
+ "attachment.signUrl": ADMIN_ANY,
28374
+ "attachment.groupAdd": ADMIN_ANY,
28375
+ "attachment.groupRemove": ADMIN_ANY,
28376
+ "attachment.groupList": ADMIN_ANY,
28377
+ "attachment.groupListPersona": ADMIN_ANY
28378
+ };
28379
+ function computeGrantForFrame(method, frame) {
28380
+ const rule = METHOD_GRANT_MAP[method];
28381
+ if (!rule) return { kind: "public" };
28382
+ if (rule.kind === "public") return { kind: "public" };
28383
+ if (rule.kind === "fixed") {
28384
+ return { kind: "check", resource: rule.resource, action: rule.action };
28385
+ }
28386
+ const picked = rule.pick(frame);
28387
+ if (!picked) return { kind: "public" };
28388
+ return { kind: "check", resource: picked.resource, action: picked.action };
28389
+ }
28390
+
28391
+ // src/permission/capability.ts
28392
+ function matchResource(grant, target) {
28393
+ if (grant.type === "*") return true;
28394
+ if (grant.type !== target.type) return false;
28395
+ return grant.id === target.id;
28396
+ }
28397
+ function assertGrant(grants, resource, action) {
28398
+ for (const g2 of grants) {
28399
+ if (!matchResource(g2.resource, resource)) continue;
28400
+ if (g2.actions.includes(action)) return true;
28401
+ if (g2.actions.includes("admin")) return true;
28402
+ }
28403
+ return false;
28404
+ }
28405
+
27738
28406
  // src/index.ts
27739
28407
  async function startDaemon(config) {
27740
28408
  const logger = createLogger({
27741
28409
  level: config.logLevel,
27742
- file: import_node_path27.default.join(config.dataDir, "clawd.log")
28410
+ file: import_node_path26.default.join(config.dataDir, "clawd.log")
27743
28411
  });
27744
28412
  logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
27745
28413
  const stateMgr = new StateFileManager({ dataDir: config.dataDir });
@@ -27751,29 +28419,70 @@ async function startDaemon(config) {
27751
28419
  logger.warn("stale state file detected, overwriting", { pid: pre.existing.pid });
27752
28420
  }
27753
28421
  let resolvedAuthToken = null;
27754
- let authFile = null;
27755
28422
  if (config.authToken && config.authToken.trim()) {
27756
28423
  resolvedAuthToken = config.authToken.trim();
27757
28424
  } else if (config.tunnel) {
27758
- authFile = loadOrCreateAuthFile({ dataDir: config.dataDir });
27759
- resolvedAuthToken = authFile.token;
28425
+ resolvedAuthToken = loadOrCreateAuthToken({ dataDir: config.dataDir });
27760
28426
  }
27761
28427
  const authMode = resolvedAuthToken == null ? "none" : "first-message";
28428
+ const capabilityStore = new CapabilityStore(config.dataDir);
28429
+ const capabilityRegistry = new CapabilityRegistry(capabilityStore);
28430
+ const capabilityManager = new CapabilityManager(capabilityRegistry, {
28431
+ onIssued: (cap, token) => {
28432
+ wsServer?.broadcastToOwners({
28433
+ type: "capability:tokenIssued",
28434
+ capability: stripSecretHash(cap),
28435
+ token
28436
+ });
28437
+ },
28438
+ onRevoked: (cap) => {
28439
+ wsServer?.broadcastToOwners({
28440
+ type: "capability:tokenRevoked",
28441
+ capabilityId: cap.id,
28442
+ revokedAt: cap.revokedAt
28443
+ });
28444
+ wsServer?.closeConnectionsByCapability(cap.id);
28445
+ const cleanup = cleanupGuestSessionsForCapability(cap, sessionStoreFactory);
28446
+ if (cleanup.removed.length > 0) {
28447
+ logger.info("capability revoke cascade: guest sessions removed", {
28448
+ capabilityId: cap.id,
28449
+ removedDirs: cleanup.removed
28450
+ });
28451
+ }
28452
+ }
28453
+ });
27762
28454
  let wsServer = null;
27763
28455
  const authGate = authMode === "first-message" ? new AuthGate({
27764
28456
  shouldEnforce: buildShouldEnforce({ tunnel: config.tunnel }),
28457
+ // Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
28458
+ // owner 路径 constantTimeEqual 防侧信道;guest 路径走 capabilityRegistry.
27765
28459
  expectedToken: resolvedAuthToken,
28460
+ authenticate: (t) => authenticate(t, {
28461
+ isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
28462
+ capabilityRegistry
28463
+ }),
28464
+ onAuthed: (h, ctx) => wsServer?.attachClientContext(h.id, ctx),
28465
+ buildOwnerContext: ownerContext,
27766
28466
  closeConnection: (h, code, reason) => wsServer?.closeClient(h.id, code, reason),
27767
28467
  sendOk: (h, payload) => wsServer?.sendToClient(h.id, payload)
27768
28468
  }) : null;
27769
28469
  resetRegistry();
27770
- const store = new SessionStore({ dataDir: config.dataDir });
28470
+ const migrateResult = migrateFlattenSessions({ dataDir: config.dataDir });
28471
+ if (!migrateResult.skipped && (migrateResult.movedBare || migrateResult.movedVmOwner || migrateResult.archivedListener)) {
28472
+ logger.info("sessions migration applied", {
28473
+ movedBare: migrateResult.movedBare,
28474
+ movedVmOwner: migrateResult.movedVmOwner,
28475
+ archivedListener: migrateResult.archivedListener
28476
+ });
28477
+ }
28478
+ const sessionStoreFactory = new SessionStoreFactory({ dataDir: config.dataDir });
28479
+ const store = sessionStoreFactory.forBare();
27771
28480
  const workspace = new WorkspaceBrowser();
27772
28481
  const skills = new SkillsScanner();
27773
28482
  const agents = new AgentsScanner();
27774
28483
  const history = new ClaudeHistoryReader();
27775
28484
  let transport = null;
27776
- const personaStore = new PersonaStore(import_node_path27.default.join(config.dataDir, "personas"));
28485
+ const personaStore = new PersonaStore(import_node_path26.default.join(config.dataDir, "personas"));
27777
28486
  const defaultsRoot = findDefaultsRoot();
27778
28487
  if (defaultsRoot) {
27779
28488
  seedDefaultPersonas({ store: personaStore, defaultsRoot, logger });
@@ -27784,11 +28493,14 @@ async function startDaemon(config) {
27784
28493
  const groupFileStore = new GroupFileStore({ dataDir: config.dataDir, logger });
27785
28494
  const manager = new SessionManager({
27786
28495
  store,
28496
+ // Phase 2 (capability platform plan §1): factory 注入后 manager.storeFor 走
28497
+ // 新布局派生 (sessions/* + personas/<pid>/.clawd/sessions/owner/*)
28498
+ storeFactory: sessionStoreFactory,
27787
28499
  logger,
27788
28500
  getAdapter,
27789
28501
  historyReader: history,
27790
28502
  dataDir: config.dataDir,
27791
- personaRoot: import_node_path27.default.join(config.dataDir, "personas"),
28503
+ personaRoot: import_node_path26.default.join(config.dataDir, "personas"),
27792
28504
  personaStore,
27793
28505
  ownerDisplayName,
27794
28506
  mode: config.mode,
@@ -27811,7 +28523,7 @@ async function startDaemon(config) {
27811
28523
  // 文件可能 agent 写完又被自己删(罕见),用 size=0 / fallback mime 兜底。
27812
28524
  attachmentGroup: {
27813
28525
  onFileEdit: (input) => {
27814
- const absPath = import_node_path27.default.isAbsolute(input.relPath) ? input.relPath : import_node_path27.default.join(input.cwd, input.relPath);
28526
+ const absPath = import_node_path26.default.isAbsolute(input.relPath) ? input.relPath : import_node_path26.default.join(input.cwd, input.relPath);
27815
28527
  let size = 0;
27816
28528
  try {
27817
28529
  size = import_node_fs24.default.statSync(absPath).size;
@@ -27910,23 +28622,25 @@ async function startDaemon(config) {
27910
28622
  httpToken: resolvedAuthToken,
27911
28623
  // file-sharing attachment.* RPC。signUrl 用 owner token 做 HMAC secret;group RPC
27912
28624
  // 根据 sessionId 反查 scope 写盘。
27913
- //
27914
- // sessionStore / getSessionScope 都走 manager 的跨 scope 公开 API(findOwnedSession /
27915
- // findOwnedSessionScope)—— 否则 default-only SessionStore 找不到 persona owner-mode
27916
- // session,attachment 整套 RPC 对 persona session 都会报 "session not found"。
27917
- // 详见 fix(daemon) #703:file-sharing v1 预存 wiring bug,#701 把 desktop 默认 --tunnel
27918
- // 后首次让 desktop 用户用上 file-sharing 才被暴露。
27919
28625
  attachment: {
27920
28626
  groupFileStore,
27921
- sessionStore: { read: (sid) => manager.findOwnedSession(sid) },
27922
28627
  getHttpBaseUrl,
27923
- // HMAC sign secret:~/.clawd/auth.json signSecret 字段(与 WS Bearer token 独立)。
27924
- // --auth-token CLI 模式 / noAuth 模式 authFile 为 null → handler 自己返 NOT_IMPLEMENTED。
27925
- getSignSecret: () => authFile?.signSecret ?? "",
27926
- // group RPC + sign 都用:根据 sessionId 反查 scopeowner-mode persona session 走
28628
+ // HMAC sign secret:复用 ~/.clawd/auth.json owner token(持久跨重启)。
28629
+ // noAuth 模式 resolvedAuthToken 为 null → handler 自己返 NOT_IMPLEMENTED。
28630
+ getSignSecret: () => resolvedAuthToken ?? "",
28631
+ // group RPC:根据 sessionId 反查 scopeowner-mode persona session 走
27927
28632
  // 'persona/<pid>/owner',default 走 'default'。
27928
- getSessionScope: (sid) => manager.findOwnedSessionScope(sid)
27929
- }
28633
+ getSessionScope: (sessionId) => {
28634
+ const file = store.read(sessionId);
28635
+ if (!file) return null;
28636
+ if (file.ownerPersonaId) {
28637
+ return { kind: "persona", personaId: file.ownerPersonaId, mode: "owner" };
28638
+ }
28639
+ return { kind: "default" };
28640
+ }
28641
+ },
28642
+ // Task 1.9: capability:issue/list/revoke handler 依赖
28643
+ capabilityManager
27930
28644
  });
27931
28645
  const authResolver = new AuthContextResolver({
27932
28646
  ownerToken: resolvedAuthToken,
@@ -27939,9 +28653,8 @@ async function startDaemon(config) {
27939
28653
  personaStore,
27940
28654
  groupFileStore,
27941
28655
  sessionStore: store,
27942
- // /files HMAC verify auth.json signSecret 字段(与 attachment.signUrl 同源)。
27943
- // --auth-token CLI 模式没 signSecret → 路由返 501,sign URL 功能整体禁用。
27944
- getSignSecret: () => authFile?.signSecret ?? null
28656
+ // /files HMAC verify 用同一份 owner token secret(与 attachment.signUrl 同源)
28657
+ getSignSecret: () => resolvedAuthToken ?? null
27945
28658
  });
27946
28659
  wsServer = new LocalWsServer({
27947
28660
  host: config.host,
@@ -28014,7 +28727,18 @@ async function startDaemon(config) {
28014
28727
  const requestId = typeof frame.requestId === "string" ? frame.requestId : void 0;
28015
28728
  const handler = handlers[type];
28016
28729
  if (!handler) throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, `not implemented: ${type}`);
28017
- const result = await handler(frame, client);
28730
+ const ctx = wsServer.getClientContext(client.id) ?? ownerContext();
28731
+ const verdict = computeGrantForFrame(type, frame);
28732
+ if (verdict.kind === "check") {
28733
+ const ok = assertGrant(ctx.grants, verdict.resource, verdict.action);
28734
+ if (!ok) {
28735
+ throw new ClawdError(
28736
+ ERROR_CODES.UNAUTHORIZED,
28737
+ `principal ${ctx.principal.kind}:${ctx.principal.id} cannot ${verdict.action} on ${verdict.resource.type}${"id" in verdict.resource ? ":" + verdict.resource.id : ""}`
28738
+ );
28739
+ }
28740
+ }
28741
+ const result = await handler(frame, client, ctx);
28018
28742
  if (requestId && result.response) {
28019
28743
  client.send({ ...result.response, requestId });
28020
28744
  }
@@ -28072,15 +28796,15 @@ async function startDaemon(config) {
28072
28796
  });
28073
28797
  try {
28074
28798
  const r = await tunnelMgr.start({ localPort: config.port });
28075
- stateSnapshot = { ...stateSnapshot, tunnelUrl: r.url, tunnelError: void 0 };
28799
+ stateSnapshot = { ...stateSnapshot, tunnelUrl: r.url };
28076
28800
  stateMgr.write(stateSnapshot);
28077
28801
  currentTunnelUrl = r.url;
28078
28802
  const connectUrl = resolvedAuthToken ? `${r.url}#token=${resolvedAuthToken}` : r.url;
28079
28803
  const lines = [
28080
28804
  `Tunnel: ${r.url}`,
28081
28805
  ...resolvedAuthToken ? [`Connect: ${connectUrl}`] : [],
28082
- `Frpc config: ${import_node_path27.default.join(config.dataDir, "frpc.toml")}`,
28083
- `Frpc log: ${import_node_path27.default.join(config.dataDir, "frpc.log")}`
28806
+ `Frpc config: ${import_node_path26.default.join(config.dataDir, "frpc.toml")}`,
28807
+ `Frpc log: ${import_node_path26.default.join(config.dataDir, "frpc.log")}`
28084
28808
  ];
28085
28809
  const width = Math.max(...lines.map((l) => l.length));
28086
28810
  const bar = "\u2550".repeat(width + 4);
@@ -28093,30 +28817,19 @@ ${bar}
28093
28817
 
28094
28818
  `);
28095
28819
  try {
28096
- const connectPath = import_node_path27.default.join(config.dataDir, "connect.txt");
28820
+ const connectPath = import_node_path26.default.join(config.dataDir, "connect.txt");
28097
28821
  import_node_fs24.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
28098
28822
  } catch {
28099
28823
  }
28100
28824
  } catch (err) {
28101
- const tunnelError = err?.message ?? String(err);
28102
28825
  try {
28103
28826
  await tunnelMgr.stop();
28104
28827
  } catch {
28105
28828
  }
28106
28829
  tunnelMgr = null;
28107
- stateSnapshot = { ...stateSnapshot, tunnelUrl: void 0, tunnelError };
28108
- try {
28109
- stateMgr.write(stateSnapshot);
28110
- } catch {
28111
- }
28112
- wss.broadcastAll({
28113
- type: "tunnel:unavailable",
28114
- reason: tunnelError,
28115
- failedAt: (/* @__PURE__ */ new Date()).toISOString()
28116
- });
28117
- process.stdout.write(`Tunnel: unavailable (local mode) \u2014 ${tunnelError}
28118
- `);
28119
- logger.warn("tunnel unavailable, degraded to local mode", { reason: tunnelError });
28830
+ stateMgr.delete();
28831
+ await wss.stop();
28832
+ throw err;
28120
28833
  }
28121
28834
  }
28122
28835
  const shutdown = async () => {