@cocorograph/hub-agent 0.8.121 → 0.8.122
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/codex-appserver-client.mjs +2 -0
- package/src/codex-stream-bridge.mjs +2 -2
- package/src/delegate-display-auth.mjs +80 -0
- package/src/delegation-event-store.mjs +18 -14
- package/src/main.mjs +9 -0
- package/src/sibling-inject.mjs +53 -12
- package/src/sibling-token.mjs +10 -0
package/package.json
CHANGED
|
@@ -98,6 +98,8 @@ export class CodexAppServerClient extends EventEmitter {
|
|
|
98
98
|
"COCKPIT_SIBLING_SELF_KIND",
|
|
99
99
|
"COCKPIT_SIBLING_CWD",
|
|
100
100
|
"COCKPIT_SIBLING_TOKEN",
|
|
101
|
+
"COCKPIT_DELEGATE_DISPLAY_TOKEN",
|
|
102
|
+
"COCKPIT_DELEGATE_EVENT_ACK_VERSION",
|
|
101
103
|
]) {
|
|
102
104
|
const val = spawnEnv[key]
|
|
103
105
|
// TOML 文字列としてクォート (パスの空白/特殊文字対策)。値に " は含まれない
|
|
@@ -4204,7 +4204,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
4204
4204
|
}
|
|
4205
4205
|
|
|
4206
4206
|
/** Display events never start a turn and only target an exact thread in this cwd. */
|
|
4207
|
-
emitToStream(cwd, sessionId, event) {
|
|
4207
|
+
emitToStream(cwd, sessionId, event, notificationName = "cockpit/review_event") {
|
|
4208
4208
|
if (!cwd || !sessionId) return { status: "not_found" }
|
|
4209
4209
|
const matches = [...this._liveSessions()].filter((s) => s.cwd === cwd && s.threadId === sessionId)
|
|
4210
4210
|
if (matches.length !== 1) return { status: matches.length ? "ambiguous" : "not_found" }
|
|
@@ -4214,7 +4214,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
4214
4214
|
producer_seq: session.seq,
|
|
4215
4215
|
thread_id: session.threadId,
|
|
4216
4216
|
cwd: session.cwd,
|
|
4217
|
-
notification: { method:
|
|
4217
|
+
notification: { method: notificationName, params: event },
|
|
4218
4218
|
})
|
|
4219
4219
|
return { status: "delivered", session: this._describe(session) }
|
|
4220
4220
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto"
|
|
4
|
+
import { paths } from "./config.mjs"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Publish a complete key atomically: write a temp file in the same directory, then link(2) it to
|
|
8
|
+
* the final name. link fails with EEXIST if another process won, so the published file is always
|
|
9
|
+
* complete and never overwritten (overwriting would invalidate tokens already handed out).
|
|
10
|
+
* An existing malformed key is NOT regenerated here; key() rejects it so the failure stays visible.
|
|
11
|
+
*/
|
|
12
|
+
function publishKey(dir, file) {
|
|
13
|
+
const tmp = path.join(dir, `key.${process.pid}.${randomBytes(8).toString("hex")}.tmp`)
|
|
14
|
+
try {
|
|
15
|
+
const fd = fs.openSync(tmp, "wx", 0o600)
|
|
16
|
+
try {
|
|
17
|
+
fs.writeFileSync(fd, randomBytes(32))
|
|
18
|
+
fs.fsyncSync(fd)
|
|
19
|
+
} finally { fs.closeSync(fd) }
|
|
20
|
+
try {
|
|
21
|
+
fs.linkSync(tmp, file)
|
|
22
|
+
} catch (err) {
|
|
23
|
+
if (err.code !== "EEXIST") throw new Error("delegate display auth key creation failed")
|
|
24
|
+
}
|
|
25
|
+
} finally {
|
|
26
|
+
try { fs.unlinkSync(tmp) } catch {}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function key() {
|
|
31
|
+
const dir = process.env.HUB_DELEGATE_DISPLAY_AUTH_DIR || path.join(paths.configDir, "delegate-display-auth")
|
|
32
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
33
|
+
const directory = fs.lstatSync(dir)
|
|
34
|
+
if (!directory.isDirectory() || directory.isSymbolicLink() || process.getuid?.() == null ||
|
|
35
|
+
directory.uid !== process.getuid() || (directory.mode & 0o777) !== 0o700) {
|
|
36
|
+
throw new Error("unsafe delegate display auth directory")
|
|
37
|
+
}
|
|
38
|
+
const file = path.join(dir, "key")
|
|
39
|
+
let info
|
|
40
|
+
try {
|
|
41
|
+
info = fs.lstatSync(file)
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err.code !== "ENOENT") throw err
|
|
44
|
+
publishKey(dir, file)
|
|
45
|
+
info = fs.lstatSync(file)
|
|
46
|
+
}
|
|
47
|
+
if (!info.isFile() || info.isSymbolicLink() || info.uid !== process.getuid() ||
|
|
48
|
+
(info.mode & 0o777) !== 0o600 || info.size !== 32) {
|
|
49
|
+
throw new Error("unsafe delegate display auth key")
|
|
50
|
+
}
|
|
51
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW)
|
|
52
|
+
try {
|
|
53
|
+
const opened = fs.fstatSync(fd)
|
|
54
|
+
if (!opened.isFile() || opened.ino !== info.ino || opened.dev !== info.dev ||
|
|
55
|
+
opened.uid !== info.uid || (opened.mode & 0o777) !== 0o600 || opened.size !== 32) {
|
|
56
|
+
throw new Error("delegate display auth key changed while reading")
|
|
57
|
+
}
|
|
58
|
+
const bytes = Buffer.alloc(33)
|
|
59
|
+
let length = 0
|
|
60
|
+
while (length < bytes.length) {
|
|
61
|
+
const count = fs.readSync(fd, bytes, length, bytes.length - length, null)
|
|
62
|
+
if (!count) break
|
|
63
|
+
length += count
|
|
64
|
+
}
|
|
65
|
+
if (length !== 32) throw new Error("invalid delegate display auth key length")
|
|
66
|
+
return bytes.subarray(0, 32)
|
|
67
|
+
} finally { fs.closeSync(fd) }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function displayTokenForCwd(cwd) {
|
|
71
|
+
if (typeof cwd !== "string" || !cwd) throw new Error("invalid delegate display cwd")
|
|
72
|
+
return "v1." + createHmac("sha256", key()).update("delegate-event\0" + cwd).digest("base64url")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function verifyDisplayToken(cwd, token) {
|
|
76
|
+
if (typeof cwd !== "string" || !cwd || typeof token !== "string" || !/^v1\.[A-Za-z0-9_-]{43}$/.test(token)) return false
|
|
77
|
+
const expected = Buffer.from(displayTokenForCwd(cwd))
|
|
78
|
+
const actual = Buffer.from(token)
|
|
79
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected)
|
|
80
|
+
}
|
|
@@ -92,9 +92,13 @@ export class DelegationEventStore {
|
|
|
92
92
|
this.queue = pending.catch(() => {})
|
|
93
93
|
return pending
|
|
94
94
|
}
|
|
95
|
-
|
|
95
|
+
_target(cwd, sessionId, kind = "claude") {
|
|
96
|
+
if (!["claude", "codex"].includes(kind)) throw new Error("invalid delegation kind")
|
|
97
|
+
return kind === "codex" ? [cwd, sessionId, kind] : [cwd, sessionId]
|
|
98
|
+
}
|
|
99
|
+
_file(cwd, sessionId, kind = "claude") {
|
|
96
100
|
if ([cwd, sessionId].some((value) => typeof value !== "string" || !value)) throw new Error("invalid delegation target")
|
|
97
|
-
return path.join(this.dir, createHash("sha256").update(JSON.stringify(
|
|
101
|
+
return path.join(this.dir, createHash("sha256").update(JSON.stringify(this._target(cwd, sessionId, kind))).digest("hex") + ".json")
|
|
98
102
|
}
|
|
99
103
|
async _checkDirectory() {
|
|
100
104
|
const info = await fs.lstat(this.dir)
|
|
@@ -135,7 +139,7 @@ export class DelegationEventStore {
|
|
|
135
139
|
snapshot = JSON.parse(buffer.subarray(0, length).toString("utf8"))
|
|
136
140
|
} finally { await handle.close() }
|
|
137
141
|
if (!snapshot || snapshot.schema_version !== 1 || !Array.isArray(snapshot.target) ||
|
|
138
|
-
snapshot.target.length
|
|
142
|
+
!(snapshot.target.length === 2 || (snapshot.target.length === 3 && snapshot.target[2] === "codex")) || snapshot.target.some((v) => typeof v !== "string" || !v) ||
|
|
139
143
|
(target && JSON.stringify(snapshot.target) !== JSON.stringify(target)) ||
|
|
140
144
|
this._file(...snapshot.target) !== file || !Array.isArray(snapshot.rows)) throw new Error("invalid delegation snapshot")
|
|
141
145
|
const ids = new Set()
|
|
@@ -159,23 +163,23 @@ export class DelegationEventStore {
|
|
|
159
163
|
.slice(0, Math.max(0, oldest.length - DELEGATION_SESSION_LIMIT)))
|
|
160
164
|
return rows.filter((row) => !removed.has(row))
|
|
161
165
|
}
|
|
162
|
-
rows(cwd, sessionId) {
|
|
166
|
+
rows(cwd, sessionId, kind = "claude") {
|
|
163
167
|
return this._serial(async () => {
|
|
164
168
|
await this._prepare()
|
|
165
|
-
const file = this._file(cwd, sessionId)
|
|
166
|
-
return this._prune(await this._read(file,
|
|
169
|
+
const file = this._file(cwd, sessionId, kind)
|
|
170
|
+
return this._prune(await this._read(file, this._target(cwd, sessionId, kind)))
|
|
167
171
|
})
|
|
168
172
|
}
|
|
169
|
-
record(cwd, sessionId, event) {
|
|
173
|
+
record(cwd, sessionId, event, kind = "claude") {
|
|
170
174
|
// Snapshot caller-owned input before waiting for the writer.
|
|
171
175
|
const copy = structuredClone(event)
|
|
172
|
-
return this._serial(() => this._record(cwd, sessionId, copy))
|
|
176
|
+
return this._serial(() => this._record(cwd, sessionId, copy, kind))
|
|
173
177
|
}
|
|
174
|
-
async _record(cwd, sessionId, event) {
|
|
178
|
+
async _record(cwd, sessionId, event, kind = "claude") {
|
|
175
179
|
if (!validEvent(event)) throw new Error("invalid delegation event")
|
|
176
180
|
await this._prepare()
|
|
177
|
-
const file = this._file(cwd, sessionId)
|
|
178
|
-
const target =
|
|
181
|
+
const file = this._file(cwd, sessionId, kind)
|
|
182
|
+
const target = this._target(cwd, sessionId, kind)
|
|
179
183
|
const rows = this._prune(await this._read(file, target))
|
|
180
184
|
const result = this._apply(rows, target, event)
|
|
181
185
|
if (result.acceptance === "accept") await this._write(file, result.serialized)
|
|
@@ -246,10 +250,10 @@ export class DelegationEventStore {
|
|
|
246
250
|
}
|
|
247
251
|
}
|
|
248
252
|
/** Reconcile the entire liveness plan against one fresh snapshot, then write at most once. */
|
|
249
|
-
replayPlan(cwd, sessionId, plan, emit) {
|
|
253
|
+
replayPlan(cwd, sessionId, plan, emit, kind = "claude") {
|
|
250
254
|
return this._serial(async () => {
|
|
251
|
-
const file = this._file(cwd, sessionId)
|
|
252
|
-
const target =
|
|
255
|
+
const file = this._file(cwd, sessionId, kind)
|
|
256
|
+
const target = this._target(cwd, sessionId, kind)
|
|
253
257
|
const rows = this._prune(await this._read(file, target))
|
|
254
258
|
const current = new Map(rows.map((row) => [row.id, JSON.stringify(row)]))
|
|
255
259
|
// One working copy for the whole plan; `current` keeps the snapshot used for matching.
|
package/src/main.mjs
CHANGED
|
@@ -4365,6 +4365,15 @@ export async function dispatch(msg, ctx) {
|
|
|
4365
4365
|
} catch (err) {
|
|
4366
4366
|
ctx.logger?.warn?.({ err: err?.message }, "failed to replay review events")
|
|
4367
4367
|
}
|
|
4368
|
+
try {
|
|
4369
|
+
if (info.threadId) {
|
|
4370
|
+
void ctx.siblingInjectBridge
|
|
4371
|
+
?.replayDelegations?.(resolvedCwd || process.env.HOME || process.cwd(), info.threadId, "codex")
|
|
4372
|
+
?.catch((err) => ctx.logger?.warn?.({ err: err?.message }, "failed to replay delegation events"))
|
|
4373
|
+
}
|
|
4374
|
+
} catch (err) {
|
|
4375
|
+
ctx.logger?.warn?.({ err: err?.message }, "failed to replay delegation events")
|
|
4376
|
+
}
|
|
4368
4377
|
const session = ctx.codexBridge.sessions?.get(stream_id)
|
|
4369
4378
|
if (session && info.resuming) {
|
|
4370
4379
|
void loadResumeChatContext(session, "codex").catch(() => {})
|
package/src/sibling-inject.mjs
CHANGED
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
* 判断材料として **外部契約に残す**。
|
|
72
72
|
*/
|
|
73
73
|
|
|
74
|
+
import { verifyDisplayToken } from "./delegate-display-auth.mjs"
|
|
74
75
|
import { watch } from "node:fs"
|
|
75
76
|
import { chmod, lstat, mkdir, readFile, writeFile, rename, unlink, readdir } from "node:fs/promises"
|
|
76
77
|
import { execFile as execFileCallback } from "node:child_process"
|
|
@@ -458,6 +459,12 @@ export class SiblingInjectBridge {
|
|
|
458
459
|
}
|
|
459
460
|
}
|
|
460
461
|
|
|
462
|
+
_emitDelegation(kind, cwd, sessionId, event) {
|
|
463
|
+
const bridge = kind === "codex" ? this.codexBridge : this.claudeBridge
|
|
464
|
+
return bridge?.emitToStream?.(cwd, sessionId, event,
|
|
465
|
+
...(kind === "codex" ? ["cockpit/delegate_event"] : [])) || { status: "not_found" }
|
|
466
|
+
}
|
|
467
|
+
|
|
461
468
|
_liveClaudeSession(cwd, sessionId) {
|
|
462
469
|
const matches = describeLiveSessionsUnion(this._bridges(), {
|
|
463
470
|
cwd,
|
|
@@ -497,10 +504,10 @@ export class SiblingInjectBridge {
|
|
|
497
504
|
}
|
|
498
505
|
|
|
499
506
|
/** attach/再接続時に同じ cwd+session の進行中・終了済み委譲を開始順で再送する。 */
|
|
500
|
-
async replayDelegations(cwd, sessionId) {
|
|
507
|
+
async replayDelegations(cwd, sessionId, kind = "claude") {
|
|
501
508
|
if (!cwd || !sessionId) return
|
|
502
509
|
try {
|
|
503
|
-
const snapshot = await this.delegationEvents.rows(cwd, sessionId)
|
|
510
|
+
const snapshot = await this.delegationEvents.rows(cwd, sessionId, kind)
|
|
504
511
|
const rows = snapshot.filter((row) => row.started).sort((a, b) => a.startedAt - b.startedAt)
|
|
505
512
|
const plan = []
|
|
506
513
|
for (const row of rows) {
|
|
@@ -526,7 +533,7 @@ export class SiblingInjectBridge {
|
|
|
526
533
|
}
|
|
527
534
|
}
|
|
528
535
|
await this.delegationEvents.replayPlan(cwd, sessionId, plan,
|
|
529
|
-
(event) => this.
|
|
536
|
+
(event) => this._emitDelegation(kind, cwd, sessionId, event), kind)
|
|
530
537
|
} catch (err) {
|
|
531
538
|
this.logger?.warn?.({ error: err?.message }, "delegation replay failed")
|
|
532
539
|
}
|
|
@@ -749,6 +756,8 @@ export class SiblingInjectBridge {
|
|
|
749
756
|
pid,
|
|
750
757
|
pid_started_at,
|
|
751
758
|
request_ack,
|
|
759
|
+
display_auth,
|
|
760
|
+
ack_capability,
|
|
752
761
|
} = body || {}
|
|
753
762
|
// 連携カード種別 (Phase 2, 2026-07-23): 注入を受け手画面で専用カード表示させるためのタグ。
|
|
754
763
|
// 既知値 ('peer'|'orchestrator') のみ通し、未知値/未指定は undefined に落とす (未知の source を
|
|
@@ -758,6 +767,15 @@ export class SiblingInjectBridge {
|
|
|
758
767
|
const isList = op === "list"
|
|
759
768
|
const isExplicit = op === "inject"
|
|
760
769
|
const isDelegateEvent = op === "delegate-event"
|
|
770
|
+
const ackV2 = isDelegateEvent && request_ack === true && ack_capability === "persisted-delivered-v2"
|
|
771
|
+
const delegateKind = target_cli_kind === undefined ? "claude" : target_cli_kind
|
|
772
|
+
let displayAuthorized = false
|
|
773
|
+
const hasDisplayAuth = isDelegateEvent && Object.hasOwn(body, "display_auth")
|
|
774
|
+
if (hasDisplayAuth) {
|
|
775
|
+
try { displayAuthorized = verifyDisplayToken(cwd, display_auth) } catch {
|
|
776
|
+
this.logger?.warn?.({}, "delegate display auth unavailable")
|
|
777
|
+
}
|
|
778
|
+
}
|
|
761
779
|
const isReviewEvent = op === "review-event"
|
|
762
780
|
const hasRequestAck = Object.prototype.hasOwnProperty.call(body || {}, "request_ack")
|
|
763
781
|
const isRestart = op === "restart"
|
|
@@ -765,6 +783,13 @@ export class SiblingInjectBridge {
|
|
|
765
783
|
const badKind = !["claude", "codex", "grok"].includes(target_cli_kind)
|
|
766
784
|
const delegatePhases = new Set(["started", "completed", "failed", "stopped"])
|
|
767
785
|
const badDelegateBody =
|
|
786
|
+
!["claude", "codex"].includes(delegateKind) ||
|
|
787
|
+
(target_session_id !== undefined && typeof target_session_id !== "string") ||
|
|
788
|
+
Object.keys(body || {}).some((key) => ![
|
|
789
|
+
"op", "cwd", "target_cli_kind", "token", "target_session_id", "delegation_id", "phase",
|
|
790
|
+
"agent", "description", "background", "prompt", "timestamp", "pid", "pid_started_at",
|
|
791
|
+
"request_ack", "display_auth", "ack_capability",
|
|
792
|
+
].includes(key)) ||
|
|
768
793
|
typeof delegation_id !== "string" ||
|
|
769
794
|
!delegation_id ||
|
|
770
795
|
delegation_id.length > DELEGATION_ID_MAX_LENGTH ||
|
|
@@ -782,7 +807,7 @@ export class SiblingInjectBridge {
|
|
|
782
807
|
const badBody = isReviewEvent
|
|
783
808
|
? !validReviewEvent(body.event) || typeof target_session_id !== "string" || !target_session_id ||
|
|
784
809
|
typeof cwd !== "string" || !path.isAbsolute(cwd) ||
|
|
785
|
-
Object.keys(body).some((key) => !["op", "token", "cwd", "target_cli_kind", "target_session_id", "event"].includes(key)) ||
|
|
810
|
+
Object.keys(body).some((key) => !["op", "token", "cwd", "target_cli_kind", "target_session_id", "event", "display_auth", "ack_capability"].includes(key)) ||
|
|
786
811
|
!requestInfo.isFile() || requestInfo.isSymbolicLink() || requestInfo.uid !== process.getuid?.() ||
|
|
787
812
|
(requestInfo.mode & 0o777) !== 0o600
|
|
788
813
|
: isDelegateEvent
|
|
@@ -833,7 +858,7 @@ export class SiblingInjectBridge {
|
|
|
833
858
|
}
|
|
834
859
|
} else if (!cwd || (!isDelegateEvent && badKind) || badBody) {
|
|
835
860
|
result = { ok: false, error: "invalid_request" }
|
|
836
|
-
} else if (!verifySiblingTokenAny(token)) {
|
|
861
|
+
} else if (hasDisplayAuth ? !displayAuthorized : !verifySiblingTokenAny(token)) {
|
|
837
862
|
// 2026-07-23 統一再設計 1-2: 認可を「ターゲット cwd 所有」→「いずれかの cwd の有効トークン
|
|
838
863
|
// 所有 (= 同一ユーザーの spawn 済みセッション)」へ緩和し、クロス cwd 連携を解禁する。
|
|
839
864
|
// cwd は引き続きターゲット解決のスコープとして使う (誤 cwd 着弾・情報漏洩は解決側で担保)。
|
|
@@ -895,18 +920,32 @@ export class SiblingInjectBridge {
|
|
|
895
920
|
let acceptance
|
|
896
921
|
let persisted = true
|
|
897
922
|
try {
|
|
898
|
-
acceptance = await this.delegationEvents.record(cwd, target_session_id, event)
|
|
923
|
+
acceptance = await this.delegationEvents.record(cwd, target_session_id, event, delegateKind)
|
|
899
924
|
} catch (err) {
|
|
900
925
|
persisted = false
|
|
901
926
|
this.logger?.warn?.({ request_id, error: err?.message }, "delegation persistence failed")
|
|
902
927
|
}
|
|
903
|
-
if (
|
|
928
|
+
if (ackV2) {
|
|
929
|
+
persisted = persisted && acceptance !== "full"
|
|
930
|
+
const delivery = acceptance === "stale" ? { status: "not_found" }
|
|
931
|
+
: this._emitDelegation(delegateKind, cwd, target_session_id, event)
|
|
932
|
+
const delivered = delivery.status === "delivered"
|
|
933
|
+
result = !persisted
|
|
934
|
+
? { ok: false, ack_version: 2, persisted: false, delivered,
|
|
935
|
+
error: acceptance === "full" ? "delegation_store_full" : "delegation_persistence_failed", retryable: true }
|
|
936
|
+
: { ok: true, ack_version: 2, persisted: true, delivered,
|
|
937
|
+
...(acceptance === "duplicate" ? { duplicate: true } : {}),
|
|
938
|
+
...(acceptance === "stale" ? { stale: true } : {}),
|
|
939
|
+
...(!delivered && acceptance !== "stale" && acceptance !== "duplicate"
|
|
940
|
+
? { delivery_status: delivery.status } : {}) }
|
|
941
|
+
} else if (acceptance === "duplicate") {
|
|
904
942
|
// ACK は「過去に受理した」だけでなく、対象 stream が現在も live であることも示す。
|
|
905
943
|
// 台帳だけで成功を返すと、宛先消失後の retry を受理済みと誤認させる。
|
|
906
|
-
const live = this._liveClaudeSession(cwd, target_session_id)
|
|
944
|
+
const live = delegateKind === "claude" ? this._liveClaudeSession(cwd, target_session_id)
|
|
945
|
+
: { status: "delivered" }
|
|
907
946
|
// 保存後に未配送のまま切断した可能性があるため、duplicate も再配送する。
|
|
908
947
|
const delivered = live.status === "delivered"
|
|
909
|
-
? this.
|
|
948
|
+
? this._emitDelegation(delegateKind, cwd, target_session_id, event)
|
|
910
949
|
: live
|
|
911
950
|
result = delivered.status === "delivered"
|
|
912
951
|
? { ok: true, duplicate: true }
|
|
@@ -916,9 +955,7 @@ export class SiblingInjectBridge {
|
|
|
916
955
|
} else if (acceptance === "full") {
|
|
917
956
|
result = { ok: false, error: "delegation_store_full" }
|
|
918
957
|
} else {
|
|
919
|
-
const r = this.
|
|
920
|
-
? this.claudeBridge.emitToStream(cwd, target_session_id, event)
|
|
921
|
-
: { status: "not_found" }
|
|
958
|
+
const r = this._emitDelegation(delegateKind, cwd, target_session_id, event)
|
|
922
959
|
if (r.status === "delivered") {
|
|
923
960
|
result = { ok: true, delivered_to: r.session }
|
|
924
961
|
} else {
|
|
@@ -1109,6 +1146,10 @@ export class SiblingInjectBridge {
|
|
|
1109
1146
|
}
|
|
1110
1147
|
}
|
|
1111
1148
|
|
|
1149
|
+
if (ackV2 && result.ack_version !== 2) {
|
|
1150
|
+
result = { ok: false, ack_version: 2, persisted: false, delivered: false,
|
|
1151
|
+
error: result.error, retryable: false }
|
|
1152
|
+
}
|
|
1112
1153
|
this.logger?.[result.ok ? "info" : "warn"]?.(
|
|
1113
1154
|
{
|
|
1114
1155
|
request_id,
|
package/src/sibling-token.mjs
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
* 範囲がこの cwd 限定トークンより大きくなるため。
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
+
import { displayTokenForCwd } from "./delegate-display-auth.mjs"
|
|
28
|
+
|
|
27
29
|
import { randomBytes } from "node:crypto"
|
|
28
30
|
|
|
29
31
|
/** @type {Map<string, string>} cwd -> token */
|
|
@@ -126,6 +128,14 @@ export function buildSiblingEnv(cwd, { selfKind, delegateEventAck = false, strip
|
|
|
126
128
|
...(delegateEventAck ? { COCKPIT_DELEGATE_EVENT_ACK: "1" } : {}),
|
|
127
129
|
COCKPIT_SIBLING_SELF_KIND: selfKind,
|
|
128
130
|
}
|
|
131
|
+
delete env.COCKPIT_DELEGATE_DISPLAY_TOKEN
|
|
132
|
+
delete env.COCKPIT_DELEGATE_EVENT_ACK_VERSION
|
|
133
|
+
try {
|
|
134
|
+
env.COCKPIT_DELEGATE_DISPLAY_TOKEN = displayTokenForCwd(cwd)
|
|
135
|
+
env.COCKPIT_DELEGATE_EVENT_ACK_VERSION = "2"
|
|
136
|
+
} catch {
|
|
137
|
+
console.warn("delegate display auth unavailable; display capability omitted")
|
|
138
|
+
}
|
|
129
139
|
// ACK を付けない CLI では、継承元に ACK があっても子へ持ち込まない。
|
|
130
140
|
if (!delegateEventAck) delete env.COCKPIT_DELEGATE_EVENT_ACK
|
|
131
141
|
if (stripParentSessionIds) {
|