@clawos-dev/clawd 0.2.70 → 0.2.71-beta.123.6a5ed6f

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 +667 -202
  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
  ];
@@ -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
 
@@ -6247,7 +6312,7 @@ 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 fs29 = require("fs");
6251
6316
  var EventEmitter2 = require("events");
6252
6317
  var inherits = require("util").inherits;
6253
6318
  var path32 = require("path");
@@ -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) fs29.mkdirSync(path32.dirname(file), { recursive: true });
6373
+ const fd = fs29.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
+ fs29.mkdir(path32.dirname(file), { recursive: true }, (err) => {
6316
6381
  if (err) return fileOpened(err);
6317
- fs28.open(file, flags, mode, fileOpened);
6382
+ fs29.open(file, flags, mode, fileOpened);
6318
6383
  });
6319
6384
  } else {
6320
- fs28.open(file, flags, mode, fileOpened);
6385
+ fs29.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 = () => fs29.writeSync(this.fd, this._writingBuf);
6427
+ fsWrite = () => fs29.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 fs29.writeSync(this.fd, this._writingBuf);
6372
6437
  }
6373
- return fs28.writeSync(this.fd, this._writingBuf, "utf8");
6438
+ return fs29.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 fs29.write(this.fd, this._writingBuf, this.release);
6378
6443
  }
