dsh-bailinghub 0.4.0 → 0.5.0

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.
@@ -1,3 +1,6 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { subjectDisplayLabel, targetSubjectDisplay } from './subject-display.js'
3
+
1
4
  function canonical(value) {
2
5
  if (Array.isArray(value)) return value.map(canonical)
3
6
  if (value && typeof value === 'object') {
@@ -11,7 +14,7 @@ export function declarationKey(tool) {
11
14
  return JSON.stringify(canonical(tool))
12
15
  }
13
16
 
14
- export function sharedDeclarations(targets, limit, preferredNames = []) {
17
+ export function sharedDeclarations(targets, limit, preferredNames = [], crossSystem = false, preferredRefs = []) {
15
18
  const grouped = new Map()
16
19
  const conflicts = new Set()
17
20
  for (const target of targets) {
@@ -19,15 +22,24 @@ export function sharedDeclarations(targets, limit, preferredNames = []) {
19
22
  if (run?.status !== 'active') continue
20
23
  for (const tool of run.activeTools) {
21
24
  const key = declarationKey(tool)
22
- const existing = grouped.get(tool.name)
23
- if (existing && existing.key !== key) conflicts.add(tool.name)
24
- if (!existing) grouped.set(tool.name, { tool, key, targets: [] })
25
- grouped.get(tool.name).targets.push(target)
25
+ // Identical schemas in different systems do not prove identical meaning.
26
+ // In a cross-system scope, also keep differing declarations within one
27
+ // system distinct instead of hiding an otherwise usable target's tool.
28
+ const identity = crossSystem ? JSON.stringify([target.authorization.systemRef, key]) : tool.name
29
+ const existing = grouped.get(identity)
30
+ if (existing && existing.key !== key) conflicts.add(identity)
31
+ const name = crossSystem
32
+ ? `bh_${createHash('sha256').update(identity).digest('hex').slice(0, 20)}_${tool.name.slice(0, 40)}` : tool.name
33
+ if (!existing) grouped.set(identity, { tool, key, name, identity, targets: [] })
34
+ grouped.get(identity).targets.push(target)
26
35
  }
27
36
  }
28
37
  const priority = new Map(preferredNames.map((name, index) => [name, index]))
29
- const tools = [...grouped.values()].filter(({ tool }) => !conflicts.has(tool.name))
30
- .sort((a, b) => (priority.get(a.tool.name) ?? Infinity) - (priority.get(b.tool.name) ?? Infinity))
38
+ const targetPriority = (entry) => crossSystem && preferredRefs.length
39
+ ? (entry.targets.some((target) => preferredRefs.includes(target.authorization.ref)) ? 0 : 1) : 0
40
+ const tools = [...grouped.values()].filter(({ identity }) => !conflicts.has(identity))
41
+ .sort((a, b) => targetPriority(a) - targetPriority(b) ||
42
+ (priority.get(a.tool.name) ?? Infinity) - (priority.get(b.tool.name) ?? Infinity))
31
43
  return { tools: tools.slice(0, limit), conflicts: [...conflicts].sort(), omitted: Math.max(0, tools.length - limit) }
32
44
  }
33
45
 
@@ -57,5 +69,8 @@ export function unwrapAuthorization(value) {
57
69
  }
58
70
 
59
71
  export function authorizedResult(target, result) {
60
- return { authorization_ref: target.authorization.ref, authorization_label: target.authorization.label, result }
72
+ const subject = targetSubjectDisplay(target)
73
+ return { authorization_ref: target.authorization.ref, authorization_label: subjectDisplayLabel(subject),
74
+ subject_display: subject.subjectDisplay, subject_display_status: subject.subjectDisplayStatus,
75
+ ...(target.authorization.systemRef ? { system_ref: target.authorization.systemRef, workspace: target.workspace } : {}), result }
61
76
  }
@@ -5,7 +5,7 @@ import { homedir } from 'node:os'
5
5
  import { join, resolve } from 'node:path'
6
6
  import { setTimeout as delay } from 'node:timers/promises'
7
7
 
8
- const SCHEMA = 'bailing.agent-conversation-outbox.v1'
8
+ const SCHEMAS = new Set(['bailing.agent-conversation-outbox.v1', 'bailing.agent-conversation-outbox.v2'])
9
9
  const MAX_BYTES = 32 * 1024 * 1024
10
10
  const LOCK_TIMEOUT_MS = 1_000
11
11
  const SECRET_KEYS = new Set(['token', 'accesstoken', 'refreshtoken', 'password', 'clientsecret', 'authorization', 'credential', 'credentials'])
@@ -55,7 +55,7 @@ function snapshotJson(value, seen = new Set()) {
55
55
 
56
56
  function snapshotRecord(sessionId, record) {
57
57
  const copy = snapshotJson(record)
58
- if (!copy || Array.isArray(copy) || copy.schema !== SCHEMA || copy.sessionId !== sessionId ||
58
+ if (!copy || Array.isArray(copy) || !SCHEMAS.has(copy.schema) || copy.sessionId !== sessionId ||
59
59
  !Number.isSafeInteger(copy.revision) || copy.revision < 1) throw storeError()
60
60
  if (Buffer.byteLength(JSON.stringify(copy)) > MAX_BYTES) throw storeError()
61
61
  return copy
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from 'node:crypto'
2
2
  import { createFileConversationArchiveStore } from './conversation-archive-store.js'
3
3
 
4
4
  const SCHEMA = 'bailing.agent-conversation-outbox.v1'
5
+ const TARGET_SCHEMA = 'bailing.agent-conversation-outbox.v2'
5
6
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
6
7
  const KEY = /^conn_[0-9a-f]{32}$/
7
8
  const KINDS = new Set(['turn_start', 'user_message', 'assistant_message', 'run_link', 'turn_end'])
@@ -24,25 +25,29 @@ function eventPayload(value) {
24
25
 
25
26
  function contextSnapshot(value) {
26
27
  const copy = structuredClone(value)
28
+ const crossSystem = copy?.binding && !Object.hasOwn(copy.binding, 'clientAppId')
27
29
  if (typeof copy?.clientConversationId !== 'string' || !copy.clientConversationId || copy.clientConversationId.length > 256 ||
28
- !copy.binding || typeof copy.binding.hubUrl !== 'string' || typeof copy.binding.clientAppId !== 'string' || typeof copy.binding.workspace !== 'string' ||
30
+ !copy.binding || typeof copy.binding.hubUrl !== 'string' ||
31
+ (!crossSystem && (typeof copy.binding.clientAppId !== 'string' || typeof copy.binding.workspace !== 'string')) ||
29
32
  !Array.isArray(copy.members) || !copy.members.length || copy.members.length > 64 ||
30
33
  Object.keys(copy).some((key) => !['clientConversationId', 'binding', 'members'].includes(key)) ||
31
- Object.keys(copy.binding).some((key) => !['hubUrl', 'clientAppId', 'workspace'].includes(key))) throw fail('ARCHIVE_MEMBERS_INVALID')
34
+ Object.keys(copy.binding).some((key) => !(crossSystem ? ['hubUrl'] : ['hubUrl', 'clientAppId', 'workspace']).includes(key))) throw fail('ARCHIVE_MEMBERS_INVALID')
32
35
  const keys = new Set()
33
36
  const identities = new Set()
34
37
  for (const member of copy.members) {
35
38
  if (!KEY.test(member?.connectionKey ?? '') || keys.has(member.connectionKey) || !UUID.test(member.expectedSessionId ?? '') ||
36
- identities.has(member.expectedSessionId) || member.workspace !== copy.binding.workspace || typeof member.label !== 'string' || member.label.length > 128 ||
37
- Object.keys(member).some((key) => !['connectionKey', 'workspace', 'expectedSessionId', 'label'].includes(key))) throw fail('ARCHIVE_MEMBERS_INVALID')
39
+ identities.has(member.expectedSessionId.toLowerCase()) ||
40
+ (crossSystem ? member.hubUrl !== copy.binding.hubUrl || !/^[a-z0-9][a-z0-9_-]{1,63}$/.test(member.clientAppId ?? '') || !/^[a-z0-9][a-z0-9_-]{1,63}$/.test(member.workspace ?? '')
41
+ : member.workspace !== copy.binding.workspace) || typeof member.label !== 'string' || member.label.length > 128 ||
42
+ Object.keys(member).some((key) => !['connectionKey', 'workspace', 'expectedSessionId', 'label', ...(crossSystem ? ['hubUrl', 'clientAppId'] : [])].includes(key))) throw fail('ARCHIVE_MEMBERS_INVALID')
38
43
  keys.add(member.connectionKey)
39
- identities.add(member.expectedSessionId)
44
+ identities.add(member.expectedSessionId.toLowerCase())
40
45
  }
41
46
  return copy
42
47
  }
43
48
 
44
49
  function validateRecord(record, sessionId, context) {
45
- if (!record || record.schema !== SCHEMA || record.sessionId !== sessionId || !Number.isSafeInteger(record.revision) || record.revision < 1 ||
50
+ if (!record || record.schema !== (Object.hasOwn(context.binding, 'clientAppId') ? SCHEMA : TARGET_SCHEMA) || record.sessionId !== sessionId || !Number.isSafeInteger(record.revision) || record.revision < 1 ||
46
51
  !UUID.test(record.clientArchiveId ?? '') || digest(contextSnapshot(record.context)) !== digest(context) ||
47
52
  !Number.isSafeInteger(record.acknowledged) || record.acknowledged < 0 || !Array.isArray(record.events) || record.acknowledged > record.events.length ||
48
53
  record.conversationId !== null && !UUID.test(record.conversationId ?? '') ||
@@ -88,7 +93,8 @@ export class ConversationOutbox {
88
93
  if (saved) entry.record = validateRecord(saved, entry.sessionId, entry.context)
89
94
  else {
90
95
  const record = {
91
- schema: SCHEMA, sessionId: entry.sessionId, revision: 1, clientArchiveId: randomUUID(), context: entry.context,
96
+ schema: Object.hasOwn(entry.context.binding, 'clientAppId') ? SCHEMA : TARGET_SCHEMA,
97
+ sessionId: entry.sessionId, revision: 1, clientArchiveId: randomUUID(), context: entry.context,
92
98
  acknowledged: 0, conversationId: null, events: [], syncState: 'pending',
93
99
  }
94
100
  await this.store.save(entry.sessionId, record, null)