@clawos-dev/clawd 0.2.221-beta.434.45d5a69 → 0.2.221

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
@@ -45,6 +45,9 @@ var init_methods = __esm({
45
45
  "session:update",
46
46
  "session:delete",
47
47
  "session:send",
48
+ // per-turn sender 2026-07-08 spec §4.6:MCP servers (ticket / inbox ...) 走 HTTP RPC
49
+ // /api/rpc/session.currentTurnSender 拉当前 turn caller;WS handler 同名(loopback path 复用)
50
+ "session:currentTurnSender",
48
51
  "session:stop",
49
52
  // 温柔中断当前轮次,不杀进程(走 CC IPC control_request.subtype=interrupt)
50
53
  "session:interrupt",
@@ -4520,6 +4523,59 @@ var init_attachment_schemas = __esm({
4520
4523
  }
4521
4524
  });
4522
4525
 
4526
+ // ../protocol/src/principal.ts
4527
+ function makeOwnerPrincipal(id, displayName, feishuUnionId) {
4528
+ return PrincipalSchema.parse({
4529
+ id,
4530
+ kind: "owner",
4531
+ displayName,
4532
+ ...feishuUnionId ? { feishuUnionId } : {}
4533
+ });
4534
+ }
4535
+ function principalToSender(p2) {
4536
+ return {
4537
+ did: p2.id,
4538
+ displayName: p2.displayName,
4539
+ principal: p2.kind,
4540
+ ...p2.feishuUnionId ? { feishuUnionId: p2.feishuUnionId } : {}
4541
+ };
4542
+ }
4543
+ function makeGuestSender(did, displayName, feishuUnionId) {
4544
+ return SenderInfoSchema.parse({
4545
+ did,
4546
+ displayName,
4547
+ principal: "guest",
4548
+ ...feishuUnionId ? { feishuUnionId } : {}
4549
+ });
4550
+ }
4551
+ var PrincipalKindSchema, PrincipalSchema, SenderInfoSchema;
4552
+ var init_principal = __esm({
4553
+ "../protocol/src/principal.ts"() {
4554
+ "use strict";
4555
+ init_zod();
4556
+ PrincipalKindSchema = external_exports.enum(["owner", "guest"]);
4557
+ PrincipalSchema = external_exports.object({
4558
+ id: external_exports.string().min(1),
4559
+ kind: PrincipalKindSchema,
4560
+ displayName: external_exports.string(),
4561
+ /**
4562
+ * 飞书 union_id。owner: 本机 daemon 已飞书登录时透传自 owner-identity.json;未登录 undefined。
4563
+ * guest: v1 保持 undefined(connect token 当前不携带 unionId;未来若签名 payload 加字段可透传)。
4564
+ * 消费方(如 ticket MCP / 未来别的飞书身份挂钩场景)按 undefined 决定是否降级;本层不做 fallback。
4565
+ */
4566
+ feishuUnionId: external_exports.string().optional()
4567
+ }).strict();
4568
+ SenderInfoSchema = external_exports.object({
4569
+ /** = principal.id = deviceId(决策 #16)。稳定路由 id */
4570
+ did: external_exports.string().min(1),
4571
+ displayName: external_exports.string(),
4572
+ /** owner 未登录 / guest 无飞书身份时缺省。消费方各自决定是否降级 */
4573
+ feishuUnionId: external_exports.string().optional(),
4574
+ principal: PrincipalKindSchema
4575
+ }).strict();
4576
+ }
4577
+ });
4578
+
4523
4579
  // ../protocol/src/persona-schemas.ts
4524
4580
  var PersonaFileSchema, PersonaSkillSummarySchema, PersonaPluginSummarySchema, PersonaSandboxSettingsSchema, PersonaInfoResponseSchema, PersonaCreateArgsSchema, PersonaIdArgsSchema, PersonaUpdateArgsSchema, CodexSandboxSettingsSchema;
4525
4581
  var init_persona_schemas = __esm({
@@ -4628,6 +4684,7 @@ var init_schemas = __esm({
4628
4684
  init_zod();
4629
4685
  init_events();
4630
4686
  init_attachment_schemas();
4687
+ init_principal();
4631
4688
  init_persona_schemas();
4632
4689
  SessionStatusSchema = external_exports.enum(SESSION_STATUS_VALUES);
4633
4690
  UsageSchema = external_exports.object({
@@ -4894,13 +4951,18 @@ var init_schemas = __esm({
4894
4951
  // 真实用户输入:daemon 通过 --replay-user-messages 拿到 stdin 回显,区分于 assistant text。
4895
4952
  // meta 非空表示是 CC 注入的系统消息(task-notification / slash-command / system-reminder 等),
4896
4953
  // UI 据此走低视觉权重渲染或子 agent 通知卡。
4954
+ //
4955
+ // sender(2026-07-08 spec §4.1):daemon 'send' 命令 synth 的 user_text 携带发起人 SenderInfo;
4956
+ // observer 回灌路径(cc jsonl)永远不带 —— cc 自己写盘不知道 daemon 层身份。
4957
+ // UI 端 sender 缺省 → 不渲染 displayName label;有 → 气泡下方渲染。
4897
4958
  external_exports.object({
4898
4959
  ...ParsedEventBase,
4899
4960
  kind: external_exports.literal("user_text"),
4900
4961
  text: external_exports.string(),
4901
4962
  meta: HistoryUserMetaSchema.optional(),
4902
4963
  taskId: external_exports.string().optional(),
4903
- parentToolUseId: external_exports.string().optional()
4964
+ parentToolUseId: external_exports.string().optional(),
4965
+ sender: SenderInfoSchema.optional()
4904
4966
  }),
4905
4967
  external_exports.object({
4906
4968
  ...ParsedEventBase,
@@ -5509,24 +5571,6 @@ var init_persona_mode = __esm({
5509
5571
  }
5510
5572
  });
5511
5573
 
5512
- // ../protocol/src/principal.ts
5513
- function makeOwnerPrincipal(id, displayName) {
5514
- return PrincipalSchema.parse({ id, kind: "owner", displayName });
5515
- }
5516
- var PrincipalKindSchema, PrincipalSchema;
5517
- var init_principal = __esm({
5518
- "../protocol/src/principal.ts"() {
5519
- "use strict";
5520
- init_zod();
5521
- PrincipalKindSchema = external_exports.enum(["owner", "guest"]);
5522
- PrincipalSchema = external_exports.object({
5523
- id: external_exports.string().min(1),
5524
- kind: PrincipalKindSchema,
5525
- displayName: external_exports.string()
5526
- }).strict();
5527
- }
5528
- });
5529
-
5530
5574
  // ../protocol/src/capability.ts
5531
5575
  function stripSecretHash(cap) {
5532
5576
  const { secretHash: _hash, ...wire } = cap;
@@ -41651,6 +41695,47 @@ function escapeAttr(v2) {
41651
41695
  return v2.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
41652
41696
  }
41653
41697
 
41698
+ // src/shift/end-frame-handler.ts
41699
+ function makeEndFrameHandler(sessionId, onEnd) {
41700
+ return (frame) => {
41701
+ if (!frame || typeof frame !== "object") return;
41702
+ const f = frame;
41703
+ if (f.type !== "session:status") return;
41704
+ if (f.sessionId !== sessionId) return;
41705
+ if (f.status === "running-idle") {
41706
+ onEnd({ kind: "idle" });
41707
+ return;
41708
+ }
41709
+ if (f.status === "stopped") {
41710
+ onEnd({ kind: "stopped" });
41711
+ return;
41712
+ }
41713
+ if (f.status === "error") {
41714
+ onEnd({ kind: "error", message: f.message ?? "cc proc error" });
41715
+ return;
41716
+ }
41717
+ };
41718
+ }
41719
+
41720
+ // src/shift/end-dispatcher.ts
41721
+ function dispatchShiftEnd(info, settle, killImpl) {
41722
+ if (info.kind === "idle") {
41723
+ settle({ ok: true });
41724
+ killImpl();
41725
+ return;
41726
+ }
41727
+ if (info.kind === "stopped") {
41728
+ settle({ error: "cc exited before turn completed" });
41729
+ return;
41730
+ }
41731
+ if (info.kind === "error") {
41732
+ settle({ error: info.message });
41733
+ return;
41734
+ }
41735
+ const _exhaustive = info;
41736
+ throw new Error(`dispatchShiftEnd: unhandled kind ${JSON.stringify(_exhaustive)}`);
41737
+ }
41738
+
41654
41739
  // src/session/runner.ts
41655
41740
  var import_node_os4 = __toESM(require("os"), 1);
41656
41741
  var import_node_path7 = __toESM(require("path"), 1);
@@ -41817,7 +41902,9 @@ function cloneState(s) {
41817
41902
  // ReadyGate v2:readyForSend 是基础类型直接复制;pendingSend 必须浅拷贝防止
41818
41903
  // reduce 内 push/shift 在调用方原 state 上副作用(reducer 纯函数契约)
41819
41904
  readyForSend: s.readyForSend,
41820
- pendingSend: s.pendingSend.slice()
41905
+ pendingSend: s.pendingSend.slice(),
41906
+ // per-turn sender (spec §4.3):SenderInfo 只在 case 'send' 覆盖,其它 kind 直接复用引用
41907
+ currentTurnSender: s.currentTurnSender
41821
41908
  };
41822
41909
  }
41823
41910
  var IDLE_KILL_DELAY_MS = 6e5;
@@ -41882,8 +41969,7 @@ function buildSpawnContext(state, deps) {
41882
41969
  const personaId = file.ownerPersonaId;
41883
41970
  if (personaId) env.CLAWD_PERSONA_ID = personaId;
41884
41971
  const dispatchMcpConfigPath2 = deps.getDispatchMcpConfigPath?.() ?? null;
41885
- const ticketMcpConfigPathRaw = deps.getTicketMcpConfigPath?.() ?? null;
41886
- const ticketMcpConfigPath2 = ticketMcpConfigPathRaw && file.ownerPersonaId === "persona-ticket-manager" ? ticketMcpConfigPathRaw : null;
41972
+ const ticketMcpConfigPath2 = deps.getTicketMcpConfigPath?.() ?? null;
41887
41973
  const shiftMcpConfigPath2 = deps.getShiftMcpConfigPath?.() ?? null;
41888
41974
  const inboxMcpConfigPath2 = deps.getInboxMcpConfigPath?.() ?? null;
41889
41975
  const ctx = {
@@ -42157,10 +42243,12 @@ function applyCommand(state, command, deps) {
42157
42243
  }
42158
42244
  });
42159
42245
  }
42246
+ next.currentTurnSender = command.senderPrincipal;
42160
42247
  const userEvent = {
42161
42248
  kind: "user_text",
42162
42249
  text: command.text,
42163
- uuid: deps.genUuid?.()
42250
+ uuid: deps.genUuid?.(),
42251
+ sender: command.senderPrincipal
42164
42252
  };
42165
42253
  const pushed = pushEventToBuffer(next, userEvent, deps);
42166
42254
  effects.push(...pushed.effects);
@@ -42708,7 +42796,7 @@ var SessionRunner = class {
42708
42796
  getDispatchMcpConfigPath: this.hooks.getDispatchMcpConfigPath,
42709
42797
  getShiftMcpConfigPath: this.hooks.getShiftMcpConfigPath,
42710
42798
  getInboxMcpConfigPath: this.hooks.getInboxMcpConfigPath,
42711
- // Ticket MCP:透传 ticket.mcp.json 路径闭包(reducer 内做 persona-ticket-manager gating)
42799
+ // Ticket MCP:透传 ticket.mcp.json 路径闭包(所有 persona session 都挂,无 gating)
42712
42800
  getTicketMcpConfigPath: this.hooks.getTicketMcpConfigPath,
42713
42801
  lookupDispatchByBSessionId: this.hooks.lookupDispatchByBSessionId,
42714
42802
  // ReadyGate v2:透传 mode 让 reducer send / ready-detected 分支决定走暂存队列还是直写
@@ -43227,7 +43315,9 @@ function makeInitialState(file, subSessionMeta) {
43227
43315
  // ReadyGate v2:默认值兼容 SDK happy path(直写 stdin,不走队列)。
43228
43316
  // TUI 模式由 reducer send 分支按 deps.mode='tui' 主动翻 readyForSend=false
43229
43317
  readyForSend: true,
43230
- pendingSend: []
43318
+ pendingSend: [],
43319
+ // per-turn sender 2026-07-08 spec §4.3:初始 null;case 'send' 首次覆盖
43320
+ currentTurnSender: null
43231
43321
  };
43232
43322
  }
43233
43323
  var SessionManager = class {
@@ -43900,7 +43990,7 @@ var SessionManager = class {
43900
43990
  ]
43901
43991
  };
43902
43992
  }
43903
- send(args) {
43993
+ send(args, senderPrincipal) {
43904
43994
  const existing = this.getFile(args.sessionId);
43905
43995
  let runner = this.runners.get(args.sessionId);
43906
43996
  if (!runner) {
@@ -43908,10 +43998,22 @@ var SessionManager = class {
43908
43998
  this.runners.set(args.sessionId, runner);
43909
43999
  }
43910
44000
  const { broadcast } = this.withCollector(() => {
43911
- runner.input({ kind: "command", command: { kind: "send", text: args.text } });
44001
+ runner.input({
44002
+ kind: "command",
44003
+ command: { kind: "send", text: args.text, senderPrincipal }
44004
+ });
43912
44005
  });
43913
44006
  return { response: { ok: true }, broadcast };
43914
44007
  }
44008
+ /**
44009
+ * 只读拿 SessionState;session 不存在返 null(不 throw)。
44010
+ * handlers/session.ts:currentTurnSender 用(loopback HTTP RPC 绝对不能因不存在的 sid 崩)。
44011
+ * 2026-07-08 spec §4.6。
44012
+ */
44013
+ tryGetState(sessionId) {
44014
+ const runner = this.runners.get(sessionId);
44015
+ return runner?.getState() ?? null;
44016
+ }
43915
44017
  // 等子进程真退出再 ack。reducer 推 SIGKILL effect(见 reducer.ts),内核立即回收子进程,
43916
44018
  // proc.on('exit') 几乎瞬时触发;waitUntilStopped 的 3s 超时只在子进程已是 zombie 或
43917
44019
  // 极端 OS 异常时才会用上。这样修掉了"前端 sessionStop 还没等 CC 退就去删 worktree
@@ -44311,7 +44413,10 @@ var SessionManager = class {
44311
44413
  }
44312
44414
  const runner = this.ensureRunnerForScope(file, args.scope);
44313
44415
  const { broadcast } = this.withCollector(() => {
44314
- runner.input({ kind: "command", command: { kind: "send", text: args.text } });
44416
+ runner.input({
44417
+ kind: "command",
44418
+ command: { kind: "send", text: args.text, senderPrincipal: args.senderPrincipal }
44419
+ });
44315
44420
  });
44316
44421
  return { response: { ok: true }, broadcast };
44317
44422
  }
@@ -44455,7 +44560,27 @@ var SessionManager = class {
44455
44560
  sourceJsonlPath: args.sourceJsonlPath,
44456
44561
  dispatchId: args.dispatchId
44457
44562
  });
44458
- runner.input({ kind: "command", command: { kind: "send", text: taskPack } });
44563
+ let dispatchSender;
44564
+ if (args.guestPrincipalId) {
44565
+ dispatchSender = makeGuestSender(args.guestPrincipalId, args.guestDisplayName ?? args.guestPrincipalId);
44566
+ } else {
44567
+ if (!this.deps.ownerPrincipalId) {
44568
+ throw new Error(
44569
+ "createDispatchedSession requires ManagerDeps.ownerPrincipalId to build owner sender"
44570
+ );
44571
+ }
44572
+ dispatchSender = principalToSender(
44573
+ makeOwnerPrincipal(
44574
+ this.deps.ownerPrincipalId,
44575
+ this.deps.ownerDisplayName ?? "Owner",
44576
+ this.deps.ownerFeishuUnionId
44577
+ )
44578
+ );
44579
+ }
44580
+ runner.input({
44581
+ kind: "command",
44582
+ command: { kind: "send", text: taskPack, senderPrincipal: dispatchSender }
44583
+ });
44459
44584
  this.deps.logger?.info("dispatch.createDispatchedSession.task-pack-sent", {
44460
44585
  dispatchId: args.dispatchId,
44461
44586
  bSessionId: sessionId
@@ -44470,8 +44595,9 @@ var SessionManager = class {
44470
44595
  * 跟 createDispatchedSession 的差异:
44471
44596
  * - 不带 dispatchId / source / task pack;prompt 直接当第一条 user message
44472
44597
  * - 不调 personaDispatchManager.registerBSession
44473
- * - 多返一个 ended Promise,监听 session:status (stopped/error) 帧通过现有
44474
- * subscribe() 入口(白名单已含 session:status)
44598
+ * - 多返一个 ended Promise,监听 session:status 帧翻译成 shift 业务终态 union
44599
+ * (ShiftSessionEnded:{ok:true} | {error:string}),不经 exit code 中转
44600
+ * - turn-idle 到达即视为业务完成 → settle {ok:true} + fire-and-forget kill cc
44475
44601
  * - kill 通过 runner.input({command:{kind:'stop'}}) 触发 SIGKILL → proc-exit
44476
44602
  */
44477
44603
  createShiftSession(args) {
@@ -44515,18 +44641,32 @@ var SessionManager = class {
44515
44641
  unsubscribe();
44516
44642
  resolveEnded(info);
44517
44643
  };
44518
- unsubscribe = this.subscribe(sessionId, scope, (frame) => {
44519
- if (frame.type !== "session:status") return;
44520
- const f = frame;
44521
- if (f.sessionId !== sessionId) return;
44522
- if (f.status === "stopped") {
44523
- settle({ exitCode: f.exitCode ?? null });
44524
- } else if (f.status === "error") {
44525
- settle({ exitCode: null, error: f.message ?? "cc proc error" });
44526
- }
44527
- });
44644
+ const killImpl = () => {
44645
+ const r = this.runners.get(sessionId);
44646
+ if (!r) return;
44647
+ r.input({ kind: "command", command: { kind: "stop" } });
44648
+ };
44649
+ unsubscribe = this.subscribe(
44650
+ sessionId,
44651
+ scope,
44652
+ makeEndFrameHandler(sessionId, (info) => {
44653
+ dispatchShiftEnd(info, settle, killImpl);
44654
+ })
44655
+ );
44528
44656
  const runner = this.ensureRunnerForScope(file, scope);
44529
44657
  this.deps.logger?.info("shift.createShiftSession.runner-ready", { sessionId });
44658
+ if (!this.deps.ownerPrincipalId) {
44659
+ throw new Error(
44660
+ "createShiftSession requires ManagerDeps.ownerPrincipalId to build owner sender"
44661
+ );
44662
+ }
44663
+ const shiftSender = principalToSender(
44664
+ makeOwnerPrincipal(
44665
+ this.deps.ownerPrincipalId,
44666
+ this.deps.ownerDisplayName ?? "Owner",
44667
+ this.deps.ownerFeishuUnionId
44668
+ )
44669
+ );
44530
44670
  runner.input({
44531
44671
  kind: "command",
44532
44672
  command: {
@@ -44534,15 +44674,11 @@ var SessionManager = class {
44534
44674
  text: wrapShiftFirePrompt({
44535
44675
  prompt: args.prompt,
44536
44676
  targetPersona: args.targetPersona
44537
- })
44677
+ }),
44678
+ senderPrincipal: shiftSender
44538
44679
  }
44539
44680
  });
44540
- const kill = () => {
44541
- const r = this.runners.get(sessionId);
44542
- if (!r) return;
44543
- r.input({ kind: "command", command: { kind: "stop" } });
44544
- };
44545
- return { sessionId, ended, kill };
44681
+ return { sessionId, ended, kill: killImpl };
44546
44682
  }
44547
44683
  /**
44548
44684
  * 老板插话:把 'inject-owner-text' input 喂给对应 runner,
@@ -48052,7 +48188,7 @@ function ticketMcpConfigPath(dataDir) {
48052
48188
  function writeTicketMcpConfig(args) {
48053
48189
  const cfgPath = ticketMcpConfigPath(args.dataDir);
48054
48190
  const content = {
48055
- _comment: "daemon \u542F\u52A8\u65F6\u81EA\u52A8\u751F\u6210\uFF1Bcc spawn \u901A\u8FC7 --mcp-config \u6CE8\u5165\u3002ticket-mcp-server.cjs \u662F daemon \u81EA\u5E26\u7684 stdio MCP server\uFF0CHTTP client \u8C03 clawos api /api/tickets/*\u3002env \u542B api url\u3001\u5199\u6B7B API key\u3001caller did/name/scene\uFF1BCLAWD_TICKET_LOG \u6307 server \u5199\u65E5\u5FD7\u7684 append \u6587\u4EF6\u8DEF\u5F84\u3002cc \u573A\u666F scene \u56FA\u5B9A persona\u3002",
48191
+ _comment: "daemon \u542F\u52A8\u65F6\u81EA\u52A8\u751F\u6210\uFF1Bcc spawn \u901A\u8FC7 --mcp-config \u6CE8\u5165\u3002ticket-mcp-server.cjs \u662F daemon \u81EA\u5E26\u7684 stdio MCP server\uFF0CHTTP client \u8C03 clawos api /api/tickets/*\u3002env \u542B api url\u3001\u5199\u6B7B API key\u3001scene\uFF1Bdid/name \u6BCF\u6B21 tool call \u8D70 CLAWD_DAEMON_URL + CLAWD_SESSION_ID (cc \u5DF2\u6CE8) fetch /api/rpc/session.currentTurnSender \u62FF\u5F53\u524D turn caller (2026-07-08 spec \xA74.7)\u3002CLAWD_TICKET_LOG \u6307 server \u5199\u65E5\u5FD7\u7684 append \u6587\u4EF6\u8DEF\u5F84\u3002cc \u573A\u666F scene \u56FA\u5B9A persona\u3002",
48056
48192
  mcpServers: {
48057
48193
  "clawd-ticket": {
48058
48194
  type: "stdio",
@@ -48061,8 +48197,6 @@ function writeTicketMcpConfig(args) {
48061
48197
  env: {
48062
48198
  CLAWD_TICKET_API_URL: args.apiBaseUrl,
48063
48199
  CLAWD_TICKET_API_KEY: args.apiKey,
48064
- CLAWD_TICKET_DID: args.did,
48065
- CLAWD_TICKET_NAME: args.name,
48066
48200
  CLAWD_TICKET_SCENE: "persona",
48067
48201
  ...args.logPath ? { CLAWD_TICKET_LOG: args.logPath } : {}
48068
48202
  }
@@ -48447,15 +48581,11 @@ function createShiftSpawnFn(deps) {
48447
48581
  void created.ended.then((info) => {
48448
48582
  clearTimeout(timer);
48449
48583
  if (settled) return;
48450
- if (info.error) {
48451
- settle({ kind: "error", error: info.error });
48452
- return;
48453
- }
48454
- if (info.exitCode === 0) {
48584
+ if ("ok" in info) {
48455
48585
  settle({ kind: "ok" });
48456
48586
  return;
48457
48587
  }
48458
- settle({ kind: "error", error: `cc exited with code ${info.exitCode}` });
48588
+ settle({ kind: "error", error: info.error });
48459
48589
  });
48460
48590
  return { sessionId: created.sessionId, ended };
48461
48591
  };
@@ -50177,7 +50307,12 @@ var EXPOSED_RPC_METHODS = [
50177
50307
  "attachment.groupAdd",
50178
50308
  "attachment.signUrl",
50179
50309
  // persona 受控发 P2P IM DM(sender 由 session scope 推,driver 分级授权见 handlers/inbox.ts)
50180
- "inbox.sendDm"
50310
+ "inbox.sendDm",
50311
+ // per-turn sender 2026-07-08 spec §4.1:MCP servers (ticket / inbox / ...) 每 tool call
50312
+ // 查当前 turn caller —— cc 长驻 + owner/guest 交替驱动同一 session 时不能靠启动时 env
50313
+ // 写死身份,必须运行时 fetch。header X-Clawd-Session-Id 作 session 定位,handler 内查
50314
+ // manager.tryGetState(sid).currentTurnSender 返 SenderInfo | null。
50315
+ "session.currentTurnSender"
50181
50316
  ];
50182
50317
  async function handleRpcRequest(input) {
50183
50318
  const { method, body, sessionId, dispatcher } = input;
@@ -50362,9 +50497,9 @@ function constantTimeEqual2(a, b2) {
50362
50497
  }
50363
50498
 
50364
50499
  // src/transport/connection-context.ts
50365
- function ownerContext(ownerPrincipalId, displayName) {
50500
+ function ownerContext(ownerPrincipalId, displayName, feishuUnionId) {
50366
50501
  return {
50367
- principal: makeOwnerPrincipal(ownerPrincipalId, displayName),
50502
+ principal: makeOwnerPrincipal(ownerPrincipalId, displayName, feishuUnionId),
50368
50503
  grants: [{ resource: { type: "*" }, actions: ["admin"] }]
50369
50504
  };
50370
50505
  }
@@ -50393,7 +50528,10 @@ function visitorGuestContext(p2) {
50393
50528
  async function authenticate(token, deps) {
50394
50529
  if (!token) return { ok: false, code: "NO_TOKEN" };
50395
50530
  if (deps.isOwnerToken(token)) {
50396
- return { ok: true, context: ownerContext(deps.ownerPrincipalId, deps.ownerDisplayName) };
50531
+ return {
50532
+ ok: true,
50533
+ context: ownerContext(deps.ownerPrincipalId, deps.ownerDisplayName, deps.ownerFeishuUnionId)
50534
+ };
50397
50535
  }
50398
50536
  if (deps.verifyVisitorToken) {
50399
50537
  const vr = deps.verifyVisitorToken(token);
@@ -54192,6 +54330,22 @@ var LoginFlow = class {
54192
54330
  } catch (err) {
54193
54331
  return { ok: false, reason: `user_info failed: ${err.message}` };
54194
54332
  }
54333
+ if (!identity.unionId) {
54334
+ return { ok: false, reason: "union_id missing from TTC user_info" };
54335
+ }
54336
+ try {
54337
+ await this.deps.upsertBinding({
54338
+ deviceId: this.deps.getDeviceId(),
54339
+ unionId: identity.unionId,
54340
+ displayName: identity.displayName,
54341
+ avatarUrl: identity.avatarUrl ?? null
54342
+ });
54343
+ } catch (err) {
54344
+ return {
54345
+ ok: false,
54346
+ reason: `device_binding upsert failed: ${err.message}`
54347
+ };
54348
+ }
54195
54349
  const expiresAtNum = params.expiresAt ? Number.parseInt(params.expiresAt, 10) : NaN;
54196
54350
  this.deps.store.write({
54197
54351
  identity,
@@ -54360,6 +54514,58 @@ async function centralListDevices(opts) {
54360
54514
  return out;
54361
54515
  }
54362
54516
 
54517
+ // src/feishu-auth/device-binding-client.ts
54518
+ var DeviceBindingClientError = class extends Error {
54519
+ constructor(code, message, cause) {
54520
+ super(message);
54521
+ this.code = code;
54522
+ this.cause = cause;
54523
+ this.name = "DeviceBindingClientError";
54524
+ }
54525
+ code;
54526
+ cause;
54527
+ };
54528
+ var DEFAULT_TIMEOUT_MS4 = 8e3;
54529
+ async function upsertDeviceBinding(opts) {
54530
+ const doFetch = opts.fetchImpl ?? fetch;
54531
+ const ac = new AbortController();
54532
+ const timer = setTimeout(() => ac.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS4);
54533
+ const body = { unionId: opts.unionId };
54534
+ if (opts.avatarUrl != null) body.avatarUrl = opts.avatarUrl;
54535
+ let res;
54536
+ try {
54537
+ res = await doFetch(`${opts.apiUrl}/api/device-bindings/self`, {
54538
+ method: "PUT",
54539
+ signal: ac.signal,
54540
+ headers: {
54541
+ authorization: `Bearer ${opts.apiKey}`,
54542
+ "x-did": encodeURIComponent(opts.deviceId),
54543
+ "x-name": encodeURIComponent(opts.displayName),
54544
+ "x-scene": "daemon",
54545
+ "content-type": "application/json"
54546
+ },
54547
+ body: JSON.stringify(body)
54548
+ });
54549
+ } catch (err) {
54550
+ clearTimeout(timer);
54551
+ throw new DeviceBindingClientError(
54552
+ "NETWORK",
54553
+ `network error: ${err.message}`,
54554
+ err
54555
+ );
54556
+ } finally {
54557
+ clearTimeout(timer);
54558
+ }
54559
+ if (!res.ok) {
54560
+ let detail = "";
54561
+ try {
54562
+ detail = JSON.stringify(await res.json());
54563
+ } catch {
54564
+ }
54565
+ throw new DeviceBindingClientError("API_ERROR", `HTTP ${res.status}: ${detail}`);
54566
+ }
54567
+ }
54568
+
54363
54569
  // src/feishu-auth/verify-token.ts
54364
54570
  var crypto14 = __toESM(require("crypto"), 1);
54365
54571
  var CONNECT_TOKEN_PREFIX = "clawdtk1";
@@ -54618,9 +54824,25 @@ function buildSessionHandlers(deps) {
54618
54824
  const send = async (frame, _client, ctx) => {
54619
54825
  const args = SessionSendArgs.parse(frame);
54620
54826
  ensureSessionAccess(ctx, args.sessionId, "send");
54621
- const { broadcast } = manager.send(args);
54827
+ const sender = ctx ? principalToSender(ctx.principal) : principalToSender(
54828
+ makeOwnerPrincipal(ownerPrincipalId, deps.ownerDisplayName, deps.ownerFeishuUnionId)
54829
+ );
54830
+ const { broadcast } = manager.send(args, sender);
54622
54831
  return { response: { type: "session:send", ok: true }, broadcast };
54623
54832
  };
54833
+ const currentTurnSender = async (frame, _client, ctx) => {
54834
+ const sid = typeof frame === "object" && frame != null && "sessionId" in frame ? String(frame.sessionId) : "";
54835
+ if (!sid) {
54836
+ return { response: { type: "session:currentTurnSender", sender: null } };
54837
+ }
54838
+ if (ctx && "principal" in ctx) {
54839
+ ensureSessionAccess(ctx, sid, "read");
54840
+ }
54841
+ const state = manager.tryGetState(sid);
54842
+ return {
54843
+ response: { type: "session:currentTurnSender", sender: state?.currentTurnSender ?? null }
54844
+ };
54845
+ };
54624
54846
  const stop = async (frame, _client, ctx) => {
54625
54847
  const args = SessionIdArgs.parse(frame);
54626
54848
  ensureSessionAccess(ctx, args.sessionId, "send");
@@ -54823,6 +55045,7 @@ function buildSessionHandlers(deps) {
54823
55045
  "session:update": update,
54824
55046
  "session:delete": del,
54825
55047
  "session:send": send,
55048
+ "session:currentTurnSender": currentTurnSender,
54826
55049
  "session:stop": stop,
54827
55050
  "session:interrupt": interrupt,
54828
55051
  "session:rewind": rewind,
@@ -57891,7 +58114,14 @@ function buildAppBuilderHandlers(deps) {
57891
58114
  broadcastProjectUpdated(f);
57892
58115
  },
57893
58116
  injectUserText: async (sessionId, text) => {
57894
- const { broadcast } = sessionManager.send({ sessionId, text });
58117
+ const ownerSender = principalToSender(
58118
+ makeOwnerPrincipal(
58119
+ deps.ownerPrincipalId,
58120
+ deps.ownerDisplayName ?? "Owner",
58121
+ deps.ownerFeishuUnionId
58122
+ )
58123
+ );
58124
+ const { broadcast } = sessionManager.send({ sessionId, text }, ownerSender);
57895
58125
  for (const entry of broadcast) {
57896
58126
  deps.broadcastSessionFrame(entry.frame, entry.target);
57897
58127
  }
@@ -58190,6 +58420,9 @@ function buildMethodHandlers(deps) {
58190
58420
  usersRoot: deps.usersRoot,
58191
58421
  devServerLifecycle: deps.devServerLifecycle,
58192
58422
  sessionManager: deps.manager,
58423
+ ownerPrincipalId: deps.ownerPrincipalId,
58424
+ ownerDisplayName: deps.ownerDisplayName,
58425
+ ownerFeishuUnionId: deps.ownerFeishuUnionId,
58193
58426
  getTunnelUrl: deps.getTunnelUrl,
58194
58427
  broadcastProjectUpdated: deps.broadcastProjectUpdated,
58195
58428
  logger: deps.logger,
@@ -58518,6 +58751,10 @@ var METHOD_GRANT_MAP = {
58518
58751
  "session:update": CAPABILITY_SCOPED,
58519
58752
  "session:delete": CAPABILITY_SCOPED,
58520
58753
  "session:send": CAPABILITY_SCOPED,
58754
+ // per-turn sender 2026-07-08 spec §4.6:dispatcher 层走 'public'(HTTP RPC path 需 dispatcher 放行);
58755
+ // WS path 的隐私边界(防 guest 猜 sid 枚举 owner feishuUnionId)由 handler 内 ensureSessionAccess
58756
+ // 显式兜底——见 handlers/session.ts:currentTurnSender。dispatcher 'public' ≠ handler 免检。
58757
+ "session:currentTurnSender": { kind: "public" },
58521
58758
  "session:stop": CAPABILITY_SCOPED,
58522
58759
  "session:interrupt": CAPABILITY_SCOPED,
58523
58760
  "session:rewind": CAPABILITY_SCOPED,
@@ -59090,6 +59327,7 @@ async function startDaemon(config) {
59090
59327
  let ownerId = feishuIdentity?.identity.ownerId ?? "";
59091
59328
  let ownerProvider = feishuIdentity?.identity.provider ?? "";
59092
59329
  let ownerDisplayName = feishuIdentity?.identity.displayName ?? loadOwnerDisplayName(config.dataDir);
59330
+ let ownerFeishuUnionId = feishuIdentity?.identity.unionId;
59093
59331
  const authMode = resolvedAuthToken == null ? "none" : "first-message";
59094
59332
  const inboxStore = new InboxStore(config.dataDir);
59095
59333
  const inboxManager = new InboxManager(inboxStore, (_peerDeviceId, frame) => {
@@ -59102,6 +59340,7 @@ async function startDaemon(config) {
59102
59340
  isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
59103
59341
  ownerPrincipalId,
59104
59342
  ownerDisplayName,
59343
+ ownerFeishuUnionId,
59105
59344
  // persona web 分享:访客 visitor token = daemon 自签 HMAC(无 aud),独立于飞书 connect token
59106
59345
  // 基建——secret 恒存在(auth-store backfill),故无条件注入。验签 ok → guest ctx(persona wildcard)。
59107
59346
  verifyVisitorToken: (t) => {
@@ -59185,7 +59424,7 @@ async function startDaemon(config) {
59185
59424
  return result;
59186
59425
  },
59187
59426
  onAuthed: (h, ctx) => wsServer?.attachClientContext(h.id, ctx),
59188
- buildOwnerContext: () => ownerContext(ownerPrincipalId, ownerDisplayName),
59427
+ buildOwnerContext: () => ownerContext(ownerPrincipalId, ownerDisplayName, ownerFeishuUnionId),
59189
59428
  closeConnection: (h, code, reason) => wsServer?.closeClient(h.id, code, reason),
59190
59429
  sendOk: (h, payload) => wsServer?.sendToClient(h.id, payload)
59191
59430
  }) : null;
@@ -59257,18 +59496,14 @@ async function startDaemon(config) {
59257
59496
  import_node_path64.default.join(here, "..", "dist", "ticket", "mcp-server.cjs")
59258
59497
  ];
59259
59498
  const ticketServerScriptPath = ticketServerCandidates.find((p2) => import_node_fs51.default.existsSync(p2));
59260
- const ticketCallerDid = feishuIdentity?.identity.unionId ?? "";
59261
- const ticketCallerName = feishuIdentity?.identity.displayName ?? "";
59262
59499
  let ticketMcpConfigPath2;
59263
- if (ticketServerScriptPath && ticketCallerDid) {
59500
+ if (ticketServerScriptPath) {
59264
59501
  const ticketLogPath = import_node_path64.default.join(config.dataDir, "ticket-mcp-server.log");
59265
59502
  ticketMcpConfigPath2 = writeTicketMcpConfig({
59266
59503
  dataDir: config.dataDir,
59267
59504
  serverScriptPath: ticketServerScriptPath,
59268
59505
  apiBaseUrl: config.clawosApi ?? DEFAULT_CLAWOS_API,
59269
59506
  apiKey: resolveClawdTicketsApiKey(),
59270
- did: ticketCallerDid,
59271
- name: ticketCallerName,
59272
59507
  logPath: ticketLogPath
59273
59508
  });
59274
59509
  logger.info("ticket.mcp.json written", {
@@ -59278,7 +59513,7 @@ async function startDaemon(config) {
59278
59513
  });
59279
59514
  } else {
59280
59515
  logger.warn("ticket-mcp skipped", {
59281
- reason: !ticketCallerDid ? "caller did missing (login required)" : "ticket-mcp-server.cjs not found (need build)"
59516
+ reason: "ticket-mcp-server.cjs not found (need build)"
59282
59517
  });
59283
59518
  }
59284
59519
  const shiftServerCandidates = [
@@ -59357,6 +59592,7 @@ async function startDaemon(config) {
59357
59592
  personaStore,
59358
59593
  ownerDisplayName,
59359
59594
  ownerPrincipalId,
59595
+ ownerFeishuUnionId,
59360
59596
  contactStore,
59361
59597
  mode: config.mode,
59362
59598
  // 单栏 refactor (spec 2026-06-02 §5.1):cc 子进程 env CLAWD_DAEMON_URL 闭包来源。
@@ -59529,7 +59765,16 @@ async function startDaemon(config) {
59529
59765
  store: ownerIdentityStore,
59530
59766
  fetchUserInfo: (ttcToken) => fetchTtcUserInfo({ apiBase: TTC_HOSTS.api, token: ttcToken }),
59531
59767
  ttcAuthBase: TTC_HOSTS.auth,
59532
- getCallbackUrl: () => `http://127.0.0.1:${config.port}/auth/callback`
59768
+ getCallbackUrl: () => `http://127.0.0.1:${config.port}/auth/callback`,
59769
+ // device_bindings upsert(did ↔ 飞书 unionId 映射):
59770
+ // deviceId 从 auth.json 稳定读;apiUrl 走同一份 clawosApi;apiKey 复用 CLAWD_TICKETS_API_KEY。
59771
+ // 失败即登录失败(fail-hard,login-flow.spec §upsert 失败)。
59772
+ getDeviceId: () => authFile.deviceId,
59773
+ upsertBinding: (opts) => upsertDeviceBinding({
59774
+ apiUrl: config.clawosApi ?? DEFAULT_CLAWOS_API,
59775
+ apiKey: resolveClawdTicketsApiKey(),
59776
+ ...opts
59777
+ })
59533
59778
  });
59534
59779
  const visitorStore = createVisitorStore({ dir: config.dataDir });
59535
59780
  const exchangeVisitorToken = buildVisitorLogin({
@@ -59610,6 +59855,8 @@ async function startDaemon(config) {
59610
59855
  ownerDisplayName,
59611
59856
  // 决策 #16: whoami owner.id = 本机 deviceId(路由主键)+ ownerId/provider 归属人
59612
59857
  ownerPrincipalId,
59858
+ // per-turn sender §4.6:send handler 无 ctx 兜底 + AppBuilder publish injectUserText 需要
59859
+ ownerFeishuUnionId,
59613
59860
  ownerId,
59614
59861
  ownerProvider,
59615
59862
  personaStore,
@@ -59893,6 +60140,7 @@ async function startDaemon(config) {
59893
60140
  ownerId = result.identity.ownerId;
59894
60141
  ownerProvider = result.identity.provider;
59895
60142
  ownerDisplayName = result.identity.displayName;
60143
+ ownerFeishuUnionId = result.identity.unionId;
59896
60144
  handlers = makeHandlers();
59897
60145
  wsServer?.broadcastToOwners({ type: "auth:login:done", identity: result.identity });
59898
60146
  setImmediate(() => wsServer?.closeAllClients());
@@ -60031,7 +60279,7 @@ async function startDaemon(config) {
60031
60279
  wss.onFrame(async (client, frame) => {
60032
60280
  const type = frame.type;
60033
60281
  const requestId = typeof frame.requestId === "string" ? frame.requestId : void 0;
60034
- const ctx = wsServer.getClientContext(client.id) ?? ownerContext(ownerPrincipalId, ownerDisplayName);
60282
+ const ctx = wsServer.getClientContext(client.id) ?? ownerContext(ownerPrincipalId, ownerDisplayName, ownerFeishuUnionId);
60035
60283
  const result = await dispatchRpc(type, frame, client, ctx, {
60036
60284
  handlers,
60037
60285
  feishuActive: () => feishuActive