@cocorograph/hub-agent 0.8.27 → 0.8.29
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/package.json +1 -1
- package/src/claude-stream-bridge.mjs +23 -0
- package/src/sibling-inject.mjs +98 -7
package/package.json
CHANGED
|
@@ -3000,6 +3000,29 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
3000
3000
|
)
|
|
3001
3001
|
}
|
|
3002
3002
|
|
|
3003
|
+
/**
|
|
3004
|
+
* cwd と session_id で一意に確定したライブセッションの claude.event 経路へ内部イベントを流す。
|
|
3005
|
+
* sibling-inject の delegate-event 専用で、本文 FIFO や sendMessage は経由しない。
|
|
3006
|
+
*/
|
|
3007
|
+
emitToStream(cwd, sessionId, event) {
|
|
3008
|
+
if (!cwd || !sessionId) return { status: "not_found" }
|
|
3009
|
+
const matches = this._liveCandidatesForCwd(cwd, true).filter(
|
|
3010
|
+
({ session }) => session.sessionId === sessionId,
|
|
3011
|
+
)
|
|
3012
|
+
if (matches.length === 0) return { status: "not_found" }
|
|
3013
|
+
if (matches.length > 1) {
|
|
3014
|
+
return {
|
|
3015
|
+
status: "ambiguous",
|
|
3016
|
+
candidates: matches.map(({ session, attached }) =>
|
|
3017
|
+
this._describeCandidate(session, attached),
|
|
3018
|
+
),
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
const { session, attached } = matches[0]
|
|
3022
|
+
session._emit(event)
|
|
3023
|
+
return { status: "delivered", session: this._describeCandidate(session, attached) }
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3003
3026
|
/**
|
|
3004
3027
|
* cwd 内の live セッション候補を `{session, attached}` の配列で返す共通スキャン。
|
|
3005
3028
|
* this.sessions (attached=browser 接続中) と _liveBySession (attached=false=soft-detached の warm)
|
package/src/sibling-inject.mjs
CHANGED
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
*/
|
|
60
60
|
|
|
61
61
|
import { watch } from "node:fs"
|
|
62
|
-
import { mkdir, readFile, writeFile, rename, unlink, readdir } from "node:fs/promises"
|
|
62
|
+
import { chmod, lstat, mkdir, readFile, writeFile, rename, unlink, readdir } from "node:fs/promises"
|
|
63
63
|
import path from "node:path"
|
|
64
64
|
|
|
65
65
|
import { verifySiblingTokenAny, siblingTokenCwd } from "./sibling-token.mjs"
|
|
@@ -268,9 +268,46 @@ export class SiblingInjectBridge {
|
|
|
268
268
|
return { claudeBridge: this.claudeBridge, codexBridge: this.codexBridge }
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
async _lstatDir() {
|
|
272
|
+
return lstat(this.dir)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** IPC ディレクトリを所有者限定にし、安全性を確認できなければ例外で閉じる。 */
|
|
276
|
+
async _prepareDir() {
|
|
277
|
+
try {
|
|
278
|
+
await this._lstatDir()
|
|
279
|
+
} catch (err) {
|
|
280
|
+
if (err?.code !== "ENOENT") throw err
|
|
281
|
+
await mkdir(this.dir, { recursive: true, mode: 0o700 })
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const info = await this._lstatDir()
|
|
285
|
+
if (info.isSymbolicLink()) {
|
|
286
|
+
throw new Error("IPC directory must not be a symbolic link")
|
|
287
|
+
}
|
|
288
|
+
if (!info.isDirectory()) {
|
|
289
|
+
throw new Error("IPC path is not a directory")
|
|
290
|
+
}
|
|
291
|
+
const uid = process.getuid?.()
|
|
292
|
+
if (uid == null || info.uid !== uid) {
|
|
293
|
+
throw new Error("IPC directory is not owned by the current user")
|
|
294
|
+
}
|
|
295
|
+
if ((info.mode & 0o777) !== 0o700) {
|
|
296
|
+
await chmod(this.dir, 0o700)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
271
300
|
/** 監視ディレクトリを作成し、既存要求を sweep してから fs.watch を張る。 */
|
|
272
301
|
async start() {
|
|
273
|
-
|
|
302
|
+
try {
|
|
303
|
+
await this._prepareDir()
|
|
304
|
+
} catch (err) {
|
|
305
|
+
this.logger?.warn(
|
|
306
|
+
{ err: err?.message, dir: this.dir },
|
|
307
|
+
"sibling-inject IPC directory is unsafe, IPC disabled",
|
|
308
|
+
)
|
|
309
|
+
return
|
|
310
|
+
}
|
|
274
311
|
await this._sweep()
|
|
275
312
|
try {
|
|
276
313
|
this._watcher = watch(this.dir, (_event, filename) => {
|
|
@@ -332,7 +369,21 @@ export class SiblingInjectBridge {
|
|
|
332
369
|
// (T06271 レビュー major①): 旧 hub-agent は `!text` で invalid_request を返すため、
|
|
333
370
|
// 未知フィールドを無視して sendToCwd 推測注入する事故が **単一リクエストで原理的に
|
|
334
371
|
// 起きない** (probe→注入の TOCTOU を排除)。
|
|
335
|
-
const {
|
|
372
|
+
const {
|
|
373
|
+
op,
|
|
374
|
+
cwd,
|
|
375
|
+
target_cli_kind,
|
|
376
|
+
text,
|
|
377
|
+
message,
|
|
378
|
+
token,
|
|
379
|
+
target_session_id,
|
|
380
|
+
source,
|
|
381
|
+
delegation_id,
|
|
382
|
+
phase,
|
|
383
|
+
agent,
|
|
384
|
+
description,
|
|
385
|
+
timestamp,
|
|
386
|
+
} = body || {}
|
|
336
387
|
// 連携カード種別 (Phase 2, 2026-07-23): 注入を受け手画面で専用カード表示させるためのタグ。
|
|
337
388
|
// 既知値 ('peer'|'orchestrator') のみ通し、未知値/未指定は undefined に落とす (未知の source を
|
|
338
389
|
// frontend へ流して分岐を汚さない fail-closed)。bridge が user echo にこれを付ける。
|
|
@@ -340,9 +391,22 @@ export class SiblingInjectBridge {
|
|
|
340
391
|
const isListAll = op === "list-all"
|
|
341
392
|
const isList = op === "list"
|
|
342
393
|
const isExplicit = op === "inject"
|
|
394
|
+
const isDelegateEvent = op === "delegate-event"
|
|
343
395
|
let result
|
|
344
396
|
const badKind = target_cli_kind !== "claude" && target_cli_kind !== "codex"
|
|
345
|
-
const
|
|
397
|
+
const delegatePhases = new Set(["started", "completed", "failed", "stopped"])
|
|
398
|
+
const badDelegateBody =
|
|
399
|
+
typeof delegation_id !== "string" ||
|
|
400
|
+
!delegation_id ||
|
|
401
|
+
!delegatePhases.has(phase) ||
|
|
402
|
+
(phase === "started" && (typeof agent !== "string" || !agent)) ||
|
|
403
|
+
(agent !== undefined && typeof agent !== "string") ||
|
|
404
|
+
(description !== undefined && typeof description !== "string") ||
|
|
405
|
+
(timestamp !== undefined &&
|
|
406
|
+
(typeof timestamp !== "string" || Number.isNaN(Date.parse(timestamp))))
|
|
407
|
+
const badBody = isDelegateEvent
|
|
408
|
+
? badDelegateBody
|
|
409
|
+
: isList
|
|
346
410
|
? false
|
|
347
411
|
: isExplicit
|
|
348
412
|
? !target_session_id || !message // 明示は id と message 必須 (空指定 fail-open を防ぐ)
|
|
@@ -361,7 +425,7 @@ export class SiblingInjectBridge {
|
|
|
361
425
|
// 判定は live-sessions.mjs に一本化する (tmux.list_sessions の warm と同じ関数)。
|
|
362
426
|
result = { ok: true, live_sessions: describeLiveSessionsUnion(this._bridges()) }
|
|
363
427
|
}
|
|
364
|
-
} else if (!cwd || badKind || badBody) {
|
|
428
|
+
} else if (!cwd || (!isDelegateEvent && badKind) || badBody) {
|
|
365
429
|
result = { ok: false, error: "invalid_request" }
|
|
366
430
|
} else if (!verifySiblingTokenAny(token)) {
|
|
367
431
|
// 2026-07-23 統一再設計 1-2: 認可を「ターゲット cwd 所有」→「いずれかの cwd の有効トークン
|
|
@@ -384,7 +448,34 @@ export class SiblingInjectBridge {
|
|
|
384
448
|
}
|
|
385
449
|
}
|
|
386
450
|
const bridge = target_cli_kind === "codex" ? this.codexBridge : this.claudeBridge
|
|
387
|
-
if (
|
|
451
|
+
if (isDelegateEvent) {
|
|
452
|
+
// 委譲カードは別 Claude 会話へ誤配送すると誤解を生むため、cwd 内の 1 本へ推測配送しない。
|
|
453
|
+
// target_session_id が無い場合も、指定先が一意に見つからない場合も同じ fail-closed 契約にする。
|
|
454
|
+
if (!target_session_id) {
|
|
455
|
+
result = { ok: false, error: AMBIGUOUS_TARGET_ERROR }
|
|
456
|
+
} else {
|
|
457
|
+
const event = {
|
|
458
|
+
type: "system",
|
|
459
|
+
subtype: "codex_delegate",
|
|
460
|
+
delegation_id,
|
|
461
|
+
phase,
|
|
462
|
+
...(agent !== undefined ? { agent } : {}),
|
|
463
|
+
...(description !== undefined ? { description } : {}),
|
|
464
|
+
timestamp: timestamp || new Date().toISOString(),
|
|
465
|
+
}
|
|
466
|
+
const r = this.claudeBridge?.emitToStream
|
|
467
|
+
? this.claudeBridge.emitToStream(cwd, target_session_id, event)
|
|
468
|
+
: { status: "not_found" }
|
|
469
|
+
result =
|
|
470
|
+
r.status === "delivered"
|
|
471
|
+
? { ok: true, delivered_to: r.session }
|
|
472
|
+
: {
|
|
473
|
+
ok: false,
|
|
474
|
+
error: AMBIGUOUS_TARGET_ERROR,
|
|
475
|
+
...(r.candidates ? { candidates: r.candidates } : {}),
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
} else if (isList) {
|
|
388
479
|
// 宛先候補の列挙 (cockpit_orch.py --list 用)。この cwd の同種セッションを返す。
|
|
389
480
|
// ⚠️ tmux.list_sessions の `warm` と同じ関数を通す (二重定義にすると「グレー表示
|
|
390
481
|
// なのに宛先一覧に出ない」食い違いが再発する)。
|
|
@@ -586,7 +677,7 @@ export class SiblingInjectBridge {
|
|
|
586
677
|
const fp = path.join(this.dir, `${request_id}.result.json`)
|
|
587
678
|
const tmp = `${fp}.tmp`
|
|
588
679
|
try {
|
|
589
|
-
await writeFile(tmp, JSON.stringify(result))
|
|
680
|
+
await writeFile(tmp, JSON.stringify(result), { mode: 0o600 })
|
|
590
681
|
await rename(tmp, fp)
|
|
591
682
|
} catch (err) {
|
|
592
683
|
this.logger?.warn({ err: err?.message, request_id }, "sibling-inject result write failed")
|