@clawos-dev/clawd 0.2.164 → 0.2.165

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
@@ -105,6 +105,12 @@ var init_methods = __esm({
105
105
  "persona:get",
106
106
  "persona:update",
107
107
  "persona:delete",
108
+ // ---- personaDispatch:* (跨设备 dispatch) ----
109
+ // personaDispatch:run 有两个入口:本地 cc 经 /api/rpc(EXPOSED_RPC_METHODS,127.0.0.1 无 token);
110
+ // 跨设备时 A 的 peer daemon 经 /rpc(Bearer connect token)。后者要求它是 WS METHOD_NAME +
111
+ // METHOD_GRANT_MAP capability-scoped(handler 自检 persona.public + assertGrant(send))。
112
+ // personaDispatch:complete 仅 B 本地 cc 调(/api/rpc),不跨设备,故不列入 WS method。
113
+ "personaDispatch:run",
108
114
  // ---- session:pty 双向透传(仅 CLAWD_CC_MODE=tui 才生效,对应 session:pty push 帧的上行) ----
109
115
  // session:pty:input — UI 把用户键盘字节(base64 UTF-8)发给 daemon,daemon 直写 pty stdin。
110
116
  // 高频帧(每次按键 1 帧),不入 reducer / 不广播。response 是 fire-and-forget 风格 ack。
@@ -5982,7 +5988,10 @@ var init_dispatch = __esm({
5982
5988
  ]);
5983
5989
  DispatchRunArgsSchema = external_exports.object({
5984
5990
  targetPersona: external_exports.string().min(1),
5985
- prompt: external_exports.string()
5991
+ prompt: external_exports.string(),
5992
+ // 跨设备 dispatch:非空 = A 角色,转发到该 contact deviceId 的 peer daemon;
5993
+ // 省略 = 本地 dispatch / B 角色本地执行。A 转发给 B 时 body 不带此字段。
5994
+ targetDeviceId: external_exports.string().min(1).optional()
5986
5995
  });
5987
5996
  DispatchCompleteArgsSchema = external_exports.object({
5988
5997
  dispatchId: external_exports.string().min(1),
@@ -32142,6 +32151,14 @@ tool \u4F1A\u963B\u585E\u6302\u8D77\u7B49\u5BF9\u65B9\u4EBA\u683C\u5E72\u5B8C\uF
32142
32151
  - \u4E00\u53E5\u8BDD\u91CC\u591A\u4E2A \`@persona/<id>\` \u65F6\u9009\u7B2C\u4E00\u4E2A\uFF08tool \u4E00\u6B21\u53EA\u5904\u7406\u4E00\u4E2A target\uFF09
32143
32152
  - \u59D4\u6D3E\u5931\u8D25\uFF08tool_result.is_error=true\uFF09\u65F6\u5982\u5B9E\u544A\u8BC9\u7528\u6237\u539F\u56E0\uFF0C**\u4E0D\u8981\u91CD\u8BD5**
32144
32153
 
32154
+ ### \u8DE8\u8BBE\u5907\u59D4\u6D3E\uFF08remote persona\uFF09
32155
+
32156
+ \u5F53 @ token \u5F62\u5982 \`@persona/<id>@<\u5BF9\u65B9>\`\uFF08\`<\u5BF9\u65B9>\` \u662F\u8054\u7CFB\u4EBA\u540D\uFF09\u65F6\uFF0C\u662F\u628A\u4EFB\u52A1\u59D4\u6D3E\u7ED9\u90A3\u53F0\u8BBE\u5907\u4E0A\u7684 persona\u3002\u9664 targetPersona / prompt \u5916\u518D\u5E26\u4E00\u4E2A\u53C2\u6570\uFF1A
32157
+
32158
+ - \`targetDeviceId = <\u5BF9\u65B9\u5BF9\u5E94\u7684 contact deviceId>\`
32159
+
32160
+ \u4F60\u4E0D\u77E5\u9053\u786E\u5207 deviceId \u65F6\u6309\u8054\u7CFB\u4EBA\u540D\u539F\u6837\u586B \`targetDeviceId\`\uFF0Cdaemon \u4F1A\u89E3\u6790\uFF1B\u89E3\u6790\u4E0D\u5230 / \u5BF9\u65B9\u672A\u6388\u6743\u4F1A\u8FD4\u56DE\u660E\u786E\u9519\u8BEF\uFF0C\u7167\u5E38\u628A\u9519\u8BEF\u8F6C\u8FF0\u7ED9\u7528\u6237\u5373\u53EF\u3002\u5176\u4F59\u884C\u4E3A\uFF08\u963B\u585E\u7B49\u7ED3\u679C\u3001\u5931\u8D25\u4E0D\u91CD\u8BD5\uFF09\u4E0E\u672C\u5730\u59D4\u6D3E\u4E00\u81F4\u3002
32161
+
32145
32162
  ## \u88AB dispatch \u63A5\u5230\u4EFB\u52A1
32146
32163
 
32147
32164
  \u5982\u679C\u4F60\u7684 session \u7B2C\u4E00\u6761 user message \u662F\u4EE5 \`[Dispatched from owner \u2014 another persona session\u59D4\u6D3E\u7ED9\u4F60\u6267\u884C\u4EFB\u52A1]\` \u8D77\u5934\u7684 plain text \u4EFB\u52A1\u5305\uFF0C\u4F60\u662F\u88AB\u53E6\u4E00\u4E2A persona \u59D4\u6D3E\u6765\u5E72\u6D3B\u7684\uFF08\u4EFB\u52A1\u5305\u4E0D\u7528 \`<system-reminder>\` \u5305\u662F\u4E3A\u4E86 reducer dedup \u5DE5\u4F5C\u6B63\u5E38\uFF0C\u89C1 manager.ts buildDispatchTaskPack \u6CE8\u91CA\uFF09\uFF1A
@@ -34666,7 +34683,12 @@ var SessionManager = class {
34666
34683
  throw new Error("createDispatchedSession: personaDispatchManager missing in ManagerDeps");
34667
34684
  }
34668
34685
  const sessionId = newSessionId();
34669
- const scope = {
34686
+ const scope = args.guestPrincipalId ? {
34687
+ kind: "persona",
34688
+ personaId: args.targetPersona,
34689
+ mode: "guest",
34690
+ capId: args.guestPrincipalId
34691
+ } : {
34670
34692
  kind: "persona",
34671
34693
  personaId: args.targetPersona,
34672
34694
  mode: "owner"
@@ -34679,6 +34701,13 @@ var SessionManager = class {
34679
34701
  tool: "claude",
34680
34702
  ownerPersonaId: args.targetPersona,
34681
34703
  dispatchedFromSessionId: args.sourceSessionId,
34704
+ // 跨设备显示关键(spec §8):creatorPrincipalId = A 的 deviceId,让 A 的
34705
+ // useRemoteSessionsByOwner(按 creatorPrincipalId === A.deviceId 过滤)能 list 到此 B session。
34706
+ // creatorDisplayName = A 的 displayName,供 guest meta callerDisplayName(reducer 要求)。
34707
+ ...args.guestPrincipalId ? {
34708
+ creatorPrincipalId: args.guestPrincipalId,
34709
+ ...args.guestDisplayName ? { creatorDisplayName: args.guestDisplayName } : {}
34710
+ } : {},
34682
34711
  createdAt: now,
34683
34712
  updatedAt: now
34684
34713
  };
@@ -37908,12 +37937,104 @@ function resolveClawdTicketsApiKey() {
37908
37937
 
37909
37938
  // src/handlers/persona-dispatch.ts
37910
37939
  init_protocol();
37940
+
37941
+ // src/permission/capability.ts
37942
+ function matchResource(grant, target) {
37943
+ if (grant.type === "*") return true;
37944
+ if (grant.type !== target.type) return false;
37945
+ if (grant.id === "*") return true;
37946
+ return grant.id === target.id;
37947
+ }
37948
+ function assertGrant(grants, resource, action) {
37949
+ for (const g2 of grants) {
37950
+ if (!matchResource(g2.resource, resource)) continue;
37951
+ if (g2.actions.includes(action)) return true;
37952
+ if (g2.actions.includes("admin")) return true;
37953
+ }
37954
+ return false;
37955
+ }
37956
+
37957
+ // src/permission/session-access.ts
37958
+ function canAccessSession(ctx, sessionId, action, deps) {
37959
+ if (ctx.principal.kind === "owner") return true;
37960
+ const file = deps.readSession(sessionId);
37961
+ if (!file) return true;
37962
+ return canAccessPersona(ctx.grants, file.ownerPersonaId, action);
37963
+ }
37964
+ function canAccessPersona(grants, personaId, action) {
37965
+ if (!personaId) return false;
37966
+ const resource = { type: "persona", id: personaId };
37967
+ return assertGrant(grants, resource, action);
37968
+ }
37969
+
37970
+ // src/handlers/persona-dispatch.ts
37911
37971
  function buildPersonaDispatchHandlers(deps) {
37912
37972
  const { personaDispatchManager: mgr, spawnB, logger } = deps;
37913
- const run = async (frame) => {
37973
+ const run = async (frame, _client, ctx) => {
37914
37974
  const { type: _t, requestId: _r, ...rest } = frame;
37915
37975
  const sourceSessionId = typeof rest.sessionId === "string" ? rest.sessionId : void 0;
37916
37976
  const args = DispatchRunArgsSchema.parse(rest);
37977
+ if (args.targetDeviceId) {
37978
+ if (ctx?.principal.kind === "guest") {
37979
+ throw new ClawdError(
37980
+ ERROR_CODES.UNAUTHORIZED,
37981
+ "cross-device forward cannot be initiated by a guest"
37982
+ );
37983
+ }
37984
+ if (!deps.forwardToPeer) {
37985
+ throw new Error("cross-device dispatch not wired (forwardToPeer missing)");
37986
+ }
37987
+ logger?.info("dispatch.run.forward", {
37988
+ targetDeviceId: args.targetDeviceId,
37989
+ targetPersona: args.targetPersona
37990
+ });
37991
+ const outcome2 = await deps.forwardToPeer({
37992
+ targetDeviceId: args.targetDeviceId,
37993
+ targetPersona: args.targetPersona,
37994
+ prompt: args.prompt
37995
+ });
37996
+ return {
37997
+ response: { type: "personaDispatch:run:ok", outcome: outcome2 }
37998
+ };
37999
+ }
38000
+ if (ctx?.principal.kind === "guest") {
38001
+ const isPublic = deps.getPersonaPublic?.(args.targetPersona) ?? false;
38002
+ if (!isPublic || !canAccessPersona(ctx.grants, args.targetPersona, "send")) {
38003
+ throw new ClawdError(
38004
+ ERROR_CODES.UNAUTHORIZED,
38005
+ `persona not dispatchable: ${args.targetPersona}`
38006
+ );
38007
+ }
38008
+ const { dispatchId: dispatchId2 } = mgr.start({
38009
+ sourceSessionId: ctx.principal.id,
38010
+ targetPersona: args.targetPersona
38011
+ });
38012
+ logger?.info("dispatch.run.received.guest", {
38013
+ dispatchId: dispatchId2,
38014
+ sourcePrincipal: ctx.principal.id,
38015
+ targetPersona: args.targetPersona,
38016
+ promptLen: args.prompt.length
38017
+ });
38018
+ void spawnB({
38019
+ dispatchId: dispatchId2,
38020
+ sourceSessionId: ctx.principal.id,
38021
+ targetPersona: args.targetPersona,
38022
+ prompt: args.prompt,
38023
+ guestPrincipalId: ctx.principal.id,
38024
+ guestDisplayName: ctx.principal.displayName
38025
+ }).then(() => logger?.info("dispatch.spawnB.ok", { dispatchId: dispatchId2 })).catch((err) => {
38026
+ const reason = err instanceof Error ? err.message : String(err);
38027
+ logger?.warn("dispatch.spawnB.failed", { dispatchId: dispatchId2, reason });
38028
+ mgr.complete(dispatchId2, {
38029
+ kind: "failure",
38030
+ reason: `failed to spawn B: ${reason}`
38031
+ });
38032
+ });
38033
+ const outcome2 = await mgr.wait(dispatchId2);
38034
+ return {
38035
+ response: { type: "personaDispatch:run:ok", outcome: outcome2 }
38036
+ };
38037
+ }
37917
38038
  if (!sourceSessionId) {
37918
38039
  throw new Error(
37919
38040
  "personaDispatch:run requires sessionId (caller must pass x-clawd-session-id header)"
@@ -37972,6 +38093,46 @@ function buildPersonaDispatchHandlers(deps) {
37972
38093
  };
37973
38094
  }
37974
38095
 
38096
+ // src/dispatch/peer-forward.ts
38097
+ function wsUrlToHttp(url) {
38098
+ if (url.startsWith("wss://")) return "https://" + url.slice("wss://".length);
38099
+ if (url.startsWith("ws://")) return "http://" + url.slice("ws://".length);
38100
+ return url;
38101
+ }
38102
+ async function forwardDispatchToPeer(args) {
38103
+ const f = args.fetchImpl ?? fetch;
38104
+ const base = wsUrlToHttp(args.contact.remoteUrl).replace(/\/+$/, "");
38105
+ const url = `${base}/rpc/personaDispatch:run`;
38106
+ let res;
38107
+ try {
38108
+ res = await f(url, {
38109
+ method: "POST",
38110
+ headers: {
38111
+ "content-type": "application/json",
38112
+ authorization: `Bearer ${args.contact.connectToken}`
38113
+ },
38114
+ // 注意:不带 targetDeviceId —— B 端据此判定为本地执行(B 角色)。
38115
+ body: JSON.stringify({ targetPersona: args.targetPersona, prompt: args.prompt })
38116
+ });
38117
+ } catch (err) {
38118
+ const msg = err instanceof Error ? err.message : String(err);
38119
+ return { kind: "failure", reason: `forward to peer failed: ${msg}` };
38120
+ }
38121
+ let json;
38122
+ try {
38123
+ json = await res.json();
38124
+ } catch {
38125
+ return {
38126
+ kind: "failure",
38127
+ reason: `peer returned non-JSON response (HTTP ${res.status})`
38128
+ };
38129
+ }
38130
+ if (json.ok === false) {
38131
+ return { kind: "failure", reason: `peer rejected: ${json.error}: ${json.message}` };
38132
+ }
38133
+ return json.result.outcome;
38134
+ }
38135
+
37975
38136
  // src/tools/codex-history.ts
37976
38137
  var import_node_child_process5 = require("child_process");
37977
38138
  var DEFAULT_TIMEOUT_MS = 8e3;
@@ -42472,35 +42633,6 @@ function forkSession(input) {
42472
42633
  return { forkedToolSessionId, forkedFilePath };
42473
42634
  }
42474
42635
 
42475
- // src/permission/capability.ts
42476
- function matchResource(grant, target) {
42477
- if (grant.type === "*") return true;
42478
- if (grant.type !== target.type) return false;
42479
- if (grant.id === "*") return true;
42480
- return grant.id === target.id;
42481
- }
42482
- function assertGrant(grants, resource, action) {
42483
- for (const g2 of grants) {
42484
- if (!matchResource(g2.resource, resource)) continue;
42485
- if (g2.actions.includes(action)) return true;
42486
- if (g2.actions.includes("admin")) return true;
42487
- }
42488
- return false;
42489
- }
42490
-
42491
- // src/permission/session-access.ts
42492
- function canAccessSession(ctx, sessionId, action, deps) {
42493
- if (ctx.principal.kind === "owner") return true;
42494
- const file = deps.readSession(sessionId);
42495
- if (!file) return true;
42496
- return canAccessPersona(ctx.grants, file.ownerPersonaId, action);
42497
- }
42498
- function canAccessPersona(grants, personaId, action) {
42499
- if (!personaId) return false;
42500
- const resource = { type: "persona", id: personaId };
42501
- return assertGrant(grants, resource, action);
42502
- }
42503
-
42504
42636
  // src/handlers/session.ts
42505
42637
  init_protocol();
42506
42638
  function buildSessionHandlers(deps) {
@@ -46138,6 +46270,10 @@ var METHOD_GRANT_MAP = {
46138
46270
  "persona:get": CAPABILITY_SCOPED,
46139
46271
  "persona:update": ADMIN_ANY,
46140
46272
  "persona:delete": ADMIN_ANY,
46273
+ // 跨设备 dispatch:A 的 peer daemon 凭 connect token 经 /rpc 调进来;dispatcher 放行,
46274
+ // run handler 内做 persona.public + assertGrant(send) 双重校验(guest grants 是 wildcard,
46275
+ // 单 assertGrant 不够)。本地 cc 仍走 /api/rpc(EXPOSED_RPC_METHODS),不经此表。
46276
+ "personaDispatch:run": CAPABILITY_SCOPED,
46141
46277
  // 2026-05-26 修 clawd people TUI 渲染 mix 串扰回归(参考 f1133ff0 原 owner 路径
46142
46278
  // snapshot/buffer/flush 修复):之前三件套 ADMIN_ANY 把 guest 拦在 dispatcher 401,
46143
46279
  // 导致 XtermPanel 重挂载拉 snapshot 在 guest 路径不生效,老的字节流断裂串扰回来。
@@ -47212,29 +47348,52 @@ async function startDaemon(config) {
47212
47348
  sourceSessionId: args.sourceSessionId
47213
47349
  });
47214
47350
  const sourceFile = manager.findOwnedSession(args.sourceSessionId);
47215
- if (!sourceFile) {
47351
+ if (!sourceFile && !args.guestPrincipalId) {
47216
47352
  throw new Error(`personaDispatch:run source session not found: ${args.sourceSessionId}`);
47217
47353
  }
47218
- const sourceJsonlPath = sourceFile.toolSessionId ? import_node_path48.default.join(
47219
- import_node_os20.default.homedir(),
47220
- ".claude",
47221
- "projects",
47222
- cwdToHashDir(sourceFile.cwd),
47223
- `${sourceFile.toolSessionId}.jsonl`
47224
- ) : "(no transcript yet \u2014 operate from the task description alone)";
47354
+ let sourceJsonlPath = "(no transcript yet \u2014 operate from the task description alone)";
47355
+ if (sourceFile && sourceFile.toolSessionId) {
47356
+ sourceJsonlPath = import_node_path48.default.join(
47357
+ import_node_os20.default.homedir(),
47358
+ ".claude",
47359
+ "projects",
47360
+ cwdToHashDir(sourceFile.cwd),
47361
+ `${sourceFile.toolSessionId}.jsonl`
47362
+ );
47363
+ }
47225
47364
  logger.info("dispatch.spawnB.source-resolved", {
47226
47365
  dispatchId: args.dispatchId,
47227
47366
  sourceJsonlPath,
47228
- hasToolSessionId: Boolean(sourceFile.toolSessionId)
47367
+ hasToolSessionId: Boolean(sourceFile?.toolSessionId)
47229
47368
  });
47230
47369
  manager.createDispatchedSession({
47231
47370
  dispatchId: args.dispatchId,
47232
47371
  sourceSessionId: args.sourceSessionId,
47233
47372
  targetPersona: args.targetPersona,
47234
47373
  prompt: args.prompt,
47235
- sourceJsonlPath
47374
+ sourceJsonlPath,
47375
+ // B 角色(跨设备):非空 → guest scope + creatorPrincipalId/creatorDisplayName = A。
47376
+ guestPrincipalId: args.guestPrincipalId,
47377
+ guestDisplayName: args.guestDisplayName
47236
47378
  });
47237
- }
47379
+ },
47380
+ // A 角色:从 ContactStore 取 peer 可达 URL + connect token,转发到对端 daemon /rpc。
47381
+ forwardToPeer: async ({ targetDeviceId, targetPersona, prompt }) => {
47382
+ const contact = contactStore.get(targetDeviceId);
47383
+ if (!contact || !contact.remoteUrl || !contact.connectToken) {
47384
+ return {
47385
+ kind: "failure",
47386
+ reason: `unknown or unreachable contact: ${targetDeviceId}`
47387
+ };
47388
+ }
47389
+ return forwardDispatchToPeer({
47390
+ contact: { remoteUrl: contact.remoteUrl, connectToken: contact.connectToken },
47391
+ targetPersona,
47392
+ prompt
47393
+ });
47394
+ },
47395
+ // B 角色:判断 targetPersona 是否 public(跨设备授权边界,private 拒)。
47396
+ getPersonaPublic: (personaId) => personaRegistry.get(personaId)?.public ?? false
47238
47397
  });
47239
47398
  handlers = { ...handlers, ...dispatchHandlers };
47240
47399
  const authResolver = new AuthContextResolver({
@@ -39576,7 +39576,9 @@ async function handlePersonaDispatchToolCall(input, ctx) {
39576
39576
  },
39577
39577
  body: JSON.stringify({
39578
39578
  targetPersona: input.targetPersona,
39579
- prompt: input.prompt
39579
+ prompt: input.prompt,
39580
+ // 跨设备:非空时透传,daemon run handler 据此走 A 角色转发分支。
39581
+ ...input.targetDeviceId ? { targetDeviceId: input.targetDeviceId } : {}
39580
39582
  })
39581
39583
  });
39582
39584
  } catch (err) {
@@ -39708,6 +39710,9 @@ async function main() {
39708
39710
  targetPersona: external_exports.string().describe("persona id (e.g. persona-bug-fixer)"),
39709
39711
  prompt: external_exports.string().describe(
39710
39712
  "the task to perform, with the @persona/<id> token stripped out"
39713
+ ),
39714
+ targetDeviceId: external_exports.string().optional().describe(
39715
+ "contact deviceId of the remote device when delegating cross-device (omit for a local persona)"
39711
39716
  )
39712
39717
  }
39713
39718
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawos-dev/clawd",
3
- "version": "0.2.164",
3
+ "version": "0.2.165",
4
4
  "description": "Standalone clawd daemon — Claude Code (and future Codex) session server over WebSocket",
5
5
  "type": "module",
6
6
  "license": "MIT",