@clawos-dev/clawd 0.2.127 → 0.2.128-beta.255.69495b2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -195,6 +195,10 @@ var init_methods = __esm({
195
195
  // updateProjectPort: 校验新端口段内 + 未被别的 project 占 + 重启 dev server
196
196
  // setProdUrl: 发布上线后 assistant 调一次写入 .clawd-project.json.prodUrl(取代 prod marker)
197
197
  "appBuilder:listProjects",
198
+ // getProject:按 sessionId 取单个绑定 project(含 status)。people/mirror 跨 daemon 场景:
199
+ // 本机 owner 在远端是 guest,listProjects 被 per-user 隔离拿不到别人的 project;getProject
200
+ // 按 session 可见性授权(ensureSessionAccess read)放行。本地也复用(省掉拉全量再 find)。
201
+ "appBuilder:getProject",
198
202
  "appBuilder:createProject",
199
203
  "appBuilder:deleteProject",
200
204
  "appBuilder:updateProjectPort",
@@ -4566,7 +4570,7 @@ var init_persona_schemas = __esm({
4566
4570
  });
4567
4571
 
4568
4572
  // ../protocol/src/schemas.ts
4569
- 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, PeerSessionUpsertArgs, PeerSessionUpsertResponseSchema, PeerSessionRemoveArgs, PeerSessionRemoveResponseSchema, SessionReorderPinsArgs, GitRootArgs, GitRootResponseSchema, GitBranchArgs, GitBranchResponseSchema, GitBranchesArgs, GitBranchesResponseSchema, HistoryRecentDirsArgs, RecentDirEntrySchema, HistoryRecentDirsResponseSchema, SessionQuestionFrameSchema, SessionQuestionClearedFrameSchema, AnswerQuestionArgs, AnswerQuestionResponseSchema, CancelQuestionArgs, CancelQuestionResponseSchema, AuthRequestFrameSchema, AuthOkFrameSchema, TunnelReadyEventSchema, TunnelExitedEventSchema, TunnelUnavailableEventSchema, InfoRunningSessionSchema, InfoResponseSchema, PROJECT_PORT_MIN, PROJECT_PORT_MAX, projectNameRegex, DEFAULT_DEV_COMMAND, ProjectMetadataSchema, PROJECT_STAGE_VALUES, ProjectStageSchema, ProjectWithStatusSchema, ListProjectsArgsSchema, ListProjectsResultSchema, CreateProjectArgsSchema, CreateProjectResultSchema, DeleteProjectArgsSchema, DeleteProjectResultSchema, UpdateProjectPortArgsSchema, UpdateProjectPortResultSchema, SetProdUrlArgsSchema, SetProdUrlResultSchema, StartDevServerArgsSchema, StartDevServerResultSchema, ASSISTANT_REPORTABLE_STAGES, ReportStageArgsSchema, ReportStageResultSchema, StopDevServerArgsSchema, StopDevServerResultSchema, AppBuilderProjectUpdatedEventSchema, PublishArgsSchema, PublishResultSchema, DismissPublishJobArgsSchema, DismissPublishJobResultSchema, AppBuilderPublishProgressEventSchema, AppBuilderPublishFailedEventSchema;
4573
+ 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, PeerSessionUpsertArgs, PeerSessionUpsertResponseSchema, PeerSessionRemoveArgs, PeerSessionRemoveResponseSchema, SessionReorderPinsArgs, GitRootArgs, GitRootResponseSchema, GitBranchArgs, GitBranchResponseSchema, GitBranchesArgs, GitBranchesResponseSchema, HistoryRecentDirsArgs, RecentDirEntrySchema, HistoryRecentDirsResponseSchema, SessionQuestionFrameSchema, SessionQuestionClearedFrameSchema, AnswerQuestionArgs, AnswerQuestionResponseSchema, CancelQuestionArgs, CancelQuestionResponseSchema, AuthRequestFrameSchema, AuthOkFrameSchema, TunnelReadyEventSchema, TunnelExitedEventSchema, TunnelUnavailableEventSchema, InfoRunningSessionSchema, InfoResponseSchema, PROJECT_PORT_MIN, PROJECT_PORT_MAX, projectNameRegex, DEFAULT_DEV_COMMAND, ProjectMetadataSchema, PROJECT_STAGE_VALUES, ProjectStageSchema, ProjectWithStatusSchema, ListProjectsArgsSchema, ListProjectsResultSchema, GetProjectArgsSchema, GetProjectResultSchema, CreateProjectArgsSchema, CreateProjectResultSchema, DeleteProjectArgsSchema, DeleteProjectResultSchema, UpdateProjectPortArgsSchema, UpdateProjectPortResultSchema, SetProdUrlArgsSchema, SetProdUrlResultSchema, StartDevServerArgsSchema, StartDevServerResultSchema, ASSISTANT_REPORTABLE_STAGES, ReportStageArgsSchema, ReportStageResultSchema, StopDevServerArgsSchema, StopDevServerResultSchema, AppBuilderProjectUpdatedEventSchema, PublishArgsSchema, PublishResultSchema, DismissPublishJobArgsSchema, DismissPublishJobResultSchema, AppBuilderPublishProgressEventSchema, AppBuilderPublishFailedEventSchema;
4570
4574
  var init_schemas = __esm({
4571
4575
  "../protocol/src/schemas.ts"() {
4572
4576
  "use strict";
@@ -5202,7 +5206,7 @@ var init_schemas = __esm({
5202
5206
  httpToken: external_exports.string().optional()
5203
5207
  });
5204
5208
  PROJECT_PORT_MIN = 6173;
5205
- PROJECT_PORT_MAX = 6182;
5209
+ PROJECT_PORT_MAX = 6272;
5206
5210
  projectNameRegex = /^[a-z][a-z0-9-]{0,39}$/;
5207
5211
  DEFAULT_DEV_COMMAND = "cd server && pnpm dev";
5208
5212
  ProjectMetadataSchema = external_exports.object({
@@ -5289,6 +5293,13 @@ var init_schemas = __esm({
5289
5293
  ListProjectsResultSchema = external_exports.object({
5290
5294
  projects: external_exports.array(ProjectWithStatusSchema)
5291
5295
  }).strict();
5296
+ GetProjectArgsSchema = external_exports.object({
5297
+ sessionId: external_exports.string().min(1)
5298
+ }).strict();
5299
+ GetProjectResultSchema = external_exports.object({
5300
+ /** session 未绑 project / project 不存在时为 null(UI 走单栏 / 折叠) */
5301
+ project: ProjectWithStatusSchema.nullable()
5302
+ }).strict();
5292
5303
  CreateProjectArgsSchema = external_exports.object({
5293
5304
  /** 必填:caller session,daemon 自动绑过去(spec 2026-06-02 §3 invariant 1 Session ↔ 1 Project) */
5294
5305
  sessionId: external_exports.string().min(1),
@@ -7082,7 +7093,7 @@ var require_atomic_sleep = __commonJS({
7082
7093
  var require_sonic_boom = __commonJS({
7083
7094
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
7084
7095
  "use strict";
7085
- var fs49 = require("fs");
7096
+ var fs50 = require("fs");
7086
7097
  var EventEmitter3 = require("events");
7087
7098
  var inherits = require("util").inherits;
7088
7099
  var path60 = require("path");
@@ -7139,20 +7150,20 @@ var require_sonic_boom = __commonJS({
7139
7150
  const mode = sonic.mode;
7140
7151
  if (sonic.sync) {
7141
7152
  try {
7142
- if (sonic.mkdir) fs49.mkdirSync(path60.dirname(file), { recursive: true });
7143
- const fd = fs49.openSync(file, flags, mode);
7153
+ if (sonic.mkdir) fs50.mkdirSync(path60.dirname(file), { recursive: true });
7154
+ const fd = fs50.openSync(file, flags, mode);
7144
7155
  fileOpened(null, fd);
7145
7156
  } catch (err) {
7146
7157
  fileOpened(err);
7147
7158
  throw err;
7148
7159
  }
7149
7160
  } else if (sonic.mkdir) {
7150
- fs49.mkdir(path60.dirname(file), { recursive: true }, (err) => {
7161
+ fs50.mkdir(path60.dirname(file), { recursive: true }, (err) => {
7151
7162
  if (err) return fileOpened(err);
7152
- fs49.open(file, flags, mode, fileOpened);
7163
+ fs50.open(file, flags, mode, fileOpened);
7153
7164
  });
7154
7165
  } else {
7155
- fs49.open(file, flags, mode, fileOpened);
7166
+ fs50.open(file, flags, mode, fileOpened);
7156
7167
  }
7157
7168
  }
7158
7169
  function SonicBoom(opts) {
@@ -7193,8 +7204,8 @@ var require_sonic_boom = __commonJS({
7193
7204
  this.flush = flushBuffer;
7194
7205
  this.flushSync = flushBufferSync;
7195
7206
  this._actualWrite = actualWriteBuffer;
7196
- fsWriteSync = () => fs49.writeSync(this.fd, this._writingBuf);
7197
- fsWrite = () => fs49.write(this.fd, this._writingBuf, this.release);
7207
+ fsWriteSync = () => fs50.writeSync(this.fd, this._writingBuf);
7208
+ fsWrite = () => fs50.write(this.fd, this._writingBuf, this.release);
7198
7209
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
7199
7210
  this._writingBuf = "";
7200
7211
  this.write = write;
@@ -7203,15 +7214,15 @@ var require_sonic_boom = __commonJS({
7203
7214
  this._actualWrite = actualWrite;
7204
7215
  fsWriteSync = () => {
7205
7216
  if (Buffer.isBuffer(this._writingBuf)) {
7206
- return fs49.writeSync(this.fd, this._writingBuf);
7217
+ return fs50.writeSync(this.fd, this._writingBuf);
7207
7218
  }
7208
- return fs49.writeSync(this.fd, this._writingBuf, "utf8");
7219
+ return fs50.writeSync(this.fd, this._writingBuf, "utf8");
7209
7220
  };
7210
7221
  fsWrite = () => {
7211
7222
  if (Buffer.isBuffer(this._writingBuf)) {
7212
- return fs49.write(this.fd, this._writingBuf, this.release);
7223
+ return fs50.write(this.fd, this._writingBuf, this.release);
7213
7224
  }
7214
- return fs49.write(this.fd, this._writingBuf, "utf8", this.release);
7225
+ return fs50.write(this.fd, this._writingBuf, "utf8", this.release);
7215
7226
  };
7216
7227
  } else {
7217
7228
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -7268,7 +7279,7 @@ var require_sonic_boom = __commonJS({
7268
7279
  }
7269
7280
  }
7270
7281
  if (this._fsync) {
7271
- fs49.fsyncSync(this.fd);
7282
+ fs50.fsyncSync(this.fd);
7272
7283
  }
7273
7284
  const len = this._len;
7274
7285
  if (this._reopening) {
@@ -7382,7 +7393,7 @@ var require_sonic_boom = __commonJS({
7382
7393
  const onDrain = () => {
7383
7394
  if (!this._fsync) {
7384
7395
  try {
7385
- fs49.fsync(this.fd, (err) => {
7396
+ fs50.fsync(this.fd, (err) => {
7386
7397
  this._flushPending = false;
7387
7398
  cb(err);
7388
7399
  });
@@ -7484,7 +7495,7 @@ var require_sonic_boom = __commonJS({
7484
7495
  const fd = this.fd;
7485
7496
  this.once("ready", () => {
7486
7497
  if (fd !== this.fd) {
7487
- fs49.close(fd, (err) => {
7498
+ fs50.close(fd, (err) => {
7488
7499
  if (err) {
7489
7500
  return this.emit("error", err);
7490
7501
  }
@@ -7533,7 +7544,7 @@ var require_sonic_boom = __commonJS({
7533
7544
  buf = this._bufs[0];
7534
7545
  }
7535
7546
  try {
7536
- const n = Buffer.isBuffer(buf) ? fs49.writeSync(this.fd, buf) : fs49.writeSync(this.fd, buf, "utf8");
7547
+ const n = Buffer.isBuffer(buf) ? fs50.writeSync(this.fd, buf) : fs50.writeSync(this.fd, buf, "utf8");
7537
7548
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
7538
7549
  buf = releasedBufObj.writingBuf;
7539
7550
  this._len = releasedBufObj.len;
@@ -7549,7 +7560,7 @@ var require_sonic_boom = __commonJS({
7549
7560
  }
7550
7561
  }
7551
7562
  try {
7552
- fs49.fsyncSync(this.fd);
7563
+ fs50.fsyncSync(this.fd);
7553
7564
  } catch {
7554
7565
  }
7555
7566
  }
@@ -7570,7 +7581,7 @@ var require_sonic_boom = __commonJS({
7570
7581
  buf = mergeBuf(this._bufs[0], this._lens[0]);
7571
7582
  }
7572
7583
  try {
7573
- const n = fs49.writeSync(this.fd, buf);
7584
+ const n = fs50.writeSync(this.fd, buf);
7574
7585
  buf = buf.subarray(n);
7575
7586
  this._len = Math.max(this._len - n, 0);
7576
7587
  if (buf.length <= 0) {
@@ -7598,13 +7609,13 @@ var require_sonic_boom = __commonJS({
7598
7609
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
7599
7610
  if (this.sync) {
7600
7611
  try {
7601
- const written = Buffer.isBuffer(this._writingBuf) ? fs49.writeSync(this.fd, this._writingBuf) : fs49.writeSync(this.fd, this._writingBuf, "utf8");
7612
+ const written = Buffer.isBuffer(this._writingBuf) ? fs50.writeSync(this.fd, this._writingBuf) : fs50.writeSync(this.fd, this._writingBuf, "utf8");
7602
7613
  release(null, written);
7603
7614
  } catch (err) {
7604
7615
  release(err);
7605
7616
  }
7606
7617
  } else {
7607
- fs49.write(this.fd, this._writingBuf, release);
7618
+ fs50.write(this.fd, this._writingBuf, release);
7608
7619
  }
7609
7620
  }
7610
7621
  function actualWriteBuffer() {
@@ -7613,7 +7624,7 @@ var require_sonic_boom = __commonJS({
7613
7624
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
7614
7625
  if (this.sync) {
7615
7626
  try {
7616
- const written = fs49.writeSync(this.fd, this._writingBuf);
7627
+ const written = fs50.writeSync(this.fd, this._writingBuf);
7617
7628
  release(null, written);
7618
7629
  } catch (err) {
7619
7630
  release(err);
@@ -7622,7 +7633,7 @@ var require_sonic_boom = __commonJS({
7622
7633
  if (kCopyBuffer) {
7623
7634
  this._writingBuf = Buffer.from(this._writingBuf);
7624
7635
  }
7625
- fs49.write(this.fd, this._writingBuf, release);
7636
+ fs50.write(this.fd, this._writingBuf, release);
7626
7637
  }
7627
7638
  }
7628
7639
  function actualClose(sonic) {
@@ -7638,12 +7649,12 @@ var require_sonic_boom = __commonJS({
7638
7649
  sonic._lens = [];
7639
7650
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
7640
7651
  try {
7641
- fs49.fsync(sonic.fd, closeWrapped);
7652
+ fs50.fsync(sonic.fd, closeWrapped);
7642
7653
  } catch {
7643
7654
  }
7644
7655
  function closeWrapped() {
7645
7656
  if (sonic.fd !== 1 && sonic.fd !== 2) {
7646
- fs49.close(sonic.fd, done);
7657
+ fs50.close(sonic.fd, done);
7647
7658
  } else {
7648
7659
  done();
7649
7660
  }
@@ -8322,7 +8333,7 @@ var require_transport = __commonJS({
8322
8333
  "use strict";
8323
8334
  var { createRequire } = require("module");
8324
8335
  var getCallers = require_caller();
8325
- var { join: join15, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
8336
+ var { join: join15, isAbsolute: isAbsolute2, sep: sep2 } = require("path");
8326
8337
  var sleep = require_atomic_sleep();
8327
8338
  var onExit = require_on_exit_leak_free();
8328
8339
  var ThreadStream = require_thread_stream();
@@ -8430,7 +8441,7 @@ var require_transport = __commonJS({
8430
8441
  let fixTarget2;
8431
8442
  for (const filePath of callers) {
8432
8443
  try {
8433
- const context = filePath === "node:repl" ? process.cwd() + sep3 : filePath;
8444
+ const context = filePath === "node:repl" ? process.cwd() + sep2 : filePath;
8434
8445
  fixTarget2 = createRequire(context).resolve(origin);
8435
8446
  break;
8436
8447
  } catch (err) {
@@ -10850,7 +10861,7 @@ var init_lib = __esm({
10850
10861
  tokenize: function tokenize(value) {
10851
10862
  return Array.from(value);
10852
10863
  },
10853
- join: function join4(chars) {
10864
+ join: function join5(chars) {
10854
10865
  return chars.join("");
10855
10866
  },
10856
10867
  postProcess: function postProcess(changeObjects) {
@@ -11705,27 +11716,6 @@ var init_claude_history = __esm({
11705
11716
  }
11706
11717
  });
11707
11718
 
11708
- // src/tools/sandbox.ts
11709
- function shouldSandbox(cwd, personaRoot) {
11710
- if (!personaRoot) return false;
11711
- const sep3 = personaRoot.endsWith(path13.sep) ? "" : path13.sep;
11712
- return cwd.startsWith(personaRoot + sep3) && cwd !== personaRoot;
11713
- }
11714
- function inferSandboxSettingsPath(cwd, personaRoot) {
11715
- if (!shouldSandbox(cwd, personaRoot)) return null;
11716
- const rel = path13.relative(personaRoot, cwd);
11717
- const personaId = rel.split(path13.sep)[0];
11718
- if (!personaId) return null;
11719
- return path13.join(personaRoot, personaId, ".clawd", "sandbox-settings.json");
11720
- }
11721
- var path13;
11722
- var init_sandbox = __esm({
11723
- "src/tools/sandbox.ts"() {
11724
- "use strict";
11725
- path13 = __toESM(require("path"), 1);
11726
- }
11727
- });
11728
-
11729
11719
  // src/tools/claude.ts
11730
11720
  function macOSDesktopCandidates(home) {
11731
11721
  return [
@@ -11790,9 +11780,11 @@ function buildSpawnArgs(ctx) {
11790
11780
  throw new Error(`unexpected personaMode: ${String(_exhaustive)}`);
11791
11781
  }
11792
11782
  }
11793
- const sandboxSettings = ctx.extraSettings ?? inferSandboxSettingsPath(ctx.cwd, ctx.personaRoot);
11794
- if (sandboxSettings) args.push("--settings", sandboxSettings);
11783
+ if (ctx.extraSettings) args.push("--settings", ctx.extraSettings);
11795
11784
  if (ctx.extraSystemPrompt) args.push("--append-system-prompt", ctx.extraSystemPrompt);
11785
+ if (ctx.addDirs) {
11786
+ for (const dir of ctx.addDirs) args.push("--add-dir", dir);
11787
+ }
11796
11788
  if (ctx.effort) args.push("--effort", ctx.effort);
11797
11789
  if (ctx.toolSessionId) args.push("--resume", ctx.toolSessionId);
11798
11790
  return args;
@@ -12191,7 +12183,6 @@ var init_claude = __esm({
12191
12183
  init_protocol();
12192
12184
  init_claude_history();
12193
12185
  init_tool_result_extra();
12194
- init_sandbox();
12195
12186
  ATTACHMENT_SILENT_SUBTYPES2 = /* @__PURE__ */ new Set([
12196
12187
  "hook_additional_context",
12197
12188
  "hook_success",
@@ -30745,8 +30736,8 @@ function startRunCaseRecorder(opts) {
30745
30736
  });
30746
30737
  const ensureStream = () => {
30747
30738
  if (stream) return stream;
30748
- import_node_fs32.default.mkdirSync(dir, { recursive: true });
30749
- stream = import_node_fs32.default.createWriteStream(opts.recordPath, { flags: "a" });
30739
+ import_node_fs33.default.mkdirSync(dir, { recursive: true });
30740
+ stream = import_node_fs33.default.createWriteStream(opts.recordPath, { flags: "a" });
30750
30741
  stream.on("close", () => closedResolve());
30751
30742
  return stream;
30752
30743
  };
@@ -30771,11 +30762,11 @@ function startRunCaseRecorder(opts) {
30771
30762
  };
30772
30763
  return { tap, close, closed };
30773
30764
  }
30774
- var import_node_fs32, import_node_path48;
30765
+ var import_node_fs33, import_node_path48;
30775
30766
  var init_recorder = __esm({
30776
30767
  "src/run-case/recorder.ts"() {
30777
30768
  "use strict";
30778
- import_node_fs32 = __toESM(require("fs"), 1);
30769
+ import_node_fs33 = __toESM(require("fs"), 1);
30779
30770
  import_node_path48 = __toESM(require("path"), 1);
30780
30771
  }
30781
30772
  });
@@ -30819,7 +30810,7 @@ var init_wire = __esm({
30819
30810
  // src/run-case/controller.ts
30820
30811
  async function runController(opts) {
30821
30812
  const now = opts.now ?? Date.now;
30822
- const cwd = opts.cwd ?? (0, import_node_fs33.mkdtempSync)(import_node_path49.default.join(import_node_os19.default.tmpdir(), "clawd-runcase-"));
30813
+ const cwd = opts.cwd ?? (0, import_node_fs34.mkdtempSync)(import_node_path49.default.join(import_node_os19.default.tmpdir(), "clawd-runcase-"));
30823
30814
  const ownsCwd = opts.cwd === void 0;
30824
30815
  const recorder = startRunCaseRecorder({ recordPath: opts.record, now });
30825
30816
  const spawnCtx = { cwd };
@@ -30980,17 +30971,17 @@ async function runController(opts) {
30980
30971
  if (sigintHandler) process.off("SIGINT", sigintHandler);
30981
30972
  if (ownsCwd) {
30982
30973
  try {
30983
- (0, import_node_fs33.rmSync)(cwd, { recursive: true, force: true });
30974
+ (0, import_node_fs34.rmSync)(cwd, { recursive: true, force: true });
30984
30975
  } catch {
30985
30976
  }
30986
30977
  }
30987
30978
  return exitCode ?? 0;
30988
30979
  }
30989
- var import_node_fs33, import_node_os19, import_node_path49;
30980
+ var import_node_fs34, import_node_os19, import_node_path49;
30990
30981
  var init_controller = __esm({
30991
30982
  "src/run-case/controller.ts"() {
30992
30983
  "use strict";
30993
- import_node_fs33 = require("fs");
30984
+ import_node_fs34 = require("fs");
30994
30985
  import_node_os19 = __toESM(require("os"), 1);
30995
30986
  import_node_path49 = __toESM(require("path"), 1);
30996
30987
  init_claude();
@@ -31228,7 +31219,7 @@ Env (advanced):
31228
31219
 
31229
31220
  // src/index.ts
31230
31221
  var import_node_path47 = __toESM(require("path"), 1);
31231
- var import_node_fs31 = __toESM(require("fs"), 1);
31222
+ var import_node_fs32 = __toESM(require("fs"), 1);
31232
31223
 
31233
31224
  // src/logger.ts
31234
31225
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -31266,7 +31257,7 @@ function createLogger(opts = {}) {
31266
31257
  }
31267
31258
 
31268
31259
  // src/session/store-factory.ts
31269
- var path5 = __toESM(require("path"), 1);
31260
+ var path6 = __toESM(require("path"), 1);
31270
31261
 
31271
31262
  // src/session/store.ts
31272
31263
  var import_node_fs3 = __toESM(require("fs"), 1);
@@ -31275,6 +31266,22 @@ init_protocol();
31275
31266
 
31276
31267
  // src/session/scope.ts
31277
31268
  var import_node_path3 = __toESM(require("path"), 1);
31269
+
31270
+ // src/session/user-workdir.ts
31271
+ var path3 = __toESM(require("path"), 1);
31272
+ function safeUserId(userId) {
31273
+ const base = userId.replace(/[^a-zA-Z0-9_\-.]/g, "_");
31274
+ const cleaned = base.replace(/^\.+/, (dots) => "_".repeat(dots.length));
31275
+ if (!cleaned) throw new Error(`deriveUserWorkDir: invalid userId: ${userId}`);
31276
+ return cleaned.replace(/\.\./g, "__");
31277
+ }
31278
+ function deriveUserWorkDir(userId, usersRoot) {
31279
+ if (!userId) throw new Error("deriveUserWorkDir: userId required");
31280
+ if (!usersRoot) throw new Error("deriveUserWorkDir: usersRoot required");
31281
+ return path3.join(usersRoot, safeUserId(userId));
31282
+ }
31283
+
31284
+ // src/session/scope.ts
31278
31285
  function scopeKey(scope) {
31279
31286
  if (scope.kind === "default") return "default";
31280
31287
  if (scope.mode === "guest") return `persona:${scope.personaId}:guest:${scope.capId}`;
@@ -31285,15 +31292,18 @@ function scopeSubPath(scope) {
31285
31292
  if (scope.mode === "guest") return [scope.personaId, "guests", scope.capId];
31286
31293
  return [scope.personaId, scope.mode];
31287
31294
  }
31288
- function metaFromScope(scope, personaRoot) {
31295
+ function metaFromScope(scope, personaRoot, usersRoot) {
31289
31296
  if (scope.kind === "default") return void 0;
31290
31297
  if (scope.mode === "owner") {
31291
31298
  return { idleKillEnabled: false, personaMode: "owner" };
31292
31299
  }
31300
+ const userWorkDir = deriveUserWorkDir(scope.capId, usersRoot);
31293
31301
  return {
31294
31302
  idleKillEnabled: true,
31295
31303
  personaMode: "guest",
31296
- extraSettings: import_node_path3.default.join(personaRoot, scope.personaId, ".clawd", "sandbox-settings.json")
31304
+ extraSettings: import_node_path3.default.join(personaRoot, scope.personaId, ".clawd", "sandbox-settings.json"),
31305
+ userWorkDir,
31306
+ addDirs: [userWorkDir]
31297
31307
  };
31298
31308
  }
31299
31309
  function scopeForFile(file, ownerPrincipalId) {
@@ -31403,10 +31413,10 @@ var SessionStoreFactory = class {
31403
31413
  }
31404
31414
  // ---- root path 派生(暴露给 migration / revoke cascade 等外部消费方) ----
31405
31415
  bareRoot() {
31406
- return path5.join(this.dataDir, "sessions");
31416
+ return path6.join(this.dataDir, "sessions");
31407
31417
  }
31408
31418
  vmOwnerRoot(personaId) {
31409
- return path5.join(
31419
+ return path6.join(
31410
31420
  this.dataDir,
31411
31421
  "personas",
31412
31422
  safeFileName(personaId),
@@ -31416,7 +31426,7 @@ var SessionStoreFactory = class {
31416
31426
  );
31417
31427
  }
31418
31428
  vmGuestRoot(personaId, capId) {
31419
- return path5.join(
31429
+ return path6.join(
31420
31430
  this.dataDir,
31421
31431
  "personas",
31422
31432
  safeFileName(personaId),
@@ -31550,7 +31560,12 @@ var LISTENER_TEMPLATE = `# \u8FDE\u63A5\u4E0A\u4E0B\u6587
31550
31560
  \u81EA\u6211\u4ECB\u7ECD\u65F6\u4F7F\u7528\u7B2C\u4E09\u4EBA\u79F0\u6307\u4EE3 owner\uFF0C\u4F8B\u5982\u300C\u6211\u662F {ownerLabel} \u7684 xxx \u52A9\u624B\uFF0C\u4F60\u53EF\u4EE5\u901A\u8FC7\u6211\u770B\u5230\u4ED6\u7684 xxx\u300D\u3002\u4E0D\u8981\u628A\u5BF9\u65B9\u5F53\u6210 owner\uFF0C\u4E0D\u8981\u66FF owner \u505A\u51FA\u627F\u8BFA\u3002`;
31551
31561
  function buildConnectionPrompt(args) {
31552
31562
  const template = args.mode === "owner" ? OWNER_TEMPLATE : LISTENER_TEMPLATE;
31553
- return template.replace(/\{ownerLabel\}/g, args.ownerLabel);
31563
+ let result = template.replace(/\{ownerLabel\}/g, args.ownerLabel);
31564
+ if (args.workDir) {
31565
+ result += `
31566
+ \u4F60\u7684\u6587\u4EF6\u5DE5\u4F5C\u76EE\u5F55\u662F ${args.workDir}\uFF0C\u6240\u6709\u6587\u4EF6\u8BFB\u5199\u90FD\u653E\u5728\u8FD9\u4E2A\u76EE\u5F55\u4E0B\u3002persona \u76EE\u5F55\u5BF9\u4F60\u53EA\u8BFB\uFF0C\u4E0D\u8981\u5F80\u91CC\u5199\u3002`;
31567
+ }
31568
+ return result;
31554
31569
  }
31555
31570
 
31556
31571
  // src/session/reducer.ts
@@ -31647,12 +31662,16 @@ function buildSpawnContext(state, deps) {
31647
31662
  ctx.personaMode = meta.personaMode;
31648
31663
  ctx.extraSystemPrompt = buildConnectionPrompt({
31649
31664
  mode: meta.personaMode,
31650
- ownerLabel: deps.ownerDisplayName
31665
+ ownerLabel: deps.ownerDisplayName,
31666
+ workDir: meta.userWorkDir
31651
31667
  });
31652
31668
  }
31653
31669
  if (meta?.extraSettings) {
31654
31670
  ctx.extraSettings = meta.extraSettings;
31655
31671
  }
31672
+ if (meta?.addDirs && meta.addDirs.length > 0) {
31673
+ ctx.addDirs = meta.addDirs;
31674
+ }
31656
31675
  return ctx;
31657
31676
  }
31658
31677
  function sessionInfoFrame(file) {
@@ -33097,7 +33116,7 @@ var SessionManager = class {
33097
33116
  newRunner(file, scope) {
33098
33117
  const adapter = this.deps.getAdapter(file.tool ?? "claude");
33099
33118
  const store = this.storeFor(scope);
33100
- const subSessionMeta = metaFromScope(scope, this.deps.personaRoot ?? "");
33119
+ const subSessionMeta = metaFromScope(scope, this.deps.personaRoot ?? "", this.deps.usersRoot ?? "");
33101
33120
  const attachmentGroup = this.deps.attachmentGroup;
33102
33121
  const runner = new SessionRunner(makeInitialState(file, subSessionMeta), {
33103
33122
  broadcastFrame: (frame, target) => this.routeFromRunner(frame, target),
@@ -34294,28 +34313,24 @@ var SessionManager = class {
34294
34313
 
34295
34314
  // src/persona/store.ts
34296
34315
  var fs7 = __toESM(require("fs"), 1);
34297
- var path9 = __toESM(require("path"), 1);
34316
+ var path10 = __toESM(require("path"), 1);
34298
34317
  init_protocol();
34299
- var DEFAULT_SETTINGS = {
34300
- permissions: {
34301
- // silent-deny for tools accessing outside cwd — no permission prompt shown to persona
34302
- defaultMode: "dontAsk"
34303
- },
34304
- sandbox: {
34305
- enabled: true,
34306
- // sandboxed Bash auto-runs without prompting the user
34307
- autoAllowBashIfSandboxed: true,
34308
- // prevents CC from retrying denied commands with dangerouslyDisableSandbox: true
34309
- allowUnsandboxedCommands: false,
34310
- filesystem: {
34311
- // deny home dir explicitly; default sandbox allows all reads, only limits writes to cwd
34312
- denyRead: ["~/"],
34313
- allowRead: ["."],
34314
- denyWrite: ["~/"],
34315
- allowWrite: ["."]
34316
- }
34317
- }
34318
- };
34318
+
34319
+ // src/tools/guest-settings.ts
34320
+ function buildGuestSettingsV1(paths) {
34321
+ const p2 = (dir) => dir.replace(/^\//, "");
34322
+ return {
34323
+ permissions: {
34324
+ defaultMode: "dontAsk",
34325
+ allow: ["Bash"],
34326
+ deny: [`Edit(//${p2(paths.personaDir)}/**)`, `Read(//${p2(paths.personaDir)}/.clawd/**)`]
34327
+ },
34328
+ // 先不加 OS 沙箱(见文件头说明);目录隔离靠 user-dir 分配 + --add-dir + RPC 守卫。
34329
+ sandbox: { enabled: false }
34330
+ };
34331
+ }
34332
+
34333
+ // src/persona/store.ts
34319
34334
  var PersonaStore = class {
34320
34335
  constructor(root) {
34321
34336
  this.root = root;
@@ -34323,13 +34338,13 @@ var PersonaStore = class {
34323
34338
  }
34324
34339
  root;
34325
34340
  personaDir(personaId) {
34326
- return path9.join(this.root, safeFileName(personaId));
34341
+ return path10.join(this.root, safeFileName(personaId));
34327
34342
  }
34328
34343
  metaPath(personaId) {
34329
- return path9.join(this.personaDir(personaId), ".clawd", "persona.json");
34344
+ return path10.join(this.personaDir(personaId), ".clawd", "persona.json");
34330
34345
  }
34331
34346
  claudeMdPath(personaId) {
34332
- return path9.join(this.personaDir(personaId), "CLAUDE.md");
34347
+ return path10.join(this.personaDir(personaId), "CLAUDE.md");
34333
34348
  }
34334
34349
  /**
34335
34350
  * Sandbox settings 落盘路径 —— 故意放在 `.clawd/` 而不是 `.claude/`,让 CC 的
@@ -34339,15 +34354,15 @@ var PersonaStore = class {
34339
34354
  * 加载 persona 人格,只有 listener 多一层 OS sandbox。
34340
34355
  */
34341
34356
  sandboxSettingsPath(personaId) {
34342
- return path9.join(this.personaDir(personaId), ".clawd", "sandbox-settings.json");
34357
+ return path10.join(this.personaDir(personaId), ".clawd", "sandbox-settings.json");
34343
34358
  }
34344
34359
  write(persona, personality) {
34345
34360
  const dir = this.personaDir(persona.personaId);
34346
- fs7.mkdirSync(path9.join(dir, ".clawd"), { recursive: true });
34361
+ fs7.mkdirSync(path10.join(dir, ".clawd"), { recursive: true });
34347
34362
  this.atomicWrite(this.claudeMdPath(persona.personaId), personality);
34348
34363
  this.atomicWrite(
34349
34364
  this.sandboxSettingsPath(persona.personaId),
34350
- JSON.stringify(DEFAULT_SETTINGS, null, 2)
34365
+ JSON.stringify(buildGuestSettingsV1({ personaDir: dir }), null, 2)
34351
34366
  );
34352
34367
  this.atomicWrite(this.metaPath(persona.personaId), JSON.stringify(persona, null, 2));
34353
34368
  }
@@ -34402,14 +34417,14 @@ var PersonaStore = class {
34402
34417
  }
34403
34418
  /** Persona 私有 skills 目录路径:<personaDir>/.claude/skills */
34404
34419
  skillsDir(personaId) {
34405
- return path9.join(this.personaDir(personaId), ".claude", "skills");
34420
+ return path10.join(this.personaDir(personaId), ".claude", "skills");
34406
34421
  }
34407
34422
  /**
34408
34423
  * Claude Code 项目级 settings 路径:`<personaDir>/.claude/settings.json`。
34409
34424
  * 这里只读 `enabledPlugins` 字段,由 owner 通过 CC `/plugin` 之类命令维护,daemon 不写。
34410
34425
  */
34411
34426
  claudeSettingsPath(personaId) {
34412
- return path9.join(this.personaDir(personaId), ".claude", "settings.json");
34427
+ return path10.join(this.personaDir(personaId), ".claude", "settings.json");
34413
34428
  }
34414
34429
  /**
34415
34430
  * 读取 persona 的 `.claude/settings.json` 中 `enabledPlugins` map,把 value === true
@@ -34443,7 +34458,7 @@ var PersonaStore = class {
34443
34458
  list() {
34444
34459
  if (!fs7.existsSync(this.root)) return [];
34445
34460
  return fs7.readdirSync(this.root).filter((name) => {
34446
- return fs7.existsSync(path9.join(this.root, name, ".clawd", "persona.json"));
34461
+ return fs7.existsSync(path10.join(this.root, name, ".clawd", "persona.json"));
34447
34462
  });
34448
34463
  }
34449
34464
  remove(personaId) {
@@ -34873,7 +34888,7 @@ var PersonaManager = class {
34873
34888
 
34874
34889
  // src/persona/seed.ts
34875
34890
  var fs9 = __toESM(require("fs"), 1);
34876
- var path11 = __toESM(require("path"), 1);
34891
+ var path12 = __toESM(require("path"), 1);
34877
34892
  var import_node_url = require("url");
34878
34893
  var import_meta = {};
34879
34894
  var DEFAULT_PERSONAS = [
@@ -34913,14 +34928,9 @@ var DEFAULT_PERSONAS = [
34913
34928
  label: "App Builder",
34914
34929
  model: "opus",
34915
34930
  iconKey: "assist",
34916
- public: false,
34917
- // 默认关 sandboxFC 部署链路(aliyun CLI + ossutil)/ pnpm install / curl daemon RPC
34918
- // 都需要走 sandbox 之外的能力。assistant sandboxed Bash 下会被 silent-deny 一堆操作,
34919
- // 影响正常工作流;老板明确说 app-builder 不要 sandbox。
34920
- sandboxOverride: {
34921
- permissions: { defaultMode: "dontAsk" },
34922
- sandbox: { enabled: false }
34923
- }
34931
+ public: false
34932
+ // 不设 sandboxOverrideowner 不注入 sandbox(天然不沙箱,部署/pnpm/curl freedom),
34933
+ // guest sandbox-settings.json guest 沙箱配置(store.write 默认生成)。
34924
34934
  },
34925
34935
  {
34926
34936
  personaId: "persona-developer",
@@ -34940,14 +34950,14 @@ var DEFAULT_PERSONAS = [
34940
34950
  function findDefaultsRoot() {
34941
34951
  const candidates = [];
34942
34952
  try {
34943
- const here = path11.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
34944
- candidates.push(path11.resolve(here, "defaults"));
34945
- candidates.push(path11.resolve(here, "persona-defaults"));
34953
+ const here = path12.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
34954
+ candidates.push(path12.resolve(here, "defaults"));
34955
+ candidates.push(path12.resolve(here, "persona-defaults"));
34946
34956
  } catch {
34947
34957
  }
34948
34958
  if (process.argv[1]) {
34949
- const argvDir = path11.dirname(process.argv[1]);
34950
- candidates.push(path11.resolve(argvDir, "persona-defaults"));
34959
+ const argvDir = path12.dirname(process.argv[1]);
34960
+ candidates.push(path12.resolve(argvDir, "persona-defaults"));
34951
34961
  }
34952
34962
  for (const c of candidates) {
34953
34963
  try {
@@ -34964,7 +34974,7 @@ function seedDefaultPersonas(args) {
34964
34974
  args.logger.info("persona.seed.skip", { personaId: entry.personaId, reason: "exists" });
34965
34975
  continue;
34966
34976
  }
34967
- const bundleDir = path11.join(args.defaultsRoot, entry.personaId);
34977
+ const bundleDir = path12.join(args.defaultsRoot, entry.personaId);
34968
34978
  if (!fs9.existsSync(bundleDir)) {
34969
34979
  args.logger.warn("persona.seed.skip", {
34970
34980
  personaId: entry.personaId,
@@ -34973,7 +34983,7 @@ function seedDefaultPersonas(args) {
34973
34983
  });
34974
34984
  continue;
34975
34985
  }
34976
- const claudeMdPath = path11.join(bundleDir, "CLAUDE.md");
34986
+ const claudeMdPath = path12.join(bundleDir, "CLAUDE.md");
34977
34987
  if (!fs9.existsSync(claudeMdPath)) {
34978
34988
  args.logger.warn("persona.seed.skip", {
34979
34989
  personaId: entry.personaId,
@@ -35005,8 +35015,8 @@ function seedDefaultPersonas(args) {
35005
35015
  function copyBundleExtras(srcDir, dstDir) {
35006
35016
  for (const entry of fs9.readdirSync(srcDir, { withFileTypes: true })) {
35007
35017
  if (entry.name === "CLAUDE.md" || entry.name === ".clawd") continue;
35008
- const srcPath = path11.join(srcDir, entry.name);
35009
- const dstPath = path11.join(dstDir, entry.name);
35018
+ const srcPath = path12.join(srcDir, entry.name);
35019
+ const dstPath = path12.join(dstDir, entry.name);
35010
35020
  if (entry.isDirectory()) {
35011
35021
  fs9.cpSync(srcPath, dstPath, { recursive: true, dereference: true });
35012
35022
  } else if (entry.isFile()) {
@@ -35018,14 +35028,14 @@ var DAEMON_MANAGED_PATHS = ["extension-kit", "CLAUDE.md", ".mcp.json"];
35018
35028
  function refreshDaemonManagedDirs(args) {
35019
35029
  const entries = args.entries ?? DEFAULT_PERSONAS;
35020
35030
  for (const entry of entries) {
35021
- const bundleDir = path11.join(args.defaultsRoot, entry.personaId);
35031
+ const bundleDir = path12.join(args.defaultsRoot, entry.personaId);
35022
35032
  if (!fs9.existsSync(bundleDir)) continue;
35023
35033
  const personaDir = args.store.personaDirPath(entry.personaId);
35024
35034
  if (!fs9.existsSync(personaDir)) continue;
35025
35035
  for (const relPath of DAEMON_MANAGED_PATHS) {
35026
- const srcPath = path11.join(bundleDir, relPath);
35036
+ const srcPath = path12.join(bundleDir, relPath);
35027
35037
  if (!fs9.existsSync(srcPath)) continue;
35028
- const dstPath = path11.join(personaDir, relPath);
35038
+ const dstPath = path12.join(personaDir, relPath);
35029
35039
  try {
35030
35040
  fs9.cpSync(srcPath, dstPath, { recursive: true, force: true, dereference: true });
35031
35041
  args.logger.info("persona.refresh.synced", {
@@ -40259,7 +40269,8 @@ function buildSessionHandlers(deps) {
40259
40269
  ensureSessionAccess(ctx, args.sessionId, "send");
40260
40270
  const sessionFile = manager.findOwnedSession(args.sessionId);
40261
40271
  const projectName = sessionFile?.appBuilderProject ?? null;
40262
- if (projectName && deps.devServerLifecycle && deps.appBuilderStore) {
40272
+ const projectStore = (sessionFile?.creatorPrincipalId ? deps.getProjectStore?.(sessionFile.creatorPrincipalId) : void 0) ?? deps.appBuilderStore;
40273
+ if (projectName && deps.devServerLifecycle && projectStore) {
40263
40274
  try {
40264
40275
  await deps.devServerLifecycle.stopForProject(projectName);
40265
40276
  } catch (err) {
@@ -40270,7 +40281,7 @@ function buildSessionHandlers(deps) {
40270
40281
  });
40271
40282
  }
40272
40283
  try {
40273
- await deps.appBuilderStore.delete(projectName);
40284
+ await projectStore.delete(projectName);
40274
40285
  } catch (err) {
40275
40286
  deps.logger?.warn("session-delete.delete-project-failed", {
40276
40287
  sessionId: args.sessionId,
@@ -40555,11 +40566,16 @@ function getAllowedPersonaIds(grants, action) {
40555
40566
  }
40556
40567
  return ids;
40557
40568
  }
40558
- function isGuestPathAllowed(grants, absPath, personaRoot, action = "read") {
40559
- const root = path41.resolve(personaRoot);
40569
+ function isGuestPathAllowed(grants, absPath, personaRoot, action = "read", userWorkDir) {
40560
40570
  const target = path41.resolve(absPath);
40561
- const sep3 = root.endsWith(path41.sep) ? "" : path41.sep;
40562
- if (!target.startsWith(root + sep3)) return false;
40571
+ if (userWorkDir) {
40572
+ const u = path41.resolve(userWorkDir);
40573
+ const usep = u.endsWith(path41.sep) ? "" : path41.sep;
40574
+ if (target === u || target.startsWith(u + usep)) return true;
40575
+ }
40576
+ const root = path41.resolve(personaRoot);
40577
+ const sep2 = root.endsWith(path41.sep) ? "" : path41.sep;
40578
+ if (!target.startsWith(root + sep2)) return false;
40563
40579
  const rel = path41.relative(root, target);
40564
40580
  if (!rel || rel.startsWith("..")) return false;
40565
40581
  const personaId = rel.split(path41.sep)[0];
@@ -40571,8 +40587,8 @@ function isGuestPathAllowed(grants, absPath, personaRoot, action = "read") {
40571
40587
  function personaIdFromPath(absPath, personaRoot) {
40572
40588
  const root = path41.resolve(personaRoot);
40573
40589
  const target = path41.resolve(absPath);
40574
- const sep3 = root.endsWith(path41.sep) ? "" : path41.sep;
40575
- if (!target.startsWith(root + sep3)) return null;
40590
+ const sep2 = root.endsWith(path41.sep) ? "" : path41.sep;
40591
+ if (!target.startsWith(root + sep2)) return null;
40576
40592
  const rel = path41.relative(root, target);
40577
40593
  if (!rel || rel.startsWith("..")) return null;
40578
40594
  const id = rel.split(path41.sep)[0];
@@ -40581,9 +40597,10 @@ function personaIdFromPath(absPath, personaRoot) {
40581
40597
 
40582
40598
  // src/handlers/history.ts
40583
40599
  init_protocol();
40584
- function assertGuestPath(ctx, absPath, personaRoot, method) {
40600
+ function assertGuestPath(ctx, absPath, personaRoot, method, usersRoot) {
40585
40601
  if (!ctx || ctx.principal.kind !== "guest" || !personaRoot) return;
40586
- if (!isGuestPathAllowed(ctx.grants, absPath, personaRoot, "read")) {
40602
+ const userWorkDir = usersRoot ? deriveUserWorkDir(ctx.principal.id, usersRoot) : void 0;
40603
+ if (!isGuestPathAllowed(ctx.grants, absPath, personaRoot, "read", userWorkDir)) {
40587
40604
  throw new ClawdError(
40588
40605
  ERROR_CODES.UNAUTHORIZED,
40589
40606
  `guest ${ctx.principal.id} cannot ${method} path ${absPath}`
@@ -40591,10 +40608,11 @@ function assertGuestPath(ctx, absPath, personaRoot, method) {
40591
40608
  }
40592
40609
  }
40593
40610
  function buildHistoryHandlers(deps) {
40594
- const { manager, history, store, personaRoot } = deps;
40611
+ const { manager, history, store, personaRoot, usersRoot } = deps;
40595
40612
  const projects = async (_frame, _client, ctx) => {
40596
40613
  const list2 = await history.listProjects();
40597
40614
  if (ctx?.principal.kind === "guest" && personaRoot) {
40615
+ const userWorkDir = usersRoot ? deriveUserWorkDir(ctx.principal.id, usersRoot) : void 0;
40598
40616
  const filtered = list2.filter((p2) => {
40599
40617
  const pid = personaIdFromPath(p2.projectPath, personaRoot);
40600
40618
  if (!pid) return false;
@@ -40602,7 +40620,8 @@ function buildHistoryHandlers(deps) {
40602
40620
  ctx.grants,
40603
40621
  path42.join(personaRoot, pid),
40604
40622
  personaRoot,
40605
- "read"
40623
+ "read",
40624
+ userWorkDir
40606
40625
  );
40607
40626
  });
40608
40627
  return { response: { type: "history:projects", projects: filtered } };
@@ -40643,21 +40662,22 @@ function buildHistoryHandlers(deps) {
40643
40662
  };
40644
40663
  const subagents = async (frame, _client, ctx) => {
40645
40664
  const args = HistorySubagentsArgs.parse(frame);
40646
- assertGuestPath(ctx, path42.resolve(args.cwd), personaRoot, "history:subagents");
40665
+ assertGuestPath(ctx, path42.resolve(args.cwd), personaRoot, "history:subagents", usersRoot);
40647
40666
  const subs = await history.listSubagents(args);
40648
40667
  return { response: { type: "history:subagents", subagents: subs } };
40649
40668
  };
40650
40669
  const subagentRead = async (frame, _client, ctx) => {
40651
40670
  const args = HistorySubagentReadArgs.parse(frame);
40652
- assertGuestPath(ctx, path42.resolve(args.cwd), personaRoot, "history:subagent-read");
40671
+ assertGuestPath(ctx, path42.resolve(args.cwd), personaRoot, "history:subagent-read", usersRoot);
40653
40672
  const res = await history.readSubagent(args);
40654
40673
  return { response: { type: "history:subagent-read", ...res } };
40655
40674
  };
40656
40675
  const recentDirs = async (_frame, _client, ctx) => {
40657
40676
  const dirs = listRecentDirs(store);
40658
40677
  if (ctx?.principal.kind === "guest" && personaRoot) {
40678
+ const userWorkDir = usersRoot ? deriveUserWorkDir(ctx.principal.id, usersRoot) : void 0;
40659
40679
  const filtered = dirs.filter(
40660
- (d) => isGuestPathAllowed(ctx.grants, path42.resolve(d.cwd), personaRoot, "read")
40680
+ (d) => isGuestPathAllowed(ctx.grants, path42.resolve(d.cwd), personaRoot, "read", userWorkDir)
40661
40681
  );
40662
40682
  return { response: { type: "history:recentDirs", dirs: filtered } };
40663
40683
  }
@@ -40700,9 +40720,10 @@ function filterGuestAgents(list, enabledPluginNames) {
40700
40720
  return false;
40701
40721
  });
40702
40722
  }
40703
- function assertGuestPath2(ctx, absPath, personaRoot, method) {
40723
+ function assertGuestPath2(ctx, absPath, personaRoot, method, usersRoot) {
40704
40724
  if (!ctx || ctx.principal.kind !== "guest" || !personaRoot) return;
40705
- if (!isGuestPathAllowed(ctx.grants, absPath, personaRoot, "read")) {
40725
+ const userWorkDir = usersRoot ? deriveUserWorkDir(ctx.principal.id, usersRoot) : void 0;
40726
+ if (!isGuestPathAllowed(ctx.grants, absPath, personaRoot, "read", userWorkDir)) {
40706
40727
  throw new ClawdError(
40707
40728
  ERROR_CODES.UNAUTHORIZED,
40708
40729
  `guest ${ctx.principal.id} cannot ${method} path ${absPath}`
@@ -40710,28 +40731,28 @@ function assertGuestPath2(ctx, absPath, personaRoot, method) {
40710
40731
  }
40711
40732
  }
40712
40733
  function buildWorkspaceHandlers(deps) {
40713
- const { workspace, skills, agents, personaRoot, personaManager } = deps;
40734
+ const { workspace, skills, agents, personaRoot, personaManager, usersRoot } = deps;
40714
40735
  const list = async (frame, _client, ctx) => {
40715
40736
  const args = WorkspaceListArgs.parse(frame);
40716
40737
  const isGuest = ctx?.principal.kind === "guest";
40717
40738
  const fallbackCwd = isGuest && personaRoot ? personaRoot : os15.homedir();
40718
40739
  const resolvedCwd = path43.resolve(args.cwd ?? fallbackCwd);
40719
40740
  const target = args.path ? path43.resolve(resolvedCwd, args.path) : resolvedCwd;
40720
- assertGuestPath2(ctx, target, personaRoot, "workspace:list");
40741
+ assertGuestPath2(ctx, target, personaRoot, "workspace:list", usersRoot);
40721
40742
  const res = workspace.list({ ...args, cwd: resolvedCwd });
40722
40743
  return { response: { type: "workspace:list", ...res } };
40723
40744
  };
40724
40745
  const read = async (frame, _client, ctx) => {
40725
40746
  const args = WorkspaceReadArgs.parse(frame);
40726
40747
  const target = path43.isAbsolute(args.path) ? path43.resolve(args.path) : path43.resolve(args.cwd, args.path);
40727
- assertGuestPath2(ctx, target, personaRoot, "workspace:read");
40748
+ assertGuestPath2(ctx, target, personaRoot, "workspace:read", usersRoot);
40728
40749
  const res = workspace.read(args);
40729
40750
  return { response: { type: "workspace:read", ...res } };
40730
40751
  };
40731
40752
  const skillsList = async (frame, _client, ctx) => {
40732
40753
  const args = SkillsListArgs.parse(frame);
40733
40754
  const cwdAbs = path43.resolve(args.cwd);
40734
- assertGuestPath2(ctx, cwdAbs, personaRoot, "skills:list");
40755
+ assertGuestPath2(ctx, cwdAbs, personaRoot, "skills:list", usersRoot);
40735
40756
  const list2 = skills.list(args);
40736
40757
  if (ctx?.principal.kind === "guest" && personaRoot) {
40737
40758
  const personaId = personaIdFromPath(cwdAbs, personaRoot);
@@ -40743,7 +40764,7 @@ function buildWorkspaceHandlers(deps) {
40743
40764
  const agentsList = async (frame, _client, ctx) => {
40744
40765
  const args = AgentsListArgs.parse(frame);
40745
40766
  const cwdAbs = path43.resolve(args.cwd);
40746
- assertGuestPath2(ctx, cwdAbs, personaRoot, "agents:list");
40767
+ assertGuestPath2(ctx, cwdAbs, personaRoot, "agents:list", usersRoot);
40747
40768
  const list2 = agents.list(args);
40748
40769
  if (ctx?.principal.kind === "guest" && personaRoot) {
40749
40770
  const personaId = personaIdFromPath(cwdAbs, personaRoot);
@@ -40845,9 +40866,10 @@ async function listGitBranches(cwd) {
40845
40866
  }
40846
40867
 
40847
40868
  // src/handlers/git.ts
40848
- function assertGuestCwd(ctx, cwd, personaRoot, method) {
40869
+ function assertGuestCwd(ctx, cwd, personaRoot, method, usersRoot) {
40849
40870
  if (!ctx || ctx.principal.kind !== "guest" || !personaRoot) return;
40850
- if (!isGuestPathAllowed(ctx.grants, path45.resolve(cwd), personaRoot, "read")) {
40871
+ const userWorkDir = usersRoot ? deriveUserWorkDir(ctx.principal.id, usersRoot) : void 0;
40872
+ if (!isGuestPathAllowed(ctx.grants, path45.resolve(cwd), personaRoot, "read", userWorkDir)) {
40851
40873
  throw new ClawdError(
40852
40874
  ERROR_CODES.UNAUTHORIZED,
40853
40875
  `guest ${ctx.principal.id} cannot ${method} cwd ${cwd}`
@@ -40855,22 +40877,22 @@ function assertGuestCwd(ctx, cwd, personaRoot, method) {
40855
40877
  }
40856
40878
  }
40857
40879
  function buildGitHandlers(deps) {
40858
- const { personaRoot } = deps;
40880
+ const { personaRoot, usersRoot } = deps;
40859
40881
  const root = async (frame, _client, ctx) => {
40860
40882
  const args = GitRootArgs.parse(frame);
40861
- assertGuestCwd(ctx, args.cwd, personaRoot, "git:root");
40883
+ assertGuestCwd(ctx, args.cwd, personaRoot, "git:root", usersRoot);
40862
40884
  const res = await getGitRoot(args.cwd);
40863
40885
  return { response: { type: "git:root", ...res } };
40864
40886
  };
40865
40887
  const branch = async (frame, _client, ctx) => {
40866
40888
  const args = GitBranchArgs.parse(frame);
40867
- assertGuestCwd(ctx, args.cwd, personaRoot, "git:branch");
40889
+ assertGuestCwd(ctx, args.cwd, personaRoot, "git:branch", usersRoot);
40868
40890
  const res = await readGitBranch(args.cwd);
40869
40891
  return { response: { type: "git:branch", ...res } };
40870
40892
  };
40871
40893
  const branches = async (frame, _client, ctx) => {
40872
40894
  const args = GitBranchesArgs.parse(frame);
40873
- assertGuestCwd(ctx, args.cwd, personaRoot, "git:branches");
40895
+ assertGuestCwd(ctx, args.cwd, personaRoot, "git:branches", usersRoot);
40874
40896
  const res = await listGitBranches(args.cwd);
40875
40897
  return { response: { type: "git:branches", ...res } };
40876
40898
  };
@@ -41364,9 +41386,10 @@ var import_node_path33 = __toESM(require("path"), 1);
41364
41386
  init_protocol();
41365
41387
  init_protocol();
41366
41388
  var DEFAULT_TTL_SECONDS = 24 * 3600;
41367
- function assertGuestAttachmentPath(ctx, absPath, personaRoot, method) {
41389
+ function assertGuestAttachmentPath(ctx, absPath, personaRoot, method, usersRoot) {
41368
41390
  if (!ctx || ctx.principal.kind !== "guest" || !personaRoot) return;
41369
- if (!isGuestPathAllowed(ctx.grants, absPath, personaRoot, "read")) {
41391
+ const userWorkDir = usersRoot ? deriveUserWorkDir(ctx.principal.id, usersRoot) : void 0;
41392
+ if (!isGuestPathAllowed(ctx.grants, absPath, personaRoot, "read", userWorkDir)) {
41370
41393
  throw new ClawdError(
41371
41394
  ERROR_CODES.UNAUTHORIZED,
41372
41395
  `guest ${ctx.principal.id} cannot ${method} path ${absPath}`
@@ -41416,7 +41439,7 @@ function buildAttachmentHandlers(deps) {
41416
41439
  }
41417
41440
  const cwdAbs = import_node_path33.default.resolve(sessionFile.cwd);
41418
41441
  const candidateAbs = import_node_path33.default.isAbsolute(args.relPath) ? import_node_path33.default.resolve(args.relPath) : import_node_path33.default.resolve(cwdAbs, args.relPath);
41419
- assertGuestAttachmentPath(ctx, candidateAbs, deps.personaRoot, "attachment.signUrl");
41442
+ assertGuestAttachmentPath(ctx, candidateAbs, deps.personaRoot, "attachment.signUrl", deps.usersRoot);
41420
41443
  const entries = deps.groupFileStore.list(scope, args.sessionId);
41421
41444
  const entry = entries.find((e) => {
41422
41445
  const storedAbs = import_node_path33.default.isAbsolute(e.relPath) ? import_node_path33.default.resolve(e.relPath) : import_node_path33.default.resolve(cwdAbs, e.relPath);
@@ -41443,7 +41466,7 @@ function buildAttachmentHandlers(deps) {
41443
41466
  if (!ctx || ctx.principal.kind !== "guest" || !deps.personaRoot || !deps.sessionStore) return;
41444
41467
  const f = deps.sessionStore.read(sessionId);
41445
41468
  if (!f) return;
41446
- assertGuestAttachmentPath(ctx, import_node_path33.default.resolve(f.cwd), deps.personaRoot, method);
41469
+ assertGuestAttachmentPath(ctx, import_node_path33.default.resolve(f.cwd), deps.personaRoot, method, deps.usersRoot);
41447
41470
  }
41448
41471
  const groupAdd = async (frame, _client, ctx) => {
41449
41472
  if (!deps.groupFileStore || !deps.getSessionScope) {
@@ -42603,6 +42626,8 @@ async function recoverInterruptedJobs(deps) {
42603
42626
  // src/handlers/app-builder.ts
42604
42627
  init_protocol();
42605
42628
  var import_node_path40 = require("path");
42629
+ var import_node_fs30 = require("fs");
42630
+ var APP_BUILDER_PERSONAS = ["persona-app-builder"];
42606
42631
  var PUBLISH_SCRIPT_REL = "extension-kit/scripts/publish.sh";
42607
42632
  var DEV_SERVER_READY_TIMEOUT_MS = 3e4;
42608
42633
  async function recoverInterruptedPublishJobs(store, logger) {
@@ -42613,6 +42638,29 @@ async function recoverInterruptedPublishJobs(store, logger) {
42613
42638
  }
42614
42639
  function buildAppBuilderHandlers(deps) {
42615
42640
  const { store, devServerLifecycle, sessionManager, getTunnelUrl, broadcastProjectUpdated } = deps;
42641
+ function resolveStore(opts) {
42642
+ if (!deps.getStore) return store;
42643
+ if (opts.sessionId) {
42644
+ const s = sessionManager.findOwnedSession(opts.sessionId);
42645
+ if (s?.creatorPrincipalId) return deps.getStore(s.creatorPrincipalId);
42646
+ }
42647
+ if (opts.projectName) {
42648
+ const s = sessionManager.findOwnedSessionByAppBuilderProject(opts.projectName);
42649
+ if (s?.creatorPrincipalId) return deps.getStore(s.creatorPrincipalId);
42650
+ }
42651
+ if (opts.ctx) return deps.getStore(opts.ctx.principal.id);
42652
+ return store;
42653
+ }
42654
+ function ensureProjectWriteAccess(ctx, opts) {
42655
+ if (!ctx) return;
42656
+ const sessionId = opts.sessionId ?? (opts.projectName ? sessionManager.findOwnedSessionByAppBuilderProject(opts.projectName)?.sessionId : void 0);
42657
+ if (!sessionId) return;
42658
+ if (!canAccessSession(ctx, sessionId, "send", {
42659
+ readSession: (sid) => sessionManager.findOwnedSession(sid) ?? null
42660
+ })) {
42661
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, `cannot write project of session ${sessionId}`);
42662
+ }
42663
+ }
42616
42664
  const stages = /* @__PURE__ */ new Map();
42617
42665
  const publishRegistry = new PublishJobRegistry();
42618
42666
  function withPublishJobStatus(meta) {
@@ -42629,13 +42677,13 @@ function buildAppBuilderHandlers(deps) {
42629
42677
  if (recorded) return recorded;
42630
42678
  return devServerLifecycle.isRunning(name) ? { stage: "running" } : { stage: "stopped" };
42631
42679
  }
42632
- async function setStageAndBroadcast(name, stage, reason) {
42680
+ async function setStageAndBroadcast(name, stage, reason, s = store) {
42633
42681
  if (reason !== void 0) {
42634
42682
  stages.set(name, { stage, reason });
42635
42683
  } else {
42636
42684
  stages.set(name, { stage });
42637
42685
  }
42638
- const projects = await store.list();
42686
+ const projects = await s.list();
42639
42687
  const meta = projects.find((p2) => p2.name === name);
42640
42688
  if (!meta) {
42641
42689
  deps.logger?.warn("app-builder.stage-broadcast.project-missing", { name, stage });
@@ -42657,28 +42705,54 @@ function buildAppBuilderHandlers(deps) {
42657
42705
  ...reason !== void 0 ? { stageReason: reason } : {}
42658
42706
  });
42659
42707
  }
42660
- const listProjects = async () => {
42661
- const projects = await store.list();
42708
+ async function listAllUsersProjects() {
42709
+ if (!deps.usersRoot || !deps.getStore) return [];
42710
+ const getStore = deps.getStore;
42711
+ const userIds = await import_node_fs30.promises.readdir(deps.usersRoot).catch(() => []);
42712
+ const perUser = await Promise.all(
42713
+ userIds.map((uid) => getStore(uid).list().catch(() => []))
42714
+ );
42715
+ return perUser.flat();
42716
+ }
42717
+ function toProjectWithStatus(p2) {
42718
+ const s = getStageOf(p2.name);
42662
42719
  return {
42663
- response: {
42664
- projects: projects.map((p2) => {
42665
- const s = getStageOf(p2.name);
42666
- const publishJobWithStatus = withPublishJobStatus(p2);
42667
- return {
42668
- ...p2,
42669
- isRunning: devServerLifecycle.isRunning(p2.name),
42670
- boundSessionId: devServerLifecycle.boundSessionId(p2.name),
42671
- stage: s.stage,
42672
- ...s.reason !== void 0 ? { stageReason: s.reason } : {},
42673
- // publishJob spread ...p 之后覆写:原始 ProjectMetadata.publishJob 没有 status,
42674
- // 这里实时拼上 status 字段;字段不存在时显式 undefined(覆盖掉 spread 的可能值)。
42675
- publishJob: publishJobWithStatus
42676
- };
42677
- })
42678
- }
42679
- };
42720
+ ...p2,
42721
+ isRunning: devServerLifecycle.isRunning(p2.name),
42722
+ boundSessionId: devServerLifecycle.boundSessionId(p2.name),
42723
+ stage: s.stage,
42724
+ ...s.reason !== void 0 ? { stageReason: s.reason } : {},
42725
+ publishJob: withPublishJobStatus(p2)
42726
+ };
42727
+ }
42728
+ const listProjects = async (_frame, _client, ctx) => {
42729
+ let metas;
42730
+ if (ctx?.principal.kind === "owner" && deps.usersRoot && deps.getStore) {
42731
+ metas = await listAllUsersProjects();
42732
+ } else {
42733
+ metas = await resolveStore({ ctx }).list();
42734
+ }
42735
+ return { response: { projects: metas.map(toProjectWithStatus) } };
42680
42736
  };
42681
- const createProject = async (frame) => {
42737
+ const getProject = async (frame, _client, ctx) => {
42738
+ const f = frame;
42739
+ if (typeof f.sessionId !== "string" || !f.sessionId) {
42740
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, "getProject: sessionId required");
42741
+ }
42742
+ if (ctx && !canAccessSession(ctx, f.sessionId, "read", {
42743
+ readSession: (sid) => sessionManager.findOwnedSession(sid) ?? null
42744
+ })) {
42745
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, `cannot read session ${f.sessionId}`);
42746
+ }
42747
+ const session = sessionManager.findOwnedSession(f.sessionId);
42748
+ const projectName = session?.appBuilderProject ?? null;
42749
+ if (!projectName) return { response: { project: null } };
42750
+ const meta = (await resolveStore({ ctx, sessionId: f.sessionId }).list()).find(
42751
+ (p2) => p2.name === projectName
42752
+ );
42753
+ return { response: { project: meta ? toProjectWithStatus(meta) : null } };
42754
+ };
42755
+ const createProject = async (frame, _client, ctx) => {
42682
42756
  const f = frame;
42683
42757
  if (typeof f.sessionId !== "string" || !f.sessionId) {
42684
42758
  throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, "createProject: sessionId required");
@@ -42690,10 +42764,10 @@ function buildAppBuilderHandlers(deps) {
42690
42764
  if (!session) {
42691
42765
  throw new ClawdError(ERROR_CODES.SESSION_NOT_FOUND, `session ${f.sessionId} not found`);
42692
42766
  }
42693
- if (session.ownerPersonaId !== "persona-app-builder") {
42767
+ if (!APP_BUILDER_PERSONAS.includes(session.ownerPersonaId ?? "")) {
42694
42768
  throw new ClawdError(
42695
42769
  ERROR_CODES.WRONG_PERSONA,
42696
- `createProject only allowed in persona-app-builder sessions (got: ${session.ownerPersonaId ?? "<none>"})`
42770
+ `createProject only allowed in app-builder persona sessions (got: ${session.ownerPersonaId ?? "<none>"})`
42697
42771
  );
42698
42772
  }
42699
42773
  if (session.appBuilderProject) {
@@ -42702,10 +42776,12 @@ function buildAppBuilderHandlers(deps) {
42702
42776
  `session ${f.sessionId} already bound to project ${session.appBuilderProject}`
42703
42777
  );
42704
42778
  }
42779
+ const userStore = resolveStore({ ctx, sessionId: f.sessionId });
42705
42780
  deps.logger?.info("app-builder.create-project.start", { name: f.name, sessionId: f.sessionId });
42706
- const project = await store.create(f.name);
42781
+ const reservedPorts = new Set((await listAllUsersProjects()).map((p2) => p2.port));
42782
+ const project = await userStore.create(f.name, reservedPorts);
42707
42783
  try {
42708
- const scaffoldResult = await store.scaffold(project.name);
42784
+ const scaffoldResult = await userStore.scaffold(project.name, void 0, deps.personaRoot);
42709
42785
  deps.logger?.info("app-builder.scaffold.done", {
42710
42786
  name: project.name,
42711
42787
  stdout: scaffoldResult.stdout.slice(0, 200)
@@ -42713,7 +42789,7 @@ function buildAppBuilderHandlers(deps) {
42713
42789
  } catch (err) {
42714
42790
  const message = err instanceof Error ? err.message : String(err);
42715
42791
  deps.logger?.warn("app-builder.scaffold.failed", { name: project.name, error: message });
42716
- await store.delete(project.name).catch((delErr) => {
42792
+ await userStore.delete(project.name).catch((delErr) => {
42717
42793
  deps.logger?.warn("app-builder.scaffold.rollback-failed", {
42718
42794
  name: project.name,
42719
42795
  error: delErr instanceof Error ? delErr.message : String(delErr)
@@ -42725,46 +42801,51 @@ function buildAppBuilderHandlers(deps) {
42725
42801
  sessionId: f.sessionId,
42726
42802
  patch: { appBuilderProject: project.name }
42727
42803
  });
42728
- await setStageAndBroadcast(project.name, "install-pending");
42804
+ await setStageAndBroadcast(project.name, "install-pending", void 0, userStore);
42729
42805
  deps.logger?.info("app-builder.create-project.done", {
42730
42806
  name: project.name,
42731
42807
  port: project.port,
42732
42808
  sessionId: f.sessionId
42733
42809
  });
42734
- return { response: { project }, broadcast };
42810
+ return { response: { project, projectDir: userStore.projectDir(project.name) }, broadcast };
42735
42811
  };
42736
- const deleteProject = async (frame) => {
42812
+ const deleteProject = async (frame, _client, ctx) => {
42737
42813
  const name = frame.name;
42738
42814
  if (typeof name !== "string") throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, "deleteProject: name required");
42815
+ const userStore = resolveStore({ ctx, projectName: name });
42739
42816
  await devServerLifecycle.stopForProject(name);
42740
- await store.delete(name);
42817
+ await userStore.delete(name);
42741
42818
  stages.delete(name);
42742
42819
  return { response: {} };
42743
42820
  };
42744
- const updateProjectPort = async (frame) => {
42821
+ const updateProjectPort = async (frame, _client, ctx) => {
42745
42822
  const f = frame;
42746
42823
  if (typeof f.name !== "string") throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, "updateProjectPort: name required");
42747
42824
  if (typeof f.newPort !== "number") throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, "updateProjectPort: newPort required");
42825
+ const userStore = resolveStore({ ctx, projectName: f.name });
42748
42826
  await devServerLifecycle.stopForProject(f.name);
42749
- const project = await store.updatePort(f.name, f.newPort);
42750
- await setStageAndBroadcast(f.name, "stopped");
42827
+ const project = await userStore.updatePort(f.name, f.newPort);
42828
+ await setStageAndBroadcast(f.name, "stopped", void 0, userStore);
42751
42829
  return { response: { project } };
42752
42830
  };
42753
- const setProdUrl = async (frame) => {
42831
+ const setProdUrl = async (frame, _client, ctx) => {
42754
42832
  const f = frame;
42755
42833
  if (typeof f.name !== "string") throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, "setProdUrl: name required");
42756
42834
  if (typeof f.url !== "string") throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, "setProdUrl: url required");
42757
- const project = await store.setProdUrl(f.name, f.url);
42835
+ ensureProjectWriteAccess(ctx, { projectName: f.name });
42836
+ const userStore = resolveStore({ ctx, projectName: f.name });
42837
+ const project = await userStore.setProdUrl(f.name, f.url);
42758
42838
  const current = getStageOf(f.name);
42759
- await setStageAndBroadcast(f.name, current.stage, current.reason);
42839
+ await setStageAndBroadcast(f.name, current.stage, current.reason, userStore);
42760
42840
  return { response: { project } };
42761
42841
  };
42762
- const startDevServer = async (frame) => {
42842
+ const startDevServer = async (frame, _client, ctx) => {
42763
42843
  const f = frame;
42764
42844
  if (typeof f.sessionId !== "string" || !f.sessionId) {
42765
42845
  throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, "startDevServer: sessionId required");
42766
42846
  }
42767
42847
  const force = f.force === true;
42848
+ ensureProjectWriteAccess(ctx, { sessionId: f.sessionId });
42768
42849
  const session = sessionManager.findOwnedSession(f.sessionId);
42769
42850
  if (!session) throw new ClawdError(ERROR_CODES.SESSION_NOT_FOUND, `session ${f.sessionId} not found`);
42770
42851
  const projectName = session.appBuilderProject;
@@ -42774,7 +42855,8 @@ function buildAppBuilderHandlers(deps) {
42774
42855
  `session ${f.sessionId} is not bound to any project; cannot start dev server`
42775
42856
  );
42776
42857
  }
42777
- const projects = await store.list();
42858
+ const userStore = resolveStore({ ctx, sessionId: f.sessionId });
42859
+ const projects = await userStore.list();
42778
42860
  const meta = projects.find((p2) => p2.name === projectName);
42779
42861
  if (!meta) {
42780
42862
  throw new ClawdError(ERROR_CODES.SESSION_NOT_FOUND, `project ${projectName} not found on disk`);
@@ -42794,20 +42876,20 @@ function buildAppBuilderHandlers(deps) {
42794
42876
  deps.logger?.info("app-builder.start-dev-server.force-stop", { name: projectName });
42795
42877
  await devServerLifecycle.stopForProject(projectName);
42796
42878
  }
42797
- await setStageAndBroadcast(projectName, "starting-dev-server");
42879
+ await setStageAndBroadcast(projectName, "starting-dev-server", void 0, userStore);
42798
42880
  const tunnelUrl = getTunnelUrl();
42799
42881
  const tunnelHost = tunnelUrl ? new URL(tunnelUrl.replace(/^wss?:\/\//i, "https://")).host : null;
42800
42882
  devServerLifecycle.startForSession({
42801
42883
  sessionId: f.sessionId,
42802
42884
  projectName,
42803
- cwd: store.projectDir(projectName),
42885
+ cwd: userStore.projectDir(projectName),
42804
42886
  port: meta.port,
42805
42887
  tunnelHost,
42806
42888
  devCommand: meta.devCommand
42807
42889
  });
42808
42890
  try {
42809
42891
  await devServerLifecycle.awaitReady(projectName, { timeoutMs: DEV_SERVER_READY_TIMEOUT_MS });
42810
- await setStageAndBroadcast(projectName, "running");
42892
+ await setStageAndBroadcast(projectName, "running", void 0, userStore);
42811
42893
  } catch (err) {
42812
42894
  const reason = err instanceof Error ? err.message : "dev server not ready";
42813
42895
  deps.logger?.warn("app-builder.start-dev-server.ready-failed", {
@@ -42815,11 +42897,11 @@ function buildAppBuilderHandlers(deps) {
42815
42897
  sessionId: f.sessionId,
42816
42898
  reason
42817
42899
  });
42818
- await setStageAndBroadcast(projectName, "failed", reason);
42900
+ await setStageAndBroadcast(projectName, "failed", reason, userStore);
42819
42901
  }
42820
42902
  return { response: { project: meta } };
42821
42903
  };
42822
- const reportStage = async (frame) => {
42904
+ const reportStage = async (frame, _client, ctx) => {
42823
42905
  const f = frame;
42824
42906
  if (typeof f.sessionId !== "string" || !f.sessionId) {
42825
42907
  throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, "reportStage: sessionId required");
@@ -42849,14 +42931,16 @@ function buildAppBuilderHandlers(deps) {
42849
42931
  reason,
42850
42932
  sessionId: f.sessionId
42851
42933
  });
42852
- await setStageAndBroadcast(projectName, f.stage, reason);
42934
+ const userStore = resolveStore({ ctx, sessionId: f.sessionId });
42935
+ await setStageAndBroadcast(projectName, f.stage, reason, userStore);
42853
42936
  return { response: { ok: true } };
42854
42937
  };
42855
- const stopDevServer = async (frame) => {
42938
+ const stopDevServer = async (frame, _client, ctx) => {
42856
42939
  const f = frame;
42857
42940
  if (typeof f.sessionId !== "string" || !f.sessionId) {
42858
42941
  throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, "stopDevServer: sessionId required");
42859
42942
  }
42943
+ ensureProjectWriteAccess(ctx, { sessionId: f.sessionId });
42860
42944
  const session = sessionManager.findOwnedSession(f.sessionId);
42861
42945
  if (!session) {
42862
42946
  throw new ClawdError(ERROR_CODES.SESSION_NOT_FOUND, `session ${f.sessionId} not found`);
@@ -42868,6 +42952,7 @@ function buildAppBuilderHandlers(deps) {
42868
42952
  `session ${f.sessionId} is not bound to any project; cannot stop dev server`
42869
42953
  );
42870
42954
  }
42955
+ const userStore = resolveStore({ ctx, sessionId: f.sessionId });
42871
42956
  deps.logger?.info("app-builder.stop-dev-server.called", {
42872
42957
  name: projectName,
42873
42958
  sessionId: f.sessionId,
@@ -42877,7 +42962,7 @@ function buildAppBuilderHandlers(deps) {
42877
42962
  await devServerLifecycle.stopForProject(projectName);
42878
42963
  }
42879
42964
  try {
42880
- const projects = await store.list();
42965
+ const projects = await userStore.list();
42881
42966
  const meta = projects.find((p2) => p2.name === projectName);
42882
42967
  if (meta) {
42883
42968
  await killPortOccupants(meta.port, devServerLifecycle.getOwnedPids(), deps.logger);
@@ -42888,12 +42973,14 @@ function buildAppBuilderHandlers(deps) {
42888
42973
  error: err instanceof Error ? err.message : String(err)
42889
42974
  });
42890
42975
  }
42891
- await setStageAndBroadcast(projectName, "stopped");
42976
+ await setStageAndBroadcast(projectName, "stopped", void 0, userStore);
42892
42977
  return { response: { ok: true } };
42893
42978
  };
42894
- const publish = async (frame) => {
42979
+ const publish = async (frame, _client, ctx) => {
42895
42980
  const args = PublishArgsSchema.parse(frame);
42896
- const projects = await store.list();
42981
+ ensureProjectWriteAccess(ctx, { projectName: args.name });
42982
+ const userStore = resolveStore({ ctx, projectName: args.name });
42983
+ const projects = await userStore.list();
42897
42984
  const project = projects.find((p2) => p2.name === args.name);
42898
42985
  if (!project) {
42899
42986
  throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, `publish: project "${args.name}" not found`);
@@ -42911,7 +42998,7 @@ function buildAppBuilderHandlers(deps) {
42911
42998
  previousJobId: project.publishJob.jobId,
42912
42999
  previousStage: project.publishJob.stage
42913
43000
  });
42914
- await store.clearPublishJob(args.name);
43001
+ await userStore.clearPublishJob(args.name);
42915
43002
  }
42916
43003
  const scriptPath = (0, import_node_path40.join)(deps.personaRoot, PUBLISH_SCRIPT_REL);
42917
43004
  deps.logger?.info("app-builder.publish.start", {
@@ -42923,19 +43010,19 @@ function buildAppBuilderHandlers(deps) {
42923
43010
  {
42924
43011
  registry: publishRegistry,
42925
43012
  setPublishJob: async (name, job) => {
42926
- await store.setPublishJob(name, job);
43013
+ await userStore.setPublishJob(name, job);
42927
43014
  const current = getStageOf(name);
42928
- await setStageAndBroadcast(name, current.stage, current.reason);
43015
+ await setStageAndBroadcast(name, current.stage, current.reason, userStore);
42929
43016
  },
42930
43017
  clearPublishJob: async (name) => {
42931
- await store.clearPublishJob(name);
43018
+ await userStore.clearPublishJob(name);
42932
43019
  const current = getStageOf(name);
42933
- await setStageAndBroadcast(name, current.stage, current.reason);
43020
+ await setStageAndBroadcast(name, current.stage, current.reason, userStore);
42934
43021
  },
42935
43022
  setProdUrl: async (name, url) => {
42936
- await store.setProdUrl(name, url);
43023
+ await userStore.setProdUrl(name, url);
42937
43024
  const current = getStageOf(name);
42938
- await setStageAndBroadcast(name, current.stage, current.reason);
43025
+ await setStageAndBroadcast(name, current.stage, current.reason, userStore);
42939
43026
  },
42940
43027
  broadcast: (f) => {
42941
43028
  broadcastProjectUpdated(f);
@@ -42946,7 +43033,7 @@ function buildAppBuilderHandlers(deps) {
42946
43033
  deps.broadcastSessionFrame(entry.frame, entry.target);
42947
43034
  }
42948
43035
  },
42949
- projectDir: (name) => store.projectDir(name),
43036
+ projectDir: (name) => userStore.projectDir(name),
42950
43037
  logger: deps.logger
42951
43038
  },
42952
43039
  {
@@ -42957,9 +43044,11 @@ function buildAppBuilderHandlers(deps) {
42957
43044
  );
42958
43045
  return { response: { ...result } };
42959
43046
  };
42960
- const dismissPublishJob = async (frame) => {
43047
+ const dismissPublishJob = async (frame, _client, ctx) => {
42961
43048
  const args = DismissPublishJobArgsSchema.parse(frame);
42962
- const projects = await store.list();
43049
+ ensureProjectWriteAccess(ctx, { projectName: args.name });
43050
+ const userStore = resolveStore({ ctx, projectName: args.name });
43051
+ const projects = await userStore.list();
42963
43052
  const project = projects.find((p2) => p2.name === args.name);
42964
43053
  if (!project) {
42965
43054
  throw new ClawdError(
@@ -42982,14 +43071,15 @@ function buildAppBuilderHandlers(deps) {
42982
43071
  jobId: project.publishJob.jobId,
42983
43072
  stage: project.publishJob.stage
42984
43073
  });
42985
- await store.clearPublishJob(args.name);
43074
+ await userStore.clearPublishJob(args.name);
42986
43075
  const current = getStageOf(args.name);
42987
- await setStageAndBroadcast(args.name, current.stage, current.reason);
43076
+ await setStageAndBroadcast(args.name, current.stage, current.reason, userStore);
42988
43077
  }
42989
43078
  return { response: { ok: true } };
42990
43079
  };
42991
43080
  return {
42992
43081
  "appBuilder:listProjects": listProjects,
43082
+ "appBuilder:getProject": getProject,
42993
43083
  "appBuilder:createProject": createProject,
42994
43084
  "appBuilder:deleteProject": deleteProject,
42995
43085
  "appBuilder:updateProjectPort": updateProjectPort,
@@ -43094,6 +43184,7 @@ function buildMethodHandlers(deps) {
43094
43184
  ...deps,
43095
43185
  devServerLifecycle: deps.devServerLifecycle,
43096
43186
  appBuilderStore: deps.appBuilderStore,
43187
+ getProjectStore: deps.getProjectStore,
43097
43188
  getTunnelUrl: deps.getTunnelUrl,
43098
43189
  logger: deps.logger
43099
43190
  }),
@@ -43138,6 +43229,8 @@ function buildMethodHandlers(deps) {
43138
43229
  }),
43139
43230
  ...buildAppBuilderHandlers({
43140
43231
  store: deps.appBuilderStore,
43232
+ getStore: deps.getProjectStore,
43233
+ usersRoot: deps.usersRoot,
43141
43234
  devServerLifecycle: deps.devServerLifecycle,
43142
43235
  sessionManager: deps.manager,
43143
43236
  getTunnelUrl: deps.getTunnelUrl,
@@ -43153,7 +43246,7 @@ function buildMethodHandlers(deps) {
43153
43246
  }
43154
43247
 
43155
43248
  // src/app-builder/project-store.ts
43156
- var import_node_fs30 = require("fs");
43249
+ var import_node_fs31 = require("fs");
43157
43250
  var import_node_child_process9 = require("child_process");
43158
43251
  var import_node_path43 = require("path");
43159
43252
  init_protocol();
@@ -43163,11 +43256,11 @@ var SCAFFOLD_SCRIPT_REL = "extension-kit/scripts/new-extension.sh";
43163
43256
  var DEFAULT_TEMPLATE = "nestjs-react";
43164
43257
  var SCAFFOLD_TIMEOUT_MS = 3e4;
43165
43258
  var ProjectStore = class {
43166
- /** @param personaRoot persona 目录,例如 `~/.clawd/personas/persona-app-builder/` */
43167
- constructor(personaRoot) {
43168
- this.personaRoot = personaRoot;
43259
+ /** @param root projects 父目录,例如 `~/.clawd/users/<userId>/`(v2 per-user) */
43260
+ constructor(root) {
43261
+ this.root = root;
43169
43262
  }
43170
- personaRoot;
43263
+ root;
43171
43264
  /** projects/<name>/.clawd-project.json 路径 */
43172
43265
  metaPath(name) {
43173
43266
  return (0, import_node_path43.join)(this.projectsRoot(), name, META_FILE);
@@ -43177,12 +43270,12 @@ var ProjectStore = class {
43177
43270
  return (0, import_node_path43.join)(this.projectsRoot(), name);
43178
43271
  }
43179
43272
  projectsRoot() {
43180
- return (0, import_node_path43.join)(this.personaRoot, PROJECTS_DIR);
43273
+ return (0, import_node_path43.join)(this.root, PROJECTS_DIR);
43181
43274
  }
43182
43275
  async list() {
43183
43276
  let entries;
43184
43277
  try {
43185
- entries = await import_node_fs30.promises.readdir(this.projectsRoot());
43278
+ entries = await import_node_fs31.promises.readdir(this.projectsRoot());
43186
43279
  } catch (err) {
43187
43280
  if (err.code === "ENOENT") return [];
43188
43281
  throw err;
@@ -43190,7 +43283,7 @@ var ProjectStore = class {
43190
43283
  const out = [];
43191
43284
  for (const name of entries) {
43192
43285
  try {
43193
- const raw = await import_node_fs30.promises.readFile(this.metaPath(name), "utf8");
43286
+ const raw = await import_node_fs31.promises.readFile(this.metaPath(name), "utf8");
43194
43287
  const json = JSON.parse(raw);
43195
43288
  let migrated = false;
43196
43289
  if (typeof json.devCommand !== "string" || json.devCommand.length === 0) {
@@ -43201,7 +43294,7 @@ var ProjectStore = class {
43201
43294
  if (parsed.success) {
43202
43295
  out.push(parsed.data);
43203
43296
  if (migrated) {
43204
- void import_node_fs30.promises.writeFile(this.metaPath(name), JSON.stringify(parsed.data, null, 2) + "\n", "utf8").catch(() => {
43297
+ void import_node_fs31.promises.writeFile(this.metaPath(name), JSON.stringify(parsed.data, null, 2) + "\n", "utf8").catch(() => {
43205
43298
  });
43206
43299
  }
43207
43300
  }
@@ -43210,12 +43303,18 @@ var ProjectStore = class {
43210
43303
  }
43211
43304
  return out.sort((a, b2) => a.name.localeCompare(b2.name));
43212
43305
  }
43213
- async create(name) {
43306
+ /**
43307
+ * @param reservedPorts 其它 user 已占端口(v2 per-user:端口必须全局唯一,所有 user 的
43308
+ * dev server 同机跑;per-user store 各自只 list 自己,靠 handler 收集全局端口传进来,
43309
+ * 否则不同 user 的首个项目都从 PROJECT_PORT_MIN 起 → 全撞 6173)。
43310
+ */
43311
+ async create(name, reservedPorts) {
43214
43312
  const existing = await this.list();
43215
43313
  if (existing.some((p2) => p2.name === name)) {
43216
43314
  throw new Error(`project "${name}" already exists / \u5DF2\u5B58\u5728`);
43217
43315
  }
43218
43316
  const usedPorts = new Set(existing.map((p2) => p2.port));
43317
+ if (reservedPorts) for (const p2 of reservedPorts) usedPorts.add(p2);
43219
43318
  let port = -1;
43220
43319
  for (let p2 = PROJECT_PORT_MIN; p2 <= PROJECT_PORT_MAX; p2++) {
43221
43320
  if (!usedPorts.has(p2)) {
@@ -43239,8 +43338,8 @@ var ProjectStore = class {
43239
43338
  throw new Error(`invalid name "${name}": ${validated.error.message}`);
43240
43339
  }
43241
43340
  const dir = this.projectDir(name);
43242
- await import_node_fs30.promises.mkdir(dir, { recursive: true });
43243
- await import_node_fs30.promises.writeFile(this.metaPath(name), JSON.stringify(meta, null, 2) + "\n", "utf8");
43341
+ await import_node_fs31.promises.mkdir(dir, { recursive: true });
43342
+ await import_node_fs31.promises.writeFile(this.metaPath(name), JSON.stringify(meta, null, 2) + "\n", "utf8");
43244
43343
  return meta;
43245
43344
  }
43246
43345
  /**
@@ -43251,11 +43350,12 @@ var ProjectStore = class {
43251
43350
  * spec 2026-06-02 §5.6.3:scaffold 对 assistant 不可见,避免 assistant 被诱导去"读 example
43252
43351
  * 代码再参考"。assistant 进 project 时目录已是完整模板,直接 pnpm install 即可。
43253
43352
  */
43254
- async scaffold(name, template = DEFAULT_TEMPLATE) {
43255
- const scriptPath = (0, import_node_path43.join)(this.personaRoot, SCAFFOLD_SCRIPT_REL);
43353
+ async scaffold(name, template = DEFAULT_TEMPLATE, personaRoot = this.root) {
43354
+ const scriptPath = (0, import_node_path43.join)(personaRoot, SCAFFOLD_SCRIPT_REL);
43355
+ const destDir = this.projectDir(name);
43256
43356
  return await new Promise((resolve6, reject) => {
43257
- const child = (0, import_node_child_process9.spawn)("bash", [scriptPath, name, template], {
43258
- cwd: this.personaRoot,
43357
+ const child = (0, import_node_child_process9.spawn)("bash", [scriptPath, name, template, destDir], {
43358
+ cwd: personaRoot,
43259
43359
  env: { ...process.env, PATH: process.env.PATH ?? "" },
43260
43360
  stdio: ["ignore", "pipe", "pipe"]
43261
43361
  });
@@ -43284,7 +43384,7 @@ var ProjectStore = class {
43284
43384
  }
43285
43385
  async delete(name) {
43286
43386
  const dir = this.projectDir(name);
43287
- await import_node_fs30.promises.rm(dir, { recursive: true, force: true });
43387
+ await import_node_fs31.promises.rm(dir, { recursive: true, force: true });
43288
43388
  }
43289
43389
  async updatePort(name, newPort) {
43290
43390
  if (newPort < PROJECT_PORT_MIN || newPort > PROJECT_PORT_MAX) {
@@ -43300,7 +43400,7 @@ var ProjectStore = class {
43300
43400
  throw new Error(`port ${newPort} already used / \u5DF2\u88AB project "${conflict.name}" \u5360\u7528`);
43301
43401
  }
43302
43402
  const updated = { ...target, port: newPort };
43303
- await import_node_fs30.promises.writeFile(this.metaPath(name), JSON.stringify(updated, null, 2) + "\n", "utf8");
43403
+ await import_node_fs31.promises.writeFile(this.metaPath(name), JSON.stringify(updated, null, 2) + "\n", "utf8");
43304
43404
  return updated;
43305
43405
  }
43306
43406
  /**
@@ -43317,7 +43417,7 @@ var ProjectStore = class {
43317
43417
  if (!validated.success) {
43318
43418
  throw new Error(`invalid prodUrl "${url}": ${validated.error.message}`);
43319
43419
  }
43320
- await import_node_fs30.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
43420
+ await import_node_fs31.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
43321
43421
  return validated.data;
43322
43422
  }
43323
43423
  /**
@@ -43338,7 +43438,7 @@ var ProjectStore = class {
43338
43438
  if (!validated.success) {
43339
43439
  throw new Error(`invalid publishJob: ${validated.error.message}`);
43340
43440
  }
43341
- await import_node_fs30.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
43441
+ await import_node_fs31.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
43342
43442
  return validated.data;
43343
43443
  }
43344
43444
  /** 清掉 .clawd-project.json.publishJob 字段。其他字段保持原样。 */
@@ -43353,7 +43453,7 @@ var ProjectStore = class {
43353
43453
  if (!validated.success) {
43354
43454
  throw new Error(`failed to clear publishJob: ${validated.error.message}`);
43355
43455
  }
43356
- await import_node_fs30.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
43456
+ await import_node_fs31.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
43357
43457
  return validated.data;
43358
43458
  }
43359
43459
  };
@@ -43755,22 +43855,24 @@ var METHOD_GRANT_MAP = {
43755
43855
  // 即使 persona-app-builder 是 PreinstalledPersona,project 管理也属本机 owner 行为,
43756
43856
  // 不暴露给 guest(远端接入者)。
43757
43857
  "appBuilder:listProjects": ADMIN_ANY,
43858
+ // getProject 走 capability-scoped(区别于其它 appBuilder 的 owner-only ADMIN_ANY):mirror/people
43859
+ // 的本机 owner 在远端是 guest,需能调 getProject 拿它 mirror 的 session 的 project。dispatcher
43860
+ // 放行后由 handler 内 canAccessSession(read) 按 session 可见性授权(防 guest 越权拿别人 project)。
43861
+ "appBuilder:getProject": CAPABILITY_SCOPED,
43758
43862
  "appBuilder:createProject": ADMIN_ANY,
43759
43863
  "appBuilder:deleteProject": ADMIN_ANY,
43760
43864
  "appBuilder:updateProjectPort": ADMIN_ANY,
43761
- // 单栏默认 refactor (spec 2026-06-02):发布上线后 assistant 调写公网 URL。
43762
- "appBuilder:setProdUrl": ADMIN_ANY,
43763
- // 单栏 refactor §5.6.2 stage fix:assistant scaffold + install 完成后调 startDevServer 起进程;
43764
- // 中间用 reportStage 上报阶段(scaffolding / installing / failed)。
43765
- "appBuilder:startDevServer": ADMIN_ANY,
43865
+ // people/mirror 协同写操作(spec 2026-06-04-people-mode-app-builder-write-and-push):以下 5 个写
43866
+ // method 走 CAPABILITY_SCOPED(不再 owner-only ADMIN_ANY),handler 内 ensureProjectWriteAccess
43867
+ // session 可见性(canAccessSession 'send')授权——mirror 的本机 owner 在远端是 guest,能 mirror
43868
+ // session 就能操作它绑定的 project(发布凭证是 persona 共享资产,老板决策"统一 session 授权")。
43869
+ "appBuilder:setProdUrl": CAPABILITY_SCOPED,
43870
+ "appBuilder:startDevServer": CAPABILITY_SCOPED,
43871
+ // reportStage 仍 owner-only:assistant 走 HTTP RPC 上报(不过 WS admin grant),UI 不调它。
43766
43872
  "appBuilder:reportStage": ADMIN_ANY,
43767
- // 显式控制 fix (spec 2026-06-02-app-builder-explicit-dev-server-control):用户点"收起预览"触发停 dev server,
43768
- // 对称 startDevServer。同时砍了 setActiveSession(dev server 不再 follow-the-eyes)。
43769
- "appBuilder:stopDevServer": ADMIN_ANY,
43770
- // 发布上线脚手架化 (spec 2026-06-03-app-builder-publish-scaffold-design):
43771
- // owner-only:PreviewPane「发布上线」按钮直接调 publish;UI [取消] / interrupted 恢复按钮调 dismissPublishJob。
43772
- "appBuilder:publish": ADMIN_ANY,
43773
- "appBuilder:dismissPublishJob": ADMIN_ANY
43873
+ "appBuilder:stopDevServer": CAPABILITY_SCOPED,
43874
+ "appBuilder:publish": CAPABILITY_SCOPED,
43875
+ "appBuilder:dismissPublishJob": CAPABILITY_SCOPED
43774
43876
  };
43775
43877
  function computeGrantForFrame(method, frame) {
43776
43878
  const rule = METHOD_GRANT_MAP[method];
@@ -44279,6 +44381,7 @@ async function startDaemon(config) {
44279
44381
  const history = new ClaudeHistoryReader();
44280
44382
  let transport = null;
44281
44383
  const personaStore = new PersonaStore(import_node_path47.default.join(config.dataDir, "personas"));
44384
+ const usersRoot = import_node_path47.default.join(config.dataDir, "users");
44282
44385
  const defaultsRoot = findDefaultsRoot();
44283
44386
  if (defaultsRoot) {
44284
44387
  seedDefaultPersonas({ store: personaStore, defaultsRoot, logger });
@@ -44297,6 +44400,7 @@ async function startDaemon(config) {
44297
44400
  historyReader: history,
44298
44401
  dataDir: config.dataDir,
44299
44402
  personaRoot: import_node_path47.default.join(config.dataDir, "personas"),
44403
+ usersRoot,
44300
44404
  personaStore,
44301
44405
  ownerDisplayName,
44302
44406
  ownerPrincipalId,
@@ -44328,7 +44432,7 @@ async function startDaemon(config) {
44328
44432
  const absPath = import_node_path47.default.isAbsolute(input.relPath) ? input.relPath : import_node_path47.default.join(input.cwd, input.relPath);
44329
44433
  let size = 0;
44330
44434
  try {
44331
- size = import_node_fs31.default.statSync(absPath).size;
44435
+ size = import_node_fs32.default.statSync(absPath).size;
44332
44436
  } catch (err) {
44333
44437
  logger.warn("attachment.onFileEdit stat failed", {
44334
44438
  sessionId: input.sessionId,
@@ -44457,9 +44561,8 @@ async function startDaemon(config) {
44457
44561
  ttcAuthBase: TTC_HOSTS.auth,
44458
44562
  getCallbackUrl: () => `http://127.0.0.1:${config.port}/auth/callback`
44459
44563
  });
44460
- const appBuilderStore = new ProjectStore(
44461
- import_node_path47.default.join(config.dataDir, "personas/persona-app-builder")
44462
- );
44564
+ const getProjectStore = (userId) => new ProjectStore(deriveUserWorkDir(userId, usersRoot));
44565
+ const appBuilderStore = getProjectStore(ownerPrincipalId);
44463
44566
  recoverInterruptedPublishJobs(appBuilderStore, logger).catch((err) => {
44464
44567
  logger.warn("app-builder.publish.recover-startup-failed", {
44465
44568
  error: err instanceof Error ? err.message : String(err)
@@ -44510,11 +44613,14 @@ async function startDaemon(config) {
44510
44613
  // group RPC + sign 都用:根据 sessionId 反查 scope。owner-mode persona session 走
44511
44614
  // 'persona/<pid>/owner',default 走 'default'。
44512
44615
  getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
44513
- // guest path guard:candidate 必须在 personaRoot 子树下
44514
- personaRoot: import_node_path47.default.join(config.dataDir, "personas")
44616
+ // guest path guard:candidate 必须在 personaRoot 子树或调用者自己的 user-dir 下
44617
+ personaRoot: import_node_path47.default.join(config.dataDir, "personas"),
44618
+ usersRoot
44515
44619
  },
44516
44620
  // workspace/git/history/skills/agents handler 共用的 guest path guard 锚点
44517
44621
  personaRoot: import_node_path47.default.join(config.dataDir, "personas"),
44622
+ // v2 多人 persona 隔离:handler 派生 guest user-dir 放行
44623
+ usersRoot,
44518
44624
  // capability:list / delete handler 依赖
44519
44625
  capabilityManager,
44520
44626
  // v2 Phase 6: whoami handler 装在 owner principal.displayName + persona 解析
@@ -44582,10 +44688,22 @@ async function startDaemon(config) {
44582
44688
  },
44583
44689
  // app-builder
44584
44690
  appBuilderStore,
44691
+ getProjectStore,
44585
44692
  devServerLifecycle: devServerSupervisor,
44586
44693
  // 单栏 refactor §5.6.2 stage fix:appBuilder:project-updated 帧通过 wsServer broadcast。
44587
44694
  // 推给所有 owner 连接(picker 可能没订阅特定 session,全局 broadcast 最稳)。
44588
- broadcastProjectUpdated: (event) => wsServer?.broadcastToOwners(event),
44695
+ // people/mirror(spec 2026-06-04-people-mode-app-builder-write-and-push):额外推给订阅该 project
44696
+ // 绑定 session 的 client(mirror guest 经 session:observe 订阅),让远端协同者也看到 stage / 发布进度。
44697
+ // 同一帧 owner 也走 project-updated 复用给 publish-progress/failed(app-builder.ts 内 broadcast 闭包):
44698
+ // name 在 event.project.name(project-updated)或 event.name(publish 帧)。owner 双栏会同时命中
44699
+ // owner-broadcast + session-broadcast,但 UI patch 幂等(同 project 覆盖)无害。
44700
+ broadcastProjectUpdated: (event) => {
44701
+ const ev = event;
44702
+ wsServer?.broadcastToOwners(ev);
44703
+ const projectName = event.project?.name ?? event.name;
44704
+ const boundSid = projectName ? manager.findOwnedSessionByAppBuilderProject(projectName)?.sessionId : void 0;
44705
+ if (boundSid) wsServer?.broadcastToSession(boundSid, ev);
44706
+ },
44589
44707
  // 发布上线脚手架化 (spec 2026-06-03 §5.2):
44590
44708
  // appBuilderPersonaRoot 用于拼 publish.sh 绝对路径(persona-app-builder 安装在
44591
44709
  // dataDir/personas/persona-app-builder 之下,extension-kit/scripts/publish.sh 是相对路径)。
@@ -44852,7 +44970,7 @@ ${bar}
44852
44970
  `);
44853
44971
  try {
44854
44972
  const connectPath = import_node_path47.default.join(config.dataDir, "connect.txt");
44855
- import_node_fs31.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
44973
+ import_node_fs32.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
44856
44974
  } catch {
44857
44975
  }
44858
44976
  } catch (err) {
@@ -44925,7 +45043,7 @@ ${bar}
44925
45043
  function migrateDropPersonsDir(dataDir) {
44926
45044
  const dir = import_node_path47.default.join(dataDir, "persons");
44927
45045
  try {
44928
- import_node_fs31.default.rmSync(dir, { recursive: true, force: true });
45046
+ import_node_fs32.default.rmSync(dir, { recursive: true, force: true });
44929
45047
  } catch {
44930
45048
  }
44931
45049
  }