6379
- return fs28.write(this.fd, this._writingBuf, "utf8", this.release);
6444
+ return fs29.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
+ fs29.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
+ fs29.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
+ fs29.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) ? fs29.writeSync(this.fd, buf) : fs29.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
+ fs29.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 = fs29.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) ? fs29.writeSync(this.fd, this._writingBuf) : fs29.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
+ fs29.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 = fs29.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
+ fs29.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
+ fs29.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
+ fs29.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: join5 } = 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"] || join5(__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: join5, isAbsolute, sep } = 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"] || join5(__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"] || join5(__dirname, "worker.js");
7572
7637
  options.pipelines = [pipeline2.map((dest) => {
7573
7638
  return {
7574
7639
  ...dest,
@@ -7590,7 +7655,7 @@ var require_transport = __commonJS({
7590
7655
  return origin;
7591
7656
  }
7592
7657
  if (origin === "pino/file") {
7593
- return join4(__dirname, "..", "file.js");
7658
+ return join5(__dirname, "..", "file.js");
7594
7659
  }
7595
7660
  let fixTarget2;
7596
7661
  for (const filePath of callers) {
@@ -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 join5 = ",";
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
+ join5 = `,
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 += join5;
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 += `${join5}"... ${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
+ join5 = `,
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 = join5;
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 = join5;
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 join5 = ",";
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
+ join5 = `,
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 += join5;
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 += `${join5}"... ${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
+ join5 = `,
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 = join5;
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 join6 = `,
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 += join6;
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 += `${join6}"... ${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 join5 = `,
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, join5, maximumBreadth);
8836
8901
  keys = keys.slice(value.length);
8837
8902
  maximumPropertiesToStringify -= value.length;
8838
- separator = join4;
8903
+ separator = join5;
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 = join5;
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 = join5;
8856
8921
  }
8857
8922
  if (separator !== "") {
8858
8923
  res = `
@@ -18696,7 +18761,7 @@ var require_websocket = __commonJS({
18696
18761
  var http2 = require("http");
18697
18762
  var net = require("net");
18698
18763
  var tls = require("tls");
18699
- var { randomBytes, createHash } = require("crypto");
18764
+ var { randomBytes: randomBytes2, createHash: createHash3 } = require("crypto");
18700
18765
  var { Duplex, Readable: Readable3 } = require("stream");
18701
18766
  var { URL: URL2 } = require("url");
18702
18767
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -19226,7 +19291,7 @@ var require_websocket = __commonJS({
19226
19291
  }
19227
19292
  }
19228
19293
  const defaultPort = isSecure ? 443 : 80;
19229
- const key = randomBytes(16).toString("base64");
19294
+ const key = randomBytes2(16).toString("base64");
19230
19295
  const request = isSecure ? https.request : http2.request;
19231
19296
  const protocolSet = /* @__PURE__ */ new Set();
19232
19297
  let perMessageDeflate;
@@ -19356,7 +19421,7 @@ var require_websocket = __commonJS({
19356
19421
  abortHandshake(websocket, socket, "Invalid Upgrade header");
19357
19422
  return;
19358
19423
  }
19359
- const digest = createHash("sha1").update(key + GUID).digest("base64");
19424
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
19360
19425
  if (res.headers["sec-websocket-accept"] !== digest) {
19361
19426
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
19362
19427
  return;
@@ -19723,7 +19788,7 @@ var require_websocket_server = __commonJS({
19723
19788
  var EventEmitter2 = require("events");
19724
19789
  var http2 = require("http");
19725
19790
  var { Duplex } = require("stream");
19726
- var { createHash } = require("crypto");
19791
+ var { createHash: createHash3 } = require("crypto");
19727
19792
  var extension2 = require_extension();
19728
19793
  var PerMessageDeflate2 = require_permessage_deflate();
19729
19794
  var subprotocol2 = require_subprotocol();
@@ -20024,7 +20089,7 @@ var require_websocket_server = __commonJS({
20024
20089
  );
20025
20090
  }
20026
20091
  if (this._state > RUNNING) return abortHandshake(socket, 503);
20027
- const digest = createHash("sha1").update(key + GUID).digest("base64");
20092
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
20028
20093
  const headers = [
20029
20094
  "HTTP/1.1 101 Switching Protocols",
20030
20095
  "Upgrade: websocket",
@@ -20112,7 +20177,7 @@ var require_websocket_server = __commonJS({
20112
20177
  // src/run-case/recorder.ts
20113
20178
  function startRunCaseRecorder(opts) {
20114
20179
  const now = opts.now ?? Date.now;
20115
- const dir = import_node_path28.default.dirname(opts.recordPath);
20180
+ const dir = import_node_path27.default.dirname(opts.recordPath);
20116
20181
  let stream = null;
20117
20182
  let closing = false;
20118
20183
  let closedSettled = false;
@@ -20152,12 +20217,12 @@ function startRunCaseRecorder(opts) {
20152
20217
  };
20153
20218
  return { tap, close, closed };
20154
20219
  }
20155
- var import_node_fs25, import_node_path28;
20220
+ var import_node_fs25, import_node_path27;
20156
20221
  var init_recorder = __esm({
20157
20222
  "src/run-case/recorder.ts"() {
20158
20223
  "use strict";
20159
20224
  import_node_fs25 = __toESM(require("fs"), 1);
20160
- import_node_path28 = __toESM(require("path"), 1);
20225
+ import_node_path27 = __toESM(require("path"), 1);
20161
20226
  }
20162
20227
  });
20163
20228
 
@@ -20200,7 +20265,7 @@ var init_wire = __esm({
20200
20265
  // src/run-case/controller.ts
20201
20266
  async function runController(opts) {
20202
20267
  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-"));
20268
+ const cwd = opts.cwd ?? (0, import_node_fs26.mkdtempSync)(import_node_path28.default.join(import_node_os14.default.tmpdir(), "clawd-runcase-"));
20204
20269
  const ownsCwd = opts.cwd === void 0;
20205
20270
  const recorder = startRunCaseRecorder({ recordPath: opts.record, now });
20206
20271
  const spawnCtx = { cwd };
@@ -20367,13 +20432,13 @@ async function runController(opts) {
20367
20432
  }
20368
20433
  return exitCode ?? 0;
20369
20434
  }
20370
- var import_node_fs26, import_node_os14, import_node_path29;
20435
+ var import_node_fs26, import_node_os14, import_node_path28;
20371
20436
  var init_controller = __esm({
20372
20437
  "src/run-case/controller.ts"() {
20373
20438
  "use strict";
20374
20439
  import_node_fs26 = require("fs");
20375
20440
  import_node_os14 = __toESM(require("os"), 1);
20376
- import_node_path29 = __toESM(require("path"), 1);
20441
+ import_node_path28 = __toESM(require("path"), 1);
20377
20442
  init_claude();
20378
20443
  init_stdout_splitter();
20379
20444
  init_permission_stdio();
@@ -20605,7 +20670,7 @@ Env (advanced):
20605
20670
  `;
20606
20671
 
20607
20672
  // src/index.ts
20608
- var import_node_path27 = __toESM(require("path"), 1);
20673
+ var import_node_path26 = __toESM(require("path"), 1);
20609
20674
  var import_node_fs24 = __toESM(require("fs"), 1);
20610
20675
 
20611
20676
  // src/logger.ts
@@ -22106,19 +22171,9 @@ var SessionManager = class {
22106
22171
  return entries.filter((e) => e.isDirectory() && e.name !== "default").map((e) => e.name);
22107
22172
  }
22108
22173
  // 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 命中
22174
+ // SessionManager 在这里扫盘定位(default 直接试,未命中再轮询所有 persona owner 目录)。
22115
22175
  // 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
22176
  findOwnedSession(sessionId) {
22120
- const runner = this.runners.get(sessionId);
22121
- if (runner) return runner.getState().file;
22122
22177
  const dflt = this.deps.store.read(sessionId);
22123
22178
  if (dflt) return dflt;
22124
22179
  for (const personaId of this.listPersonaIdsOnDisk()) {
@@ -22128,13 +22183,6 @@ var SessionManager = class {
22128
22183
  }
22129
22184
  return null;
22130
22185
  }
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
22186
  // 合并 default + 所有 persona owner 的 SessionFile —— 桌面 App 主 session 列表入口。
22139
22187
  // 同样不含 listener sub-session(listener 走 persona:listSubSessions RPC 单独入口)。
22140
22188
  listAllOwned() {
@@ -25240,6 +25288,9 @@ var LocalWsServer = class {
25240
25288
  httpServer = null;
25241
25289
  frameHandler = null;
25242
25290
  clients = /* @__PURE__ */ new Map();
25291
+ // Task 1.7 capability platform:capId → Set<clientId>,撤销时 O(1) 找到该 cap 的
25292
+ // 所有活跃连接做关闭级联(Task 1.10)。同一 cap 可能多端同时连(多设备 / 多 tab)。
25293
+ capabilityIdToClients = /* @__PURE__ */ new Map();
25243
25294
  logger;
25244
25295
  pingIntervalMs;
25245
25296
  async start() {
@@ -25330,6 +25381,17 @@ var LocalWsServer = class {
25330
25381
  this.safeSend(c.ws, frame);
25331
25382
  }
25332
25383
  }
25384
+ // Task 1.9 capability platform:仅广播给 owner 连接(跳过 guest ws)。
25385
+ // 用于 capability:tokenIssued / capability:revoked 等 owner-only push frame——
25386
+ // guest 没必要也无法消费这类管理帧。noAuth localhost ws 没有 ctx, 视作 owner.
25387
+ broadcastToOwners(frame) {
25388
+ const gate = this.opts.authGate;
25389
+ for (const c of this.clients.values()) {
25390
+ if (gate && !gate.isAuthed(c.handle.id)) continue;
25391
+ if (c.ctx && c.ctx.principal.kind !== "owner") continue;
25392
+ this.safeSend(c.ws, frame);
25393
+ }
25394
+ }
25333
25395
  firstSubscriber(sessionId) {
25334
25396
  for (const c of this.clients.values()) {
25335
25397
  if (c.handle.subscribedSessions.has(sessionId)) return c.handle;
@@ -25351,6 +25413,40 @@ var LocalWsServer = class {
25351
25413
  if (!c) return;
25352
25414
  this.safeSend(c.ws, frame);
25353
25415
  }
25416
+ // Task 1.7 capability platform:AuthGate.onAuthed 回调里调本方法,把 ConnectionContext
25417
+ // 绑到 client;guest 连接同时进 capId 索引(Task 1.10 撤销级联用)。
25418
+ attachClientContext(clientId, ctx) {
25419
+ const c = this.clients.get(clientId);
25420
+ if (!c) return;
25421
+ c.ctx = ctx;
25422
+ if (ctx.capabilityId) {
25423
+ let set = this.capabilityIdToClients.get(ctx.capabilityId);
25424
+ if (!set) {
25425
+ set = /* @__PURE__ */ new Set();
25426
+ this.capabilityIdToClients.set(ctx.capabilityId, set);
25427
+ }
25428
+ set.add(clientId);
25429
+ }
25430
+ }
25431
+ // 测试 / Task 1.8 dispatcher 用:拿 client 当前 ConnectionContext(null = 未鉴权 / noAuth)
25432
+ getClientContext(clientId) {
25433
+ return this.clients.get(clientId)?.ctx ?? null;
25434
+ }
25435
+ // Task 1.10 撤销级联:关闭某 capability 的所有活跃 ws 连接。close code 4401
25436
+ // 让客户端识别 'capability revoked' 而非普通断连。
25437
+ closeConnectionsByCapability(capabilityId, code = 4401, reason = "TOKEN_REVOKED") {
25438
+ const set = this.capabilityIdToClients.get(capabilityId);
25439
+ if (!set) return;
25440
+ const ids = [...set];
25441
+ for (const id of ids) {
25442
+ const c = this.clients.get(id);
25443
+ if (!c) continue;
25444
+ try {
25445
+ c.ws.close(code, reason);
25446
+ } catch {
25447
+ }
25448
+ }
25449
+ }
25354
25450
  // URL path 路由:'/' → 主连接路径;其他 → close 4404
25355
25451
  routeConnection(socket, req) {
25356
25452
  const remoteAddress = req?.socket?.remoteAddress;
@@ -25385,7 +25481,7 @@ var LocalWsServer = class {
25385
25481
  } catch {
25386
25482
  }
25387
25483
  }, this.pingIntervalMs);
25388
- this.clients.set(id, { handle, ws: socket, pingTimer });
25484
+ this.clients.set(id, { handle, ws: socket, pingTimer, ctx: null });
25389
25485
  this.logger?.info("client connected", { clientId: id, total: this.clients.size, remoteAddress });
25390
25486
  const authGate = this.opts.authGate;
25391
25487
  let authed = true;
@@ -25409,6 +25505,12 @@ var LocalWsServer = class {
25409
25505
  socket.on("close", () => {
25410
25506
  const c = this.clients.get(id);
25411
25507
  if (c?.pingTimer) clearInterval(c.pingTimer);
25508
+ const capId = c?.ctx?.capabilityId;
25509
+ if (capId) {
25510
+ const set = this.capabilityIdToClients.get(capId);
25511
+ set?.delete(id);
25512
+ if (set && set.size === 0) this.capabilityIdToClients.delete(capId);
25513
+ }
25412
25514
  this.clients.delete(id);
25413
25515
  authGate?.unregister(id);
25414
25516
  this.logger?.info("client disconnected", { clientId: id, remaining: this.clients.size });
@@ -25557,15 +25659,27 @@ var AuthGate = class {
25557
25659
  this.markFailed(handle.id);
25558
25660
  return frame.type === "auth" ? "consumed" : "reject";
25559
25661
  }
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";
25662
+ let ctx = null;
25663
+ if (this.opts.authenticate) {
25664
+ const r = this.opts.authenticate(parsed.data.token);
25665
+ if (!r.ok) {
25666
+ this.opts.closeConnection(handle, 4401, r.code);
25667
+ this.markFailed(handle.id);
25668
+ return "consumed";
25669
+ }
25670
+ ctx = r.context;
25671
+ } else {
25672
+ if (!this.opts.expectedToken) {
25673
+ this.opts.closeConnection(handle, 1008, "auth not configured");
25674
+ this.markFailed(handle.id);
25675
+ return "consumed";
25676
+ }
25677
+ if (!constantTimeEqual(parsed.data.token, this.opts.expectedToken)) {
25678
+ this.opts.closeConnection(handle, 1008, "auth failed");
25679
+ this.markFailed(handle.id);
25680
+ return "consumed";
25681
+ }
25682
+ ctx = this.opts.buildOwnerContext?.() ?? null;
25569
25683
  }
25570
25684
  if (st.timer != null) {
25571
25685
  const clear = this.opts.clearTimer ?? ((h) => clearTimeout(h));
@@ -25573,6 +25687,7 @@ var AuthGate = class {
25573
25687
  }
25574
25688
  st.authed = true;
25575
25689
  st.timer = null;
25690
+ if (ctx && this.opts.onAuthed) this.opts.onAuthed(handle, ctx);
25576
25691
  this.opts.sendOk(handle, { type: "auth:ok" });
25577
25692
  return "consumed";
25578
25693
  }
@@ -25648,6 +25763,219 @@ function constantTimeEqual2(a, b2) {
25648
25763
  return diff2 === 0;
25649
25764
  }
25650
25765
 
25766
+ // ../protocol/src/index.ts
25767
+ init_runtime();
25768
+
25769
+ // src/transport/connection-context.ts
25770
+ function ownerContext() {
25771
+ return {
25772
+ principal: OWNER_PRINCIPAL,
25773
+ grants: [{ resource: { type: "*" }, actions: ["admin"] }]
25774
+ };
25775
+ }
25776
+ function guestContext(cap) {
25777
+ return {
25778
+ principal: { id: cap.id, kind: "guest", displayName: cap.displayName },
25779
+ grants: cap.grants,
25780
+ capabilityId: cap.id
25781
+ };
25782
+ }
25783
+ function authenticate(token, deps) {
25784
+ if (!token) return { ok: false, code: "NO_TOKEN" };
25785
+ if (deps.isOwnerToken(token)) return { ok: true, context: ownerContext() };
25786
+ if (!deps.capabilityRegistry) return { ok: false, code: "BAD_TOKEN" };
25787
+ const v2 = deps.capabilityRegistry.verifyToken(token);
25788
+ if (v2.ok) return { ok: true, context: guestContext(v2.capability) };
25789
+ if (v2.code === "TOKEN_INVALID") return { ok: false, code: "BAD_TOKEN" };
25790
+ return { ok: false, code: v2.code };
25791
+ }
25792
+
25793
+ // src/permission/capability-store.ts
25794
+ var fs15 = __toESM(require("fs"), 1);
25795
+ var path16 = __toESM(require("path"), 1);
25796
+ var CAPABILITIES_FILE_NAME = "capabilities.json";
25797
+ var FILE_VERSION = 1;
25798
+ var CapabilityStore = class {
25799
+ constructor(dataDir) {
25800
+ this.dataDir = dataDir;
25801
+ fs15.mkdirSync(dataDir, { recursive: true });
25802
+ this.cache = this.readFromDisk();
25803
+ }
25804
+ dataDir;
25805
+ cache;
25806
+ list() {
25807
+ return [...this.cache];
25808
+ }
25809
+ upsert(cap) {
25810
+ const idx = this.cache.findIndex((c) => c.id === cap.id);
25811
+ if (idx >= 0) {
25812
+ this.cache[idx] = cap;
25813
+ } else {
25814
+ this.cache.push(cap);
25815
+ }
25816
+ this.flush();
25817
+ }
25818
+ remove(id) {
25819
+ const next = this.cache.filter((c) => c.id !== id);
25820
+ if (next.length === this.cache.length) return;
25821
+ this.cache = next;
25822
+ this.flush();
25823
+ }
25824
+ filePath() {
25825
+ return path16.join(this.dataDir, CAPABILITIES_FILE_NAME);
25826
+ }
25827
+ readFromDisk() {
25828
+ const file = this.filePath();
25829
+ let raw;
25830
+ try {
25831
+ raw = fs15.readFileSync(file, "utf8");
25832
+ } catch (err) {
25833
+ if (err?.code === "ENOENT") return [];
25834
+ return [];
25835
+ }
25836
+ if (!raw.trim()) return [];
25837
+ let parsed;
25838
+ try {
25839
+ parsed = JSON.parse(raw);
25840
+ } catch {
25841
+ return [];
25842
+ }
25843
+ if (!parsed || typeof parsed !== "object") return [];
25844
+ const arr = parsed.capabilities;
25845
+ if (!Array.isArray(arr)) return [];
25846
+ const out = [];
25847
+ for (const item of arr) {
25848
+ const r = CapabilitySchema.safeParse(item);
25849
+ if (r.success) out.push(r.data);
25850
+ }
25851
+ return out;
25852
+ }
25853
+ flush() {
25854
+ const content = { version: FILE_VERSION, capabilities: this.cache };
25855
+ this.atomicWrite(this.filePath(), JSON.stringify(content, null, 2));
25856
+ }
25857
+ atomicWrite(file, content) {
25858
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
25859
+ fs15.writeFileSync(tmp, content, { mode: 384 });
25860
+ fs15.renameSync(tmp, file);
25861
+ try {
25862
+ fs15.chmodSync(file, 384);
25863
+ } catch {
25864
+ }
25865
+ }
25866
+ };
25867
+
25868
+ // src/permission/capability-registry.ts
25869
+ var crypto4 = __toESM(require("crypto"), 1);
25870
+ var CapabilityRegistry = class {
25871
+ constructor(store, now = () => Date.now()) {
25872
+ this.store = store;
25873
+ this.now = now;
25874
+ for (const cap of store.list()) {
25875
+ this.bySecretHash.set(cap.secretHash, cap);
25876
+ }
25877
+ }
25878
+ store;
25879
+ now;
25880
+ // sha256(token) → Capability
25881
+ bySecretHash = /* @__PURE__ */ new Map();
25882
+ list() {
25883
+ return this.store.list();
25884
+ }
25885
+ verifyToken(token) {
25886
+ if (!token) return { ok: false, code: "TOKEN_INVALID" };
25887
+ const hash = sha256Hex(token);
25888
+ const cap = this.bySecretHash.get(hash);
25889
+ if (!cap) return { ok: false, code: "TOKEN_INVALID" };
25890
+ if (cap.revokedAt) return { ok: false, code: "TOKEN_REVOKED" };
25891
+ if (cap.expiresAt && this.now() >= cap.expiresAt) {
25892
+ return { ok: false, code: "TOKEN_EXPIRED" };
25893
+ }
25894
+ if (cap.maxUses && cap.usedCount >= cap.maxUses) {
25895
+ return { ok: false, code: "TOKEN_EXHAUSTED" };
25896
+ }
25897
+ const updated = { ...cap, usedCount: cap.usedCount + 1 };
25898
+ this.bySecretHash.set(hash, updated);
25899
+ this.store.upsert(updated);
25900
+ return { ok: true, capability: updated };
25901
+ }
25902
+ upsertCapability(cap) {
25903
+ this.bySecretHash.set(cap.secretHash, cap);
25904
+ this.store.upsert(cap);
25905
+ }
25906
+ markRevoked(id, revokedAt) {
25907
+ const current = this.store.list().find((c) => c.id === id);
25908
+ if (!current) return null;
25909
+ if (current.revokedAt) return current;
25910
+ const updated = { ...current, revokedAt };
25911
+ this.bySecretHash.set(updated.secretHash, updated);
25912
+ this.store.upsert(updated);
25913
+ return updated;
25914
+ }
25915
+ findById(id) {
25916
+ return this.store.list().find((c) => c.id === id) ?? null;
25917
+ }
25918
+ };
25919
+ function sha256Hex(s) {
25920
+ return crypto4.createHash("sha256").update(s).digest("hex");
25921
+ }
25922
+
25923
+ // src/permission/capability-manager.ts
25924
+ var crypto5 = __toESM(require("crypto"), 1);
25925
+ var CapabilityManager = class {
25926
+ constructor(registry2, hooks = {}) {
25927
+ this.registry = registry2;
25928
+ this.hooks = hooks;
25929
+ }
25930
+ registry;
25931
+ hooks;
25932
+ list() {
25933
+ return this.registry.list();
25934
+ }
25935
+ issue(args) {
25936
+ if (!args.displayName.trim()) {
25937
+ throw new Error("CapabilityManager.issue: displayName must be non-empty");
25938
+ }
25939
+ const token = (this.hooks.generateToken ?? defaultGenerateToken)();
25940
+ const id = (this.hooks.generateId ?? defaultGenerateId)();
25941
+ const now = (this.hooks.now ?? Date.now)();
25942
+ const cap = {
25943
+ id,
25944
+ secretHash: sha256Hex2(token),
25945
+ displayName: args.displayName,
25946
+ grants: args.grants,
25947
+ issuedAt: now,
25948
+ usedCount: 0,
25949
+ ...args.expiresAt !== void 0 ? { expiresAt: args.expiresAt } : {},
25950
+ ...args.maxUses !== void 0 ? { maxUses: args.maxUses } : {}
25951
+ };
25952
+ this.registry.upsertCapability(cap);
25953
+ this.hooks.onIssued?.(cap, token);
25954
+ return { token, capability: cap };
25955
+ }
25956
+ revoke(id) {
25957
+ const existing = this.registry.findById(id);
25958
+ if (!existing) return null;
25959
+ const now = (this.hooks.now ?? Date.now)();
25960
+ if (existing.revokedAt) {
25961
+ return { revokedAt: existing.revokedAt, capability: existing };
25962
+ }
25963
+ const updated = this.registry.markRevoked(id, now);
25964
+ if (!updated) return null;
25965
+ this.hooks.onRevoked?.(updated);
25966
+ return { revokedAt: now, capability: updated };
25967
+ }
25968
+ };
25969
+ function defaultGenerateToken() {
25970
+ return crypto5.randomBytes(24).toString("base64url");
25971
+ }
25972
+ function defaultGenerateId() {
25973
+ return "cap_" + crypto5.randomBytes(6).toString("base64url");
25974
+ }
25975
+ function sha256Hex2(s) {
25976
+ return crypto5.createHash("sha256").update(s).digest("hex");
25977
+ }
25978
+
25651
25979
  // src/transport/http-router.ts
25652
25980
  var import_node_fs14 = __toESM(require("fs"), 1);
25653
25981
  var import_node_path16 = __toESM(require("path"), 1);
@@ -26869,24 +27197,14 @@ var AUTH_FILE_NAME = "auth.json";
26869
27197
  function authFilePath(dataDir) {
26870
27198
  return import_node_path22.default.join(dataDir, AUTH_FILE_NAME);
26871
27199
  }
26872
- function loadOrCreateAuthFile(opts) {
27200
+ function loadOrCreateAuthToken(opts) {
26873
27201
  const file = authFilePath(opts.dataDir);
26874
- const generate = opts.generate ?? defaultGenerate;
26875
- const now = opts.now ?? (() => /* @__PURE__ */ new Date());
26876
27202
  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;
27203
+ if (existing && existing.token) return existing.token;
27204
+ const token = (opts.generate ?? defaultGenerate)();
27205
+ const now = (opts.now ?? (() => /* @__PURE__ */ new Date()))();
27206
+ writeAuthFile(file, { token, createdAt: now.toISOString() });
27207
+ return token;
26890
27208
  }
26891
27209
  function defaultGenerate() {
26892
27210
  return import_node_crypto8.default.randomBytes(32).toString("base64url");
@@ -26895,14 +27213,13 @@ function readAuthFile(file) {
26895
27213
  try {
26896
27214
  const raw = import_node_fs20.default.readFileSync(file, "utf8");
26897
27215
  const parsed = JSON.parse(raw);
26898
- if (typeof parsed?.token !== "string" || parsed.token.length === 0) {
26899
- return null;
27216
+ if (typeof parsed?.token === "string" && parsed.token.length > 0) {
27217
+ return {
27218
+ token: parsed.token,
27219
+ createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : (/* @__PURE__ */ new Date(0)).toISOString()
27220
+ };
26900
27221
  }
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
- };
27222
+ return null;
26906
27223
  } catch (err) {
26907
27224
  const code = err?.code;
26908
27225
  if (code === "ENOENT") return null;
@@ -27446,6 +27763,65 @@ function buildCapabilitiesHandlers(deps) {
27446
27763
  };
27447
27764
  }
27448
27765
 
27766
+ // src/handlers/capability.ts
27767
+ init_zod();
27768
+ init_protocol();
27769
+ var IssueArgsSchema = external_exports.object({
27770
+ displayName: external_exports.string().min(1),
27771
+ grants: external_exports.array(GrantSchema),
27772
+ expiresAt: external_exports.number().int().positive().optional(),
27773
+ maxUses: external_exports.number().int().positive().optional()
27774
+ }).strict();
27775
+ var RevokeArgsSchema = external_exports.object({
27776
+ capabilityId: external_exports.string().min(1)
27777
+ }).strict();
27778
+ function buildCapabilityHandlers(deps) {
27779
+ const { manager } = deps;
27780
+ const issue = async (frame) => {
27781
+ const { type: _type, requestId: _requestId, ...rest } = frame;
27782
+ const args = IssueArgsSchema.parse(rest);
27783
+ const { token, capability } = manager.issue(args);
27784
+ return {
27785
+ response: {
27786
+ type: "capability:issued",
27787
+ token,
27788
+ capability: stripSecretHash(capability)
27789
+ }
27790
+ };
27791
+ };
27792
+ const list = async () => {
27793
+ return {
27794
+ response: {
27795
+ type: "capability:list",
27796
+ capabilities: manager.list().map(stripSecretHash)
27797
+ }
27798
+ };
27799
+ };
27800
+ const revoke = async (frame) => {
27801
+ const { type: _type, requestId: _requestId, ...rest } = frame;
27802
+ const args = RevokeArgsSchema.parse(rest);
27803
+ const result = manager.revoke(args.capabilityId);
27804
+ if (!result) {
27805
+ throw new ClawdError(
27806
+ ERROR_CODES.VALIDATION_ERROR,
27807
+ `capability not found: ${args.capabilityId}`
27808
+ );
27809
+ }
27810
+ return {
27811
+ response: {
27812
+ type: "capability:revoked",
27813
+ capabilityId: args.capabilityId,
27814
+ revokedAt: result.revokedAt
27815
+ }
27816
+ };
27817
+ };
27818
+ return {
27819
+ "capability:issue": issue,
27820
+ "capability:list": list,
27821
+ "capability:revoke": revoke
27822
+ };
27823
+ }
27824
+
27449
27825
  // src/handlers/meta.ts
27450
27826
  var import_node_os13 = __toESM(require("os"), 1);
27451
27827
  init_protocol();
@@ -27571,7 +27947,6 @@ function buildPersonaHandlers(deps) {
27571
27947
  }
27572
27948
 
27573
27949
  // src/handlers/attachment.ts
27574
- var import_node_path26 = __toESM(require("path"), 1);
27575
27950
  init_protocol();
27576
27951
  init_protocol();
27577
27952
  var DEFAULT_TTL_SECONDS = 24 * 3600;
@@ -27596,41 +27971,8 @@ function buildAttachmentHandlers(deps) {
27596
27971
  "httpBaseUrl unavailable (daemon HTTP not ready)"
27597
27972
  );
27598
27973
  }
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
27974
  const ttl = args.ttlSeconds === null ? null : args.ttlSeconds ?? DEFAULT_TTL_SECONDS;
27633
- const parts = signUrlParts(secret, candidateAbs, ttl);
27975
+ const parts = signUrlParts(secret, args.absPath, ttl);
27634
27976
  const url = buildSignedFileUrl(httpBaseUrl, parts);
27635
27977
  return {
27636
27978
  response: {
@@ -27731,15 +28073,112 @@ function buildMethodHandlers(deps) {
27731
28073
  personaManager: deps.personaManager,
27732
28074
  personaRegistry: deps.personaRegistry
27733
28075
  }),
28076
+ ...buildCapabilityHandlers({ manager: deps.capabilityManager }),
27734
28077
  ...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {}
27735
28078
  };
27736
28079
  }
27737
28080
 
28081
+ // src/handlers/method-grants.ts
28082
+ var ADMIN_ANY = {
28083
+ kind: "fixed",
28084
+ resource: { type: "*" },
28085
+ action: "admin"
28086
+ };
28087
+ var METHOD_GRANT_MAP = {
28088
+ // ---- public(meta-only,guest 也能调) ----
28089
+ "info": { kind: "public" },
28090
+ "ping": { kind: "public" },
28091
+ // ---- capability platform(admin-only,本 PR 新增) ----
28092
+ "capability:issue": ADMIN_ANY,
28093
+ "capability:list": ADMIN_ANY,
28094
+ "capability:revoke": ADMIN_ANY,
28095
+ // ---- 业务方法:Phase 1 全 admin-only(owner 自动通过;guest 无法调用) ----
28096
+ "session:create": ADMIN_ANY,
28097
+ "session:list": ADMIN_ANY,
28098
+ "session:get": ADMIN_ANY,
28099
+ "session:update": ADMIN_ANY,
28100
+ "session:delete": ADMIN_ANY,
28101
+ "session:send": ADMIN_ANY,
28102
+ "session:stop": ADMIN_ANY,
28103
+ "session:interrupt": ADMIN_ANY,
28104
+ "session:rewind": ADMIN_ANY,
28105
+ "session:rewind-diff": ADMIN_ANY,
28106
+ "session:rewindable-message-ids": ADMIN_ANY,
28107
+ "session:fork": ADMIN_ANY,
28108
+ "session:new": ADMIN_ANY,
28109
+ "session:resume": ADMIN_ANY,
28110
+ "session:observe": ADMIN_ANY,
28111
+ "session:events": ADMIN_ANY,
28112
+ "session:subscribe": ADMIN_ANY,
28113
+ "session:unsubscribe": ADMIN_ANY,
28114
+ "session:pin": ADMIN_ANY,
28115
+ "session:reorderPins": ADMIN_ANY,
28116
+ "permission:respond": ADMIN_ANY,
28117
+ "session:answerQuestion": ADMIN_ANY,
28118
+ "session:cancelQuestion": ADMIN_ANY,
28119
+ "history:projects": ADMIN_ANY,
28120
+ "history:list": ADMIN_ANY,
28121
+ "history:read": ADMIN_ANY,
28122
+ "history:subagents": ADMIN_ANY,
28123
+ "history:subagent-read": ADMIN_ANY,
28124
+ "history:recentDirs": ADMIN_ANY,
28125
+ "workspace:list": ADMIN_ANY,
28126
+ "workspace:read": ADMIN_ANY,
28127
+ "skills:list": ADMIN_ANY,
28128
+ "agents:list": ADMIN_ANY,
28129
+ "git:root": ADMIN_ANY,
28130
+ "git:branch": ADMIN_ANY,
28131
+ "git:branches": ADMIN_ANY,
28132
+ "capabilities:get": ADMIN_ANY,
28133
+ "persona:create": ADMIN_ANY,
28134
+ "persona:list": ADMIN_ANY,
28135
+ "persona:get": ADMIN_ANY,
28136
+ "persona:update": ADMIN_ANY,
28137
+ "persona:delete": ADMIN_ANY,
28138
+ "persona:issueToken": ADMIN_ANY,
28139
+ "persona:revokeToken": ADMIN_ANY,
28140
+ "session:pty:input": ADMIN_ANY,
28141
+ "session:pty:resize": ADMIN_ANY,
28142
+ // file-sharing attachment.*:handler 内部已有 requireOwner(HTTP 路径同),dispatcher 这里
28143
+ // 用 admin-only 兜底(双保险,wire-level 也拦)
28144
+ "attachment.signUrl": ADMIN_ANY,
28145
+ "attachment.groupAdd": ADMIN_ANY,
28146
+ "attachment.groupRemove": ADMIN_ANY,
28147
+ "attachment.groupList": ADMIN_ANY,
28148
+ "attachment.groupListPersona": ADMIN_ANY
28149
+ };
28150
+ function computeGrantForFrame(method, frame) {
28151
+ const rule = METHOD_GRANT_MAP[method];
28152
+ if (!rule) return { kind: "public" };
28153
+ if (rule.kind === "public") return { kind: "public" };
28154
+ if (rule.kind === "fixed") {
28155
+ return { kind: "check", resource: rule.resource, action: rule.action };
28156
+ }
28157
+ const picked = rule.pick(frame);
28158
+ if (!picked) return { kind: "public" };
28159
+ return { kind: "check", resource: picked.resource, action: picked.action };
28160
+ }
28161
+
28162
+ // src/permission/capability.ts
28163
+ function matchResource(grant, target) {
28164
+ if (grant.type === "*") return true;
28165
+ if (grant.type !== target.type) return false;
28166
+ return grant.id === target.id;
28167
+ }
28168
+ function assertGrant(grants, resource, action) {
28169
+ for (const g2 of grants) {
28170
+ if (!matchResource(g2.resource, resource)) continue;
28171
+ if (g2.actions.includes(action)) return true;
28172
+ if (g2.actions.includes("admin")) return true;
28173
+ }
28174
+ return false;
28175
+ }
28176
+
27738
28177
  // src/index.ts
27739
28178
  async function startDaemon(config) {
27740
28179
  const logger = createLogger({
27741
28180
  level: config.logLevel,
27742
- file: import_node_path27.default.join(config.dataDir, "clawd.log")
28181
+ file: import_node_path26.default.join(config.dataDir, "clawd.log")
27743
28182
  });
27744
28183
  logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
27745
28184
  const stateMgr = new StateFileManager({ dataDir: config.dataDir });
@@ -27751,18 +28190,43 @@ async function startDaemon(config) {
27751
28190
  logger.warn("stale state file detected, overwriting", { pid: pre.existing.pid });
27752
28191
  }
27753
28192
  let resolvedAuthToken = null;
27754
- let authFile = null;
27755
28193
  if (config.authToken && config.authToken.trim()) {
27756
28194
  resolvedAuthToken = config.authToken.trim();
27757
28195
  } else if (config.tunnel) {
27758
- authFile = loadOrCreateAuthFile({ dataDir: config.dataDir });
27759
- resolvedAuthToken = authFile.token;
28196
+ resolvedAuthToken = loadOrCreateAuthToken({ dataDir: config.dataDir });
27760
28197
  }
27761
28198
  const authMode = resolvedAuthToken == null ? "none" : "first-message";
28199
+ const capabilityStore = new CapabilityStore(config.dataDir);
28200
+ const capabilityRegistry = new CapabilityRegistry(capabilityStore);
28201
+ const capabilityManager = new CapabilityManager(capabilityRegistry, {
28202
+ onIssued: (cap, token) => {
28203
+ wsServer?.broadcastToOwners({
28204
+ type: "capability:tokenIssued",
28205
+ capability: stripSecretHash(cap),
28206
+ token
28207
+ });
28208
+ },
28209
+ onRevoked: (cap) => {
28210
+ wsServer?.broadcastToOwners({
28211
+ type: "capability:tokenRevoked",
28212
+ capabilityId: cap.id,
28213
+ revokedAt: cap.revokedAt
28214
+ });
28215
+ wsServer?.closeConnectionsByCapability(cap.id);
28216
+ }
28217
+ });
27762
28218
  let wsServer = null;
27763
28219
  const authGate = authMode === "first-message" ? new AuthGate({
27764
28220
  shouldEnforce: buildShouldEnforce({ tunnel: config.tunnel }),
28221
+ // Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
28222
+ // owner 路径 constantTimeEqual 防侧信道;guest 路径走 capabilityRegistry.
27765
28223
  expectedToken: resolvedAuthToken,
28224
+ authenticate: (t) => authenticate(t, {
28225
+ isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
28226
+ capabilityRegistry
28227
+ }),
28228
+ onAuthed: (h, ctx) => wsServer?.attachClientContext(h.id, ctx),
28229
+ buildOwnerContext: ownerContext,
27766
28230
  closeConnection: (h, code, reason) => wsServer?.closeClient(h.id, code, reason),
27767
28231
  sendOk: (h, payload) => wsServer?.sendToClient(h.id, payload)
27768
28232
  }) : null;
@@ -27773,7 +28237,7 @@ async function startDaemon(config) {
27773
28237
  const agents = new AgentsScanner();
27774
28238
  const history = new ClaudeHistoryReader();
27775
28239
  let transport = null;
27776
- const personaStore = new PersonaStore(import_node_path27.default.join(config.dataDir, "personas"));
28240
+ const personaStore = new PersonaStore(import_node_path26.default.join(config.dataDir, "personas"));
27777
28241
  const defaultsRoot = findDefaultsRoot();
27778
28242
  if (defaultsRoot) {
27779
28243
  seedDefaultPersonas({ store: personaStore, defaultsRoot, logger });
@@ -27788,7 +28252,7 @@ async function startDaemon(config) {
27788
28252
  getAdapter,
27789
28253
  historyReader: history,
27790
28254
  dataDir: config.dataDir,
27791
- personaRoot: import_node_path27.default.join(config.dataDir, "personas"),
28255
+ personaRoot: import_node_path26.default.join(config.dataDir, "personas"),
27792
28256
  personaStore,
27793
28257
  ownerDisplayName,
27794
28258
  mode: config.mode,
@@ -27811,7 +28275,7 @@ async function startDaemon(config) {
27811
28275
  // 文件可能 agent 写完又被自己删(罕见),用 size=0 / fallback mime 兜底。
27812
28276
  attachmentGroup: {
27813
28277
  onFileEdit: (input) => {
27814
- const absPath = import_node_path27.default.isAbsolute(input.relPath) ? input.relPath : import_node_path27.default.join(input.cwd, input.relPath);
28278
+ const absPath = import_node_path26.default.isAbsolute(input.relPath) ? input.relPath : import_node_path26.default.join(input.cwd, input.relPath);
27815
28279
  let size = 0;
27816
28280
  try {
27817
28281
  size = import_node_fs24.default.statSync(absPath).size;
@@ -27910,23 +28374,25 @@ async function startDaemon(config) {
27910
28374
  httpToken: resolvedAuthToken,
27911
28375
  // file-sharing attachment.* RPC。signUrl 用 owner token 做 HMAC secret;group RPC
27912
28376
  // 根据 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
28377
  attachment: {
27920
28378
  groupFileStore,
27921
- sessionStore: { read: (sid) => manager.findOwnedSession(sid) },
27922
28379
  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 走
28380
+ // HMAC sign secret:复用 ~/.clawd/auth.json owner token(持久跨重启)。
28381
+ // noAuth 模式 resolvedAuthToken 为 null → handler 自己返 NOT_IMPLEMENTED。
28382
+ getSignSecret: () => resolvedAuthToken ?? "",
28383
+ // group RPC:根据 sessionId 反查 scopeowner-mode persona session 走
27927
28384
  // 'persona/<pid>/owner',default 走 'default'。
27928
- getSessionScope: (sid) => manager.findOwnedSessionScope(sid)
27929
- }
28385
+ getSessionScope: (sessionId) => {
28386
+ const file = store.read(sessionId);
28387
+ if (!file) return null;
28388
+ if (file.ownerPersonaId) {
28389
+ return { kind: "persona", personaId: file.ownerPersonaId, mode: "owner" };
28390
+ }
28391
+ return { kind: "default" };
28392
+ }
28393
+ },
28394
+ // Task 1.9: capability:issue/list/revoke handler 依赖
28395
+ capabilityManager
27930
28396
  });
27931
28397
  const authResolver = new AuthContextResolver({
27932
28398
  ownerToken: resolvedAuthToken,
@@ -27939,9 +28405,8 @@ async function startDaemon(config) {
27939
28405
  personaStore,
27940
28406
  groupFileStore,
27941
28407
  sessionStore: store,
27942
- // /files HMAC verify auth.json signSecret 字段(与 attachment.signUrl 同源)。
27943
- // --auth-token CLI 模式没 signSecret → 路由返 501,sign URL 功能整体禁用。
27944
- getSignSecret: () => authFile?.signSecret ?? null
28408
+ // /files HMAC verify 用同一份 owner token secret(与 attachment.signUrl 同源)
28409
+ getSignSecret: () => resolvedAuthToken ?? null
27945
28410
  });
27946
28411
  wsServer = new LocalWsServer({
27947
28412
  host: config.host,
@@ -28014,7 +28479,18 @@ async function startDaemon(config) {
28014
28479
  const requestId = typeof frame.requestId === "string" ? frame.requestId : void 0;
28015
28480
  const handler = handlers[type];
28016
28481
  if (!handler) throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, `not implemented: ${type}`);
28017
- const result = await handler(frame, client);
28482
+ const ctx = wsServer.getClientContext(client.id) ?? ownerContext();
28483
+ const verdict = computeGrantForFrame(type, frame);
28484
+ if (verdict.kind === "check") {
28485
+ const ok = assertGrant(ctx.grants, verdict.resource, verdict.action);
28486
+ if (!ok) {
28487
+ throw new ClawdError(
28488
+ ERROR_CODES.UNAUTHORIZED,
28489
+ `principal ${ctx.principal.kind}:${ctx.principal.id} cannot ${verdict.action} on ${verdict.resource.type}${"id" in verdict.resource ? ":" + verdict.resource.id : ""}`
28490
+ );
28491
+ }
28492
+ }
28493
+ const result = await handler(frame, client, ctx);
28018
28494
  if (requestId && result.response) {
28019
28495
  client.send({ ...result.response, requestId });
28020
28496
  }
@@ -28072,15 +28548,15 @@ async function startDaemon(config) {
28072
28548
  });
28073
28549
  try {
28074
28550
  const r = await tunnelMgr.start({ localPort: config.port });
28075
- stateSnapshot = { ...stateSnapshot, tunnelUrl: r.url, tunnelError: void 0 };
28551
+ stateSnapshot = { ...stateSnapshot, tunnelUrl: r.url };
28076
28552
  stateMgr.write(stateSnapshot);
28077
28553
  currentTunnelUrl = r.url;
28078
28554
  const connectUrl = resolvedAuthToken ? `${r.url}#token=${resolvedAuthToken}` : r.url;
28079
28555
  const lines = [
28080
28556
  `Tunnel: ${r.url}`,
28081
28557
  ...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")}`
28558
+ `Frpc config: ${import_node_path26.default.join(config.dataDir, "frpc.toml")}`,
28559
+ `Frpc log: ${import_node_path26.default.join(config.dataDir, "frpc.log")}`
28084
28560
  ];
28085
28561
  const width = Math.max(...lines.map((l) => l.length));
28086
28562
  const bar = "\u2550".repeat(width + 4);
@@ -28093,30 +28569,19 @@ ${bar}
28093
28569
 
28094
28570
  `);
28095
28571
  try {
28096
- const connectPath = import_node_path27.default.join(config.dataDir, "connect.txt");
28572
+ const connectPath = import_node_path26.default.join(config.dataDir, "connect.txt");
28097
28573
  import_node_fs24.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
28098
28574
  } catch {
28099
28575
  }
28100
28576
  } catch (err) {
28101
- const tunnelError = err?.message ?? String(err);
28102
28577
  try {
28103
28578
  await tunnelMgr.stop();
28104
28579
  } catch {
28105
28580
  }
28106
28581
  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 });
28582
+ stateMgr.delete();
28583
+ await wss.stop();
28584
+ throw err;
28120
28585
  }
28121
28586
  }
28122
28587
  const shutdown = async () => {