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.
- package/CHANGELOG.md +34 -0
- package/PRIVACY.md +46 -7
- package/README.md +32 -20
- package/SECURITY.md +37 -4
- package/docs/AGENT_CLIENT_CONTRACT.md +176 -11
- package/docs/COMPATIBILITY.md +49 -4
- package/docs/CROSS_SYSTEM_CONVERSATIONS.md +136 -0
- package/docs/GETTING_STARTED.md +37 -11
- package/docs/GETTING_STARTED.zh-CN.md +32 -12
- package/docs/MIGRATION_VNEXT.md +70 -7
- package/docs/PROJECT_BOUNDARIES.md +4 -1
- package/docs/README.zh-CN.md +25 -18
- package/docs/RELEASE_NOTES_v0.5.0.md +79 -0
- package/lib/authorizations.js +23 -8
- package/lib/conversation-archive-store.js +2 -2
- package/lib/conversation-outbox.js +13 -7
- package/lib/runtime.js +215 -44
- package/lib/session-scope-store.js +2 -2
- package/lib/session-scope.js +100 -26
- package/lib/subject-display.js +39 -0
- package/lib/system-info.js +76 -0
- package/lib/transport.js +15 -0
- package/package.json +4 -2
package/lib/authorizations.js
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
30
|
-
.
|
|
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
|
-
|
|
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
|
|
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
|
|
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' ||
|
|
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) ||
|
|
37
|
-
|
|
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:
|
|
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)
|